The numbering is the flow: decisions are 02, design is 03
papa-hq reads 01 research -> 03 decision -> 02 design. The order is a scar, not a choice: 02-DESIGN existed from its initial commit, and when adr/ was finally promoted on 2026-07-13 it took the next free number rather than its place in the sequence. By then design was too settled to renumber. hal-hq was three commits old, so it is not. adr/ becomes 02-DECISIONS and 02-DESIGN becomes 03-DESIGN, and following the folder numbers now walks the process in the order it happens: research produces a decision, the decision authorises a design. 00-GENESIS becomes 00-META, matching papa's rename from the same restructure. Every path reference rewritten across documents, frontmatter, playbooks and skills. All links resolve; all 58 frontmatter blocks parse and their path fields still point at files that exist.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [hal]
|
||||
updated: 2026-08-23
|
||||
decisions:
|
||||
- 02-DECISIONS/0002-everything-is-a-module.md
|
||||
- 02-DECISIONS/0006-schema-changes-are-numbered-migrations.md
|
||||
- 02-DECISIONS/0007-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 0007](../../02-DECISIONS/0007-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 0006](../../02-DECISIONS/0006-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.
|
||||
Reference in New Issue
Block a user