diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md b/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md new file mode 100644 index 00000000..ee180334 --- /dev/null +++ b/01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md @@ -0,0 +1,85 @@ +--- +status: active +initiated: 2026-10-08 +touches: + - 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md + - 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md + - 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md + - the artifact store, the catalogue's modules that pin an image by digest +--- + +# 035 — The images from before the mesh + +## What is investigated + +The artifact store's tools ([ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)) +counted 469 unrecorded manifests marking 23.8 GiB on 2026-10-08. 374 of them are copied bases +([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md), +[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)). +Most of the rest are images pushed to the same store before the mesh existed. The predecessor's +pipeline built them, under a namespace per owner, one repository per app. + +No record of the mesh names these images, so [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) +§3 keeps every tool away from them. That is correct: the mesh did not make them. It also means they stay +until a person decides. This effort finds out, for each one, whether anything still uses it, and +recommends what to do. Nothing was deleted. + +## How it was measured + +All of this was read, nothing was written. On 2026-10-08: + +- **The store's view.** `artifact_store_references` and `artifact_store_repositories` give each + repository's manifests, tags and sizes. They give no dates. +- **What each machine declares.** The controller's `images` verb answers for each of the four machines. +- **What each machine runs.** The container runtime module's container list answers on each machine. +- **What still names them.** I searched every repository on the forge for each image's path and digest. + There are 50 repositories, and I read 31 of them closely: both catalogues, the apps' own repositories, + the setup repositories and the predecessor's. +- **Dates.** No tool gives a manifest's push time. A date below is either an image's `created` date, from + a copy still in a machine's local image store and matched by tag, or the date of the commit the tag + names. Where neither exists, the date is not known, and the table says so. + +## What was found + +[01-the-images.md](01-the-images.md) has the table. In summary: + +- **3 repositories hold an image a machine runs today.** Each is one of three app images on the anchor: + an invoicing app's API, its web front, and a static company website. A catalogue module pins each by + digest, under an older name of the same store. Each repository also holds two older tags that + nothing uses. +- **2 repositories are pinned only by catalogue modules that no machine is assigned.** One is a + magazine site's older image. The other is an older photo client, pinned by two per-person photo + modules. Both are stale definitions. +- **35 repositories, plus 2 single manifests in repositories the mesh builds, are used by nothing.** + Nothing runs them, declares them or pins them. + - Most were superseded by a module that builds its own image of the same app. + - The rest are abandoned: a trading bot, a chatbot, test images, the predecessor's dashboard and flow + engine, and the bootstrap's first builder and controller images. + - Together they mark between 5.9 and 7.5 GiB that nothing else marks. The two bounds depend on how the + blobs they share among themselves are counted. + +**A finding beside the question.** The store's tools classify the three running images as +*unrecorded*, the same word they use for an image nothing uses. The reason is that the catalogue pins +them under an older name of the store, and the mesh did not make them. Nothing removes an unrecorded +manifest today, so nothing is at risk. But any later decision to let unrecorded images go by some +other rule would take running images with it. That is recorded as +[issue 323](../../04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md). + +## What is recommended + +- **Keep** the three running images. Point their modules at an image the mesh builds, where one exists, + so the running images become the mesh's own. Two of the three apps already have a build of their own + on a branch. +- **Delete the two stale catalogue modules**, then treat their images like the rest. +- **Let go of the 35 repositories and 2 manifests** once a decision says how an image the mesh did not + make may be removed. ADR 0189 §3 forbids every tool from doing it today, and that is intended. The + decision is the operator's: the images are the operator's, and some can no longer be rebuilt. +- **Adopt none of them as a module.** Every app in use already has a module. + +## Open questions + +- Should an image the mesh did not make be removed through the controller's records, by a person + recording it (as ADR 0257 lets a person record a copied base), or through a separate verb of the + store's tools? +- Should the older tags in a repository that is in use (about 0.8 GB together) go before their modules + are moved to the mesh's own build, or with it? diff --git a/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md b/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md new file mode 100644 index 00000000..257a4e71 --- /dev/null +++ b/01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md @@ -0,0 +1,64 @@ +# 035 — The images, one by one + +Read on 2026-10-08. Sizes are the bytes a repository's manifests mark. *Only theirs* is what no other +manifest in the store marks, which is what removing the repository would free. A machine is named by its +role. + +Dates: *created* is an image's build date, from a copy in a machine's local image store matched by tag; +*commit* is the date of the commit the tag names; *not known* means neither was found. + +## In use + +| what | size | manifests | newest | in use by | named by | recommendation | +|---|---|---|---|---|---|---| +| an invoicing app's API | 1.41 GB | 3 tags | created 2023-01 | the anchor, its API container (newest tag, matched by digest) | its catalogue module, pinned under an older name of the store; also the frozen runtime base of the app's own build on a branch | keep the newest; let the two older go (about 0.7 GB); move the module to the mesh's own build | +| the same app's web front | 60.5 MB | 3 tags | created 2023-01 | the anchor, its web container (matched by digest) | its catalogue module, likewise | keep the newest; let the two older go | +| a static company website | 474 MB | 3 tags | created 2022-08 | the anchor, the website container (matched by digest) | its catalogue module, likewise; the site's repository has its own build on a branch, not yet delivered | keep the newest; let the two older go (about 75 MB) | + +## Pinned only by a stale module + +| what | size | manifests | newest | in use by | named by | recommendation | +|---|---|---|---|---|---|---| +| a magazine site, its older image | 393 MB | 3 tags | created 2025-10 | nothing: the anchor runs the mesh-built image | a catalogue module whose definition no machine uses | let the two older go; the newest once the stale module is deleted | +| a photo app's older client | 11 MB | 1 | created 2024-10 | nothing: the anchor runs the mesh-built client | two per-person photo modules, assigned to no machine | delete the two modules, then let it go | + +## Used by nothing + +Each row is one repository, unless it says otherwise. + +| what | size (only theirs) | manifests | newest | superseded by, or why abandoned | recommendation | +|---|---|---|---|---|---| +| a car-listing scraper | 2.31 GB (1.43 GB) | 3 | commit 2026-02 | the module's own build, which the home server runs | let go | +| a chatbot's API | 3.04 GB | 1 | before 2024-09 | abandoned since 2024 | let go | +| a chatbot's web front | 19.8 MB | 1 | before 2024-09 | abandoned | let go | +| a comic app's API and web front (2 repositories) | 828 MB | 1 each | created 2026-09 | the predecessor's; no module | let go, unless the app is wanted back (then build it as a module) | +| a photo app's earlier API and web front (2 repositories) | 519 MB | 4 each | not known | the photo modules | let go | +| a photo app's server and admin client (2 repositories) | 445 MB | 1 each | created 2024-10 | the photo modules' own builds | let go | +| a map game's API and web front (2 repositories) | 121 MB | 3 each | commit 2026-02 | renamed to the travel game below | let go | +| a travel game's API and web front (2 repositories) | 104 MB | 2 each | commit 2026-02 | no containers or data remain | let go (it can be rebuilt from its repository) | +| a racing-circuit site | 264 MB | 3 | created 2026-06 | the module's own build, which the home server runs | let go | +| an older company website | 400 MB | 1 | not known | the static website above, and its own build | let go | +| a payment-link site | 52.6 MB | 3 | before 2025-11 | the module's own build | let go | +| a mail-forwarding service | 58.5 MB | 1 | created 2026-03 | its repository's own module, assigned nowhere | let go | +| a snake game and a text game (2 repositories) | 88 MB (0.8 MB) | 1 each | created 2026-08 and 2026-09 | the modules' own builds | let go | +| a trading bot | 85 MB | 3 | commit 2022-08 | abandoned | let go | +| a trading bot's scheduled jobs and web front (2 repositories) | 83 MB | 1 each | not known | abandoned (setup last changed 2025-01) | let go | +| three base images of a former owner namespace (3 repositories) | 188 MB | 1 each | not known | abandoned (setup last changed 2023-05) | let go | +| test images (3 repositories) and a capability test | 135 MB | 1 to 3 | not known | tests of the predecessor | let go | +| the predecessor's dashboard and flow-engine API (2 repositories) | 165 MB (157 MB) | 2 and 3 | created 2026-09 | the predecessor is retired | let go | +| the bootstrap's first builder and controller images (2 repositories) | 30 MB (26 MB) | 1 each | created 2026-09 | the build agent; the controller runs as a bundle | let go | +| a build agent's and the controller's former server images (2 repositories) | 62 MB (55 MB) | 2 each | created 2026-09 | the same | let go | +| one manifest in the mesh-built tools image's repository | 87 MB (6.6 MB) | 1 of 4 | not known | the three recorded beside it | let go | +| one manifest in the route proxy's server repository | 9.9 MB (6.0 MB) | 1 of 6 | not known | the kept build the machines declare | let go | + +**Together**, the rows used by nothing mark between 5.9 GiB and 7.5 GiB that nothing outside them marks. +The upper bound sums each repository's own bytes. That counts twice a blob two of them share. The lower +bound subtracts every shared blob, and that also subtracts blobs shared only among these rows, which +removing all of them would free. Removing the two stale modules' images adds 0.4 GiB. Removing the older +tags of the three images in use adds about 0.8 GB. + +**Not determined.** + +- A push date for 14 of the repositories. +- Whether the forge listing of 50 repositories is all of them. A second page came back empty, so I think + it is. diff --git a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md index 2237aeac..066a53fc 100644 --- a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md +++ b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md @@ -36,6 +36,13 @@ image the manifest did not declare is refused before the build, naming the image its own stages, declared arguments and `scratch` are not fetches. An unpinned vendor image is refused: a tag is what somebody else can move. +> **The mechanism changed — 2026-10-08, by [ADR 0257](0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md).** +> A declared image is still copied into the mesh's registry before the build, and the recipe is still handed +> the copy. What moved is where the copy goes. It no longer goes "under the module's repository" +> (`/on-`). It goes into one repository per upstream image, `upstream//`, +> whichever modules stand on it. Each copy is also recorded where it is made, so the store's sweep can +> let it go. + A recipe whose `FROM` names an undeclared base was at first said, not refused: the mesh's own images — the control plane's, the tool runtime's, the route proxy's — started from a public base and declared none, and refusing those refuses genesis. *Amended the same day:* those three declare diff --git a/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md new file mode 100644 index 00000000..f11f461a --- /dev/null +++ b/02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md @@ -0,0 +1,212 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-08 +deciders: jochen +reconstructed: false +supersedes-in-part: + - 0097-a-vendor-image-is-a-declared-build-input.md +extends: 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md +--- + +# 257. A copied base is recorded where it is copied, kept by the builds that stood on it, and held once per image + +## Context + +A module's build may stand on an image published elsewhere, declared in its manifest and pinned by digest +([ADR 0097](0097-a-vendor-image-is-a-declared-build-input.md)). Before the build, the builder copies that +image into the artifact store, whole, between registries +([ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)), under the module's own repository, +`/on-`. This record calls such a copy a **copied base**. + +[ADR 0251](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +gave the store tools that classify every manifest against the controller's records. Their first reading, +on 2026-10-08, found 469 manifests marking 23.8 GiB that no record names, so nothing would ever remove +them ([ADR 0189](0189-the-store-keeps-what-the-records-name.md) §3). Issue +[321](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md) +has the details. 374 of those manifests, in 27 repositories, are copied bases. Four facts explain them: + +1. **The copy is recorded as something the build stood on, never as something it made.** The records + name what each build *made*, and the sweep reads only that. A build's `built_against` does name the + copy, but the sweep does not read it. +2. **A build that fails after copying records the copy nowhere.** A failed build keeps no module and no + `built_against`, yet the copy is in the store from the moment it is made. +3. **An upstream image is an index over several platforms.** Each platform manifest is put under its own + digest, so each is a manifest of the repository in its own right. The store's collector keeps every + manifest a repository holds. Letting the index go therefore frees none of the bytes of the images it + names. This is true of every upstream artifact the mesh has ever let go of, not only of copied bases. +4. **There is one copy per module, not one per image.** Several modules standing on one image hold + several copies. Their bytes are stored once, because the store keeps each blob once whichever + repositories link it, so the copies cost little disk. But each module's first copy asks the public + registry for the whole image again, and that registry rations anonymous pulls. + +The largest single case is a route proxy's Go toolchain image: 34 manifests and 10.4 GiB in one +repository. That is two versions of the toolchain image, each with every platform it is published for, +Windows included. + +## Considered Options + +**What records a copy.** + +1. *Read `built_against` as the record.* Rejected as the only record, because it misses fact 2: a + failed build has none. It is kept as the source of the backfill (point 2 below). +2. **A record of its own, written when the build reports, whether the build worked or not.** Chosen. + The builder says what it copied in every outcome it announces. The controller keeps one row per copy. + +**What keeps a copy.** + +1. *A definition naming it, as for an artifact.* Rejected. A definition names the image upstream, by the + digest that every copy of that image shares. As a reason it would keep every module's copy of an image + for as long as any module stood on it. And no machine is ever handed a base, so a definition is never + the reason a base is needed. +2. *Its own five most recent copies.* Rejected: a copy has no builds of its own to count. +3. **The kept builds that stood on it.** Chosen. A copy is kept while one of the five most recent + successful builds of a module the mesh holds stood on it. That is the build ADR 0189 already keeps for + a release to go back to, and it is the one a rebuild would need the copy for. + +**Where a copy lives.** + +1. *Under the module, as now.* Rejected for fact 4. +2. **One repository per upstream image, named for the image: `upstream//`.** Chosen. The + first module to stand on an image copies it, and every later module finds it already held. Two images + with one path on different hosts are not confused. On the move, a copy the module's own repository + already holds is taken from there: its manifests are read from the store and its blobs are mounted + across. Upstream is not asked. + +**What becomes of an index's platforms.** + +1. *Let the index go alone.* Rejected for fact 3. +2. *Let the index go with every platform manifest it names.* Rejected, because a kept index of the same + repository may name one of them. +3. **Let the index go with the platform manifests it names that no kept index of the repository also + names, platforms first.** Chosen. The controller reads only its records, so it cannot see an + unrecorded index that also names a platform. The store tools can see that, and their dry run lists + every manifest that stays and would lose a platform, before anybody runs collection for real. + +**The copies no record names**, made before this record by a failed build, or before builds kept what +they stood on. + +1. *Remove every manifest in a copy's repository by name.* Rejected: ADR 0189 §3 keeps removal to what a + record names, and a repository's name is not a record. +2. *Leave them for ever.* Rejected: they are most of what the store carries for no reason. +3. **A person records them, and the sweep then decides as for every other copy.** Chosen. The store + tools list each unrecorded copy at the top of a copy's repository, with what it alone marks. A new + controller verb, `mirrors`, records the ones a person names, with a reason. It refuses any reference + outside a repository only the copy step writes, and any reference the store does not hold. Recording + deletes nothing. + +**Copying only the platforms the mesh runs** was weighed and is left for a later record. It would remove +most of the route proxy's toolchain copy, which is Windows images, but the reference a build is handed +would then no longer be the upstream digest. Whether the store accepts an index whose platforms are +partly absent depends on its version and settings, and that has not been measured. + +## Decision + +**1. One repository per upstream image.** A copied base is put in `upstream//` (the public +hub is `docker.io`, and a port's colon becomes a dash). The module's own repository from before is +offered as a source: where it holds the image by digest, the copy is made from it, and nothing is asked +of upstream. A module's build arguments name the new copy. Its source fingerprint is unchanged, because +the fingerprint reads a base by digest, never by where it was copied to. + +**2. A copy is recorded where it is copied.** The builder says, in every build outcome, what it copied. +It says so when the build fails too. The controller keeps one row per copy in `artifact_mirrored`: the +build that first said it, or a person's reason. A migration records every copy that the existing build +records already name in a module's `/on-` repository, each by the earliest build that +named it. Only the copy step ever wrote a repository so named. + +**3. A copy is kept by the kept builds that stood on it, and by nothing else.** A copy is kept, with the +reason `stood-on`, while one of the five most recent successful builds of a module the mesh holds stood +on it. Otherwise it is eligible, and the sweep lets it go as it lets any eligible artifact go. A +definition never keeps a copy. A copy already let go of stays let go of. The reason `stood-on` belongs to +copies alone: another module's artifact is still kept by that module's own builds. + +**4. An index goes with its platforms.** When the sweep lets an index go, it first deletes the platform +manifests the index names that no kept index of the same repository names, and deletes the index last. A +sweep cut short leaves the index to be asked about again. If the sweep cannot read which platforms the +kept indexes name, it lets nothing go. This applies to every index the mesh let go of, upstream artifacts +included. + +**5. A person may record a copy that no record names.** The controller's `mirrors` verb lists the copies +the records hold, with their states. Given references, it records the ones that are in a repository only +the copy step writes and that the store holds. It is a dry run without `confirm`. With `confirm` it needs +a `why`, and it is recorded as a hand act. It deletes nothing. `artifact_store_references` lists the +candidates in `unrecorded_mirrors`, largest first. + +**6. The store tools count a platform as its index's.** A platform manifest takes the state of the +recorded index of its repository that names it, the more keeping state when two do. An index no record +names leaves its platforms unrecorded with it. `artifact_store_collect` counts the platforms that go with +an index, and its dry run lists, in `platforms_named_by_what_stays`, every manifest that stays and names a +platform that would go. + +ADR 0097 is superseded in one sentence only: a base is copied into the image's own repository, no longer +"under the module's repository". Everything else ADR 0097 decided stands. + +## Consequences + +- The records answer for every copy a build makes from now on, and the sweep keeps or lets go of copies + by the same rule as the artifacts. +- **Recording the copies frees little by itself.** Measured on 2026-10-08 against the records: + - 17 of the 23 recorded copies were stood on by a kept build. + - The 6 that were not hold about 0.4 GiB that only they mark. + - Most copies share their bytes with a kept copy of the same image, so letting them go frees their + manifests, not their bytes. + - Most of the 23.8 GiB is copies no record names, and pre-mesh images (research + [035](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). Those wait for a person, + through `mirrors` and, for the pre-mesh images, a decision of its own. +- The move to one repository per image costs one copy per image, taken from the store itself. After it, + each module's former copies stop being stood on, become eligible after five more builds of that + module, and go, freeing their manifests. +- A module named `upstream` would collide with the copies' namespace. The build edges already ignore + the copies' repositories, so no edge is drawn to such a module. Refusing the name is left to the + catalogue's module check, and it is not done here. +- The kept copy of the route proxy's toolchain still holds its Windows platforms. I estimate them at + about 4.9 GB, from the two platform manifests of each version that are larger than 2 GB; which index + names which platform was not read. Removing them is for the later record on copying only the platforms + the mesh runs. + +## How it is checked + +- **The builder.** + - The repository is named for the image: the hub, other hosts, a port, upper case. + - A copy held under the module's former repository is mounted, every blob, with nothing uploaded + again and nothing asked of an upstream that is gone. + - A second module standing on the image copies nothing. + - A failed build and a working build both say what they copied. +- **The records.** + - A copy only a failed build made is eligible. + - A copy is kept, with `stood-on`, by the five kept builds and by nothing else, and a definition + naming the image keeps no copy. + - Another module's artifact is not kept as a copy. + - A collected copy stays collected. + - A person's record stands over a later build's. + - The migration's backfill records one copy, by its earliest build, and records nothing outside an + `on-` repository. + - A copy's repository draws no build edge. +- **The sweep.** + - Against a fake store, an index goes with its own platforms, platforms first. + - A platform a kept index names stays. + - An image is one delete. + - A store that cannot say what a kept index names keeps the index. +- **The `mirrors` verb.** + - It is composed only with what it takes. + - It takes only references in a copy's repository. + - A dry run records nothing. + - A real run records what the store holds, and the sweep then offers it. + - It never asks the store anything but HEAD. +- **The store tools.** Over a view with two indexes of one image and an unrecorded copy: + - A platform takes its index's state, the more keeping state when two indexes name it. + - The unrecorded copy's top is offered with what it alone marks. + - Letting the old index go takes only its own platform. + - Sparing nothing would be said as a kept index losing a platform. +- **Live.** After rollout, `mesh-controller.collect` (dry run), `mesh-controller.mirrors` and + `artifact_store_references` are read. Their numbers are recorded in to-be 51. + +## References + +- [ADR 0096 — an upstream image is copied between registries](0096-an-upstream-image-is-copied-between-registries.md) +- [ADR 0097 — a vendor image is a declared build input](0097-a-vendor-image-is-a-declared-build-input.md) +- [ADR 0189 — the store keeps what the records name](0189-the-store-keeps-what-the-records-name.md) +- [ADR 0251 — the registries say what they hold and keep what is named](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +- [issue 321 — a copied base was recorded by nothing, and a let-go index kept its platforms](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md) +- [research 035 — the images from before the mesh](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md) +- [to-be 51 — the registries keep what is named](../03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md) diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index 86518bf0..66a90111 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: - - mesh-controller cmd/mesh-controller (the verbs artifacts, collect and images) + - mesh-controller cmd/mesh-controller (the verbs artifacts, collect, images and mirrors), internal/builder (copied bases), internal/inventory (artifact_mirrored) - mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed) - mesh-catalog modules/gitea (package retention) - mesh-catalog modules/docker (image pruning) @@ -10,6 +10,7 @@ updated: 2026-10-08 decisions: - 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md - 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md + - 02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md --- # 51 — The registries keep what is named @@ -80,6 +81,7 @@ the same mark, so their numbers are the collector's. |---|---|---| | kept — definition | a module's recorded definition names it | no | | kept — recent build | one of the five most recent builds of a module the mesh holds | no | +| kept — stood on | a copied base one of those builds stood on (ADR 0257) | no | | holder of a kept archive | the manifest that keeps a kept archive's blob | no | | eligible | the mesh made it and keeps it for no reason | yes, through the controller | | let go of, yet present | the controller recorded letting it go, and the store still holds it | no — said, for a person to look at | @@ -90,7 +92,32 @@ the same mark, so their numbers are the collector's. A holder is recognised by its shape: a manifest with an empty configuration and one layer, which is the blob of an archive the records name. -### The controller's two verbs for the store +**A platform manifest is its index's** ([ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)). +An upstream image is an index over several platforms, and each platform manifest is a manifest of the +repository in its own right. A platform manifest takes the state of the recorded index of its +repository that names it, and the more keeping state when two indexes name it. An index that no record +names leaves its platforms unrecorded with it. + +### Copied bases + +A build that stands on an image published elsewhere copies it into the store first. Since +[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md): + +- **One repository per upstream image**, `upstream//`, whichever modules stand on it. A copy + that a module's former repository, `/on-`, already holds is copied from there, with + each blob mounted across, and upstream is not asked. +- **Recorded where it is copied.** The builder says what it copied in every build outcome, including a + failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing + build records already named were recorded by the migration that made the table. +- **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never + keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. +- **An index goes with its platforms.** The sweep deletes the platform manifests an index names that + no kept index of its repository also names, and deletes the index last. +- **A copy no record names** waits for a person. `artifact_store_references` lists each one in + `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a + person names, and the sweep then decides about them. + +### The controller's verbs for the store - **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and, for kept ones, the reason; collected ones only when `collected` is asked for; narrowed to one @@ -98,7 +125,15 @@ the blob of an archive the records name. - **`collect`** says what the sweep would let go of. With `confirm` and a `why` it runs the sweep that runs after a build: every kept archive held first, then each eligible reference let go and recorded as collected, stopping at the first refusal by the store. It is bounded by a count and a time, says - what is left, and is a hand-act recorded with its `why`. + what is left, and is a hand-act recorded with its `why`. An index is let go of with its own platform + manifests (ADR 0257). +- **`mirrors`** lists the copied bases the records hold, with their states. Given references, it records + the copies that are in a repository only the copy step writes and that the store holds. It is a dry + run without `confirm`; with `confirm` it needs a `why`, and it is a hand act. It deletes nothing. + +`artifact_store_collect` counts the platforms that go with an index, and its dry run lists, in +`platforms_named_by_what_stays`, every manifest that stays and names a platform that would go. The +controller cannot see such a manifest, because it reads only its records. ## The package registry @@ -118,6 +153,13 @@ repository on the forge. Ranges are read as npm reads them: exact, `^`, `~`, `x` and `*`, comparisons, hyphen ranges and `||`. A range that cannot be read is named, and its package keeps every version until it can. +Lockfiles are read in all three of their versions: version 1's nested `dependencies`, and the `packages` +of versions 2 and 3, where an entry's own `dependencies` is a map of names to ranges. A file that could +not be read whole is named, and like an unread repository it stops a real run before anything is deleted +([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)). +Every version is measured from the forge's file sizes, and a size that could not be read is said, never +counted as zero. + ## A machine's images The container runtime module's tool `docker_prune_images` works on the machine it runs on. @@ -150,3 +192,12 @@ removed and what the runtime refused. *In progress, 2026-10-08.* The decision is recorded; the code is being built in the controller and in the store, forge and container runtime modules. The live dry runs are recorded here once the tools run. + +The first readings, 2026-10-08, before ADR 0257 was rolled out: + +- **The store** held 25.7 GiB. 469 manifests marking 23.8 GiB were unrecorded. Of those, 374 manifests in + 27 repositories were copied bases ([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md)), + and most of the rest were images pushed before the mesh existed + ([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). +- **The package registry** held one package with four versions, all kept. Every lockfile on the forge + went unread, and every version read zero bytes ([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)). diff --git a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md new file mode 100644 index 00000000..7a64a20e --- /dev/null +++ b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md @@ -0,0 +1,36 @@ +--- +status: located +opened: 2026-10-08 +located-in: [mesh-controller internal/builder (standingOn, the copy of a base), internal/inventory (the keep set), internal/artifacts (LetGo), cmd/mesh-controller (the sweep); mesh-catalog modules/artifact-store-tools (classification)] +fixed-by: +amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +--- + +# 321. A copied base was recorded by nothing, and a let-go index kept its platforms + +## Symptom + +On 2026-10-08 at 13:35, `artifact_store_references` (the store's tools of +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)) +read the artifact store at 25.7 GiB. 469 manifests marking 23.8 GiB were **unrecorded**: no controller +record names them, so no sweep will ever let them go +([ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) §3). + +374 of those manifests are in 27 repositories named `/on-`. Only the builder writes +such a name: before a build, it copies there the image published elsewhere that the module's build +stands on ([ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md)). The largest +is a route proxy's Go toolchain copy: 34 manifests, 10.4 GiB, all unrecorded. + +The same reading shows a second case. The repository of the route proxy's upstream artifact (a mirrored +`upstream` image, kind `upstream`) has one kept manifest of 23 KiB and 16 unrecorded manifests marking +30 MB. + +## Why it is a design issue + +ADR 0189 rests on one sentence: the mesh "has never put anything there it did not record". For copied +bases that sentence is false, and nothing said so until the store's tools counted it. Each copy is a +thing the mesh put in its store, and the records do not show it as made. So the sweep can never let a +copy go, however old it is, and the store only grows. The second case is the same promise broken +another way: letting an index go was recorded as collecting it, and the store kept every byte it named. + +The diagnosis is in [01-diagnosis.md](01-diagnosis.md). diff --git a/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md new file mode 100644 index 00000000..80d9149d --- /dev/null +++ b/04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/01-diagnosis.md @@ -0,0 +1,55 @@ +# 321 — Diagnosis + +## 2026-10-08 + +**Where a copy is made.** The builder resolves a module's declared bases before it builds anything. +For an image published elsewhere, it copies the image into the store under +`/on-` and hands the recipe the copy. It also returns the copy as one of +the references the build stood on. The controller keeps those references in the build's +`built_against`. It keeps the build's artifacts in `made`. + +**What the sweep reads.** The keep set and the list of everything the mesh made are both read from +`made`. `built_against` is read only for build edges. So a copy is in the records, but not where the +sweep looks, and it is never counted as something the mesh made. + +**A failed build.** The controller records a failed build with no module and no `built_against`. A +build that copied its base and then failed, for example on a recipe refused for an undeclared fetch, +leaves its copy in the store with no record at all. Reading the records on 2026-10-08: + +- 23 distinct copies are named in `built_against` of successful builds. +- 17 of them were stood on by one of the five most recent builds of a module the controller holds. +- Several `on-` repositories in the store are named by no build at all. Examples: three copies of an + invoicing app's bases, from builds whose rows have no `built_against`; a module's Node base; and a + second version of the route proxy's toolchain image. + +**Why one per module.** The repository name is built from the module's name, so two modules standing +on one image hold two copies. The store keeps each blob once, so the copies cost manifests rather than +bytes. But the first copy into each repository asks the public registry for the whole image again. The +public registry rations anonymous pulls, and on 2026-09-28 it refused a whole catalogue's rebuild. + +**Why 10.4 GiB.** A copy is an image index with every platform the upstream publishes, Windows +included (ADR 0096 copies the index whole). The route proxy's repository holds two versions of a Go +toolchain image, each an index with 16 platform manifests. Four of those platform manifests are each +larger than 2 GB. I think those four are the Windows images, two per version, because the Linux +platforms are around 300 MB each; the index contents were not read to confirm it. + +**Why letting an index go frees nothing.** The copy puts each platform manifest under its own digest, +so each is a revision of the repository. The store's collector, run without deleting untagged +manifests, keeps every revision a repository holds. The sweep deletes the index's revision and nothing +else, so the platform manifests stay, still revisions, and every blob they name stays with them. The +store's tools show this in the route proxy's upstream-artifact repository: one kept index, and 16 +unrecorded platform manifests left behind by the indexes collected before it. + +**Ruled out.** + +- *The store's tools misclassify.* They classify a platform manifest on its own, which is also what the + store's collector does. So "unrecorded" is what collection actually sees. +- *Blob sharing makes the copies free.* It makes duplicate copies cheap. It does not make old versions + of an image free, and nothing ever lets them go. + +**Located** in the controller: the builder (where a copy is made and named), the inventory (what the +sweep reads), and the store client (what letting an index go deletes). The store's tools are located +there too, because they count a platform manifest apart from its index. + +**The fix** is [ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md) +and its two pull requests (the controller and the catalogue's store tools). diff --git a/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md b/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md new file mode 100644 index 00000000..d67b15a1 --- /dev/null +++ b/04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md @@ -0,0 +1,53 @@ +--- +status: located +opened: 2026-10-08 +located-in: [mesh-catalog modules/gitea (the npm-registry bundle: refs.go, retention.go)] +fixed-by: +amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +--- + +# 322. Package retention read no lockfile, and said zero bytes for every kept version + +## Symptom + +On 2026-10-08 at 13:45, the forge's `npm_retention` was run as a dry run, with these results: + +- It read 50 repositories. +- It found one package with four versions and kept all four. +- It listed 32 `package-lock.json` files under `files_not_read`, each with the same error: + `json: cannot unmarshal string into Go struct field npmLockEntry.packages.dependencies of type main.npmLockEntry`. +- Every version read 0 bytes. The forge's own file sizes for the same versions, read through + `npm_packages`, are 20 954 to 28 426 bytes. + +## Why it is a design issue + +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +§4 keeps a version when "a lockfile on the default branch of any repository on the forge names it". No +lockfile was read, so that rule kept nothing. Two further faults were silent: + +- **A size that was never measured read as zero.** The dry run asked the forge for the size of a version + only when it would delete that version. Every kept version therefore answered 0 bytes, with no word + that it had not been measured. +- **An unread file did not stop a real run.** Only an unread *repository* did. A real run would have + deleted a version that only an unread lockfile named, while its answer listed that file as unread. + +## Diagnosis + +1. **The reader knew one lockfile shape.** A lockfile of version 2 or 3 has a `packages` map from paths + to entries. In an entry, `dependencies` (like `devDependencies`, `optionalDependencies` and + `peerDependencies`) is a map from names to range strings. The reader decoded every entry with the + type of a version 1 entry, whose `dependencies` is a map of nested entries. One string failed the + whole file. +2. **Sizes were read only for the versions to delete.** This was an optimisation, and it made a kept + version's size indistinguishable from an empty one. +3. **The refusal checked repositories, not files.** + +**The fix** is in the forge module's Go bundle: + +- It reads versions 1, 2 and 3 one entry at a time. The package name comes from the path after the last + `node_modules/`, or from an alias's `name`. Workspace links are skipped. +- An entry of an unknown shape is named, and the rest of the file is still read. +- Every version is measured, and a size that could not be read is said. +- A file not read whole stops a real run, as an unread repository does. + +Tests use real lockfile shapes of all three versions, and a fake forge that serves file sizes. diff --git a/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md b/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md new file mode 100644 index 00000000..c2e4e06d --- /dev/null +++ b/04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md @@ -0,0 +1,38 @@ +--- +status: open +opened: 2026-10-08 +located-in: [] +fixed-by: +amended-design: +--- + +# 323. An image a definition pins under an older name of the store reads as unrecorded + +## Symptom + +On 2026-10-08, `artifact_store_references` classified three manifests as **unrecorded**: "no record names +it". The anchor runs all three in containers today. They are an invoicing app's API and web front, and a +static company website. Each is pinned by digest in a catalogue module's definition, under an older name +of the same artifact store. The anchor's declaration names them, and its containers run them; both were +matched by digest ([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md)). + +The same word, *unrecorded*, describes 35 repositories and 2 manifests that nothing runs, declares or pins. + +## Why it is a design issue + +[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md) +§2 lists the states a manifest is in. *Kept — a definition names it* is read only for what the mesh made. +An image a definition names, which the mesh did not make, falls through to *unrecorded*. So the answer +that should tell a person "a machine runs this" tells them the same as "nothing knows this". + +Nothing removes an unrecorded manifest today (ADR 0189 §3), so no running image is at risk now. The risk +comes later. Research 035 asks how the images nothing uses may be let go of. A rule that reads +*unrecorded* as *unused* would take three running images with it, and nothing in the answer would warn +the person who runs it. + +## Questions to answer + +- Should the store's tools read the definitions, under every name the store has had, and say *named by a + definition* for a manifest the mesh did not make? +- Or should the three modules be moved to images the mesh builds, so that the case no longer exists, and + the catalogue's module check refuse a definition that pins an image under an older name of the store?