Jochen asked whether the order made sense. It did not -- it followed when things happened to be decided, which after consolidation is fictional anyway since record 5 alone folds decisions taken across a week. Concretely wrong before: the domain statement sat at 8, after five engineering rules; the constitution was scattered across 5, 12 and 17; the tiers landed at 15, 16, 21 and 22 with process records in between. Now it walks: what the mesh is (1-3), its tiers from the bottom up (4-8), what runs on them and how it gets there (9-10), how it is built (11-16), how it is checked (17-18), how we work (19-23). Two things made this safe rather than free. It is a permutation, not a compaction, so the renames go through temporary names -- otherwise two files want one slot and one is lost. And the reference rewrite is a single simultaneous pass, because almost every number moved into a slot another number was vacating; replacing one at a time would have cascaded and pointed things at the wrong record while still resolving. Verified: 284 [ADR NNNN](path) links across the repository, all with matching text and target. The ordering principle is now stated in 19 rather than left implicit -- the repository already said "the numbering is the flow" about its folders, and there was no reason for the records to be the exception.
114 lines
6.0 KiB
Markdown
114 lines
6.0 KiB
Markdown
---
|
|
layer: as-is
|
|
status: implemented
|
|
code: [hal]
|
|
updated: 2026-08-23
|
|
decisions:
|
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
|
- 02-DECISIONS/0013-schema-changes-are-numbered-migrations.md
|
|
- 02-DECISIONS/0014-no-npm-workspace.md
|
|
---
|
|
|
|
# Modules, manifests and features
|
|
|
|
Everything the mesh installs is a module: a directory with a manifest. There is no second
|
|
mechanism.
|
|
|
|
## What a manifest declares
|
|
|
|
| Declares | Meaning |
|
|
|---|---|
|
|
| Identity | Name and version. **Version is owned by the builder** — a hand-edited version is a defect, and reviewers revert it. |
|
|
| Environment | Every variable the module reads, with how each is produced: a static default, a generated secret, a value pulled from the node's own record, or a template composed from the others. A variable not declared here is invisible to the mesh and will not be generated, injected or audited. |
|
|
| What it provides | The resource type this module can provision for others, and the network on which it is reachable. |
|
|
| What it requires | Resources it needs from other modules, and the mapping from each resource's connection fields onto its own environment variables. |
|
|
| Service shape | The primary container, and the data directories that must exist with the right ownership before it starts. |
|
|
| Exposure | The public names this module's interfaces answer on, declared portably so the reverse proxy configuration can be generated rather than written. |
|
|
| Images | Container images this module builds, so the pipeline builds and publishes them before publishing the module. |
|
|
|
|
## Features are the unit of work
|
|
|
|
A module is not the unit the pipeline addresses. A **feature** is.
|
|
|
|
A feature is a kind of content a module can carry: a service, a set of capabilities, a
|
|
long-running process, managed configuration files, migrations, firewall rules, an installable
|
|
application. One module can carry several.
|
|
|
|
Features are **detected from directory contents**, not declared. A module with a capabilities
|
|
directory has that feature; a module with a daemon directory has that one. An explicit
|
|
declaration was supported and is now discouraged, because a declared list and the directory it
|
|
describes drift, and the directory is the one that is true.
|
|
|
|
Every pipeline command and event names a feature. There is no per-module build.
|
|
|
|
### What detection costs
|
|
|
|
Detection makes the manifest shorter and the truth singular, and it makes the directory
|
|
structure load-bearing in a way that is not obvious from reading a manifest. Renaming a
|
|
directory changes what a module *is*, silently. The recurring failure is a hook named for a
|
|
feature the module does not carry: it is skipped without complaint, and the change it was
|
|
supposed to make simply never happens.
|
|
|
|
An unknown key in a manifest is likewise accepted in silence — which is how a firewall rule
|
|
can appear to restrict a port and restrict nothing (see
|
|
[`04-ISSUES/003`](../../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md)).
|
|
|
|
## Kinds of module
|
|
|
|
The kinds are not a type system — they are what the detected features add up to.
|
|
|
|
- **A service module** carries a container definition. It gets a runtime directory, generated
|
|
environment, data directories, and is started under supervision.
|
|
- **A capability module** carries capabilities and no service. It contributes what a node can
|
|
do, locally and to its peers.
|
|
- **A flag module** carries nothing but a manifest. Its presence in a node's assignment is the
|
|
entire content: it gates behaviour elsewhere.
|
|
- **Combinations** are ordinary. A database module is a service *and* a capability provider
|
|
*and* a provisioner.
|
|
|
|
## Selections
|
|
|
|
A module can ship variants of the same feature and a node takes the one that fits it — a build
|
|
for one accelerator or another, a configuration for a public node or a private one. The
|
|
artifact stays selection-blind; the choice is a property of the assignment, held in the mesh
|
|
database.
|
|
|
|
This is one of the areas where behaviour has repeatedly diverged from intent, in both
|
|
directions: selection files that were never packaged into the artifact at all, and a stale
|
|
staged override on a node that silently won over the newly selected one. Both classes are
|
|
recorded in the knowledge base; both presented as "the change did not apply" with no error.
|
|
|
|
## Dependencies between modules
|
|
|
|
Modules depend on each other, above all on the shared library they all build against. There is
|
|
**no workspace** ([ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md)): each module is a standalone
|
|
package consuming published dependencies, including the mesh's own.
|
|
|
|
The pipeline resolves modules into dependency **levels** and completes a level before starting
|
|
the next, so a module always builds against its dependencies as just published.
|
|
|
|
The cost is a publish-and-consume round trip for every cross-package change, and the absence of
|
|
any repository-wide build. One thing that assumed a repository-wide build has stayed broken
|
|
since (see
|
|
[`04-ISSUES/005`](../../04-ISSUES/005-pipeline-test-harness-unbuildable/00-report.md)).
|
|
|
|
## Persistent state
|
|
|
|
A module that owns state owns its migrations: numbered, written in the module's own language,
|
|
compiled with it, frozen once they have run anywhere, and idempotent so that re-running is safe
|
|
([ADR 0013](../../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md)).
|
|
|
|
Two kinds exist and the distinction matters: migrations against the module's **own** local
|
|
state, and migrations against a **provisioned** resource, which run on the node that consumes
|
|
the resource rather than on the node that built the module.
|
|
|
|
## A documented rule with no enforcement
|
|
|
|
Every module exposing capabilities is documented as required to declare the mesh's core runtime
|
|
as a dependency. **Zero of the catalogue's modules do.**
|
|
|
|
This is recorded here rather than quietly corrected, because it is the clearest instance of the
|
|
rule this repository states about itself: a rule whose enforcement does not exist is
|
|
indistinguishable from a wrong one, and costs more, because people believe it. Whether the rule
|
|
or the catalogue is wrong has not been decided.
|