Skip to content

The ontology

Twenty-five repositories, seven kinds of thing, and five relations between them. This page is the controlled vocabulary: what a member can be, what can be true of a pair of members, and which of those facts are derived, declared or checked against another system.

It exists because the ecosystem had a vocabulary already and it lived in prose. Six edges in the old README diagram carried six different phrasings — “issues tokens for”, “governs”, “capacities”, “pins + certifies”, “by tag”, “installed via PRODUCT=” — and nothing could tell you which two meant the same kind of thing, or check that any of them were still true.

Derive what can be derived, declare only what cannot, and gate both.

A field a human types is a field that goes stale. This repository has already paid for that twice: two wrong statuses in its first commit, taken from README prose over a tree that contradicted it, and — in a sibling repository — a hand-written inventory field that was wrong 28 times in 70 rows before it was derived. Everything below says which category it is in, and a fact with no gate named against it is a fact nobody is checking.

Every member has a tier. There are eight, and the set is closed.

tierwhat it ishow many
emulatora service this family emulates7
compositionthe BOM: pins, family compose, chain test1
hubthis repository: directory, docs site, CI sweep1
sourcesthe vendor systems every cell pulls from1
corethe data product defined once: SQL, contracts, numbers1
leafone cell’s product half7
platformone cell’s infrastructure half7
applicationsomething built ON the family rather than part of it3

application is the newest and the one that says most about this file. The other seven tiers describe how the family is ASSEMBLED, and every one of them is drawn on the map. An application CONSUMES the assembly — data-agent-service answers questions over the estate the cells build, data-agent-voice speaks over that service, sqlglot-go is the guard one of them runs — so it carries no engine, no orchestrator and no kind, and the map does not draw it.

It also existed for weeks before it was a tier. Three public repositories were part of the ecosystem while check_registry.py reported the registry complete every thirty minutes, because that check recognises a member by its NAME (*-emulator, contoso-*, *-platform-*) and none of those three followed a convention written before they existed. A completeness check keyed on a convention is blind to exactly the members that broke it, which is the same shape as the defects the rest of this document exists to prevent, applied to the guard rather than to the data.

leaf and platform members carry both. Nothing else carries either.

  • enginefabric, databricks, snowflake
  • orchestratorairflow3, airflow-builtin, notebook-pipelines, jobs, tasks

A cell is an (engine, orchestrator) pair. There are seven, and a cell is always exactly two repositories.

kind splits the emulators by what they are for, which the old role prose could not be read for:

kindmembers
enginefabric, databricks, snowflake
identityentra
control-planearm
secretskeyvault
gatewayapim

engine is the load-bearing one: a platform targets exactly one engine emulator, and every other emulator is cross-cutting, running in a stack without being what the stack is named for.

An engine emulator also declares which engine it serves, so targets is a join on a field rather than a match on a name prefix. fabric-emulator begins with fabric today and that is a coincidence the ontology should not rest on. This was found by writing the map generator against this page: it reached for the emulator’s engine and there was none.

bom: true means the member is one of the six azure-emulators certifies: pinned in the BOM, present in the family compose, covered by the chain test. snowflake-emulator is kind: engine and bom: false, and that pair of facts is the whole of its adjacency. It keeps the same discipline as the certified six — graded ledger, witness manifest, checker enforcing both — and is not in the set the pin gate and the chain test are built around.

The line is BOM membership, not whether Azure touches the product. Azure and Fabric integrate with Snowflake, so “not an Azure service” invites an objection it cannot answer. databricks-emulator is bom: true because it emulates Azure Databricks, down to the first-party app id and federated Entra tokens.

Five, and the set is closed. Each one says where it comes from.

relationfrom → tosource
pairs_withleaf ↔ platformderived from equal (engine, orchestrator)
targetsplatform → emulatorderived: joins the platform’s engine to the emulator’s
pins_by_tagleaf → coreinvariant: every leaf, no exceptions
materialises_fromplatform → sourcesinvariant: every platform
certifiescomposition → emulatordeclared bom, checked against azure-emulators

Why pairs_with is derived rather than listed

Section titled “Why pairs_with is derived rather than listed”

Listing it would let a leaf name a platform that does not exist, or two leaves claim one platform, and nothing would notice. Derived, the absence is the error: a leaf whose (engine, orchestrator) has no platform is an unpaired row, and so is a platform with no leaf. That distinction — reserved on purpose versus missing by accident — is the same one status and ci exist to draw, and it is the entry that earns the registry.

Nothing in this repository can know what the BOM pins. bom is therefore a declaration, and a declaration that nothing verifies is a claim. It is compared against azure-emulators’ published main, the way the parity report already reads published state over HTTP rather than trusting a checkout.

authenticates. Every emulator validates tokens against entra-emulator, which makes it true of six edges and therefore not worth drawing: an arrow that is always present carries no information. It belongs in prose, and it is in 02 — the emulators.

Anything about a cell’s health. status, ci and the sweep already carry that, they change hourly, and folding them into the structure would mean the map goes stale every time a workflow runs.

These are the statements the gates enforce. Each names the gate.

#invariantgate
1every leaf has exactly one platform with the same (engine, orchestrator), and the reversecheck_ontology.py
2engine and orchestrator appear on leaf and platform members and nowhere elsecheck_ontology.py
3kind appears on emulator members and nowhere else, and engine on an emulator means kind: enginecheck_ontology.py
4exactly one member per (engine, orchestrator, tier) — no duplicate cellscheck_ontology.py
5every declared value is in the closed set for its fieldmembers.schema.json
6bom: true matches what azure-emulators pinscheck_ontology.py --bom
7the committed map matches the registryrender_map.py --check
8the committed tables match the registryrender_tables.py --check
9no ecosystem repo is missing from the registry, and none is phantomcheck_registry.py
10ports appears on platform members and nowhere else, and no host port is published by two memberscheck_ontology.py
11every platform’s ports equals the host ports its published compose mapscheck_ontology.py --ports
12every built member answers its tier’s verb floor, read from its published Makefilecheck_verbs.py
13a leaf is contoso-data-product-<engine>-<orchestrator>, a platform is <engine>-platform-<orchestrator>, an emulator is <service>-emulator; azure-keyvault-emulator and azure-apim-emulator are grandfathered by namecheck_ontology.py

Three patterns, one per runnable tier, and a name that breaks its pattern is a gate failure rather than a style remark:

tiername
leafcontoso-data-product-<engine>-<orchestrator>
platform<engine>-platform-<orchestrator>
emulator<service>-emulator

Leaf and platform names are derived from the member’s engine and orchestrator, so the check is not “does this look right” but “does this repository’s name agree with the cell it declares”. A leaf named for Fabric and declared as Snowflake is the kind of mistake prose never catches.

Two emulators keep a prefix the rule does not have. azure-keyvault-emulator and azure-apim-emulator were named before the pattern existed; their five siblings are entra-, arm-, fabric-, databricks- and snowflake-emulator. They are grandfathered by name, listed in the checker, and the reason is a cost the family has measured elsewhere: renaming a published image touches every pin, every compose, every link and every memory of it, and buys a reader nothing, since the registry’s tier already says what each is. The exception is closed: a new emulator gets the unprefixed name, and the gate says so.

A platform’s compose publishes host ports, and that is the one fact a reader needs before make up that no README states reliably. ports carries them by service name. It is declared rather than derived because deriving it would mean every reader of the registry fetching seven compose files; it is checked against those files on published main so the declaration cannot go stale, and checked across members so two stacks cannot claim one port. The second check is the one a single compose file can never make: it does not know what its siblings publish, and the registry is the only place that sees all seven at once.

“Run the medallion and tell me whether it worked” was spelled four ways across seven platforms, two emulators answered no up at all, and leaves ranged from a full up/down/run to no Makefile. The contract is a floor per tier. A repository may answer more, and the rest of its Makefile is its own business; the floor is what a reader who has learned one member may assume of every other.

tieranswers
emulatorup down logs doctor test
platformup down logs doctor test witness
leaf, sourcestest

Two words are load-bearing. up means the published image, not a checkout: it is what a platform pins and what a newcomer means by “start it”. witness takes no arguments and exits non-zero when the cell did not produce what it claims. It is the family’s word because a witness is the family’s unit of evidence, and because a platform that needs to be told which DAG must derive it from the product it was pointed at: a platform that names a product has stopped being a platform, and a test in each says so.

check_verbs.py reads each member’s Makefile from published main, the way every other gate here reads published state. --local reads the sibling checkouts for a pre-merge answer, and says which it read.

A cell’s witnesses, and it should not. How much of an emulator’s claimed surface a third party has proved is a per-repository ledger with its own checker, assembled by family_parity.py. Restating any of it here would create a second copy that drifts, and the interesting number is not a structural fact about the family: it is a measurement of one repository against its own claims.