diff --git a/02-DECISIONS/0072-two-graphs-and-the-build-chain.md b/02-DECISIONS/0072-two-graphs-and-the-build-chain.md new file mode 100644 index 0000000..354cf94 --- /dev/null +++ b/02-DECISIONS/0072-two-graphs-and-the-build-chain.md @@ -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. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 499fddd..758393a 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -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) - **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) +- **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