Publish one edge
Schemas are nodes; mappings are edges. On .maiv — the
mapping file that turns schema interop from a pile of
converter scripts into a graph you join by publishing one
edge — and on microschemas, the small, finishable standards
that make the graph worth joining.
Every pair of tools that disagree about shape spawns a
converter. The inventory system calls it hostname; the
monitoring agent wants check_host; the deploy tool wants
address. Somewhere between them lives a script — forty
lines of Python with opinions nobody reviews, owned by
whoever last touched it, silently wrong since the March
migration. Multiply by every pair of tools that must agree,
and integration becomes what it is everywhere today: N²
small programs, each a private treaty between two shapes.
kaiv’s answer is to make the treaty a file — inspectable, validatable, publishable, and composable. Not a program: a mapping.
A mapping is a file, not a program
Here is a source schema — ACME’s edge-server config, shaped the way ACME likes it:
$ cat edge.saiv
.!saiv 1 acme/edge
!str
/server::name=
!int
/server::listen=
{TRACE,DEBUG,INFO,WARN,ERROR,FATAL}
log_level=
And here is hub/server-endpoint — a microschema: three
fields, one of them optional with a default. Keep its size in
mind; it is the point of this article.
$ cat server-endpoint.saiv
.!saiv 1 hub/server-endpoint
!str
host=
!int[0,65535]
port=
!int[1,3600]
timeout_ms?=500
The mapping between them is a .maiv file. It reuses the two
constructs kaiv already has for “receives from” — =
assignment, target on the left as everywhere in kaiv, and the
$ dereference sigil pointing into the source:
$ cat edge-to-hub.maiv
.!maiv
.!source acme/edge
.!target hub/server-endpoint
.!drop ::log_level
::host=$/server::name
::port=$/server::listen
That is the entire mapping language. A target namepath
receives a source namepath’s value, or a literal constant —
and nothing else. No conditionals, no expressions, no string
functions. A mapping is a name-to-name rewrite table, and
every line of it is an assertion of semantic equivalence:
ACME’s /server::name and the hub’s ::host mean the same
thing. The .!drop line documents the field ACME chose not
to map — machine-readable intent, so tooling can tell an
omission from an oversight.
Stand up a registry (this local directory plays the part of
s.kaiv.io; schemas publish in compiled form):
$ mkdir -p registry/acme registry/hub registry/watchco registry/shipco
$ printf '/registries::acme=./registry\n/registries::hub=./registry\n/registries::watchco=./registry\n/registries::shipco=./registry\n' > kaiv.kaiv
$ kaiv schema edge.saiv > registry/acme/edge.csaiv
$ kaiv schema server-endpoint.saiv > registry/hub/server-endpoint.csaiv
The mapping validates against both ends:
$ kaiv mapping validate edge-to-hub.maiv
ok
and applies to data:
$ cat edge.kaiv
/server:=name=edge-fra-1.acme.example|listen=8443
log_level=TRACE
$ kaiv mapping apply edge-to-hub.maiv edge.kaiv
.!daiv
.!schema:hub/server-endpoint
!str'::host=edge-fra-1.acme.example
!str'::port=8443
!str'::timeout_ms=500
$ kaiv mapping apply edge-to-hub.maiv edge.kaiv | kaiv validate
pass
Three things happened in that output. The namepaths were
rewritten and reordered into the target schema’s order —
source field order never constrains a mapping. The output
declared its own schema, so the final kaiv validate needed
no arguments. And timeout_ms=500 appeared out of nowhere:
ACME never mapped it, because the target schema’s own default
covered it. A mapping only has to say what the target
cannot say for itself.
Wrong mappings die before any data exists
Because a mapping is purely structural, its correctness is a static question. Leave a required target field unproduced —
$ printf '.!maiv\n.!source acme/edge\n.!target hub/server-endpoint\n\n::host=$/server::name\n' > half.maiv
$ kaiv mapping validate half.maiv
kaiv: IncompleteMappingError
— and it fails at publish time, with no data in sight:
::port is required by the target, and no mapping line, no
constant, and no target default produces it. The same check
runs on every namepath (a reference to a field neither schema
declares is an error, not a silent skip) and on every
override constant. A converter script fails on the first
awkward payload, in production. A mapping fails on the day
you write it, or never.
Arrays map element-wise with the /* wildcard binding every
index:
$ cat racks.kaiv
/@racks+:=machine=r1.acme.example|ssh=22
/@racks+:=machine=r2.acme.example|ssh=2222
$ kaiv schema racks.saiv > registry/acme/racks.csaiv
$ kaiv schema nodelist.saiv > registry/hub/nodelist.csaiv
$ kaiv mapping apply racks-to-nodelist.maiv racks.kaiv
.!daiv
.!schema:hub/nodelist
!str'/@nodes/0::host=r1.acme.example
!str'/@nodes/1::host=r2.acme.example
Microschemas
Look again at hub/server-endpoint: three fields. It is not
an attempt to describe servers completely — it is the shared
minimum of what anything that talks to a server endpoint
needs: where, which port, how patient. That smallness is not
a limitation. It is the property that makes everything else
in this article work.
A three-field schema can be finished. Reviewed in a
morning, argued about for a week, and then frozen — published
to the registry where, at beta, publication is forever. Big
standards never reach that state; there is always a working
group, always a v-next, always a corner nobody implements.
A microschema reaches it by construction: there is nothing
left to argue about. And a frozen schema is something a
mapping can target permanently — the edge you publish
against hub/server-endpoint today cannot be broken by
hub/server-endpoint tomorrow, because there is no tomorrow
for a published schema; a revision would be a new schema at a
new address.
Microschemas are to data what components are to interfaces:
small units that compose, rather than platforms that annex.
You do not adopt a monitoring vendor’s data model. You
assert — in three machine-checked lines — that your name is
what the hub calls host, and you are done describing
yourself.
Publish one edge
Here is why that one small act is worth more than it looks. Suppose the monitoring vendor has declared how a probe is configured, and — because the hub microschema exists — has published its own edge from the hub:
$ cat probe.saiv
.!saiv 1 watchco/probe
!str
check_host=
!int[0,65535]
check_port=
!int[100,60000]
interval_ms?=5000
$ cat hub-to-probe.maiv
.!maiv
.!source hub/server-endpoint
.!target watchco/probe
::check_host=$::host
::check_port=$::port
::interval_ms=$::timeout_ms|5000
$ kaiv schema probe.saiv > registry/watchco/probe.csaiv
ACME has never heard of watchco. ACME published exactly one edge — to the hub. But mappings compose, and composition is a join on namepaths, not a program-synthesis problem: where the first mapping’s target meets the second mapping’s source, the reference substitutes through.
$ kaiv mapping compose edge-to-hub.maiv hub-to-probe.maiv
.!maiv
.!source acme/edge
.!target watchco/probe
.!via acme/edge/mapto/hub/server-endpoint
.!via hub/server-endpoint/mapto/watchco/probe
::check_host=$/server::name
::check_port=$/server::listen
$ kaiv mapping compose edge-to-hub.maiv hub-to-probe.maiv > edge-to-probe.maiv
$ kaiv mapping apply edge-to-probe.maiv edge.kaiv
.!daiv
.!schema:watchco/probe
!str'::check_host=edge-fra-1.acme.example
!str'::check_port=8443
!str'::interval_ms=5000
The composed mapping is an ordinary .maiv — auditable, with
its derivation recorded hop by hop in .!via. And the same
move works for every other schema attached to the hub. A
deploy tool, say:
$ kaiv schema target.saiv > registry/shipco/target.csaiv
$ kaiv mapping compose edge-to-hub.maiv hub-to-target.maiv > edge-to-target.maiv
$ kaiv mapping apply edge-to-target.maiv edge.kaiv
.!daiv
.!schema:shipco/target
!str'::address=edge-fra-1.acme.example
!str'::port=8443
!str'::protocol=tls
Count the edges. Seven sources and three targets around one hub is ten mappings — but seven-times-three reachable pairs. The hub turns O(N²) private treaties into O(N) published edges, and every new arrival makes every existing participant more reachable, having asked nobody’s permission.
This is also why composition is sound rather than
heuristic. An ETL connector maps field A to field B
operationally — it cannot say whether A and B mean the same
thing, so chaining connectors compounds guesses. A .maiv
line asserts semantic equivalence inside a shared type
system, and equivalence is transitive: if ACME’s name is
the hub’s host, and the hub’s host is watchco’s
check_host, then the composed claim is not a guess — it is
the only thing it could be.
Where edges live
Mappings publish to the schema registry — they are edges between schemas, and they live beside them. A published mapping has no name of its own: its identity is its endpoint pair, and its address derives from it:
https://s.kaiv.io/acme/edge/mapto/hub/server-endpoint/v1.maiv
└────────┘ └───────┘ └───┘ └─────────────────┘ └┘
registry source marker target edition
No naming decisions, no naming disputes — and edge discovery
without an index: to find the mapping from A to B you
construct its URL, and a 404 is the answer. The address
reads as a sentence because the mapto marker makes the
direction explicit; no convention knowledge required in 2040.
The trailing edition segment is mandatory and write-once: a
refined mapping publishes as v2 beside v1, never over it.
For the common case of migrating your own schema, the shared
namespace elides — acme/config-v1/mapto/config-v2/v1.maiv
reads exactly like what it is.
The marker also names the publisher. A mapto address begins
with the source’s namespace, so the source’s owner publishes
it — ACME declaring how ACME’s data maps outward. For the
inbound case there is mapfrom: an address under the
target’s namespace —
watchco/probe/mapfrom/hub/server-endpoint/v1.maiv — which
is how a vendor attaches its schema to a hub it does not own,
permissionlessly, with the marker itself recording who
vouches for the edge. The mapto address stays canonical:
hub curators can later bless an official out-edge that takes
precedence, and — write-once everywhere — nothing is ever
deleted to make that happen.
The graph you join by naming things
The pieces reinforce each other. Microschemas are small enough to finish, so they can be frozen; frozen schemas are safe to map against forever; mappings are structural, so they validate statically and compose transitively; derived addresses make every edge discoverable by construction. None of these properties survives alone — a mapping language with transformations would not compose soundly, a mutable schema would rot its edges, an unpublished mapping helps only its author.
Together they change what it means to describe your data. The treaty pile becomes a public graph; the graph’s nodes are three-field standards a person can hold in their head; and joining it costs exactly one honest sentence about your own fields, written as a file that a machine can check and the whole network can reuse. Publish one edge.