--- 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 - 02-DECISIONS/0036-a-node-is-a-managed-machine.md - 03-DESIGN/00-as-is/02-modules-and-manifests.md - 03-DESIGN/00-as-is/10-module-catalogue.md - 04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md --- # 011 — The module graph ## What is being investigated Whether the catalogue's missing structure is a **graph** — modules declaring what they need, what they offer, and what they exclude — and what that replaces. **[`worked-provider.md`](worked-provider.md) works one module through completely**, and breaks the tidy version. A database is nine things, not one — and a small game asking the mesh for its own database shows there are **two kinds of edge**: *presence*, where the thing must exist, and *instantiation*, where a provider makes something for a consumer and hands back credentials. Instantiation implies presence and not the reverse. The current system already had this split and the design had collapsed it. **[`features.md`](features.md) answers what happens to `feature`.** It is one word for four things spanning three tiers — artifacts built once per version, resources applied to a machine, actions run against something that is not this machine, and checks that are requirements in disguise. Measured: every one of the twenty-one handlers implements all six stages, so `configs` has a build stage with nothing to build and `npm` has a start stage with nothing to start. That emptiness is the conflation, and it is why a stage that did nothing and a stage that failed look alike. Nothing replaces it, because it was never one concept. **One property is worth keeping: content is detected, relationships are declared.** **[`cases.md`](cases.md) enumerates what a module can be** — twenty kinds of thing the mesh has to install, run, own or know about — and extracts the axes a manifest must express. Two of those axes appear in no current thinking: **how many instances** a thing may have, and **whether two can coexist**. **The design is in [`proposal.md`](proposal.md): one kind of edge.** A module provides names and requires names, and that single relation absorbs requiring a module, requiring a resource, and the interface-and-adapter idea. An abstract name is legitimate **only where providers are genuinely substitutable** — `terminal` passes, `database` does not, because a consumer speaking Postgres does not speak MongoDB. The adapter is what creates an interface; without one there is a **tag**, which describes and does not bind. A **node provides names too**, which makes capability checking stop being a separate mechanism and makes the host's own capability report an input to resolution rather than something a person reads. [`analysis.md`](analysis.md) measured the current catalogue. Its value to the design is two lessons rather than its machinery — a field that means *depends on* should say so, and placement does not belong in a manifest — and the rest is recorded as as-is evidence. **Measured, and the premise was wrong for the existing system: the graph is not missing there.** [`analysis.md`](analysis.md) — 126 manifests, 103 edges, no cycles, nothing dangling, and a resolver that topologically sorts them, already called by the tool loader, the installer and the delivery coordinator. What the effort assumed would need building is a thing to call. What survives is narrower: three declarations that do not exist (`excludes`, a required node capability, an interface with adapters), and two defects worth fixing whatever else is concluded — `provider:` is a dependency edge that is not read as one, which makes the closure for a working mesh come out without a database; and the resolver continues past a cycle and past a missing dependency, contrary to ADR 0008. [ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md) proposes grouping modules by domain. [Research 005](../005-domain-grouping/analysis.md) measured that proposal and found its evidence holds in exactly one place — reachability — which [ADR 0037](../../02-DECISIONS/0037-the-host-applies-it-does-not-decide.md) has since absorbed into the host. The measured case for domain grouping has therefore been consumed by a decision taken for unrelated reasons, and what remains is fifty modules that co-change with nothing. That leaves the original complaint unanswered: the catalogue records **what was installed** rather than **what anything is for**, and nothing in the system can see a relationship between two modules. Grouping asserts relationships. A graph records them. ## Why now A proposal to split modules into *provisioning services* and *applications* was worked through and abandoned in favour of one concept with facets, for a reason worth keeping: - The split cannot be filed consistently. A git forge is consumed as a service *and* operated through a web interface. An analytics service grants tracking identity *and* is a dashboard somebody reads. An identity provider grants authentication *and* has an admin console. - The operator's correction is the sharper form: **what runs on the machine is a supervised container, not something a user started.** That is a fact about *how a thing runs*, not about what kind of thing it is — so it is a facet, not a taxonomy. Filing decisions that follow from nothing are the disease research 005 measured. A second taxonomy would reproduce it. So [ADR 0002](../../02-DECISIONS/0002-everything-is-a-module.md) survives, and the question becomes what a module must be able to **declare**. ## The shape being investigated Five declarations, of which two exist today. | Declaration | Today | Notes | |---|---|---| | **requires a resource** — a database, a bucket | yes | provisioning, [ADR 0005](../../02-DECISIONS/0005-capabilities-are-provisioned-on-declaration.md) | | **provides a resource** | yes | as above | | **requires another module** | **no** | the dependency edge — the graph's substance | | **excludes another module** | **no** | installing A makes B unavailable | | **requires a node capability** | **no** | a graphical session, a container runtime, an architecture | And one structural idea on top: **an interface module carries no implementation.** Adapters provide it. An assistant interface with several model-provider adapters; a terminal interface with several terminal adapters. A dependent names the interface and never an implementation. **Prior art to measure against, not invent past.** This is a package manager's model, and the platform's own package manager already has all of it: `depends` is the dependency edge, `conflicts` is exclusion, and `provides` is the interface — several packages provide one virtual name, and a dependent names the virtual one. That the design arrived at the same shape independently is evidence for it. It is also a warning: dependency resolution, version constraints, conflict handling and rollback are a long-solved and easily-botched problem, and the effort should establish what to **delegate** rather than reimplement. ## Capabilities, and what may be installed The operator's formulation: *system specs are capabilities, and capabilities unlock installable modules — you cannot install a graphical application on a node with no display server.* The question that follows is whether the mesh may install a capability. The effort's working position, to be tested: - **Intrinsic capabilities** — hardware, architecture, network position — are facts about a machine. They are detected, never installed, and a module requiring one it does not have is not unresolved but **impossible** on that node. - **Provided capabilities** — a display server, a container runtime — are not a separate kind of thing at all. They are modules that provide a capability, and requiring one is an ordinary edge the graph resolves by installing it. If that holds, "may the mesh install a capability" is not a policy question. It is dependency resolution, and the only genuinely new thing is detection. Which is where [issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) bears directly: *an installed package is not a capability*. A provided capability is not real because a package is present — it is real when it is present, running and working, and the difference is exactly the class of fault this repository keeps recording. ## What it touches beyond the catalogue The operator's assessment is that the machinery around a module is wanted and its integration is not: **scheduled tasks, hooks, migrations, configuration and environment settings are worth keeping; seeds are not; and the current integration is wrong enough to need a major refactor.** That is a claim to test rather than adopt. Research 005 already found supporting evidence from a different direction — that the densest apparent coupling in the catalogue is manifest boilerplate churn, cross-cutting changes to the machinery applied N times — which is what an integration being wrong looks like from the outside. ## Open questions Struck-through rows are answered, with where. The rest are live. ### Settled | Question | Answer | |---|---| | ~~What does the graph **delete**?~~ | For the existing system: nothing, it is already there ([`analysis.md`](analysis.md)). For the design: the module/resource distinction, the interface as a kind of thing, capability checking as a separate mechanism, domain grouping, and — the first clear deletion — **grant kinds**, once a module may only be granted what it exclusively owns ([`worked-provider.md`](worked-provider.md)). | | ~~Is an interface a module, or a name?~~ | A **name**, and only where providers are genuinely substitutable. The adapter is what creates one; without an adapter there is a **tag**, which describes and does not bind ([`proposal.md`](proposal.md)). | | ~~Where do domain modules fit?~~ | They do not. There is core infrastructure — concrete modules named individually, not flavourable, nothing standing in front of them. | | ~~What happens to domain grouping?~~ | Superseded. Folders assert relationships; edges record them. What grouping was for is a tag and a query. [ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md) is `proposed` and should be superseded rather than narrowed. | | ~~Is there one kind of edge?~~ | **No — two.** *Presence*, where a thing must exist, and *instantiation*, where a provider makes something for a consumer and hands back credentials. Instantiation implies presence, not the reverse. | | ~~When two modules provide one name, who chooses?~~ | Neither the consumer naming a node nor the consumer not caring. The consumer declares the **scope of its own need** — shared across its instances, or one each — the mesh binds, and the binding is written down and sticky. Where it is written follows the scope. | | ~~Can several modules share one database?~~ | **No.** A module is granted only what it exclusively owns — no shared writes and no read role on another's store, because reading couples you to its layout just as firmly. | | ~~What about a dashboard reading a dozen stores?~~ | **The rule is about contexts, not processes.** The mesh's own board reading the mesh's own store is the mesh showing its own data — not a boundary crossing. Everything inside a context reads its store freely; what is forbidden is a *different* context reading it. An earlier answer here was wrong. | | ~~Can every registry consumer be served another way?~~ | **Largely dissolves.** Of eighteen direct consumers, the owner keeps its database, node appliers are already stopped by ADR 0037, and the bulk are **foreign tenants** — thirteen tables across three contexts — who need to move out rather than read differently. | ### Live | Question | Why it is open | |---|---| | How many instances should a module have? | Not derivable and not global: one-per-mesh is right for the broker and wrong for a store a disconnected node needs. It is per-module and nothing in the schema says it. | | Can two of something coexist? | `excludes` covers part of it. Two terminals are fine, two things wanting one port are not, two brokers might be either. | | What does a provider hand back? | Credentials and an address for a store; a command for a terminal. Same relation, different shape crossing it. | | Is provisioning one mechanism or two? | The mesh's own registry is provisioned **before there is a mesh**, so provisioning is part of the bootstrap and part of what the carried bundle expresses. At bootstrap the store is local; afterwards it is on another node. Same operation, both sides of a tier boundary. | | Is a tool surface one relation with two audiences, or two? | 56 of 126 modules carry tools — more than carry a service — and what consumes them is an **agent**, not a module. | | ~~Do the remaining cross-context reads want an interface or events?~~ | **Derived, not chosen.** Neither is SQL — that only ever runs against your own store. ADR 0036 makes disconnection ordinary, so anything that must work while disconnected cannot use a request and needs a local copy: a subscription. Anything where a stale answer is worse than none cannot use a subscription. | | What does a consumer do about events it missed while disconnected? | Replay from a point, ask once for a full picture and resume, or rebuild. The question every projection has, and unanswered here. | | What happens to a grant when its consumer is removed? | Dropping is data loss; keeping is a leak. ADR 0043's removal rule does not obviously carry, because the thing lives inside another module's state. | | Is a declaration composed per node, from what that node reported? | Some configuration follows the hardware. Either the host fills a blank — deciding, against ADR 0037 — or the control plane composes from the node's inventory first. | | Would `excludes` and capability requirements actually be used? | Zero manifests declare either, which is equally consistent with *nobody needs them* and *nobody can express them*. | | What does an exclusion mean for something already installed? | Refuse the install, or surface the conflict and let it be decided. | | Are tiers a view of the graph, or a constraint on it? | If a tier is a computed level the word is a convenience. If *a tier may depend only on tiers below it* is to be enforced, it is a constraint and must be stated as one. | | Where does resolution happen — the mesh, or the platform's package manager? | The mesh must model mesh-level edges. Whether it also resolves operating-system packages decides whether a solver gets written. | | How far may the control plane be split? | A single board over several contexts works because they are contexts *inside* one control plane with one interface. If a context becomes its own deployable with its own interface, the board is coupled to N of them and the composition has nowhere to live that tier 3 permits. A constraint on splitting, worth knowing before splitting. | | What does the pipeline schedule, once features are gone? | It schedules features today. The four categories they split into have different lifecycles, so the unit of work differs for each and needs naming. | | Should placement leave the catalogue? | A provision pins itself to a named node in the manifest. Placement is an inventory decision, and having it in the catalogue means a second node cannot provide the mesh's store without editing its consumer. |