The one place validators could disagree

Ordering text is cultural, and culture doesn’t fit in a grammar. On kaiv’s Level 3: locale collation quarantined behind an opt-in span, pinned to one CLDR version — and a conformance rule where refusing is always legal and silently disagreeing never is.


Same value, same constraint, two locales:

$ cat ol.kaiv
.!kaiv

beer=öl
$ kaiv validate ol.kaiv sv.saiv     # ..lex[sv] [a,z]
kaiv: ConstraintViolationError: ::beer=öl (type !str) violates ..lex[sv] [a,z] (line 2)
$ kaiv validate ol.kaiv de.saiv     # ..lex[de] [a,z]
pass

In Swedish, ö is a letter of its own that sorts after z — so öl falls outside [a,z]. In German, ö collates with o — comfortably inside. Same bytes, same range, opposite verdicts, and neither is wrong. Welcome to collation: the one corner of data validation where the correct answer depends on a human culture rather than on arithmetic.

Most formats handle this by not handling it. Byte order is deterministic, so sort bytes and shrug — which files Étude after Zebra and calls it alphabetical. Or reach for a collation library wherever sorting happens — unpinned, so the verdict quietly changes with the library version, the platform, the locale data of the machine that happened to run the check. Two services disagree about whether a value is in range, and nobody can even say which one is broken.

kaiv’s answer has three parts: quarantine it, pin it, and make refusal legal.

Quarantine: locale order is opt-in, per span

Plain ..lex is byte order — deterministic, culture-free, and what the format uses unless you say otherwise. Locale order is a span you ask for, field by field: ..lex[sv], ..lex[fr], attached to the named types that genuinely hold human text. Collation is a property of a catalog-title type, not of str — the culturally-dependent corner is fenced exactly as large as it needs to be and no larger.

And the fence has a name: Level 3. Levels 0–2 of kaiv — scalars, trees, tables — are byte-deterministic everywhere. A validator that implements only those levels doesn’t guess at locale spans: it rejects them, loudly (CollationUnsupportedError), and remains fully conforming. The conformance rule of Level 3 fits in one sentence: rejecting is always conforming; silently disagreeing is not.

Pin: one CLDR, one strength, one answer per tag

Two Level 3 validators agree only if they resolve a locale tag against the same collation data and the same options — so the spec pins both. Reference data is CLDR 48 (UTS #10); comparisons run at tertiary strength with non-ignorable variable handling — the CLDR root defaults — unless the tag itself carries a BCP 47 -u- extension. A given tag yields exactly one ordering under the pin; advancing the CLDR version is a change to the spec, not something an implementation does on its own. Your verdicts don’t drift because a base image updated a library.

The pin matters more than it first appears, because collation governs equality as well as order — enum membership and range endpoints, not just sorting.

Equality is deeper than bytes

Here is a validator error that looks like a bug report about itself:

$ kaiv validate nfd.kaiv byenum.saiv    # {étude}, byte order
kaiv: ConstraintViolationError: ::word=étude (type !str) violates {étude} (line 2)

étude violates {étude}? The document’s value is é written as e plus combining accent (NFD); the enum’s is the precomposed character (NFC). Byte-identity says different; your eyes say same. Under the locale span, the letters — not the bytes — are compared:

$ kaiv validate nfd.kaiv frenum.saiv    # ..lex[fr] {étude}
pass

The -u- extensions tune the comparison further, per the pinned semantics. At primary strength, accents stop counting:

$ kaiv validate res.kaiv l1.saiv   # ..lex[en-u-ks-level1] {résumé}
pass

resume is a member of {résumé} — by declaration, in the schema, reproducibly. Even the famous German phonebook collation is just a tag: under de-u-co-phonebk-ks-level1, where ö expands to oe,

$ kaiv validate ol.kaiv pb.saiv    # {oel}
pass

öl is oel. Every one of these is the same machinery — strings, an ordering, membership — with the ordering chosen precisely and pinned.

Two backends, one honesty rule

Since 0.5.0 the reference implementation ships Level 3 twice, as mutually exclusive builds:

French-Canadian is the instructive case: fr-CA sorts accents from the end of the word backwards — a context-sensitive rule no context-free table can reproduce exactly. So the two backends answer the same document differently:

$ kaiv validate qc.kaiv qc.saiv     # ..lex[fr-CA], ICU backend
pass

while the playground at demo.kaiv.io — the same crate compiled to WebAssembly with the colligo backend, 1.9 MB total where ICU’s data would have tripled it — returns, for the very same input:

{"ok":false,"error":"CollationUnsupportedError: ::city=Gatineau
  (type !str) needs a collation unavailable in ..lex[fr-CA]
  [a,z] (line 2)"}

One accepts; one refuses; both are conforming — because the one thing neither is allowed to do is return a different verdict about the ordering. That is the Level 3 rule doing its job under real engineering pressure: the lightweight build trades coverage, never correctness. (The two features even refuse to coexist — enabling both is a compile_error!, because a dependency graph silently picking a winner would be exactly the kind of disagreement the level exists to prevent.)

The design lesson

Every other corner of kaiv is arithmetic: bytes, digits, lockstep scans, one right answer. Human alphabetical order is the one ingredient that isn’t — so the format treats it like the hazardous material it is. Contain it behind an opt-in span. Pin its parameters so one tag means one ordering, everywhere, indefinitely. And write the escape hatch into the conformance rules themselves: any validator, on any budget, may always say “I don’t do locales” — loudly, by name — and remain correct. The only forbidden move is to guess.


kaiv is an immutable structural type system for data at rest. The specification § 5 defines Level 3, the reference CLDR version, and the strength options precisely; the User Manual covers collation spans hands-on; the playground runs the colligo backend — including the refusal above — in your browser.