From 0531d6fc38e39b82696940b8a11718faaef33bae Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 23:41:18 +0200 Subject: [PATCH] =?UTF-8?q?ADRs=200044=20and=200045=20=E2=80=94=20the=20mo?= =?UTF-8?q?dule=20design,=20closed;=20011=20graduates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 011 opened asking what a graph deletes and found the graph already existed. The work became design, worked through twenty cases and one provider in full. Two decisions close it. 0044 — a module declares presence, instantiation and exclusion. Two kinds of edge because a game wanting a database is not a game wanting postgres to exist: one creates something per consumer, carries credentials back, can be revoked, and leaves the provider holding state. Names are concrete unless providers are genuinely substitutable — `terminal` passes, `database` fails, and the adapter is what creates an interface. Where there is no contract there is a tag, which describes and does not bind. Exclusion is a third relation and is not derivable. A node provides names too, which makes capability checking stop being a separate mechanism and makes the host's detection an input to resolution. Constraints, never placement. Scope decides which provider and the binding is written down and sticky, in a place that follows the scope. And there are three entities, not two — the assignment carries what belongs to neither end, which is what node-agnostic modules ran out of. 0045 — a context owns its store, exclusively. No shared writes and no read roles on another context's store, because reading couples you to its layout just as firmly and invisibly. The unit is the CONTEXT, not the process: a board showing the mesh's own data is the mesh showing its own data. Asking or subscribing is derived from ADR 0036 rather than chosen. And it is the first clear list of what the design removes: grant kinds, table ownership, cross-context migration ordering, and a class of permission modelling. 0017 is superseded rather than narrowed — its text unchanged, its status changed. Folders assert relationships where edges record them, and the domain module goes with it. Left explicitly undecided in both: what a resolver delegates rather than reimplements, how many instances a module should have, and what a provider hands back. --- .../011-the-module-graph/00-overview.md | 7 +- ...-outside-the-core-are-grouped-by-domain.md | 3 +- ...es-presence-instantiation-and-exclusion.md | 126 ++++++++++++++++++ 02-DECISIONS/0045-a-context-owns-its-store.md | 93 +++++++++++++ 4 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md create mode 100644 02-DECISIONS/0045-a-context-owns-its-store.md diff --git a/01-RESEARCH/011-the-module-graph/00-overview.md b/01-RESEARCH/011-the-module-graph/00-overview.md index 787b5fb..c5f629a 100644 --- a/01-RESEARCH/011-the-module-graph/00-overview.md +++ b/01-RESEARCH/011-the-module-graph/00-overview.md @@ -1,6 +1,11 @@ --- -status: active +status: graduated initiated: 2026-08-25 +became: + - 02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md + - 02-DECISIONS/0045-a-context-owns-its-store.md + - 03-DESIGN/01-to-be/06-the-control-plane.md + - 03-DESIGN/01-to-be/07-the-substrate.md touches: - 02-DECISIONS/0002-everything-is-a-module.md - 02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md diff --git a/02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md b/02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md index 0da332b..411b283 100644 --- a/02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md +++ b/02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md @@ -1,5 +1,6 @@ --- -status: proposed +status: superseded +superseded-by: 02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md date: 2026-08-23 deciders: jochen reconstructed: false diff --git a/02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md b/02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md new file mode 100644 index 0000000..32ae6ee --- /dev/null +++ b/02-DECISIONS/0044-a-module-declares-presence-instantiation-and-exclusion.md @@ -0,0 +1,126 @@ +--- +status: accepted +date: 2026-08-26 +deciders: jochen +reconstructed: false +supersedes: 0017-modules-outside-the-core-are-grouped-by-domain.md +--- + +# 44. A module declares presence, instantiation and exclusion + +## Context + +[Research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) set out to find the +catalogue's missing structure. It opened with *what does a graph delete?* and the answer for the +existing system was **nothing — it is already there**: 126 manifests, 103 edges, no cycles, +nothing dangling, and a resolver already used for build, load and install order. + +So the work became a design question rather than a discovery one, worked through twenty cases +and one provider in full. + +## Decision + +### Two kinds of edge, one graph + +**Presence** — the thing must exist and be reachable. Nothing is created, nothing flows back. +*An editor requires a terminal. A dashboard requires a container runtime.* + +**Instantiation** — a provider is asked to make something *for this consumer*, and hands back +what the consumer needs to use it. *A game requires a database from the store, and receives one, +with credentials.* + +Instantiation implies presence; presence does not imply instantiation. They differ in whether +something is created, whether a payload returns, whether it can be revoked, and whether the +provider holds state about it — which is too much to collapse into one relation for tidiness. + +### Names are concrete unless providers are genuinely substitutable + +A requirement names either a **concrete** thing — that module and no other — or an **abstract** +name satisfied by whatever provides it. + +**An abstract name is legitimate only where a consumer can be switched between providers without +changing.** `terminal` passes. `database` fails: a consumer speaking one store's protocol does +not speak another's, so the name would promise what no provider delivers and the resolver would +report a requirement satisfied that is not. + +**The adapter is what creates an interface.** A name has several providers *and a contract they +all satisfy*, or it is not an interface. Nothing declares itself to be one. + +**Where there is no contract there is a tag.** *Database* remains a useful word for finding +things and grouping them in a catalogue. Tags describe; edges bind; keeping them apart is what +stops a second relationship appearing that looks like a dependency and is not. + +### Exclusion is the third relation, and it is not derivable + +`excludes` names what cannot coexist with this. Two modules providing one name look +interchangeable, and two things wanting one port look independent, right up until installing the +second breaks the first. + +### A node provides names too + +A node's profile is a set of provided names — a display server, a container runtime, an +architecture. A module requiring one is satisfied by **the node**, exactly as one requiring a +store is satisfied by another module. + +**So capability checking is not a separate mechanism.** There is one question — *is this name +provided by anything available here?* — and a graphical application cannot land on a node +without a display server for the same reason, through the same code, that it cannot land +without its dependencies. + +This makes the host's capability detection an **input to resolution** rather than a report for a +person. And what a node provides is partly *derived*: installing a container runtime makes the +node provide `container-runtime` thereafter. + +### Constraints, never placement + +A module says what must be true of a node and never which node. Which node runs what is an +inventory decision, and today's catalogue decides it in manifests — a module pinning its store +to a named node, so a second node cannot provide it without editing the consumer. + +### Scope decides which provider, and the binding is written down + +A consumer of an instantiation edge declares the **scope of its own need**: one instance shared +across every instance of itself, or one each. That decides without naming a node — a shared need +cannot be met by something each node runs separately. + +The mesh then binds, and **the binding is recorded and sticky**. Not recomputed: a resolver that +re-derives which store serves a consumer will one day derive a different answer and relocate a +database. **Where it is recorded follows the scope** — a shared grant belongs to the module, a +per-instance grant to the assignment. + +### A module, a node, and an assignment + +Three entities. The assignment carries what belongs to neither end: where state lives, how it is +reached, configuration derived from the hardware, and which provider instance serves this +consumer. + +Recorded because the alternative was tried: modules were once node-agnostic, and it did not +survive — there was nowhere for these to live. + +## Consequences + +- **[ADR 0017](0017-modules-outside-the-core-are-grouped-by-domain.md) is superseded.** Folders + assert relationships; edges record them. What grouping was for — finding things, seeing what + belongs together — is a tag and a query over the graph, neither of which anybody keeps true by + hand. The domain module goes with it: there is no `networking` thing to install, there are + concrete modules named individually. +- **Provider stops being a category**, as service and application already had. Any hosted thing + can be a factory — an identity provider grants clients, a mail server grants mailboxes. It is + a facet, not a kind. +- **`excludes` and node capabilities do not exist in any manifest today**, so this adds + declarations rather than removing them. What it removes is listed in + [ADR 0045](0045-a-context-owns-its-store.md), which is the other half of this design. +- **A resolver is still needed**, with version constraints and conflicts. What it delegates to + the platform's package manager rather than reimplementing is **not decided here**. +- **Two axes remain undeclarable**: how many instances a module should have — not derivable, and + opposite for a broker and a store — and what a provider hands back, which differs between + credentials and a command. + +## References + +- [Research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — the measurement, the + cases, the worked provider, and what happens to `feature`. +- [ADR 0045](0045-a-context-owns-its-store.md) — the ownership half. +- [ADR 0043](0043-a-declaration-is-an-ordered-list-of-owned-resources.md) — what the graph + produces for a node. +- [ADR 0002](0002-everything-is-a-module.md) — survives; this says what a module declares. diff --git a/02-DECISIONS/0045-a-context-owns-its-store.md b/02-DECISIONS/0045-a-context-owns-its-store.md new file mode 100644 index 0000000..7a88c1c --- /dev/null +++ b/02-DECISIONS/0045-a-context-owns-its-store.md @@ -0,0 +1,93 @@ +--- +status: accepted +date: 2026-08-26 +deciders: jochen +reconstructed: false +extends: 0044-a-module-declares-presence-instantiation-and-exclusion.md +--- + +# 45. A context owns its store, exclusively + +## Context + +[ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md) settles what a module +declares. This settles what a grant may be, and it is the half that **removes** things. + +`how-we-build` §4 already says *contexts integrate through the record, never through a shared +schema*, and states the cost: several domains share one forty-five-table schema, which is why +work belonging to one context keeps having to be implemented in another. + +That was written as a principle. Counted, it is thirteen foreign tables belonging to three +separate contexts, living in the mesh's own registry database. + +## Considered options + +1. **A schema per consumer inside a shared database.** Namespaced, revocable by dropping the + schema, with a cross-context join possible but deliberate. Rejected: it keeps the letter of + §4 and leaves the temptation in place, and a boundary that is merely inconvenient to cross + gets crossed. +2. **Read-only roles on another context's store.** Rejected for the same reason and one worse: + reading another context's tables couples you to its layout exactly as firmly as writing them, + and the coupling is invisible until the owner changes a column. +3. **Exclusive ownership.** Chosen. + +## Decision + +> **A context is granted only what it exclusively owns.** + +No shared writes. No read-only role on another context's store. If you need what another context +holds, you ask it or you subscribe to it. + +**The unit is the context, not the process.** Everything inside a context — its service, its +surface, its tools — reads its own store freely. A board showing the mesh's own nodes and +modules is the mesh showing its own data, not a boundary crossing. What is forbidden is a +*different* context reading it. + +### Asking or subscribing is derived, not chosen + +[ADR 0036](0036-a-node-is-a-managed-machine.md) makes disconnection an ordinary situation. So: + +- **Anything that must keep working while disconnected cannot ask** — there is nobody to ask. It + keeps a local copy, which means subscribing. +- **Anything where a stale answer is worse than none cannot subscribe.** A display may lag; a + decision about whether a grant is still valid may not. + +Neither is a query against another store, whatever transport it travels over. + +## What this removes + +The first clear list of what the design deletes rather than adds: + +- **Grant kinds.** There is one: an exclusive resource. No schema grants, no read roles, no + rules about who may see what inside a shared thing. +- **The question of who owns which table**, and the guessing at revocation time. Removing a + consumer drops what it was granted, whole. +- **Cross-context migration ordering.** Two contexts migrating one database must be ordered + against each other. Exclusive ownership means a context's migrations are ordered only against + itself. +- **A class of permission modelling** a shared store would otherwise need. + +## Consequences + +- **Cross-context reporting is harder, and that is the point.** Anything wanting to see across + contexts consumes their events or calls their interfaces. That is §4's argument, and the cost + it names is the one already paid. +- **A single surface over several contexts still works** — that is what a surface is. It reads + interfaces, not stores. This holds while the contexts sit behind **one** interface; splitting + a context into its own deployable costs that, and the composition would have nowhere to live + that tier 3 permits. **A real constraint on how far the control plane may be split.** +- **Three contexts must move out of the registry database**, taking thirteen tables with them. + Their dependency on the registry then shrinks to almost nothing — one of them needs a single + table. +- **The node appliers were already handled.** [ADR 0037](0037-the-host-applies-it-does-not-decide.md) + stopped the host querying the mesh database for tier reasons unrelated to this, and it removes + most of the remaining direct readers as a side effect. +- **What a consumer does about events missed while disconnected is not decided** — replay from a + point, ask once and resume, or rebuild. The question every projection has. + +## References + +- [`how-we-build.md`](../00-META/how-we-build.md) §4 — the rule this makes enforceable. +- [Research 011](../01-RESEARCH/011-the-module-graph/worked-provider.md) — the count, the worked + provider, and the dashboard case. +- [ADR 0036](0036-a-node-is-a-managed-machine.md) — why asking or subscribing is derived.