Files
hq/03-DESIGN/01-to-be/32-what-a-module-declares.md
T
jschoubben 784b487bf9 ADR 0131: everything on the mesh speaks to the broker seat, and AMQP is not a provision
Taken during the outage of 2026-09-27, when the protocol leaked into the seat's
contract: to hold mesh-broker a module had to provide amqp, so the module that
will carry the bus could not hold the seat that names the bus, while the module
being retired could. Supersedes 0127. Modules depend on the seat and reach the
bus through the sdk; no manifest provides or requires amqp; the old broker's
module and the two modules that required it leave the catalogue; the AMQP
transport is deleted once every node reports on the new bus.

Design 28 step 5 rewritten under it: the seat handover becomes its own task and
is built first, because the seat the control plane dereferences cannot be empty
in between — that emptiness was the outage. The cost note now carries what was
measured rather than what was assumed.

0128 and 0130 extended 0127; each now rests on 0131 with a dated note and
changes nothing it decided. Every other citation of 0127 names its replacement.
records.py still fails on 0120/0112, which predates this branch.
2026-09-27 23:03:05 +02:00

462 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
decisions:
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0041-events-are-a-relationship.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
---
# 32. What a module declares, and what the bus makes of it
**A module that speaks to the mesh requires the bus, and receives what it needs to connect**
([ADR 0128](../../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md)). What a module
declares are *relationships*; subjects, streams, consumers and permissions are all derived from
those, and a manifest never contains one.
> **Revised 2026-09-26.** This document opened by calling the bus *ambient* — "no module requires
> it, the way no module requires a filesystem". Two counts say otherwise: of 72 modules in the
> catalogue, **49 take a broker credential and 23 do not**, so an ambient connection would mint an
> account for a third of the catalogue that never speaks; and the 49 each hand-write the path it
> lands at, which is provisioning done badly by hand. The bus is required, and a module that does
> not require it has no account at all.
**The requirement delivers the connection; the declarations shape the authority.** `requires:
mesh-bus` says *this module talks to the mesh* and grants no subject by itself. `emits`,
`consumes`, `tools`, `uses` and a declared seat say what it may say and hear. Declaring a subject
without requiring the bus is incoherent and refused at registration.
This document is the declaration model. [Design 25](25-the-bus-on-nats.md) is the bus itself —
subjects, streams, accounts, enrolment — and stays the authority on the wire.
[Design 19](19-the-module-protocol.md) is the specification an SDK implements, and is rewritten
onto this in step 3 of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md).
## 1. A module names locally; the mesh derives the subject
This is the load-bearing rule.
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) says a module
definition names no node, mesh or path. A transport address is the same class of thing: if
manifests held literal subjects, reorganising the subject space would mean editing every module in
the catalogue, and the mesh would have hundreds of copies of a decision it made once.
| declared | derived |
|---|---|
| `emits: order.placed` | publish on `mesh.mod.<module>.event.order.placed` |
| `consumes: billing.order.placed` | durable consumer on `mesh.mod.billing.event.order.placed` |
| `tools: status` | queue-group subscription on `mesh.mod.<module>.tool.status` |
| 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
tools were not declared at all — they were known only at runtime, from an environment variable in
its image — so declaring them is new, and is what lets the mesh check that a module claiming a
seat answers what that seat's protocol promises.
**The `event` / `tool` / `accept` token is load-bearing, not decoration.** Revision, found while
defining the streams: a stream is defined by a subject filter, so a namespace holding both a
module's events and its tool calls cannot be filtered into an events stream without capturing
every tool invocation in the mesh — and a tool call must never be persisted
([design 25](25-the-bus-on-nats.md) §3 keeps tools on core NATS, where a lost call is a timeout the
caller already handles). The kind token is what makes `mesh.mod.*.event.>` a safe filter. The
first draft of this table had no token, which reads better and cannot be implemented.
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
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,
so an event's source is a fact the bus enforces rather than a claim in the body.
**Seats it holds** — `mesh.seat.<seat>.>`. Full participation: consume what the seat accepts,
publish what it emits, serve what it serves.
**Seats it uses** — publish only, and only on the `accepts` half. A sender cannot subscribe to a
seat's inbound subject and watch other modules' traffic, and cannot publish the seat's outbound
events and lie about outcomes.
A module naming anything outside these three is refused at registration. The whole permission set
is derivable from the declaration; nobody writes an access rule.
## 3. Queues are derived, never declared
A module says what it reacts to, not how delivery works. Each `consumes` becomes one durable
consumer; a seat's `accepts` becomes one work-queue consumer with a queue group named for the
seat. The module does not name them, does not know their names, and cannot misconfigure them —
and the controller stays the only writer of stream and consumer definitions
([design 25](25-the-bus-on-nats.md) §3).
**The mesh's own seats carry protocol too.** *Added 2026-09-27,
[ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md).* A seat declared by a
module says what it accepts, emits and serves; the `mesh-*` set said only who does a job. So the mesh
had roles it could not describe — a build machine with three audiences for one outcome and no way to
derive a grant for any of them, and an event genuinely about a role with nowhere to live but the
namespace of whichever module happens to hold it. The mesh's seats now take the same three fields, and
the same machinery derives the holder's authority, its work queue and its consumers.
**So a build is work submitted to a role, like any other.** The build machine seat accepts a build and
emits an outcome, and the dedicated branch that carried builds retires: a work queue shared by several
machines is exactly what `accepts` already is, and a second mechanism for it is two places a permission
can be wrong.
**And one publish reaches three audiences without anybody's inbox being opened.** A build's outcome is
the seat's own event: whoever asked matches it by the id their request carried, the controller records
it, the catalogue places it in the graph. That is the fan-out a shared exchange gave for free, written
as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs
no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.
**Retention belongs to whoever owns the namespace, not to a consumer.** A seat declares how long
its inbound backlog survives, because that is a property of the service:
```
seat: telegram-sender
scope: mesh
accepts: send retain 7d
emits: delivered, failed
serves: status
```
If each consumer could tune it, the mesh's durability would be an emergent property of whichever
manifest was edited last.
**Why a module's own events do not carry their own retention, though the same rule would allow
it.** A seat owns its namespace and gets a stream of its own, so it can say. A module's events
share one `EVENTS` stream, and three facts about JetStream decide that they must:
- **Storage is not a property of a subject.** A subject is only an address; a *stream* is a
separate object that captures subjects matching a filter. So "this topic is durable" is always
really "some stream covers it", and something has to create that stream.
- **Overlapping streams are refused, not merged.** Verified against the server: a per-module
stream beside a shared `mesh.mod.*.event.>` is rejected with *subjects overlap with an existing
stream*. So "one stream by default, its own for a module that wants different retention" is not
available — it is all of one or all of the other, and a filter cannot express an exception
either.
- **A stream per module breaks cross-module consumption.** An audit logger consuming every
module's events is one consumer on one stream today; with a stream each it becomes one consumer
per module, created and destroyed as modules come and go.
So: one stream, and **per-subject caps** for the fairness that actually matters — a noisy emitter
cannot evict a quiet one, which is verified (a cap of three, ten messages on one subject and one
on another, leaves four). What is genuinely unavailable is a different *age* per module, because
JetStream ages per stream and not per subject. A module that truly needs its own retention has a
way to say so: declare a seat, which owns its namespace and gets its own stream.
**Scope gives per-node workers without a new concept.** A module running on three nodes that each
need their own queue declares a node-scoped seat: one holder per node, three queues, same
machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared
service" versus "one worker per machine".
## 4. Five relationships
| | provision | event | job | state | tool |
|---|---|---|---|---|---|
| shape | 1:1 resource | 1:many | N:1 | 1:1 | 1:1 |
| addressed to | a provider | the emitter's own namespace | a **seat** | one node | a module or seat |
| who must act | the provider | nobody | exactly one holder | that node | the server |
| credential | sealed, per consumer | none | none | none | none |
| reply | — | none | none, or an event later | a report | awaited |
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own | `serves` |
**Job** is the one [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) had no room
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
is neither. It is not an event, because an event is a broadcast nobody is obliged to act on and a
second holder would do the work twice. It is not a provision, because there is no resource and no
credential. What makes it safe is not cleverness in the subscribe call but the seat: exactly one
holder, so exactly one worker, by construction.
**State** is the shape the deploy path needs and nothing else uses. A declaration is not an event
— replaying yesterday's is actively harmful — and not a job. Only the newest matters, which is
last-per-subject retention, and a node that has seen sequence *n* refuses *n−1* by construction.
That is the wire-level answer to
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
## 5. Seats
A module declares a seat with its protocol, and the mesh enforces one holder at its scope
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A caller declares that
it uses the *seat*, never the module, so the implementation can be replaced under it.
- The set of seats is **derived** — the mesh's own, plus every registered module's — so it is both
closed and extensible, and enumerating it is a query rather than an inventory.
- `mesh-*` is **reserved**: the prefix is the reservation rule, and a module declaring one is
refused at registration.
- Two modules declaring the same name: the second is refused.
- A module may not claim a seat whose protocol it does not implement.
- **Nobody holding a seat is not an error.** The stream exists from registration, so work queues
until a holder appears. Install the telegram module a week later and the backlog flushes.
## 6. The lifecycle: build, publish, deploy
Every shape above appears once, in order, and no step knows where the next one runs.
**A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An
event, because it is a fact about git and git's identity is the meaning.
**The change becomes work.** The controller consumes `pushed`, asks the catalogue which modules
are built from that repository and path
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and submits one
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
what it is handed, and never resolves anything. A builder that dies mid-build has its job
redelivered, because a work queue with explicit ack is what that means.
**The artifact is published.** The builder pushes to the registry seats and emits `built` —
module, version, digest. An event again: a fact about the builder.
**The build cascade is that event fanning out through a graph the mesh already has.** A module
whose image is built *on* another's artifact declares that in `build.on`. So `built` reaches the
controller, which walks the declared graph and submits rebuild jobs for everything downstream. A
dependency cascade is not special machinery — it is one event, one derived graph, and the same job
queue.
**Deployment is state, not a message.** The controller composes each affected node's declaration
and publishes it last-per-subject. A node that was away gets exactly the current one, never a
queue of superseded ones, and a replayed older one is refused by sequence.
**Applying is reported to a role.** The host applies and reports to the `mesh-controller` seat —
not to an address it was given at genesis. Held and retried while the store restarts
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
What disappears across that chain is every address. No webhook URL, no registered callback, no
"which node is the builder on", no controller endpoint baked into a joining node. That is the
class of bug
[issue 102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
names, dissolved rather than fixed.
## 7. Modules depending on each other
Three kinds, and conflating them is how deployment ordering goes wrong.
**Build-time** — A's image is built on B's artifact. Resolved by the cascade above; nothing at
runtime cares.
**Provision** — A requires a database from B. A **hard** dependency: the credential must exist
before A can start, so resolution gates delivery and A is shown as waiting until B has answered
([design 27](27-a-module-requires-the-mesh-resolves.md)).
**Seat** — A uses B's seat. A **soft** dependency, and this is the one the bus changes. A starts
whether or not anyone holds the seat, because the stream absorbs the gap. Deployment order stops
mattering for everything expressed this way, and a service being restarted, moved or upgraded is
not an outage for its callers — it is latency.
That difference is worth choosing on purpose. A dependency expressed as a provision must be
ordered; the same dependency expressed as a seat need not be.
## 8. Versioning a protocol
A seat's protocol is a compatibility surface between modules that do not know each other and are
deployed at different times. Four ways it can change, and they are not equally dangerous:
| change | example | detectable |
|---|---|---|
| **additive** | a new `accepts` subject, a new optional field | nothing breaks |
| **removal or rename** | `send` becomes `deliver` | yes, mechanically |
| **shape** | an optional field becomes required | yes, if shapes are specified |
| **semantic** | `send` starts meaning *queue for tomorrow* | **no** |
**Additive is free.** A seat may grow without a version, without re-registering a caller, and
without ceremony. Most change is this.
**A breaking change is refused while anyone is bound.** Registration computes a compatibility
fingerprint over the seat's protocol — its subjects and the shapes they carry. A registration that
alters the fingerprint while callers are bound is refused, **and the refusal names them**. The
mesh already holds the `uses` graph, so this is derived rather than declared, and it turns a
runtime breakage into a registration-time conversation.
**When a break is genuinely needed, the version goes in the subject, not the name.** The seat stays
one thing; `mesh.seat.<seat>.v2.<verb>` runs beside v1 and the holder serves both. A caller moves
when it is ready. Versioning the *seat name* was considered and rejected: it forks the role, so
"one holder" stops meaning one provider of the capability, and every document naming the seat has
to be found and changed.
**Binding is recorded, not inferred.** A caller declares `uses: telegram-sender` with no version,
and resolution binds it to the current one and records that — the same **pin** machinery
[design 27](27-a-module-requires-the-mesh-resolves.md) already uses when resolution had a choice
to make. Moving to v2 is a deliberate re-pin, so nothing drifts onto a new protocol because it
happened to be newest.
**Retirement is reported, never automatic.** When the `uses` graph shows nothing bound to v1, the
overview says it is retirable. The mesh does not remove it.
**And none of this catches a semantic change.** Same subject, same shape, new meaning: no
fingerprint sees it, and no check proposed here would. The defences are review, and pushing
meaning into shape wherever it can go — a required `channel` field is caught, a changed
interpretation of an existing one is not. Saying so is better than implying the fingerprint is
complete, because a team that believes it is complete stops reviewing for the case it misses.
## 9. Provisioning over the bus
Provisioning rides the bus, and the provider stops having an address.
| part of a provision | shape |
|---|---|
| the requirement resolving to a provider | the controller's, not the bus's |
| the grant reaching the provisioner | request/reply to a **role** |
| `holds` — the reconcile question, every minute | the same call, on a timer |
| `provisioned` / `deprovisioned` | events |
| the credential reaching the consumer | §10 — fetched, never carried |
A provisioner's interface is already three calls — create, remove, holds — which is exactly a
`serves` protocol. So **a provision interface is a seat whose protocol is those three**, which is
why [design 26](26-the-seats.md) already allows a seat to deliver a provision. The two concepts
were converging before this document; here they meet.
What stays different, and must not be unified away: a provision has a **per-consumer resource and
a sealed credential**, created and destroyed per consumer. A seat protocol has neither — it is a
role you send to. Collapsing them would mean pretending a database is a subject.
### `mesh-bus` and `nats` are two interfaces, never one name
The mesh's own bus is **`mesh-bus`**, delivered by the `mesh-broker` seat and answered by the
controller — because the bus's accounts are configuration rather than something a provisioner
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat)).
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
nervous system or a private queue, and the difference between those is the whole architecture.
0119's rule decides which is legitimate: a private bus is a backing service, never a channel to
another module.
### Where addresses survive
"Where is it?" is two different problems, and the bus solves one of them completely and the other
not at all. Keeping them apart matters, because a reader who thinks the mesh no longer has
addresses will believe a class of bug is fixed when it is untouched.
**Something that is on the bus: the address disappears.** A host reporting in used to need the
controller's address — recorded somewhere, at some moment, and wrong as soon as anything moved.
Now it publishes to `mesh.seat.mesh-controller.report` and the bus routes it to whoever holds the
seat. Nothing anywhere records where the controller is, so nothing can record it *wrongly*. The
same is true of the builder, the catalogue, the telegram sender. This class is not mitigated; it
is gone, because the information is no longer stored.
**Something that is not on the bus: the address stays, exactly as before.** A module that requires
a database does not reach postgres over NATS — it opens a postgres connection, because postgres
speaks postgres and is not listening on any subject. Its credential contains a host and a port,
and no amount of subject addressing changes that.
So the fix for that second class is unchanged and is not this document's:
[ADR 0098](../../02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) —
fetch the fact where it is used rather than storing a copy — which is what
[issue 102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
is actually about. The bus makes that class *smaller* by removing every mesh-internal address from
it. It does not make it empty.
**And one address is irreducible: the bus's own.** A node has to know where the broker is before
it can use subjects for anything, so that one cannot be a subject. It is the addressing equivalent
of §10's bootstrap — the first thing cannot be found by the mechanism that finds everything else.
## 10. Secrets, and why they never enter a stream
Everything the vault does is request/reply to the `mesh-vault` role: mint, fetch, rotate. In that
sense it is as much on the bus as anything else.
**The bus is not trusted with a secret, and does not need to be.** A secret is sealed to its
recipient, so what crosses the bus is ciphertext only that recipient can open. The broker sees
that a secret moved, and to whom — metadata, which is acceptable — and never a plaintext.
**But sealed is not enough on its own, because a stream persists.** A sealed secret written into
a JetStream stream is a durable ciphertext sitting in the mesh's own storage, and the day a
sealing key leaks, that stream is an archive rather than a moment. So:
- **A secret travels on core request/reply, never through a stream.** No persistence, no replay,
nothing to exfiltrate later.
- **A declaration names a secret; it does not carry one.** Declarations are the state shape, which
*is* a stream — so the host fetches the secret from the vault at apply time, over the core path.
That is [ADR 0098](../../02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)'s
existing discipline — *fetched from it, not carried* — applied to the one payload where carrying
it is worst.
**The bootstrap, which is circular and has a precedent.** The vault makes every secret
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)), including the bus's own
passwords. The vault is a module, and a module needs a bus account, whose password the vault
makes. Nothing can go first.
This is the shape [ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md) already resolves for
the control plane: **genesis is a pivot.** The controller mints the handful of foundation
credentials itself, raises the store, the broker and the vault, and then the vault takes over and
mints everything from there — the same move as raising a temporary control plane and reinstalling
it as an ordinary module once the registry exists.
So there are exactly two things the normal path cannot make, both at genesis, both ending the
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseded by [ADR 0131](../../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md): AMQP is not a provision, and everything speaks to the `mesh-broker` seat) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
needs an account before it can run) and **the vault's own credential**. Any third exception is a
design failure, and naming these two is what makes a third one visible.
## 11. Open
**Semantic change has no mechanical defence** (§8). Recorded as open rather than solved, because
it is the residue of a question the rest of §8 answers and the part a fingerprint cannot reach.
**Whether a module may declare a seat it does not itself claim** — the contract as one thing, the
implementation as another, which is how two competing implementations would ever exist.
**Whether `consumes` naming another module couples too tightly.** It is kept here deliberately —
an event's provenance is its meaning — but a consumer of `billing.order.placed` does depend on
billing existing under that name.
## 12. How it is checked
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
subject grammar. The rule is worthless if it is followed by convention.
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
permission set equals what its declaration implies, and a hand-written addition to it fails.
- **A sender cannot read the queue it writes to.** A bed: a module declaring `uses` is refused
subscribe on that seat's inbound subject.
- **One holder, one delivery.** A bed: a seat's job delivered once with the holder running, and
a second claim of the seat refused.
- **A queued job survives no holder.** A bed: submit with the seat unheld, assign the holder,
the job is delivered.
- **The cascade rebuilds exactly the dependents.** A bed: publish an artifact two modules build
on, and exactly those two are rebuilt.
- **A stale declaration is refused.** A bed: replay sequence *n−1* after *n*, and the node refuses
it rather than applying it.