The tidy version said a module provides names and requires names and that is the only edge. Working postgres through completely disproves it. A small game wanting to store data does not require postgres to EXIST. It requires postgres to MAKE IT A DATABASE and hand back credentials. Those are different relations in every way that matters: one creates something per consumer, carries a payload back, can be revoked, and leaves the provider holding state about who was granted what. The other creates nothing. So: two kinds of edge, one graph. Instantiation implies presence; presence does not imply instantiation. The current system already had exactly this split — `dependencies` for presence, `requires: provision:` for instantiation, with the resolver deriving one from the other. analysis.md called that derivation a convenience. It is not: it is the correct relationship between two genuinely different relations, and the design had collapsed them. Postgres also turns out to be nine things, not one. A container. Persistent state where moving nodes is a migration rather than a reschedule. Configuration partly derived from the machine's hardware. A tool surface. A provisioner. Its own bookkeeping about what it granted, which is not the data it stores. An exposure decision per node it runs on. Credentials it generates, which means a provisioning edge carries a secret. And health that is not "the container is up". Four questions the worked example makes concrete rather than abstract. WHICH postgres, when there are two — a consumer of `terminal` does not care and a consumer of a database cares permanently. How many instances a module should have, which cannot be a global rule because one-per-mesh is wrong for a store a disconnected node needs and one-per-node is wrong for the mesh's own registry. What happens to a grant when its consumer is removed, where dropping is data loss and keeping is a leak. And whether a declaration is composed PER NODE from what that node reported — because tuning follows hardware the control plane cannot know, and the alternative is the host deciding, which ADR 0037 forbids.
170 lines
13 KiB
Markdown
170 lines
13 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.
|
|
|
|
**[`worked-postgres.md`](worked-postgres.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
|
|
|
|
| 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. |
|