Issue 127 resolved; design 29 says what wildcards are and how the rule is checked

Every module named its events the way the old bus spelled a routing key, so on the
new bus every cross-module subscription pointed at a namespace nobody publishes to.
Nothing failed — the services started and none of them reacted. Converted, and the
rule now has checks at both scales: at registration for one manifest, and as a test
across the whole catalogue where a consumed event's emitter is present.

It was larger than the report said, in two directions nobody had looked. Forty-three
files of module code pass the event name at runtime, so the code mattered as much as
the manifests. And both clients had to learn the mapping — without that, converting
the modules would have broken the mesh that is actually running, which is the
opposite of what fixing this was for.

Design 29 gained three things it did not say: what a wildcard is (`*` for one name,
`**` for the rest, spelled the mesh's way and derived to each bus's own), that an
event about a role belongs on the seat and why that is not yet possible, and how the
rule is checked — because "a subscription that matches nothing is silence" is exactly
why nobody noticed thirty-seven manifests being wrong the same way.

4.2 and 4.3 are unblocked. The catch-up half of 4.5 is not: it is a decision, and it
narrowed rather than closed. It cannot be a reply to a module's inbox, because that
needs the blanket grant design 25 §4 refuses.
This commit is contained in:
2026-09-27 14:45:08 +02:00
parent dd577e9ebe
commit c4a8455e2e
4 changed files with 168 additions and 14 deletions
@@ -1,9 +1,9 @@
---
status: open
status: resolved
opened: 2026-09-27
located-in: []
fixed-by:
amended-design:
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/29-what-a-module-declares.md
---
# 127 — A module's event derives a subject nothing publishes
@@ -0,0 +1,109 @@
# 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](../../03-DESIGN/01-to-be/29-what-a-module-declares.md) §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 29 §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](../../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §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.