diff --git a/03-DESIGN/01-to-be/28-building-the-bus.md b/03-DESIGN/01-to-be/28-building-the-bus.md index 12f1aa2..a56a9a9 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -518,10 +518,11 @@ it, and the beds that need a mesh living on NATS can finally run. What none of them can stand in for is a mesh raising itself, which is what this bed is — so this is where the code stops and the lab starts - [ ] 4.2 a build source's change reaches the builder over the bus, and the build that follows is - the one the change asked for — **blocked by - [issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)** + the one the change asked for — **unblocked**: + [issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md) + is resolved, so an emitter and a consumer of the same event now land on the same subject - [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces — - **blocked by the same** + **unblocked, same** - [~] 4.4 a person's client — **the account is done**: a person is not a module and holds no seat, so their authority is a list of tools (or `*` for an administrator) and nothing else. Held to four properties, each a way of being wrong that would not announce itself: nothing @@ -533,12 +534,22 @@ it, and the beds that need a mesh living on NATS can finally run. Still to build: the client program itself — the command line and the MCP surface over it. It needs nothing from the consume side, so it is not blocked by step 3. - [~] 4.5 reports and catch-up: a node that was unreachable catches up rather than losing them — - **the reports half is in and proved against a server** (3.4): held through the store's - absence by the server rather than by the controller, superseded ones settled by the digest - they carry. The catch-up half is where issue 127 bites hardest: the controller replays a - build announcement under its **own** name rather than the builder's, so a catalogue - filtering the builder's subject hears nothing. Whether the controller may sign an event as - another module is a design question, not a wiring one, and it is open in that issue. + **the reports half is in and proved against a server** (3.4): held through the store's absence + by the server rather than by the controller, superseded ones settled by the digest they carry. + + **The catch-up half is a decision, and issue 127 narrowed it.** The controller answers a + catalogue's request by re-publishing builds under its *own* name, which nothing subscribing 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, which the derived namespace + exists to prevent. And it cannot become a reply to the catalogue's inbox either: answering a + module's inbox needs `_INBOX.>`, the blanket grant design 25 §4 refuses — found while giving the + controller the one narrow inbox it does need, to answer enrolments. + + So two options are left, and they differ in kind. A subject the controller may publish and a + catalogue may subscribe — the mesh's own event space, which does not exist yet. Or a durable + consumer that starts at the beginning of the stream, which removes the need to ask at all and + leans on retention instead: sound while the events are still there, and silent when they have + aged out, which is the failure the request was invented to avoid. **Done when.** Each converted flow is proved against the behaviour it replaced, and the full genesis bed is green. **Observation is not in this step** — heartbeats, conditions and key-value state are diff --git a/03-DESIGN/01-to-be/29-what-a-module-declares.md b/03-DESIGN/01-to-be/29-what-a-module-declares.md index c0373b3..2fa0186 100644 --- a/03-DESIGN/01-to-be/29-what-a-module-declares.md +++ b/03-DESIGN/01-to-be/29-what-a-module-declares.md @@ -2,7 +2,7 @@ layer: to-be status: proposed code: [] -updated: 2026-09-26 +updated: 2026-09-27 decisions: - 02-DECISIONS/0118-a-module-declares-its-own-seats.md - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md @@ -53,6 +53,33 @@ the catalogue, and the mesh would have hundreds of copies of a decision it made | seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.accept.send` | | `uses: telegram-sender` | publish on that seat's `accept` subjects, and nothing else | +**Wildcards, and they are the mesh's rather than a bus's.** *Added 2026-09-27, from +[issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md).* A +consumer may write `*` for one name and `**` for the rest: `*.download.completed` is that event from +any module, and `**` on its own is every event in the mesh, which an audit logger wants and says in +one token. Spelled this way rather than the wire's, for the reason everything else here is local — +the bus the mesh runs on today spells these `*` and `#`, the one being built spells them `*` and +`>`, and a manifest naming either would stop being true when the wire changed. An emitted event +carries no wildcard: it names one event. + +**A module publishes under its own name, and an event about a role belongs to the seat.** *Added +2026-09-27, same source.* The bus enforces that a namespace belongs to the module it is named for, so +an event named for somebody else cannot be published at all. Where the event is really about a role — +"the artifact store accepted an image" — the seat is the right home, because that name outlives +whoever fills it, and a consumer written against the holder's own name breaks when the holder +changes. **Not yet possible in practice**: seats carry protocol in the manifest and in the permission +model, and the shared library has no way for a module to publish on one. Until it does, such an event +lives under the emitting module's own name and the consumer carries that coupling. + +**How the rule is checked, because it was not.** *Added 2026-09-27, same source.* Two checks, because +the mistake happens at two scales. Per manifest, at registration: an event is a local name, and the +old bus's form is refused with the name to write instead. Across the whole catalogue, as a test: +where a consumed event's emitter is present, it must emit that event. The second cannot demand a live +emitter for everything — a module lives in its own repository and may be installed long before the +one whose events it wants — so it says nothing about an absent emitter and everything about a present +one. **A subscription that matches nothing is not an error, it is silence**, which is why nothing +reported thirty-seven manifests being wrong the same way. + **It is `tools:`, not `serves:`.** Revision, found while implementing: the manifest already uses `serves` for the facts a consumer needs in order to reach a provision, and two meanings under one key in the file a module author reads most is a footgun. Worth noting that until now a module's @@ -73,6 +100,13 @@ and every manifest in the catalogue is still correct. That is the property [ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) gave the sdk, applied to declarations. +**A handler names an event the way its manifest does.** *Added 2026-09-27, found while fixing issue +127.* The runtime handed a handler the event name alone, so a manifest declaring +`consumes: builder.built` produced a pattern that could never match the key it was compared against, +and a module consuming one event from two emitters could tell them apart only by reading a header. +The subject already carries the emitter, so the key a module sees names it too — which makes a +disagreement between a manifest and the code a typo rather than a category error. + ## 2. Three namespaces, and nothing else **Its own** — `mesh.mod..>`. Its events and its tools. Nothing else may publish into it, diff --git a/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md b/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md index 09b6733..754cc72 100644 --- a/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md +++ b/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md @@ -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 diff --git a/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/01-diagnosis.md b/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/01-diagnosis.md new file mode 100644 index 0000000..bc1fb92 --- /dev/null +++ b/04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/01-diagnosis.md @@ -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..`. [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 +`.` (`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 `.` 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..` 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 `.`. 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.