--- 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 - 02-DECISIONS/0006-schema-changes-are-numbered-migrations.md - 02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md - 04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.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. [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. ## Coverage against what a module carries today The five declarations describe how modules **relate**. A module is more than its relations, and the as-is manifest surface ([`02-modules-and-manifests.md`](../../03-DESIGN/00-as-is/02-modules-and-manifests.md)) is the checklist the graph has to be measured against facet by facet — otherwise the effort can look finished while most of what a module actually carries is unplaced. Weighed one at a time: | Facet today | Likely landing | Weighing | |---|---|---| | requires / provides a resource | the graph | exists today; carried over | | service shape, data directories, firewall rules, managed configuration, service units | typed resources in the host declaration ([ADR 0043](../../02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md)) | covered — and refusal-on-unknown is what closes the class of fault in [issue 003](../../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md), where a manifest key restricted nothing in silence | | environment | resolved by the control plane; reaches the node inside declared resources | covered in principle | | migrations | module runtime — never host-declared resources | [ADR 0006](../../02-DECISIONS/0006-schema-changes-are-numbered-migrations.md) stands. What must survive any re-integration is the two-kinds distinction: migrations against a module's own state, and migrations against a provisioned resource, which run where the resource is consumed | | scheduled tasks | a declared resource (a timer) or module runtime | minor either way | | per-node selections | partly dissolves: the accelerator variant is *requires a node capability*; the public-versus-private variant is configuration, not a variant | to test — if it dissolves fully, it belongs on the list of what the graph deletes | | the tool surface | **no home — open** | below | | verification | **open** | below | | cross-component contributions | **probably dissolves — confirm, don't assume** | below | | lifecycle hooks | shrink toward module-side; host-side effects become declared resources | inside the recorded "integration is wrong" claim; the code/data boundary is already set by ADRs 0039/0043, not by this effort | Three of these are genuinely open rather than mapping work. **The tool surface has no home in the tier model.** Most of the catalogue carries tools — they are how an operator drives a module. Tier 3 is "thin, no logic", but a module's tools are module-specific logic: a media library's queue commands do not belong in a generic surface. No record says whether tools are a facet the module carries — discovered by the control plane, served through tier 3 — or something else. By count of affected modules this is the largest unscoped facet. **When is a provides-edge true?** [Issue 007](../../04-ISSUES/007-an-installed-package-is-not-a-capability/00-report.md) answers negatively: not when the package is installed. The strong candidate — every `provides` names its verifier, and the resolver trusts only verified edges — would make health checks the graph's truth condition, and satisfies the rule that a rule states how it is checked. The step too far to avoid adopting untested: intrinsic capabilities are already detection, and a mandatory verifier on every edge invites trivial verifiers — which are worse than none, because they are believed. What to require, and on which edges, is open. **Cross-component contributions probably dissolve, and should be confirmed rather than assumed.** The candidate sixth relation — module A contributes something to component B's feature: a log pattern to the intrusion filter, a dashboard to the metrics stack — looks like a new edge type. For the intrusion filter it is not: that concern is absorbed into the host, so the contribution is an ordinary typed resource in the declaration and no edge exists. What remains open is whether the module-to-module cases also reduce to a typed resource the consumer reads, or genuinely need an edge. The answer decides whether the vocabulary grows — and per ADR 0043 every added type is reviewed as a security artefact, so it should be measured, not defaulted. ## Open questions | Question | Why it is open | |---|---| | What does the graph **delete**? | If modules gain declarations and lose nothing, this is motion rather than progress. The effort has not finished until it names what stops existing. | | 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. | | Where does the tool surface live, and how is it discovered? | The largest unscoped facet — see the coverage table. Tier 3's "no logic" rule and module-specific tools pull in opposite directions, and no record resolves them. | | When is a provides-edge true? | Issue 007 rules out "when installed". Whether every `provides` must name a verifier — and what stops verifiers from being trivial — is undecided, and the resolver's trust model depends on it. | | Do cross-component contributions need an edge? | Probably not — host absorption turns the measured case into a declared resource — but the module-to-module cases are unmeasured, and a new edge type versus a new resource type is a security-vocabulary decision either way. |