The mesh could already say which modules a base change invalidated, and could not do anything about it: each recipe named one particular copy of the base by fingerprint, and rebuilding produced the old one. Worse, the copy each named existed only inside a throwaway lab, so those three modules could not be built anywhere at all — and the line each replaced had the same fault.
130 lines
7.7 KiB
Markdown
130 lines
7.7 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-13
|
|
located-in: [mesh-tools, mesh-sdk]
|
|
fixed-by: mesh-tools feat/run-once-entry, mesh-sdk feat/adr-ref-resync
|
|
amended-design:
|
|
---
|
|
|
|
# 044 — The runtime every module builds on cannot be built by the mesh
|
|
|
|
## Symptom
|
|
|
|
Every module written in the mesh's own toolchain is compiled on top of one shared base image — the
|
|
tool runtime. The mesh can build the modules. It cannot build the base.
|
|
|
|
Two things stop it, and each is enough on its own:
|
|
|
|
- The base's container definition copies a compiled output directory that is not in the repository.
|
|
A clean clone has the sources and no compiled output, so the definition refers to something that
|
|
is not there.
|
|
- The base's dependency on the mesh's own software development kit resolves to a **sibling checkout
|
|
on disk**, not to anything a clone can fetch:
|
|
|
|
```
|
|
"node_modules/@novox/mesh-sdk" -> { "resolved": "../mesh-sdk", "link": true }
|
|
```
|
|
|
|
Both mean the same thing: the base can only be produced on a workstation that happens to have the
|
|
neighbouring repositories laid out beside it, and has run a compile step by hand first.
|
|
|
|
Observed while building four modules through the mesh from a repository and a path. All four
|
|
succeeded, and all four recorded that they were built on top of the same base — an image reference
|
|
that no module in the mesh produced, because no module could have.
|
|
|
|
## Why this matters
|
|
|
|
**This is the one artifact the whole build chain rests on, and it is the one artifact outside the
|
|
chain.** Three separate consequences, all visible today:
|
|
|
|
- *The base is pinned by hand.* Every module's container definition names it as a literal digest
|
|
written by a person. Nothing checks that digest against anything.
|
|
- *Nothing can tell when it moves.* A build records what it was built on top of, so an artifact is
|
|
stale when anything beneath it moved. That rule cannot fire for the base: the graph has edges
|
|
pointing at it, and no version on the other end, because no build ever registered one. The mesh
|
|
is therefore unable to answer "what must be rebuilt" for the change that would reach *every*
|
|
module at once.
|
|
- *Nobody can check what is in it.* The published base and the definition in the repository have
|
|
already drifted: the definition names one operating system base, and the image actually serving
|
|
every module is built on a different one. This was found by running the image, not by reading
|
|
anything, and it went unnoticed for exactly as long as nobody could rebuild it.
|
|
|
|
A rule the design states — an artifact is stale when anything it was built against moved — is
|
|
unenforceable for the artifact it matters most for. That is the shape of a wrong rule, not a
|
|
missing feature: everything downstream reports "nothing to rebuild" and is believed.
|
|
|
|
## How it would be checked
|
|
|
|
The check is the fix's own test: clone the repository alone, into an empty directory, and build it.
|
|
Nothing else present, no neighbouring checkouts, no compile run first. If that produces the base,
|
|
the mesh can produce it too, because that is all the mesh does.
|
|
|
|
## Open questions
|
|
|
|
- Where should the software development kit come from, for a build that has only one repository?
|
|
The mesh already knows how to run a package registry as a module, and it already publishes
|
|
container images to one of its own — so this may be the same answer twice rather than a new one.
|
|
- Is the base a module like any other, or is it one of the few things a new mesh must arrive
|
|
carrying? It behaves like both: everything is built on top of it, and it is built out of the same
|
|
sources as everything else.
|
|
- If it becomes an ordinary module, is there a circularity? **No** — checked rather than assumed:
|
|
the builder is a compiled program built from its own sources and a language toolchain, and does
|
|
not stand on this base at all. Only modules written in the mesh's scripted toolchain do. So the
|
|
thing that would build the base is not one of the things built on it, and the chain has an end.
|
|
|
|
## Resolved, 2026-09-13
|
|
|
|
The base is a module now, built from its own repository and nothing else.
|
|
|
|
Three things were in the way, and the third was not visible until the first two were gone:
|
|
|
|
- **The toolkit could not be fetched.** It is named by a pinned commit of its own repository rather
|
|
than by a folder on somebody's disk. Both repositories turned out to be readable without a
|
|
credential, so no package registry was needed after all — which is why this was smaller than it
|
|
looked.
|
|
- **The toolkit shipped no compiled output.** Every one of its entry points pointed into a directory
|
|
that is not in source control. It declares the hook that is meant to compile it on install, and
|
|
the package manager in use does not run that hook — so the build compiles it explicitly instead.
|
|
Relying on a hook firing would have failed the same invisible way this issue is about.
|
|
- **The base is also the compile environment.** Every module's recipe starts from this image and
|
|
invokes the compiler out of it. A first attempt dropped the build tools to make a smaller image,
|
|
which broke every module built on it. They stay, and the recipe now says why.
|
|
|
|
The drift this issue reported is also closed: the recipe claimed one operating system while serving
|
|
another, and now states what is actually in service. Changing that was deliberately *not* folded in
|
|
— a module already builds against it with that system's package manager, and moving the ground
|
|
under every module at the same moment as making it buildable would be two changes wearing one
|
|
commit.
|
|
|
|
**Checked the way the report said it should be.** The repository was cloned alone, with nothing
|
|
beside it, and built by the mesh. Then every module was moved onto the result, and the base changed
|
|
for real: all three went stale, each naming the base and the commits it moved between, each listed
|
|
once. Changing only a comment in the base — a new commit, a byte-identical image — correctly makes
|
|
nothing stale, which is a second bug this exercise found and fixed: staleness was comparing commits
|
|
when it should compare artifacts, so the mesh would have rebuilt itself entirely to arrive back
|
|
exactly where it started.
|
|
|
|
## And then the other half, 2026-09-13
|
|
|
|
The paragraph that stood here said a module still named its base as a literal fingerprint in its own
|
|
recipe — so the mesh could *say* which modules a base change had invalidated, and rebuilding them
|
|
produced the old base anyway until somebody edited that line by hand.
|
|
|
|
Worse than inconvenient, as it turned out. **A fingerprint names one particular copy of an image,
|
|
and a copy exists on one mesh.** The three modules in the catalogue named a copy produced inside a
|
|
throwaway lab, so none of them could be built anywhere else at all; and the line each of them
|
|
replaced named a copy from an even earlier throwaway lab. Nobody noticed because the only place they
|
|
had ever been built was the place the copy existed.
|
|
|
|
**A module names the module now, and the mesh answers with the copy it holds.** The declaration says
|
|
which module, which artifact, and the build argument the recipe reads it from. The request carries
|
|
what this mesh has built, so the builder stays a thing that clones, builds and answers rather than
|
|
something that asks the mesh questions — the answer travels with the question, because only the mesh
|
|
knows what it has. A base the mesh has not built is refused before anything is built, naming which
|
|
module has to exist first.
|
|
|
|
Checked on a bare machine: postgres with nothing supplied is refused and says to build mesh-tools
|
|
first; mesh-tools is built; postgres is built against it and the image carries the compiled module,
|
|
the toolkit and the database client it provisions through. The recipes carry no default at all, so a
|
|
build nobody told stops at the declaration rather than at a reference that resolves to nothing.
|