Files
hq/02-DECISIONS/0118-a-module-declares-its-own-seats.md
T
jschoubben d2ed3152d3 Seat renames done; 0118 was wrong that it was a migration
A holding is derived at resolution from manifests, never stored, so there
are no recorded old names to rewrite. The work is an edit plus a kept rename
table — kept because a module lives in its own repository and may be
registered long after the catalogue stopped using an old name.
2026-09-26 23:06:55 +02:00

146 lines
9.1 KiB
Markdown

---
topic: the tiers
status: accepted
date: 2026-09-26
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
---
# 118. A module declares its own seats; the mesh reserves its own
## Context
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) closed the set of seats. Its
evidence was strong and still is: nothing could answer *which seats does this mesh have, and who
holds each*. Answering it meant reading every manifest in two repositories and then the
controller's own code, and when that enumeration was done by hand while writing the record, **it
reported eleven claims where there were thirteen.** The fix was a table in the controller, and
adding a seat became a decision.
What that table cannot express is the architecture [ADR 0117](0117-the-bus-is-the-only-broker.md)
opened. With one bus and no private brokers, a module offering a service to other modules offers
it as **a role on the bus**: a set of subjects, exactly one holder, addressed by what it does
rather than by which module or node provides it. A telegram sender, a licensing master, anything
a mesh might want one of. Under a closed table, adding any of those means editing the controller
— so a capability contributed by a module would require a change to the mesh itself, which is the
coupling the module system exists to prevent.
**The two requirements look opposed and are not.** 0110 needs the set *enumerable*. The
architecture needs it *extensible*. Those conflict only if enumerable means *written down in one
place by hand* — which is exactly the property that let the count drift in the first place.
## Considered Options
1. **Keep the closed table, add each new seat by decision.** Rejected. Every capability a module
contributes would need a change to the controller and a record before it could be offered, and
the mesh would carry the names of services it does not itself implement.
2. **Free-form seats, as before 0110.** Rejected for 0110's own reason, unchanged: nothing can
say what a mesh has, and a name invented at a claim site is a name nobody can explain later.
3. **A set that is closed at any moment and derived rather than maintained**, with the mesh's own
seats reserved by name. Adopted. 0110 weighed options 1 and 2 and never considered this one.
## Decision
**A seat may be declared by a module, and the set of seats a mesh has is derived: the mesh's own,
plus those declared by every module it has registered.** The set is still closed — a seat named
nowhere is refused — but it is computed from the catalogue rather than written in the controller.
Everything 0110 decided about what a seat *is* stands untouched: one holder at its scope; a
definition says which seats a module *can* hold and an assignment says which it *does*; holding
one may deliver a provision; a seat makes a role singular, never a module.
**Enumeration is a query, not an inventory.** The catalogue knows every registered manifest, so
"which seats does this mesh have, and who holds each" is answered by asking it. This is a
stronger answer than the table gave, not a weaker one: a derived list cannot drift from reality,
and drift is how the hand-made count came out at eleven of thirteen.
**The mesh's own seats are reserved by prefix.** Every seat the mesh itself defines is named
`mesh-*`, and a module declaring any `mesh-*` name is refused at registration. The prefix *is*
the reservation rule — no list of reserved names to maintain, and no way for the mesh's own
namespace to be colonised by a manifest. This requires renaming the seats that drifted from
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention: `the-catalogue`
becomes `mesh-catalog`, `git` becomes `mesh-git`, and the node-scoped `the-build-machine`,
`the-dns-port`, `the-intrusion-prevention`, `the-packet-filter`, `the-private-network`,
`the-resolver-configuration`, `the-showcase` take the same prefix.
The mesh's seats stay the mesh's for a reason that does not apply to a module's: **the mesh's own
code looks them up by name.** The resolver *is* the thing that finds the store. `mesh-store` is
not a convention the controller follows, it is an identifier the controller dereferences.
**A declared seat carries a protocol.** A module declaring a seat says what may be sent to it,
what it emits, and what it serves. The holder must satisfy it; a module may not claim a seat whose
protocol it does not implement. Callers declare that they use the *seat*, never the module, so
replacing the implementation changes nothing for any caller.
**A seat is for a role; an event stays addressed to its emitter.** The two are not
interchangeable and the choice is not stylistic. An event is *this happened to me* — the emitter's
identity is the meaning, which is why the envelope carries source, node and time
([ADR 0042](0042-the-shape-of-an-event-on-the-wire.md)); routing it through a role would erase the
provenance an audit needs. A seat is *this capability, whoever provides it* — where not knowing
the holder is the point. Publish an event when the fact is about you; declare a seat when you are
offering something another module could offer instead.
**Two modules declaring the same seat name is refused at registration**, second one loses.
Registration is the last moment the mesh can still say no, and a seat name meaning two different
protocols is the failure nobody could diagnose afterwards.
## Consequences
- **The controller's seat table stops being the set** and becomes the mesh's own reserved entries.
Resolution reads the catalogue for the rest.
- **Ten seats are renamed.** A rename is a migration, not an edit: existing assignments hold the
old names, so the change carries a mapping and is applied once, and the lab beds that name seats
are updated with it.
- **A `uses` naming an undeclared seat is refused at registration**, which is where 0110's
guarantee lands under this model — the same refusal, at the same moment, from a derived set.
- **Adding a capability stops requiring a decision record.** That is a real loss of governance and
the intended trade: the argument for a seat's existence moves into the module that declares it,
where it is reviewed as part of the manifest. The mesh's own seats keep the old bar.
- **[ADR 0041](0041-events-are-a-relationship.md)'s machinery claim is already stale** for a
different reason, and is corrected in place there under the rule in
[`README.md`](README.md) — a progressive insight: on JetStream a subscription is a durable
consumer, a real object someone must create.
- **What got harder:** a seat's protocol is now a compatibility surface between modules that do
not know each other. Changing one breaks callers already bound to it, and nothing here says how
that is versioned. It is the first thing to answer in the design, and the thing most likely to
hurt later rather than now.
## How it is checked
- **The overview answers, and is right.** A command lists every seat, its scope, its protocol and
its holder, derived from the catalogue — and a test asserts the count against a fixture mesh,
because an enumeration nobody checks is how thirteen became eleven.
- **`mesh-*` is refused to a module.** A registration test: a manifest declaring `mesh-anything`
is refused, naming the prefix as the reason.
- **An undeclared seat is refused.** A registration test on `uses`, and a resolution test that
nothing reaches runtime unresolved.
- **A second declarer loses.** A registration test: two manifests, same seat name, the second
refused and the first untouched.
- **A holder must satisfy the protocol.** A claim whose module does not serve what the seat
declares is refused at assignment, not discovered when a caller times out.
## Progressive insight
> **Progressive insight — 2026-09-26.** *A seat rename is not a data migration.* This record's
> consequences say "a rename is a migration, not an edit: existing assignments hold the old
> names, so the change carries a mapping and is applied once". Implementing it showed there is
> nothing stored to migrate: a seat's holding is **derived at resolution** from the claims in
> manifests (`resolve.go` builds it each time), never written down, so no recorded name is left
> pointing at the old one. What exists is source — the controller's seat table, the manifests
> that claim them, and a manifest that may be registered later from its own repository. So the
> change is an edit plus a **kept** rename table, which tells a manifest written against an old
> name what it became rather than refusing it as unknown.
>
> The decision — that modules declare seats, that the mesh reserves `mesh-*`, and that the ten
> are renamed — is unchanged. Only the shape of the work was wrong.
## References
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — superseded here; its
requirement is kept and only its mechanism replaced.
- [ADR 0117](0117-the-bus-is-the-only-broker.md) — one bus, which is what makes a role addressable.
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the naming convention
the reserved prefix restores.
- [ADR 0041](0041-events-are-a-relationship.md) — the event half of the boundary drawn here.