Luma public docs
English

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ı

luma
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

luma
schema Invoice
  @desc("Satış faturası")
  @prompt("Fatura alanlarını eksiksiz çıkar; bulunmayanları null yap.") {
  invoice_number: string
  total: float
}
AnnotationNeredeEtkisi
@desc("…")Schema başlığı ve alanlarJSON 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.
@prompt bir field annotation'ı değildir

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

luma
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.

AnnotationUygulanabilir tipTekrarEtkisi
@desc("text")her tiptekJSON Schema description. Tip doğrudan bir schema referansıysa atlanır.
@title("text")her tiptekJSON Schema title.
@example(value)her tipçokluJSON değeri; examples[] dizisine eklenir.
@deprecated("reason")her tiptekdeprecated: true + x-luma-deprecated-reason.
@default(value)her tiptekAlan eksikse doldurur; required dışına çıkarır.
@alias("name")her tipçokluAlternatif giriş adı; doğrulama öncesi kanonik ada taşınır.
@feedback("message")her tiptekBu alanın tüm hata mesajlarını değiştirir.
@valid(x => expression)her tipçokluDeterministik özel kural.
@severity("error" | "warn")önündeki @validtekwarn: 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[]tekUTF-16 birimi cinsinden uzunluk — emoji gibi astral bir karakter 2 sayılır. Tek argüman = tam uzunluk.
@match("regex")string, string[]tekRegex deseni.
@fmt("format")string, string[]tekBilinen format doğrulaması.
@range(min, max)sayısal, sayısal arraytekDeğer aralığı (array'de her elemana).
@minItems(n)arraytekMinimum eleman sayısı.
@maxItems(n)arraytekMaksimum eleman sayısı.
@uniquearraytekTekrarsız elemanlar (argümansız).
@uniqueBy("field")arraytekNesne listesinde belirtilen alanın tekilliği.
@contains(value)arraytekBelirtilen elemanın listede bulunması.
Nullable alanlarda da çalışır

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ımKı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

✓ Doğru
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"))
✗ Yanlış
// @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ğerKontrol
emailbir@sey.com biçimi — boşluksuz yerel kısım, @, noktalı alan adı.
uriAyrıştırılabilir mutlak URL (protokol zorunlu).
uuidStandart UUID v1–v5 deseni.
dateYYYY-MM-DD ve gerçekten var olan bir tarih (2024-02-31 reddedilir).
date-timeISO 8601 (Z veya ±HH:MM offset zorunlu) ve ayrıştırılabilir olmalı.
ipv40–255 aralığında dört sekizli.
ipv6: içeren, yalnızca onaltılık karakter ve iki nokta — gevşek kontrol.
Bilinmeyen format sessizce geçer

@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"))
@valid null değerlerde de çalışır

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

luma
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_mail varsa değer email'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.
  • @default ile 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 alanUser.name missing.
Alias / default çözümlemesiProfile.email cannot be given by its canonical name and an alias at the same time.
Tip uyumuUser.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ğiUser.role must be one of these values: admin, user.
Array ve eleman tipiInvoice.items must be an array. · Invoice.items[2].amount missing.
Tuple uzunluğuT.coordinate must contain exactly 2 items.
Map değer tipiReport.metrics.score must be a number.
Union çözümlemesiCustomer.tax_id does not match any union type: … | …
Nested schemaİç schema'nın tüm kuralları özyinelemeli olarak çalışır.
Bilinmeyen alanUser.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"
Bilinmeyen alanlar da hatadır

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.

luma
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:

text
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.

luma
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 üzerindePath.field: <message> — o alanın tüm hataları (tip, uzunluk, aralık, @valid, eksiklik) bu mesajla değişir.
where üzerindePath: <message>
when / else üzerinde<message> — yol öneki eklenmez.
Error: A.f.v: total == subtotal + tax sağlanmıyor
Mesajı modele talimat gibi yaz

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.

Veri yolunda talimat verilecek kimse yok

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.

luma
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")
}
KuralDetay
Sonuç boolean olmalıAksi halde where rule must return a boolean result hatası.
Birden fazla whereSerbest — hepsi çalışır, hepsi ayrı hata üretebilir.
Kullanılabilir ifadelerAlan referansları, karşılaştırma, &&/||/!, built-in fonksiyonlar, pure fn'ler ve std modülleri.
IO yasakio.* çağrısı parse zamanında reddedilir: where is deterministic and cannot use IO.
JSON Schemax-luma-where uzantısı olarak taşınır; doğrulama yerel yapılır.

Kayan nokta karşılaştırması

✓ Toleranslı
import num from "std/numeric"

schema LineItem {
  quantity: float
  unit_price: float
  total: float

  where num.approximatelyEqual(quantity * unit_price, total, 0.01)
}
✗ Kırılgan
schema LineItem {
  quantity: float
  unit_price: float
  total: float

  // 0.1 * 3 == 0.30000000000000004
  where quantity * unit_price == total
}
where bölünebilir, ama yalnızca operatörden

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

luma
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.

DurumNasıl oluşurNe olur
GeçerliHer kural geçerDeğer kullanılır.
GeçersizVarsayılan severity'li bir kural düşerKoşu eskisi gibi hatayla durur.
Geçerli ama şerhliYalnızca @severity("warn") kuralları düşerDeğ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ımRed 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 annotationwhere 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.

YolTür@severity("warn") ne yapar
ask sonucusonuç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 expectsonuçOnurlandırılır.
Tool girdisigirdiYok sayılır — kural sert kalır.
Agent method parametresigirdiYok sayılır — kural sert kalır.
pure fn parametresigirdiYok 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.

Şerhli değer tipli sınırdan geçmez

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:

luma
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ıTipNe tutar
rulestringDüşen kural; valid:<ifade> ya da where:<ifade> biçiminde.
pathstringDeğerin neresinde düştüğü, ör. Maker.make.return.score.
messagestringKuralın düşerken vereceği metnin aynısı.
severitystringBugün "warn"; alan, şeklin ileride bir değer daha görürse ayakta kalması için var.
attemptintDeğerin kabul edildiği deneme — retry olmadıysa 1.
io, ve asıl mesele bu

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.

luma
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.

Dependency çağrısı bir adımdır, ifade değil

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ğil

Bugü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.

Soft kural bir kaydı geçersizler

@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.

require ve forbid DEĞERLE değil ANAHTARLA ilgilenir

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.

Alanrequire ne yaparforbid 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.

luma
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

luma
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")
}
  • when yeni bir grup başlatır; else when ve else önceki gruba katılır.
  • Bir grupta ilk eşleşen dal uygulanır; hiçbiri eşleşmezse else dalı çalışır.
  • Nokta ile nested alan yolu kullanılabilir: payload.calc.
  • Bir else, öncesinde when olmadan 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

luma
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:

json
"allOf": [
  { "if":   { "properties": { "kind": { "const": "company" } } },
    "then": { "allOf": [ { "required": ["tax_id"] } ] } }
]
Fonksiyon koşulları yalnızca yerel çalışır

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 kind

Fark 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

luma
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
AlanlarParçaların alanları sırayla birleştirilir.
ÇakışmaAynı 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.
@descschema Admin @desc("…") = User + AuditFields ile verilebilir.
CycleReddedilir: Schema composition cycle found: A -> B -> A
Reflectionschema.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:

✓ Tek tanım, her payload
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>
}
✗ Payload başına bir kopya
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.

Açılım sıradan bir schema'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 yoluBaşarısızlık ne demek
Bir ask sonucuAgent.method.return.fieldModel yanlış şekil döndürdü. Mesaj geri beslenir ve çağrı yeniden denenir.
Bir query satırıQueryName.row[N].fieldVeritabanı yanlış şekil döndürdü. Nihai — hiçbir retry tablodakini değiştiremez.
Bir parallel over elemanıvariable[N].fieldBir 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
Null değerlerde annotation davranışı
  • 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.
  • @valid her 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:

luma
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.

Dinamik schema seçimi yoktur

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 }
    }
  }
}