10 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| the mesh | accepted | 2026-10-01 | jochen | false | 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). 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).
- 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'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 <base>stays as the way to ask for the same plan by hand. - A module's
build.onremains 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 |
Built and proven live, 2026-10-01
Progressive insight — 2026-10-01. The decision stands; these are the facts of its building.
Built in mesh-controller 197 (the relation, the plan record, the driver, status), 198 (plans),
199 (a built-by edge orders and gates but never widens — the first live plan had taken the whole
catalogue along for a controller change; plans stop), 200. The first merge handled by the finished
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
the build machine; tier 1 the controller and the proxy that packages its source. The handler
returned at once; the build machine was built, rolled, and the plan read tier 0 built; waiting for
builder on novox to be applied until the machine reported; then tier 1 was asked, both built, and
the plan read done — three minutes, read through the console with plans, the receive loop taking
reports throughout. What the day between decision and proof taught is in issues
184,
186 and
188.