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.