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