Types you can link to

Types die at process boundaries: every schema language re-invents “email” locally, and every $ref is a URL someone will eventually break. On named types as published, frozen, fully-qualified citizens — resolved by address, lowered into validators, carried by name in the data itself.


Ask three services what an email address is and you will get three regexes, two of them wrong in different ways, none of them aware of the others. The type system inside each program is rich; the moment data crosses a boundary it collapses to “string”. Schema languages patched this with references — XML namespaces, JSON Schema $ref — but a reference is only as good as what it points at: a URL that can move, serving a definition that can change under you.

kaiv’s answer has two halves. Types are constraints — every type is the one primitive str refined by a pattern, an ordering, a range — so a type can be published as plain data. And published types live on a write-once registry, so a type name is a permanent address: what std/net/email means today is what it means in 2040.

The name survives into the data

Authored documents import a library and use short names; the build resolves them to fully-qualified form — in the data line itself, not in a sidecar:

$ printf '.!types std/net\n\n&email\[email protected]\n&hostname\nhost=api.example.com\n' | kaiv build
.!daiv
!std/net/email'::[email protected]
!std/net/hostname'::host=api.example.com

Every canonical line names its type’s home. grep '!std/net/email' across a corpus finds every email field in every document, whoever authored it — the type is not an annotation about the data, it is part of the line’s address in meaning-space.

A type is nothing but its constraints

There is no machinery behind a named type — no methods, no coercions. std/net/email is the WHATWG valid-e-mail pattern, and you can see exactly that the moment a value misses:

$ printf '.!types std/net\n\n&email\ncontact=not-an-email\n' | kaiv build | kaiv validate - card.saiv
kaiv: ConstraintViolationError: ::contact=not-an-email (type !std/net/email) violates /^[A-Za-z0-9.!#$%&\x27*+\/=?^_`{|}~-]+@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/ (line 2)

The error prints the type’s entire substance. That regex — pinned to the WHATWG definition, frozen at publication — is the whole truth about the type; there is nothing else to know, nothing implementation-defined, nothing that varies by consumer. The standard libraries are all like this: std/net pins its uri to RFC 3986 exactly, std/time its datetime, std/math its complex — each type a documented constraint you can read in one line.

Publishing one of your own

A type library is a .taiv file: constraint lines above &name= definitions, with // doc comments that travel with the type:

$ cat ids.taiv
.!taiv 1 acme/ids

// Customer id: C + 6 digits
/^C[0-9]{6}$/
&customerid=
$ mkdir -p registry/acme
$ cp ids.taiv registry/acme/ids.taiv
$ printf '/registries::acme=./registry\n' > kaiv.kaiv

(the local directory stands in for ktaiv.com, the type registry). Documents import it like a standard library:

$ printf '.!types acme/ids\n\n&customerid\nowner=C001234\n' | kaiv build
.!daiv
!acme/ids/customerid'::owner=C001234

Now watch what a schema does with the same type:

$ cat order.saiv
.!saiv 1 acme/order
.!types acme/ids

&customerid
owner=
$ kaiv schema order.saiv
.!csaiv 1 acme/order
/^C[0-9]{6}$/'::owner=

The compiled schema contains no type name at all — the reference lowered to its constraints. This asymmetry is deliberate, and it is the certification story in miniature: the data keeps the name (provenance of meaning, greppable forever), while the validation contract keeps only the constraints (the certified runtime byte-scans patterns and needs no resolution, no network, no type system at run time). The name is for people and tooling; the pattern is for the machine at the boundary.

Defaults travel with the type

A .taiv definition can carry a default, and the default rides the type wherever it is imported:

$ cat obs.taiv
.!taiv 1 acme/obs

{DEBUG,INFO,WARN,ERROR}
&level=INFO
$ cat svc.saiv
.!saiv 1 acme/svc
.!types acme/obs

&level
log_level?=
!str
name=
$ cp obs.taiv registry/acme/obs.taiv
$ cp svc.saiv registry/acme/svc.saiv
$ kaiv schema svc.saiv > registry/acme/svc.csaiv
$ printf '.!schema:acme/svc\nname=api\n' | kaiv build
.!daiv
.!schema:acme/svc
!str'::log_level=INFO
!str'::name=api

The document never mentioned log_level; the schema declared it optional with no default of its own; the type said INFO — and the build materialized it. Whoever imports acme/obs inherits not just the shape of a log level but its sensible resting state. A type library is not a bag of regexes; it is a package of decisions.

Unions declare alternatives, lines declare answers

Type alternatives use | in schemas and annotations — !null|str is the nullable string — but a canonical line never carries a union. It carries the alternative that is actually the case:

$ printf '!null|str\nnote=\n' | kaiv build
.!daiv
!null'::note=

The union lives in the contract; the data line answers it. Every line in a .daiv states one type, fully resolved — which is what keeps grep "^!null" meaningful and the parallel-scan validator branchless.

Resolution is layered; identity is not

How does &customerid find its definition? Four layers, most explicit first: a .!registry override in the document itself, the nearest kaiv.kaiv build configuration (what our local registry/ stand-in used), and finally the canonical hosts the toolchain defaults to — ktaiv.com for types, with the schema and unit registries beside it. But resolution layers only decide where the bytes come from. The identity acme/ids/customerid and the bytes it names are fixed at publication, write-once: every layer must produce the same frozen definition, and a mirror is just a nearer copy of the truth.

That is the property the opening paragraph was missing. A $ref points at a location; a kaiv type name is a commitment — one page of constraints, reviewed once, frozen forever, imported by name into any schema, lowered into any validator, and stamped onto every line of data it ever touches. Three services asking what an email address is get one answer, from one address, and the address cannot rot.