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

9.1 KiB

topic, status, date, deciders, reconstructed, supersedes
topic status date deciders reconstructed supersedes
the tiers accepted 2026-09-26 jochen false 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 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 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'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); 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's machinery claim is already stale for a different reason, and is corrected in place there under the rule in 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 — superseded here; its requirement is kept and only its mechanism replaced.
  • ADR 0117 — one bus, which is what makes a role addressable.
  • ADR 0079 — the naming convention the reserved prefix restores.
  • ADR 0041 — the event half of the boundary drawn here.