Dil
Schema & Validation
Schema, Luma'nın merkezindeki yapıdır. Aynı tanım gateway'e gönderilen JSON Schema'yı üretir, dönen yanıtı yerel olarak doğrular ve doğrulama başarısız olduğunda modele geri beslenecek hata metnini oluşturur. v0.4.0'dan beri modelden hiç gelmemiş veriyi de doğrular: bir query'nin döndürdüğü her veritabanı satırı ve bir parallel over'ın ürettiği her eleman aynı kurallardan geçer.
Schema tanımı
schema Invoice {
invoice_number: string
total: float
}Alanlar satır başına bir tane yazılır. Sondaki virgül isteğe bağlıdır; alanları aynı satıra koymak sözdizimi hatasıdır.
Schema seviyesi metadata
schema Invoice
@desc("Satış faturası")
@prompt("Fatura alanlarını eksiksiz çıkar; bulunmayanları null yap.") {
invoice_number: string
total: float
}| Annotation | Nerede | Etkisi |
|---|---|---|
@desc("…") | Schema başlığı ve alanlar | JSON Schema description. Schema başlığında verildiğinde kök nesnenin açıklaması olur ve modele gider. |
@prompt("…") | Yalnızca schema başlığı | Modele otomatik gönderilmez. schema.prompt(...) ile string olarak okunup prompt'a açıkça eklenir. |
Bir alanın üzerine @prompt("…") yazarsan program Unknown annotation: @prompt hatasıyla durur. Alan düzeyinde açıklama için @desc kullan.
@prompt nasıl kullanılır
import schema from "std/schema"
schema InvoiceExtract
@desc("Fatura Markdown'ından çıkarılan yapılandırılmış veri")
@prompt("Aşağıdaki Markdown'dan InvoiceExtract schema'sına uygun JSON üret. Alan açıklamalarını extraction talimatı olarak kullan. Bulunmayan değerleri uydurma; nullable alanlarda null, array alanlarda [] kullan.") {
invoice_number: string | null
items: InvoiceItem[]
}
agent Extractor {
fn extract(markdown: string) -> InvoiceExtract {
instruction = schema.prompt(InvoiceExtract)
result = ask "{instruction}\n\nFATURA MARKDOWN:\n{markdown}" -> InvoiceExtract
return result
}
}Bu ayrım bilinçlidir: extraction talimatı schema ile birlikte versiyonlanır ama prompt'un hangi noktasına gireceğine sen karar verirsin.
Field annotation referansı
Alan annotation'ları alan tanımının arkasına yazılır ve zincirlenebilir. Uzun zincirler @ ile başlayan devam satırlarına bölünebilir.
| Annotation | Uygulanabilir tip | Tekrar | Etkisi |
|---|---|---|---|
@desc("text") | her tip | tek | JSON Schema description. Tip doğrudan bir schema referansıysa atlanır. |
@title("text") | her tip | tek | JSON Schema title. |
@example(value) | her tip | çoklu | JSON değeri; examples[] dizisine eklenir. |
@deprecated("reason") | her tip | tek | deprecated: true + x-luma-deprecated-reason. |
@default(value) | her tip | tek | Alan eksikse doldurur; required dışına çıkarır. |
@alias("name") | her tip | çoklu | Alternatif giriş adı; doğrulama öncesi kanonik ada taşınır. |
@feedback("message") | her tip | tek | Bu alanın tüm hata mesajlarını değiştirir. |
@valid(x => expression) | her tip | çoklu | Deterministik özel kural. |
@severity("error" | "warn") | önündeki @valid | tek | warn: kuralın düşmesi koşuyu durdurmak yerine değeri şerhler. Sonuçlarda onurlandırılır, girdilerde yok sayılır — bkz. @severity. |
@length(min[, max]) | string, string[] | tek | UTF-16 birimi cinsinden uzunluk — emoji gibi astral bir karakter 2 sayılır. Tek argüman = tam uzunluk. |
@match("regex") | string, string[] | tek | Regex deseni. |
@fmt("format") | string, string[] | tek | Bilinen format doğrulaması. |
@range(min, max) | sayısal, sayısal array | tek | Değer aralığı (array'de her elemana). |
@minItems(n) | array | tek | Minimum eleman sayısı. |
@maxItems(n) | array | tek | Maksimum eleman sayısı. |
@unique | array | tek | Tekrarsız elemanlar (argümansız). |
@uniqueBy("field") | array | tek | Nesne listesinde belirtilen alanın tekilliği. |
@contains(value) | array | tek | Belirtilen elemanın listede bulunması. |
Tip kontrolü nullable union'ları çözer: tax_id: string | null @length(10) geçerlidir ve kısıt yalnızca string branch'ine uygulanır. Aynı şekilde scores: float[] | null @range(0, 100) array branch'ine uygulanır.
Sık karıştırılan üç ayrım
1. Değer aralığı ≠ liste uzunluğu
| Yazım | Kısıtlanan |
|---|---|
quantity: float @range(1, 3) | Tek sayının değeri 1–3 arasında. |
items: LineItem[] @minItems(1) @maxItems(3) | Listenin uzunluğu 1–3 arasında. |
values: float[] @range(1, 3) | Listedeki her elemanın değeri 1–3 arasında. Uzunluk sınırsız. |
2. @contains array içindir
tags: string[] @contains("vip")
numbers: int[] @contains(2)
// To search for a substring in a scalar string:
name: string @valid(x => contains(x, "Ayşe"))// @contains cannot be used on a scalar
// string — the parser rejects it:
name: string @contains("Ayşe")
// @contains usage: @contains(value)3. @length(n) tam uzunluktur
tax_id: string @length(10) // exactly 10 characters (minLength = maxLength = 10) name: string @length(2, 40) // between 2 and 40 characters
@fmt değerleri
| Değer | Kontrol |
|---|---|
email | bir@sey.com biçimi — boşluksuz yerel kısım, @, noktalı alan adı. |
uri | Ayrıştırılabilir mutlak URL (protokol zorunlu). |
uuid | Standart UUID v1–v5 deseni. |
date | YYYY-MM-DD ve gerçekten var olan bir tarih (2024-02-31 reddedilir). |
date-time | ISO 8601 (Z veya ±HH:MM offset zorunlu) ve ayrıştırılabilir olmalı. |
ipv4 | 0–255 aralığında dört sekizli. |
ipv6 | : içeren, yalnızca onaltılık karakter ve iki nokta — gevşek kontrol. |
@fmt("phone") gibi tanınmayan bir değer verirsen hiçbir doğrulama yapılmaz ve hata da alınmaz — format yalnızca JSON Schema'ya format olarak yazılır. Özel biçimler için @match("regex") kullan.
@valid
@valid deterministik bir lambda alır: @valid(x => expression). Desteklenen biçimler:
| Biçim | Örnek |
|---|---|
| Karşılaştırma | @valid(x => x > 0) · @valid(x => x == null) |
| Uzunluk | @valid(x => x.length > 3) |
| String method'ları | @valid(x => x.startsWith("TR")) · .endsWith(…) · .includes(…) |
| Built-in fonksiyon | @valid(x => contains(x, "Ada")) |
| Std kütüphane | @valid(x => math.abs(x) < 10) · @valid(x => regex.matches(x, "^[A-Z]+$")) |
| Pure fn | @valid(x => validCode(x)) |
| Birleşim | @valid(x => x.length > 3 && x.startsWith("Ada")) |
Diğer annotation'lar (@length, @range, @fmt…) değer null olduğunda atlanır. @valid ise her zaman çağrılır. Nullable alanda null kontrolünü ifadenin kendisine koymalısın:
// ✓ correct
code: string | null
@valid(x => x == null || regex.matches(x, "^[A-Z]{3}-[0-9]+$"))
// ✗ blows up when null arrives
code: string | null @valid(x => regex.matches(x, "^[A-Z]{3}-[0-9]+$"))&& ve || birleşimleri string ve parantez farkındalığıyla bölünür, yani regex deseninin içindeki | karakteri sorun çıkarmaz. Desteklenmeyen bir gövde program başlamadan reddedilir ve bu, şema hiç doğrulanmasa bile geçerlidir — hiç işletilmeyen bir kural da kontrol edilir, yani alanın ilk kez değer aldığı güne kadar pusuda bekleyemez. v0.7.0'a kadar parse zamanındaki kontrol yalnızca bir şekil yaklaşımıydı ve bu sınıf koşunun ortasında geliyordu; iki kontrol artık hizalı.
@alias ve @default
schema Profile {
email: string @title("E-posta") @example("ada@example.com") @alias("e_mail") @alias("mail")
active: bool @default(true)
old_code?: string @deprecated("Yerine new_code kullanılmalı")
}- Gelen veride
e_mailvarsa değeremail'e taşınır ve alias anahtarı silinir. - Hem kanonik ad hem alias birlikte gelirse hata:
Profile.email cannot be given by its canonical name and an alias at the same time. - Birden fazla alias birlikte gelirse hata:
Profile.email was given more than one alias: e_mail, mail - Bir alias, schema'daki gerçek bir alanın adı olamaz ve iki alan aynı alias'ı paylaşamaz.
@defaultile doldurulan değer sonrasında normal tip doğrulamasından geçer.
Otomatik doğrulamalar
Hiçbir annotation yazmasan bile aşağıdaki kontroller her schema için çalışır:
| Kontrol | Örnek hata mesajı |
|---|---|
| Zorunlu alan | User.name missing. |
| Alias / default çözümlemesi | Profile.email cannot be given by its canonical name and an alias at the same time. |
| Tip uyumu | User.name must be a string. |
| Integer kontrolü | User.age must be an integer. |
| Sonlu sayı kontrolü | User.score must be a number. |
| Boolean kontrolü | User.active must be a boolean. |
| Enum üyeliği | User.role must be one of these values: admin, user. |
| Array ve eleman tipi | Invoice.items must be an array. · Invoice.items[2].amount missing. |
| Tuple uzunluğu | T.coordinate must contain exactly 2 items. |
| Map değer tipi | Report.metrics.score must be a number. |
| Union çözümlemesi | Customer.tax_id does not match any union type: … | … |
| Nested schema | İç schema'nın tüm kuralları özyinelemeli olarak çalışır. |
| Bilinmeyen alan | User.extra is not defined in the schema. |
sources context kontrolü | Answer.sources contains an invalid context source: fabricated |
where invariant'ları | Order does not satisfy its where rule: total == subtotal + tax |
when koşullu kuralları | Operation conditional rule 'calc' requires field: kind == "calc" |
Model schema'da tanımlı olmayan bir alan döndürürse bu sessizce yok sayılmaz, doğrulama hatası olur ve retry döngüsünü tetikler. Üretilen JSON Schema'da da additionalProperties: false yer alır, yani hem provider tarafında hem yerel tarafta engellenir.
Özel alan: sources
sources: string[] adında bir alan tanımlarsan, içindeki her değerin programda tanımlı bir context kimliği olması zorunlu tutulur. Bu, modelin kaynak uydurmasını engellemenin doğrudan yoludur.
context contract = file("docs/contract.md")
context spec = file("docs/spec.md")
schema Answer {
summary: string
sources: string[1..3] // only "contract" or "spec" is allowed
confidence: float [0..1]
}input document: file ile verilen dosyalar da context kimliği sayılır.
Hatalar toplanarak raporlanır
Bir doğrulama turunda bulunan tüm hatalar biriktirilip tek seferde bildirilir — böylece model retry'da hepsini birden düzeltebilir:
Error: A.f.v.name must be at least 2 characters. A.f.v.age must be an integer. A.f.v.email must be in email format. A.f.v.tags must contain unique items. A.f.v.score must be at most 100. A.f.v.role must be one of these values: admin, user. A.f.v.extra is not defined in the schema.
@feedback ile mesajları özelleştirme
Varsayılan hata mesajları modele geri gönderilir. Modelin ne yapması gerektiğini daha net anlatmak istediğinde @feedback kullan.
schema Order {
subtotal: float
tax: float
total: float @feedback("Toplam, ara toplam ile KDV'nin tam toplamı olmalı")
where total == subtotal + tax @feedback("total == subtotal + tax sağlanmıyor")
}| Nerede | Üretilen mesaj biçimi |
|---|---|
| Alan üzerinde | Path.field: <message> — o alanın tüm hataları (tip, uzunluk, aralık, @valid, eksiklik) bu mesajla değişir. |
where üzerinde | Path: <message> |
when / else üzerinde | <message> — yol öneki eklenmez. |
Error: A.f.v: total == subtotal + tax sağlanmıyor
Bu metin doğrudan retry prompt'una girer. "Geçersiz değer" yerine "Tutarı KDV dahil, sayısal ve nokta ayraçlı yaz" gibi eyleme dönük bir cümle kur.
Aynı schema bir query satırını ya da bir parallel over elemanını doğruladığında @feedback metni hiçbir yere geri beslenmez — o döngüde model yoktur. Nihai hata mesajı hâline gelir ve koşu durur:
Error: BadRows.row[0]: total must equal subtotal + tax
Hiçbir retry bunu düzeltemez: köprü, veritabanında ne varsa onu döndürdü. Bu yollarda @feedback'i modele yönelik bir talimat olarak değil, hatayı okuyacak kişiye yönelik bir teşhis olarak yaz — kolonu ve beklenen ilişkiyi adıyla söyle. Veri & Entegrasyon'a bak.
where — deterministik invariant'lar
where, alanlar arasındaki ilişkiyi doğrular. Tek bir alanın kendi kısıtı için annotation, alanlar arası tutarlılık için where kullanılır.
pure fn totalAmount(items: LineItem[]) -> float {
total = reduce item in items with 0 => acc + item.amount
return total
}
schema Invoice {
items: LineItem[1..100]
total_amount: float
where totalAmount(items) == total_amount @feedback("Kalem toplamı fatura toplamıyla eşleşmeli")
}| Kural | Detay |
|---|---|
| Sonuç boolean olmalı | Aksi halde where rule must return a boolean result hatası. |
Birden fazla where | Serbest — hepsi çalışır, hepsi ayrı hata üretebilir. |
| Kullanılabilir ifadeler | Alan referansları, karşılaştırma, &&/||/!, built-in fonksiyonlar, pure fn'ler ve std modülleri. |
| IO yasak | io.* çağrısı parse zamanında reddedilir: where is deterministic and cannot use IO. |
| JSON Schema | x-luma-where uzantısı olarak taşınır; doğrulama yerel yapılır. |
Kayan nokta karşılaştırması
import num from "std/numeric"
schema LineItem {
quantity: float
unit_price: float
total: float
where num.approximatelyEqual(quantity * unit_price, total, 0.01)
}schema LineItem {
quantity: float
unit_price: float
total: float
// 0.1 * 3 == 0.30000000000000004
where quantity * unit_price == total
}Schema gövdesi satır satır ayrıştırılır ve bir satır, üstündekine yalnızca ikili operatör, virgül veya nokta ile başlıyorsa eklenir. Yani uzun bir invariant okunabilirlik için alt satıra kaydırılabilir:
where quantity == null || unit_price == null
|| num.approximatelyEqual(quantity * unit_price, total, 0.01)
@feedback("a * b toplamına uymuyor")@ ile başlayan satırlar da önceki satırı sürdürür; @feedback bu sayede kendi satırına alınabilir. Sürdürmeyen şey keyword ile başlayan satırdır — require öncesinde bölersen hâlâ Error: Invalid schema field: when kind == "company" alırsın. İfade gerçekten uzuyorsa mantığı bir pure fn'e taşı ve alanları argüman olarak geçir:
pure fn lineItemConsistent(quantity: float | null, unit_price: float | null, total: float | null) -> bool {
if quantity == null {
return true
}
if unit_price == null {
return true
}
if total == null {
return true
}
return num.approximatelyEqual(quantity * unit_price, total, 0.01)
}
schema LineItem {
quantity: float | null
unit_price: float | null
total: float | null
where lineItemConsistent(quantity, unit_price, total)
}where içinde nesnenin kendisini temsil eden bir self yoktur; alanlara doğrudan adlarıyla erişilir ve fonksiyona tek tek geçirilir.
Nullable alanlarla where
schema InvoiceItem {
quantity: float | null
unit_price: float | null
total: float | null
where quantity == null || unit_price == null || total == null || num.approximatelyEqual(quantity * unit_price, total, 0.01)
@feedback("quantity * unit_price toplamına uymuyor")
}Nullable alanlar için önce "veri yoksa geç" koşullarını yazıp sonra asıl invariant'ı ekle. || soldan sağa kısa devre yapar. @feedback alt satıra alınabilir; where'in kendisi alınamaz.
@severity — üçüncü durum
v0.8.0'a kadar doğrulanmış bir değer iki durumdan birindeydi: ya schema'yı karşılıyordu ya da koşu duruyordu. @severity("warn") üçüncüsünü ekliyor — geçerli ama şerhli. Değer kabul ediliyor, koşu devam ediyor; kuralın bulduğu şey değerle birlikte bir finding olarak yol alıyor.
| Durum | Nasıl oluşur | Ne olur |
|---|---|---|
| Geçerli | Her kural geçer | Değer kullanılır. |
| Geçersiz | Varsayılan severity'li bir kural düşer | Koşu eskisi gibi hatayla durur. |
| Geçerli ama şerhli | Yalnızca @severity("warn") kuralları düşer | Değer kullanılır ve onu üreten operasyona bir finding kaydedilir. |
Varsayılan error, yani @severity geçmeyen bir schema tam olarak eskisi gibi davranır. Küme kapalı: error ve warn, başka değer yok. Kendi tier sözlüğün — review, reject, sayısal bir skor — kuralda değil, finding'leri okuyan kodda yaşar.
Nereye yazılır
Alanda @severity, ardından geldiği @valid kuralını niteler; where satırında o invariant'ı niteler. Yalnızca bu iki yer geçerli ve ikisi de parse zamanında denetlenir.
schema Scored {
// kural koşuyu durdurmadan düşebilir
score: int @valid(x => x >= 0) @severity("warn")
// iki kural, yalnızca ikincisi soft
ratio: float @valid(x => x <= 1) @valid(x => x > 0) @severity("warn")
}
schema Line {
qty: int
total: int
where total == qty * 5 @severity("warn")
}| Yazım | Red mesajı |
|---|---|
@severity("review") | @severity value must be "error" or "warn": review |
Öncesinde @valid olmayan @severity | @severity must follow the @valid rule it modifies. |
where satırında başka bir annotation | where only supports the @feedback and @severity annotations: @desc |
Sonuçlarda onurlandırılır, girdilerde yok sayılır
Aynı schema bir yerde modelin cevabını, başka bir yerde bir tool'un argümanını tarif edebilir. Severity yalnızca schema'nın bir özelliği değildir — değerin bir sınırın hangi tarafında denetlendiğine bağlıdır.
| Yol | Tür | @severity("warn") ne yapar |
|---|---|---|
ask sonucu | sonuç | Onurlandırılır — değer kabul edilir ve şerhlenir. |
| Agent method dönüşü | sonuç | Onurlandırılır. |
parallel dalı · parallel over elemanı | sonuç | Eleman başına onurlandırılır. |
query satırı | sonuç | Satır başına onurlandırılır. |
| Tool çıktısı | sonuç | Onurlandırılır. |
test bloğundaki expect | sonuç | Onurlandırılır. |
| Tool girdisi | girdi | Yok sayılır — kural sert kalır. |
| Agent method parametresi | girdi | Yok sayılır — kural sert kalır. |
pure fn parametresi | girdi | Yok sayılır — kural sert kalır. |
Sebep, bu iki yolun kurduğu cümlenin farkı. Şerhli bir sonuç "bu veri şüpheli, sebebi de bu" der — ne yapılacağına program karar verir. Şerhli bir girdi ise "senin yan etkili kodunu, sözleşmesini ihlal ettiğini bildiğimiz veriyle çağırıyoruz" derdi; bu, Luma'nın kurallarını kapatmaya harcadığı sessiz bozulma sınıfının ta kendisi.
Dişi olan kural bu ve tek koşuda görmeye değer. Method dönüşü şerhlenip kabul ediliyor; aynı değeri aynı schema ile tiplenmiş bir parametreye geçirmek ise düşüyor — sınır sözleşmeyi yeniden dayatıyor:
Error: Sink.take.s.score does not satisfy its @valid rule: x => x >= 0
Yani warn, bir üreticinin ne döndürebileceğini genişletir; bir tüketicinin neyi kabul etmek zorunda olduğunu genişletmez. Şerhli bir değerin daha ileri gitmesini istiyorsan finding'leri oku ve karar ver, ya da tüketiciye değeri dürüstçe kabul eden bir schema ver.
ask yolunda soft kural da retry hakkı alır
Soft ihlal bir omuz silkme değildir. ask yolunda tıpkı sert bir ihlal gibi retry döngüsüne girer: düşen kurallar yollarıyla birlikte modele geri söylenir ve model düzeltme şansını alır. İlk sekiz mesaj alıntılanır, kalanı sayılır ((+3 more)).
Fark yalnızca sondadır. Denemeler tükendiğinde sert ihlal koşuyu durdurur, soft ihlal durdurmaz: son değer kabul edilir ve finding, kabul edildiği deneme numarasını kaydeder. Retry yalnızca beslenecek bir modelin olduğu yerde vardır — diğer sonuç yollarında soft ihlal doğrudan "kabul edildi ve şerhlendi"ye düşer. Bu, @feedback'in zaten taşıdığı asimetrinin aynısıdır.
Finding'leri okumak
Şerhin programa ulaşma yolu std/findings. İki fonksiyon da şerhli değeri alır ve pure değil, io'dur:
import findings from "std/findings"
schema Scored {
score: int @valid(x => x >= 0) @severity("warn")
}
schema Audit {
score: int
flaws: int
first_rule: string
}
agent Maker {
fn make(value: int) -> Scored {
return { score: value }
}
}
agent Auditor {
uses maker: Maker
fn audit() -> Audit {
shaded = maker.make(-7)
flaws = findings.of(shaded)
return { score: shaded.score, flaws: length(flaws), first_rule: flaws[0].rule }
}
}Finding alanı | Tip | Ne tutar |
|---|---|---|
rule | string | Düşen kural; valid:<ifade> ya da where:<ifade> biçiminde. |
path | string | Değerin neresinde düştüğü, ör. Maker.make.return.score. |
message | string | Kuralın düşerken vereceği metnin aynısı. |
severity | string | Bugün "warn"; alan, şeklin ileride bir değer daha görürse ayakta kalması için var. |
attempt | int | Değerin kabul edildiği deneme — retry olmadıysa 1. |
findings.of ve findings.count koşu durumunu okur, dolayısıyla io'durlar ve @valid, where ya da pure fn içinde görünemezler:
Error: findings is io and cannot be used inside @valid or where rules: findings.count
Bu red maliyetinden fazlasını kazandırır: bir kuralın kendi ihlallerini okumasını engeller — ki bu, sonucu kuralların koşma sırasına bağlı bir kurala giden en kısa yoldur.
Finding'ler operasyon başına yaşar. parallel over altında her eleman kendisininkini toplar ve sonuçlar girdi sırasında birleşir; böylece bir şerh asla yanlış satıra düşmez.
Şerhli değeri onu inceleyen bir şeye vermek
Şerhli bir değer, soft kuralını ihlal ettiği şema ile tiplenmiş bir parametreden geçemez. Bu bilinçli, ama gerçek bir soru bırakıyor: işi şerhli değerlere bakmak olan yardımcıyı nasıl yazacaksın? Parametreyi any tiplemek çalışır ve şerh geçişte korunur — ama diğer bütün alanların tip kontrolünü de bırakır.
Tipli yol, sınırın gerçekte neyi kabul ettiğini söylemek: aynı şekle sahip, sert kuralları koruyan ve yalnızca incelenen soft kuralı dışarıda bırakan bir aday şema.
schema Document {
amount: int @valid(x => x >= 0) @severity("warn")
name: string @length(2, 40)
}
// Aynı şekil. @length duruyor; incelenen kural dışarıda.
schema DocumentCandidate {
amount: int
name: string @length(2, 40)
}
agent Reviewer {
fn review(doc: DocumentCandidate) -> Report {
flaws = findings.of(doc)
return { amount: doc.amount, flaws: length(flaws), rule: flaws[0].rule }
}
}Değer geçer, findings.of şerhlerini vermeye devam eder, ve doc.amount ile doc.name tipli kalır — @length o sınırda hâlâ dayatılır. Vazgeçtiğin şey tam olarak tek bir kuraldır, yazılı olarak, önemli olduğu yerde.
Bunu agent'lar arasında kurarken bilinmeye değer ayrı bir kısıt var: uses ile bildirilmiş bir dependency üzerinden çağrı atama konumunda çözülür, return ifadesinin içinde değil.
r = reviewer.review(doc) // böyle
return { amount: r.amount }
return reviewer.review(doc) // böyle değilBugün ikinci biçim, sebebini yanlış adlandıran bir mesajla reddediliyor — #128 olarak izleniyor.
Kimsenin okumadığı şerh de duyurulur
Okumak isteğe bağlı olduğu için, okunmamış bir finding sessizce kabul edilmiş bir değer demektir — @severity'nin yapmasına yalnızca görünür olduğu için izin verilen şeyin tam tersi. Bu yüzden koşu susmaz. Finding üretilip hiç okunmadıysa koşu, üreten her operasyonu ve sayısını adlandıran bir uyarıyla standart hataya biter:
Warning: 1 finding accepted but never read: Maker.make.return (1). Read them with findings.of(...) from std/findings.
Bu bir uyarıdır, hata değil: çıkış kodu 0 kalır, çünkü koşuyu durdurmak özelliğin amacını bozardı. Finding'leri oku, koşu sessizleşir. Bu, çıplak çağrı satırının yaptığı pazarlığın aynısıdır — dil izin verir ve bu konuda susmayı reddeder.
Tavan
İki yüz bin satır dönen bir query, kural sayısıyla çarpıldığında finding'leri sınırsız biriktirebilirdi. Bu yüzden operasyon başına 1000'lik bir tavan var. Sonrasında ihlaller saklanmak yerine sayılır ve findings.capped adlı bir işaret kaydı kaç tanesinin kesildiğini tam olarak söyler:
Findings cap reached (1000); 412 more soft violations were not recorded.
Ne sınırsız büyür ne de izsiz küçülür — sayı her zaman kayıttadır.
@severity, --record ve --replay'in eşleştirdiği schema hash'inin parçasıdır, çünkü kuralın ne yaptığını değiştirir. Eklemek ya da kaldırmak o programın kaydını geçersizler — ki doğrusu budur: kayıt, tam da davranışın değişeceği anda geçerliliğini yitirir. @severity kullanmayan programların kayıtları etkilenmez.
when — koşullu kurallar
when, bir discriminator alanına göre başka alanları zorunlu veya yasak yapar. Bu bir workflow if'i değildir — schema doğrulaması ve JSON Schema üretimi içindir.
Bir when kuralının hiçbir şey yapıp yapmadığına karar veren ayrım budur. require anahtarın var olup olmadığını sorar; forbid yok olup olmadığını. İkisi de değere bakmaz, dolayısıyla null "var" sayılır.
| Alan | require ne yapar | forbid ne yapar |
|---|---|---|
x?: T (opsiyonel) | Anahtar yoksa reddeder — amaçlanan kullanım. | Anahtar varsa reddeder. |
x: T (zorunlu) | Hiçbir şey — anahtar zaten hep var. | Daima düşer; anahtar hiç yok olamaz. |
x: T | null (zorunlu, nullable) | Hiçbir şey. null var olan bir anahtardır, kural no-op'tur. | Daima düşer, aynı sebeple. |
Tablonun orta satırı tuzağın kendisi. Zorunlu nullable bir alan hiç yok olamaz — alan düzeyindeki missing kontrolü conditional'dan önce koşar — yani amount: float | null üzerine when kind == "calc" require [amount] yazmak "calc'ta amount doldurulmalı" gibi okunur ve hiçbir şey dayatmaz.
Değeri kısıtlamak için açık null kontrolüyle where kullan:
where kind != "calc" || amount != null
@feedback("calc işleminde amount zorunludur")Sözdizimsel olarak forbid, require'ın ardından gelir ve boş liste meşrudur: yalnız-forbid kuralı require [] forbid [x] diye yazılır.
schema Operation {
kind: enum["calc", "text"]
calc?: CalcInput
text?: string
when kind == "calc" require [calc] forbid [text]
@feedback("Calc işleminde calc alanı zorunludur")
when kind == "text" require [text] forbid [calc]
@feedback("Text işleminde text alanı zorunludur")
}else when / else zincirleri
schema Envelope {
payload: Payload
when payload.kind == "calc" require [payload.calc] forbid [payload.text]
@feedback("Calc payload'ında calc alanı zorunludur")
else when payload.kind == "text" require [payload.text] forbid [payload.calc]
else require [payload.text]
@feedback("Diğer payload türlerinde text alanı zorunludur")
}whenyeni bir grup başlatır;else whenveelseönceki gruba katılır.- Bir grupta ilk eşleşen dal uygulanır; hiçbiri eşleşmezse
elsedalı çalışır. - Nokta ile nested alan yolu kullanılabilir:
payload.calc. - Bir
else, öncesindewhenolmadan yazılamaz. - Kurallarda geçen tüm alan adları schema'da mevcut olmalıdır, yoksa parse hatası alınır.
Koşullu default'lar
schema Operation {
kind: enum["calc", "text", "other"]
currency?: string
description?: string
when kind == "calc" default [currency = "TRY"]
else when kind == "text" default [currency = "TEXT", description = "Metin işlemi"]
else default [currency = "GENEL"]
}Koşullu default yalnızca alan eksikse uygulanır; mevcut değer ezilmez. Doldurulan değer sonrasında normal tip doğrulamasından geçer. Default'lar, zorunluluk kontrollerinden önce çalışır.
JSON Schema'ya yansıması
Koşul field == constant biçimindeyse JSON Schema if/then/else yapısı üretilir:
"allOf": [
{ "if": { "properties": { "kind": { "const": "company" } } },
"then": { "allOf": [ { "required": ["tax_id"] } ] } }
]Koşul bir fonksiyon çağrısıysa — örneğin when num.approximatelyEqual(value, 3, 0.1) require [rounded] — JSON Schema'ya if/then olarak çevrilemez. Kural yine de yerel doğrulamada tam olarak çalışır, ama model bu kısıtı istek anında görmez. Modelin görmesini istediğin kuralları field == constant biçiminde tut.
when mi, tagged union mı?
Yukarıdaki her şey tek bir schema'nın içindeki koşullu zorunluluğu anlatır: başka bir alan belirli bir değeri taşıdığı için zorunlu, yasak ya da önceden dolu hale gelen alan. when bunun içindir ve bu iş için doğru araç olmayı sürdürür.
Aynı kurallar ikinci ve epeyce farklı bir işi de taşıyordu: "bu nesne şu şekillerden tam olarak biri" demeyi — bir kind enum'ı, şekil başına bir dal alanı, dal başına bir require/forbid çifti. Artık bu işin kendi deklarasyonu var:
schema Invoice {
invoice_number: string
}
schema Receipt {
reference: string
}
schema Document = Invoice | Receipt tagged by kindFark biçimsel değil. Zarfta dışlayıcılık senin yazdığın bir invariant'tır: bir dalın when kuralını unut, iki dalı birden taşıyan nesne sorunsuzca doğrulanır — eksik kuralı gösteren hiçbir şey yoktur. Tagged union bu biçimde yanlış olamaz, çünkü dışlayıcılık tipin kendisidir: değer tek bir üyeyi ve etiketini taşır, onu tüketen match ise her üyeyi adlandırmak zorundadır. Birini atlarsan program hiç çalışmaz — parse sırasında Match for tagged union 'Document' must handle member 'Receipt'. ile reddedilir.
Buradan hiçbir şey eksilmiyor. Zarf eskisi gibi doğrulanır ve dallar kendi başına birer schema değilse ya da dalın yanında başka alanlar duracaksa hâlâ doğru cevaptır; union üyesi de kendi where, when ve annotation'larını korur, bir Document Invoice olarak geldiğinde bunlar tam güçte çalışır. Deklarasyonun kendisi, match ve etiketin ne tuttuğu için → Tip Sistemi · Etiketli union.
Schema composition
schema User {
id: string
email: string @fmt("email")
}
schema AuditFields {
created_at: string @fmt("date-time")
created_by: string
}
schema Admin = User + AuditFields| Davranış | Detay |
|---|---|
| Alanlar | Parçaların alanları sırayla birleştirilir. |
| Çakışma | Aynı adlı alan iki parçada varsa hata: Admin composition field conflict: id |
where kuralları | Tüm parçalardan taşınır. |
when grupları | Taşınır ve grup numaraları çakışmayacak şekilde yeniden numaralandırılır. |
@desc | schema Admin @desc("…") = User + AuditFields ile verilebilir. |
| Cycle | Reddedilir: Schema composition cycle found: A -> B -> A |
| Reflection | schema.components(Admin) → ["User", "AuditFields"]; composition olmayan schema için []. |
Her payload'a tek sarmalayıcı
Composition, ikisi de zaten var olan iki schema'yı birleştirir. Yeniden kullanım probleminin öbür yarısı, payload'ı değişirken biçimi tekrar eden schema'dır — Page, Result, Envelope. Elle yazıldığında bu, payload tipi başına bir kopya demektir:
schema LineItem {
sku: string
}
schema Customer {
name: string
}
schema Page<T> {
items: T[]
total: int
next_cursor: string | null
}
schema Report {
lines: Page<LineItem>
customers: Page<Customer>
}schema LineItemPage {
items: LineItem[]
total: int
next_cursor: string | null
}
schema CustomerPage {
items: Customer[]
total: int
// next_cursor was added on the left only
}Bugün iki taraf da aynı değerleri doğruluyor. Fark, next_cursor'ın eklendiği gün ortaya çıkar: solda tek hamlede bütün payload'lara iner, sağda düzenlemeyi yapan kişinin aklına geldiği yerlere iner. Atlanan kopyayı kimse bildirmez, çünkü ortada bozuk bir şey yoktur — CustomerPage gayet geçerli bir schema'dır, yalnızca olması gereken sarmalayıcıdan başka bir schema'dır. Kayma dediğimiz şey budur ve onu baştan imkânsız kılan tek şey tanımın tek olmasıdır.
Page<LineItem> parse sırasında Page_LineItem adlı somut bir schema'ya açılır; bu yüzden bu sayfadaki hiçbir konu onu özel durum saymaz: where değişmezleri, when grupları, annotation'lar, @desc, doğrulama sırası ve std/schema reflection'ı, generic tanımda nasıl yazıldıysa ona da aynen uygulanır. schema.name(Page_LineItem) açılmış adı verir, schema.rules(Page_LineItem) tanımın taşıdığı kuralları listeler. Parametrik tanım, schema'lar için bir şablondur; yeni bir schema türü değildir. Tanımın yazılışı, hangi pozisyonlarda kullanılabildiği ve parser'ın neyi reddettiği için → Tip Sistemi · Parametrik schema.
Doğrulama sırası
Bir nesne doğrulanırken adımlar tam olarak şu sırayla çalışır. Sıra önemlidir: örneğin default'lar zorunluluk kontrolünden önce dolar, where ise en sona kalır çünkü tüm alanların geçerli olduğunu varsayar.
Bu sıraya üç giriş noktası vardır ve hepsi aynı adımları çalıştırır; yani aşağıdaki her kural — annotation'lar, when, where, composition — modele hiç değmemiş veri için de aynen geçerlidir. Yalnızca hata yolu öneki değişir:
| Giriş noktası | Hata yolu | Başarısızlık ne demek |
|---|---|---|
Bir ask sonucu | Agent.method.return.field | Model yanlış şekil döndürdü. Mesaj geri beslenir ve çağrı yeniden denenir. |
Bir query satırı | QueryName.row[N].field | Veritabanı yanlış şekil döndürdü. Nihai — hiçbir retry tablodakini değiştiremez. |
Bir parallel over elemanı | variable[N].field | Bir elemanın gövdesi yanlış şekil üretti. O eleman için nihai; on_error continue onu atar. |
1 alias çözümleme alias → kanonik ad taşıma
2 @default doldurma eksik alanlara sabit değer
3 koşullu default'lar when/else default [ … ]
│
4 her alan için
├─ alias çakışma kontrolü
├─ zorunluluk kontrolü "X.y missing."
├─ tip doğrulaması (nested schema'lar özyinelemeli)
├─ sources context kontrolü (alan adı "sources" ise)
├─ koleksiyon kuralları minItems · maxItems · unique · uniqueBy · contains
├─ değer kuralları fmt · length · match · range (her eleman için)
└─ @valid ifadeleri null değerlerde de çalışır
│
5 bilinmeyen alan kontrolü "X.k is not defined in the schema."
6 when require / forbid grup içinde ilk eşleşen dal
7 where invariant'ları
↓
tüm hatalar birleştirilip tek seferde raporlanır- String kuralları (
@length,@match,@fmt) yalnızca değer gerçekten string ise çalışır. - Sayısal kurallar (
@range, aralık kısayolu) yalnızca değer sayı ise çalışır. - Koleksiyon kuralları yalnızca değer array ise çalışır.
@validher zaman çalışır — null guard'ını sen yazmalısın.
Schema reflection
Schema'lar compile-time tanımlardır; runtime'da değiştirilemezler. Ancak std/schema ile deterministik olarak okunabilirler — prompt üretmek, katalog çıkarmak veya JSON Schema'yı dışa aktarmak için:
import schema from "std/schema" name = schema.name(Invoice) // "Invoice" description = schema.description(Invoice) // the @desc text or null instruction = schema.prompt(Invoice) // the @prompt text or null fields = schema.fields(Invoice) // field descriptors field = schema.field(Invoice, "invoice_number") var = schema.hasField(Invoice, "seller.name") rules = schema.rules(Invoice) // where + conditional rules parts = schema.components(Admin) // ["User", "AuditFields"] catalog = schema.catalog([Invoice, Receipt]) js = schema.toJsonSchema(Invoice)
Tam imza listesi ve dönüş şekilleri için → Standard Library · std/schema.
Bir string'den schema seçip ask -> <that schema> yapmak mevcut bir yetenek değildir; hedef schema kaynakta sabittir ve reflection bir schema'yı okur, kurmaz.
Bir method'un döndürdüğü şey hâlâ tek bir adlandırılmış tiptir — ama o tip bir tagged union olabilir; o zaman her dal kendi üye schema'sıyla ask yapmakta serbesttir. Dallanma statik kalır, modele dal başına tek bir dar schema gösterilir ve sonuç etiketini taşıyarak döner:
schema Invoice {
invoice_number: string
}
schema Receipt {
reference: string
}
schema Document = Invoice | Receipt tagged by kind
agent Extractor {
fn extract(kind: string, text: string) -> Document {
switch kind {
case "Invoice" { ask "Fatura alanlarını çıkar:\n{text}" -> Invoice }
default { ask "Dekont alanlarını çıkar:\n{text}" -> Receipt }
}
}
}