Files
hq/03-DESIGN/01-to-be/16-module-coverage.md
T
jschoubben 411f0680b8 Document the whole module surface, and keep it true with a test
A reference table goes stale the day somebody adds a field, so this one points at
mesh-catalog/modules/showcase — a module that uses all of it — and a test that
fails when it stops doing so. Read the module when the table disagrees with it.

Two rows in the coverage survey were stale because of this week's work: systemd
units were a file plus a service, which made every author write unit syntax and
is why "process" exists; and building from source was images only, where a
bundle now names a language and lets the mesh choose the toolchain.

And the two rows at the bottom of the resource table are the interesting ones.
"action" is refused to modules outright — the link may not carry a command, so a
module needing something done ships a program that reconciles. "service"
installs no unit by design, right for software shipping one and wrong for code
the mesh built, which has none until the mesh writes it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:06:16 +02:00

10 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be designed
2026-09-01
02-DECISIONS/0009-modules-and-the-graph.md
02-DECISIONS/0005-the-node-host.md
02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md

What a module must be able to say

Measured, not guessed. 127 manifests in the system being replaced were read and every key counted, then set against what the new manifest can express. This document is the coverage checklist: what is already sayable, what is deliberately not, and what is missing.

Surveyed 2026-09-01. Counts are modules, not occurrences, unless stated.

Already sayable

what it says used by how it is said here
depends on another module 65 requires naming a module. A requirement naming a module means that module, not anything providing the name
system packages 28 the package shape
a container 48 the container shape, pinned by digest
systemd units 17 a process, which the mesh writes the unit for. Was: a file for the unit and a service for its state — which made every module author write unit syntax, and is why process exists (18)
how to reach it 17 serves, with the mesh adding which machine and where
a public name 11 requiring route and contributing the name
ports it opens 11 listens, from which filtering is computed
data directories, and who owns them 38 directory with owner; never removed while holding anything (ADR 0030)
what it provides and requires 15 + 2 provides / requires, named for what the consumer is coupled to
restart when something changes 11 restart-on
a generated credential 20 own-secrets, sealed to the machine
a value that differs per node 43 settings, and computed for what only the mesh knows
images built from source 2 build.artifacts — an image from a Dockerfile, or a bundle, which names a language and lets the mesh choose the toolchain (18)

Deliberately not sayable

Stage hooks — 36 modules. Arbitrary code at install, configure and start. The link may not carry an action (ADR 0005): what may be pushed is bounded by form, and a command to run is not a form. A module needing setup logic ships a program that reads what the mesh delivered and reconciles — which is what the provisioners are, and they are ~350 lines each including the reasoning.

Flavours — 6 modules. Variants of one module. Retired in favour of claims (ADR 0009): two display servers are two modules that both claim the seat, and adding a third changes nothing anywhere else. What is lost is extends chains, which were doing inheritance and are better as separate modules.

Missing, and what each would take

Ordered by how many modules need it. The largest entry turned out not to be a gap at all, which is left in place rather than deleted: the first framing of it was wrong in an instructive way, and a checklist that quietly loses its biggest item reads as though nobody looked.

Tool servers — 56 modules — not a gap

Over half the modules ship a tools/ directory that becomes tools an agent can call on that node. This was written up as the largest single gap. It is expressible with what exists, and the first framing of it was wrong in a way worth keeping: a module provides tools, the session requires them does not work, because a requirement has exactly one answer and 56 modules offering tools would be 56 answers to one question.

Turn it around and it fits exactly. The session provides tool-host; every module offering tools requires it and contributes where its tools are. Many-to-one is what contributes has always been, and the session receives all of them in one file:

given:  { from: gitea,  values: { at: … } }
        { from: minio,  values: { at: … } }
        { from: umami,  values: { at: … } }

Verified by resolving it, not by reading the code. It also only became possible today: until 022 was fixed, several modules on one node requiring the same thing was refused outright.

What remains is not vocabulary but a decision about what a tool server is — a container the module already runs, and what the session does with the list. That is work, not a missing shape.

Schema migrations — 14 modules

A module with a database needs its schema brought up to date before it runs. The mesh does this for its own contexts and has no way for a module to declare it. The provisioner pattern covers it — a program that runs migrations and exits — but nothing expresses this must happen before that starts, which is the actual requirement.

Configuration merging — 18 modules, 134 files

Files assembled from a module's default plus per-node overrides, with a strategy (replace, merge) and a format (toml, yaml, json). Settings already merge into a file's content; what is missing is format-aware merging.

And it should stay missing. A mechanism that understands TOML will be asked for YAML, then INI — which is how the arrangement being replaced became something nobody could hold in their head. The module knows its own format because it wrote the rest of the file.

Health checks — 7 modules

{type: port|url, expect: …}. The mesh knows whether a container is running, which is not the same as whether it answers — a distinction this project has paid for twice already.

An action carries a verify and is exactly this shape. It is not available to a module: the link may not carry a command to run (ADR 0005), and a module's resources reach a machine over the link. So a health check needs a way to say ask this and expect that without saying run this — closer to a listens entry than to an action.

This is the gap most worth closing, because running and answering being conflated is a class of fault, not an inconvenience.

Theme knobs — 3 modules, 101 values

{theme: {kind: color|font|string, label}} — declared so a ricing tool can offer them. Settings already carry the value; what is missing is the metadata saying a value is presentable and what kind it is. Small, self-contained, and only interesting once something presents them.

Event routing — 2 modules

{routing-key: tool}, generating a consumer. Two modules; wait for a third before deciding.

Publishing a package — 6 modules

Modules published to a registry and consumed as libraries. This is a build output the mesh does not deliver to a node, so it may not belong here at all.

What checking the coverage found

Two faults, both surfaced by asking what a real node looks like rather than what a test does. Neither is about the vocabulary; both are about the machinery under it.

A credential belonged to a machine, not to a module (022, fixed). A node running three services against one database could not be planned at all — and on the consumer's side did not refuse, it just gave two of the three no credential. Every scenario written to date had one consumer per node, which is the natural shape of a small test and not the shape of a machine.

A consumer could not build a connection string (023, fixed). It had its password in the right shape and the host, port and user name were out of reach: the user name was invented by the provisioner and recorded nowhere, and the bound values sat in a JSON document that an application reading KEY=value cannot use.

The asymmetry was backwards, which is what made it worth stating. The secret is the hard case — the mesh must not be able to read it — and the secret was the part that already arrived. The host and port are ordinary facts held in the clear, and they were the ones stuck. Both halves came from the same thing: the mesh knew something and did not say it.

What the survey found that is not about coverage

Declaration and reality had drifted in the system being replaced. Several live provisions are brokered by modules whose manifests declare nothing — a speech-to-text engine served to a consumer on another node, an object-store bucket held by a module whose manifest mentions only its database. A manifest that does not have to be true stops being true, which is the argument for resolution refusing rather than warning.

Two derivations of the same fact. A module's kind was computed in two places from different evidence — one from the manifest, one from what is on disk — producing different labels for the same module. There is one derivation here, and there should stay one.

A live listing returned credentials in plaintext. Not a coverage question, but the reason sealing is worth its inconvenience.

An image store is a module, and was written up here as something the mesh does. It was considered for the substrate and removed, because the test is not can it grant itself one — nearly anything passes that — but whether the control plane needs it before it can give its first instruction. It does not. So a registry somebody runs for their own images is the same module as the one the mesh runs for its own: it offers a place to push, and claims that role once per machine.

A rule was enforced only at the far end. A module may not declare an action, and the host refused one correctly — but the control plane accepted it into the catalogue, resolved it and pushed it, so the refusal arrived on a machine with nothing tying it back to the manifest. The rule held; it was just unusable, which is the same shape as the network shape that cost five failing tests before anyone read the host's log. It is now refused where it is written.