Files
hq/02-DECISIONS/0072-two-graphs-and-the-build-chain.md
jschoubben 58ad0742d8 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
2026-09-12 23:09:23 +02:00

4.7 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the tiers accepted 2026-09-12 jochen false 0070-the-catalogue-owns-the-module-graph.md

72. Two graphs, and a build chain that orders itself

Context

ADR 0070 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.