A module is a repository and a path, the builder announces what it made, and the mesh acts on it #20

Merged
jschoubben merged 7 commits from feat/a-module-is-a-repository-and-a-path into main 2026-09-13 09:16:33 +00:00
Owner

The control plane's half of the self-upgrade loop.

The builder builds from a repository and a path, not from a repository alone — which was survivable only while every module was assumed to sit at a repository's root, an assumption that matched nothing that exists. Includes the migration that records the path.

It announces what it built on the events exchange: the module, the commit, the resolved manifest, everything it published, and every pinned artifact it was built on top of. That last one is what makes the graph's edges derived rather than declared. Its own account is scoped to send it.

The mesh acts on what the catalogue decided that build meant. Only the control plane knows which machines run the thing, so it is the one that acts — and what it does is a recorded choice rather than behaviour: upgrade <module> record | roll-out [--together], defaulting to record. Recording needed no new state, because a machine not running what the mesh would send it is already something the mesh reports.

It builds one module and stops, with no broker and no registry, which is how a mesh is raised. With nowhere to publish, the image stays in the runtime of the machine that will run it, named by the digest of its own configuration — the same identity the installer has always used for the image it carried.

Its own declaration now lives here, where a module's declaration belongs. It was in the catalogue, so what the control plane is and what it is made of sat in different repositories with nothing to notice them drifting.

And it says when a module is unbuilt rather than letting a machine call it malformed. A container naming an artifact is a module saying the mesh builds this; until a build publishes one there is nothing to run. What reached machines was an unresolved field their language has no room for, so they refused whole declarations and reported that a container does not use it — which reads as a broken manifest. It is not broken, it is unbuilt, and only the mesh can tell those apart. Found by the four-machine bed.

Verified in the lab on one machine and on four.

The control plane's half of the self-upgrade loop. **The builder builds from a repository and a path**, not from a repository alone — which was survivable only while every module was assumed to sit at a repository's root, an assumption that matched nothing that exists. Includes the migration that records the path. **It announces what it built** on the events exchange: the module, the commit, the resolved manifest, everything it published, and every pinned artifact it was built on top of. That last one is what makes the graph's edges derived rather than declared. Its own account is scoped to send it. **The mesh acts on what the catalogue decided that build meant.** Only the control plane knows which machines run the thing, so it is the one that acts — and what it does is a recorded choice rather than behaviour: `upgrade <module> record | roll-out [--together]`, defaulting to record. Recording needed no new state, because a machine not running what the mesh would send it is already something the mesh reports. **It builds one module and stops**, with no broker and no registry, which is how a mesh is raised. With nowhere to publish, the image stays in the runtime of the machine that will run it, named by the digest of its own configuration — the same identity the installer has always used for the image it carried. **Its own declaration now lives here**, where a module's declaration belongs. It was in the catalogue, so what the control plane *is* and what it is *made of* sat in different repositories with nothing to notice them drifting. **And it says when a module is unbuilt** rather than letting a machine call it malformed. A container naming an artifact is a module saying the mesh builds this; until a build publishes one there is nothing to run. What reached machines was an unresolved field their language has no room for, so they refused whole declarations and reported that a container does not use it — which reads as a broken manifest. It is not broken, it is unbuilt, and only the mesh can tell those apart. Found by the four-machine bed. Verified in the lab on one machine and on four.
jschoubben added 7 commits 2026-09-13 09:16:26 +00:00
Answering and announcing are different acts. The reply goes to whoever asked and
is correlated to their request; the announcement says to the whole mesh that a
module now exists at a commit, which is what the catalogue places in the module
graph (novox/hq ADR 0072). A build nobody asked for still has to be announced, or
the graph knows less than the registry does.

What it was built on top of is read out of the build's own inputs rather than
declared, because a declared list drifts from what the code actually uses
(ADR 0009). These are artifact references, which is what a build input names;
resolving them to module-versions is the catalogue's work, since it is what knows
which module-version published which artifact.

Events ride the topic exchange, not the direct one nodes speak over, so the
builder's account is granted both: it must be able to answer and to announce.
The envelope is the sdk's, reproduced exactly — a second shape would be a second
thing for consumers to handle, and they are written against the first.

Announcing is not allowed to fail a build. The work was done and was answered; a
build reported as failed because saying so failed is a lie about it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
An event whose origin reads a container id names something no other module
can look up. The mesh already knows the answer, and a module's environment
file is a file resource, so ${machine:name} reaches it with no composer change.
The builder says what it built and the catalogue decides whether that was an
upgrade. Only the control plane knows which machines run the thing, so it is
the one that acts — and what it does is a choice somebody recorded, not a
behaviour compiled in: record that they are behind, or send it, one machine at
a time or together.

Recording is the absence of an action rather than a second path: a machine not
running what the mesh would send it is already something the mesh reports.

Defaulted to recording. A mesh that rolls out everything it builds the moment
it builds it is reasonable to want and a bad thing to arrive by default — the
first module to inherit it would be the control plane, upgrading itself out
from under the push applying it.
This is how a mesh is raised: the installer carries this program and runs it
once, before anything exists, to produce the control plane from the same
repository and path every later rebuild will use. What raises the mesh is then
the same thing that maintains it, rather than a second mechanism exercised once
per new mesh — which is how often enough to rot.

With nowhere to publish, an image stays in the machine's own runtime and is
named by the digest of its own configuration: the same identity the installer
has always used for the image it carried.
A module is a repository and a path within it, and the manifest sits at that
path. This one sat in the catalogue instead, so the thing that says what the
control plane is and the thing it is made of lived in different repositories
and could drift apart with nothing to notice.

It also names an artifact it builds rather than a placeholder digest somebody
fills in, which is what lets the mesh build its own control plane.
A container naming an artifact is a module saying the mesh builds this. Until a
build publishes one there is nothing to run — and what reached the machine was
an unresolved field, which its language has no room for, so it refused the whole
declaration and reported that a container does not use "artifact". That reads
as a broken manifest. It is not broken, it is unbuilt, and only the mesh can
tell those apart.

Found by the four-machine bed, which assigns modules the mesh has not built.
jschoubben merged commit cb5108a864 into main 2026-09-13 09:16:33 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-controller#20