ADR 0237, to-be 45 Phase 5: a change is judged against the mesh that runs before it merges

The operator approved Phase 5. Decides what the design left open: the build seat runs the
merge check, the facts live in the artifact store, the merge gate composes the mesh as it is
and with the change and judges only what the change adds, a replay lives where its incident
is, and the controller's tests run a bus of their own at the mesh's release. A core issue now
resolves only with a replay or a stated reason, checked by cycle.py.
This commit is contained in:
jochen
2026-10-06 21:10:54 +02:00
parent be0754ec7f
commit d245df7dea
11 changed files with 244 additions and 3 deletions
@@ -0,0 +1,162 @@
---
topic: the mesh
status: accepted
date: 2026-10-06
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md
---
# 237. A change is judged against the mesh that runs, before it merges, on the build seat
## Context
**The operator approved starting Phase 5** of [to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md)
on 2026-10-06: the facts snapshot, the merge gate, versions tested as run, and the replays of
[ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) rule 9. The
design says what they are; building them left decisions it does not make — where a check runs on a mesh
that has no CI, what a snapshot must hold for a check to compose a machine faithfully, where a replay lives
when its incident is in one repository's logic and when it is in what the mesh runs, and how a test suite
that failed in parallel can be one a gate is allowed to read.
The evidence is research 031's class (h), *checks that pass in CI and fail live*, and the incidents of the
window since:
- **236**: a manifest passed `module check` and was refused whole by the node-engine on the first machine
it was assigned to. **263**: a real machine's name made a consumer's identity 26 characters against a
bound of 20, and the provider's whole machine could not be pushed. **262**: an answer glibc forgave was
final to musl. Each check was right about the world it was given; none was given the mesh's.
- **278**: a file of a module no machine held read as shared code, and a merge rebuilt 103 modules. Its
width could have been read before the merge; nobody was shown it.
- **The controller's suite failed when its packages ran in parallel** against one shared bus, and one test
hung under the race detector: the live tests assert, read and remove the mesh's own objects by their
fixed names, so two packages at once were one deleting what the other read. A red suite read as noise.
- **The bus tests ran a release the mesh did not run** (2.10's instructions in the tests' own comments
after the bus had moved to 2.11), and the 2.10 release that skipped messages (266) had been tested by
nobody's tests.
## Options weighed
**Where a check runs.**
1. *An outside CI.* Rejected: a second system to run, watch and keep in step, with no access to the mesh's
facts, its artifact store or its toolchains — the very things a check must be fed.
2. *The live controller composes the change.* Rejected as the gate: a change to the controller is judged by
the controller it changes, so the running one cannot judge it; and a composition run inside the serving
controller is load and risk on the control node for every pull request.
3. **The build seat, as one more kind of work on its queue** — chosen. The build machine already holds the
repositories, a container runtime, the artifact store and the mesh's Go toolchain; the controller already
asks it for work and watches every ask (S6).
**Where the facts are kept.** A key-value bucket on the bus would need a grant for every reader and a bus
that carries a document of a megabyte. **The artifact store, under `facts:latest`** — the design's choice —
is read by any machine of the mesh over plain HTTP, keeps one version, and is where the build seat already
reads and writes.
**Where a replay lives.** All in mesh-lab would put a test of the controller's planning in a repository that
cannot import it. All in their own repositories would leave the bus's release and the resolver's answers —
what the mesh *runs*, not what it wrote — with no home. **Both, by kind** — chosen.
**What the tests' bus is.** A shared bus per run needs serialised packages and still leaks between tests.
**A server per test, linked in at the release the mesh runs** — chosen.
## Decision
1. **The facts snapshot.** The controller (lease holder) composes it every ten minutes and keeps it in the
artifact store as `facts:latest` when its content moved or the one kept is a day old; the manifest it
replaced is deleted by digest, so the nightly collector takes it and the store keeps one. It holds every
machine — under a stable pseudonym of the same length as its name — with its roles in words, system,
C library, architecture, builds, reported capabilities, assignments, pins, settings and the names (never
the values) of secrets a person gave; every seat and holder; every module the mesh holds, as its manifest,
with its source and build edges; the bus's, store's and node-engine's versions as they run; and how each
machine's declaration composes today. No secret, no address: a key or value reading as a secret is
withheld, an address becomes one from a documentation range, and machine, site, account and domain
names are replaced wherever they appear. `facts`, `facts compose`, `facts export` read and write it by
hand. **S14** raises `facts-stale` past two days.
2. **The merge gate is the controller's `merge-gate`.** It raises two throwaway stores from the snapshot
through the controller's own records — the mesh as it is, and with the change's manifests in place of the
mesh's — composes every machine twice in each (the first time making the credentials a push makes), and
runs the node-engine's validator over each body. **What composes in the first and not in the second is
the change's**, named by the machine's roles and the module; what was already broken is said and fails
nothing. It also fails: a manifest the judging controller cannot read; a consumer the change leaves out
of a grant or a credential it leaves bound elsewhere; a module a machine runs removed from its source;
two definitions of one module name; a stored setting the changed definition cannot keep; and a module no
machine runs yet that the node-engine would refuse on the first machine that could run it (tried there
and taken back). It **warns** when a merge would rebuild more than twelve modules, and says when shared
code is why — the width read with the same rule the merge handler uses, the changed directories that
hold a definition read from the tree as the forge's announcer reads them.
3. **A catalogue change is judged by the controller the mesh runs** (version skew caught: a field only a
newer controller reads fails the pull request, not the registration after it); while the running one
predates the gate, by the controller's main, said. A controller change is judged by itself. A node-engine
change is judged by the running controller built with the change's validator in place of the one it
vendors.
4. **The check runs on the build seat.** The forge's announcer — the one that announces merges — announces
each new head of an open pull request (`pull.updated`) and marks it pending; the controller, for a
repository it builds a module from into that branch, asks the build seat a check: the head, and beside it
the controller the mesh runs and its main, the catalogue, the node-engine it runs, and mesh-lab. The
builder reads the snapshot, raises a throwaway store and bus **of the versions the snapshot says run**,
builds the judge, and runs the repository's own `merge-check.sh` — or the gate alone for a repository
with none — **in the mesh's Go toolchain, in a container of its own with no container runtime socket**:
a pull request is code nobody has approved yet. Then mesh-lab's replays from its main — reviewed code —
with the socket, against that bus and the change's own catalogue. The verdict is pass, warning, fail, or
**error, never read as a pass**, said by the controller as `checked`; the forge's holder sets it as the
head commit's status `mesh/merge-gate` and, when it is not a pass, comments with the check's own account.
Nothing a check does is recorded or registered. Whether the status is required to merge is the
operator's setting on the forge.
5. **A replay lives where its incident is.** One of a component's logic is a test in that repository,
named `TestReplay<issue>`, written only with what the component had before the fix, so it can be laid
over the older commit. One of what the mesh runs — the bus server's release, the resolver the catalogue
configures under musl and glibc — is in mesh-lab's `replays/`. mesh-lab's `register.go` names every
replay with its issue and fix, and `replays/cmd/prove` runs each on the commit before its fix (it must
fail, or, for a check that did not exist, not build) and on its fix (it must pass).
6. **A core issue resolves with its replay or a stated reason.** An issue opened from 2026-10-07 whose
`located-in` names a core repository — mesh-controller, mesh-host, mesh-tools, mesh-sdk, or the
catalogue's bus, forge or build-agent module — cannot be `resolved` without `replay:` (a register id) or
`replay-none:` (why none is possible). The issues the replays were written from carry theirs.
7. **The tests' bus is a server of their own, of the release the mesh runs.** The controller's suite links
the bus server in at the version go.mod pins and starts one per test; a test holds that pin to the
catalogue's bus image and, given a snapshot, to the release the mesh runs. The suite runs its packages in
parallel and under the race detector; a test reading timing, not state, is rewritten to read state. A
person may still point a run at a bus of their own with `MESH_TEST_NATS_EXTERNAL=1`.
## Consequences
- **A pull request to the core or the catalogue waits minutes for its verdict**, and the controller's
merge check runs the whole suite. That is the cost ADR 0227 accepted against the hours each of 202, 236
and 263 cost.
- **The gate is as faithful as the snapshot.** What the snapshot does not carry — a machine's adopted
state's detail, its tunnels, ports chosen by hand — a machine may compose differently in the gate than on
the mesh; then the gate says the machine composes on the mesh and not as raised, and judges it by what the
change adds. A gap that hides a real failure is found by the live probe D1, which stays.
- **The build agent pulls the toolchain, store and bus images for every check**, and mesh-lab's replays
pull the resolver and two C libraries' images from the public registry.
- **A bus upgrade is a pin moved in two places**: the catalogue's image and the controller's go.mod, which
a test holds equal. That is the rule, not a cost of it.
- **The forge's holder stays TypeScript** for this change; porting it to Go is its own piece of work.
## How it is checked
| Rule | Checked by |
|---|---|
| the snapshot carries no secret, address or name, and every machine's length | the controller's test raising a mesh with secrets, settings holding a password, an address and a machine's name, a credential made, and asserting none is in the snapshot; the scrubber's own tests |
| the snapshot is kept, one version, and said when stale | the artifact store's live test against the store's registry (put, read back by tag, the replaced one collected); S14's suppression in the generated signals test |
| the gate fails 236, 263, version skew, a removal, and says 278's width | the controller's gate tests, one per incident, against a raised mesh |
| a check runs the repository's script beside the mesh's versions, leaves nothing, and is never a pass when it cannot run | the builder's live tests against a container runtime and a registry |
| the verdict reaches the pull request, an error never as a success | the forge module's tests |
| 236, 262, 263, 266 (and 273) fail before their fix and pass after | `replays/cmd/prove` in mesh-lab |
| a core issue resolves only with a replay or a reason | `00-META/checks/cycle.py` |
| the tests' bus is the mesh's release, and the suite is deterministic | `internal/testbus`'s tests; the suite run three times in parallel under the race detector |
## References
- [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) rule 9,
[to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §9 and Phase 5.
- [ADR 0225](0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md),
[ADR 0232](0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md),
[ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md).
- Issues [236](../04-ISSUES/236-the-catalogue-check-passes-a-manifest-the-host-refuses/00-report.md),
[262](../04-ISSUES/262-an-alpine-container-could-not-find-a-machine-by-its-mesh-name/00-report.md),
[263](../04-ISSUES/263-every-consumer-pays-for-the-tightest-backends-name-limit/00-report.md),
[266](../04-ISSUES/266-a-merge-on-the-bus-was-never-handed-to-the-controller/00-report.md),
[273](../04-ISSUES/273-a-rule-for-the-resolver-moved-a-machines-databases/00-report.md),
[278](../04-ISSUES/278-a-module-held-by-no-machine-was-read-as-shared-code/00-report.md).
+1
View File
@@ -205,6 +205,7 @@ python3 00-META/checks/index.py fail if stale
- **0231** — [A healer acts on what observation raised, and only observation says it worked](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)
- **0234** — [The mesh holds a conversation with its operator, over channels that are seats, and an answer that performs an action is authorised by the controller](0234-the-mesh-holds-a-conversation-with-its-operator.md)
- **0236** — [A build is judged on its first machine and put back by something other than itself, and so it rolls out unattended](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
- **0237** — [A change is judged against the mesh that runs, before it merges, on the build seat](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)
### Its tiers, from the bottom up