docs(plans): re-measure S20-A steps 19a and 19b, retire dead clauses
What this delivers
Corrections to docs/plans/2026-08-11-s20a-lifecycle-closed-beta.md.
The set grew during review, and the section Added during review at the end lists what was added and why.
Guardrail 4 bars a step merge request from editing the plan file.
So these corrections cannot ride on !1912 (merged) or !1916.
The precedent for the shape is !1914 (merged), which changed the plan file alone.
Every figure here comes from a command, run against a fresh fetch. No figure is adjusted arithmetically from the number it replaces. A stale figure usually has a stale input behind it. Arithmetic carries that input forward and still looks like a correction.
The coordinates of the measurement:
| Half | Head | Base | Taken |
|---|---|---|---|
| Step 19a | 79f8705cb, its squash commit on main |
934df0036, that commit's parent |
at the merge |
| Step 19b | 93fc975b8 |
8686d21cc, the head of step 18's branch then |
2026-08-26 |
Step 19a merged on 2026-08-27, so its figures come from the merge and no longer move. Step 19b is still open and its branch moves, so its figures stay as a dated measurement and the plan writes no current coordinate for it.
Correction 1: the size-forecast table is re-measured
The table's last two rows read 1044 / 1373 / 2417 for step 19a, and 207 / 1469 / 1676 for step 19b.
The three columns are Source, Tests and Total.
Measured at the merge, step 19a is 25 files at +3147 / −1577.
That splits into production Go +666 / −1180 across 11 files, test Go +1442 / −397 across 13, and docs/dev/storage-accounting.md alone at +1039.
git diff --numstat 934df0036 79f8705cb reproduces every one of those figures.
Step 19b's own share was 16 files at +1322 / −302 on 2026-08-26.
That splits into production Go +73 / −224 across 7 files, test Go +1180 / −68 across 5, and 4 docs, CI and recipe files at +69 / −10.
The direction moved, not only the digits, and the table now says so.
Step 19a's production Go fell, from 1044 added lines to 666.
Step 19b's own production Go shrinks, because cmd/artifact-registry/wire_lifecycle.go measures +15 / −104.
The two npm unpublish handlers also move comment blocks out to docs/dev/storage-accounting.md.
A row with production Go coming off while tests grow is a stronger record for the LOC guardrail than one row of added lines.
Correction 2: a retired contingency in step 19a's scope
The scope said three reap arms keep reporting zero. It named !1901 (merged) as a merge request that can turn the Maven remote zero into an over-count.
!1901 (merged) merged on 2026-08-25.
RemoteCacheStore.emitCommittedFillCounters is now on main, in internal/format/maven/remote_fill_emit.go.
Step 19a's branch converts that arm, through a sumFreedRepositoryBytes call in internal/datastore/lifecycle_reap_maven_remote.go.
So five arms report bytes and two stay at zero.
Hosted OCI's condition is discharged the same way.
!1895 (merged) merged on 2026-08-27 as 998529c09, so cmd/artifact-registry/wire_oci.go constructs oci.NewCounterSink and a hosted OCI push raises repositories.size_bytes.
That turns ContainerReaper's zero into a real over-count, and it is a reclassification rather than a conversion, because !1895 (merged) does not touch that arm's projection obstacle.
The npm remote cache fill has no merge request at all, only #834, so that arm's condition stands.
The remainder belongs to #942 rather than to #681 (closed), whose own ## Scope names neither ContainerReaper nor NpmRemoteVersionReaper.
Correction 3: the stale size forecast is in the plan as well
!1914 (merged)'s description carried a size forecast of "22 files, +2417/−296" for step 19a. That merge request is merged, so its description is history and this branch does not touch it.
The plan carries the same forecast, in the paragraph that opens "Both halves are measured rather than forecast". It is corrected there, together with every restatement of it in the plan.
Which classes of figure this corrects
A reviewer can check the set is complete rather than sampled:
- The two measurement rows of the size-forecast table, and the paragraph under it.
- The size forecast for step 19a, and the partition of step 19b's branch that follows it.
- The per-file divergence between the two halves, which is now two files rather than four.
- The per-file figures in step 19b's
Files:list, forpurge_internal_test.go,lifecycle_scan.goandlifecycle_reap_npm.go. - The figures in step 19b's Accepted smell, which read 37 files and 4135 added lines.
- The count of reap arms that cannot meet the artifact-purge criterion, which read five.
- The claim count in step 19b's
Files:bullet fordocs/dev/storage-accounting.md, which read two. - The converted-arm counts in step 19a's
Files:, Acceptance andTests:entries, which read four converted arms and three at zero. - The work item that owns the two arms still at zero, which read #681 (closed).
- The three sentences claiming every file step 19a touches sits under
internal/datastore/.
One count is removed rather than corrected
Step 19b's Files: bullet said the step makes "two claims stop being true" in docs/dev/storage-accounting.md.
The count is wrong, and this branch does not put a different number in its place.
What counts as one claim has no fixed boundary in that diff, so any number is a judgement a reader cannot check.
The bullet names the topics instead, and a reader can check those.
The corrections to that document's existing text sit in two hunks and replace six lines.
Measured at 93fc975b8 against 8686d21cc, they cover the construction-site facts for datastore.ContainerManifestDeleter and datastore.ContainerBlobUnlinker.
That includes the forward-looking clause which had the purge paths joining both helpers later.
They also cover the reading of tagsDeleted, and the attribution of the Δsize computation to the format layer.
The same bullet put the purger's entry in that document's ## Testing a call site section.
No hunk of either branch touches that section.
The suite documentation lands under ### What the reap suites pin and #### What the emit suites pin, inside the new ## Purging a tombstone section.
That half is corrected rather than removed.
Two more stale figures in the same document
These two were already false before this change, rather than made false by it.
They sit in step 19a's Files: list and its Acceptance block, next to figures this branch corrects.
A reader who meets corrected figures beside uncorrected ones cannot tell which figures in the document are current.
The probe declares nine statement names rather than seven: four freed-size names and five repository-resolution names.
The declared query-name count moves from 448 at 91909cdf9 to 457 at 48a51a2b4, which is past the ceiling of 450 that base carried.
Both endpoints now carry a commit, so the pair reads as the measurement it is rather than as a claim about wherever step 19a's head sits.
The ceiling is not raised in that step: !1870 (merged) raised it from 450 to 500 when it merged on 2026-08-26, in commit 7e94f0a73.
!1948 (merged) removed TestNameBudget_CoversEveryDeclaredQueryName the same day, in commit 3555452a3, so nothing reads either catalog against the budget, and #908 decides what replaces the check.
The plan read "423 to 430 against a ceiling of 450".
An earlier revision of this merge request added internal/metrics/cardinality.go to that Files: list and had step 19a raise the budget; both are dropped, because internal/metrics/cardinality.go is not among !1912 (merged)'s 25 changed paths.
A false clause in the plan's testing strategy
The **e2e scenario impact.** paragraph said that docs/testing/e2e/maven.md carries no storage-counter scenario, and that docker.md and oci.md put remote repositories outside catalog scope, so no catalog other than npm is affected.
Both halves are wrong, and they were wrong when they were written rather than made wrong later.
The docs/testing/e2e/maven.md catalog carries e2e.maven.lifecycle.management-delete-storage-counters, docker.md carries e2e.docker.lifecycle.delete-storage-counters, and oci.md carries e2e.oci.lifecycle.delete-storage-counters.
Each has its own usage-data line, and each carries a size_bytes assertion that waits on the reap.
f42703ddc added all three on 2026-08-25 at 23:35 UTC, about nine hours before ee1e1b656 wrote the clause.
The remote-repository exclusion is real but carries no conclusion. All three rows are hosted-repository journeys, and maven.md excludes remote on the same terms, so the property does not separate those catalogs from npm.
Tests and the e2e catalogs
This merge request changes one Markdown file and no Go code, so it adds no test.
It also adds no e2e scenario.
The plan is not behavior-changing work, and the scenario that step 19b's branch does touch, e2e.npm.lifecycle.repository-storage-counters, moves in !1916 rather than here.
Verification
pre-commit run --files docs/plans/2026-08-11-s20a-lifecycle-closed-beta.md passes.
That run covers markdownlint-cli2, lychee with --include-fragments, and vale.
Added during review
Four blocking notes from @dmeshcharakou, all four saying a plan line is false because a merge request merged under it. Each is answered in its own thread.
- Step 19a's figures are pinned to the merge. !1912 (merged) merged as
79f8705cb, whose parent onmainis934df0036. The branch head48a51a2b4is on no ref, so the old pair described a state a reader cannot fetch. The48a51a2b4generation stays as a dated superseded measurement beside the 22-file one. - Hosted OCI's condition is discharged and the remainder moves to #942. Covered under Correction 2 above.
- Step 19b keeps a dated measurement and gains no live pin. Its branch moves, so the partition sentence is replaced by a condition and its event, plus the command that reproduces the own-share set over the paths neither merge touches. The Merge order note's target branch and the Accepted smell's self-healing direction move with it.
- The file-location clause is deleted at three sites. The merged step changed 25 paths, and
internal/lifecycle/chunk.go,internal/lifecycle/chunk_integration_test.goanddocs/dev/storage-accounting.mdare not underinternal/datastore/. Each sentence already carried the accurate reason, so the reason stands alone. The Dependencies row for 19a gains the independence fact its step entry already carries, and records the merge commit.
One stale clause is observed and not worked here, because no note raised it: the two !1892 (merged) obligations in step 19b's Merge order bullet are written as falling due at the rebase and at no point before it, and ceb93849 is already an ancestor of that branch's merge base with main.
This is a bot message