A module is a repository and a path within it

The design said a module's manifest sits at a repository's root, full stop, which
means one repository per module. Nothing that exists is shaped that way: the
catalogue holds sixty-seven modules one to a directory, no code repository has a
manifest at its root, and the system being replaced has always built a module
from a repository and a path.

So the builder could be asked to build nothing that exists — pointed at the
catalogue it finds no manifest, pointed at a module's source it finds none
either. Recorded as a decision because it moves the core modules' manifests
beside their source, and corrects the design that said otherwise.

Also corrects, in the same document, how the three things the build loop cannot
produce actually arrive. They were written as though all three were carried in.
Only the control plane is: the registry is pulled from the public internet, and
the builder has no route at all — which is now stated as the open one rather than
implied to be solved.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-12 16:51:37 +02:00
parent 3d939b5c77
commit ddf104f8aa
3 changed files with 146 additions and 2 deletions
+55 -2
View File
@@ -2,12 +2,14 @@
layer: to-be
status: in-progress
code:
- mesh-catalog modules/builder
- mesh-control internal/builder
- mesh-control internal/catalogue/build.go
- mesh-control internal/inventory/secrets.go
- mesh-control cmd/mesh-builder
updated: 2026-08-31
updated: 2026-09-12
decisions:
- 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0010-delivery.md
- 02-DECISIONS/0005-the-node-host.md
@@ -39,13 +41,25 @@ resolution, and assigning it brings both. So the module count does not return, b
that made it return — *a module is expensive, so put several things in one* — is gone. A module
here is cheap: a manifest and, usually, nothing else.
## One file at the root
## One file, at the module's own path
`module.json`, and a convention somebody can look for beats a setting somebody has to find. It
says what the module is, what it provides and requires, what it claims, what capabilities it
needs, what it puts on a machine — and, if anything must be produced from the source, what to
build.
**A module is a repository and a path within it** ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)).
The manifest sits at that path, beside the source it describes, and everything it declares is
produced from there. A module whose path is the repository root is the ordinary case of this and
not a separate shape.
*Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which
made a repository hold exactly one module. Nothing that exists is shaped that way: the catalogue
holds sixty-seven modules in their own directories, the system being replaced has always built a
module from a repository and a path, and the root-only reading left every existing module
unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source
it finds no manifest either.*
## The manifest in the repository is not the manifest the mesh holds
A resource names an artifact:
@@ -345,3 +359,42 @@ first copy comes from outside, exactly once, and every copy after it is the mesh
machine across the private network, because a mesh-scoped provision that only answers locally is
not one. A container that is running is not a registry that replies, and this project has paid for
that distinction once already.*
## The three the loop cannot build, and there are only three
*2026-09-12. Written down because it keeps being rediscovered as if it were new, once per
component. It is one rule, it has three instances, and the list is closed.*
**Anything the build loop needs in order to run cannot be delivered by the build loop.** It arrives
from outside exactly once, and from then on it is an ordinary module, upgraded like one. What
"from outside" means is *not* the same for all three, and saying so matters, because two of them
have a route and one does not:
| What | Why it cannot come through the loop | How it arrives |
|---|---|---|
| The control plane | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)) |
| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) |
| The builder | It is what builds. Nothing builds it before it runs. | **Nothing yet.** Not carried, not public. See below. |
**The builder has no route, and this is the open one.** It is built from the control plane's
repository, so it cannot be pulled from the public internet like the registry; and the installer
carries one image only, the control plane's. So a mesh raised by the installer today has no builder
and no way to obtain one, which means it cannot build the catalogue, which means every module
waiting on a digest keeps waiting. Whatever answers this — the installer carrying a second image,
the control plane's own build producing both, or the first builder being fetched some other way —
is the last thing between a raised mesh and a self-upgrading one.
**There is no fourth.** Everything else the mesh runs is either upstream — a third-party image
pulled by digest — or built by the builder from a repository and published to the registry. So the
question "how does *this* one get here first?" has an answer for every module without asking it
again: if it is not one of the three above, it comes through the loop.
**A carried artifact is not a differently-pinned artifact.** Once published it is named by a digest
the mesh's registry assigned, exactly like everything the builder produces. A reader cannot tell
from a running mesh which of its images were carried, and that is the point: carrying is how the
first copy arrives, not what it permanently is.
*Checked by the thing already checked at genesis: after installing, the running control plane is
pinned to a digest the mesh's own registry assigned, and not to the id of the image the installer
carried. The same check applies to the builder and to the registry, and it is the same check —
an image id where a registry digest belongs means the pivot did not finish.*