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.
146 lines
9.1 KiB
Markdown
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.
|