Two graphs, and a build chain that orders itself

Corrects ADR 0070, written an hour earlier, which had the control plane consuming
the catalogue in order to compose a declaration. That was written before the two
graphs had been told apart and creates a dependency that need not exist: a
catalogue that is down would leave the control plane unable to compose the thing
that would repair it.

The catalogue links module-versions to each other and does not know nodes exist.
The control plane links module-versions to nodes, and holds capabilities and
claims. They meet only when something is installed, and everything between them
travels as events over the broker, on durable queues, so nothing is lost when a
receiver is away.

Build order is not computed anywhere. The builder never consults the graph and
builds what it is asked for; the catalogue asks for the next build after the
previous registration, so ordering holds by construction. Its rule is a condition
rather than a schedule — rebuild once everything a module was built against is
current — which covers a chain and a diamond alike.

Left open: whether an upgrade is applied or merely noticed, and whether a module
on several machines upgrades on all at once.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-12 23:09:23 +02:00
parent 90f7bcb209
commit 58ad0742d8
2 changed files with 87 additions and 0 deletions
@@ -0,0 +1,86 @@
---
topic: the tiers
status: accepted
date: 2026-09-12
deciders: jochen
reconstructed: false
extends: 0070-the-catalogue-owns-the-module-graph.md
---
# 72. Two graphs, and a build chain that orders itself
## Context
[ADR 0070](0070-the-catalogue-owns-the-module-graph.md) gave the catalogue the module graph and
said the control plane consumes it — that resolving a requirement and composing what a machine
should be *"both need to know what modules are, so the dependency runs from the control plane to
the catalogue."*
**That paragraph is wrong, and this record corrects it.** It was written before the two graphs had
been told apart, and it creates a dependency that does not need to exist: a catalogue that is down
would leave the control plane unable to compose the declaration that would repair it.
## Decision
**There are two graphs, with different owners, and they meet only when something is installed.**
| Graph | Owner | What it links | Answers |
|---|---|---|---|
| the module graph | the **catalogue** | module-versions to each other | what was this built against · what must rebuild now · what does this need |
| the runtime graph | the **control plane** | module-versions to nodes | what runs where · who consumes this provision · what breaks if this machine goes |
The catalogue does not know nodes exist. The control plane holds module-versions and nodes, along
with capabilities and claims, because deciding whether a machine qualifies needs every node.
**So the control plane never asks the catalogue anything.** It holds what it needs to compose a
declaration. A catalogue that is down stops new installs and stops the rebuild fan-out, and does not
touch anything already running or the ability to repair it.
**Everything between them travels as events over the broker**, like all module-to-module
communication. The builder finishes and announces that a module was built. The catalogue registers
it, places it among what it depends on, and announces that a module was upgraded. The control plane
reacts to *that*, not to build output — a semantic fact rather than an artifact.
**Nothing is lost if a receiver is down.** A consuming module's queue is durable with a dead-letter
exchange, declared by the mesh rather than by the module, so an event waits for a consumer that is
not there.
**Build order is not computed. It emerges from the chain.** The builder never consults the graph: it
builds what it is asked for, one at a time. The catalogue asks for the next build after the previous
registration, so *"do not start this until that is registered"* holds by construction rather than by
a schedule somebody maintains.
**The catalogue's rule is a condition, not an order:** ask for a module to be rebuilt once everything
it was built against is current. That handles a chain and a diamond with one rule, where an
order-based approach needs to know the shape in advance.
**Two refusals belong to the catalogue.** A cycle, because the chain would never settle. And a
rebuild whose artifacts are identical to what it replaced, which is not an upgrade and must not be
announced as one — or a single change ripples outward forever through modules that did not change.
**Genesis does none of this.** The init builder has a fixed, short list — control plane, catalogue,
builder — in a written order, because there is no catalogue yet to ask.
## Consequences
The builder stays simple, and independent of the catalogue. That is what makes genesis possible at
all: the thing that builds the catalogue cannot require the catalogue.
The two sides can be briefly out of step — the catalogue may hold a module-version a moment before
the control plane knows of it. Assigning in that instant fails, and should say why rather than
report that no such module exists.
A module's declared events stop being documentation and become its permissions: an account is
scoped from what a module emits and consumes, so a builder that announces what it built is granted
what it needs by the ordinary mechanism rather than by a special case.
## Open
**Whether an upgrade is applied or merely noticed.** Today the system this replaces deploys
automatically, and that is a defensible default for core modules on a mesh its operator runs. But
the design as it stands does the opposite: it records that the source moved ahead, makes it visible,
and waits to be told. This must become a setting with a chosen default rather than inherited
behaviour — and the choice matters most on the day a bad commit reaches something that carries mail.
**Whether a module assigned to several machines upgrades on all of them at once.** Doing so makes
one bad commit simultaneous everywhere. Doing one machine and pausing turns it into one casualty.
+1
View File
@@ -104,6 +104,7 @@ python3 00-META/checks/index.py fail if stale
- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md) - **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md)
- **0070** — [The catalogue owns the module graph, and genesis builds rather than carries](0070-the-catalogue-owns-the-module-graph.md) - **0070** — [The catalogue owns the module graph, and genesis builds rather than carries](0070-the-catalogue-owns-the-module-graph.md)
- **0071** — [Genesis clones from a mesh, and checks what it got](0071-where-genesis-gets-its-source.md) - **0071** — [Genesis clones from a mesh, and checks what it got](0071-where-genesis-gets-its-source.md)
- **0072** — [Two graphs, and a build chain that orders itself](0072-two-graphs-and-the-build-chain.md)
### What runs on them, and how it gets there ### What runs on them, and how it gets there