--- topic: the mesh status: accepted date: 2026-10-01 deciders: jochen reconstructed: false extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md --- # 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue ## Context A merge on the forge reaches the controller as an event, and the controller asks the build machine for what that merge changed. Until today that meant the modules whose recorded source is that repository; since this afternoon it also means everything standing on what moved ([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)). Both are done inside the handler that received the event: it asks one build, waits for it, asks the next, and returns when the last is done. Three things followed from that shape on 2026-10-01: - The controller hears nothing else for the length of the work — twenty-five minutes for the runtime image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)). - A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as history, and the dependents are asked by hand. - Nothing is deployed between builds. A merge that changes the build machine and something the build machine builds asks for both in order, but the second is built by whichever build machine is running — the old one, unless somebody pushed in between. The order the dependents are sorted in exists for the artifacts; it says nothing about what must be *running*. And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a build was made against, the repositories a build read, and the fact that every source-built module is built by the build machine, each read by a different function in the merge handler. ## Decision **1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the module's build reads the other's repository), `built-by` (the module is built by the holder of the build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside the controller needs it — and nothing else computes an edge. The edges are derived from facts recorded at two moments and written by nobody: registration records the manifest (`declared`, and `built-by` for anything with a source), a build's take-in records what the image was built on and which repositories it read (`stands-on`, `packages`). A module's first build places it by its declared edges alone; from its second it is placed by what was true. **The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build* dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports. One cycle is real and resolved by the kinds themselves: the runtime image is built by the build machine, and the build machine stands on the runtime image; the image comes first, built by the build machine that is running, which is the only one there could be — a `built-by` edge never orders a module after a build machine that stands on it. A provision is not a dependency of this relation: a consumer binds to its provider through what the push renders, and a change to the provider's image changes nothing in the consumer's; a consumer whose build does read a provider's source declares it. "A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and not the plan's. **2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers: tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it answers, the tiers, and each module's state — is written to the store before any build is asked. The handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store. **3. A tier is done when it is built, and when what the next tier needs from it is running.** A module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next tier is asked only once every module in this tier is built and every rolled-out module of this tier that a later tier is `built-by` has been applied by the machines running it — the machines' reports say so. A module whose policy says *record* is built and not waited for. So a merge touching the build machine and the controller builds the build machine, waits until it is the build machine that is running, and only then asks for the controller's build. **4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built. A plan that has waited past a bound is named red there, which is the first fact of [issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list. ## Consequences - The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue 184's remaining cause — a handler that waits for its own work — is removed rather than worked around; the bus's heartbeats stop being dropped under a merge. - A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are in the queue (mesh-controller 194). - A release across repositories is a plan whose edges cross repositories; the order a person kept in a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan, not by a separate release record. - The explicit `build --on ` stays as the way to ask for the same plan by hand. - A module's `build.on` remains the one place a manifest states a dependency the store cannot see. ## How this is checked | Rule | Checked by | |---|---| | Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call | | A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said | | Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases | | The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome | | An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after | | A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone | | `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan | | Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked | ## References - [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) - [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) - Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)