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.