Files
hq/02-DECISIONS/0134-the-mesh-says-what-it-applied.md
T
jschoubben 891c7a945e ADR 0133 and 0134: who runs migrations, and the mesh saying what it applied
0133 — a module owns its migrations and the mesh owns when they run. A container declares what must
run before it; the mesh derives the gated step from the resource it precedes, so the image, the
environment and the credentials come from the one place they are described. The module owns the SQL,
the dialect and the lock; the mesh owns the moment and refuses to start a version whose step failed.
Per node, with no level: a step that ran once somewhere leaves every other machine ungated, and
'once, mesh-wide' is what holding a seat already means.

0134 — the pipeline is observable from a merge to an artifact and goes dark at the machine. What a
node now runs, and what it refused, become facts under the control plane's own seat, emitted when
what a machine runs changes rather than on every convergence pass.

Design 32's lifecycle carries both; issue 133 points at them as what ends the matter it opened.
2026-09-28 11:45:47 +02:00

127 lines
8.2 KiB
Markdown

---
topic: the mesh
status: accepted
date: 2026-09-28
deciders: jochen
reconstructed: false
---
# 134. The mesh says what it applied
## Context
The pipeline is observable on the bus from a merge to an artifact, and modules already plug into it:
the forge emits `pull.merged`, the build machine's seat emits `built`, the catalogue emits `registered`,
`upgraded` and `rebuild-needed`, providers emit `postgres.database.provisioned` and its siblings. Things
consume them today — the catalogue consumes `built`, each provider consumes its own provisioning events,
model-usage consumes `*.usage.*`, the audit logger consumes `**`. Nothing had to be invented for any of
that; subscribing *is* plugging in.
**It goes dark at the moment it touches a machine.** A host applies a declaration and reports to the
control plane on the control branch, which only the control plane may read — correctly, because a report
carries what a machine is and enrolment travels the same way. So nothing on the mesh says *this machine
now runs version Y of module Z*, or that it refused to, or why.
What that cost on 2026-09-28, in one morning:
- A build result the store refused was visible only to whoever was waiting on that build's reply. For
three quarters of an hour the mesh built things and recorded none of them, while the overview said
every module was current ([issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)).
- A module crash-looping at start — 338 restarts — was found by reading a container's logs by hand.
Nothing on the bus said the mesh's graph had stopped learning.
- A run-once step that fails now stops an upgrade by design ([ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md)),
and the same silence would cover it: the version simply would not appear.
**And the one thing the control plane does emit is refused by its own account.** Answering a catalogue
that asks to catch up, it publishes each recorded build under `mesh.mod.control-plane.event.…` — a
module namespace for a module that does not exist. Its own permissions refuse it, so a catalogue that
restarts gets nothing and keeps its gap. The control plane has facts to state and nowhere to state
them.
## Decision
**The mesh emits the deploy half of the pipeline as facts on the bus.** What a machine now runs, and
what it refused to run and why. Both are facts about the mesh doing its work, in the same form as every
other fact on the bus, so anything that wants them subscribes the way the catalogue subscribes to
`built`.
**The control plane states them, as the holder of the `mesh-controller` seat.** Its facts live under the
seat's own namespace, which is where a role's events belong
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md),
[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md)) and which survives the control plane being
replaced. That is also what gives the catch-up replay a subject it may publish instead of an invented
module namespace.
**Emitted when what a machine runs changes, not on every convergence pass.** A host reconciles
continuously and reports each time; a fact per pass would be a fact per minute per machine that says
nothing. The report carries the declaration it applied and what changed, so the control plane has what
it needs to speak only when there is something to say.
**A refusal is a fact with a subject in it** — which machine, which resource, and the reason as the host
gave it. A refusal that names only the machine is the silence this record is about, one level up.
**Reports stay where they are.** A node's report remains control traffic that only the control plane
reads. The deploy facts are derived from it, which makes them second-hand on purpose: one emitter, one
ordering, and no widening of the narrowest account in the mesh.
## Options considered
1. **Leave it as it is, and let whatever cares ask the control plane.** Rejected: asking for a fact that
already arrives is the shape the mesh removed everywhere else, and nothing can react at the moment a
machine changes — which is exactly when a graph, an audit or an operator wants to know.
2. **Each node emits its own facts.** Rejected: it widens every host's account to an event namespace, and
a host's authority is deliberately the narrowest in the mesh. Its report already reaches the one thing
that can speak for it.
3. **Widen who may read the control branch.** Rejected: that branch carries what machines say *to* the
control plane, enrolment included. Widening its readers widens that too, for an unrelated reason.
4. **A registry of deploy hooks** — something registers interest and is called. Rejected: an event is
already the mechanism; there is nothing to register, and a callback is an address the mesh spent
[issue 102](../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
learning not to keep.
5. **Put the facts on the node's own declaration stream.** Rejected: that stream is last-per-subject by
design, so a machine away for an hour gets exactly the current declaration and nothing older. A
history of what happened cannot live in a stream built to forget.
## Consequences
**The audit logger gets the deploy half for nothing**, because it consumes everything.
**A failure becomes visible where the mesh is watched** rather than where someone happened to be
looking. That answers the open question [issue 133](../04-ISSUES/133-the-control-planes-schema-is-migrated-at-birth-and-never-again/00-report.md)
left about a record the store refused.
**The catch-up replay stops being a burst of events.** With the control plane able to state its own
facts, replaying history as if it were happening now is a choice rather than the only option — and the
better shape is the question the catalogue is actually asking, answered once
([design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md)).
**The facts are second-hand.** The control plane says what a machine reported, so a machine that cannot
reach the bus produces no fact at all. Absence is not health, and what a machine was last heard from
stays the place that says so.
**The events stream carries more.** Bounded by emitting on change rather than on every pass, and each
fact is small; the stream's own limits remain what keeps it finite.
## How this is checked
- **The control plane's grant names exactly the subjects it emits**, derived from its seat like every
other principal's, and the composed user list is compared against a golden file — so a fact it cannot
publish fails a test rather than a catalogue's replay.
- **A convergence that changed nothing emits nothing.** A test with two identical reports and one
expected fact, because the failure this guards against is a fact per minute per machine.
- **A refusal names its resource.** A test where a host reports a failed resource and the emitted fact
carries which one and why, not merely that something went wrong.
- **What the mesh emits is what something consumes.** The subject a module declares it consumes derives
to the subject the control plane publishes — the same agreement test that already keeps the
controller's own subscriptions honest.
## References
- [ADR 0041](0041-events-are-a-relationship.md) — an event is a relationship, not a call
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, because the emitter's identity is the meaning
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — a role's events belong to the role
- [ADR 0083](0083-one-push-leaves-the-mesh-consistent.md) — a report is held for the store rather than lost
- [ADR 0133](0133-a-module-owns-its-migrations-and-the-mesh-owns-when-they-run.md) — the gate whose failure this makes visible
- [`03-DESIGN/01-to-be/32-what-a-module-declares.md`](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §6 — the lifecycle, which ends today at a report nobody else may read
- Measured 2026-09-28: 45 minutes of builds recorded nowhere with the overview reporting health; a module at 338 restarts found by hand; the control plane's only emitted event refused by its own permissions