Files
hq/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md
T
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

94 lines
5.8 KiB
Markdown

---
status: resolved
opened: 2026-09-27
located-in: [mesh-catalog modules, mesh-control internal/catalogue, mesh-tools src]
fixed-by: mesh-catalog 7b06a7a, mesh-tools fbeb373, mesh-control 05ff606
amended-design: 03-DESIGN/01-to-be/32-what-a-module-declares.md
---
# 127 — A module's event derives a subject nothing publishes
## What was observed
[Design 29](../../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1 says a module names an event
locally and the mesh derives the subject: `emits: order.placed` becomes
`mesh.mod.<module>.event.order.placed`, and a consumer declaring `consumes: shop.order.placed`
subscribes the emitter's own subject. That derivation is built and tested.
**Every event name in the catalogue is still written the way a routing key on the bus the mesh has
is written** — `module.<module>.<verb>` — and the derivation reads it as `<emitter>.<event>`. Asked
of the composer directly, with the module names and declarations the catalogue holds today:
| declared | derived |
|---|---|
| `builder` emits `module.builder.built` | publish `mesh.mod.builder.event.module.builder.built` |
| the catalogue consumes `module.builder.built` | subscribe `mesh.mod.module.event.builder.built` |
| a media module emits `module.<itself>.download.completed` | publish `mesh.mod.<itself>.event.module.<itself>.download.completed` |
| a player consumes `module.*.download.completed` | subscribe `mesh.mod.module.event.*.download.completed` |
The consumer's subject names a module called `module`. **No cross-module subscription in the
catalogue matches what any emitter publishes.** Thirty-seven manifests declare events; every one of
their consume declarations derives this way.
Two further consequences of the same cause, found in the same check:
- One module declares `consumes: "#"` — the wildcard of the bus the mesh has, which is not a
subject at all. The composer **refuses it outright**, so that module's account cannot be composed
and the module cannot be assigned.
- One module emits under a name that is not its own — it declares `module.<other>.image.pushed`
while being a differently named module — which the derivation puts inside *its* namespace. Whether
that is legitimate is a design question: design 32 §2 makes an event's source a fact the server
enforces, and this is a module claiming another's name in its own event.
None of it fails on the bus the mesh runs on today, where a routing key is matched literally and
nothing derives anything. It fails only once the subject is derived — which is to say it fails on
the first mesh raised on the new bus, and not before.
Evidence: run against the controller's own `PermissionsFor` on the current feature branch, with the
declarations read from the catalogue's manifests. Found while wiring the controller's consume side
(design 28 step 3.4), when the controller's own subscription had to be written and the subject it
would have to name turned out not to be the one design 32 specifies.
## Why it matters beyond this instance
**This is the failure [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md)
exists to catch, arriving by a route the conformance suite does not cover.** Two implementations
that disagree about an envelope do not fail to compile — they ignore each other while both keep
running. Here it is not two implementations disagreeing but a *declaration* and a *derivation*
disagreeing, and the symptom is identical: every service starts, every log is quiet, and nothing
reacts to anything.
The fixtures cannot catch 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 check nothing performs, because until the subject was
derived there was nothing to compare.
It also means the rule 3.8 established is weaker than it reads. That task asserted **no manifest
contains a subject**, which holds: a manifest contains a local name. What nothing asserts is that a
local name derives to a subject some emitter actually publishes, and the rule as stated is satisfied
by thirty-seven manifests whose names derive to nothing.
And it blocks work already scheduled. Design 28's step 4.2 (a build source's change reaching the
builder over the bus) and 4.3 (an installation completing over the bus) are both event flows through
exactly these pairs, and the catch-up flow the controller answers is a third — the controller
currently replays a build announcement under its *own* name rather than the builder's, which a
consumer filtering the builder's subject will not hear either.
## Open questions
- Is a local name converted per manifest (`emits: built`), or does the derivation keep accepting the
old form and strip a redundant prefix? The first is thirty-seven manifests and a rule that can be
checked; the second is a rule that cannot, because `module.foo.bar` is also a legitimate three-part
local name.
- What checks the pair? An emitter's derived subject against every consumer's derived subject is a
whole-catalogue check, not a per-manifest one — and a module lives in its own repository and may
be registered long after the catalogue was checked.
- What are `#` and `*` in a consumed name? The bus the mesh has and the bus being built spell
wildcards differently, and a `consumes` pattern is the one place a module writes one.
- May a module emit an event named after another module, and if not, what does the module that does
it today declare instead?
- Who replays? A catch-up answer published by the controller under a builder's subject is the
controller signing an event as another module, which is the thing the derived namespace prevents.
If it must not, then a replay is a different message from an announcement, and the consumer needs
to be told so.