Files
hq/01-RESEARCH/011-the-module-graph/00-overview.md
T
jschoubben c9c2dfe686 011: what a feature is, and what it splits into
The operator wants features gone, and 006 left it open. Measured, and the answer
is that nothing replaces them because they were never one concept.

A feature is a kind of content a module carries, detected from its directory:
twenty-one of them, each with a handler owning six stages — build, publish,
install, configure, start, verify.

The structural finding: EVERY handler implements EVERY stage. `configs` writes
files onto a node, has nothing to build, and has a build stage. `npm` publishes
to a registry, has nothing to start, and has a start stage. One interface spans
build-time and apply-time, so every kind of content must implement both halves
and most do nothing in one — and a stage that does nothing looks exactly like a
stage that failed to do anything.

They split four ways, across three tiers. Artifacts built once per version and
published, where no node is involved — delivery. Resources that are desired
state on a machine, which is what ADR 0043 already describes and the host already
does — tier 0. Actions run once against something that is not this machine, like
a migration against a database on another node — delivery, and seeds go
entirely. And checks: the prerequisites are REQUIREMENTS IN DISGUISE, a module
saying what must be true before it can be installed, which is what an edge in
the graph says; the verifiers are the read-back the host already performs.

So `feature` is one word for four things spanning three tiers, which is why the
pipeline is hard to reason about.

One property must survive the split, and it is the thing the current design got
right: content is DETECTED, relationships are DECLARED. A module that says it
has migrations and has none is a fault nobody sees until it matters — but what
it requires and provides is not visible in a directory and has to be said.
2026-08-26 22:51:19 +02:00

163 lines
12 KiB
Markdown

---
status: active
initiated: 2026-08-25
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.
**[`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
| Question | Why it is open |
|---|---|
| ~~What does the graph **delete**?~~ | **Answered for the design** in [`proposal.md`](proposal.md): the distinction between requiring a module and requiring a resource, the interface as a kind of thing, capability checking as a separate mechanism, domain grouping, and possibly tiers as a separate concept. *(For the existing system the answer was nothing, because it is already there — [`analysis.md`](analysis.md).)* |
| When two modules provide one name, who chooses? | The substance of the interface idea, and the proposal does not settle it: a node setting, a mesh setting, or an explicit pin. |
| What does a provider hand back? | A consumer requiring `postgres` needs credentials and an address; one requiring `terminal` needs a command. Same relation, different shape flowing across it. |
| ~~Where do domain modules fit?~~ | **Answered: they do not.** A `networking` module gathering a firewall, resolver and proxy under one name came from an older shape. There is no such thing to install — there is core infrastructure, a set of concrete modules named individually, not flavourable, with no grouping module in front of them. |
| Would the missing declarations be used? | Zero manifests declare exclusions or capabilities, which is equally consistent with *nobody needs them* and *nobody can express them*. Nothing measured separates those. |
| Should `provider:` become a real edge, or should the relationship be declared twice? | It names a module and means *depends on*. Reading it as an edge fixes the closure; the alternative is requiring the consumer to also list it under `dependencies:`, which is duplication a resolver already avoids elsewhere. |
| Should placement leave the catalogue? | A provision pins itself to a named node, in the manifest. Which node runs what is an inventory decision — tier 2 by the skeleton's own test — and having it in tier 4 means a second node cannot provide the mesh's database without editing the module that consumes it. |
| Where does resolution happen — mesh or platform package manager? | The mesh must model mesh-level edges. Whether it also resolves operating-system packages, or delegates, decides whether a solver has to be written. |
| Is an interface a module, or a name? | Arch makes it a name that packages claim. Making it a module gives it a manifest, an owner and a place to document the contract — and a thing with no implementation to install. |
| What does an exclusion mean for something already installed? | Refuse the install, or make the conflict visible and let it be decided. The second is a policy surface; the first is a package manager. |
| Does node adoption scan for capabilities, applications, or both? | The operator proposes scanning an adopted node and enabling what it finds. Under the working position above, the scan is for capabilities — but a machine with a terminal already installed is also a module already satisfied, and whether that is adoption or drift is undecided. |
| One installation image, or several? | Proposed: pre-built images carrying different capability sets, so a machine is adopted quickly. Several images bake capability sets at image time, which is the filing problem in a new form and reintroduces what detection exists to avoid. One image carrying the host and nothing else is [ADR 0038](../../02-DECISIONS/0038-a-node-joins-by-linking-first.md)'s *one binary installed by hand*, automated. The effort should settle which. |
| What happens to domain grouping? | [ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md) is still `proposed`. If the graph is the answer, 0017 is superseded rather than narrowed — its text is never edited. |