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.