kaiv — Specification

version 1.0-draft.15

Terminology reference. All kaiv terms used in this document (namespace, namepath, field address, canonical form, Compiler, Denormalizer, Validator, integrity check, etc.) are defined in Appendix A, the single source of truth for kaiv terminology.

Status. This document is the working specification, organized around the canonical Level system (Scalars / Trees / Tables / Collation / Corpus-Dependent Features) defined in the Level chapters below and summarized in the Terminology appendix (§A.11). Foundations, Levels 0–4, Compiled Schema (including the map form), Mappings (.maiv), Parsing Requirements, the Formal Grammar (ABNF, Levels 0–1), Errors, and File Representation are populated. The built-in unit set is enumerated normatively (§2.7.7), Level 3 collation is pinned to a reference CLDR version and strength (§5.3), and the pipeline materializes defaults and nulls into .daiv — the Denormalizer is schema-aware (§2.6.17). Implementation-side background (the build/runtime split, the certification boundary, the performance model) is collected in the non-normative Appendix B. This document’s conformance surface is Levels 0–3; Level 4 (Corpus-Dependent Features) is defined and reserved here (§6) and specified separately in the experimental corpus specification, on its own version cadence — this document’s stability promise does not extend to it. The unit-definition file format (.faiv, §2.7.9) and the .!units import are populated; constrained-union lowering is specified (§2.6.14); and namespace-schema delegation — the discriminated schema set — is specified (§7.6), with reference-implementation support pending.

Introduction

kaiv is an immutable structural type system for data at rest. It is realized as a family of line-oriented UTF-8 text formats — authored data (.kaiv), canonical data (.raiv, .daiv), schemas (.saiv, .csaiv), type libraries (.taiv), unit definitions (.faiv), and schema mappings (.maiv) — that share one line grammar, so a single per-line classifier reads every file in the family. Data is validated against compiled schemas by a constant-memory parallel scan, and the fully denormalized canonical form (.daiv) is the only artifact a downstream consumer ever needs to trust.

This document specifies the format family: the foundations (data model, line grammar, sigil system, ordered keys), the five conformance Levels — Level 0 (Scalars) through Level 4 (Corpus-Dependent Features; reserved, specified separately) — the compiled schema (.csaiv) validation contract, parsing requirements, the formal grammar (ABNF, Levels 0–1), the error taxonomy, and the file representation (extensions, encoding, media types).

The intended audience is implementers — of Lexers, of the Compiler, Denormalizer, and Validator stages, and of tooling that consumes the canonical forms — and authors of kaiv schemas, type libraries, and data.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in small capitals, as shown here.

1 Foundations

This section establishes the substrate that every Level builds on: the data model, the line grammar shared by every kaiv file (caiv), the sigil system, and the ordered-keys property.

1.1 Scope

This specification defines:

This specification does not define:

Conformance is determined by adherence to this specification, not by behavioral equivalence with any particular implementation. Many implementations may coexist (streaming parsers for embedded systems, channel-based parsers in Go or Erlang, iterator-based parsers in Rust or Python), each conformant to the same spec.

1.2 The Data Model

The data model is a tree: interior nodes are namespaces, leaf nodes are literals (scalar fields and scalar array elements). A name is a scope-local identifier (analogous to “key” in JSON). A namepath is a globally unique address composed of names separated by /. A namespace is the container that a name (or namepath) refers to. The :: operator is the field projection operator: it transitions from the tree (namespaces) to a leaf value, selecting one named property from a namespace — either a scalar field (::host) or a scalar array element (/@ports::0, where the numeric index 0 is the field name).

Arrays are a special form of namespace that uses integer (index) fields. The @ sigil marks an array namespace. Array elements are accessed by the operator immediately after the array name and before the index: @name::index projects a scalar element (the index is the field), and @name/index descends into a namespace element (the element is an interior node). This is the same operator distinction that governs every other path step — :: exits the tree to a leaf value, / stays in the tree. A mixed array uses ::index for some elements and /index for others, per element.

kaiv text is what you author and what the Lexer reads — the raw characters in a .kaiv file, before any parsing or validation. A kaiv document is the validated abstract tree with the root namespace, all namepaths resolved, and all types checked — the result of the full three-stage pipeline. The Compiler produces relational kaiv text (.raiv) — the relational intermediate where field references are preserved, all variables resolved and elided, and all namepaths fully qualified. The Denormalizer then reads .raiv — together with the compiled schema (.csaiv) when the data declares one — expands $field references, materializes absent optional fields (§[sec:default-values]), lifts untyped lines to their fields’ retained head types (§7.3), and converts authored same-dimension units into each field’s declared unit (§2.7.3) to produce denormalized kaiv text (.daiv) — the self-contained deployment artifact where all field references are resolved to their values and every schema-declared field is present. The Validator checks .daiv against the compiled schema (.csaiv) and produces pass/fail. .kaiv is source code; .raiv is the compiled intermediate; .daiv is the fully denormalized deployment artifact; only .daiv is what safety-critical systems ever see.

1.3 The Universal Line Grammar (caiv)

Every file in the kaiv family — .kaiv, .daiv, .raiv, .saiv, .csaiv, .taiv, .faiv, .maiv, .qaiv, and the draft .msaiv (reserved — corpus specification, §6) — shares the same line grammar. This shared grammar is called caiv (core attributed information values, pronounced “cave”). Once you understand caiv, you know the shape of every kaiv file. Each chapter then only needs to explain what is different about its file type, not what is the same.

caiv is a pedagogical anchor — a named invariant that makes the spec easier to read, teach, and discuss. It is not a file extension (no .caiv files exist on disk), not an implementation component, and not a certification target.

1.3.1 The Six Rules

For each line in any kaiv file, exactly one of the six rules below applies. Authored .kaiv/.saiv files add one structural case ahead of rule 5 — block-delimiter lines — described in the note after the table; canonical files (.daiv/.raiv/.csaiv) never contain them.

Rule Test Classification
1 Line is blank (empty or whitespace only) Skip
2 Line starts with # Comment
3 Line starts with // Doc comment
4 Line starts with .! or .? Declaration — see §2.1.1 for the full keyword set
5 Line contains = Content line — split on the first = into a left side and a right side
6 None of the above (no =, not declaration/comment/blank) Metadata annotation — classify by first character (!, ?, &; in .saiv/.taiv also constraint leaders; in .faiv a definition-line leader — see below). In authored .kaiv, a $/. leader is a variable-splat line (§2.5.2)

Block-delimiter lines (authored files). A line whose first character (after leading-whitespace stripping) is [ or ( is a structure line — a section-block or namespace-block open or close: [/@servers], [], (/server), (), including a section-open line carrying a Level 2 table header (§10.7). Structure lines are recognized by their bracket/paren delimiters before the rule-5 = test, because a table header may itself contain = ([/@servers host=! min=1]) and would otherwise misclassify as a content line. A line that both opens and closes with brackets ([…]) or parens ((…)) is a structure line; this is what distinguishes it from a range-constraint metadata line ([0,100] ..num), which does not end in ]. Structure lines occur only in authored .kaiv and .saiv files; canonical files have blocks already expanded to indexed namepaths. The pairing of open/close lines is resolved by the Compiler (§3.1.2), not the line classifier.

Collection-constraint lines (.csaiv).

A compiled schema carries collection-constraint lines (§5.5) — an array or namespace path followed by bracketed clauses (uniqueness, cardinality, and foreign-key on arrays; map-key and schema-delegation on namespaces): /@servers [unique::host,port] [min=1] [max=50]. A cardinality clause may contain = ([min=1]), which would misclassify under rule 5. In .csaiv files, a line whose first character is / and that contains no ' delimiter is a collection-constraint line, recognized before the rule-5 = test — the same carve-out-ahead-of-rule-5 mechanism as block-delimiter lines in authored files.

Map-key lines (.saiv).

Inside a map schema block, the key line is a whole pattern in key position (§7.5): /^[a-z]{2,3}$/=. The pattern body may itself contain =, so — by the same mechanism as the rule-6 priority — a .saiv line leading with / whose entire text parses as a single pattern followed by a terminal = is classified as a content line with the pattern as its key, ahead of the rule-5 first-= split. The pattern is the key grammar, so the bare-name key check (INVALID_KEY_ERROR) does not apply to it; the schema compiler rejects the line kind anywhere outside a map schema block.

1.3.2 Classifier Pseudocode

for each line:                       # leading whitespace already stripped
    if blank                    → skip
    if starts with '#'          → comment
    if starts with '//'         → doc comment
    if starts with '.!' or '.?' → declaration
    if starts with '[' or '(' AND ends with ']' or ')'
                                → structure line   # authored .kaiv/.saiv only:
                                                    # block open/close, incl. table
                                                    # header; BEFORE the '=' test.
                                                    # The closing-delimiter test is
                                                    # what keeps "[0,100] ..num" a
                                                    # rule-6 constraint line
    if starts with '/' and has no "'"
                                → collection line  # .csaiv only: collection
                                                   # constraints; a cardinality
                                                   # clause may contain '='
    if starts with a metadata leader for this file kind
       and the ENTIRE line parses as rule 6
                                → metadata annotation  # rule-6 priority: a pattern
                                                       # or enum item may contain '='
    if contains '='             → content line: split on first '=' → (left, right)
    else                        → metadata annotation: classify by first char
                                  # .kaiv: !, ?, &, $/. (splat)
                                  # .saiv/.taiv also: / { .. [

The classifier is =-based, not first-character-based for content lines. The ! sigil means different things in different file types, which is why first-character classification alone is insufficient.

Rule 4 tests for the two-character declaration sigils .! and .?, not for a bare leading . — a line beginning with . followed by any other character is not a declaration. In particular, hidden-variable definitions (.name=value, see §2.5) begin with . but fall through to rule 5 as ordinary content lines.

Rule-6 priority for metadata-leader lines. A pattern or enumeration item may contain a literal = — an annotation like !str/^a=b$/, or an enum with = in a member. Under naive rule ordering, rule 5 would misclassify these as content lines. So: for a line whose first character is a rule-6 metadata leader for the file kind (!, ?, & in data files; additionally /, {, [, .. in .saiv/.taiv; in .faiv any character that can begin a definition line — ALPHA/DIGIT opening a unit expression, or $ for a currency dimension), the classifier first attempts the full rule-6 parse; the parse must consume the entire line to classify, otherwise the line falls through to the rule-5 =-test. Falling through is what keeps every existing content form correct: a canonical line !str'::host=x fails the annotation parse at ' and lands in rule 5; a .taiv definition &int= fails the named-annotation parse at = and lands in rule 5; an authored /server::host=x in a schema fails the constraint-line parse and lands in rule 5.

For example: in .kaiv, a line like !int (no =) is a metadata annotation (rule 6 — a type annotation above the next data line); in .daiv, a line like !int?sensor23'::temperature_f=100 (contains =) is a content line (rule 5). The disambiguation is by the presence or absence of =, refined by the rule-6 priority above for metadata-leader lines whose grammar admits an embedded =.

1.3.3 How Each File Type Interprets the Left Side and Right Side

Each file type defines its own interpretation of the left side and right side of =:

File type Left side interpretation Right side interpretation
.daiv / .raiv !type:unit?prov'namepath — metadata prefix (!type plus optional :unit and optional ?prov) followed by ' then a fully-qualified namepath Value (scalar string)
.csaiv constraint'namepath — constraint form followed by ' then a namepath The compile-time-resolved applicable default (often empty); requiredness is carried by the operator (= required, ?= optional). The Validator ignores the right side
.kaiv Bare key, namespaced key (/server::host), array op (/@ports+=), variable, field reference, etc. Value, variable reference, or field reference
.saiv Field definition key — the type annotation sits on the metadata line above The field’s default value (a kaiv value is never absent, only empty — an empty right side is the empty-string default); optionality is carried by the operator (= required, ?= optional)
.taiv Named type definition — &name The type’s default value (an empty right side is the inert empty-string default), inherited by fields through the default cascade (§[sec:default-values])
.faiv Unit definition — &name (the dimension/factor line sits above) Empty, or an alias target (&alias=name)
.qaiv Query pattern — path expression with optional predicates Match expression

1.3.4 Metadata Annotations (Rule 6)

Metadata annotations are lines with no = that are not declarations, comments, blank, or structure lines. In data files (.kaiv) they are a small, closed set of sigil-prefixed lines:

First character Meaning
! Type annotation (!int, !str, !bool, etc.) — annotates the type of the next content line; the elided-type form !:unit asserts only the unit (§2.7.4)
? Provenance annotation (?id or ?id@timestamp) — annotates the provenance source of the next content line
& Named type annotation (&port, &datetime) — like !type but references a named type from a library

In .saiv schema files and .taiv type library files, rule-6 lines additionally include constraint lines — space-separated constraint items placed above a field or &name= definition (§2.8, §[sec:named-types-in-schemas]). A constraint line may lead with / (pattern), { (enum), .. (span, a leading .), [ (range, when followed by further items so the line does not end in ]), in addition to !/&. So the rule-6 first-character set for schema and type library files is { ! ? & / { . [ } plus lines leading with the reserved re{sep} pattern-literal introducer, not the three-element data-file set. A re{sep}-leading line that fails the full-line parse is a malformed literal (INVALID_CONSTRAINT_ERROR), never a content line. A length constraint cannot lead a constraint line: rule 2 fires first and classifies any #-leading line as a comment, so #[2,8] alone is a comment, not a constraint. Authors put another item first (..lex #[2,8]) or use the whitespace-free annotation form (!str#[2,8]), where the # is not line-leading. The mirror-image trap: a [-leading constraint line whose final item ends in ] ([0,100] #[2,8]) opens and closes with brackets and therefore classifies as a structure line — glue such items into an annotation instead (!str[0,100]#[2,8]). The authoritative production is constraint-line in §10; a line that is wholly […] or (…) is a structure line (§1.3.1), recognized ahead of rule 6.

A metadata annotation binds to the next content line as a whole: when that line expands to several canonical lines (;= vector assignment, := struct assignment, +:= array-append struct), the annotation applies to every line of the expansion — !int above /limits:=rps=500|burst=200 types both fields. This is what makes single-annotation authoring of homogeneous structs and vectors possible; heterogeneous members need the per-line form.

Annotations of different kinds stack: at most one type-designating annotation (!type… or &name) and at most one provenance annotation (?sourceID…) may appear, in either order, above the same content line — this is how a data line carrying both a type and provenance (!int?sensor23'::temperature_f=100 in canonical form) is authored. A second annotation of the same kind above one content line raises MetadataWithoutTargetError11.2).

Metadata annotations appear only in authored files (.kaiv, .saiv, .taiv). In canonical files (.daiv, .raiv, .csaiv), rule 6 never applies — every content line contains =, and type and provenance information is folded into the left side of = as part of the metadata prefix before ' (.csaiv collection-constraint lines are recognized by their own carve-out ahead of rule 5, see §1.3.1). Metadata annotations as separate lines exist only in authoring.

Once you know caiv, you know the shape of every kaiv file. The Six Rules apply universally — across all ten file types, across all conformance Levels, across all tool stages. Each Level only needs to explain what is different about its file types: how they interpret the left side and right side of =, which comment forms they allow, which declarations they use, and whether they have metadata annotations. The Six Rules themselves never change.

These Foundations tables are the teaching presentation of the line grammar. The normative, machine-checkable form is the ABNF in §10, which wins on any conflict with the prose here (it says so itself). When implementing, treat that grammar — not these tables — as authoritative, and report any discrepancy.

1.4 The Sigil System

The sigils are type discriminators that map onto the fundamental data structures shared by every major interchange format. Every major structured data format supports scalars, arrays, and objects; kaiv’s sigils correspond to exactly those three categories, plus declarations.

Sigil kaiv Construct JSON TOML ProtoBuf GraphQL ASN.1
(none) key=value scalar primitive value scalar field scalar field primitive type
@ array — a namespace with integer fields. Scalar array elements are projected via @name::index; namespace array elements are descended into via @name/index array array repeated field list type SEQUENCE OF
/ namespace — a structural sigil that survives canonicalization. /server::host in authored .kaiv becomes !str'/server::host=localhost in canonical .daiv. The / is part of the canonical namepath object table message / nested message type / nested type SEQUENCE / module
.! declaration (.!kaiv, .!schema, .!types, .!registry) — format and schema declarations, type library imports, and registry prefix overrides package / syntax module header
.? provenance source declaration (.?id uri) — document-level declaration that maps a short provenance ID to a full URI
$ dereference operator. Variables (dot-prefixed): $.name, [email protected], $/.name. Field references (no . after $): $field, $path::field. Absent in verbatim documents (§2.5.6)
! type annotation — the type sigil: !int on a metadata line in authored files, !type[constraints]:unit in the canonical metadata prefix before '. Survives canonicalization as part of every canonical line field type type type
& imported library type annotation (&name on metadata line) or named type definition (&name= in .taiv). Authoring-only — resolved to !library/path/typename in canonical form named type / logical type named type
? provenance annotation/reference — ?id (or ?id@timestamp or ?id@timestamp#dpid) on a metadata line in authored .kaiv, or inline in the metadata prefix before ' in canonical .daiv/.raiv

1.4.1 Sigil Survival Rule

The harmonized rule is: structural sigils (/, @) survive canonicalization; resolution sigils (., $) do not.

Sigil Role .kaiv .daiv Why
/ Namespace path [x] present [x] present Structural: self-describing, matches query syntax
@ Array path [x] present [x] present Structural: self-describing, matches query syntax
. Variable / hidden name [x] present [ ] elided Resolved by the Compiler — variables have no existence in canonical form
$ Dereference [x] present [ ] elided $.var resolved by the Compiler; $field resolved by the Denormalizer — neither appears in canonical form

The DFA dispatch after ' in canonical form follows directly from these rules:

First char after ' Structural meaning
/ Namespace path follows — read until :: for the field name. Array steps appear inside the path as @-prefixed steps (/@servers/0)
: Expect second :, then root field mode

This is one additional character check compared to a design where / is stripped. The benefit is that every canonical line is self-describing without context.

1.4.2 The Two Path Operators

The two path operators complement the sigils:

Operator Formal name Operation Transition
/ tree descent operator Navigate from a name to a child name — purely interior node to interior node; never terminal interior node → interior node
:: field projection operator Select a leaf from a namespace — the field name immediately follows :: and is always the last element before =. Appears in every canonical data line interior node → leaf value

/ stays within the world of tree nodes — it moves from one namespace to a child namespace. :: exits the tree: it projects a leaf value out of a namespace. After a projection, the path is terminal — there is nothing left to navigate into. This is why / can be chained indefinitely but :: always ends a path. Every canonical data line has :: as the tree→leaf boundary.

The same distinction governs arrays: after an array name (@name), using ::index makes that index a scalar field — !type'/@name::index=value is a leaf. Using /index descends into a namespace element — !type'/@name/index::field=value requires further :: projection to reach the leaf. The @ sigil always means “array namespace”; the operator after the array name and before the index determines element kind.

1.4.3 Authoring vs. Canonical Sigil Resolution

& joins several authoring constructs that canonicalize to a more explicit form. None of these appear in .daiv or .csaiv:

Authoring form (.kaiv/.saiv) Canonical form (.daiv/.csaiv) What the Compiler/Denormalizer/Validator does
&name type annotation !library/path/name (non-core) or !core-shorthand (for std/core) Resolves &name against imported type libraries; std/core types stay as !int/!bool etc., all others become !library/path/name
/ns::field=value (authored namespace path) /ns::field=value (canonical — unchanged) / survives canonicalization. It is a structural sigil, not a resolution sigil.
+= array append Indexed lines (/@name::0=v, /@name::1=v, …) Tracks index counter, emits numbered assignments
;= vector assignment Indexed lines (same as += but batch) Same as += but processes semicolon-separated values
:= namespace assignment Individual field lines (/path::field=value, …) Splits pipe-delimited pairs into separate /-prefixed assignments; / is preserved
+:= array-append namespace assignment Indexed namespace-element lines (/@name/0::host=a, /@name/0::port=1, …) Tracks index counter like +=, splits pipe-delimited pairs like :=, emits /@name/index::field=value lines (see §3.1.1)
$.name / [email protected] / $/.name variable reference Inlined value (variable elided from output) Substitutes value from variable table; elides dot-prefixed definitions
$field / $path::field field reference .raiv: preserved as-is; .daiv: resolved In .daiv, inlines the value from the field table; in .raiv, preserved verbatim
!:unit elided-type unit annotation .raiv: preserved; .daiv: resolved head (or !float) with the declared unit, value converted Denormalizer inherits the governing head, else float; converts to the declared unit (§2.7.4)
?id (or ?id@timestamp, etc.) metadata line provenance list inline before ' in canonical metadata prefix Collapses per-line provenance annotations into the canonical metadata prefix. Unlike &name, ?id is not resolved away — it survives into canonical form

1.4.4 $ Is Additive (Prepend, Not Replace)

kaiv’s $ is always additive — it never replaces a sigil. The full container type is always visible in the reference:

The triple character sequence $@. is three distinct pieces of information: $ (“look up and substitute”), @ (“the thing being looked up is an array”), . (“the name is hidden, elided from canonical output”). This contrasts with Perl’s $array[0], where @ is replaced by $ and the container type is lost in the reference.

The discriminant between hidden variables and visible data fields is the . (dot) immediately after $ (or after $@/$/):

The $ operator is always additive. The container sigil (@, /) is always preserved. The hidden marker (.) is always visible. Three orthogonal pieces of information, three separate characters, no ambiguity.

1.5 Ordered Keys

Key ordering is significant in kaiv. This is a deliberate choice of strictness over looseness, with consequences throughout the pipeline.

Variable expansion is well-defined. $.name in a value can only refer to names defined on previous lines. There are no forward references, no circular dependencies, and no resolution-order ambiguity. The rules are simple and local. This strict ordering is what makes variable interpolation resolvable in a single left-to-right pass with no dependency-graph construction.

Streaming validation stays single-pass. The compiled schema parallel scan processes entries in document order and never needs to look ahead or revisit prior tokens. This is what makes the O(N) linear validation structure (described under §7 below) possible.

Diffing and merging are meaningful. Two kaiv documents with the same fields in different orders are different documents. This makes kaiv documents suitable for version-controlled storage with meaningful diffs.

Target formats that do not care simply ignore the ordering. Conversion from kaiv preserves order; conversion to kaiv from an unordered format requires a canonical ordering rule. Acceptable rules include: alphabetical order, schema-definition order, or original-definition order (for formats that have one). The choice of ordering rule is part of the interchange profile for that target format.

2 Level 0: Scalars

Level 0 covers the per-line concerns: declarations, variables, type annotations, provenance, scalar key=value lines, and the constraint forms applied within type annotations. Everything in Level 0 is processable by a minimal DFA in constant memory.

2.1 Declarations

Declarations are commands that apply to the entire kaiv text. They MUST be placed at the top of the text, before any content lines. Declarations use the .! sigil (or .? for provenance source declarations).

2.1.1 Declaration Inventory

The complete set of declaration keywords, across all file types, is:

Keyword Syntax File types Defined in
.!kaiv .!kaiv [VERSION] (bare form means version 1) .kaiv (optional — absent means authored .kaiv version 1) §2.1.2
.!raiv .!raiv [VERSION] (bare form means version 1) .raiv (always present — emitted by the Compiler; §2.1.2) §2.1.2
.!daiv .!daiv [VERSION] (bare form means version 1) .daiv (always present — emitted by the Denormalizer; §2.1.2) §2.1.2
.!saiv .!saiv [VERSION] ID-OR-URL [strict] (bare form means version 1) .saiv §[sec:named-types-in-schemas]; strict modifier in §11
.!csaiv .!csaiv [VERSION] ID-OR-URL [strict] (bare form means version 1) .csaiv (always present — emitted by the schema compiler; §2.1.2) §2.1.2
.!taiv .!taiv [VERSION] LIBRARY-ID (bare form means version 1) .taiv §2.6.13
.!faiv .!faiv [VERSION] LIBRARY-ID (bare form means version 1) .faiv §2.7.9
.!maiv .!maiv [VERSION] (bare form means version 1; no identity token — a mapping’s identity is its endpoint pair, §8.2) .maiv §8.1
.!source .!source ID-OR-URL .maiv §8.1
.!target .!target ID-OR-URL .maiv §8.1
.!via .!via MAP-ID .maiv §8.6
.!drop .!drop NAMEPATH .maiv §8.1
.!schema .!schema:ID / .!schema ID-OR-URL / .!schema:/ns ID-OR-URL / .!schema:/@arr ID-OR-URL .kaiv, .raiv, .daiv, .saiv (inheritance) §2.1.3; §3.5.6
.!types .!types LIBRARY-ID .saiv, .taiv, .kaiv; resolved at build time — does not survive into canonical output (canonical type names are fully qualified) §2.6.13
.!units .!units LIBRARY-ID .kaiv, .saiv, .taiv; survives into canonical output (§2.7.10) §2.7.10
.!verbatim .!verbatim (no arguments; at most once) .kaiv, .raiv; carried into .raiv by the Compiler, discharged by the Denormalizer — does not survive into .daiv §2.5.6
.!registry .!registry prefix=base_url all authored file types; SHOULD survive into .daiv2.1.4) §2.1.4
.!provenance .!provenance:LEVEL .saiv, .csaiv §2.4.3
.!ref .!ref:alias schemapath reserved — corpus spec §6
.!compose .!compose:NAMEPATH SCHEMA JOIN reserved — corpus spec §6
.!msaiv .!msaiv [VERSION] CORPUS-ID (bare form means version 1) reserved — corpus spec §6
.!bind .!bind:PATTERN SCHEMA reserved — corpus spec §6
.!unique .!unique:PATTERN NAMEPATH reserved — corpus spec §6
.!fk .!fk:PATTERN NAMEPATH TARGET::NAMEPATH reserved — corpus spec §6
.?<id> .?id uri .kaiv, .raiv, .daiv §2.4

This table is the authoritative recognizer set: a lexer classifies a line as a declaration iff it begins with .! or .? (rule 4 of the Six Rules), and a .! line whose keyword is not in this table is an INVALID_DIRECTIVE_ERROR (see §11.1). Which keywords are meaningful varies by file type per the table; a keyword valid in the grammar but out of place for the file type is diagnosed by the consuming stage, not the lexer.

2.1.2 Format Declaration

The format declaration names the file’s kind and, optionally, the kaiv text version. The declaration keyword mirrors the file extension — every kind in the family is self-describing by its first line, with no filename or out-of-band context needed (.qaiv, whose design is unfrozen and outside this document, will pin its declaration when it freezes):

File Declaration Presence
.kaiv .!kaiv [VERSION] OPTIONAL (absent means .kaiv version 1)
.raiv .!raiv [VERSION] always present (emitted by the Compiler)
.daiv .!daiv [VERSION] always present (emitted by the Denormalizer)
.saiv .!saiv [VERSION] ID [strict] REQUIRED
.csaiv .!csaiv [VERSION] ID [strict] always present (emitted by the schema compiler)
.taiv .!taiv [VERSION] LIBRARY-ID REQUIRED
.faiv .!faiv [VERSION] LIBRARY-ID REQUIRED
.maiv .!maiv [VERSION] REQUIRED (bare form means version 1)
.msaiv .!msaiv [VERSION] CORPUS-ID REQUIRED (reserved — corpus spec)

The version is optional for every kind, and the bare form is canonical: a versioned declaration naming version 1 (.!kaiv 1, .!kaiv 1.0, .!kaiv 1.0.0, .!saiv 1 acme/x) is equivalent authored input, and the build pipeline emits the bare form. The version token exists for forward evolution only; a future major revision would be a separate specification in any case, free to decide its own marker. The identity-carrying kinds (.!saiv, .!csaiv, .!taiv, .!faiv, .!msaiv) carry a library or schema identity after the keyword, and the declaration is split by token shape: a digit-first first token is the version (validated against the version syntax below); any other first token is the identity, and the version is 1. The two can never collide — a version is digits and dots only, while an identity is a library path whose first segment starts with a letter (library-path, §10.1) or an absolute URL. .!maiv carries no identity token of its own — a mapping’s identity is its endpoint pair (§8.2) — so its bare form is the keyword alone.

The format declaration, when present, MUST be placed on the first line of the kaiv text — or, when an optional shebang line is present (§12.5), on the first line after it.

In authored .kaiv data files the format declaration is OPTIONAL: a file that carries no format declaration is read as authored .kaiv, version 1, exactly as if it opened with .!kaiv. The default is deliberate — it keeps plain KEY=value files, including any well-formed .env file, valid kaiv documents as-is, with no header required. The one caveat is $: a bare dollar in a value is reference syntax at compile time (§2.5.3), so a .env whose values carry dollars lexes as-is but compiles only with a .!verbatim declaration prepended (§2.5.6).

Canonical files always carry their kind declaration explicitly, and the pipeline rewrites the keyword at each stage: the Compiler MUST emit .!raiv as the first line of .raiv output, the Denormalizer MUST emit .!daiv as the first line of .daiv output, and the schema compiler MUST emit .!csaiv (preserving the identity and any strict modifier from the .!saiv header, with a version-1 header normalized to the bare form and an explicit non-1 version preserved) as the first line of .csaiv output. Each emits its keyword whether or not the source document carried a declaration of its own. Consumers of canonical files are strict: a stream presented as a canonical kind whose first line is missing the matching declaration — or carries a different kind’s — raises FORMAT_KIND_ERROR11.1). The same gate applies to the REQUIRED-header authored kinds: a .saiv / .taiv / .faiv / .maiv stream whose first line is missing its declaration — or carries another kind’s — raises the same error in its consuming stage. This is what makes validation context-free: .kaiv and .daiv can no longer be confused for one another, because only the former may omit the declaration and each names its own kind. Canonical excerpts elsewhere in this document sometimes omit the format declaration when illustrating individual lines or isolated constructs; a complete canonical document always opens with it.

The version, when given, is one to three dot-separated decimal integers — major, major.minor, or major.minor.patch — matching ^[0-9]+(\.[0-9]+){0,2}$. Omitted components are zero: 1, 1.0, and 1.0.0 name the same version, and all three canonicalize to the bare form — which is why the examples in this document write bare declarations throughout. The same version syntax applies to the identity-carrying declarations.

2.1.3 Schema Declaration

The schema declaration uses the .!schema command followed by the schema reference. Schema references fall into two categories: (1) a kaiv schema registry reference, and (2) a custom URL. The two categories each use a different syntax.

A kaiv schema registry reference appends a schema identifier to the .!schema command, separated by a colon. For example:

.!schema:acme/test-one
.!schema:acme/api-request

A custom URL schema reference is a space-separated URL on the .!schema line:

.!schema https://example.org/schema.saiv
.!schema https://example.org/schema.csaiv

Note that the custom URL schema reference MUST include the file extension: these are concrete file references. The kaiv schema registry reference, on the other hand, returns the compiled schema (.csaiv) by default; content negotiation with the kind media-type parameter selects another kind — Accept: application/kaiv;kind=saiv requests the authored schema (§12.4.2).

An encapsulated schema reference scopes the hub schema’s fields under a sub-namespace rather than merging them at root. The colon after .!schema introduces a namespace qualifier using the / namespace prefix:

.!schema:/server hub/server-endpoint
.!schema:/auth hub/credentials

This places hub/server-endpoint fields under the /server namespace and hub/credentials fields under the /auth namespace. The same hub may be encapsulated multiple times under different namespaces:

.!schema:/upstream hub/server-endpoint
.!schema:/downstream hub/server-endpoint

URL references also accept the namespace qualifier:

.!schema:/server https://example.org/schema.csaiv

Flat extension (fields at root) and encapsulated extension (fields under a namespace) may be combined freely in the same kaiv text.

2.1.4 Type Registry Resolution

All named type references (!library/path/typename) and type library imports (.!types) implicitly resolve through ktaiv.com. This works for the public ecosystem but does not cover air-gapped environments, IP-protected enterprise types, regulatory data sovereignty, or builds that need to be independent of ktaiv.com uptime. The solution is a layered resolution model: four resolution layers evaluated in priority order so that the most specific configuration wins.

Layer Mechanism Priority Internet required?
1 .!registry declaration in the document Highest No
2 Build-time configuration (kaiv.kaiv / environment variables) High No
3 Registry redirect aliasing (HTTP 301/302 from ktaiv.com for types, ksaiv.com for schemas) Low Yes
4 Default registry (ktaiv.com for .taiv; ksaiv.com for .saiv/.csaiv/.maiv) Lowest Yes
Layer 1: .!registry declaration.

A .!registry declaration in the document maps a library-path prefix to an alternative base URL. It is a document-level declaration — placed after the format declaration, before content lines. Declarations pass through to canonical form (the format declaration itself is rewritten to the output kind’s keyword, §2.1.2), so a .daiv carrying registry-resolved identities reads:

.!daiv
.!registry acme=https://types.acme.com
.!registry internal=https://types.internal.corp.net
.!schema:acme/server-config
!acme/ourtypes/customerid'::owner=CUST-001
!internal/auth/token'::session=abc123

Rules:

Layer 2: Build-time configuration. A kaiv.kaiv file (project-level, at the project root) maps prefixes to alternative registries with no impact on the document format. The toolchain’s own configuration is a kaiv document:

# kaiv.kaiv
.!kaiv

/registries::acme=https://types.acme.com
/registries::internal=https://types.internal.corp.net
/registries::default=https://ktaiv.com

The file is deliberately restricted to the Level 0–1 scalar subset: flat key=value fields under the single /registries namespace (namespaces and :: projection are Level 1 constructs), no schema, no type annotations, no named-type references. This is what makes the bootstrap sound — a kaiv processor parses kaiv.kaiv with the core Level 0–1 pipeline before any type resolution exists, so the configuration that drives resolution never needs resolution itself. A registry prefix containing characters outside the bare-name grammar (- is common in path-seg prefixes) is written as a quoted name: /registries::"acme-corp"=https://types.acme-corp.com.

A base value is an absolute http(s) URL, or a filesystem path — absolute, or relative to the directory containing kaiv.kaiv — for air-gapped and local-tree resolution. Resolution appends {library/path}.taiv to the base either way: /registries::acme=./types resolves !acme/ourtypes/customerid against ./types/acme/ourtypes.taiv. The reserved key default overrides the Layer 4 default registry for unmatched prefixes. The same map resolves every registry-shaped identifier, with the appended extension determined by the consumer: type references and .!types imports append .taiv, .!units imports append .faiv2.7.10), and .!schema IDs append .saiv when the schema compiler consumes them (inheritance, §7.3) and .csaiv when the Denormalizer/Validator does — the conformance suite exercises all three.

Environment variables override the file:

KAIV_REGISTRY_ACME=https://types.acme.com
KAIV_REGISTRY_INTERNAL=https://types.internal.corp.net
KAIV_REGISTRY=https://types.internal.corp.net   # default override for unmatched prefixes

Naming convention: KAIV_REGISTRY_{PREFIX} (prefix uppercased) for per-prefix override; KAIV_REGISTRY for the default. Air-gapped environments change configuration without touching any source file. The .daiv file is identical regardless of which registry was used — type identity is a logical path, independent of the resolution mechanism.

Layer 3: Registry redirect aliasing. ksaiv.com and ktaiv.com support HTTP 301/302 redirects. A registered namespace prefix configured by its owner can transparently redirect resolution requests to the owner’s own server:

!acme/ourtypes/customerid
  → GET ktaiv.com/acme/ourtypes.taiv
  → HTTP 301 Moved Permanently → https://acmekaiv.example.com/types/acme/ourtypes.taiv
  → fetches .taiv from owner's server

The canonical type path is unchanged — the redirect is transparent to type identity. The registry acts as a naming authority: it validates prefix ownership and ensures global uniqueness of library paths. Implementations SHOULD cache resolved .taiv files keyed by their canonical URL.

Layer 4: Default registry. The kaiv registries are split by artifact kind: ktaiv.com hosts type libraries (.taiv); ksaiv.com hosts schemas (.saiv, .csaiv) and mappings (.maiv). The file extension discriminates artifact kind and selects the registry.

Artifact Extension Resolution path
Type library .taiv ktaiv.com/{library/path}.taiv
Schema .saiv ksaiv.com/{schema/path}.saiv
Compiled schema .csaiv ksaiv.com/{schema/path}.csaiv

For example, !std/net/port resolves to ktaiv.com/std/net.taiv, and the schema acme/server-config to ksaiv.com/acme/server-config.saiv.

Implementations MUST support this layer. std/core is an exception — implementations SHOULD ship it bundled (embedded) so it is always available, even offline.

Alpha hosting. Through the 1.0-draft series of this specification the reference hosts are the kaiv.io registry subdomains — t.kaiv.io (types), s.kaiv.io (schemas), f.kaiv.io (units) — and no eternalink permanence is promised: alpha artifacts (std/net included) may still change, and the alpha registries may be reset. The k*aiv.com production domains named throughout this specification activate at beta, and the write-once, read-forever contract begins there. The default-host table in a conforming implementation is the single switch point.

Type identity vs. type resolution. Type identity is a logical path; type resolution is a deployment concern. Two .daiv files with the same data have the same canonical lines regardless of which resolution layer was used to fetch the source .taiv:

# Layer 1 resolution (fetches from acme.com):
.!registry acme=https://types.acme.com

— or, with no .!registry declaration at all, Layer 4 resolution fetches the same library from ktaiv.com. Both produce the identical canonical line:

!acme/ourtypes/customerid'::owner=CUST-001

The certified runtime never resolves type names — type resolution is build-time only. The integrity check reads .daiv and .csaiv (which carries lowered constraints), not .taiv.

A future extension reserves the design space for DNS-based authority: if the first path segment contains a ., it is treated as a domain that directly serves the type (!acme.com/ourtypes/customerid). This is not specified in the current version but is documented to prevent future path patterns from accidentally closing it off.

Trust model and strict resolution.

The resolution layers differ in who authors them. Layer 2 is the consumer’s own configuration; Layers 3–4 are the registry authority. Layer 1 alone is authored by the document’s producer — and it outranks every other layer. A document can therefore pin a well-known schema identity while redirecting its resolution: one declaring both .!schema:hub/invoice and .!registry hub=https://evil.example validates against whatever the declared base serves, while appearing to validate against the registry’s hub/invoice. Identity is unaffected (a logical path, above), but the content bound to that identity is chosen by the producer. An untrusted document must not be allowed to choose where its own contract comes from.

Implementations MUST therefore provide a strict resolution mode, off by default: when enabled, a .!registry declaration that would determine the base of any resolved artifact raises RegistryStrictError instead of resolving. Dormant declarations — prefixes no resolved reference uses — are not an error, since declarations survive into canonical form. Layer 2 not only remains in effect under strict mode — it shadows Layer 1 for every prefix it covers (exact entry or default), so only an uncovered Layer 1 win is refused. Vendoring the affected prefix in kaiv.kaiv is thus the sanctioned way to validate such a document: the consumer’s configuration decides, and the document’s declaration becomes dormant. Implementations SHOULD also report resolution provenance: for each resolved artifact, the reference, the winning layer, and the resolved base and location.

Implementations SHOULD additionally provide a canonical resolution mode: Layers 1–2 are ignored entirely and every artifact resolves through the Layer 4 default registry — no document declaration, configuration file, or environment variable can redirect resolution. Where strict mode protects a consumer from a document’s overrides, canonical mode asserts that resolution depends on nothing but the reference and the canonical hosts. It subsumes strict mode: a .!registry declaration is dormant by construction, so nothing is refused.

The reference toolchain exposes strict mode as --registry-strict / KAIV_REGISTRY_STRICT=1, canonical mode as --registry-canonical / KAIV_REGISTRY_CANONICAL=1, and provenance reporting as -v.

2.2 Comments

kaiv distinguishes two comment syntaxes with different semantic roles:

Syntax .kaiv .saiv .taiv .faiv .maiv .daiv .csaiv Semantic role
# [x] [x] [x] [x] [x] [ ] [ ] Human annotation — no semantic content
// [ ] [x] [x] [x] [x] SHOULD SHOULD Field/type/unit documentation — schema metadata ([ ]: classified but dropped like #; no doc semantics in data)

# comments never appear in canonical files. // doc strings SHOULD be carried into .daiv/.csaiv by the build pipeline and MAY be omitted for constrained deployments (§2.2.2).

2.2.1 General Comments (#)

# comments are allowed in all authored file types — .kaiv data files, .saiv schema files, .taiv type library files, .faiv unit definition files, and .maiv mapping files. The Lexer emits them as COMMENT tokens but filters them out before the Parser stage. They carry no semantic content and have no effect on the AST, schema validation, or canonical output. The pipeline never emits them into .daiv or .csaiv; a consumer encountering one there (a hand-maintained artifact) classifies and discards it like any comment line — the Six Rules are universal (§1.3.1).

# This is a general comment --- filtered before the Parser stage
host=localhost

2.2.2 Documentation Comments (//)

// doc comments are meaningful only in the definition-bearing file types.saiv schema files, .taiv type library files, .faiv unit definition files, .maiv mapping files, and reserved .msaiv metaschemas — never in .kaiv data files. Documentation is a schema/type/unit concern, not a data concern: “what does this field mean?” is answered by the schema, not by the data instance. Every peer format takes the same position: ProtoBuf comments document .proto schema fields, Avro "doc" is a schema property, GraphQL """...""" descriptions are on SDL, XSD xs:annotation is on schema elements, JSON Schema "description" is a schema keyword.

In .kaiv the six-rule classifier still produces a DOC token — the rules are universal (§1.3.1) — but the token carries no documentation semantics there: the Compiler MUST drop it exactly as it drops a # comment. Doc strings acquire meaning only in the definition-bearing file types.

The Parser associates each DOC token with the immediately following field or type definition line.

// The hostname for the primary database connection
host=
// Port number in the range 0--65535
/^-?[0-9]+$/ ..num [0,65535]
&port=

In .saiv, doc comments document field definitions; in .taiv, they document type definitions. The documentation string is distinct from the field’s value and is not part of the data payload. Avro’s "doc" property maps directly during Avro schema generation, enabling round-trip fidelity.

Documentation in canonical form. The build pipeline SHOULD include // doc strings from the schema in .daiv and .csaiv. Implementations MAY omit them for constrained deployments (safety-critical ECUs, bandwidth-limited buses). When present, the Validator skips them during validation — they are metadata that has no effect on the parallel scan, type checking, or constraint evaluation. When omitted from .daiv, the documentation remains recoverable from the .saiv/.csaiv/.taiv source.

Two deployment profiles are valid:

2.3 Quoted Names

kaiv’s identifier system rests on a single principled boundary: bare names are POSIX identifiers; quoted names exist for interchange with formats whose identifier rules differ from POSIX. No widening of the bare-name alphabet for convenience.

2.3.1 What Gets Quoted

Quoting applies to individual names — the atomic identifiers between path operators. Never to operators, never to entire namepaths.

Namepath component Quotable? Example
Name (between / operators) [x] "Content-Type"
Field (after :: operator) [x] ::"Accept-Language"
Array name (after @) [x] @"x-items"
Path operators (/, ::, @) [ ] Never Always bare
Entire namepath [ ] Never Always composed of individually-quoted-or-bare segments

2.3.2 When to Quote

A name MUST be quoted if and only if it is not a valid POSIX identifier:

bare-name = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" )

If a name matches bare-name, it MUST NOT be quoted in canonical form. If it doesn’t, it MUST be quoted. This makes quoting deterministic — there is exactly one canonical representation for any namepath. No ambiguity, no stylistic choice. Index segments are outside this rule: an element index (/@servers/0, ::0) is its own segment kind (index in §10), always unquoted; a quoted all-digit segment ("0") is an ordinary quoted name — a map key or field name — and never addresses an array element. Authored text MAY quote a bare-able name — necessary when the bare spelling is positionally reserved, e.g. a schema field named re2.6.11) — and the Compiler normalizes it to the bare spelling on canonicalization.

2.3.3 Quoted Name Rules

  1. A quoted name is enclosed in double quotes (").

  2. A double quote character within a name is represented as "" (repeated double quote).

  3. All other characters are literal — no backslash escapes, no \n, no \t. This preserves kaiv’s no-escape-sequences principle; "" is not an escape sequence, it’s a doubling convention (same mechanism as SQL identifiers and CSV fields).

  4. A quoted name MUST contain at least one character (empty quoted names are not allowed).

2.3.4 Examples in Canonical Form

!str'::"Content-Type"=application/json

!str'/app/"dark-mode"::enabled=true

!int'/@"x-servers"/0::"retry-count"=3

!str'::"weird""name"=value with a literal quote in the key

The last example: the name is weird"name where the "" represents a single " character within the quoted identifier.

2.3.5 DFA Impact

Zero structural change. The Lexer sees " at the start of a name position and enters a quoted-name sub-state. Inside that state, "" produces a literal " and a lone " terminates the name. The state machine has three states (START → QUOTED → MAYBE_END). No stack, no lookahead beyond one character. The grammar remains regular.

2.4 Provenance

Provenance records where a value came from and when it was observed. It is an optional prefix on data lines, written before the ' delimiter.

2.4.1 Provenance Syntax

The full provenance prefix grammar is:

provenance_list = '?' provenance (',' provenance)*
provenance      = id ('@' instant)? ('#' dpid)?
id              = identifier
instant         = YYYY-MM-DD 'T' HH:mm:SS 'Z'   (20 chars, canonical)
                | YYYYMMDD 'T' HHmmSS 'Z'       (16 chars, deprecated)
dpid            = identifier

A single provenance entry carries a mandatory source id and two optional qualifiers:

  1. ?sourceID — the source identifier (a short name declared via .?id uri at file top, resolving to a URI, sensor ID, URN, or other opaque string).

  2. @instant — the temporal qualifier recording when the value was observed. The canonical form is dashed extended ISO 8601 UTC, YYYY-MM-DDTHH:MM:SSZ (20 characters): ?sensor1@2026-07-28T14:00:00Z. Producers MUST emit this form.

    The compact basic form YYYYMMDDTHHmmSSZ (16 characters) remains an accepted parse form through the 1.0-draft series and is deprecated: consumers MUST accept it, producers MUST NOT emit it, and it is removed at 1.0. A document carrying the compact form canonicalizes to the dashed form.

    The dashed form introduces : into the provenance run, where : elsewhere in an annotation opens the unit sigil. The two never collide: a provenance run is introduced by ? and consumes to whitespace or ', so every : inside it belongs to the instant. This is the same disambiguation-by-introducer rule that lets # mean data point identifier here and nothing else.

  3. #dpid — the data point identifier: an optional stable identifier assigned to this data point. Any valid identifier is accepted; the identifier is not required to be a UUID. In manually-authored .kaiv files a user may write #request-42 or #row-17; the chraiv.com ingestion pipeline auto-assigns UUID values (#uuid) at ingest time.

The timestamp and data-point qualifiers are optional and independent; the source id anchors every entry — a qualifier cannot appear without one (the grammar above; §10). Provenance answers who says so: the qualifiers refine that attribution, so an attributor-less timestamp is not a provenance form (a document that genuinely has no source names a trivial one). The full canonical metadata prefix when all three are present is:

!type?sourceID@timestamp#dpid'namepath=value

2.4.2 Disambiguation from Length Constraints

The # sigil is used in two distinct positions:

  1. Type-annotation position (after a type or pattern constraint, before ') — denotes a length constraint: #[min,max] or #{a,b,c}. See §2.8.2.

  2. Provenance position (after @timestamp or after ?sourceID when no timestamp is present, still before ') — denotes the data point identifier: #dpid.

The DFA distinguishes the two uses by position: a # encountered while processing the provenance list (after ? and any @timestamp) is always a data point identifier; a # encountered while processing a type annotation (after !type and any pattern or range constraints, with no intervening ?) is always a length constraint. No lookahead or disambiguation table is required.

2.4.3 Requiring Provenance in Schemas

By default provenance is unconstrained: any data line MAY carry any subset of the provenance triple. A schema can make provenance part of the validation contract with the .!provenance declaration in the .saiv header:

.!saiv hub/log-entry
.!provenance:required

Three levels are supported:

Declaration Meaning
.!provenance:required Both ?sourceID and @timestamp must be present on every data line
.!provenance:source ?sourceID required; @timestamp optional
.!provenance:none Provenance prohibited (for schemas where it would be noise)

The declaration constrains only the source and timestamp components. The #dpid component is always optional and is never required or prohibited by .!provenance. A violation is a ProvenanceSchemaError, detected by the Validator against the compiled schema, not a lexer error — the lines are syntactically well-formed either way. The required and source levels are incompatible with optional fields: materialization (§2.6.17) synthesizes a .daiv line for every absent optional field, and a synthesized line carries no provenance — the pipeline could never produce a valid artifact. The schema compiler MUST reject the combination at compile time (ProvenanceSchemaError), the same static-rejection posture as SchemaOptionalWithoutDefaultError. Because the Validator reads the .csaiv, not the .saiv, the schema compiler propagates the .!provenance declaration verbatim into the .csaiv header, exactly as it propagates the strict modifier (§11). A .csaiv with no .!provenance header imposes no provenance requirement.

2.5 Variables and Field References

Variable interpolation enables DRY composition in authored kaiv text. Variables are temporary named values — scalars, arrays, or namespaces — defined with dot-prefixed hidden names and referenced with the $ dereference operator in values. They are resolved during Compiler canonicalization and completely elided from the canonical output. In addition to hidden variable references, kaiv supports field references that reference previously-defined data fields — distinguishable from variable references by the absence of . after $. A document can opt out of this machinery wholesale with the .!verbatim declaration (§2.5.6), which makes values fully verbatim — every $ literal, no references.

2.5.1 The Variable Name System

Variable names use a dot prefix (.name) — following Unix hidden-file convention (.bashrc is present but hidden from ls). The dot is part of the name itself. Structural sigils (@, /) come before the dot-name because they describe the container type, not the name’s visibility.

On the definition side (left of =), no $ — POSIX-aligned (shell: HOST=value):

Definition syntax Reference syntax Contains Available at Level
.name=value $.name scalar Level 0
@.name+=value / @.name;=a;b [email protected] array Level 1
/.name:=host=a|port=1 $/.name namespace Level 1

On the reference side (right of =, inside values), $ dereferences the hidden name — POSIX-aligned (shell: $HOST). The @ or / structural sigil follows $ and precedes the dot-name to identify the container kind.

The dot distinguishes a hidden name from data. .host is a hidden scalar; @.ports is an array with a hidden name; /.base is a namespace with a hidden name. No $ on the left side — $ is purely a value-side dereference operator. The Parser knows at definition time that dot-prefixed names are scaffolding and elides them from canonical output.

2.5.2 Namespace-Variable Splat

Scalar and array variable references are substituted inside values — scalars as text; an array variable holds elements, not text, so [email protected] is a splice, legal in exactly one position: as the entire right side of an append (+=) or extend (;=) line — targeting a visible array (/@mirrors;[email protected]) or another hidden array (@[email protected]). Expansion is element-wise: each element of the variable appends one element (one canonical line per element on a visible target). In any other position — mid-value, a scalar assignment, a struct-pair value — the reference has no text representation and is a VariableContextError11).

A namespace variable holds pairs, not text, so its reference $/.name is not a value substitution — it is a splat: the variable’s pairs are expanded as if they had been written out at the reference point. The splat form appears in exactly two positions:

  1. As the entire right side of a struct assignment (:= or +:=): /server/api:=$/.base expands to the same lines as writing the variable’s pairs inline; the target may itself be a hidden namespace variable (/.derived:=$/.base) (/server/api:=host=localhost|ssl=true). Subsequent lines may override individual expanded fields (last-write-wins, §2.5.4).

  2. As a standalone line inside an open section or namespace block: each of the variable’s pairs is expanded as if written as a key=value line at that point in the block. In the line classifier, such a line has no = and is recognized within rule 6 by its $/. leader (authored .kaiv only).

A namespace-variable reference in any other position — inside a scalar value, mixed with other text, or as a pair’s value — is a VariableContextError11.2): a namespace variable has no text representation to substitute.

2.5.3 Field References

In addition to hidden variable references ($.name), kaiv supports field references that reference previously-defined data fields — visible, schema-defined fields, not hidden variables.

Reference Syntax Discriminant from variables
Root-level field $field No . after $ (variables always have . after $)
Namespaced field $path::field Contains ::

The discriminant is unambiguous: the presence of . immediately after $ (or after $@/$/) identifies a hidden variable reference. The absence of . — a bare name or a path with :: — identifies a field reference.

Token boundaries. A reference embedded in a value ends deterministically (field-ref in §10): the token is the longest run of reference charactersALPHA/DIGIT/_, the separators / and ::, and @ where a step may begin (token start or after /) — with a dangling trailing separator excluded from the token. The first character outside the set ends the reference; the rest of the value is literal text (in log=$dir/today.txt the token runs to $dir/today, which is not a well-formed field reference — no :: projection — so the line fails rather than referencing $dir; separators bind tighter than intuition suggests, so restructure when in doubt, e.g. define a variable). Note that digits are reference characters, since indices are addressable: price is $5 off reads $5 as a root-field reference and is always an UndefinedReferenceError — a bare name cannot be all digits, so no root field ::5 can exist — a literal dollar is always written $$9.5) — except in a verbatim document (§2.5.6), where every $ is literal and price is $5 off means what it says. A reference without a leading / is the same path with one ($server::host$/server::host). Quoted names cannot appear in a reference: a field whose namepath requires quoting is not addressable by reference.

Behavior in Each Canonical Form

Construct In .raiv In .daiv
Variable definitions (.name=) Elided Elided
Variable references ($.name) Resolved Resolved
Field references ($path::field) Preserved Resolved (inlined)
Data fields Preserved Preserved
Type annotations Fully qualified Fully qualified

Resolution rules. Same left-to-right, no-forward-references rule as variables. The Compiler maintains a field table alongside the variable table. Every data field that is emitted into the output is added to the field table (keyed by fully-qualified namepath). When a field reference $path::field is encountered in a value:

Forward references and circular references are impossible by construction — the left-to-right rule ensures a field is always defined before it can be referenced.

2.5.4 Resolution Rules

  1. Document-scoped. Variables are visible from their definition to the end of the kaiv text.

  2. Left-to-right resolution. A variable can only reference variables defined on previous lines. No forward references, no circular dependencies.

  3. Templates can reference templates. A /. namespace variable can include $.name references in its values, which must already be defined. Resolution is sequential substitution, not recursive expansion.

  4. Override semantics. When a namespace variable is expanded and a subsequent line redefines a field, last-write-wins. This enables template-then-override patterns.

  5. Elision. All dot-prefixed definitions are removed from the canonical output. Only schema-defined data fields survive canonicalization.

2.5.5 The “Almost Verbatim” Principle

kaiv’s original design principle was that values are verbatim: what you write between = and end-of-line is exactly the value, with no escape sequences and no interpretation. Variable interpolation and field references introduce controlled exceptions:

The principle becomes: values are verbatim except that $.identifier (variable) or $field / $path::field (field reference) is expanded (in .daiv) or preserved (in .raiv), and $$ produces a literal $. In .daiv, all references are resolved — values are truly verbatim with no special characters.

This section states the default mode — and the .maiv right-side grammar builds on its doubling convention (§8.3), so the rules above stand on their own. The original unconditional principle is recoverable per document: a .!verbatim declaration removes the exceptions along with the reference machinery.

2.5.6 Verbatim Documents (.!verbatim)

A document may opt out of the reference machinery wholesale. The .!verbatim declaration — no arguments, at most once, placed with the other header declarations (§2.1) — makes a verbatim document, whose values are fully verbatim:

What stands between = and end-of-line is exactly the value — the original verbatim principle (§2.5.5) without exceptions. A short pipeline run:

.!kaiv
.!verbatim
price=$5
pattern=^abc$

compiles to the .raiv

.!raiv
.!verbatim
!str'::price=$5
!str'::pattern=^abc$

and denormalizes to the .daiv

.!daiv
!str'::price=$5
!str'::pattern=^abc$

The .daiv is byte-identical to that of the default-mode document authoring the same values as price=$$5 and pattern=^abc$$.

No reference machinery. A verbatim document declares that references do not exist, so defining or using the machinery is a contradiction, surfaced as a VerbatimContextError11.2). The ban is positional — it covers exactly the three positions where the machinery is recognized structurally, and never the content of value text:

  1. a variable-definition line: .name=, @.name+= / @.name;=, or /.name:=;

  2. a standalone namespace-variable splat line ($/.name, §2.5.2);

  3. a compound-form right side that is entirely a splice: a := / +:= right side $/.name, or a += / ;= value that is exactly [email protected].

A $ inside value text is never this error: in a verbatim document note=see [email protected] is a scalar whose value contains exactly those bytes. A whole value that would read as class 3 is authored through the standard escape hatch — restructure into the single-value key=value form (§9.5).

Survival. The declaration lives exactly as long as the machinery it disables. Field references are preserved in .raiv and resolved into .daiv2.5.3); correspondingly, .!verbatim is carried into .raiv by the Compiler — emitted within the leading declaration run, so .raiv values need no doubling either — and discharged by the Denormalizer: it does not appear in .daiv. A .daiv is already fully verbatim by kind (every reference resolved, every $ literal), so the declaration would be inert there — and admitting it would give one data text two .daiv forms, which the content-addressed uses of .daiv exclude. A .!verbatim encountered in .daiv — or in any kind other than .kaiv / .raiv — is diagnosed by the consuming stage (§2.1.1).

Pipeline behavior. The mode switches the resolution sub-passes off rather than reconfiguring them: the Compiler emits values untouched and enforces the positional ban; the Denormalizer, seeing the declaration in the .raiv header, performs no $$ collapse and no reference expansion. Both stages remain single-pass — declarations precede content (§2.1), so the mode is fixed before the first value in both .kaiv and .raiv. The Six Rules and the ' split are untouched: the mode changes what a value denotes, never how a line is classified. When re-authoring .kaiv from canonical form, a formatter MAY target verbatim mode (emit .!verbatim and raw values) instead of the default $$ doubling.

The same bytes, two readings. The mode is document-level, so identical authored bytes denote different values under different headers: price=$$5 is the two bytes $5 in default mode and the three bytes $$5 in a verbatim document — and price=$5, an UndefinedReferenceError in default mode (§2.5.3), is simply $5 in a verbatim one. Keep the header in view when copying lines between documents.

2.5.7 Worked Example — Variables

Authored kaiv

.!kaiv
.!schema:acme/cluster-config

# Template variables (elided from canonical output)
/.base_endpoint:=host=localhost|ssl=true
.default_timeout=30

# Data using templates
/server/api:=$/.base_endpoint
/server/api::port=8080
/server/api::timeout=$.default_timeout

/server/admin:=$/.base_endpoint
/server/admin::port=9090
/server/admin::timeout=$.default_timeout

[/@workers]
$/.base_endpoint
name=worker-1
port=7001
[/@workers]
$/.base_endpoint
name=worker-2
port=7002
[]

Canonical Output (.daiv)

.!daiv
.!schema:acme/cluster-config
!str'/server/api::host=localhost
!str'/server/api::ssl=true
!str'/server/api::port=8080
!str'/server/api::timeout=30
!str'/server/admin::host=localhost
!str'/server/admin::ssl=true
!str'/server/admin::port=9090
!str'/server/admin::timeout=30
!str'/@workers/0::host=localhost
!str'/@workers/0::ssl=true
!str'/@workers/0::name=worker-1
!str'/@workers/0::port=7001
!str'/@workers/1::host=localhost
!str'/@workers/1::ssl=true
!str'/@workers/1::name=worker-2
!str'/@workers/1::port=7002

All $.name references are resolved. All dot-prefixed definitions are elided. Overrides are applied (port values differ per namespace). The denormalized form is pure data.

2.5.8 Worked Example — Field References

Authored kaiv

.!kaiv
.!schema:acme/cluster-config

# Primary server (real data)
/server/api::host=localhost
/server/api::port=8080
/server/api::timeout=30

# Backup server (references primary via field references)
/server/backup::host=$server/api::host
/server/backup::port=9090
/server/backup::timeout=$server/api::timeout

Relational Output (.raiv) — Field References Preserved

.!raiv
.!schema:acme/cluster-config
!str'/server/api::host=localhost
!str'/server/api::port=8080
!str'/server/api::timeout=30
!str'/server/backup::host=$server/api::host
!str'/server/backup::port=9090
!str'/server/backup::timeout=$server/api::timeout

Denormalized Output (.daiv) — Field References Resolved

.!daiv
.!schema:acme/cluster-config
!str'/server/api::host=localhost
!str'/server/api::port=8080
!str'/server/api::timeout=30
!str'/server/backup::host=localhost
!str'/server/backup::port=9090
!str'/server/backup::timeout=30

In .raiv, the relational structure is preserved: $server/api::timeout makes the derivation relationship visible and auditable. In .daiv, the value 30 appears directly — the relationship is gone, but the file is self-contained and trivially validatable.

2.5.9 Architectural Impact

Variable interpolation and field references are an authoring-layer concern, with one exception: the Denormalizer’s $path::field resolution is also an authoring/build-time concern, not a runtime one.

2.5.10 Why .raiv Exists

The .raiv (relational canonical form) is the intermediate produced by the Compiler before the Denormalizer expands field references:

Property Detail
.raiv.daiv Straightforward — resolve all $path::field references left-to-right and materialize absent optional fields from the .csaiv[sec:default-values]); one pass, O(N) plus schema-sized memory
.daiv.raiv Impossible — the relational information is destroyed by denormalization; there is no way to recover which values were copied and which were original
Runtime consumption The certified runtime consumes only .daiv + .csaiv — never .raiv
Schema validation .raiv can be validated against a schema (field references are syntactically valid values)

Use cases for .raiv: meaningful diffs (a single source-of-truth change shows as one changed line, not many), referential-integrity-by-construction in tooling, schema-evolution tracking, round-trip fidelity for .kaiv ↔︎ .raiv, and converters that target relational formats.

2.6 Type System

2.6.1 The Single Primitive: str

kaiv has exactly one primitive type: str. Every value between = and end-of-line is a string of characters. There is no binary form at any stage — not at authoring time, not at canonical time, not at schema time, not at validation time.

str is the identity type: no pattern constraint (any string matches) and ..lex span ordering by default. !str in a type annotation means “raw string with no additional constraints.”

Every other type — including int, float, bool, null, b64 — is a named type defined in terms of str plus constraints. Named types live in type library files (.taiv); the standard library std/core is always implicitly imported and supplies int, float, bool, null, and b64. The ! prefix marks a type annotation in canonical form (!int, !str, !std/net/port); the & prefix marks a named-type reference in authored form (&int, &port), which the Compiler resolves to the canonical !library/path/typename form. For std/core types, the short name is preserved in canonical form (!int, not !std/core/int).

2.6.2 Unannotated Scalars Canonicalize to !str

The type annotation the Compiler writes reflects the type asserted at authoring time — an explicit !type or &name annotation on the source line. An authored scalar carrying no annotation canonicalizes to !str, the identity type. Canonicalization performs no type inference from a value’s lexical shape: port=8080, ssl=true, and flag=null all compile to !str'…::…=8080 / =true / =null unless the source line is explicitly annotated (!int, !bool, &null, …). This follows directly from str being the single primitive and values being verbatim (§9.5) — there is no lexer or compiler stage that examines a value to guess a narrower type. .raiv therefore always carries the authored type.

Schema-declared types reach data lines by declaration, never inference — and only in the deployment artifact. When the document declares a schema, the schema-aware Denormalizer lifts each untyped (!str) line to the field’s retained head type from the .csaiv7.3), so the .daiv line itself carries the schema’s statement of what the field is (§2.6.17; for !text heads the lift is the guarded str→text coercion, §2.6.4). The lift fills in what the author left unsaid; it never overrides an assertion — a data line whose annotation conflicts with its field’s head is a TypeMismatchError7.1), not a rewrite — and in a schemaless document the .daiv line keeps !str unchanged.

2.6.3 The std/core Standard Library

All “built-in” types are defined in std/core.taiv — the standard type library that is always implicitly imported. It is hosted at ktaiv.com for implementers to reference, but since it is frozen (never changes after initial publication), implementations SHOULD ship it embedded or bundled rather than downloading it at runtime. It is part of the library, tool, and parser distribution.

.!taiv std/core

// Integer --- decimal string with numeric ordering
/^-?[0-9]+$/ ..num
&int=

// Floating-point number --- decimal string with numeric ordering
/^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$/ ..num
&float=

// Boolean --- two-valued enumeration
{true,false}
&bool=

// Null --- empty value only
/^$/
&null=

// Base64url-encoded binary data (RFC 4648 section 5, unpadded)
/^([A-Za-z0-9_-]{4})*([A-Za-z0-9_-]{2,3})?$/
&b64=

// Multi-line text in readable form --- the value is a |:|-separated
// sequence of lines. The separator is interpreted only at the
// application/export layer; within kaiv the value is verbatim.
&text=

The b64 pattern admits exactly the encoded lengths unpadded base64url can produce — ≡ 0, 2, or 3 (mod 4) — so an impossible-length value fails the type itself, consistent with the decoded-byte translation of length constraints (§2.8.2). Trailing-bit canonicality (RFC 4648 §3.5 pad bits) is deliberately not checked: rejecting non-canonical pad bits is a decoder MAY in the RFC, and kaiv does not out-strict the ecosystem it interoperates with. The quad-form pattern stays inside the pinned regex dialect and the empty value (zero bytes) still matches.

For std/core types, !int, !bool, etc. are canonical shorthands — they remain as !int, !bool in canonical form (they do NOT expand to !std/core/int). For all other named types, !library/path/typename is the fully-qualified canonical form that replaces &name authoring annotations.

Written (authoring) Canonical form Note
!int !int std/core shorthand — stays as !int in canonical form
!str !str Identity type (no pattern, ..lex default)
!bool !bool std/core shorthand — stays as !bool
!float !float std/core shorthand — stays as !float
!null !null std/core shorthand — stays as !null. Null values always have an empty payload after =: !null'::field=. Distinct from !str'::field= (empty string). See §2.6.17.
!b64 !b64 std/core shorthand — stays as !b64
!text !text std/core shorthand — stays as !text. Multi-line text in readable form; see §2.6.4.
&port !std/net/port Resolved from std/net.taiv — library path std/net, type port
&datetime !std/time/datetime Resolved from std/time.taiv — library path std/time, type datetime
&customerid (from acme/ourtypes) !acme/ourtypes/customerid Resolved from acme/ourtypes.taiv — library path acme/ourtypes, type customerid

2.6.4 The text Type

Multi-line text in readable form. A !text value is a sequence of lines joined by the fixed separator |:| — the value’s segments, split on every occurrence of the separator, are the text’s lines, joined by newlines:

!text
basho=old pond|:|a frog jumps in|:|sound of water

canonicalizes to !text'::basho=old pond|:|a frog jumps in|:|sound of water and exports — to JSON, YAML, TOML, or any other target — as the three-line string. The rules:

2.6.5 The std/enc Encoding Library

kaiv values are single-line: readable multi-line text travels as the core !text type (§2.6.4), and everything else — binary content, text the |:| separator cannot carry, structured payloads — embeds as !b64. Plain !b64 says nothing about what the decoded bytes are; the std/enc library types the payload — each member is a named type derived from !b64 whose name states the decoded content:

.!taiv std/enc

// JSON document
!b64
&json=

Members: bin (generic binary), plain (multi-line UTF-8 text — the embedded fallback for content the !text separator cannot carry), json, yaml, toml, xml, html, md, csv. Usage:

.!kaiv
.!types std/enc

&json
config=eyJvbiI6dHJ1ZX0

canonicalizes to !std/enc/json'::config=eyJvbiI6dHJ1ZX0. Like std/core, implementations SHOULD ship std/enc embedded; unlike std/core it is not implicitly imported — documents opt in with .!types std/enc (only std/core gets the implicit import and the short canonical names). Private payload types are ordinary type libraries deriving from !b64 (acme/enc/product), resolved through the registries like any other.

Lineage. This restores the era-1 !b64 TYPE encoded-type parameter (!b64 json, !b64 x/ProductDetails) as ordinary named types — the parameter dissolved into the type system rather than surviving as special grammar. Since every type is a constraint triple, “b64-of-JSON” is just a name for the b64 constraint with a documented payload interpretation; validators check the base64url shape and nothing more (payload interpretation is the application’s concern, exactly as era 1 specified).

2.6.6 The std/time Time Library

RFC 3339 temporal shapes with chronological (..time) span ordering, mirroring TOML’s four datetime flavors — the library behind the &datetime!std/time/datetime resolution example used throughout this document:

.!taiv std/time

// Offset date-time: 2026-07-03T21:00:00Z
/^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/ ..time
&datetime=

Members: datetime (offset date-time), localdatetime, date, time. Like std/enc, implementations SHOULD ship std/time embedded, and it is imported explicitly (.!types std/time); canonical identity is the full path (!std/time/datetime). Range constraints ([2026-01-01,2026-12-31]) work at every Level: ..time compares instants, offset-aware (§2.6.12), so mixed-offset documents validate chronologically rather than bytewise.

2.6.7 The std/num Numeric Markers Library

kaiv floats are deliberately finite: the core float pattern admits only finite decimals, which keeps the ..num span a total, decidable order — range checks never meet IEEE non-finite semantics. The non-finite markers are therefore dedicated enum types, mirroring null-as-a-type:

.!taiv std/num

// Signed infinity
{inf,-inf}
&inf=

// Not-a-number
{nan}
&nan=

An extended-real field is the same idiom as a nullable one — a union whose compiled form carries each alternative’s group:

!float|std/num/inf        →   !float(/…float…/..num)|std/num/inf({inf,-inf})'::x?=

The canonical marker spellings are inf, -inf, nan (format converters map TOML’s inf/nan and YAML’s .inf/.nan onto them). Like the other marker libraries, std/num SHOULD ship embedded and is imported explicitly (.!types std/num).

2.6.8 The std/net Network Identifiers Library

Network identifiers are the validation everybody re-implements; std/net pins them to their defining documents. Five types, each a pure refinement of str:

The uri, url, and email patterns are the reason the dialect carries \x2710): apostrophe sits in RFC 3986’s sub-delims and in the e-mail local-part alphabet, and \x27 is its only expressible spelling. Schema converters map JSON Schema’s format: values (uri, email, hostname) and XSD’s xs:anyURI onto these types instead of dropping them. Like the other libraries, std/net SHOULD ship embedded and is imported explicitly (.!types std/net).

2.6.9 The std/math Mathematical Types Library

std/math holds mathematical value types beyond the core scalars; its first member is complex:

// Complex number --- a+bi, both components always present.
/^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?[+-][0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?i$/
&complex=

One spelling per value: both components are always present (3+0i, 0+1i, never a bare real or a lone 2i), the separator sign is the imaginary part’s sign (-1.5-0.5i; 3+-2i is ill-formed), no whitespace, and the i suffix is mandatory. Components use the float token grammar — integer spellings included, so Gaussian integers (3+2i) are expressible without a dedicated type.

complex deliberately carries no numeric span: the complex numbers admit no total order compatible with their arithmetic, so ordering stays the default byte order — deterministic for uniqueness checks, meaningless for ranges, which is exactly the mathematical situation. Range constraints on complex are therefore inert; constrain components by pattern instead. Like the other libraries, std/math SHOULD ship embedded and is imported explicitly (.!types std/math).

2.6.10 Standard Library Evolution

Registries are append-only and entries are permanent: a published library file never changes. A standard library therefore evolves by new name, never by republication — a revised network library is published as std/net2 beside the eternal std/net, and .maiv mappings bridge the two. The old name keeps serving every document that ever referenced it; nothing published against it can break.

std/core is the one library this convention cannot reach: it is unversionable by construction. Its members resolve to the canonical shorthands (!int, !text, …) that appear in every canonical document without a library path — core is implicitly imported everywhere, so no core2 could slot into existing documents, and any change to core’s membership is a change to canonical form itself. The membership of std/core frozen at 1.0 is therefore final in a stronger sense than any other library’s; altering it is major-revision territory.

2.6.11 The Constraint Triple

The universal type constructor is the constraint triple (pattern, span, range). Every type is str narrowed by zero or more of:

For example, int (in std/core) is defined as str + /^-?[0-9]+$/ + ..num: a string matching the integer regex, ordered numerically. A port number type adds a range: the constraint line &int [0,65535] above the definition line &port=.

Two additional constraint kinds compose with the triple:

2.6.12 Span Orderings

The span ordering declared on a type determines how range constraints [min,max] are evaluated. The built-in spans are a small, fixed, certifiable set:

Span Meaning
..num Decimal numeric ordering — parse string as number, compare mathematically.
..lex Lexicographic (byte-by-byte) ordering. The default for str.
..lex[locale] Locale-aware lexicographic ordering. Level 3 only; BCP 47 language tag selects collation rules.
..time RFC 3339 chronological ordering — instants, offset-aware.
..ver Version ordering — dot-separated components, numeric where both parse.

A range constraint is meaningful only relative to a span: [1,65535] on &int (..num) means numeric comparison; [1,65535] on a ..lex type would compare strings lexicographically. Schema authors normally never have to think about this — the span is set on the named type and inherited by any range constraint that uses it.

The comparison function each span ordering uses:

Span Comparison Implementation
..num Numeric parse + mathematical comparison parse_int(a) <= parse_int(b)
..lex Byte-by-byte memcmp(a, b)
..lex[locale] Locale-aware collation ICU ucol_strcoll() or equivalent (CLDR 48, tertiary strength; §5.3)
..time RFC 3339 chronological Instant comparison, offset-aware (see below)
..ver Dotted version Component-wise comparison (see below)
Numeric domain of ..num.

parse_int/parse_float above is schematic. Normatively: for a value constrained by an int-derived type, ..num comparison is exact integer comparison (arbitrary precision — no silent truncation at 2^53); for a float-derived type it is IEEE-754 double comparison. A value that is syntactically valid for its type but outside the representable range, or a non-finite token (NaN, inf), fails the type’s own pattern constraint first (e.g. NaN does not match the float pattern) and so never reaches the range check. Range endpoints are parsed in the same domain as the value.

Temporal domain of ..time.

..time comparison is chronological over instants: operands in the RFC 3339 shapes are parsed and compared as instants, with UTC offsets normalized — 2025-12-31T23:00:00+01:00 and 2026-01-01T00:00:00+02:00 are equal, and a range check meeting a differing offset compares the instants, not the bytes. Within a single fixed offset this coincides with byte order. An operand outside the RFC 3339 shapes falls back to byte comparison — a ..time type’s own pattern normally excludes that case before any range check runs. The parse is constant-memory; no calendar library is required. (Pinning string comparison instead was considered and rejected: it would freeze a cross-offset footgun whose later fix would be validation-semantics drift on eternal artifacts.)

Version domain of ..ver.

..ver comparison is component-wise over dot-separated components, left to right: a component pair that both parse as unsigned integers compares numerically (1.10 > 1.9); any other pair compares bytewise; and when one operand exhausts its components first, the shorter orders lower (1.2 < 1.2.0). This is deliberately not full SemVer — there are no prerelease or build-metadata semantics; a domain needing them defines a named type whose pattern pins the accepted shape.

Performance is identical to a primitive-based approach. The Validator reads the string "8080" from the .daiv line and validates against the compiled constraints (pattern, span, range) from the .csaiv. Implementations MAY — and SHOULD — hardcode optimized validators for std/core types as an optimization. The spec defines the semantics; the implementation may recognize !int and skip the regex match in favor of a direct integer parse.

2.6.13 Named Types

The Named Type System is kaiv’s type system. All types except str are named types — types defined in type library files (.taiv) as str plus optional constraints. This includes the “built-in” types (int, float, bool, null, b64) which are defined in std/core.taiv. The design follows the #include <stdint.h> model from C, but taken further: even int itself is a library type, not a language primitive.

The key principle: one Lexer, same line grammar everywhere. Type library files (.taiv), schema files (.saiv), and data files (.kaiv / .daiv) are all processed by the same Lexer with the same metadata-above / definition-below line grammar.

Type Library Files (.taiv).

A type library file (.taivtype kaiv, pronounced “tave”) defines a set of named types. Each type definition follows the same pattern as every other definition in the kaiv family: metadata lines above, definition line below.

The format declaration for a .taiv file is .!taiv, paralleling .!saiv for .saiv files. The optional version and the library identifier (acme/net) follow the same convention as other format declarations.

Example — a domain type library (acme/net.taiv)

The example library below is fictional — deliberately distinct from the shipped std/net2.6.8), whose normative membership and RFC-exact patterns it does not reproduce:

.!taiv acme/net

// IPv4 address in dotted-decimal notation (simplified pattern --- production use would validate 0--255 per octet)
/^(\d{1,3}\.){3}\d{1,3}$/
&ipv4=

// IPv6 address
!str
&ipv6=

// Network port number (inherits &int range semantics via ..num)
&int [0,65535]
&port=

// URI (RFC 3986)
!str
&uri=

// Email address (simplified pattern --- production use would follow RFC 5321 more precisely)
/^[^@]+@[^@.]+\.[^@]+$/
&email=

Each type definition consists of:

  1. Optional // doc comment(s) above

  2. Constraint line(s): /regex/ pattern, ..span ordering, {enum} enumeration, or a base named type reference (!str, &int, etc.) plus any narrowing constraints — any combination on one or more metadata lines

  3. Definition line: &name= — the named type being defined, optionally with a default value (&name=defaultvalue)

The &name= definition line is a KV token where the key starts with &. It is the same token kind as field= in a schema — the & prefix distinguishes a type definition from a field definition. A .taiv file is a kaiv document: lexed by the same Lexer, same comment syntax, same line structure, same ordered-keys rule. Its content happens to be type definitions rather than data.

The & Sigil.

The & sigil identifies named type references in authoring files. It appears in two positions:

Position File type Syntax Meaning
Definition position .taiv &name= / &name=default Defines a named type (the definition line, analogous to field= in schemas)
Annotation position .saiv, .kaiv &name on a metadata line above a field/data line Annotates a field or value with a named type (same position as !int, !str, etc.)

& is an authoring-level sigil only. It does not appear in canonical form (.daiv) or compiled schemas (.csaiv). During Compiler canonicalization, every &name annotation is resolved to !library/path/typename — the fully-qualified canonical form:

The resolution rule for !library/path/typename:

& joins +=, ;=, :=, and dot-prefixed variable definitions as authoring sugar that canonicalizes to a more explicit form.

! vs. & at a Glance

Prefix Where it appears Meaning
! .daiv (canonical data), .csaiv (compiled schema), .kaiv/.saiv (as shorthand for std/core) Canonical type reference. !str = identity type. !int, !bool, !float, !null, !b64, !text = canonical std/core forms (not expanded further). !library/path/name = fully-qualified for all other types.
& .kaiv (authored data), .saiv (authored schema), .taiv (type definitions) Short-form type reference. Resolved against imported type libraries declared via .!types. In canonical form, &name!library/path/name (or !core-shorthand for std/core types). Never survives canonicalization.
Type Library Import (.!types).

A schema imports type libraries with the .!types declaration. It follows the same pattern as .!schema declarations: placed after the format declaration, before content lines. Multiple .!types declarations are allowed.

.!saiv https://example.org/server-config.saiv
.!types std/net
.!types std/time

The .!types declaration is the kaiv equivalent of #include <stdint.h> in C. It tells the schema compiler to load the named type library and make its named types available for & annotation resolution.

The resolution of .!types library paths follows the same layered resolution as type annotations (see §2.1.4). Type libraries form a one-directional dependency: .taiv files reference only str (the primitive) or other named types from already-imported libraries. Schemas reference type libraries. Data files reference schemas. No circular imports are possible. std/core is implicitly available everywhere — it does not need to be declared with .!types.

Default Values.

A schema field definition is a complete caiv content line, and its right side is the field’s default value. This mirrors the data layer exactly: in a .kaiv file, key= assigns the empty string — a kaiv value is never absent, only empty — so in a .saiv file, key= declares the empty-string default. Every field therefore carries a default; the empty string is the degenerate one, and there is no “no default” state to represent.

Types carry defaults too. A named-type definition is also a complete content line, and its right side is the type’s default: &port=443 in a .taiv defines a port type defaulting to 443. Every level of the definition chain — the schema field, its type, that type’s base, transitively — therefore holds a default slot, and the schema compiler resolves them as a cascade:

The applicable default is the most specific one that satisfies the field’s own constraints: the field’s, else the type’s, else the base chain’s, else the (inert) empty string.

Because the empty string fails every constrained type’s own pattern, inheritance needs no syntax — a bare timeout?= under &port inherits 443 precisely because its own "" default is inert, while an explicit admin?=9090 wins by being both more specific and applicable. The honest edge: for an unconstrained str field the empty string is a valid value, so it shadows any type default; a constrained string type (any pattern or length) behaves like the typed cases.

The compiled schema carries the resolved default. The schema compiler runs the cascade at compile time and bakes the winner into the .csaiv right side (…::listen?=443), so consumers of the compiled artifact — the form the registries serve — can materialize defaults without fetching the authored sources. The csaiv-field-line right side is thus [ value ], empty when the resolved default is the empty string.

Two rules keep defaults orthogonal to validation:

An optional field whose resolved default is inapplicable and whose type does not admit !null would leave the Denormalizer with nothing to materialize when the field is absent; the schema compiler rejects such a declaration (SchemaOptionalWithoutDefaultError, §11.2).

Named Types in Schemas.

In a schema (.saiv), &name appears on the metadata line above a field definition — the same position as !int or !str:

.!saiv https://example.org/server-config.saiv
.!types acme/net
.!types std/time

// Server hostname
&ipv4
host=

// Server port
&port
port=

// When the server was configured
&datetime
created_at=

The &ipv4 line sits in the exact same position as !str would — it is a type annotation metadata line that applies to the immediately following field definition line. The schema compiler resolves &ipv4 against the imported type libraries (acme/net) to retrieve the constraints (pattern /^(\d{1,3}\.){3}\d{1,3}$/) and lower them to the compiled form in the .csaiv.

Named Types in Data Files. In authored data files (.kaiv), &name appears in the same metadata position as !type, above the data line:

.!kaiv
.!schema:acme/server-config

&datetime
created_at=2025-01-15T09:30:00Z

&ipv4
host=192.168.1.1

&port
port=8080

When the schema already supplies type information, the explicit & annotation in data files is optional — the same relationship as !int in a schema vs. authored data. When present, the annotation is checked against the compiled field’s retained head type (§7.3) under the parallel scan’s type-check rules (§7.1): nominally where the head’s name is its semantics (a union, !text, !null, std/enc/*, an explicit !str head), structurally everywhere else — a chain-compatible annotation such as !int under a !std/net/port head is accepted, and the head’s constraint group governs the value either way. Only a field whose head is the elided identity str carries no type item; there the annotation is trusted as carried documentation — the value’s conformance is enforced by the constraint check, and downstream converters key on the annotation as written.

In canonical form (.daiv), &name is resolved: std/core types become their canonical shorthand (!int, !bool, etc.), all other types become !library/path/typename. So the above becomes:

.!daiv
.!schema:acme/server-config
!std/time/datetime'::created_at=2025-01-15T09:30:00Z
!acme/net/ipv4'::host=192.168.1.1
!acme/net/port'::port=8080

No & appears in the canonical output.

Constraint Narrowing. A schema can add further constraints on a field that uses a named type, narrowing the base definition:

// In std/net.taiv --- base definition:
!int [0,65535]
&port=

// In my-schema.saiv --- narrowed for this field:
&port [1024,65535]
listen_port=

The &port [1024,65535] annotation metadata line narrows the base [0,65535] range. This is the same constraint-narrowing mechanism as !int[1024,65535] narrowing plain !int — the named type annotation can carry additional inline constraints on the metadata line that tighten (but never widen) the base definition from the type library.

Anonymous Refinement (Bare Constraint Lines).

A .saiv metadata line consisting solely of value-constraint items — pattern, span, range, enum, length — refines the implicit str type of the next field definition:

// Equivalent to !str/^[a-z]+$/#[1,8] on one line:
/^[a-z]+$/ #[1,8]
name=

This is the .taiv definition shape (§2.6.13) applied to a field instead of a &name= definition — the natural reading for a kaiv author, since str is the universal value type and every constraint already refines it. The compiled form carries no type item: the field lowers to a bare constraint group, which the Validator evaluates without a nominal check. This is the deliberate contrast with an authored !str annotation carrying the same items — the identity declaration, retained as a nominal head (§7.3): write !str to reject asserted types on the field, write the bare constraints to tolerate them. Type-reference items (!type, &name) are not admitted on a bare constraint line — a line carrying a type reference is authored as a type-annotation or named-annotation line, which lead with their own sigils. A schema compiler encountering a rule-6 metadata line it cannot interpret — a bare line with a type-reference item, or any other leader with no .saiv meaning — MUST reject it; silently dropping an annotation would weaken the compiled contract relative to the authored one.

Why Named Types Resolve to Fully-Qualified Form in Canonical Output. Named type annotations are resolved to fully-qualified form (!library/path/typename) in .daiv because:

Exporters Resolve Custom Heads.

That lookup is specified: a format exporter interpreting a custom head (a library-path type outside the built-in std tables) resolves it, in order, through (1) the type registries — the standard Layer 1–4 resolution, so vendored local libraries serve offline devices exactly as they serve validation; and (2) the document’s declared .!schema, whose compiled form already carries every field’s lowered base kind — a device holding the .csaiv for validation already holds everything an exporter needs, with no further resolution. A chain whose lowering carries the ..num span exports as the target format’s number; a lowered {true,false} enum as its boolean. Interpretation is best-effort and total: a head neither source can interpret — and a value that does not fit the resolved kind — exports as the string the value already is, never an error. Resolution refines; it cannot reject. (Implementations expose the uninterpreted form as an option — the reference CLI’s --no-resolve.)

The compiled .csaiv schema carries the lowered form — constraint pattern + span + range/enum — for the Validator’s parallel scan. The runtime never sees & references; it sees only constraint forms. Type identity (the fully-qualified !library/path/name) is carried in the .daiv data file for tooling and format conversion; constraint forms (patterns, spans, ranges) are carried in the .csaiv for validation.

2.6.14 Tagged Unions (oneOf)

Schema syntax. !int|str — pipe-separated type alternatives.

Data text MUST include an explicit type annotation choosing one alternative. The type annotation on a data line is the discriminant that determines which constraint set is applied during the Validator’s parallel scan validation.

!null|T is the nullable pattern: a field that can be either null or a value of type T is declared as !null|T in the schema. The canonical data line carries the active variant — !null'::field= when null, !T'::field=value when non-null. See §2.6.17 for the complete null model.

Units on alternatives. An alternative carries its own unit exactly as a non-union annotation does — the unit attaches to the alternative it follows and is part of that alternative’s identity:

!null|float:km        →   !null(/^$/)|float:km(/…float…/..num)'::dist?=

!null|float:km is the nullable-quantity pattern — the shape the optionality interlock forces for an optional measurement with no applicable default (§[sec:default-values]): the canonical line carries !float:km'::dist=42 when present and !null'::dist= when null. The union itself never carries a unit; each alternative’s unit, or its absence, participates in the discriminant (§7.1).

Maps to:

Target Construct
ProtoBuf oneof
GraphQL union
ASN.1 CHOICE
JSON schema oneOf

2.6.15 Schema Composition (allOf)

Schema syntax. Multiple .!schema declarations in the same kaiv text.

The data MUST satisfy all referenced schemas simultaneously. Composition operates at the schema level, not the data level — each referenced schema contributes field definitions to the compiled schema (.csaiv).

Maps to:

Target Construct
GraphQL implements (interface implementation)
ASN.1 component inclusion
JSON schema allOf

2.6.16 anyOf

No distinct syntax is needed. anyOf is subsumable under the constraint system and does not require a first-class language construct. None of ProtoBuf, GraphQL, or ASN.1 has a native anyOf concept, so there is no target format to drive a dedicated syntax for it.

2.6.17 Null Semantics

This section formalizes the null model in kaiv: what null means, how it is represented in canonical form, how nullable fields are declared in schemas, and how null interacts with defaults and field absence.

The Canonical Representation: !null'::field=.

A null value in canonical form is:

!null'::field=

The type annotation is !null and the value after = is always empty. The Validator MUST reject !null'::field=something — null carries no payload. The /^$/ pattern constraint on &null in std/core.taiv enforces this: the value must be the empty string. Inside a !null|T union the same holds: the null alternative’s compiled group carries /^$/, so a non-empty !null payload fails validation (§2.6.14).

Null vs Empty String — The Type Annotation Disambiguates. The distinction between null and empty string is entirely in the type annotation:

Line Meaning
!null'::field= Null — the field has no value
!str'::field= Empty string — the field has a value, and that value is ""

Both lines have nothing after =. They are semantically different because of the type annotation. This is a direct consequence of kaiv’s “every value has an explicit type” principle.

Nullable Fields Require Explicit !null|T Declaration.

A field is nullable only if its schema declaration includes !null in a union type. Nullability is explicit and opt-in — never implicit.

# In .saiv schema:

# nullable string --- can be null or any string (including empty)
!null|str
name=

# required non-nullable string --- must have a string value
!str
host=

# nullable integer --- can be null or an integer
!null|int
timeout=

A field declared as !str cannot hold null. Only fields declared with !null|T (or !null|T1|T2|...) can hold null.

Active-Variant Annotation in Canonical Form. The full union type !null|str is a schema concept (it appears in .saiv and .csaiv). A canonical metadata prefix (§10) has no union form: the data line carries only the active variant — the concrete type of the actual value. A union annotation on an authored data line is sugar, and the Compiler MUST resolve it per emitted value (a splice’s elements may each pick a different alternative) to the first alternative — the head type first, then left to right — whose lowered definition (base type plus authored narrowing) the value satisfies; a value satisfying no alternative is TypeMismatchError at compile time. Order therefore matters: an unconstrained str alternative accepts everything, so it belongs last.

# Schema declares: name is !null|str

# When the value is null:
!null'::name=

# When the value is a non-empty string:
!str'::name=hello

# When the value is an empty string (NOT null):
!str'::name=

This is consistent with how all union types work in canonical form: the canonical line always carries the concrete type, not the union declaration. The parallel scan validator checks that the data line’s type annotation is one of the allowed union alternatives declared in the .csaiv.

Materialization of Absent Fields. A field that is absent from the authored .kaiv has a schema-dependent outcome, applied by the Denormalizer at build time (§[sec:default-values]):

Situation .daiv output
Field present with value !type'::field=value
Field explicitly null (!null annotation above an empty-valued line) !null'::field=
Field absent, applicable default (§[sec:default-values]) !type'::field=default_value
Field absent, optional, no applicable default, nullable (!null|T) !null'::field=
Field absent, required (=) Build error — the Denormalizer raises RequiredFieldSchemaError

The Denormalizer emits !null'::field= for absent nullable fields and the resolved default for absent defaulted fields. This preserves the “every schema-declared field appears in .daiv” invariant required by the parallel scan. (The remaining combination — optional, no applicable default, non-nullable — is rejected when the schema itself is compiled: SchemaOptionalWithoutDefaultError, §[sec:default-values].)

Two consequences of working from the compiled schema: the !type token of a materialized line is the .csaiv field’s retained head type (§7.3) — !int, !type:unit, !acme/net/port, or the matching union alternative — and !str only for a head-less identity field. The same head is lifted onto present untyped lines: a !str data line on a typed field is emitted into .daiv under the head type (the schema type wins in the deployment artifact; for !text heads this is the guarded str→text coercion, §2.6.4), so exporters and every other downstream consumer read the type from the line itself, never from the schema. And inside a namespace array, materialization applies per element: each element run must present the full schema-declared field sequence for the strict lockstep scan, so an element’s absent optional fields are materialized within that element’s run.

Explicit null in authored .kaiv may also be written as:

!null
name=

The !null type annotation above the data line explicitly marks the value as null. The Compiler canonicalizes this to !null'::name=.

Every Schema-Declared Field Appears in .daiv — No Absent Lines.

The parallel scan between .daiv and .csaiv requires every schema-declared field to have a corresponding line in .daiv. There are no “absent” lines in canonical form: the Denormalizer materializes every schema-declared field (§[sec:default-values]), in schema-declared order. This preserves the pure lockstep parallel scan — no schema lookup and no branching logic for absent fields is needed at runtime, which is precisely what makes .daiv self-contained for embedded and safety-critical consumers.

Default Values and Nullability Are Independent

Schema declaration Outcome when the field is absent in .kaiv
!str + field= (required, non-nullable) Build error — RequiredFieldSchemaError
!str + field?= (optional, unconstrained str) Empty-string default applies (§[sec:default-values]): !str'::field=
!int + field?= (optional, constrained, no applicable default) Schema rejected at compile time — SchemaOptionalWithoutDefaultError
!str + field?=default_value (has default) Default materialized: !str'::field=default_value
!null|int + field?= (nullable, no applicable default) Null materialized: !null'::field=
!null|str + field?=default_value (nullable, has default) Default materialized: !str'::field=default_value

2.6.18 Map Type

Maps are collections of key-value pairs where keys are arbitrary strings and values conform to a declared type. Every major interchange format supports maps:

Format Map construct
Avro map
JSON object with dynamic keys
ProtoBuf map<string, V>
GraphQL not a first-class type; represented as custom scalars or key-value list types
TOML inline table with dynamic keys

In kaiv, maps are represented using the !map type annotation. A map field declares its value type in the schema and is populated with arbitrary string keys at the data layer. Unlike the other core-type keywords, map is a structural type constructor, not a std/core named type — it is not defined as str plus constraints; its semantics are the namespace-with-arbitrary-fields construction below.

Authored Syntax

!map
/config/settings={}

Or with inline key-value pairs (key and value separated by :, pairs separated by ;):

!map
options=key1:val1;key2:val2

Schema Declaration

In a .saiv schema, !map<VALUETYPE> is a type annotation like any other — a metadata line above the field definition, in the same position !str or &ipv4 would occupy:

!map<str>
settings=

This declares settings as a map whose values are strings.

Canonical form

A map is a namespace with arbitrary string-named fields — the same construction as an array, which is a namespace with integer fields. In .daiv, each map entry is one canonical line projecting the entry key as a field of the map’s namespace:

!str'/config/settings::key1=val1
!str'/config/settings::key2=val2

Every map-entry line therefore ends with ::key=value like all other canonical data lines — the universal :: rule (§1.4.2) has no map exception. A key that does not match the bare-name grammar is quoted per §2.3 (!str'/config/settings::"weird key"=val). An empty map (authored ={}) produces no canonical entry lines.

This keeps the canonical form flat and DFA-walkable, consistent with the treatment of namespaces and arrays. The schema DFA validates each map entry’s value against the declared value type. The key is unconstrained (arbitrary string) unless a key-pattern constraint is declared in the schema.

Because : separates key from value and ; separates pairs, neither character can appear literally in an inline map key or value — there are no escape sequences (§9.5). Entries containing these characters are authored as direct namespaced field lines instead (/config/settings::key1=a:b;c, with the key quoted per §2.3 if needed) — the value after = is verbatim, so any character is representable there.

2.7 Units

Numeric types may carry a unit annotation declaring the physical or economic quantity the value represents. The unit attaches to the type sigil with a : separator and lives inside the metadata prefix, before any provenance and before the ' boundary:

!float:km'/server::distance=42
!int:s'::timeout=30
!float:km/h'/vehicle::speed=80
!float:~EUR'/order::total=99.50

Units are restricted to types whose span ordering is ..num!int, !float, and any named type derived from them. Annotating a ..lex-, ..time-, or ..ver-ordered type (including !str, !bool, !null, !b64) with a unit is a compile-time error. In authored annotations the type name may be elided — !:km — inheriting the field’s schema head, or float where none governs (§2.7.4).

2.7.1 Metadata Prefix Order

The full canonical metadata prefix order is:

!type[inline-constraints]:unit?provenance'namepath

For example, !int[1,3600]:s?sensor1'/timeout::value=30 declares an integer in seconds, range-constrained to one hour, sourced from sensor1. Inline constraints stack with units exactly as they stack with provenance: each component is independently optional, and the order is fixed.

Two examples with every slot populated, showing the fixed DFA ordering end to end:

!float[0,100]:km?gps1@2025-01-15T09:30:00Z#p-17'/trip::length=42.5
!str/[a-zA-Z0-9.-]+/#[1,253]?dns1@2025-01-15T09:30:00Z#req-42'/server::host=example.com

The first line is a numeric type with an inline range, a unit, and the full provenance triple. The second is a string type with a pattern and a length constraint (length constraints sit inside the inline-constraints slot, before :unit and ?provenance), then the full provenance triple. In both, everything before ' is one whitespace-free token; the # in #[1,253] is a length constraint because it appears before any ?, and the # in #p-17 / #req-42 is a data point identifier because it appears inside the provenance list (§2.4.2).

The / inside a compound unit expression is unambiguous because units always precede ' and the namepath always follows it — the DFA reads from the : after the type up to the ? of a provenance list or, absent one, the ' delimiter, as a single unit token, with no context-sensitive parsing (? cannot occur inside a unit expression).

2.7.2 Validation: Units Do Not Convert

The Validator checks two things about units:

  1. The unit string parses as a known unit expression — built-in, or defined by a .faiv library imported with .!units2.7.10).

  2. The data line’s unit string is byte-identical, in canonical form, to the unit declared by the schema for that field. The unit rides the retained type token in the compiled schema (!float:km as the field’s first item); a mismatch — or a missing unit — is a TypeMismatchError (the unit is part of the type’s identity). For a union field the unit rides the matched alternative2.6.14); an alternative without a unit demands none.

It does not convert values across units. !float:km'::d=42 and !float:m'::d=42000 are different canonical lines with different literal payloads, and the Validator treats them as such. Conversion belongs to the build’s authored-unit conversion (§2.7.3) and to consumer tooling — queries, post-processing, format converters — never to the Validator or the certified runtime.

This keeps unit handling within the constant-memory, no-arithmetic discipline of every other Validator check: parse, compare strings, accept or reject.

2.7.3 Authored-Unit Conversion

The declared unit is the field’s contract; the authored unit is the author’s convenience. When a data line’s annotation carries a different unit of the same dimension than the field’s declared unit, the schema-aware Denormalizer converts the value into the declared unit at build time — the unit-flavored twin of the head-type lift (§2.6.17): the schema’s unit wins in the deployment artifact, and the value is converted so that meaning is preserved.

# schema declares !float:m; authored:
!float:km
/trip::dist=42
# .raiv --- authored unit preserved:
!float:km'/trip::dist=42
# .daiv --- declared unit, value converted:
!float:m'/trip::dist=42000

The rules:

2.7.4 Elided-Type Unit Annotation (!:unit)

Under authored-unit conversion, the only information an annotation contributes on a schema-defined numeric field is the unit — the type is the schema’s, and a restated type name is unchecked noise that can drift. The elided-type unit annotation says exactly the new thing:

!:km
/trip::dist=42

!:unit is resolved by the schema-aware Denormalizer, exactly like an unannotated line under the head-type lift (§2.6.17):

This is elision, not inference: the default keys off the annotation the author wrote — a unit is definitionally a ..num property — never off the value’s lexical shape (§2.6.2). An author who wants integer semantics writes !int:km. The two defaults compose into one rule: no annotation inherits the head or falls back to str; a unit-only annotation inherits the head or falls back to float.

The form is minimal by design. !:unit admits no inline constraints (![0,100]:km is ill-formed — constraints belong to the schema in inherit mode) and no trailing pattern literal; a provenance list composes as usual (!:km?sensor1); and it is a type-designating annotation for the stacking rule (§1.3.4). Like $field references, the form survives the schema-independent Compiler into .raiv (!:km'/trip::dist=42) and is resolved by the Denormalizer; it never appears in .daiv. DFA processing is unaffected: the six-rule classifier and the ' split see the same line shapes, and the certified runtime consumes only .daiv.

2.7.5 Compound Units

Compound units use SI-style operators on the unit string:

Operator Meaning Example
* multiplication !float:N*m (torque)
/ division !float:m/s (velocity)
^ integer exponent !float:m/s^2 (acceleration)

1 is the dimensionless unit — the multiplicative identity. It is the canonical form of any value with no physical dimension (ratios, fractions, counts as dimensionless quantities, fully-cancelled compound expressions). It also serves as the sole numerator for “per X” ratios that have no named SI unit, e.g. !float:1/s (frequency expressed as inverse seconds, equivalent to Hz; the format does not auto-substitute named SI units for canonical compound forms).

The grammar is strict:

2.7.6 Canonical Form: ASCII-Sorted Factors

Compound units are canonicalized at parse time so that semantically equivalent expressions produce byte-identical lines. The canonical form is a numerator (one or more *-joined factors) followed by zero or more denominator factors, each introduced by its own /:

factor[*factor]*[/factor[/factor]*]

The canonicalization rules:

  1. Numerator factors are *-joined and sorted by base unit name (the letters, with a currency’s leading ~ included), compared as ASCII byte strings. The exponent is not part of the sort key: after rule 3, each base name occurs at most once per side, so ties cannot arise. Thus mm^2*mAmA*mm^2 (mA < mm because A 0x41 < m 0x6D), not mm^2*mA — the ^2 never enters the comparison.

  2. Denominator factors each carry their own / and are sorted by base unit name as a list, by the same key as rule 1.

  3. Repeated occurrences of the same factor on the same side collapse to positive integer exponents (m*mm^2, m/s/sm/s^2).

  4. Authored negative exponents fold into denominator factors (m*s^-1m/s); canonical exponents are always positive integers ≥ 2.

  5. Integer exponents of 1 are omitted (m^1m).

  6. A factor that appears in both numerator and denominator cancels (m*s/ms). If cancellation empties the numerator, the canonical numerator becomes 1 (the dimensionless unit) and the denominator survives (m/m^21/m). If cancellation empties both sides, the canonical form is 1 (m/m1, kg*s/kg/s1).

  7. The dimensionless unit 1 is elided from canonical numerator and denominator unless it is the sole factor on that side (1*mm, 1/m is canonical, 1 alone is canonical).

Authored Canonical
m*kg/s^2 kg*m/s^2
s*A A*s
m/s/s m/s^2
N/s/m^2 N/m^2/s
m*s^-1 m/s
kg*m^2/A/s^3 kg*m^2/A/s^3 (already canonical)
mm^2*mA mA*mm^2 (sort by base name; ^2 ignored)
s^-1 1/s
m/m 1
kg*s/kg/s 1

Two unit expressions denote the same unit if and only if their canonical forms are byte-identical. The .csaiv and .daiv files always carry the canonical form; the .raiv likewise — canonicalization happens in the Compiler.

2.7.7 Built-in Units

A frozen, enumerated set of units ships embedded with every implementation — the SI base units, the named SI-derived units, the SI decimal prefixes applied to them, and a curated set of non-SI and US/imperial units. Like std/core for types, this set is part of every conforming distribution: implementations MUST recognize exactly the set enumerated in this section, and no kfaiv.com lookup is required to validate any unit in it. Membership is checked at build time — at the lex stage when the document declares no .!units import (the namespace is closed over the built-in set, §2.7.10), and by the Compiler when imports leave the set open; either way a unit name that is neither built-in nor resolvable is an INVALID_CONSTRAINT_ERROR. The certified Validator only byte-compares already-canonical unit strings and never converts (§2.7.2). Because unit names are ASCII (unit-name is 1*ALPHA), non-ASCII symbols use ASCII spellings: u for micro (µ), ohm for Ω.

SI base units. (dimension symbol; conversion factor 1):

Unit Quantity Dimension
m length L
kg mass M
s time T
A electric current I
K thermodynamic temperature Θ
mol amount of substance N
cd luminous intensity J

Dimension symbols (L M T I Θ N J) and unit names occupy separate namespaces: a unit string is never parsed as a dimension, so the standard SI overloads (unit N newton vs dimension N; unit T tesla vs dimension T; unit J joule vs dimension J) do not collide.

Named SI-derived units. (coherent; each has conversion factor 1 and the SI expansion shown):

Unit Quantity SI expansion
rad plane angle 1
sr solid angle 1
Hz frequency 1/s
N force kg*m/s^2
Pa pressure kg/m/s^2
J energy kg*m^2/s^2
W power kg*m^2/s^3
C electric charge A*s
V electric potential kg*m^2/A/s^3
F capacitance A^2*s^4/kg/m^2
ohm resistance kg*m^2/A^2/s^3
S conductance A^2*s^3/kg/m^2
Wb magnetic flux kg*m^2/A/s^2
T magnetic flux density kg/A/s^2
H inductance kg*m^2/A^2/s^2
lm luminous flux cd
lx illuminance cd/m^2
Bq activity 1/s
Gy absorbed dose m^2/s^2
Sv dose equivalent m^2/s^2
kat catalytic activity mol/s

SI decimal prefixes. A single prefix from the table below may be prepended to any SI base unit (for mass, to the gram g, not kg), any named SI-derived unit above, and the litre L, forming a distinct built-in unit whose factor is 10^power times the base’s. The unprefixed gram g (0.001 kg) is itself a member of the built-in set — it is the prefix-attachment base for mass, even though kg is the SI base unit. At most one prefix per unit; prefixes do not apply to non-SI/imperial units or to currencies. So km, mm, MHz, kPa, mA, ns, mL, mg are built-in; kft, k~USD are not.

Prefix Power Prefix Power
Y 24 d −1
Z 21 c −2
E 18 m −3
P 15 u −6
T 12 n −9
G 9 p −12
M 6 f −15
k 3 a −18
h 2 z −21
da 1 y −24

Non-SI and US/imperial units. (curated; exact conversion factors to the SI base of the same dimension):

Unit Quantity = SI
min time 60 s
h time 3600 s
d time 86400 s
t mass 1000 kg (tonne)
L volume 0.001 m^3
in length 0.0254 m
ft length 0.3048 m
yd length 0.9144 m
mi length 1609.344 m
nmi length 1852 m
lb mass 0.45359237 kg
oz mass 0.028349523125 kg
gal volume 0.003785411784 m^3 (US gallon)

Information units. The bit and the byte are a base dimension of their own — information, underivable from SI — and ship built in (dimension symbol B; the byte’s factor is 8 relative to the bit):

Unit Quantity =
b information 1 bit (base)
B information 8 b (byte)

Two prefix families attach to b and B, and only the multiplying members — a millibyte is not a thing:

KB is rejected as ambiguous — at the unit grammar level, so no context (not even a custom .faiv) can claim the spelling. K is not an SI prefix; JEDEC KB means 1024 bytes while readers assume 1000, and kaiv refuses to guess. The rejection is a teaching error naming both resolutions: KB is ambiguous: write kB (1000 B) or KiB (1024 B) — likewise Kb for bits.

Data rates are ordinary compound units — b/s, kb/s, MiB/s, GB/s — and need no dedicated names. The telecom spellings are accepted as authored input and canonicalize to the decimal-bit compounds: bpsb/s, kbpskb/s, MbpsMb/s, GbpsGb/s, TbpsTb/s. kbps is 1000 bits per second everywhere networks are sold — never kibibits.

Offset (affine) units are excluded. The unit model is factor-only — a unit is a pure scale relative to its base — so temperature scales with a non-zero offset (°C, °F) are not built in; kelvin (K) is the only built-in temperature unit. Consumers needing degrees Celsius/Fahrenheit apply the offset in application code.

Compound units inherit their dimensions from their factors by the same multiplication/division/exponentiation rules as the unit expression itself: kg*m/s^2 has dimension M*L/T^2 — the SI expression of force. The dimensionless unit 1 has the empty dimension product.

Within a dimension, base units are reference units (conversion factor 1); every other unit’s factor above is relative to that base. Conversion factors are not consulted by the Validator — they exist for the build’s authored-unit conversion (§2.7.3) and for consumer tooling that elects to convert between same-dimension values.

2.7.8 Custom units (kfaiv.com)

Domain-specific units (astronomical units, specialized SI extensions, industry-specific scales) are defined in unit library files served from kfaiv.com — the unit-definition registry, paralleling ktaiv.com for types and ksaiv.com for schemas. Resolution and override mechanics follow the same layered model as type registry resolution: a registry-override declaration overrides per prefix; build-time configuration overrides for matching prefixes; the default base URL is kfaiv.com.

A custom unit definition declares:

  1. The unit’s name and any aliases.

  2. The unit’s dimension, expressed by reference to other units.

  3. A conversion factor to the dimension’s base unit.

2.7.9 Unit Definition Files (.faiv)

Unit libraries are .faiv files (Factor Attributed Information Values — a unit definition is fundamentally a conversion-factor declaration). The extension pairs with its registry domain like the rest of the family (ktaiv.com.taiv, ksaiv.com.saiv, kfaiv.com.faiv). A .faiv file is caiv: a .!faiv [VERSION] LIBRARY-ID header, then a definition line above each &name=:

.!faiv astro/units

// Astronomical unit
m 1.495978707e11
&au=
&AU=au

// Custom currency with a rate source
$ @https://rates.example.com/v1?code={code}&at={timestamp}
&~XYZ=

The definition line is DIMENSION [FACTOR | @RATE-URL]:

faiv-def-line  = dimension [ 1*ws ( factor / rate-source ) ] eol
                 ; factor REQUIRED for physical dimensions,
                 ; absent or a rate-source for "$"
dimension      = unit-expr / "$"
factor         = 1*DIGIT [ "." 1*DIGIT ] [ ( "e" / "E" ) [ "+" / "-" ] 1*DIGIT ]
rate-source    = "@" uri              ; "{code}" / "{timestamp}" placeholders
faiv-name-line = "&" ( unit-name / currency ) "=" [ unit-name / currency ] eol
faiv-decl  = ".!faiv" [ 1*ws version ] 1*ws library-path eol

A rate-source URL may contain = (query strings); the rule-6 classification priority (§1.3.1) applies to .faiv definition lines exactly as it does to patterns.

2.7.10 Referencing Custom Units: .!units

Unit expressions carry bare names — a library path inside a unit string is impossible, since / already means division (:astro/units/au would parse as astro ÷ units ÷ au). A document therefore imports the unit libraries it uses with the .!units LIBRARY-ID declaration, paralleling .!types:

.!kaiv
.!units astro/units

!float:au
/probe::distance=1.5

Rules mirror .!types: multiple imports are allowed; a unit name MUST resolve against the built-in set or exactly one imported library (ambiguity across imports is an error, and a custom unit MUST NOT shadow a built-in name); .!units is valid in .kaiv, .saiv, and .taiv, and survives into canonical output as resolution metadata. Resolution follows the standard layered model with {base}/{library/path}.faiv and kfaiv.com as the Layer 4 default. With no .!units declaration in the document, the unit namespace is closed over the built-in set and membership is checkable at lex time; with imports present, membership is resolution-dependent and checked at compile time (an unknown name remains an INVALID_CONSTRAINT_ERROR condition). Canonicalization is unchanged — custom names sort with built-ins by the same base-name key, and the canonical .daiv/.csaiv line carries the bare canonical unit string.

2.7.11 Units on Named Types

A named type derived from !int or !float may carry a unit at its definition site:

.!taiv acme/distances

// A non-negative length expressed in kilometres.
!float:km [0,]
&distance_km=

The unit is part of the named type’s canonical identity. In data files, the unit appears on the canonical line alongside the fully-qualified type name:

!acme/distances/distance_km:km'/trip::length=42

The unit is immutable at use sites — unit rules are deliberately not the constraint-narrowing model:

2.7.12 Currencies

Currencies are a variant of units distinguished by a tilde prefix on the unit code:

!float:~EUR'/order::total=99.50
!float:~USD'::price=12.00
!int:~JPY'::amount=1500

A currency code is ~ followed by exactly three uppercase ASCII letters — the ISO 4217 shape. Well-formedness (three uppercase letters) is required; membership in the ISO 4217 register is not checked — a well-formed but unassigned code (~XYZ) is accepted, because a currency carries no dimension breakdown or conversion factor to validate against, and the register is external and time-varying. All currency units share a single dimension, written $ (the dimension symbol for monetary value). Compound expressions containing currencies — !float:~USD/h for an hourly rate, !float:~EUR*kg^-1 for a price-per-mass — are formed and canonicalized by the same rules as physical compound units. The tilde stays attached to the currency factor under canonicalization.

Currency definitions deliberately omit conversion factors: exchange rates are time-dependent and external to any unit definition. The Validator therefore performs no cross-currency conversion, and mixing multiple currencies of the same $ dimension within a document is permitted. Converting between currencies is a consumer concern, typically resolved by joining the value with an exchange-rate source keyed off the @timestamp qualifier of the value’s provenance.

A currency definition on kfaiv.com MAY declare a rate-source URL template in place of a conversion factor ($ @https://…?code={code}&at={timestamp}, §2.7.9), so that consumer tooling can pull the exchange rate contemporaneous with the @timestamp qualifier of a value’s provenance. The Validator never consults it.

2.8 Constraints

The constraint forms — pattern, span, range, enumeration, length — are introduced in the Type System section above. This section covers their use directly on type annotations (inline, without a named type) and the length constraint in detail.

2.8.1 Inline Constraints on Type Annotations

Any constraint form may be applied directly in a type annotation, immediately following the type sigil:

Multiple constraints of different kinds may appear on the same annotation:

!str/[a-zA-Z0-9.-]+/#[1,253]'/server::host=

Here the pattern constraint /[a-zA-Z0-9.-]+/ and the length constraint #[1,253] are independent predicates applied to the same field; both must be satisfied.

2.8.2 Length Constraints

A length constraint applies a constraint to the length of the value rather than to the value itself. It is written as a # prefix immediately before the constraint bracket or brace: #[min,max] or #{a,b,c}.

The length semantics depend on the type:

Valid Length Constraint Forms

The #/regex/ form is not valid: applying a pattern constraint to a length is not meaningful.

Examples. On strings:

!str/[a-zA-Z0-9.-]+/#[1,253]'/server::host=
!str#[8,128]'/user::password=
!str#[2,2]'/address::country_code=

On arrays (in a .saiv schema — the annotation constrains the element type, the length constraint counts elements; the vector operator ;= declares the scalar array, §7.3):

!str#[1,10]
/@tags;=

!float#[2,2]
/@coords;=

On binary:

!b64#[32,32]'::hash=
!b64#{16,24,32}'::key=

Composability. A field may carry both a value constraint and a length constraint simultaneously. The two are independent predicates:

!str/[a-zA-Z0-9.-]+/#[1,253]'/server::host=

The /[a-zA-Z0-9.-]+/ checks the value; the #[1,253] checks the character count. Both must be satisfied. Order within the type annotation is insignificant — they are commutative predicates on the same field.

DFA Compatibility. The # prefix is a single additional DFA state transition. The DFA sees # after the type annotation, enters length-constraint mode, then parses the following [min,max] or {a,b,c} using the same constraint-parsing path as value constraints. At validation time the check is len(value) ∈ constraint instead of value ∈ constraint. No new parsing machinery is required.

Narrowing. Length constraints follow the same narrowing rules as value constraints: a narrowed constraint must be a subset of the parent’s constraint.

// Base type: hostname allows up to 253 chars
!str/[a-zA-Z0-9.-]+/#[1,253]
&hostname=

// Narrowed: short hostname (embedded devices)
&hostname #[1,63]
&short_hostname=

The #[1,63] narrows the length constraint from [1,253] to [1,63]. The value constraint (/[a-zA-Z0-9.-]+/) is inherited unchanged.

3 Level 1: Trees

Level 1 introduces tree-shaped data: arrays (@), namespaces and structs (/), the field projection operator (::), and the assignment operators that build them (+=, ;=, :=). Level 1 still parses in constant memory.

3.1 Arrays

An array is an ordered collection of elements. Arrays use the @ sigil, and plural form in identifiers is RECOMMENDED: e.g. /@hosts, /@roles.

There are two syntax variants for defining arrays: value accumulation, and inline assignment.

With value accumulation, the array appending operator += is used in order to append a value to an array. For example:

/@hosts+=localhost
/@hosts+=example.com

The inline syntax variant of array definition uses an array extending operator ;= to extend the array with a list of semicolon-separated values. For example:

/@hosts;=localhost;example.com

The two variants are syntactically different, hence parsers emit different entries. However, they are semantically equivalent — applications MUST treat them identically. The two variants can be intermixed in building one and the same array structure:

/@numbers+=one
/@numbers;=two;three
/@numbers+=four

The extending syntax can be thought of as syntactic sugar for the appending syntax; if the data contains semicolons, simply use the += operator with one element per line instead of the multi-element ;= operator.

The extending/appending syntax terminology is based on the premise that the array is constructed by accumulation. However, arrays are often used to represent tuples or vectors, defined in one command and never appended or extended, for example:

/@numbers;=one;two;three;four

The ;= operator is therefore also called the vector assignment operator. The terms array extending operator and vector assignment operator are synonyms — we use one or the other depending on context.

3.1.1 Arrays Are Namespaces with Integer Fields

The indexed namepath syntax naturally handles arrays of namespaces — the construct that all major target formats support. The challenge: how to represent a repeated composite type without introducing syntactic nesting. The answer is that both authoring forms reduce to the same indexed namepath representation in canonical form.

Authored Form — Inline (+:= Operator, Array-Append Namespace-Assignment)

/@servers+:=host=a|port=1
/@servers+:=host=b|port=2

Authored Form — Section Blocks

[/@servers]
host=a
port=1
[/@servers]
host=b
port=2
[]

Canonical Form (Both Reduce to the Same Output)

!str'/@servers/0::host=a
!str'/@servers/0::port=1
!str'/@servers/1::host=b
!str'/@servers/1::port=2

Both the inline +:= syntax and the section block [/@key]…[] syntax are lexer-level syntactic sugar. The Compiler canonicalizes both into indexed namepaths. The Application layer sees only the flat canonical stream.

Scalar arrays vs. namespace arrays. The distinction between the two kinds of arrays is visible in canonical form through the operator between the array name and the index. Scalar array elements (!int'/@ports::0=8080) use :: before the index — the index is the field, and :: projects it as a leaf value. Namespace array elements (!str'/@servers/0::host=a) use / before the index — the index descends into a child namespace, and the fields within are then projected with ::. The same operator rule that governs all other path steps (:: exits the tree, / stays in it) determines the array kind.

3.1.2 Section Block Semantics

Section blocks ([/@path][]) and namespace blocks ((/path)()) are authoring sugar that the Compiler expands into fully-indexed canonical namepaths. The Lexer only classifies the delimiter lines (§1.3.1); all pairing and indexing is a Compiler concern. The Compiler maintains a block stack and, for each array path, a monotonic element counter.

Canonical index spelling. An element index has exactly one spelling — the counter’s decimal value, with no leading zeros (index in §10) — parallel to the single canonical representation of names (§2.3.2). An all-digit namepath segment with a leading zero fails the Lexer’s key check (INVALID_KEY_ERROR), so /@a/0 and /@a/00 can never denote two elements or alias one. A collection line covers only canonical spellings: a hand-assembled index beyond an implementation’s documented magnitude limit (§9.9) is simply not covered — an undefined field, tolerated by a relaxed schema and rejected by a strict one.

Section-open [/@path]. Opens a new element of the array at /@path (resolved against the enclosing block’s prefix). The element index is that array path’s next counter value, starting at 0; content lines that follow are emitted under /@path/<index> until the block is closed or superseded. A section-open may carry a Level 2 table header after the path ([/@servers host=! min=1]); the array path is the first whitespace-separated token and the header is consumed by the schema layer (§4.2), not by the path logic.

A repeated open advances the element. A [/@path] for the array that is currently the innermost open block — with no intervening [] — closes the current element and opens the next (counter += 1). This is how [/@servers]…[/@servers]…[] yields elements 0 and 1 (conformance vector valid/008 in the spec repository).

A new section-open closes the current one. Opening any section block while a different section block is the innermost open block closes (abandons) the current one and starts the new array at its own path’s next index. Consequently section blocks do not nest inside one another: a nested array (a /@outer whose elements each contain a /@inner) is authored with indexed namepaths or the +:= / ;= forms (§3.1), not by nesting […] inside […]. Namespace blocks, by contrast, do nest — a (/meta) opened inside a section-block element extends the current prefix (/@servers/0/meta) and is closed by ().

Close [] / (). [] pops the innermost block iff it is a section (array) block; () pops it iff it is a namespace block. A close whose kind does not match the innermost open block — or a close with no block open — is a tolerated no-op that leaves the stack unchanged, not an error. Blocks still open at end of input are closed implicitly; no error is raised (implementations MAY warn).

Counters persist per array path. Element counters are keyed by the fully-resolved array path and live for the whole document. Reopening an array path later in the file (after it was closed) continues its index rather than resetting: arrays are append-only, and an array’s elements are numbered in first-seen order across every section block and every += / +:= line that targets that path.

3.1.3 Mixed Arrays

A mixed array contains elements that may be either scalar or namespace, with the operator before the index discriminating per element:

!str'/@items::0=hello
!str'/@items/1::name=Alice
!int'/@items::2=42
!int'/@items/3::x=1
!int'/@items/3::y=2

This represents ["hello", {"name": "Alice"}, 42, {"x": 1, "y": 2}]. Elements 0 and 2 are scalars (indexed with ::); elements 1 and 3 are namespaces (indexed with /). No new syntax is required — the existing :: / / operator distinction naturally handles mixed arrays, enabling kaiv to represent all JSON lists faithfully.

3.1.4 Nesting Depth

With indexed namepaths, nesting depth is no longer an architectural constraint — it becomes a schema-bounded policy.

Depth 2 Example

A namespace array /@matrix, each element containing a nested scalar array @values:

!str'/@matrix/0/@values::0=a
!str'/@matrix/0/@values::1=b
!str'/@matrix/1/@values::0=c

Depth 3 Example

!int'/@cube/0/@planes/0/@points/0::x=1

The Lexer does not care about depth — it sees a key string, =, and a value. The Parser tracks N index counters for N nesting levels; each counter is a single integer. There is no stack, no recursion, no pushdown automaton. The schema defines the maximum nesting depth for any given field, and the compiled schema (.csaiv) declares that maximum. The Validator pre-allocates exactly that many index counters at startup. Memory usage is static and schema-determined.

Flat grammar. kaiv’s lexical grammar is flat: every line is a self-contained (type, key_path, value) tuple. Structural nesting is encoded in the namepath, not in the syntax. The schema bounds the nesting depth for any given document, and the runtime validator’s memory usage is statically determined by the schema.

3.2 Structs

Structs are key-value data structures using the / prefix. Structs are assigned to using the struct assignment operator :=, where the right side is a pipe-separated sequence of key-values: a sequence of scalar and array assignments with EOL replaced with pipes (and final EOL stripped). For example:

/server:=host=localhost|port=8080

Structs can contain arrays as values:

/meta:=category=info|@tags;=doc;manual;syntax|published=true

The semantic on the right side is the same as for arrays (defined in the Arrays section above) with the only syntactic difference being EOL vs pipes. Therefore, the same array @tags could have been constructed by accumulation:

/meta:=category=info|@tags+=doc|@tags+=manual|@tags+=syntax|published=true

Note that, as arrays are ordered by definition, this implies that structs are also ordered structures (otherwise the ordering of @tags would be lost in the second example).

As recursion is not allowed in kaiv (core principle), structs cannot contain nested structs. However, this functionality is expressed in a different syntactic form using namespaces.

Because the pipe is the field separator, a literal | cannot appear in a value inside a struct assignment — there are no escape sequences (§9.5). The same rule as for ;= applies: if the data contains pipes, do not use the inline sugar. Author the fields as separate namespaced field lines (/server::motd=a|b), where the value after = is verbatim and may contain any character. The same restriction and the same escape hatch apply to +:= (use section blocks instead).

3.3 Fields and Literals

Before we define namespaces, we need to introduce the concepts of fields and literals.

Keys and values are syntactic concepts designating whatever is on the left and right side of the assignment (or appending/extending) operator, respectively. Fields and literals are similar concepts, but operating on the semantic plane: a field is an identifier that is tied to a literal, regardless of how this relation is established syntactically. A literal can be a scalar or an array value, but not a struct value.

With simple string values assigned to simple identifiers, fields coincide with keys, and literals with values:

x=1

Here, x is both a key and a field, and 1 is both a value and a literal.

With structs, however, we encountered a situation where both fields and literals are on the right side of the assignment operator, they are both part of the value:

/server:=host=localhost|port=8080

Here, the value is host=localhost|port=8080, where host and port are fields, and localhost and 8080 are literals.

3.4 Namespaces

A namespace is a slash-separated prefix in a key. Namespaces always carry the / prefix. An example of a namespace is /app/db.

The fact that namespaces and structs use the same prefix is not accidental. As both namespaces and structs add hierarchy to data (namespaces do so on the left side of the assignment operator, and structs on the right), in kaiv they both represent one and the same concept.

3.4.1 Namespaced Structs

An example of a namespaced struct (a struct with a namespace prefix) is:

/app/db/server:=host=localhost|port=5432

Here /app/db/server designates the namespaced struct. (One might think of app/db as being the namespace and server being the struct, but as we will see soon, the two are so organically tied in kaiv that drawing this line feels artificial — this is why the prefix / prefixes the entire identifier, and we don’t see /server in the key.)

3.4.2 Namespaced Field Keys

With field keys — keys designating simple string values or array values, but not struct values — the syntax for applying a namespace is different: for example, applying the namespace /book/intro to the field num_chars reads:

/book/intro::num_chars=1500

Here, :: is the field projection operator (§1.4.2).

Thus we have come full circle: it should make sense now why the struct assignment operator := uses this particular syntax.

/book/intro:=num_chars=1500|num_paragraphs=3

The struct assignment operator := tells us that the fields are to be defined on the right side of the assignment operator. On the other hand, here:

/book/intro::num_chars=1500

we first access the field, then assign a simple literal to it.

And so, when keeping our eyes on the fields and literals, the line where namespaces end and structs begin becomes irrelevant.

3.5 Namespace Blocks

Namespace blocks are an authoring-layer scoping construct that allows a group of lines to share a common namespace prefix — and optionally a scoped schema for sub-namespace validation. They are syntactically parallel to array section blocks ([/@key]…[]) and reduce to flat namepath lines in canonical form.

3.5.1 Syntax

(/name)
  field1=value1
  field2=value2
()

With an optional schema annotation:

(/name schema:schema-id)
  field1=value1
  field2=value2
()

The opening delimiter (/name) or (/name schema:schema-id) opens the block. Every line inside the block is prepended with name/ (accumulating with any outer block prefix). The closing delimiter () ends the block.

The block syntax is consistent with the existing block family:

Block syntax Scope Effect
[/@name]…[] Array element Each line inside is prepended /@name/n::
(/name)…() Namespace Each line inside is prepended name/
(/name schema:ID)…() Namespace with schema Each line prepended name/ and contents validated against the named schema (e.g. schema:crypto/rsa-params)

3.5.2 Basic Example

Authored kaiv

.!kaiv
.!schema:acme/server-config

(/server)
host=localhost
port=8080
()

Canonical Output

.!daiv
.!schema:acme/server-config
!str'/server::host=localhost
!str'/server::port=8080

The namespace block is purely authoring sugar. The canonical output is identical to writing /server::host=localhost and /server::port=8080 directly.

3.5.3 Namespace-Scoped Schemas

When a schema: annotation is included, the namespace block’s contents are validated against the named member of the schema set the parent schema declares for that namespace (see below); the parent’s own scan governs everything outside the block. This is DFA composition: the parent schema delegates validation of the block’s contents to a sub-DFA, then resumes the parent DFA after the block closes.

.!kaiv
.!schema:x509/algorithm-identifier

algorithm=rsa

(/parameters schema:crypto/rsa-params)
modulus=00:ab:cd:...
exponent=65537
()

Changing the algorithm and schema:

algorithm=ecdsa

(/parameters schema:crypto/ecdsa-params)
curve=P-256
public-key=04:ab:cd:...
()

The parent schema (x509/algorithm-identifier) declares that parameters accepts a discriminated schema set — analogous to how a tagged union declares a set of type alternatives. The block’s schema: annotation selects which member validates its contents. Ordered keys ensure that algorithm is resolved before the (/parameters …) block is entered, making the selection available at parse time.

Declaring the set (.saiv).

The parent schema declares a delegated namespace ostensively, exactly as the map schema block does (§7.5): the same block the data writes, carrying the set of admissible schemas — schema: with pipe-separated alternatives — and an empty body:

.!saiv x509/algorithm-identifier

{rsa,ecdsa}
algorithm=

(/parameters schema:crypto/rsa-params|crypto/ecdsa-params)
()

Rules:

3.5.4 Nested Namespace Blocks

Namespace blocks compose naturally. The accumulated prefix grows with each nesting level:

(/server)
  host=localhost
  port=8080

  (/db schema:db/postgres-config)
  host=db.local
  port=5432
  max_connections=100
  ()

  (/cache schema:cache/redis-config)
  host=redis.local
  ttl=300
  ()
()

Canonical Output

!str'/server::host=localhost
!str'/server::port=8080
!str'/server/db::host=db.local
!str'/server/db::port=5432
!str'/server/db::max_connections=100
!str'/server/cache::host=redis.local
!str'/server/cache::ttl=300

Each nested namespace block scopes its lines with the accumulated path prefix. Each schema: annotation validates its block’s contents independently. The parent schema validates the top-level structure.

3.5.5 Canonical Form

The namespace block is authoring sugar. In canonical form it expands to flat !type'namepath=value lines — just like array blocks. The schema: annotation is the one part that must survive: the Compiler MUST emit it as a scoped declaration line —

.!schema:/parameters crypto/rsa-params

— the standard namespace-qualified form of the .!schema declaration (schema-registry-ref in §10). The emission is mechanical and schema-free, so the schema-independent Compiler can perform it. When the parent schema delegates the namespace (§3.5.3), this declaration is the block’s discriminant: exactly as a union’s data line carries its active variant, the canonical document carries its active schema, so .daiv stays self-contained and the Validator selects the member from the artifact alone (§7.6) — a delegated namespace whose declaration is absent fails validation (DelegationSchemaError). For a namespace the parent does not delegate, the declaration is a per-document assertion that tooling MAY validate against as an additional contract.

3.5.6 Encapsulated Hub Schema Extension (.!schema:/ns hub/x)

The encapsulated extension syntax is a document-level declaration (introduced under §2.1.3 in Level 0) that scopes an entire hub schema’s fields under a sub-namespace rather than merging them at root. This is distinct from the namespace block schema: annotation — it is a schema inheritance declaration (a compiled-in merge of one known contract), not a local validation delegation (the one-of-N case: §3.5.3, compiled per §7.6).

The complete syntax table:

Form Syntax Effect
Flat — space-separated .!schema hub/x Hub fields merged at root
Flat — registry ref .!schema:hub/x Hub fields merged at root
Encapsulated (namespace) .!schema:/ns hub/x Hub fields scoped under /ns
Encapsulated URL .!schema:/ns https://url Hub fields scoped under /ns
Array-element .!schema:/@arr hub/x Hub schema applied to each element of /@arr
Array-element URL .!schema:/@arr https://url Hub schema applied to each element of /@arr

Given:

.!schema:/server hub/server-endpoint

where hub/server-endpoint declares ::host, ::port, ::timeout_ms?, the fields in the extending schema are addressed as /server::host, /server::port, /server::timeout_ms (optional).

Multiple instantiation of the same hub. The same hub may be encapsulated under different namespaces in the same document — giving two fully independent instances with independent field values:

.!schema:/upstream hub/server-endpoint
.!schema:/downstream hub/server-endpoint

This yields /upstream::host, /upstream::port, /downstream::host, /downstream::port as four independent fields. Flat extension cannot achieve this — a second .!schema:hub/server-endpoint would be a duplicate.

Auto-derived mappings. The registry auto-derives the mapping edge at publish time: .!schema:/ns hub/x generates the declaration ns::field → hub/x::field for every field in hub/x. No hand-written .maiv file is needed.

Constraint inheritance. Hub field constraints are inherited under the namespace prefix. The namespace prefix does not change constraint semantics — /server::port is still constrained to !std/net/port (0–65535) if the hub declares it so. The schema author may narrow the constraint (e.g. [1024,65535]) but may not widen it.

Compiled merge. In the extending schema’s .csaiv, the inherited schema’s compiled field and collection lines merge at the .!schema declaration’s position, in the inherited schema’s order (their namepaths carrying the namespace or array-element prefix, and any foreign-key target paths re-anchored under it). A field the extending schema redeclares — the narrowing case above — replaces the inherited line in place, keeping the inherited field order for the parallel scan. The inherited schema’s own header declarations (.!saiv, .!provenance) do not carry over: the extending schema’s header governs. Inheritance chains resolve recursively; an implementation MUST bound the chain depth and report a cycle as a SchemaInheritanceCycleError11.2).

3.5.7 Array-Element Hub Schema Extension (.!schema:/@arr hub/x)

The array-element extension applies a hub schema to every element of a named array, rather than to the document root or a single namespace.

.!schema:/@servers hub/server-endpoint

where hub/server-endpoint declares ::host, ::port, ::timeout_ms?, each element of /@servers is addressed as /@servers/n::host, /@servers/n::port, /@servers/n::timeout_ms (optional). The Validator applies the hub schema’s field rules to every array element independently. Required hub fields MUST be present in every element; optional fields (?=) may be absent.

Relationship to table definitions. The .!schema:/@arr hub/x declaration complements the table-definition syntax ([/@arr …][] — see §4). A table definition declares collection-level constraints (UNIQUE, FOREIGN KEY, cardinality); the array-element schema declaration declares the structural shape each element must conform to. Both can appear together:

# element fields from hub/server-endpoint;
# host unique, at least one element required:
.!schema:/@servers hub/server-endpoint
[/@servers host=! min=1]

3.5.8 Schema Composition for Namespace Blocks

The key architectural property of namespace-scoped schemas is that they preserve the parallel-scan structure of the Validator’s validation:

  1. The Validator’s scanner reaches the delegation line for the namespace (§7.6).

  2. It suspends the parent schema scan and delegates to the member .csaiv selected by the document’s scoped .!schema declaration — the discriminant (§3.5.5).

  3. The member’s scan validates the namespace’s lines in lockstep.

  4. When the namepath prefix leaves the namespace, control returns to the parent scan at the next schema line (canonical form has no block delimiters — the prefix change is the boundary).

This is schema composition, not recursion. The set of valid sub-schemas is declared in the parent schema and all sub-schema files are pre-loaded at the same time as the parent. No dynamic dispatch, no heap allocation, no recursion. The composed validator retains constant-memory properties.

Composition adds one level of delegation per nesting level; since nesting depth is bounded by the schema, the composed validator’s state is also statically bounded.

4 Level 2: Tables

Level 2 introduces table definitions — collection-level constraints on arrays of namespaces that cannot be expressed at Level 1. These are kaiv’s equivalents of SQL’s UNIQUE, FOREIGN KEY, and row-cardinality constraints. Validation requires a post-scan pass with O(N) memory for uniqueness hash sets, isolated from the Level 0–1 constant-memory parallel scan.

See §B.2 for the certification boundary that Level 2 constraints operate within.

4.1 The Gap: No Collection-Level Constraints

kaiv’s Level 1 data model supports arrays of namespaces — ordered, indexed sequences of composite records. At the data level, the [/@arr]…[] section block syntax and the inline /@key+:=field=val|field=val form both canonicalize to indexed namepaths (/@servers/0::host=a, /@servers/1::host=b). This is expressive enough to represent anything from a list of server configs to a product catalog.

At the schema level, however, there is a gap: field definitions inside an array element can declare types, constraints, and required/optional status — but nothing can say “the host field must be unique across all elements” or “the department field must reference a value that exists in another array” or “there must be at least one element.” These are collection-level constraints — constraints that apply to the array as a whole, not to individual element fields.

This gap means kaiv’s DDL, prior to Level 2, has no equivalent for SQL’s UNIQUE, FOREIGN KEY, or CHECK (COUNT...) on a table. Table definitions bridge that gap.

4.2 Table Declaration Syntax

At the schema level, a table definition uses the same [/@name]…[] block syntax as the data-level array section block — but with constraint annotations on the opening line and no values. The block declares both the element field schema and the collection-level constraints in one place.

Full Syntax

[/@arrayname field1=!|field2=!,field3=! field4=/@other/*::ref min=N max=M]
  ...element field definitions...
[]

Where the table declaration line ([/@arrayname …]) contains zero or more of:

Component Syntax Meaning
Single unique field=! Values of field must be unique across all elements
Compound unique field1=!,field2=! The combination must be unique across all elements
Independent constraints …|… Pipe separates independent unique/ref constraint groups
Foreign key reference field=/@path Field values must appear in the referenced array field
Minimum cardinality min=N Array must contain at least N elements
Maximum cardinality max=M Array must contain at most M elements

Minimal Example — Unique Host Within a Server List

[/@servers host=!]
!str
host=
!int[1,65535]
port=
[]

This is identical to the data-level [/@servers]…[] block except that the opening line carries constraint annotations. The element field definitions inside follow the same schema syntax as any other field schema.

4.3 Unique Constraints

A field=! on the table declaration line declares that the named field must have a distinct value for every element in the array. This is the table-level equivalent of SQL’s UNIQUE constraint (or PRIMARY KEY when combined with = inside the element body).

Single-Field Unique Constraint

[/@users username=!]
!str
username=
!str
email=
[]

Every user must have a distinct username.

Compound Unique Constraint

field1=!,field2=! declares that the combination of those fields must be unique, not each field individually:

[/@servers host=!,port=!]
!str
host=
!int[1,65535]
port=
[]

The pair (host, port) must be unique — two servers may share the same host (different ports) or the same port (different hosts), but not both.

Multiple Independent Unique Constraints

The pipe separator | introduces a second independent uniqueness requirement on the same array:

[/@servers id=!|host=!,port=!]
!str
id=
!str
host=
!int[1,65535]
port=
[]

This declares two independent constraints: id must be globally unique (any two elements must differ in id), AND the (host, port) combination must also be globally unique. Both constraints must hold simultaneously.

4.4 Foreign Key References

A field=/@path on the table declaration line declares that the field’s value in every element must appear as a value in another array’s field. The path uses qaiv path syntax — but Level 2 needs (and a Level 2 implementation MUST support) only the fixed shape pinned as fk-path in §10: /-descent to an array, the any-element wildcard *, and a single ::field projection (/@departments/*::name). The full qaiv language (predicates, comparison operators, additional wildcards) is a superset defined in QUERY.md, which is an early design document; the fk-path subset defined here is normative and stable independently of it.

Example

[/@employees department=/@departments/*::name]
!str
name=
!str
department=
[]

Every department value in the employees array must exist as a name value in the departments array. This is the kaiv DDL equivalent of:

FOREIGN KEY (department) REFERENCES departments(name)

The qaiv path /@departments/*::name reads: “in the departments array, any element (*), project the name field.” This reuses the existing query language path syntax — no new syntax is introduced.

Foreign Key Combined with Uniqueness

[/@orders id=!|customer=/@customers/*::id]
!str
id=
!str
customer=
[]

id must be unique within orders, and every customer value must reference an existing customer id.

4.5 Cardinality Constraints

min=N and max=M on the table declaration line constrain the number of elements the array may contain.

[/@servers host=! min=1 max=50]
!str
host=
!int[1,65535]
port=
[]

This declares: at least 1 server must be present, at most 50 servers are allowed, and all host values must be distinct.

Cardinality constraints are O(1) to validate — the Pass 1 parallel scan maintains a counter per array and checks bounds on completion. Unlike uniqueness and referential integrity, cardinality does not require O(N) memory. The counter is compatible with the constant-memory certification profile; implementations MAY validate cardinality in Pass 1 without promoting to Level 2.

Constraint Memory Pass
min=N / max=M O(1) — single counter Pass 1 (parallel scan)
field=! (unique) O(N) — hash set of values Pass 2 (post-scan)
field=/@path (foreign key) O(M) — hash set of referenced values Pass 2 (post-scan)

4.6 Compiled Form

Table declarations compile to collection constraint lines in the .csaiv — a new first-class line kind in the compiled schema. Collection constraint lines immediately precede the element field definitions for the array.

Authored .saiv

[/@servers id=!|host=!,port=! min=1 max=50]
!str
id=
!str
host=
!int[1,65535]
port=
!int[1,3600]
timeout?=
[]

Compiled .csaiv

/@servers [unique::id]|[unique::host,port] [min=1] [max=50]
!str'/@servers/::id=
!str'/@servers/::host=
!int /^-?[0-9]+$/ ..num [1,65535]'/@servers/::port=
!int /^-?[0-9]+$/ ..num [1,3600]'/@servers/::timeout?=

The collection constraint line /@servers [unique::id]|[unique::host,port] [min=1] [max=50] is recognized exactly as §1.3.1 classifies it: a line whose first character is / and that contains no ' delimiter — an array path followed by bracketed constraint clauses (in compiled .csaiv form the leading / is always present). Pass 2 recognizes this line kind and registers the constraints for post-scan checking. The element field definitions that follow use the single-line ' format.

Foreign Key Compiled Form

// authored .saiv:
[/@employees department=/@departments/*::name]
...

// compiled .csaiv:
/@employees [ref::department=/@departments/*::name]
...

4.7 Validation

Table constraint validation uses a two-pass approach (the build/runtime placement of the pipeline stages is in Appendix B.1):

Pass 1 — parallel scan (constant memory, Levels 0–1). The existing Validator parallel scan validates all field-level constraints: types, namepaths, ranges, patterns, enumerations, and required/optional. During Pass 1, the Validator also maintains per-array element counters for cardinality checking (min/max). Cardinality errors are reported at the end of Pass 1 when the array boundary is detected.

Pass 2 — table constraint check (O(N) memory, Level 2). After Pass 1 completes, Pass 2 executes for each array that has a collection constraint line in the .csaiv:

Pass 2 runs entirely in the Application layer — it is not part of the Lexer or the Validator’s parallel scan. A Level 1 runtime terminates after Pass 1 and never executes Pass 2.

Pseudocode

// Pass 2 --- table constraint check

for each collection_constraint in csaiv:
    if constraint is [unique::fields]:
        seen = new hash_set();
        for each element in array:
            key = element[fields];  // tuple for compound unique
            if key in seen:
                error("uniqueness violation", array, fields, key);
            seen.add(key);

    if constraint is [ref::field=/@path]:
        ref_values = collect_field_values(/@path);  // hash set
        for each element in array:
            if element[field] not in ref_values:
                error("referential integrity violation", array, field, element[field]);

Reconstructing elements. .daiv is a flat line stream; for each element in array means the Validator groups consecutive lines sharing the /@arr/<i>:: prefix into one element as it scans — the same index-run boundary the Pass 1 array loop uses. An empty array (zero element lines) is valid and contributes nothing to Pass 2: arrays are exempt from the Pass 1 “every schema field appears once” invariant, and a minimum element count is enforced only by an explicit [min=N] clause (CardinalityViolationError).

Omitted fields. Pass 2 operates on the materialized .daiv: an element that omits an optional constrained field participates with its materialized value — the resolved default, or the empty payload of a materialized !null line (§[sec:default-values], §2.6.17). Two elements that both materialize the same default (or both materialize !null) therefore collide under a unique constraint, and an omitted foreign-key field references its materialized value, which must appear in the target field like any other.

Compound-key encoding. For a compound [unique::f1,f2], element[fields] MUST be serialized so that distinct field-value tuples never collide — length-prefix each value (or use a delimiter that cannot occur in a value). Plain concatenation is non-conforming: values are verbatim byte sequences, so (f1="a", f2="bc") and (f1="ab", f2="c") would otherwise hash to the same key.

4.8 Why This Is Level 2

Table definitions are Level 2 because they break the constant-memory guarantee of Levels 0–1:

The O(N) memory requirement for uniqueness and referential integrity cannot be eliminated without a fundamentally different algorithm (e.g. requiring sorted input, which the format does not guarantee). For safety-critical environments that require constant-memory certification at Levels 0–1, Level 2 constraints are excluded from the certified runtime.

The Two-Pass Split Preserves the Level 0–1 Certification Boundary

Pass Memory Levels Certification
Pass 1 (Validator parallel scan) O(1) constant 0–1 MISRA-C certifiable
Cardinality counter in Pass 1 O(1) 2 (MAY be Pass 1) MISRA-C certifiable
Pass 2 (table constraint check) O(N) bounded 2 Not constant-memory certified

A deployment that uses table definitions (Level 2) runs both passes. A deployment that requires constant-memory certification uses Levels 0–1 only — table definitions are not declared in the schema, Pass 2 never runs, and the full constant-memory certification profile applies.

The key architectural insight: the Level 1 constant-memory parallel scan is never compromised. Level 2 adds a post-scan pass that runs after the certified scan completes. The certified scan output is still produced by a constant-memory DFA + parallel scan. Level 2 constraints are checked against that output in a separate pass that explicitly acknowledges O(N) memory usage.

4.9 Architectural Impact

Component Impact
Lexer Recognizes constraint annotations on [/@name …] schema block opening lines: field=!, field1=!,field2=!, |, field=/@path, min=N, max=M. All regular-grammar extensions — no change to DFA structure.
Compiler Compiles [/@name constraints]…[] schema blocks to collection constraint lines + element field definitions in .csaiv. Resolves constraint field references.
Validator (Pass 1) Recognizes collection constraint lines in .csaiv and registers them for Pass 2. Validates cardinality constraints (O(1) counters). Otherwise unchanged from Level 1.
Pass 2 (new — Level 2 only) Post-scan table constraint check. Builds hash sets for uniqueness and referential integrity. Runs after Pass 1 in the Application layer. Not part of the certified constant-memory runtime.
Schema compiler Parses [/@name constraints]…[] in .saiv source, emits collection constraint lines in .csaiv. Collection constraint lines use bracket-delimited clauses that the schema compiler generates from the authored constraint annotation syntax.
Compiled schema (.csaiv) Gains collection constraint lines (/@name [unique::field] [min=N] [max=M]). These are a new line kind — first character /, no ' delimiter (§1.3.1): an array path followed by bracket clauses. Element field definitions that follow use the single-line constraint'/@name/::field= format.
Canonical form (.daiv) Unchanged — data files are unaffected by table definitions.
Query language (qaiv) The /@path syntax used in [ref::field=/@path] reuses existing qaiv path syntax unchanged. No new query operators are required.
Certification Level 0–1 certification profiles are entirely unaffected. Pass 1 (the certified parallel scan) gains only a cardinality counter. Pass 2 is an Application-layer addition that is explicitly O(N) and not part of the certified runtime.

4.10 SQLite Comparison

Table definitions (Level 2) close the DDL gap between kaiv and SQLite’s CREATE TABLE statement. SQLite is the most common embedded database, used as a file format, a data interchange format, and a configuration store. The comparison is precise because SQLite blurs the line between storage and schema — it is schema-driven, file-based, and serverless, making it the closest peer to kaiv in the storage-and-schema space.

SQLite DDL Feature kaiv Equivalent Assessment
CREATE TABLE name (…) [/@name constraints]…[] in .saiv Equivalent
NOT NULL = required field (the default operator) Equivalent
DEFAULT value Schema defaults Equivalent
CHECK (range) Range constraint [min,max] Equivalent
CHECK (pattern) Pattern constraint /regex/ kaiv stronger (first-class regex)
UNIQUE (field) [/@name field=!] Equivalent
UNIQUE (f1, f2) [/@name f1=!,f2=!] Equivalent
Primary key (implicit unique + required) field= inside element body + field=! on declaration Equivalent minus auto-increment index
FOREIGN KEY (f) REFERENCES t(c) [/@name f=/@t/*::c] Equivalent
Cardinality (min/max rows) [/@name min=N max=M] kaiv stronger (SQLite has no row-count constraint)
INTEGER, TEXT, REAL (type affinity) !int, !str, !float (explicit types) kaiv stronger (explicit, pattern-validated)
Enum values via CHECK (IN …) {A,B,C} enumeration constraint Equivalent
CREATE INDEX [ ] SQLite stronger (kaiv is not a database)
SQL joins [ ] SQLite stronger (DML, not DDL)
Aggregation (GROUP BY, etc.) [ ] SQLite stronger (DML, not DDL)
Triggers [ ] SQLite stronger (DML, not DDL)
Transactions [ ] SQLite stronger (DML, not DDL)
Partial reads (B-tree) [ ] SQLite stronger (storage engine)
Concurrent writes (WAL) [ ] SQLite stronger (storage engine)

With table definitions (Level 2), kaiv’s DDL covers SQLite’s schema definition DDL completely and exceeds it in several areas. kaiv has stronger constraint expressiveness than SQLite (which uses type affinity — an INTEGER column can store text without error) and equivalent structural integrity constraints. The remaining SQLite features (indexes, joins, aggregation, triggers, transactions, WAL) are database engine features — DML and storage concerns, not DDL concerns. See DDL_COMPARISONS.md §7 (in the spec repository) for the full comparison.

5 Level 3: Collation

Level 3 introduces locale-aware string ordering via ..lex[locale] — a span ordering that takes a BCP 47 language tag and produces locale-correct lexicographic comparisons. Collation is a property of named types, never of bare str, and is the only feature in kaiv where two conforming validators may produce different comparison results (the spec pins a reference CLDR version and collation strength for conformance — §5.3).

5.1 The Problem: ..lex Is Byte Order

..lex means byte-by-byte comparison (memcmp). This is correct and efficient for ASCII identifiers and English strings — field names, hostnames, port numbers, URIs, and version strings all sort correctly under byte order. But byte order breaks for locale-sensitive data:

For domain-specific named types — product names, city names, personal names, geographic identifiers — ..lex (byte order) produces incorrect ordering and range constraint evaluation.

5.2 Syntax: ..lex[locale]

..lex[locale]

Where locale is a BCP 47 / IETF language tag — the same identifier system used by ICU, CLDR, HTML lang=, and HTTP Accept-Language.

Example Locale
..lex[fr-CA] French Canadian
..lex[de-DE] German (Germany)
..lex[zh-Hans] Simplified Chinese
..lex[en-US] English (United States)
..lex[ja] Japanese

The locale tag is parsed as a bracketed string immediately after ..lex. The brackets […] are part of the span token — the Lexer recognizes ..lex[tag] as a single span ordering token. This is a regular-grammar extension: the DFA gains states to consume the bracketed tag after ..lex, but the overall token classification structure is unchanged.

..lex (bare) remains unchanged — byte-by-byte comparison, available at all Levels including Level 0.

5.3 Reference Collation: CLDR Version and Strength

Two conforming Level 3 validators agree only if they resolve ..lex[locale] against the same collation data and the same comparison options. This specification pins both, and Level 3 conformance is defined against them:

Because collation governs equality as well as order, the pinned version and strength apply to enum membership and [lo,hi] range evaluation for ..lex[locale] fields exactly as they apply to sorting.

Partial implementations (the honest-partial rule). A Level 3 validator MUST, for each ..lex[locale] constraint it encounters, either evaluate it exactly as pinned above or reject that constraint with CollationUnsupportedError. Producing an ordering or equality result that differs from the pinned semantics is non-conformant. Conformant Level 3 validators may therefore differ in which constraints they refuse — never in how they order. A lightweight implementation may support the plain locale tailorings and reject the -u- overrides, and remains fully Level 3 conformant in doing so.

Unknown and ill-formed input. Nothing about a locale tag degrades silently:

5.4 In Type Definitions (.taiv)

Collation is a property of named types, not of str. str stays ..lex (bare, byte order). Domain-specific types carry the collation they need:

.!taiv acme/catalog

// French Canadian product name with locale-aware ordering
/^.{1,200}$/ ..lex[fr-CA]
&product_name=

// German city name
/^.{1,100}$/ ..lex[de-DE]
&city=

// Japanese personal name
/^.{1,100}$/ ..lex[ja]
&person_name=

Key principle: the collation declaration sits next to the pattern constraint on the type definition line. Any field annotated &product_name automatically uses French Canadian collation for range constraints and query comparisons.

5.5 In Compiled Schema (.csaiv)

The collation tag appears as part of the constraint on the element field definition line in the compiled schema:

// .csaiv output for &product_name field:
!acme/catalog/product_name /^.{1,200}$/ ..lex[fr-CA]'::product_name=

The runtime reads ..lex[fr-CA] (from the part before ') and selects the corresponding collation function. A range constraint like [Arbre,Zèbre] on a ..lex[fr-CA] field is evaluated using French Canadian collation rules, not byte order.

5.6 Certification Impact

..lex[locale] is Level 3 because it introduces three properties absent from Levels 0–2:

The boundary. Levels 0–2 are platform-independent (same input → same output everywhere). Level 3 introduces platform dependency — the only Level where two conforming validators may disagree.

A Level 0–2 runtime encountering ..lex[locale] MUST either:

5.7 Query Impact

qaiv predicate comparisons inherit span ordering from the field’s type. If a field has ..lex[fr-CA], then:

/@products[name>café]/*::price

The query engine uses French Canadian collation to evaluate name > café. The comparison function is selected from the field’s span ordering, resolved through the compiled schema.

A Level 0–2 qaiv engine encountering a collation predicate follows the same reject/fall-back rule as the Validator.

5.8 Architectural Impact

Component Impact
Lexer Recognizes ..lex[tag] as a span ordering token. The [tag] is parsed as a bracketed string after ..lex. Regular-grammar extension — no DFA structural change.
Compiler Passes collation tag through to canonical output unchanged.
Validator Selects comparison function based on span + collation tag. ..lexmemcmp, ..lex[locale] → collation library.
Schema compiler Preserves collation tag in .csaiv output.
Compiled schema (.csaiv) Gains ..lex[tag] syntax on span declarations.
Type libraries (.taiv) Can declare collation on named types: ..lex[fr-CA].
Query language (qaiv) Comparison operators use the field’s span ordering including collation.
Certification ..lex (bare) unchanged — available at all Levels. ..lex[locale] requires collation library — Level 3 only.

6 Level 4: Corpus-Dependent Features (Reserved)

Level 4 collects operations that read or query a corpus of .daiv files rather than a single document: path identity, the metaschema (authored as .msaiv), schema composition (.!compose), and cross-schema foreign keys (.!ref / $alias.field). Corpus-dependent features break the self-containment property Levels 0–3 share — validating or compiling a .daiv requires only that .daiv plus its .csaiv — and they run only in kaiv db-class tooling, never in the certified runtime.

Level 4 is defined, numbered, and reserved here — and specified separately: the corpus specification is an experimental document versioned on its own cadence (the corpus spec draft and the metaschema design notes in the specification repository). Nothing in the corpus specification is part of this document’s conformance surface, and this document’s stability promise does not extend to it. Levels 3 and 4 remain independent extensions of Level 2 — neither subsumes the other.

Reserved surface in this document. Two reservations keep the corpus specification a conforming superset of this document rather than a fork:

7 Compiled Schema (.csaiv)

The compiled schema (.csaiv) is the validation contract — the artifact the Validator reads in lockstep with .daiv to produce pass/fail. It is caiv where the left side of = is constraint'namepath and the operator is ?= for optional fields or = for required fields. Required is the default.

The optional marker is build-time information: it tells the Denormalizer which fields it may materialize when absent from the authored data (§[sec:default-values]). The Validator does not branch on it — materialization guarantees every declared field a .daiv line, so the parallel scan is a strict lockstep walk. The one exception is collection element lines (elided-index namepaths like /@ports::=): an empty collection contributes no data lines, so the scan may advance past an unmatched element line; element counts are enforced by the Pass-1 cardinality check, not by presence.

The compiled grammar is a floor. Compiled schemas are published to registries whose entries are immutable and permanent; a .csaiv fetched today must remain readable by every conformant validator, forever. Version 1.0 validators are therefore the permanent floor for the compiled-schema grammar: an extension of the clause grammar after 1.0 would mint eternal registry artifacts that 1.0 validators cannot read. The clause grammar of this chapter freezes at 1.0; any later addition requires a major revision that gives the compiled schema a new format identity rather than silently widening this one.

7.1 Parallel Scan Validation

The Validator reads the data and .csaiv files in parallel. Each line is split on ' to separate the metadata prefix (type annotation plus optional provenance list) from the namepath. The provenance list, if present, is in the metadata prefix before ' — it is skipped or optionally validated against the .? header table, and does not affect the '-split rule. For each pair of lines:

Step Action
Split Split each line on ' to extract (type/constraint, namepath+value) from .daiv and (constraint, namepath+optional-marker) from .csaiv
Type check Nominal only where the name IS the semantics; structural everywhere else. A data line whose annotation equals the retained head always applies the head’s constraints. Otherwise, for a non-union head other than str / text / null / std/enc/*, the check is structural: the head’s constraint group governs the value whatever the data line’s own annotation — the pre-lift !str authored form and a chain-compatible name (!int under !std/net/port) both apply — except that !null- and std/enc-typed lines never satisfy it. Nominal cases: a union — the annotation MUST match one alternative (the discriminant, §2.6.14); !text — accepts !text, or !str under the |:| guard (§2.6.4; a |:|-carrying value is DelimiterCollisionError), and !text data satisfies no other head; !null — only !null data; an explicit !str head (the identity declaration — retained iff authored, §7.3) — only str-typed lines; std/enc/* — only their own name (embedded payloads never satisfy plain fields, and vice versa). A unit-carrying head byte-compares the canonical unit both directions (§2.7.2); in a union, on the matched alternative. Violations are TypeMismatchError. A head-less field line (identity str; its lex-saver, when present, is the vacuous pattern /^/ — a constraint item, not a head) has no nominal requirement at all.
Namepath match Compare namepath from schema line against data line
Empty-collection skip If the schema line is a collection element line (elided-index namepath) and the data line does not match it, the collection is empty — advance the schema pointer without consuming the data line. Element counts are checked by Pass-1 cardinality, not presence
Presence check If a non-element schema line does not match the data line, a schema-declared field is missing or out of order — emit RequiredFieldSchemaError. Materialization (§2.6.17) guarantees every declared field a line, so the scan never branches on the optional marker
Duplicate check A data line whose namepath equals an exact schema-declared name that the scan has already seen is DuplicateKeySchemaError (scoping per §9.8); the seen set is schema-sized. Data-introduced names (collection entry keys, relaxed-mode undefined fields) are outside this check
Constraint validation If schema line has constraints, validate data value against them
Array loop A schema namepath containing @ is a collection element line. A scalar array (a single element line) stays on that line, consuming the indexed data lines until the namepath prefix changes. A namespace array is a group of element field lines sharing the /@name/ prefix: for each element index the Validator walks the group in order, then resets to the group’s first line when the index increments, repeating until the prefix changes. Element fields arrive in group order — materialization (§2.6.17) gives every element the full field sequence — so a skipped or absent group field, optional or not, is RequiredFieldSchemaError, and a field repeated within one element is DuplicateKeySchemaError

The two array kinds are handled identically by the loop rule:

Tagged unions. A union field retains its type names in the compiled line as one type item; each alternative carries its lowered constraint group in parentheses — and its unit, glued to its name, when it carries one (float:km(…)) — so the .csaiv stays self-contained (the certified runtime never resolves types). The group concatenates the alternative’s lowered definition plus any authored narrowing, without whitespace — lowered items are self-delimiting (/pattern/, ..span, [range], {enum}, #[length]), which keeps the whole union one whitespace-free item token. An alternative with no constraints stays bare:

// authored .saiv:                      // compiled .csaiv:
!null|int[1,3600]                       !null(/^$/)|int(/^-?[0-9]+$/..num[1,3600])'::timeout?=
timeout?=

The Validator checks that the data line’s type annotation (the part before ') is the head type or one of the alternatives (else TypeMismatchError); the matched alternative’s group — including its own span — then governs the value. The discriminant match includes the unit: a data line matches a unit-carrying alternative by name and byte-identical canonical unit, and an alternative without a unit matches only a unit-less annotation. A !null alternative therefore enforces its /^$/ empty-payload constraint (§2.6.17). In authored form, an inline constraint attaches to the alternative it textually follows: in !null|int[1,3600] the range narrows int, not the union head. A bracketed span argument inside a group belongs only to ..lex (..lex[fr-CA]); after any other span, [ starts the next (range) item.

7.2 Validator Pseudocode

The entire Validator parallel scan (also used by the integrity check at deployment/runtime) is approximately:

// split_on_apostrophe: splits on the first unquoted ' (quoted names use
// "" doubling, never '', so the first bare ' is always the metadata-data
// delimiter; a provenance list ?id[,id...] with optional @timestamp and
// optional #dpid sits before it and is skipped or validated against the
// .? header table). d, s are the data-line and schema-line cursors.
d = 0; s = first(csaiv);
while d < ndata:
    (data_type, data_np) = split_on_apostrophe(data[d]);
    if !matches_any_schema_line(data_np):          // undefined field
        if strict: error("undefined field", data_np);
        d += 1; continue;                          // relaxed: skip, keep s
    if is_exact_schema_name(data_np) && !seen_add(data_np):
        error("duplicate key", data_np);           // schema-declared name
                                                   // repeated (§ Errors);
                                                   // seen is schema-sized
    (sc, sreq) = split_on_apostrophe(s);
    while s && is_element_line(sreq) && !namepath_matches(sreq, data_np):
        // empty collection: contributes no data lines; element counts
        // are enforced by the Pass-1 cardinality check, not here
        s = next(s);
        if s: (sc, sreq) = split_on_apostrophe(s);
    if !s || !namepath_matches(sreq, data_np):
        // materialization guarantees presence and order (§ Null Semantics):
        // a mismatch is always a missing or out-of-order declared field
        error("missing required field", sreq);

    if is_namespace_array(sreq):          // a GROUP of element field lines
        group = element_group(s);         // the /@name/ lines, in group order
        apre  = array_prefix(sreq);       // "/@name/"
        while d < ndata && starts_with(namepath_of(data[d]), apre):
            idx = element_index(data[d]);
            g = 0;                        // per-element cursor into the group
            while d < ndata && starts_with(namepath_of(data[d]), apre)
                            && element_index(data[d]) == idx:
                k = find_in_group(group, element_field(data[d]));  // whole group
                if k == NONE:             // not a group field
                    if strict: error("undefined field", data[d]); // else skip
                else if k < g:            // already consumed in this element
                    error("duplicate key", data[d]);
                else if k > g:            // group[g] was skipped: lockstep order
                    error("missing required field", group[g]);
                else:
                    check_type_and_constraints(group[k], data[d]);
                    g += 1;
                d += 1;
            if g < len(group):            // element ended short of the full
                error("missing required field", group[g]);  // field sequence
        s = after(group);
        continue;

    // flat field, scalar-array element, or map entry: one schema line
    if !type_matches(sc, data_type): error("type mismatch", sreq);
    validate_constraints(sc, value_of(data_np));
    d += 1;
    if is_element_line(sreq) && d < ndata
                             && namepath_matches(sreq, namepath_of(data[d])):
        continue;                         // stay on s for the next entry/index
    s = next(s);

while s:                                  // remaining schema lines: empty
    if !is_element_line(namepath_of(s)):  //   collections are fine, anything
        error("missing required field", namepath_of(s));   // else is missing
    s = next(s);

Splitting rule. Split on the first bare ' in the line. Since quoted names in namepaths use "" (double-quote doubling) as their only escape — not '' — there is no ambiguity: the first ' in any canonical data line is always the metadata-data delimiter. Any provenance list (?sourceID, ?id@timestamp, ?id@timestamp#dpid, ?id#dpid, or ?id1,id2@timestamp) in the metadata prefix sits before this ' and does not interfere with the split — no provenance ID or data point ID contains '. Collection constraint lines in .csaiv (e.g. /@servers [unique::field] [min=1] [max=50]) have no ' and are recognized by their @-prefixed structure with bracket clauses.

Undefined fields and the schema pointer. A data line that matches no schema line — an undefined field — MUST NOT advance the schema pointer: relaxed schemas MAY interleave undefined fields at any point outside a namespace-array run11, strict vs. relaxed), and the defined fields that follow still have to find their schema lines in order. A namespace array’s data lines are one atomic, contiguous run — canonical output emits each array as a single block of ascending elements, so no conforming producer wedges a foreign line into one. Inside a run the sub-scan consumes only lines under the array prefix: an unknown field under the prefix (a newer producer’s per-element addition such as /@servers/0::region) is skipped under a relaxed schema and is not an interruption, but any line outside the prefix ends the run — an element left short of its field sequence is RequiredFieldSchemaError, and array lines resuming after the break fail the presence check as out-of-order defined fields. matches_any_schema_line is a membership check against the resolved schema (an exact-namepath set plus the collection-line prefix forms) — schema-sized memory, which is already resident; no data-sized allocation. Ordering of defined fields remains enforced: a defined field appearing out of schema order still fails with RequiredFieldSchemaError via the presence check.

Constant memory — a namespace-array element needs one cursor into its group, and the group itself is schema text, already resident — linear time, no data-proportional allocation. The flat, map, and scalar-array path is a dozen lines of C; a namespace array adds the bounded element-group sub-scan above. Certifiable at any ASIL level.

Integrity check. The Validator’s parallel scan logic may optionally be re-run on an existing .daiv at deployment or runtime (loading .daiv and .csaiv and performing the same line-by-line scan). This is not a separate pipeline stage — the build pipeline ends at .daiv. The integrity check re-runs the same constant-memory validation to verify that the artifact has not been corrupted or tampered with after production.

7.3 The Schema Compiler

The schema compiler is a compilation-pipeline canonicalizer for .saiv.csaiv. It reads authored schema text, which is a kaiv document using the same Lexer and the same line grammar as data files. The schema compiler:

The schema compiler uses the same Compiler/Denormalizer/Validator pipeline as for data files, adapted for schema syntax. It is not a DFA that produces a DFA — it is a canonicalizer that produces canonical text. For certification purposes: compile-time tools run on the developer’s workstation, not on the safety-critical target. Only runtime components — the Lexer and the integrity check parallel scan — run on target hardware and require certification.

7.4 Table Declarations in the Compiled Schema

Table definitions (Level 2) introduce a new kind of line in the .csaiv: the collection constraint line. It immediately precedes the element field definitions for the array and declares the collection-level constraints that Pass 2 must check.

Authored .saiv Table Definition

[/@servers host=!,port=! min=1 max=50]
!str
host=
!int[1,65535]
port=
!int[1,3600]
timeout?=
[]

Compiled .csaiv Output

/@servers [unique::host,port] [min=1] [max=50]
!str'/@servers/::host=
!int /^-?[0-9]+$/ ..num [1,65535]'/@servers/::port=
!int /^-?[0-9]+$/ ..num [1,3600]'/@servers/::timeout?=

The /@servers [unique::host,port] [min=1] [max=50] line is the collection constraint line — a new first-class line kind in .csaiv. It carries:

Clause Syntax Meaning
[unique::field] Single-field unique constraint All values of this field across all elements must be distinct
[unique::f1,f2] Compound unique constraint The combination of f1 and f2 must be distinct across elements
[unique::f1]|[unique::f2,f3] Multiple independent unique constraints Two separate uniqueness requirements on the same array
[ref::field=/@path] Foreign key reference Field values must exist in the referenced array field
[min=N] Minimum element count Array must have at least N elements
[max=M] Maximum element count Array must have at most M elements

Authored array schemas below Level 2. The element-level compiled lines do not require the Level 2 collection machinery. A scalar array is declared with the vector operator in schema position — !int above /@ports;= compiles to !int /^-?[0-9]+$/ ..num'/@ports::= — mirroring ?=’s role shift from data to schema. A namespace array’s element fields are declared with a constraint-free section block ([/@servers][]; the table-declaration syntax explicitly allows zero constraints), compiling to the {items}'/@servers/::field= element lines. What makes a table Level 2 is the collection constraints (unique/ref/min/max) and their O(N) Pass 2 — element-shape validation alone is single-pass and Level 1. Collections are never themselves required: an empty array or map is valid absent an explicit [min=N].

The collection constraint line has no ' delimiter — it is /@name [clauses] without a namepath. The element field definitions that follow use the single-line ' format and the array namepath prefix (!str'/@servers/::host=) to scope them to the array without an explicit index — the :: immediately following / signals that this is an element-level schema line, not a specific indexed element.

Multiple independent unique constraints compile to adjacent [unique::] clauses separated by | on the collection constraint line:

// authored .saiv:
[/@servers id=!|host=!,port=!]
...

// compiled .csaiv:
/@servers [unique::id]|[unique::host,port]

Foreign key reference compiles to a [ref::] clause:

// authored .saiv:
[/@employees department=/@departments/*::name]
...

// compiled .csaiv:
/@employees [ref::department=/@departments/*::name]

7.5 Maps in the Compiled Schema

A map field (§2.6.18) is a namespace with arbitrary string-named entries that all share one value type. Its compiled form parallels the scalar array: where an array declares its element constraint once against an elided integer index (!str'/@servers/::host=), a map declares its entry constraint once against an elided string key.

Authored .saiv

!map<int>
/config/settings=

Compiled .csaiv

!int /^-?[0-9]+$/ ..num'/config/settings::=

(A root-level map — settings= with a bare-name key — compiles the same way with the map’s own name as the single namespace step: /settings::=.) The map-entry line uses the empty-terminal namepath mapnamespace:: — the same canonical-steps "::" form as a scalar-array element (§10.6), distinguished by the absence of @: a ::-terminated schema line whose steps contain an @ is a scalar-array element (integer keys), and one whose steps contain no @ is a map entry (string keys). The value type is lowered to its constraint form exactly like a scalar field (!map<str>!str'/config/settings::=, the identity item).

Validation scan. The map-entry line is consumed by the same variable-run loop as the scalar-array element (§7.1, Array loop): the Validator stays on the entry line while consecutive data lines share the map’s namepath prefix (/config/settings::<key>), validating each entry’s value against the value constraint. Zero entries is valid — a map may be empty (authored ={} emits no entry lines), so a map field imposes no minimum entry count by itself.

Key constraints (optional). By default a map key is any legal name (§2.3). A schema MAY constrain keys with a key clause on a collection-style line preceding the entry line, and MAY bound the entry count with the same [min=N] / [max=M] clauses used for arrays:

/config/settings [key::/^[a-z][a-z0-9_]*$/] [max=100]
!int /^-?[0-9]+$/ ..num'/config/settings::=

Each entry’s key — the terminal after ::MUST match the key pattern; a violation raises a ConstraintViolationError.

Authored key constraints — the map schema block

The authored surface for key constraints follows kaiv’s ostensive definition principle: the schema shows a specimen of the data, in the data’s own shape. Exactly as a table schema mirrors the data’s [/@name] block, a keyed map schema mirrors the data’s (/name) block — with a /regex/ standing in key position where a data entry would carry its key, and the entry-count bounds riding the block header as min= / max= clauses, exactly as table headers carry them:

(/config/settings max=100)
!int
/^[a-z][a-z0-9_]*$/=
()

Inside the block, the type-annotation line applies to entry values; the following key line carries the key grammar in key position — “entries look like this” — and takes no default. The block lowers to exactly the compiled form above: the key clause and cardinality clauses onto the collection-style line, the value type onto the map-entry line. The flat !map<VALUETYPE> field form remains the authored surface for unkeyed maps; the block form is its keyed superset. A literal key line inside a map schema block (a required named entry, e.g. en=) is reserved: the schema compiler MUST reject it in this revision.

Implementation status. The reference pipeline implements both sides: map data lowering (the entry lines, §2.6.18) and !map<…> schema-field lowering with the map validation scan (conformance group schema/005-map); the ostensive map schema block and key-clause validation are exercised by conformance group schema/041-map-keys.

7.6 Delegated Namespaces in the Compiled Schema

A discriminated schema set (§3.5.3) compiles to a delegation line — a collection-style line (first character /, no ') carrying a single bracketed clause with the member references, fully qualified, in authored order:

/parameters [schema::crypto/rsa-params|crypto/ecdsa-params]

The delegated namespace contributes no field lines of its own to the parent .csaiv — the delegation line is its entire compiled presence. Contrast the encapsulated extension (§3.5.6), which merges one schema’s compiled lines in place: a merge inlines a single known contract; a delegation defers a one-of-N choice to the document.

Validation. The Validator resolves and preloads every member’s .csaiv at startup — the set is static, so the working set remains schema-sized. When the scan reaches the delegation line, it reads the document’s scoped declaration (.!schema:/parameters crypto/rsa-params — the block’s discriminant, §3.5.5): a missing declaration, or one naming a schema outside the set, is a DelegationSchemaError. The lines under the delegated namespace are then walked in lockstep against the selected member — its constraints, its strict modifier, and its .!provenance level govern within the namespace — and the parent scan resumes at the first line outside the prefix. The sub-scan is the same constant-memory parallel scan (§7.1); composition adds one delegation level per nesting, and the chain is bounded statically by the schema graph (§3.5.3).

Materialization. The schema-aware Denormalizer applies the same selection at build time: the selected member’s absent optional fields are materialized within the namespace, in the member’s declared order, so the every-declared-field-appears invariant (§2.6.17) holds inside a delegated namespace exactly as outside it — including the head-type lift onto the member’s untyped lines (§7.3).

Implementation status. The delegation clause is specified ahead of the reference implementation so that its syntax lands inside the 1.0 compiled-grammar freeze (§7 — the clause grammar is a floor). Conformance vectors follow with the kaiv-rs implementation (REVIEW.md D-10, in the spec repository).

8 Mappings (.maiv)

A mapping declares a structural correspondence between two schemas: which target field receives which source field. Because kaiv schemas describe pure data — no methods, no computed properties — a mapping is exhaustive and purely structural: a name-to-name rewrite table with no value transformations, no predicates, and no conditionals. Mappings are the edges of the schema graph: published to ksaiv.com alongside schemas, they make independently authored schemas mutually convertible, and their composition is a join on namepaths rather than a program-synthesis problem.

A mapping file (.maiv) is caiv: the same six-rule line classifier, the same declarations mechanism, and — decisively — the same two core constructs that already express "receives from": = assignment and the $ dereference sigil. A .maiv line assigns to a target namepath (left of =, as everywhere in kaiv) a value that is either a $-reference into the source schema’s namespace or a literal constant. No new operators are required.

8.1 Header Declarations

.!maiv
.!source acme/server-config
.!target hub/server-endpoint

8.2 Registry Addressing

Mappings publish to ksaiv.com — they are edges between schemas, and they live beside them. A published mapping has no chosen name; its address derives from its endpoints and its edition:

https://s.kaiv.io/{source}/mapto/{target}/{version}.maiv

where {source} and {target} are the fully qualified {namespace}/{name} schema references (schema references on the registries are exactly two segments), mapto is the direction marker, and {version} is the mapping’s edition: v1, v2, … The marker makes the address self-evident — it reads as a sentence — and names the publisher: a mapto address is the source owner’s assertion. The rules:

8.3 Mapping Lines

Every mapping line is a rule-5 content line: a target namepath, =, and a right side.

# Simple field rename: target host receives source hostname
::host=$::hostname

# Namepath restructure
/network::listen_port=$/server::port

# Array field mapping --- the /* wildcard maps every element
/@nodes/*::host=$/@servers/*::hostname

# Constant override: source values outside the target constraint
# fall back to the constant (here: TRACE/FATAL -> DEBUG)
::level=$::level|DEBUG

# Null fallback: requires the target field to be nullable
::region=$::legacy_region|!null

# Constant: target field with no source counterpart
::api_version=v2

The right-side forms:

Right side Meaning
$namepath The target field receives the source field’s value
$namepath|constant As above; if the source value fails the target field’s constraint, the constant is emitted instead
$namepath|!null As above with a null fallback; the target field MUST be declared nullable (!null|T)
literal Constant: the target field always receives this literal. Also serves as the default for a target field with no source counterpart

Rules:

8.4 Execution Model

The mapper is a single-pass line rewriter — build-time tooling, outside the certified runtime:

Input:   source .daiv + source .csaiv + target .csaiv + .maiv
Output:  target .daiv

For each line in source .daiv:
    1. Split on ' -> metadata prefix + namepath=value
    2. Look up the source namepath in the mapping table
    3. Mapped:   rewrite the namepath, resolve the target type from
                 the target .csaiv, and emit; if the value fails the
                 target constraint and the line carries an override,
                 emit the override constant (or !null) instead
    4. Unmapped: skip (dropped or out of scope - no error)

Then emit every constant line (targets with literal right sides)
not produced above, and assemble the output in target schema order.

The output is a canonical .daiv against the target schema: fully materialized, in target schema order (§2.6.17), ready for the target’s Validator with no further resolution. When the target reference is a registry path, the output carries a .!schema declaration naming it, so downstream validation is self-describing; source-side declarations do not carry over, except .? provenance source declarations, which survive alongside the per-value provenance they anchor. The mapper reads the source in one pass with the mapping table and both compiled schemas resident, collects the produced target lines, and assembles them in target schema order — a mapping may permute fields arbitrarily, so the working set is the schemas plus the produced output (output-sized, not merely schema-sized). It performs no corpus I/O.

8.5 Publish-Time Validation

A .maiv is validated against both schemas when published (and SHOULD be validated by tooling before use):

8.6 Composition

Mappings compose by joining on namepaths: given BA and CB, the composed CA replaces each source namepath of the second mapping with the corresponding source namepath of the first — string substitution, no synthesis. This is exactly the property that pure structural mapping buys. Overrides are the one lossy spot: a composed line carries a single override, while the two-hop chain distinguishes a value that fails the intermediate schema’s constraint from one that fails the final one. A composed mapping is therefore a faithful endpoint contract — its override fires on the final constraint only — not a replay of the chain’s intermediate checks; the .!via trail preserves the hops for a consumer that needs the exact chained semantics.

A composed, published mapping records its derivation with .!via declarations, one per hop in application order:

.!maiv
.!source acme/server-config
.!target helm/values-v1
.!via acme/server-config/mapto/k8s/deployment/v1
.!via k8s/deployment/mapto/helm/values-v1/v2

Each named hop is itself a published .maiv, so the trail is auditable; the hop endpoints are recoverable from the referenced mappings’ own .!source/.!target headers.

8.7 Auto-Derived Mappings

Schema extension produces mapping edges without a hand-written .maiv: when a schema extends a hub schema (§3.5.6), the extending fields are structurally identical to the hub’s by declaration, so the registry derives the edge at publish time (/ns::field=$::field for each hub field, in the encapsulated case). Hand-written mappings and auto-derived edges participate in the same graph; only hand-written ones carry overrides.

9 Parsing Requirements

This section specifies what a conformant Lexer (the regular-grammar token producer at the front of the pipeline) MUST and MAY do. The Lexer’s responsibility ends with token emission; the Compiler/Denormalizer/Validator stages described under §7 take over from there (their build/runtime placement: Appendix B.1).

9.1 Line Numbers

Lexers MUST track line numbers and include them with every emitted token.

9.2 UTF-8 Processing

Lexers MUST process input as UTF-8. Validation may occur:

9.2.1 BOM Handling

The UTF-8 Byte Order Mark is not supported. If a kaiv text begins with the bytes EF BB BF, the Lexer MUST raise a BOM_ERROR. BOM detection MUST occur before any other parsing action (including UTF-8 validation), and the BOM bytes MUST NOT be interpreted as part of the kaiv text.

9.2.2 Forbidden Characters

A kaiv text MUST NOT contain:

A Lexer encountering either MUST raise an INVALID_CHARACTER_ERROR. Range U+D800 – U+DFFF (UTF-16 surrogates) is implicitly excluded by valid UTF-8.

9.3 EOL

EOL is LF (U+000A) or CRLF (U+000D U+000A). Intermixing of LF and CRLF within the same kaiv text is tolerated. Canonical emitters write LF only (§12.3).

Every line in a kaiv text MUST be terminated with an EOL, including the final line. Without a final EOL, streaming Lexers cannot reliably distinguish incomplete transmissions from valid end-of-input. A Lexer encountering a non-empty final line without an EOL terminator MUST raise a MISSING_FINAL_EOL_ERROR.

An empty kaiv text (0 bytes) is valid and yields no tokens.

9.4 Whitespace Handling

In this specification, whitespace refers exclusively to:

Lexers MUST:

Whitespace within a key is not optional indentation but a key-character violation; it MUST raise an INVALID_KEY_ERROR. The = character is the only assignment operator at the lexical level — neither space nor colon is recognized.

9.5 Value Preservation

Lexers MUST treat all kaiv-text values as strings, and MUST preserve them verbatim. Lexers MUST NOT interpret any characters as having special meaning or initiating escape sequences. Specifically:

Values MUST NOT contain EOL characters. All other characters, including any leading or trailing whitespace, MUST be preserved verbatim.

9.5.1 Empty Values

When an assignment operator is immediately followed by EOL, the Lexer MUST emit the value as an empty string. The line KEY= is a valid KV token with key KEY and empty value.

9.6 Empty Documents

An empty text (0 bytes) is a valid kaiv text and yields no tokens.

9.7 Parsing Models

Lexers implement one of two models:

  1. Eager parsing — validate the entire text before emitting any tokens.

  2. Streaming parsing — emit tokens as lines are processed.

Lexers MUST document which model they implement, and MUST document their behavior when an error occurs:

Lines causing errors SHOULD NOT emit tokens.

9.8 Duplicate Keys

The Lexer MUST NOT detect or process duplicate keys — duplicate-key handling is an application-level concern. Multiple data lines with the same fully-qualified namepath produce multiple tokens, and the application chooses the resolution strategy. Common strategies:

Strategy Behavior
First wins Use the first occurrence; ignore subsequent values.
Last wins Use the last occurrence; override earlier values.
Concatenate Join all occurrences into a single string.
Preserve all Keep all values as a list or multiset.
Reject Treat duplicates as an error.

In the canonical pipeline, the Compiler uses last-write-wins for namespace-block field overrides (see §2.5) but does not enforce a global rule. The one schema-level exception is DuplicateKeySchemaError11.2): a repeated schema-declared field is a validation error, while data-introduced names — collection entry keys, fields undeclared in a relaxed schema — remain governed by this section.

9.9 Implementation Limits

This specification does not mandate maximum limits for line length, number of tokens in a document, key length, value length, or total document size. Implementations MAY impose reasonable limits based on available resources, but SHOULD document those limits and provide clear error messages when limits are exceeded.

10 Formal Grammar (Levels 0–1)

This section consolidates the line grammar into ABNF (RFC 5234). It is normative for Levels 0 and 1 plus the Level 0/1 subset of .saiv, .taiv, and .csaiv files. Where a prose section and this grammar disagree, the grammar wins; report the discrepancy. Level 2 adds only the table-declaration and collection-constraint productions given at the end; Level 3 adds only the ..lex[locale] span argument; the Level 4 declaration keywords appear only as reserved productions with opaque interiors (§6).

Two prose-level rules frame everything below:

  1. Line independence. The grammar is line-oriented and regular. Every line matches exactly one of the six rules (§1.3.1); no production spans an EOL. Block delimiters ([/@x], [], (/x), ()) are themselves single lines; the pairing of open/close lines is a Compiler concern, not a lexical one.

  2. Leading whitespace is stripped before classification on every line (§9.4); the productions below describe lines after that stripping. Whitespace around the = split is stripped; everything after = is verbatim.

10.1 Common Productions

eol           = LF / (CR LF)
ws            = SP / HTAB
any-char      = <any valid UTF-8 character except NUL, CR, LF>
                ; CR is permitted only as part of CRLF (§ Forbidden characters)

value         = *any-char        ; verbatim --- no escape sequences (§ Value Preservation)

bare-name     = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" )
quoted-name   = DQUOTE 1*( qn-char / (DQUOTE DQUOTE) ) DQUOTE
qn-char       = <any-char except DQUOTE>
name          = bare-name / quoted-name

index         = "0" / ( %x31-39 *DIGIT )
                ; no leading zeros: one canonical spelling per element

path-seg      = ( ALPHA / DIGIT ) *( ALPHA / DIGIT / "_" / "-" )
                ; library paths and schema IDs (std/net, acme/server-config,
                ; hub/log-entry). Note: allows "-", unlike bare-name. A "." in
                ; the first segment is reserved for future DNS-based authority
                ; (§ Type identity vs. type resolution) and currently invalid.
lib-seg0      = ALPHA *( ALPHA / DIGIT / "_" / "-" )
                ; alpha-first: a library path can never be mistaken for a
                ; version token (§ Format Declaration)
library-path  = lib-seg0 *( "/" path-seg )

prov-ident    = ( ALPHA / DIGIT / "_" ) *( ALPHA / DIGIT / "_" / "-" )
                ; provenance source IDs and data point IDs (sensor1, req-42, UUIDs)

10.2 Line Classification

document        = [ shebang-line ] *line
                  ; the format declaration is optional in authored
                  ; .kaiv (absent means authored .kaiv version 1);
                  ; canonical .raiv/.daiv always open with
                  ; .!raiv/.!daiv (§ Format Declaration)
shebang-line    = "#!" *any-char eol
                  ; physical first line only (§ Shebang Lines);
                  ; recognized before line classification --- never
                  ; a comment

line            = blank-line / comment-line / doc-line / declaration-line
                / content-line / metadata-line          ; rules 1--6 in order
blank-line      = eol                                    ; rule 1
comment-line    = "#" *any-char eol                      ; rule 2
doc-line        = "//" *any-char eol                     ; rule 3
; declaration-line (rule 4): begins ".!" or ".?" --- see § Declarations below
; content-line (rule 5): contains "=" --- split on the FIRST "="
; metadata-line (rule 6): no "=", first char one of "!", "?", "&"

Rule 5’s split on the first = is what makes the grammar regular: the left side is everything before the first = (with surrounding whitespace stripped), the right side is everything after (verbatim). One qualification: a line whose left side begins with " enters the quoted-name sub-state (§2.3) first, and an = inside the quoted name is part of the name, not the split point — the split is on the first = outside a quoted name. The sub-DFA already implies this; it is stated here because a naive byte scan for = would get "a=b"=v wrong. A second qualification: rule 6 has priority over the split for metadata-leader lines whose entire text parses as a metadata/constraint line (§1.3.1, rule-6 priority) — a pattern or enum item may contain = without making the line a content line. All assignment operators are recognized as suffixes of the left side: a left side ending in + is +=, ending in ; is ;=, ending in +: is +:=, ending in : is := (a key can never otherwise end in a single : — colons appear in keys only as the :: projection mid-path), ending in ? (in schema files) is ?=. Block-delimiter lines ([...] / [] / (...) / ()) contain no = at authoring level except inside table clauses, and are recognized before the rule-5 split by their bracket/paren first character; they are given under §10.7 below.

10.3 Declaration Lines (Rule 4)

declaration-line = format-decl / saiv-decl / taiv-decl / faiv-decl
                 / maiv-decl / msaiv-decl / schema-decl / types-decl
                 / units-decl / verbatim-decl
                 / registry-decl / provenance-req-decl
                 / ref-decl / compose-decl / bind-decl / unique-decl / fk-decl
                 / source-decl / target-decl / via-decl / drop-decl
                 / source-id-decl

version          = 1*DIGIT [ "." 1*DIGIT [ "." 1*DIGIT ] ]
                   ; omitted components are zero (§ Format Declaration)

format-decl      = ( ".!kaiv" / ".!raiv" / ".!daiv" )
                   [ 1*ws version ] eol
                   ; the keyword mirrors the file kind; bare form
                   ; means version 1 and is canonical
                   ; (§ Format Declaration)
saiv-decl  = ( ".!saiv" / ".!csaiv" ) [ 1*ws version ] 1*ws
                   ( library-path / url ) [ 1*ws "strict" ] eol
                   ; .!saiv authored, .!csaiv emitted by the
                   ; schema compiler; bare form means version 1 ---
                   ; version and identity are distinguished by
                   ; shape (§ Format Declaration)
taiv-decl    = ".!taiv" [ 1*ws version ] 1*ws library-path eol
                   ; faiv-decl (.faiv header) in § Unit Definition Files
maiv-decl     = ".!maiv" [ 1*ws version ] eol
                   ; bare form means version 1; no identity token
                   ; (§ Mappings --- identity is the endpoint pair)

; .maiv header declarations (§ Mappings):
source-decl      = ".!source" 1*ws ( library-path / url ) eol
target-decl      = ".!target" 1*ws ( library-path / url ) eol
via-decl         = ".!via" 1*ws library-path eol
drop-decl        = ".!drop" 1*ws maiv-namepath eol

schema-decl      = ".!schema" ( ":" schema-registry-ref
                              / 1*ws ( library-path / url ) ) eol
schema-registry-ref = [ ns-path 1*ws ] ( library-path / url )
                   ; .!schema:acme/x  |  .!schema hub/x
                   ; |  .!schema:/server hub/x
                   ; |  .!schema:/@arr hub/x  |  .!schema URL
                   ; the flat space-separated and colon forms are
                   ; equivalent for an unscoped ID; ns-path is
                   ; defined under § Content lines

types-decl       = ".!types" 1*ws library-path eol
units-decl       = ".!units" 1*ws library-path eol
verbatim-decl    = ".!verbatim" eol
                   ; no arguments; .kaiv and .raiv only --- values are
                   ; fully verbatim ("$" is a literal character; no
                   ; doubling, no references); carried into .raiv by
                   ; the Compiler, discharged by the Denormalizer
                   ; (§ Verbatim Documents)
registry-decl    = ".!registry" 1*ws path-seg "=" registry-base eol
registry-base    = url / fs-path
fs-path          = <a filesystem path --- absolute, or relative to the
                    document's directory (§ Type Registry Resolution)>
provenance-req-decl = ".!provenance:" ( "required" / "source" / "none" ) eol
source-id-decl   = ".?" prov-ident 1*ws uri eol

ref-decl         = ".!ref:" bare-name 1*ws library-path eol            ; reserved (corpus spec)
compose-decl     = ".!compose:" *any-char eol                          ; reserved (corpus spec)
msaiv-decl = ".!msaiv" [ 1*ws version ] 1*ws library-path eol
                                                                       ; reserved (corpus spec)
bind-decl        = ".!bind:" *any-char eol                             ; reserved (corpus spec)
unique-decl      = ".!unique:" *any-char eol                           ; reserved (corpus spec)
fk-decl          = ".!fk:" *any-char eol                               ; reserved (corpus spec)

url              = <an absolute http(s) URI per RFC 3986>
uri              = <a URI per RFC 3986, or an opaque non-whitespace identifier>

The reserved productions are recognized by the Lexer (keyword membership in §2.1.1) with their interiors deliberately opaque: their normative grammar belongs to the corpus specification (§6), and this document only pins that the keywords can never be reassigned.

10.4 Type References, Constraints, Units, Provenance

core-type     = "int" / "float" / "bool" / "null" / "b64" / "text" / "str" / "map"
                ; "map" is a structural type constructor (§ Map Type), not a
                ; std/core named type --- unlike the others it is not defined
                ; as str + constraints. Bare "!map" annotates authored map
                ; assignments; "map<T>" (map-type) is the schema form.
type-ref      = core-type / library-path "/" path-seg
                ; !int  |  !std/net/port --- last segment is the type name
type-name     = bare-name                     ; the &name= definition in .taiv
union-alt     = type-ref *inline-constraint [ ":" unit-expr ]
                                              ; constraints and the unit
                                              ; attach to the alternative
                                              ; they follow (§ Tagged Unions)
union-type    = union-alt *( "|" union-alt )  ; schema position; on an authored
                                              ; data line it is sugar the Compiler
                                              ; resolves to the active variant
                                              ; (§ Null Semantics) --- canonical
                                              ; metadata prefixes carry no union
csaiv-alt     = type-ref [ ":" unit-expr ] [ "(" *lowered-item ")" ]
                ; compiled form: each alternative's lowered definition +
                ; narrowing, concatenated whitespace-free (items are
                ; self-delimiting); bare when the group is empty. The
                ; unit rides the alternative, never the union --- an
                ; alternative without a unit demands none
csaiv-union   = csaiv-alt *( "|" csaiv-alt )
lowered-item  = pattern / span / range / enum / length
map-type      = "map<" type-ref ">"           ; schema annotation position only

pattern       = "/" pattern-body "/"
              / re-literal                        ; authoring-only alternative
pattern-body  = *( ( "\" any-char ) / p-char )
re-literal    = "re" re-sep *re-char re-sep       ; no escaping: re-char is any
re-sep        = ":" / ";" / "%" / "~" / "@" / "#" ; character except the line's
                                                  ; opening re-sep, "'", CR, LF;
                                                  ; lowered to the "/" form
p-char        = <any-char except "/", "\", and "'">
                ; the closing delimiter is the first "/" not preceded by "\";
                ; "\/" inside the body is passed to the regex engine verbatim
                ; (§ The Constraint Triple). Sole escape in the kaiv family.
                ; "'" is excluded --- like ep-char/em-char --- so the metadata /
                ; namepath delimiter is always the first "'" on a canonical
                ; line, keeping the split a single memchr (§ Parallel Scan
                ; Validation). A literal apostrophe in matched data is
                ; expressible as \x27 --- the hex escape, below.

range         = "[" [ endpoint ] "," [ endpoint ] "]"
endpoint      = 1*ep-char
ep-char       = <any-char except ",", "]", "'", SP, HTAB>
enum          = "{" enum-member *( "," enum-member ) "}"
enum-member   = 1*em-char
em-char       = <any-char except ",", "}", "'", SP, HTAB>
length        = "#" ( range / enum )

span          = "..num" / "..lex" / "..lex[" locale "]" / "..time" / "..ver"
locale        = <a BCP 47 language tag>       ; Level 3 only

inline-constraint = pattern / range / enum / length
                ; concatenated, whitespace-free, at most one of each kind;
                ; kinds are commutative predicates (§ Composability), but the
                ; canonical order emitted by tooling is pattern, range, enum,
                ; length

unit-expr     = unit-term *( ( "*" / "/" ) unit-term )
unit-term     = unit-factor [ "^" exponent ]
unit-factor   = "1" / unit-name / currency
unit-name     = 1*ALPHA                       ; a built-in (base/derived/prefixed)
                                              ; or kfaiv.com unit; ASCII letters,
                                              ; "u"=micro, "ohm"=Ω (§ Built-in units)
currency      = "~" 3( %x41-5A )              ; ISO 4217 shape: 3 uppercase letters
                                              ; (well-formed; membership unchecked)
exponent      = [ "-" ] %x31-39 *DIGIT
                ; negative exponents authoring-only; canonical exponents are
                ; positive integers >= 2 (§ Canonical form: ASCII-sorted factors)

provenance-list = provenance *( "," provenance )
                ; a value may carry several sources; canonical form preserves
                ; the authored (first-seen) order of the list -- it is not sorted
provenance    = prov-ident [ "@" timestamp ] [ "#" prov-ident ]
instant       = instant-ext / instant-basic
instant-ext   = 4DIGIT "-" 2DIGIT "-" 2DIGIT "T"
                2DIGIT ":" 2DIGIT ":" 2DIGIT "Z"
                ; 2026-07-28T14:00:00Z, 20 chars --- CANONICAL.
                ; The ":" here is unambiguous: a provenance run
                ; opens with "?" and runs to whitespace or "'",
                ; so it never reaches the unit sigil's branch.
instant-basic = 8DIGIT "T" 6DIGIT "Z"
                ; YYYYMMDDTHHmmSSZ, 16 chars --- DEPRECATED.
                ; Accepted through the 1.0-draft series, never
                ; emitted, removed at 1.0.

Regex dialect. Pattern bodies use a deliberately restricted dialect chosen to keep validation finite-state: literals, character classes [...] (with ranges and negation), ., anchors ^ $, grouping (...), alternation |, quantifiers * + ? {m} {m,n}, and the escapes \d \. \/ \\ and \xHH. Backreferences, lookaround, and lazy quantifiers are excluded — they are incompatible with the constant-memory DFA execution model (§B.3). An escaped ASCII letter or digit other than \d and \x (\1, \w, \s, \b, …) is outside the dialect and rejected — treating it as a literal would silently change the pattern’s meaning relative to the dialects these shorthands come from. The conformance suite exercises exactly this subset; a pattern outside it is an INVALID_CONSTRAINT_ERROR. A pattern body additionally may not contain a literal ' (p-char, above), which keeps the metadata/namepath delimiter the first ' on the line.

The \xHH escape — exactly two hexadecimal digits, either case, naming the ASCII character %x00%x7F — is the one letter escape beyond \d, admitted because it means the same character in every dialect that has it. It is valid both as an atom and as a character-class member or range endpoint ([\x20-\x7e]). A truncated form, a non-hex digit, or a value past 7F is outside the dialect. \x27 is how a pattern matches the one character its body cannot contain literally — the ' delimiter — which RFC-faithful types need: ' sits in RFC 3986’s sub-delims and in the e-mail address grammar.

10.5 Metadata Annotation Lines (Rule 6 — Authored Files Only)

metadata-line   = type-annotation-line / prov-annotation-line
                / named-annotation-line / constraint-line
                / faiv-def-line          ; .faiv only (§ Unit Definition Files)

type-annotation-line  = "!" ( union-type / map-type / type-ref *inline-constraint
                        [ ":" unit-expr ] ) [ 1*ws re-literal ] eol
                      / "!:" unit-expr [ "?" provenance-list ] eol
                        ; elided-type unit annotation (§ Elided-Type Unit
                        ; Annotation): inherits the governing head, else
                        ; float; no inline constraints, no re-literal
prov-annotation-line  = "?" provenance-list eol
named-annotation-line = "&" type-name *( 1*ws constraint-item ) eol

; constraint lines --- space-separated items above a .taiv &name=
; definition, or above a .saiv field definition as an anonymous
; refinement of the implicit str (value items only --- no "!"/"&"
; items on the .saiv form):
constraint-line = constraint-item *( 1*ws constraint-item ) eol
constraint-item = pattern / span / range / enum / length
                / "!" type-ref *inline-constraint [ ":" unit-expr ]
                / "&" type-name
                ; a re-literal item stands alone between whitespace
                ; boundaries --- never glued to a neighboring item

10.6 Content Lines (Rule 5)

Canonical data lines (.daiv / .raiv):

canonical-line   = metadata-prefix "'" canonical-namepath "=" value eol
metadata-prefix  = "!" type-ref *inline-constraint [ ":" unit-expr ]
                   [ "?" provenance-list ]
                 / "!:" unit-expr [ "?" provenance-list ]
                   ; the elided-type form is .raiv-only --- resolved by
                   ; the Denormalizer, never present in .daiv
                   ; (§ Elided-Type Unit Annotation)

canonical-namepath = canonical-steps "::" terminal    ; /a/b::f  |  /@a::0
                   / "::" name                        ; root field: ::f
canonical-steps  = 1*( "/" step )
step             = name / "@" name / index
terminal         = name / index

Structural constraints the regular grammar does not capture (enforced by the Validator against the schema, accepted by the Lexer): an index step or terminal is meaningful only directly after an @-step; in .raiv, $-field references may still appear in value position (§2.5.10), and the elided-type prefix !:unit may still appear — resolved by the Denormalizer, never present in .daiv2.7.4).

Compiled schema lines (.csaiv):

csaiv-line       = csaiv-field-line / collection-line / map-coll-line
                 / deleg-line
                 / declaration-line / comment-line / doc-line / blank-line
csaiv-field-line = csaiv-constraint "'" csaiv-namepath [ "?" ] "=" [ value ] eol
                   ; the right side is the compile-time-resolved
                   ; applicable default (§ Default Values); the
                   ; Validator ignores it
map-coll-line    = ns-path 1*( 1*ws map-clause ) eol
map-clause       = "[key::" pattern "]"
                 / "[" ( "min" / "max" ) "=" 1*DIGIT "]"
                   ; key-pattern and entry-count bounds are each
                   ; independently optional --- at least one clause
                   ; (§ Maps in the Compiled Schema); ns-path has no "@"
deleg-line       = ns-path 1*ws deleg-clause eol
deleg-clause     = "[schema::" schema-alt *( "|" schema-alt ) "]"
                   ; delegated namespace (§ Delegated Namespaces in the
                   ; Compiled Schema): the namespace's entire compiled
                   ; presence --- it contributes no field lines of its own
csaiv-constraint = csaiv-item *( 1*ws csaiv-item )
csaiv-item       = pattern / span / range / enum / length
                 / "!" ( csaiv-union
                       / type-ref [ ":" unit-expr ] )
                   ; the retained head type item (§ The Schema Compiler): the
                   ; authored head name ("!int", "!text",
                   ; "!std/time/datetime", "!acme/net/port"), optionally
                   ; unit-carrying ("!type:unit" --- the Validator
                   ; byte-compares the unit), or a union (the data-line
                   ; discriminant). The identity head "str" is
                   ; retained iff authored --- the identity declaration;
                   ; an unannotated field carries no type item, and its
                   ; lex-saver, when one is needed, is the vacuous
                   ; pattern "/^/" --- a constraint item, not a head.
                   ; Never "&".
                   ; canonical item order: head type, pattern, span,
                   ; range/enum, length
csaiv-namepath   = canonical-namepath
                 / canonical-steps "/" "::" name   ; element-level: /@servers/::host
                 / canonical-steps "::"            ; empty terminal: @ in steps ->
                                                   ; scalar-array element (/@ports::);
                                                   ; no @ -> map entry
                                                   ; (/config/settings::)

The elided index position (/@servers/::host, /@ports::) marks an element-level schema line — the constraint applies to every indexed element (§7.4).

Mapping lines (.maiv, §8):

maiv-map-line   = maiv-namepath "=" maiv-rhs eol
maiv-rhs        = "$" maiv-namepath [ "|" ( override-value / "!null" ) ]
                / value              ; constant injection / default; a
                                     ; constant starting with "$" is
                                     ; written with "$$" doubling
override-value  = <value not containing "|">
                  ; a literal "|" in an override constant is a
                  ; DelimiterCollisionError (§ Errors)
maiv-namepath   = [ maiv-steps ] "::" ( name / index )
maiv-steps      = "/" maiv-step *( "/" maiv-step )
maiv-step       = step / "*"
                  ; "*" (every element) is valid only directly after
                  ; an "@"-step, in the same position as an index

Authored data lines (.kaiv) and schema field definitions (.saiv):

kaiv-content     = kv-line / append-line / extend-line / struct-line
                 / append-struct-line / map-assign-line / var-def-line
                 / var-splat-line
kv-line          = key "=" value eol
key              = name / authored-namepath
authored-namepath = [ "/" ] step *( "/" step ) "::" ( name / index )
                    ; leading "/" conventional; bare segments tolerated in sugar
append-line      = array-path "+=" value eol
extend-line      = array-path ";=" ext-value *( ";" ext-value ) eol
ext-value        = <value not containing ";">
                   ; ";" inside data → use "+=" (§ Arrays)
struct-line      = ns-path ":=" ( pair *( "|" pair ) / ns-var-ref ) eol
append-struct-line = array-path "+:=" ( pair *( "|" pair ) / ns-var-ref ) eol
pair             = ( name "=" value-np ) / ( "@" name ( "+=" / ";=" ) value-np )
value-np         = <value not containing "|">      ; "|" in data → separate lines
array-path       = [ "/" ] *( step "/" ) "@" name
ns-path          = "/" step *( "/" step )

map-assign-line  = ns-path "=" map-value eol
                   ; namespace map assignment --- requires a !map
                   ; annotation above (§ Map Type). With a bare-name
                   ; key (root-level map) the line is an ordinary
                   ; kv-line; the !map annotation selects the
                   ; interpretation.
map-value        = "{}" / map-entry *( ";" map-entry )
map-entry        = map-key ":" map-val
map-key          = <text not containing ":" or ";">
map-val          = <text not containing ";">
                   ; ":" / ";" cannot appear literally in inline
                   ; entries --- author such entries as namespaced
                   ; field lines instead (§ Map Type)

var-def-line     = "." bare-name "=" value eol                        ; scalar
                 / "@." bare-name ( "+=" / ";=" ) value eol           ; array
                 / "/." bare-name ":=" ( pair *( "|" pair )
                                       / ns-var-ref ) eol            ; namespace

ns-var-ref       = "$/." bare-name
var-splat-line   = ns-var-ref eol
                   ; namespace-variable splat as a standalone line:
                   ; valid only inside an open section or namespace
                   ; block; expands the variable's pairs at that
                   ; point (§ Namespace-Variable Splat)

; reference tokens embedded in values (§ Field References, token
; boundaries): the token is the longest run of reference characters;
; a trailing separator is not part of it; the first character outside
; the set ends the reference and the remainder is literal text.
; In a document declaring .!verbatim (§ Verbatim Documents) these
; tokens do not exist --- every "$" in a value is literal.
hidden-ref       = "$." 1*ref-char
field-ref        = "$" ( ref-name / ref-path "::" ref-name )
ref-path         = ref-step *( "/" ref-step )
ref-step         = [ "@" ] ref-name
ref-name         = 1*ref-char                  ; bare names and indices
ref-char         = ALPHA / DIGIT / "_"

saiv-field-line  = key [ "?" ] "=" value eol
                   ; "?=" optional, "=" required; the right side is the
                   ; field's DEFAULT --- a kaiv value is never absent, only
                   ; empty, so an empty right side is the empty-string
                   ; default; the type annotation is the metadata line
                   ; above (§ Named Types in Schemas)
saiv-vector-line = array-path ";=" eol
                   ; scalar-array declaration: the annotation above
                   ; constrains every element; compiles to the
                   ; element-level `items'/@path::=` line

taiv-def-line    = "&" type-name "=" value eol
                   ; .taiv type definition: the right side is the
                   ; type's default value (empty for none); the
                   ; constraint lines are the metadata lines above
                   ; (§ Named Types)

10.7 Authored Structure Lines (Blocks — .kaiv / .saiv)

section-open   = "[" array-path [ 1*ws table-header ] "]" eol   ; [/@servers]
section-close  = "[]" eol
ns-block-open  = "(" ns-path [ 1*ws "schema:" schema-alt
                 *( "|" schema-alt ) ] ")" eol
                 ; a data block names ONE schema (the selection); a
                 ; parent-schema block may name the SET --- the
                 ; discriminated schema set (§ Namespace-Scoped Schemas)
schema-alt     = library-path / url
ns-block-close = "()" eol

; Level 2 table headers and .csaiv collection-constraint lines:
table-header   = table-clause *( 1*ws table-clause )
table-clause   = clause-group / card-clause
clause-group   = clause-spec *( "|" clause-spec )    ; id=!|customer=/@customers/*::id
clause-spec    = unique-spec / fk-clause
unique-spec    = name "=!" *( "," name "=!" )        ; [/@servers host=!,port=!]
fk-clause      = name "=" fk-path                    ; department=/@departments/*::name
card-clause    = ( "min" / "max" ) "=" 1*DIGIT
fk-path        = "/" *( step "/" ) "@" name "/*::" name

collection-line = array-path 1*ws coll-clause-group *( 1*ws coll-clause-group ) eol
coll-clause-group = coll-clause *( "|" coll-clause )
coll-clause    = "[unique::" name *( "," name ) "]"
               / "[ref::" name "=" fk-path "]"
               / "[" ( "min" / "max" ) "=" 1*DIGIT "]"

; .saiv map schema blocks (ostensive key constraints,
; see Maps in the Compiled Schema):
map-block-open = "(" ns-path *( 1*ws card-clause ) ")" eol
                                       ; (/name min=1 max=40)
map-key-line   = pattern "=" eol       ; /^[a-z]{2,3}$/= --- recognized
                                       ; whole-line ahead of the rule-5
                                       ; split (§ The Six Rules): the
                                       ; pattern body may contain "="

In table headers, min and max are reserved words: a clause of the form min=N or max=N (with N all digits) is always a cardinality clause. An array element field literally named min or max MUST use a quoted name ("min"=!) to appear in a table header. Together with the pattern-literal introducer re (reserved in leading name position of .saiv/.taiv content lines — §2.6.11), this is the complete reserved-word set.

10.8 Values

There is no value grammar beyond value = *any-char: values are verbatim byte sequences (§9.5). Typed interpretation of a value (integer syntax, base64 alphabet, ISO 8601 shape) is a constraint check performed by the Validator against the compiled pattern — never a lexical concern. The two-layer split is exact: the Lexer knows lines, the Validator knows constraints, and nothing in between parses values.

11 Errors

This section catalogs the error conditions a conformant implementation detects. Errors are grouped into two categories:

In case of multiple errors on the same line, Lexers MUST report the highest-priority error (in the order listed below), and MAY report further errors from the same line.

11.1 Lexer Errors

Error Condition Notes
BOM_ERROR Text begins with the UTF-8 BOM EF BB BF. Detected before line parsing begins; reported without a line number.
INVALID_UTF8_ERROR Input contains invalid UTF-8 sequences.
INVALID_CHARACTER_ERROR Standalone CR (U+000D) not part of CRLF, or NUL (U+0000).
MISSING_FINAL_EOL_ERROR The final non-empty line lacks an EOL terminator. MAY be reported without a line number.
INVALID_VERSION_ERROR The version following a format declaration keyword does not match ^[0-9]+(\.[0-9]+){0,2}$. The version is optional for every kind (the bare form means version 1). On the identity-carrying kinds (.!saiv / .!csaiv / .!taiv / .!faiv / .!msaiv) a digit-first first token is read as the version slot (an identity is alpha-first by grammar), so a malformed digit-first token — 1.2.3.4, 1x — is this error, not an identity error. See §2.1.2 — omitted components are zero, so 11.01.0.0.
UNSUPPORTED_VERSION_ERROR The version is well-formed but not one supported by the implementation. A 1.x implementation MUST NOT raise this for any earlier 1.y where y < x.
FORMAT_KIND_ERROR A stream consumed as a kind whose declaration is not optional — the canonical kinds (.raiv, .daiv, .csaiv) and the REQUIRED-header authored kinds (.saiv, .taiv, .faiv, .maiv) — does not open with the matching format declaration: the declaration is absent, or names a different kind (e.g. a .!raiv stream fed to a .daiv consumer). Only authored .kaiv may omit the declaration (§2.1.2). Raised by the consuming stage’s Lexer, which is told the expected kind. This is what keeps the family self-describing: kind checks need no filename or out-of-band context.
EMPTY_KEY_ERROR A data line starts with = (optionally preceded by whitespace) — the key is empty or pure whitespace.
MISSING_OPERATOR_ERROR A line that is neither blank, comment, declaration, structure line, variable-splat line, .csaiv collection-constraint line, nor metadata-annotation does not contain =.
INVALID_KEY_ERROR A bare key — or any unquoted segment of an authored namepath — does not match bare-name = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" ): e.g. a leading digit (9port), a hyphen (retry-count), or a dot (a.b). Or a quoted key is empty or contains a literal " other than as the "" doubling. The Lexer MUST validate every bare namepath segment and raise this on violation. An index segment (index: all digits, no leading zero) is bare by definition and exempt from the bare-name shape. Names with -, a leading digit followed by non-digits, or other out-of-set characters MUST be quoted (§2.3). path-seg (which permits - and a leading digit) applies only to library paths, schema IDs, and provenance identifiers — never to data names. A reserved word used unquoted where it is reserved also raises this error: re in leading name position of a .saiv/.taiv content line (§2.6.11), and min/max as unquoted field names in a Level 2 table header (§10.7). A .saiv map-key line is exempt: its key is a whole pattern, not a name (§1.3.1).
INVALID_DIRECTIVE_ERROR A line beginning with .! (after stripping leading whitespace) does not begin with a keyword from the §2.1.1 table: .!kaiv, .!raiv, .!daiv, .!saiv, .!csaiv, .!taiv, .!faiv, .!maiv, .!msaiv, .!schema, .!source, .!target, .!via, .!drop, .!types, .!units, .!verbatim, .!registry, .!provenance, .!ref, .!compose, .!bind, .!unique, .!fk. The inventory table in §2.1.1 is authoritative; this list mirrors it.
INVALID_CONSTRAINT_ERROR A constraint clause does not match the constraint productions (range [min,max], enum {a,b,c}, length #[…], etc.), or a unit annotation names a unit that resolves against neither the built-in set nor an imported .faiv library (§2.7.10). Malformed clauses are Lexer-detected wherever constraints appear — schemas, .taiv constraint lines, and metadata annotations in authored .kaiv. Unknown-unit membership is Lexer-detected when no .!units import leaves the namespace open (closed built-in set), and Compiler-detected otherwise (resolution required).

Lexers SHOULD reference line numbers in error reports.

11.2 Application Errors

Schema-related application errors come in two naming conventions:

Compiler-stage errors (resolution and expansion failures in authored text) carry plain descriptive names.

Error Condition Detected by
MetadataWithoutTargetError A metadata annotation in authored .kaiv is not followed by a data line before the next blank line, comment, or EOF. A stack of at most one type-designating annotation (!type… or &name) plus at most one provenance annotation above a single data line is permitted (§1.3.4); a second annotation of the same kind also raises this error. Compiler
UndefinedReferenceError In a document not declaring .!verbatim2.5.6): a value references a hidden variable ($.x, [email protected], $/.x) or a data field ($field, $path::field) that is not defined on an earlier line (§2.5.4); or a value contains a $ that begins neither a well-formed reference nor the $$ doubling. Not possible in a verbatim document — every $ there is literal. Compiler
VariableContextError In a document not declaring .!verbatim: a container-variable reference appears where its expansion cannot be placed: $/.name outside the two splat positions (§2.5.2), or [email protected] in a scalar position. Also raised for a field reference ($field, $path::field) inside a hidden-variable definition’s value — variables resolve before the referenced field is guaranteed stable, so they cannot capture field values. In a verbatim document the machinery itself is banned — see VerbatimContextError. Compiler
VerbatimContextError A document declaring .!verbatim2.5.6) contains reference machinery in a positional form: a variable-definition line (.name=, @.name+=/;=, /.name:=), a standalone splat line ($/.name), or a compound-form right side that is entirely a splice (a :=/+:= right side $/.name; a +=/;= value exactly [email protected]). A $ inside value text is never this error — it is a literal character. Also raised when .!verbatim itself is misused: carries arguments, repeats, follows a content line (declarations precede content, §2.1), or appears in a kind other than .kaiv/.raiv (there, by the consuming stage). Compiler; consuming stage (out-of-place declaration)
DelimiterCollisionError A compound-form value collides with that form’s delimiter: a :=/+:= pair value containing | (the segment after the split is not a well-formed pair), or an inline map entry without : after a ; split. The colliding character is representable only in the single-value key=value form (§9.5). (A ; in ;= data is not a detectable collision — it simply delimits further elements, which is why such values are authored with += instead.) Also raised where a retype would reinterpret delimiter bytes: the str→text coercion on a value carrying a literal |:|2.6.4), and a .maiv override constant containing |8.3). Compiler; Denormalizer / Validator (text coercion); mapper, at publish (override)
UnitConversionError An authored value in a same-dimension unit cannot be converted exactly into the field’s declared unit: the exact decimal result is non-terminating, or violates the field’s type — e.g. a non-integer result on an int-derived field (§2.7.3). Denormalizer
SchemaDuplicateKeyError A .saiv schema text contains duplicate field definitions. Schema compiler
SchemaResolutionError A .!schema, .!types, .!source, .!target, or .!via reference cannot be retrieved from the specified URL or registry. Compiler / mapper
RegistryStrictError Strict resolution mode is enabled and a document-level .!registry declaration would determine the base of a resolved artifact (§[sec:trust-model-strict-resolution]). Raised before any retrieval is attempted. Resolver, strict mode only
SchemaInheritanceCycleError A .!schema inheritance chain among .saiv files — or a delegation-member chain (§3.5.3) — revisits a schema already in the chain (§3.5.6). Schema compiler
SchemaDelegationError A parent schema both delegates a namespace and declares fields under it, or declares an empty or duplicate-member schema set (§3.5.3). Schema compiler
DelegationSchemaError A document whose schema delegates a namespace carries no scoped .!schema declaration for it, or the declaration names a schema outside the declared set (§7.6). Denormalizer / Validator
SchemaOptionalWithoutDefaultError An optional field (?=) whose resolved default is inapplicable (fails the field’s own constraints, §[sec:default-values]) and whose type does not admit !null — the Denormalizer would have nothing to materialize for an absent instance. Schema compiler; Denormalizer, on a stale .csaiv predating the compile-time check
RequiredFieldSchemaError At build time: the authored data omits a field declared required (=) — the Denormalizer materializes optional fields only (§2.6.17). At validation time: a schema-declared field has no corresponding .daiv line — materialization guarantees every declared field a line, so a missing line of any kind fails the parallel scan. Denormalizer / Validator (Pass 1)
DuplicateKeySchemaError A data text contains two or more entries for a single field defined in the schema — a name the schema declares exactly, or a group field repeated within one namespace-array element (§7.1). Detection checks the scanned line against the schema, so memory stays schema-bounded. (Data-introduced names — fields not defined in a relaxed schema, and collection entry keys — are unaffected; their duplicate handling is application-level, §9.8.) Validator
UndefinedFieldStrictSchemaError A data text contains an entry for a field not defined in a strict schema. Validator
TypeMismatchError The data line’s type annotation fails the field’s nominal requirement (§7.1): no union alternative matches the discriminant; a !null or std/enc-typed line meets a non-null / non-embed head (or vice versa); a non-text annotation meets a !text head (or !text data meets any other head); a non-str annotation meets an explicit !str identity head; or the canonical unit is not byte-identical to the declared one. A structurally-checked head raises this only for !null-, !text-, or std/enc-typed data lines; any other annotation is accepted, with the head’s constraint group governing the value — a wrong-shaped value fails as ConstraintViolationError instead. Also raised by the Compiler when a union-annotated authored value satisfies no alternative (active-variant resolution, §2.6.17). Validator; Compiler
ConstraintViolationError A data value fails a pattern, range, enumeration, or length constraint. Validator
IncompleteMappingError A .maiv leaves a required target-schema field unproduced: no mapping line, no constant line, and no applicable target-schema default (§8.5). Checked statically at publish time. Mapper / registry publish
ProvenanceSchemaError A data line violates the schema’s .!provenance requirement level (§2.4.3): missing source/timestamp under required/source, or any provenance under none. Also raised by the schema compiler when a required/source level is combined with an optional field — materialized lines carry no provenance, so the combination is statically unsatisfiable. Validator; Schema compiler
UniquenessViolationError A field declared [unique::field] (Level 2) has duplicate values across array elements. Pass 2 (application layer, §4.7)
ReferentialIntegrityError A field declared [ref::field=/@path] (Level 2) has a value not present in the referenced field set. Pass 2 (application layer)
CardinalityViolationError An array’s element count violates min=N or max=M. Validator (Pass 1)
CollationUnsupportedError A Level 0–2 runtime encountered ..lex[locale] and was configured to reject (rather than fall back to bare ..lex); or a Level 3 validator encountered a ..lex[locale] constraint it cannot evaluate exactly per §5.3 — an unsupported locale or tailoring, a well-formed tag whose primary language subtag has no CLDR 48 tailoring, or an unrecognized -u- extension key — and rejected it under the honest-partial rule rather than order differently. Validator (Levels 0–3)

Strict-vs-relaxed schemas: schemas are relaxed by default — data texts MAY contain fields not defined in the schema, without any UndefinedFieldStrictSchemaError being raised for them. A schema marked strict requires that every field in the data text is declared. The strict modifier is the literal keyword strict as the final space-separated token of the .!saiv declaration:

.!saiv acme/server-config strict

The schema compiler carries the declaration — identity, any modifier, and any explicit non-1 version — into the .csaiv header, rewriting the keyword to .!csaiv2.1.2); that header is where the Validator reads the modifier. A .csaiv whose header carries no strict modifier is relaxed.

12 File Representation

12.1 File Extensions

Files SHOULD use the corresponding extension from the family:

Extension Role
.kaiv authored data
.raiv relational canonical
.daiv denormalized canonical
.saiv authored schema
.csaiv compiled schema
.taiv type library
.qaiv query
.msaiv metaschema (reserved — corpus specification, experimental; §6)
.faiv unit definitions (factors)
.maiv mapping

12.2 File Encoding

kaiv files MUST be encoded as UTF-8 without a Byte Order Mark.

12.3 Canonical Emission

Canonical artifacts are deterministic at the byte level. The pipeline stages that emit canonical kinds — the Compiler (.raiv), the Denormalizer (.daiv), and the schema compiler (.csaiv) — MUST:

Consequently, two conforming builds of the same inputs MUST produce byte-identical canonical artifacts — the file-level extension of the per-line determinism the format already guarantees (single index spellings, deterministic quoting, canonical unit forms, exact-decimal unit conversion). This is what makes canonical artifacts content-addressable: a hash of the .daiv bytes identifies the build’s output with no normalization step. Input tolerance is unaffected — consumers of canonical files still accept CRLF and blank lines (§9.3, §9.4); emission discipline binds producers, not readers, and DFA processing is unchanged.

12.4 Media Types

Two media types are defined for kaiv content, following the XML precedent (RFC 7303). The choice depends on the intended use of the content.

Context Recommended type Rationale
API payloads, configuration delivery, machine-to-machine, streaming pipelines application/kaiv Safe default; preserves exact byte-for-byte content; no risk of EOL normalization or stripping.
Display in browsers, email, documentation systems, debugging output text/kaiv Signals human-readability; may enable inline display or syntax highlighting.

When in doubt, use application/kaiv — it is the safer choice for preserving data integrity across transport layers.

12.4.1 Provisional Types

Until IANA registration (RFC 6838) is obtained, implementations SHOULD use the provisional forms:

12.4.2 Parameters

Both media types support the following optional parameters:

12.4.3 Transport Considerations for text/kaiv

The text/ top-level type carries inherent risks that senders MUST consider:

Senders SHOULD use application/kaiv when exact byte-for-byte preservation is required, or when the transport path is not fully trusted to preserve text/ semantics.

12.5 Shebang Lines

A shebang line is an optional first line in a kaiv file specifying how to execute the file as a script. It typically takes the form:

#!/path/to/interpreter [optional argument]

Rules:

A Terminology

This appendix is the single source of truth for kaiv terminology, consolidated from the specification repository’s former TERMINOLOGY.md (now a pointer here). It restates some body material in glossary form for quick reference; on any conflict the body text and the formal grammar (§10) win. A few entries — the ecosystem services and the time-series file types (.aaiv, .iaiv) — describe surface outside this specification’s conformance boundary; they are included so the family’s terminology has one home.

A.1 The Filesystem Analogy

The core structural concepts of kaiv map directly onto the Unix filesystem:

kaiv concept Filesystem analog Key property
namespace directory A container; you can descend into it
namespace path directory path An address pointing to a container — the steps of a namepath
field file A leaf value; terminal, nothing below it
namepath (= field address) file path The full address of a value: the steps plus the :: projection

The / operator works exactly like the Unix path separator — it means “descend from root” and “separate path segments.” The :: operator has no filesystem analog; it marks the transition from the tree (containers) to a leaf value.

A namespace path (the steps) always points to a namespace (container). A namepath always points to a value (leaf): per the formal grammar (canonical-namepath, §10), the :: field projection is the final, terminal step of the namepath — the boundary between container addressing and leaf access. Field address is a synonym for namepath, used when the leaf-addressing role needs emphasis.

A.2 Core Structural Concepts

Term Definition
namespace A data structure — a container that holds fields and/or other namespaces. The directory in the filesystem analogy. Namespaces use the / prefix. Examples: /server, /app/db, /@servers/0 (a namespace array element). Arrays are a special form of namespace that uses integer (index) fields.
namespace path The container-addressing portion of a namepath — a path of namespace names (the steps; canonical-steps in the formal grammar). Always points to a container, never to a leaf value. The directory path in the filesystem analogy. Examples: /server, /app/db, /@servers/0.
namepath The full address of a leaf value within the tree: the namespace steps followed by the :: field projection and a terminal name or index. Always points to a value. The file path in the filesystem analogy. Examples: /server::host, /@servers/0::port, ::host (root field — the steps are empty). Synonym: field address.
name A single identifier at one level of the tree; the scope-local label for a node. Each /-separated segment of the steps, or the terminal after ::.
field A named leaf value inside a namespace. The file in the filesystem analogy. Accessed via the :: field projection operator. A field is always terminal — you cannot descend further. Examples: ::host, ::port, ::timeout_ms.
field address Synonym for namepath — the steps plus the field projection, the full address of a leaf value. Always points to a value, never to a container.
field projection The operation of selecting a field from a namespace, extracting a scalar value from a tree node. Uses the :: operator. Always the last step in a field address — after ::, the path is terminal.
literal A leaf value in the data model — either a scalar field or a scalar array element. Literals are the terminal nodes of the tree; they are always reached via :: field projection.
scalar array A named, indexed collection of scalar values; each element is a leaf reached via ::index field projection, where the numeric index is the field name. Syntax: @name::index.
namespace array A named, indexed collection of namespaces (interior nodes); each element is an interior node descended into via /index, with its own fields reached by further :: projection. Syntax: @name/index.
mixed array A named, indexed array whose elements may be either scalar (@name::index=value) or namespace (@name/index::field=value). The operator before the index — :: for scalar, / for namespace — discriminates element kind per element.

A.3 Operators and Annotations

Symbol Formal name Role Example Survives
/ tree descent operator Root descent and path separator between namespace segments. Marks that what follows is a container (namespace or array) addressed from root. Present in both authored and canonical form. /server, /@servers/0 [x]
:: field projection operator Accesses a named leaf field inside a namespace. Always terminal in a field address — nothing can follow ::name. ::host, /server::host [x]
@ array annotation Part of the identifier — marks that the named container is iterable by index. @tags is the name; the @ is permanent. A root-level array uses /@arr. @tags, /@ports [x]
. hidden name prefix Part of the name for variable definitions — marks a hidden variable, elided from canonical output. Unix hidden-file convention. .host=localhost [ ]
$ dereference operator Two roles. $.name (variable dereference): substitutes a hidden variable — resolved by the Compiler, does not survive to .raiv. $field / $path::field (field reference): substitutes another field’s value — survives into .raiv, expanded by the Denormalizer into .daiv. Absent entirely in a verbatim document (§2.5.6). $.host, $server::host [ ]
& named type reference Short-form type annotation in authored files. Resolved to !library/path/typename by the Compiler. Never appears in canonical form. &int, &port [ ]
' apostrophe delimiter The type–namepath boundary in canonical form: on every canonical data line, ' separates the metadata prefix (type plus optional provenance list) from the namepath. Split always on the first bare '. !str'::host=x [x] (canonical only)
? provenance annotation Per-value source attribution. Authored .kaiv: a standalone metadata line. Canonical .daiv/.raiv: an inline provenance list before '. References source IDs declared via .?id uri. ?sensor23 [x]

A.4 Sigil Categories

Category Sigils Survives Why
Structural operators /, :: [x] Carry addressing information needed by the DFA and queries
Structural annotations @ [x] Part of the identifier — marks iterability
Resolution sigils ., $ [ ] Authoring mechanisms resolved by the Compiler (. and $.var) or by the Denormalizer ($field)
Authoring-only type ref & [ ] Resolved to !library/path/typename by the Compiler

The DFA dispatch after ' in canonical form follows from these categories; the dispatch table is in §1.4.1.

A.5 File Types

Ext Full name Role
.kaiv kaiv — authored data file Human-written data. May contain variables, namespace blocks, inline syntax, and other authoring conveniences. Source code.
.daiv Denormalized Attributed Information Values — canonical data file Machine-generated deployment artifact. One !type'namepath=value line per field, fully qualified, in schema-declared order. No variables, no blocks, no authoring sugar, no $field references. Output of the Denormalizer.
.raiv Relational Attributed Information Values — relational canonical form Like .daiv but field references ($path::field) are preserved rather than resolved. Output of the Compiler. For design tools, diffing, auditing, and converters.
.saiv authored schema file Human-written schema. Field definitions are key=default content lines (?= for optional fields; root fields are bare keys), optionally preceded by a type-annotation metadata line.
.csaiv compiled schema file Machine-generated compiled schema. Single-line constraint'namepath= format (?= marks optional fields; the right side carries the field’s resolved default). The artifact consumed by the Validator.
.taiv type library file Named type definitions. &name= pairs defining types in terms of str plus constraints.
.qaiv query file Queries over kaiv data using the qaiv query language. qaiv is derived from quarb (quarb.org) — shared surface syntax, separate streaming-DFA engine, plus kaiv-specific predicate sugars. See kaiv/QUERY.md §4 (in the spec repository).
.faiv unit-definition file Factor Attributed Information Values — custom unit libraries (§2.7.9): each definition is a dimension (by unit reference) plus a conversion factor to the dimension’s base unit; currencies ($ dimension) carry a rate-source URL template instead. Served from kfaiv.com; imported with .!units.
.maiv mapping file Structural correspondence declarations between two schemas (§8). A mapping has no name of its own: its identity is the .!source/.!target endpoint pair, and its registry address derives from it (§8.2). Each mapping line assigns a $-reference into the source schema (or a literal constant) to a target namepath — a pure namepath rewrite with no value transformation beyond an optional override constant.
.msaiv metaschema file (reserved) Corpus-level contract (Level 4 — reserved here, §6; specified separately in the experimental corpus specification, corpus/DRAFT.md in the spec repository). Declares schema bindings, corpus-wide uniqueness, and cross-document foreign keys over a corpus of .daiv files.
.aaiv aggregate file (time-series; ecosystem) Immutable, time-partitioned file holding many instances of one schema, each data point identified by its #dpid. Used by the chraiv.com services; specified in the sibling chraiv-spec repository. Outside this specification’s conformance boundary.
.iaiv predicate index file (time-series; ecosystem) Hash-bucketed sorted predicate index for structured time-series queries; binary-searchable. Used by the chraiv.com services; specified in the sibling chraiv-spec repository. Outside this specification’s conformance boundary.

A.6 Pipeline Stages

Term Definition
authored form The human-written representation (.kaiv, .saiv, .taiv). May contain variables, namespace blocks, inline syntax, and other authoring conveniences. Input to the Compiler.
canonical form The fully denormalized, machine-generated representation (.daiv). No variables, no blocks, no authoring sugar, no $field references. One line per field, fully qualified. Output of the Denormalizer.
$.var (variable dereference) A variable reference in authored .kaiv. The .var was defined with .var=value. Resolved by the Compiler during compilation; does not survive to .raiv. Pure authoring sugar.
$field (field reference) A reference to the value of another field in the same document — the data is normalized because it refers to a value by name instead of repeating it. Survives compilation into .raiv; the Denormalizer expands it, replacing the reference with the actual value. That is what “denormalization” means in the database sense.
verbatim document A document declaring .!verbatim2.5.6): values are fully verbatim ($ literal, $$ two dollars, no references) and the reference machinery is banned. The declaration is carried into .raiv and discharged by the Denormalizer — the .daiv output is byte-identical to the escaped-authored equivalent’s.
Compiler The first stage (build-time). Reads .kaiv, resolves variables, expands blocks and inline syntax, resolves &name annotations, prepends :: to root-level fields, and produces .raiv (field references preserved). Does not require the schema. Never runs on the certified target.
Denormalizer The second stage (build-time). Reads .raiv and expands $field references. Schema-aware: when the data declares a schema, it also materializes absent optional fields (the resolved default or !null) in schema-declared order, and lifts untyped lines to each field’s retained head type (§7.3) — for !text heads under the guarded str→text coercion — so .daiv carries the schema’s types on its own lines. An absent required field is a build-time RequiredFieldSchemaError. Produces .daiv.
Validator The third stage (build-time). Reads .daiv and the compiled schema (.csaiv). Performs schema constraint checking, field existence and order verification, obligation enforcement, and provenance enforcement. Produces pass/fail. Never runs on the certified target.
integrity check Optionally re-running the Validator’s validation logic on an existing .daiv at deployment or runtime. Same parallel scan against .csaiv, same constant-memory properties — different point in time. Not a separate pipeline stage.
parallel scan The Validator’s mechanism: reading .daiv and .csaiv line-by-line in lockstep, checking each canonical data line against the corresponding compiled constraint. A strict lockstep walk — materialization guarantees every schema-declared field a .daiv line — with only empty-collection element lines skippable. O(1) memory per field. Also used by the integrity check.

A.7 Type System

Term Definition
str The single primitive type — every value between = and end-of-line is a string of characters. All other types are named types defined in terms of str. Type annotation: !str.
named type A reusable type defined in a type library (.taiv) in terms of str plus optional constraints. All types other than str are named types — including int, float, bool, null, b64.
type annotation The !type prefix on a canonical data line or metadata annotation line. !int, !float, !str, !bool, !null, !b64, !text are canonical std/core shorthands; non-core types use !library/path/typename.
constraint triple (pattern, span, range) — the universal type constructor (§2.6.11). Every type is str narrowed by zero or more of: a pattern (/regex/), a span ordering, and a range. An enumeration ({a,b,c}) composes as a further narrowing constraint; a length constraint (#[min,max]) restricts character, element, or decoded byte count.
std/core The standard type library — always implicitly imported, frozen after publication, shipped embedded. Defines int, float, bool, null, b64, text. Canonical shorthands do not expand further.
std/enc The encoding library — named types derived from b64 that type the decoded payload (json, yaml, toml, xml, html, md, csv, bin, plain). Shipped embedded, imported explicitly; canonical form is the full path.
std/time The time library — RFC 3339 shapes with ..time chronological ordering (datetime, localdatetime, date, time). Shipped embedded, imported explicitly.
std/num The numeric-markers library — IEEE non-finite values as enum types (inf, nan). kaiv floats are deliberately finite; an extended-real field is the union idiom !float|std/num/inf. Shipped embedded, imported explicitly.
span ordering Declares how range constraints are evaluated: ..num numeric, ..lex byte-lexicographic, ..lex[locale] locale-aware (Level 3), ..time chronological, ..ver semantic version.
!null The canonical representation of null: !null'::field= — the value after = is always empty. Distinct from the empty string (!str'::field=). Nullability is explicit and opt-in via !null|T.
type resolution The build-time process of locating the .taiv file that defines a named type. Distinct from type identity: a type’s canonical path is fixed regardless of which registry serves its file.
unit An annotation attached to a numeric (..num) type via :, declaring the physical or economic quantity (§2.7). Compound units use * / ^ with a strict no-whitespace grammar; canonical form is ASCII-sorted. The Validator byte-compares units and never converts; the schema-aware build converts authored same-dimension units into the field’s declared unit (exact decimal arithmetic only). The elided-type annotation !:unit inherits the governing head, else float.
dimension The classification of a unit by physical character. Seven SI base dimensions plus the currency dimension $; compound units derive their dimension from their factors. The dimensionless unit 1 has the empty dimension product.
base unit The reference unit within a dimension (conversion factor 1). Conversion factors live in unit definitions and are not consulted by the Validator.
currency unit A unit variant for monetary value: ~ plus an ISO 4217-shaped code (~EUR). All currency units share dimension $; definitions deliberately omit conversion factors — exchange rates are external and time-varying.

A.8 Schema and Validation

Term Definition
caiv The shared line grammar underlying every file in the kaiv family (§1.3): six rules — blank, # comment, // doc comment, .!/.? declaration, =-split content, metadata annotation. Every file type is caiv plus an interpretation of the two sides of =. Not a file extension. Pronounced “cave.”
table definition A schema-level declaration for an array of namespaces with collection-level constraints — uniqueness, referential integrity, cardinality (§4). Written [/@name constraints]…[] in .saiv.
unique constraint field=! declares a single-field unique key; f1=!,f2=! a compound key — distinct across all elements of the array.
foreign key reference field=/@path/*::field — the fixed fk-path shape pinned in the formal grammar as the Level 2 subset of qaiv path syntax.
cardinality constraint min=N / max=M bounds on an array’s element count.
identity declaration An authored !str annotation on a schema field: retained in the .csaiv as a nominal head, so only str-typed data lines satisfy the field — the opt-in “plain string, reject asserted types” contract. Distinct from an unannotated or anonymously refined field, which carries no head and accepts structural assertions; such a field’s lex-saver, when one is needed, is the vacuous pattern /^/.
discriminated schema set The one-of-N contract a parent schema declares for a delegated namespace (§3.5.3): the parent’s ostensive block names the member schemas, the data’s block annotation selects one, the canonical document carries the selection as a scoped .!schema declaration (the discriminant), and the selected member’s compiled contract governs inside the namespace. Compiled form: the [schema::…] delegation line.

A.9 Provenance

Term Definition
provenance Source attribution metadata on data lines (§2.4): the ?sourceID@timestamp#dpid form in the canonical metadata prefix — where a value came from, optionally when it was observed, and optionally a stable data-point identifier.
provenance source A named external source — URI, sensor ID, URN, or any opaque string. Declared via .?id uri at file top; referenced via ?id on value lines.
temporal attribution The optional @instant qualifier recording when a value was observed — dashed extended ISO 8601 UTC, YYYY-MM-DDTHH:MM:SSZ, 20 characters. The compact 16-character basic form is accepted but deprecated, and is removed at 1.0.
data point identifier The optional #dpid qualifier — any valid identifier, not restricted to UUIDs. The time-series platform auto-assigns UUID values at ingest.
.? declaration A document-level declaration mapping a short provenance ID to a full URI (.?id uri). Placed at file top; preserved unchanged in .daiv and .raiv.

A.10 Higher-Level Concepts

Term Definition
mapping (.maiv) A structural correspondence declaration between two schemas (§8) — a pure namepath rewrite reusing = assignment and the $ sigil. Enables conversion between any two schemas connected through the mapping graph.
hub schema A hub/ namespace schema on ksaiv.com serving as a structural archetype for the mapping graph — common data shapes (server endpoint, credentials, postal address) that many domain schemas map to. Curated.
ktaiv.com The immutable production type-library registry (.taiv). Published content is never modified. Default base for type resolution; overridable per prefix via .!registry.
ksaiv.com The immutable production schema registry (.saiv, .csaiv, .maiv — including the hub/ namespace).
kdaiv.com The immutable production document store (.kaiv, .raiv, .daiv). See ECOSYSTEM.md (in the spec repository).
kfaiv.com The unit-definition registry (.faiv), paralleling ktaiv.com for types. Built-in units ship embedded and need no lookup.
mapping graph The graph of registered schemas connected by .maiv declarations. Enables transitive conversion: if A and B each map to hub H, data converts from A to B through H.

A.11 Level System

Levels are conformance profiles — certification targets for environments with different safety and resource constraints.

Level Name Key features
Level 0 Scalars key=value scalars, declarations, variables, type annotations, provenance. Minimal DFA, constant memory.
Level 1 Trees @ arrays, / namespace paths, :: field projection, block syntax, the +=/;=/:=/+:= operators. Still constant memory.
Level 2 Tables Table definitions: unique, foreign-key, and cardinality constraints. Post-scan validation pass; O(N) memory for uniqueness sets.
Level 3 Collation ..lex[locale] — BCP 47 locale tag on lexicographic ordering. Requires a collation library (ICU/CLDR). Platform-dependent.
Level 4 Corpus-Dependent Reserved here (§6); specified separately in the experimental corpus specification. Operations over a corpus of .daiv files; executed by kaiv db-class tooling, never by the certified runtime.

Levels 3 and 4 are independent extensions of Level 2 — neither subsumes the other. An implementation may support either, both, or neither on top of Levels 0–2.

A.12 Implementation Architecture

Term Definition
DFA Deterministic Finite Automaton — the Lexer architecture. kaiv’s regular grammar means the Lexer needs no stack, recursion, or backtracking. Each line classifies in O(1); content lines split on the first bare ' (located via SIMD memchr).
Lexer Layer 1 of the three-layer architecture. Reads raw characters; classifies each line per the six rules. The component that receives safety certification.
Parser Layer 2. Consumes Lexer tokens; comprises the Compiler (.kaiv.raiv), the Denormalizer (.raiv.daiv, with materialization and the head-type lift), and the Validator (.daiv + .csaiv → pass/fail).
Application Layer 3. Consumes the validated kaiv document — only fully-validated, typed, namepath-addressed values.
kaiv document The validated abstract tree produced by the pipeline; the root namespace with typed fields, resolved namepaths, and checked constraints. Serializable as .daiv (all references resolved) or .raiv (field references preserved).

B Implementation Notes: Certification and Performance

This appendix is non-normative. It records the implementation architecture the format was designed around — where the pipeline stages run, what gets safety-certified, and why the text format carries no performance penalty — absorbed from the specification repository’s design-era ARCHITECTURE.md, which is now frozen as a historical record. Nothing here adds conformance requirements; the normative surface is the body of this specification and the conformance suite.

B.1 The Build/Runtime Split

The pipeline stages themselves are specified normatively in the body (§1.2, §7); what this section records is where they run. The pipeline is deliberately split in two:

Everything in this pipeline is either a DFA or something simpler than one. The Lexer is a DFA over bytes; the schema compiler is a canonicalizer that emits canonical text; the parallel scan is two file pointers advancing in lockstep — simpler than a DFA walk. The .daiv file and the compiled schema (.csaiv) are the only two artifacts the certified runtime needs.

B.2 The Certification Boundary

The .daiv format defines the certification boundary: everything to its left in the pipeline is a build tool, everything to its right is the certified runtime.

Component Runs where Certified? Why
Compiler Workstation / CI No — validated through testing Stack, variable table, field table, prefix expansion — context-free mechanisms unsuited to safety certification
Denormalizer Workstation / CI No — validated through testing $field expansion; schema-aware materialization and head-type lift
Validator Workstation / CI No — validated through testing Full schema constraint checking at build time
.raiv file Build artifact Relational intermediate for design tools, diffing, auditing, converters; never consumed by the certified runtime
.daiv file Transmitted / stored The interface contract between build and runtime; the certification boundary itself
Lexer Target / production Yes DFA over bytes; regular grammar; constant memory
Integrity check Target / production Yes The parallel text scan against .csaiv; no stack, no dynamic allocation, constant memory
.csaiv file Loaded at startup Build artifact Canonical schema text, consumed read-only by the integrity check

This is a standard pattern in safety-critical systems: automotive ECUs routinely run certified firmware that consumes configuration generated by uncertified build tools. The build tool is validated; the runtime is certified; the interface format is specified. A certified deployment omits the Compiler, Denormalizer, and Validator entirely — the target never sees authoring sugar, variable interpolation, or namespace blocks, just flat, typed, fully-qualified lines it can validate in constant memory.

Levels describe runtime capability, not authoring syntax. A Level 0 certified runtime processes canonical Level 0 lines; a Level 1 runtime processes canonical Level 1 lines. The authoring syntax that produced those lines is irrelevant to the runtime.

B.3 Performance Model

kaiv is, to our knowledge, the only schema-driven data format whose entire runtime validation path — raw bytes to typed, validated values — is implementable with no heap allocation and static memory bounds. This follows from three design choices: a regular lexical grammar (the Lexer is a DFA — no stack, lookahead, or backtracking), ordered keys (a single-pass parallel scan against the compiled schema), and a flat lexical grammar (nesting lives in the namepath, bounded by the schema — no syntactic recursion). Remove any one of the three and the property is lost.

The same choices close the conventional binary-vs-text performance gap. That gap has three sources:

Gap source JSON / YAML / XML kaiv (.daiv)
Parsing complexity (stack, lookahead, backtracking) Large — recursive descent with high constant factors Zero — the DFA is O(N) with minimal constant factors, the same work per byte as binary tag dispatch
Value conversion (atoi, atof) Medium — every value converts Bounded — per-value, not per-byte; SIMD-parallelizable; dominated by the constraint validation that must run anyway
Compactness (bytes on wire / disk) Medium — delimiter overhead Accepted — the namepath is the cost of self-containment; small for string-heavy data; compression-mitigable

Three properties eliminate the first — dominant — gap entirely:

The remaining two gaps are real but bounded. Value conversion is per-value, SIMD-parallelizable (the simdjson techniques apply), and in certified deployments dominated by the pattern-constraint validation that runs regardless. Compactness favors binary for small numerics (a small-integer line is several times larger than its varint), but the namepath overhead is precisely what makes every line independently intelligible to cat, grep, and diff; for string-heavy payloads the ratio approaches parity, namepath repetition compresses at 5–10× under gzip/zstd, and the primary deployment model is block-aligned file I/O where small size ratios do not matter.

kaiv’s text format is not human-readable at the cost of performance; it is human-readable at virtually no performance cost, because the regular grammar removes the parsing overhead that makes other text formats slow.

References

The standards cited in this document: