Building the bus: the decisions the work needed, and what it taught back #150
@@ -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
|
||||
|
||||
@@ -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.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user