Files
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +02:00

6.8 KiB

Diagnosis — 2026-09-27

Where it lives

Three places, and only one of them is a bug in code.

The manifests, in the module catalogue. Thirty-seven declare events, and every one of them spells an event the way a routing key on the bus the mesh runs on today is spelled — module.<module>.<verb>. Design 29 §1 says a module names an event locally and bare (emits: order.placed) and a consumer names <emitter>.<event> (consumes: billing.order.placed). So the manifests are stale against a rule that was already decided, not wrong against an undecided one. This is the whole of the reported symptom.

The manifest's own documentation, in the parser. The comments on emits and consumes still describe the old convention and give the old examples — "dotted topic keys, e.g. module.umami.site.created", and "#" named as the audit logger's pattern. A module author reading the file they read most is being told to write the thing that does not work. That is why the drift was uniform across thirty-seven manifests rather than scattered: nobody was mistaken, everyone followed the documentation.

Nothing checks either one. ParseManifest validates the module name, the slug, what it provides and what it requires. It says nothing about an event name. So a local name that derives to a namespace belonging to a module called module is accepted by every check the mesh has, and the first thing that notices is a subscription that never fires.

What was ruled out

The derivation is not wrong. Asked directly, with the module names and declarations the catalogue holds, PermissionsFor produces exactly what design 32 §1 specifies for the input it is given: it reads a consumer's <emitter>.<event> and builds the emitter's subject. Given module.builder.built it reads the emitter as module, which is a correct reading of an incorrect declaration.

The conformance fixtures are not at fault and could not have caught it. They pin one emitter's envelope against one subject, and both halves of that pair are correct. What is wrong is only visible when an emitter's derived subject is set beside a consumer's derived subject — a comparison nothing performs, because until the subject was derived there was nothing to compare.

Task 3.8's rule is not broken, it is weaker than it reads. That task asserted no manifest contains a subject, which holds: a manifest contains a local name. Nothing asserts that a local name derives to a subject some emitter actually publishes.

What is still a decision and not a conversion

Converting the manifests is implementing design 29, not deciding anything. Three of the report's open questions are not:

  • Wildcards. Design 29's table has no wildcard row, and two manifests need one: a module consuming every download completion across several media modules, and an audit logger consuming everything. The two buses spell wildcards differently, and a consumes pattern is the one place a module writes one.
  • A module emitting under another module's name. One manifest declares an event named for a provision rather than for itself. Design 29 §2 makes an event's source a fact the bus enforces, so this cannot survive as written — and the remedy is probably not a rename but a seat, which is what a name stable across whoever implements it already is.
  • Who replays a build announcement. The controller answers a catalogue's catch-up by re-publishing builds under its own name, which no consumer of the builder's subject hears, and for which it holds no grant. Publishing them under the builder's subject would be the controller signing an event as another module — the exact thing the derived namespace prevents. So the catch-up is either a different message or a different mechanism, and that is a decision.

Owners

located-in names the manifests and the parser. The replay question reaches the controller and the catalogue module together and is recorded above rather than in that field, because it is not where this symptom lives.

Fixed — 2026-09-27

Converted, and the rule now has checks. What it took was larger than the report said, in two directions nobody had looked.

The module code, not just the manifests. Forty-three files pass an event name to emit() at runtime, and the runtime builds the subject from what it is handed. A converted manifest with unconverted code would have had the permission and the subject disagree — the same silence, one layer down.

Both clients had to learn the mapping. Each passed the name straight through, which was right on the bus the mesh runs on today only because modules were writing routing keys. So the old bus's client now turns a local name into module.<emitter>.<event> on the way out and back on the way in. Without that, converting the modules would have broken the mesh that is actually running — which is the opposite of what fixing this was for.

The declaration and the handler spoke different vocabularies. The key a module's handler saw was the event name alone, while its manifest names <emitter>.<event>. So a correct manifest produced a pattern that could never match. The subject already carries the emitter; the key names it now.

The three open questions, answered

  • Wildcards: * is one name, ** is the rest, spelled the mesh's way and derived to each bus's own. ** alone is every event, which is what the audit logger wanted and now says in one token.
  • A module emitting under another's name: not allowed, and the remedy is the seat rather than a rename — a role's name outlives whoever fills it. Deferred in practice: seats carry protocol in the manifest and in the permission model, and the shared library cannot publish on one, so the module that did this emits under its own name and its consumers carry that coupling. Worth a task when a seat's holder needs to emit.
  • Who replays a build announcement: still open, and narrowed. It cannot become a reply to the catalogue's inbox: answering a module's inbox needs _INBOX.>, which is the blanket grant design 25 §4 refuses. So the remaining options are a subject the controller may publish and the catalogue may subscribe, or a durable consumer that starts at the beginning of the stream and removes the need to ask at all. Recorded on the work breakdown as the catch-up half of task 4.5 rather than here, because it is no longer this symptom.

What it found while running

Two dangling subscriptions that predated this and nothing had reported: a module emitting an event its manifest never declared, which the new bus refuses outright, and a module waiting for an event nothing emits — a demo that could never be triggered, because only that module may publish under its own name.