Files
hq/01-RESEARCH/011-the-module-graph/00-overview.md
T
jschoubben e1febe8e0f Renumber the records 1 to 23
The consolidation left a sparse sequence -- 1, 4, 6, 7, 9, 10, 12, 15, 16, 18,
19, 25, 34, 35, 36, 37, 40, 42, 44, 45, 48, 49, 58 -- where the gaps were only
the archaeology of what used to be there.

Renumbered contiguously. Renames run in ascending order, so every target number
is already free and no two files ever collide.

The reference rewrite is one simultaneous pass rather than a sequence of
replacements. Numbers moved into slots other numbers were vacating -- the node
host went 37 to 16 while the lab went 16 to 9 -- so replacing one at a time
would have cascaded and silently pointed things at the wrong record.

Seven plain-text references survived the merges as prose rather than links,
naming records that no longer existed: the enrolment token, the link boundary,
what a declaration is, reachability, the repository structure. Each mapped to
the consolidated record that now holds it.

Verified rather than assumed: every [ADR NNNN](path) link now has matching text
and target, checked across the whole repository, and the checker passes.

Frontmatter `consolidates:` lists dropped -- they named records that are gone,
and each consolidated record already says in prose what it absorbed.
2026-08-28 23:28:34 +02:00

16 KiB

status, initiated, became, touches
status initiated became touches
graduated 2026-08-25
02-DECISIONS/0019-modules-and-the-graph.md
02-DECISIONS/0020-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
02-DECISIONS/0019-modules-and-the-graph.md
02-DECISIONS/0019-modules-and-the-graph.md
02-DECISIONS/0015-a-node-and-how-it-joins.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

A third edge was found after this graduated. This effort established presence and instantiation, and both are runtime edges — they answer what does this need in order to run. Delivery needs a different question answered — what has to be rebuilt when this changes — and that is a build edge, fixed inside an artifact rather than negotiated when it runs. Recorded by ADR 0019, which also notes what this effort's three entities turn out to be good for (ADR 0019).

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 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 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 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: 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 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 — 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 0019 proposes grouping modules by domain. Research 005 measured that proposal and found its evidence holds in exactly one place — reachability — which ADR 0016 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 0019 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 0019
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 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). 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).
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).
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 0019 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 0016, 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 0015 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 0016'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 0016 — 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.