The kaiv User Guide — from .env to typed, validated data

for kaiv 0.4.0

I First Session

Part I is a complete first session: by its end you will have typed data, structure, schemas, and provenance under your fingers. The rest of the guide deepens each layer — stop at the end of this part and you already write useful kaiv.

1 You Already Write kaiv

Here is a fact that makes kaiv the easiest format you will ever adopt: a strict .env file is already valid kaiv. Take one — keys, values, a comment, an empty value:

HOST=api.example.com
PORT=8443
# comment
EMPTY=

Build it with the kaiv toolchain (cargo install kaiv-cli), unmodified:

$ kaiv build app.env
!str'::HOST=api.example.com
!str'::PORT=8443
!str'::EMPTY=

That output is canonical kaiv — denormalized kaiv, by its full name, saved in .daiv files (authored sources are .kaiv; the intermediate .raiv form and the pipeline that produces all three arrive in §4): one line per value, each carrying a type (!str), an address (::HOST), and the value, separated by ' and =. No quotes, no escaping — a value runs verbatim to the end of the line. “Strict” .env means the plain dialect: KEY=value, no export, no quoting, no spaces around =. That dialect is kaiv’s ground floor, and everything in this guide is built on top of it, one optional layer at a time.

2 Types

Every value so far was a string — str is kaiv’s default and only primitive. To say more, put an annotation on the line above a field:

HOST=api.example.com
!int
PORT=8443
!bool
TLS=true
!null
MOTD=
$ kaiv build typed.kaiv
!str'::HOST=api.example.com
!int'::PORT=8443
!bool'::TLS=true
!null'::MOTD=

Note !null versus the earlier EMPTY=: both have nothing after =, but one is a null and the other an empty string — the type is the discriminant, not the payload. And because every canonical line names its type, grep becomes a typed query:

$ kaiv build typed.kaiv > typed.daiv
$ grep "^!int" typed.daiv
!int'::PORT=8443

3 Structure

.env is flat. kaiv adds structure with one concept: the namespace — a generalized struct, a container of named things. What other formats treat as two different constructs are both namespaces here:

Because both are namespaces, one pair of operators covers all navigation — /name descends into any namespace, :: projects a field (a leaf value) out of one — and there is no nesting syntax at all: structure lives in the path, not in braces. Four authored shapes cover everything:

# A namespace (struct): keys you choose, one field per line.
(/server)
host=api.example.com
!int
port=8443
()

# The same kind of thing, inline: the := struct assignment.
/limits:=alice=10|bob=20

# A scalar array: managed integer keys, the ;= vector form.
/@tags;=prod;eu

# An array of namespaces: one [/@mirrors] block per element.
[/@mirrors]
host=eu.example.com
[/@mirrors]
host=us.example.com
[]
$ kaiv build shape.kaiv
!str'/server::host=api.example.com
!int'/server::port=8443
!str'/limits::alice=10
!str'/limits::bob=20
!str'/@tags::0=prod
!str'/@tags::1=eu
!str'/@mirrors/0::host=eu.example.com
!str'/@mirrors/1::host=us.example.com

Structs first: the block form (/server)() opens a namespace and lists its fields, and the inline form := writes a small one in a single line — two spellings of the same chosen-key namespace. Then the managed-key kind: the ;= vector for scalars, and one [/@mirrors] block per element (closed by []) for an array of namespaces. Read the canonical lines through that lens: /server::host and /limits::alice both project a field from a chosen-key namespace — block or inline, the result is identical; /@tags::0 projects from a managed-key one, where the index is the key, which is why an array element’s address looks exactly like any other field’s; and /@mirrors/0::host chains the two: descend into element 0 (itself a namespace), project host. All of it is authoring sugar: canonical form is always the flat, fully addressed lines you see — self-contained, movable, diffable.

4 The Pipeline

What kaiv build runs is a two-stage compile: authored .kaiv compile relational .raiv denorm canonical .daiv. The middle form exists for one reason: field references. Reference another field’s value with $path::field:

/db::host=db.internal
/app::db_url=$/db::host
$ kaiv compile refs.kaiv
!str'/db::host=db.internal
!str'/app::db_url=$/db::host
$ kaiv compile refs.kaiv | kaiv denorm
!str'/db::host=db.internal
!str'/app::db_url=db.internal

The .raiv keeps the reference (for diffing and design tools); the .daiv resolves it. Deployment artifacts are always .daiv: nothing left to resolve, ever.

5 Variables

For repetition within a document, define a hidden variable with a leading dot and reference it with $ — including mid-value:

.host=api.example.com

primary=$.host
backup=$.host
url=https://$.host/v1
$ kaiv build vars.kaiv
!str'::primary=api.example.com
!str'::backup=api.example.com
!str'::url=https://api.example.com/v1

Variables resolve away at compile time — like the block sugar, they exist for the author, not the consumer. (A literal dollar is $$.)

6 Schemas

A schema is written in the same line grammar as data — fields with their annotations, values left empty. This one uses two named types from the standard std/net library and an optional field with a default:

.!kaivschema 1 guide/server
.!types std/net

&hostname
host=
&port
port=
!bool
tls?=true

Validate a conforming document, then a broken one:

$ kaiv build ok.kaiv > ok.daiv
$ kaiv validate ok.daiv server.saiv
pass
$ printf 'host=api.example.com\n!int\nport=70000\n' | kaiv build > bad.daiv
$ kaiv validate bad.daiv server.saiv
kaiv: ConstraintViolationError: ::port=70000 (type !int) violates /^-?[0-9]+$/ ..num [0,65535] (line 2)

The error names the field, the value, the compiled constraint (&port is an integer refined to [0,65535]), and the line. ?= marks a field optional — when a document declares its schema, the build materializes absent optional fields from their defaults, so validated documents are always complete. You rarely write a first schema by hand: kaiv infer derives one from a document you already have, and you tighten from there. Constraints compose from three pieces — pattern /re/, range [min,max], length #[min,max] — plus enums {a,b,c} and tagged unions !null|str.

7 Units, Libraries, and the Registry

Numeric types can carry a unit — part of the type, byte-compared, never silently converted — and the type libraries live at permanent addresses the toolchain resolves by itself (t.kaiv.io during alpha). A wrong-shaped value fails against the whole typed contract:

$ printf '!float:km/h\nspeed=fast\n' | kaiv build > sp.daiv
$ kaiv validate sp.daiv sp.saiv
kaiv: ConstraintViolationError: ::speed=fast (type !float:km/h) violates !float:km/h /^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$/ ..num (line 1)

SI and common units are built in; custom units and currencies (!float:~EUR) come from .faiv libraries. The standard type libraries — std/core, std/enc, std/time, std/num, std/net, std/math — ship embedded in every implementation and need no lookup at all.

8 Provenance

Any value can say who says so, and when — a source declared once, referenced per line, with a timestamp:

.?lab https://lab.example.com/sensors

!float:mm?lab@20260717T090000Z
rainfall=2.4
$ kaiv build rain.kaiv
.?lab https://lab.example.com/sensors
!float:mm?lab@20260717T090000Z'::rainfall=2.4

One line, five attributions: type, unit, source, time, and address. A schema can require provenance (.!provenance:required), making lineage part of the contract rather than a convention.

9 Where Next

You now have the whole ladder: .env lines, types, structure, the pipeline, variables, schemas, units, and provenance. Parts II–IV deepen each rung; the cookbooks map your other formats onto it; the specification is the law behind every rule used here.

II The Language

10 Values, Names, and Comments

Three lexical rules carry most of kaiv’s ergonomics. First: a value runs verbatim from = to the end of the line — no quoting, no escaping, with a single exception: a literal dollar sign is written $$ (because $ introduces references). Second: names are bare by default and quoted only when they need characters a bare name cannot carry (spaces, slashes, an =). Third: # starts a comment for humans, while // starts doc metadata — commentary addressed to tools and future readers of the authored file:

// The primary endpoint.
host=api.example.com
price=it costs $$5
"listen on"=0.0.0.0
$ kaiv build lines.kaiv
!str'::host=api.example.com
!str'::price=it costs $5
!str'::"listen on"=0.0.0.0

Both comment kinds serve the authored file and are gone from canonical output. Quoting, meanwhile, is not a stylistic choice — canonical form has one spelling per name, and a quoted name that could be bare normalizes:

$ printf '"port"=8443\n' | kaiv build
!str'::port=8443

Formally speaking. One spelling per construct is a design invariant of canonical form: quoted-but-bare names, leading-zero indices, and alternative operator spellings all normalize or reject, so byte comparison is semantic comparison (specification, Canonical Form).

11 Types in Depth

Annotations do more than name a scalar. A tagged union offers alternatives, and the data line’s own type records which alternative it took — the active variant:

$ printf '!int|str\na=42\n!int|str\nb=hello\n' | kaiv build
!int'::a=42
!str'::b=hello

The same authored annotation, two canonical types — each line answers “which arm?” by itself, no schema needed at read time. Two more shapes complete the everyday toolkit. A map is a namespace whose keys are open — the schema constrains the value type, not the key set:

.!kaivschema 1 g/m

!map<int>
limits=
$ printf '!int\n/limits:=alice=10|bob=20\n' | kaiv build > m.daiv
$ kaiv validate m.daiv map.saiv
pass

And the extended reals: kaiv floats are deliberately finite (so numeric ordering is total and decidable), with the IEEE non-finite markers available as named types — a union admits them exactly where you mean to:

$ printf '.!types std/num\n&inf\nratio=inf\n' | kaiv build > inf.daiv
$ kaiv validate inf.daiv inf.saiv
pass

12 Structure in Depth

Part I showed the block forms; two more authored shapes round out structure. The inline element operator +:= appends one namespace-array element per line — the most compact spelling for tabular data:

$ printf '/@servers+:=host=a|port=1\n/@servers+:=host=b|port=2\n' | kaiv build
!str'/@servers/0::host=a
!str'/@servers/0::port=1
!str'/@servers/1::host=b
!str'/@servers/1::port=2

Arrays may also be mixed — element i can be a scalar (addressed @name::i) or a namespace (addressed @name/i); the operator before the index says which, per element. Indices are always managed: consecutive, zero-based, and spelled without leading zeros — @a/01 is not an address but an error, one more one-spelling rule.

13 Variables in Full

Beyond the scalar variables of Part I, whole containers can be named. An array variable accumulates with @.name+= and splices with [email protected]:

$ printf '@.regions+=eu\[email protected]+=us\n\n/@primary;[email protected]\n/@backup;[email protected]\n' | kaiv build
!str'/@primary::0=eu
!str'/@primary::1=us
!str'/@backup::0=eu
!str'/@backup::1=us

A namespace variable is defined with the map operator under a dotted name and splatted with $/.name — its pairs expand as if written at the reference point:

$ printf '/.base:=retries=3|timeout=30\n\n/api:=$/.base\n/worker:=$/.base\n' | kaiv build
!str'/api::retries=3
!str'/api::timeout=30
!str'/worker::retries=3
!str'/worker::timeout=30

Splat means expansion, not merge: if you splat a base and then restate a field, the document simply contains both lines, and a schema will call the duplicate what it is. Like every variable, these are authoring machinery — canonical output carries no trace of them.

III Contracts

14 Requiredness, Defaults, and Materialization

kaiv’s optionality model has one rule that surprises newcomers and then becomes their favorite guarantee: validated documents are complete. An optional field (?=) must say what absence means — a default on the right side, or a !null alternative — and when a document declares its schema, the build materializes the answer:

.!kaivschema 1 acme/app

host=
!int
retries?=3
!null|str
note?=
$ mkdir -p acme
$ kaiv schema app2.saiv > acme/app.csaiv
$ printf '.!registry acme=.\n.!schema:acme/app\n\nhost=h1\n' | kaiv build
.!registry acme=.
.!schema:acme/app
!str'::host=h1
!str'::retries=3
!null'::note=

The author wrote one line; the artifact carries three — the default materialized for retries, the explicit null for note. Consumers never re-derive defaults, and the validator’s scan is strict lockstep: every declared field, present, in order. (.!registry acme=. points schema resolution at the local directory — the same mechanism that reaches the real registries, aimed at ..)

15 Strict Schemas

By default a schema is relaxed: fields it does not declare may ride along. Add strict to the header and the document may contain declared fields only:

.!kaivschema 1 g/s strict

name=
$ printf 'name=x\nextra=y\n' | kaiv build > ex.daiv
$ kaiv validate ex.daiv strict.saiv
kaiv: UndefinedFieldStrictSchemaError: ::extra is not defined in the strict schema (line 2)

Formally speaking. Strictness is what GraphQL types and ASN.1 SEQUENCEs enforce implicitly; a kaiv strict schema is the same assertion made explicit (specification, Strict vs. Relaxed).

16 The Constraint Vocabulary

Every constraint is one of three components — pattern /re/, ordering (span), range [min,max] — plus enums, lengths #[min,max], and unions. The span is what makes ranges mean the right thing. Compare version numbers under ..ver, which orders segment-wise numerically:

.!kaivschema 1 g/v
/^[0-9.]+$/ ..ver [1.2,]
version=
$ printf '!str\nversion=1.10\n' | kaiv build > v.daiv
$ kaiv validate v.daiv ver.saiv
pass
$ printf '!str\nversion=1.1\n' | kaiv build > v3.daiv
$ kaiv validate v3.daiv ver.saiv
kaiv: ConstraintViolationError: ::version=1.1 (type !str) violates /^[0-9.]+$/ ..ver [1.2,] (line 1)

1.10 passes a [1.2,] floor and 1.1 fails it — lexical order would get both wrong. The other spans are ..num (numeric), ..lex (byte order, the default), and ..time (temporal); ranges are inclusive and either end may be omitted. Patterns use a deliberately finite dialect — no backtracking, no lookaround, and \xHH for the one character a pattern cannot carry literally (the apostrophe, which e-mail and URI grammars need).

17 Tables: Uniqueness, References, Cardinality

Arrays of namespaces can graduate to tables: the block header takes clauses for unique keys, foreign keys, and element counts — and validation gains a linear post-scan pass that enforces them:

.!kaivschema 1 g/fleet

[/@servers host=! min=1 max=10]
host=
!int
port=
[]
[/@disks server=/@servers/*::host]
server=
!int
size=
[]

Three contracts, three named refusals:

$ printf '/@servers+:=host=a|port=1\n/@servers+:=host=a|port=2\n' | kaiv build > dup.daiv
$ kaiv validate dup.daiv fleet.saiv
kaiv: UniquenessViolationError: /@servers: duplicate value (a) for unique key (host)
$ printf '/@servers+:=host=a|port=1\n/@disks+:=server=ghost|size=100\n' | kaiv build > fk.daiv
$ kaiv validate fk.daiv fleet.saiv
kaiv: ReferentialIntegrityError: /@disks: server=ghost has no match in /@servers/*::host
$ printf '/@disks+:=server=a|size=1\n' | kaiv build > empty.daiv
$ kaiv validate empty.daiv fleet.saiv
kaiv: CardinalityViolationError: /@servers has 0 elements, schema requires min=1 max=10

host=! declares a unique key (compound keys separate fields with commas); server=/@servers/*::host declares that every disk’s server must match some server’s host. This is Level 2 of kaiv’s conformance ladder — the relational essentials, still one forward scan plus a bounded pass.

18 Your Own Type Library

Named types live in .taiv libraries — the same definition shape as std/core, under your namespace:

.!kaivtype 1 acme/types

// Percentage
/^[0-9]+$/ ..num [0,100]
&percent=
$ printf '.!registry acme=.\n.!types acme/types\n\n&percent\nload=87\n' | kaiv build
.!registry acme=.
!acme/types/percent'::load=87

The reference &percent is authoring shorthand; canonical form carries the full name !acme/types/percent, so the artifact is unambiguous forever. During alpha, publishing a library means depositing it at the kaiv.io registries (your namespace, one claim); the name-is-the-version rule means evolution is a new name — acme/types2 — never an edit.

19 The Standard Libraries

Six libraries ship embedded in every implementation — no lookup, ever: std/core (the scalar types), std/enc (typed embed channels for JSON, XML, and binary payloads), std/time (the four datetime shapes with temporal ordering), std/num (the non-finite markers), std/net (uri, url, email, hostname, port), and std/math:

$ printf '.!types std/math\n&complex\nz=3+2i\n' | kaiv build
!std/math/complex'::z=3+2i

Part I already put std/net to work; the cookbooks show std/enc and std/time arriving automatically when foreign formats need them.

20 Units

Units are part of a numeric type — compared byte-for-byte, never converted. The toolchain canonicalizes compound spellings so equal dimensions compare equal:

$ kaiv unit m*N
N*m
$ kaiv unit s^-2*m
m/s^2

Factors sort, negative exponents move below the bar — one spelling again. Currencies are tilde-prefixed and deliberately carry no conversion factors (rates are external and time-varying), so a mismatch is a type error, loudly:

$ printf '!float:~EUR\ntotal=99.50\n' | kaiv build > eur.daiv
$ kaiv validate eur.daiv usd.saiv
kaiv: TypeMismatchError: ::total=99.50 (type !float:~EUR) does not satisfy !float:~USD /^-?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?$/ ..num (line 1)

Custom units and currencies live in .faiv libraries (.!kaivunit header, imported with .!units) — the same registry model as types.

21 Provenance in Full

The full provenance triple is ?source@timestamp#dpid — who says so, when, and which upstream data point — with sources declared once as .?id uri and multiple sources comma-separated. A schema can make lineage mandatory:

.!kaivschema 1 g/audit
.!provenance:required

!float
reading=
$ printf '!float\nreading=2.4\n' | kaiv build > r.daiv
$ kaiv validate r.daiv audit.saiv
kaiv: ProvenanceSchemaError: ::reading: schema requires source and timestamp on every line (line 1)
$ printf '.?lab https://lab.example.com\n\n!float?lab@20260717T090000Z\nreading=2.4\n' | kaiv build > r2.daiv
$ kaiv validate r2.daiv audit.saiv
pass

Three levels exist: required (source and timestamp), source (source at least), none (provenance prohibited — for schemas where it would be noise). Because provenance rides the line, grep '?lab' is an audit query.

IV The Toolchain

22 The Command Surface

Everything in this guide used six commands; the full surface is small enough to hold in your head. compile, denorm, and build are the pipeline; schema compiles contracts; validate enforces them; unit canonicalizes; infer derives a schema from data; import/export cross formats; import-schema converts foreign schemas. All read stdin when no file is given. Registry resolution is configured by the nearest kaiv.kaiv up from the working directory, overridden by KAIV_REGISTRY_* variables; immutable artifacts cache forever, and --offline (or KAIV_OFFLINE=1) resolves from cache alone — a warm cache is a complete resolution surface.

23 Interop

Eight data formats (JSON, YAML, TOML, XML, CBOR, Avro, Protocol Buffers, ASN.1 DER) and five schema languages (JSON Schema, XSD, .proto, Avro Schema, GraphQL SDL) convert in and out of the canonical form you now read fluently. That story has its own shelf: each cookbook is a working session for one format, recipe by verified recipe, with the honest fidelity notes where a format cannot carry something back.

24 Errors and the Conformance Ladder

Every kaiv error is Name: context (line) — the pinned catalog name first (stable across implementations, what conformance tests compare), then the site: field, value, and the violated constraint exactly as compiled. You have seen a dozen; the shape is always the same, and exit codes are always 0 for pass, 1 for any refusal. Implementations declare a conformance level: L0 scalars and L1 trees validate in constant memory (the certifiable core), L2 adds the table pass, L3 adds locale-aware collation. Everything in this guide through Part II is L0–L1; the tables chapter is L2 — know which level your target runtime claims.

25 A Worked Finish

The whole ladder, end to end, in four moves. A fleet starts as .env-grade lines and ends schema-validated with a unique key — then leaves for a legacy consumer as JSON:

$ printf '/@fleet+:=host=eu.example.com|port=443\n/@fleet+:=host=us.example.com|port=443\n' | kaiv build > fleet.daiv
$ printf '.!kaivschema 1 g/f\n.!types std/net\n\n[/@fleet host=! min=1]\n&hostname\nhost=\n&port\nport=\n[]\n' > f.saiv
$ kaiv validate fleet.daiv f.saiv
pass
$ kaiv export --json fleet.daiv
{"fleet":[{"host":"eu.example.com","port":"443"},{"host":"us.example.com","port":"443"}]}

One parting detail, if you caught it: the ports exported as strings — the compact +:= lines carried no annotation, so 443 is a str that happens to satisfy &port’s integer refinement. The constraint is the type; the annotation is the record of intent. Annotate when downstream consumers care — and you are ready for the specification whenever a rule needs its exact wording; until then, the toolchain and this guide are the working surface.