docs(specs): correct S20-A's read-only premise and record the owed scan levels
What this changes
docs/specs/S20-a-lifecycle-closed-beta.md justifies indexing npm_packages alone for package-level tombstone discovery.
The premise is that Maven has no delete path at any layer and that the remote caches are read-only.
Both halves of that premise are false on main, and the spec's discovery-index inventory is stale as well.
This merge request corrects the false statements and records the three scan levels merged S17 assigns to this spec. It specifies no scan, adds no acceptance criterion for one, and touches no plan file. The three scan levels and their walks are tracked in #728.
Why the premise is false
| Table | Writer on main |
Discovery index on main |
Scan level |
|---|---|---|---|
maven_packages |
MavenPackageStore.SoftDeleteMavenPackage |
index_maven_packages_on_ns_id_soft_deleted_at |
none |
maven_remote_packages |
MavenRemoteEvictor.EvictMavenRemotePackage |
index_maven_remote_packages_on_ns_id_soft_deleted_at |
none |
npm_remote_packages |
NpmRemoteEvictor.EvictNpmRemotePackage |
index_npm_remote_packages_on_ns_id_soft_deleted_at |
none |
container_remote_images |
ContainerRemoteEvictor.EvictContainerRemoteImage |
index_container_remote_images_on_ns_id_soft_deleted_at_id |
none |
Each writer marks the named row and no child row.
LifecycleScanStore carries eight scans on main, and none of them reads a remote package or image table, or maven_packages.
A mark on any of these four rows therefore reaches no sweep, and its subtree and blob_storage_attachments rows never reap.
What the correction does not claim
No sentence here says that a request produces one of these marks.
No production caller outside internal/datastore reaches any of the three cache evictors.
!1726 (merged) serves the Maven remote package, version and file deletes.
While !1726 (merged) is open, no request path reaches MavenRemoteEvictor.
Once it merges, a request produces a maven_remote_packages mark.
Lines changed
| Section | Before | After |
|---|---|---|
soft_deleted_at inventory, five rows |
repositories, maven_remote_packages, npm_packages, npm_remote_packages and container_remote_images each read no in the discovery-index column |
each reads yes, and container_remote_images names its id DESC key |
soft_deleted_at inventory, the #472 (closed) container bullet |
"That no writer sets container_remote_images.soft_deleted_at today is not a property the purger may rely on" |
names ContainerRemoteEvictor.EvictContainerRemoteImage as the writer and keeps the purger's rule |
soft_deleted_at inventory, the index count |
"Seven tombstone-discovery indexes exist today" | twelve, with the five tables that carry none, and the migration behind each of the seven newer ones |
| One column and three indexes | "Maven has no delete path at any layer, and the remote caches are read-only" | names the four writers, and states that the missing piece is the scan rather than the index |
| The eligibility predicate and the discovery path, lead-in | "seven are and ten are not" | twelve and five, plus the statement that an index is not a scan level |
| The eligibility predicate and the discovery path, bound table | the three remote parent tables had no row | one row for the three, recording no scan level and the amendment that owes them one |
| The eligibility predicate and the discovery path, traversal | "For every *_packages table but one, that upward leg is the only route" |
names the four tables in that position and why each is reached by nothing |
| The eligibility predicate and the discovery path | no record of the amendment | one paragraph recording the three owed scan levels, the missing downward walk, and #728 |
| The rejected-shape bullet | "the remaining nine tables" | drops the stale count and records that three of those indexes are now built |
| What a tombstone scan cannot reach | the left column read as a scan claim | one sentence saying it names the tombstone-carrying side, not a scan level per table |
Acceptance criteria, the two EXPLAIN ones |
the version-level one claimed "a positive hit for every table that carries the index" | "a positive hit for each of those four", plus a note that neither criterion gains a table from the three owed levels |
| Follow-ups | no entry | one entry for the three scan levels, their walk, and #728 |
| The S10 Dependencies row | "Maven's per-entity stores, which carry no delete path at any layer: no maven_* table has a DELETE statement anywhere in internal/datastore", and a no delete path Status cell | names the soft-delete writers and the one hard delete separately, and the Status cell states both |
| The reap-inventory paragraph | "no *_remote_* store carries a DELETE at all" | names the two file-level remote deletes, MavenRemoteEvictor and NpmRemoteEvictor |
| The #464 (closed) finalizer section | "Maven is worse: no delete path at any layer" | names the one file-level hard delete, and keeps the conclusion |
| The reap-inventory paragraph, npm | "Nothing in the tree hard-deletes an npm_files, npm_versions or npm_packages row" | names NpmFileStore.DeleteNpmFile, and keeps the npm_versions and npm_packages halves, which are true |
| The reap-inventory paragraph, npm attachments | "Nothing deletes an npm hosted file's blob_storage_attachments row" | scoped to the unpublish and cascade paths, and names NpmLeafDeleter.deleteFileTx as the path that does remove it |
| Six build-state claims for the repositories and npm_packages indexes | "still to build", "this spec adds", "this spec builds" | each names S20-A step 1's migration, which landed them in !1608 (merged) |
Two of the five inventory rows and the maven_packages finding are outside the set that was escalated.
Re-verification against main found them false in the same way, so they are corrected here rather than left for a second pass.
Review round: three further corrections and the plan file
A review round on this merge request found the corrected premise still standing in three other places in the same file, and the two indexes this spec specifies described as pending after they had landed. Both are corrected here, in the two commits above the original. The rows are in the table above.
A fourth correction came out of the same round, in the npm half of the reap inventory.
The paragraph claimed that nothing in the tree hard-deletes an npm_files, npm_versions or
npm_packages row.
NpmFileStore.DeleteNpmFile removes a single npm_files row and NpmLeafDeleter reaches it, so that
third of the claim is false.
The other two thirds were each checked and are true: no DELETE anywhere in internal targets
npm_versions or npm_packages, and the DELETE inside npm_packages.go is cascadeDeleteNpmTags,
which targets npm_tags.
No reviewer raised this one.
The sentence directly below it was false in the same way and is corrected with it.
It claimed that nothing deletes an npm hosted file's blob_storage_attachments row, while
NpmLeafDeleter.deleteFileTx calls BlobStorageAttachmentStore.DeleteIfUnreferenced in the same
transaction as its single-row DeleteNpmFile.
The claim holds for the paths it was written about, so it is scoped rather than dropped: the unpublish
and cascade paths do leave the row in place, which is what the table above records for
npm_metadata_files as "attachment left behind".
The sentence after it is untouched, because its argument is that DeleteIfUnreferenced answers for npm.
One occurrence is deliberately left alone.
The acceptance criterion that reads "no maven_* table had a DELETE statement before this change"
is rewritten by !1702 (merged),
which owns that line, so editing it here would duplicate that work and conflict with it.
This merge request also edits docs/plans/2026-08-11-s20a-lifecycle-closed-beta.md, which its original
scope did not cover.
The plan quotes, as the spec's own words, a sentence that the spec change here deletes, so without the
edit the plan would quote text the spec no longer carries at the anchor it names.
The edit is one quote and its framing sentence, in Research Findings.
Guardrail 4 bars a step MR from editing the plan file; this merge request is a docs(specs)
correction with no step marker, and the edit is outside the Status table.
Merge order
This merge request merges before
!1704 (merged), which rebases onto
the corrected spec text.
Both rewrite the same container_remote_images Data Model bullet, and !1704 (merged) has an open hunk next to
the EXPLAIN acceptance criterion this merge request edits.
Evidence
- Writers:
git grep "func (e \*MavenRemoteEvictor)"and its npm and container siblings, plusMavenPackageStore.SoftDeleteMavenPackageininternal/datastore/maven_packages.go. - Indexes:
grep "WHERE (soft_deleted_at IS NOT NULL);" internal/datastore/migrations/structure.sql, filtered toON ONLY public.declarations. Twelve parent declarations of the discovery shape. - Scans:
grep "func (s \*LifecycleScanStore) Scan" internal/datastore/*.go. Eight, none on a remote package or image table. - Callers: no reference to any of the three evictors outside
internal/datastorein non-test code.
Checks
markdownlint-cli2, lychee with --include-fragments, and vale all pass on the changed file.
No code changes, so no test, conformance run, or e2e scenario is affected.
Related to #728
This is a bot message