docs(specs): add the S20-A lifecycle closed-beta spec
What
Adds docs/specs/S20-a-lifecycle-closed-beta.md with status Approved, its
docs/specs/README.md row, and the S17 amendment the new request contract needs.
The spec specifies one component and one input contract: a purger keyed on
soft_deleted_at, whose eligibility predicate is
soft_deleted_at < now() - retention_window with the window at zero in closed beta. It
does not know who wrote a tombstone and no entry point holds a reference to it, so GA is
a configuration change plus a new writer rather than a rewrite. ## Forward compatibility
makes that checkable with a per-surface reuse/flip/discard table.
Closed beta deletes permanently: no restore, no trash listing, no retention window.
Scope
| Area | What ships |
|---|---|
| The purger | repository-, version- and package-level tombstone scans, foreign-key walks in both directions, per-table reap logic, chunked per artifact |
| Sweep | River periodic job re-queueing anything eligible with no live purge job, enumerating on the same three indexes the purge scans use |
| Repository entry point (#464 (closed)) | required destructive parameter, tombstone, 202, FK-guarded finalizer |
| Artifact entry point (#470 (closed)) | npm's existing tombstones become correct once reaped; no write-path change |
| Visibility | the soft_deleted_at IS NULL filter the container resolver omits, plus two defense-in-depth sites |
| State alignment (#472 (closed)) | decision 1 absorbed, decision 2 open for hosted container only, decision 3 recorded, decision 4 a follow-up |
| Container remote (S16) | three tables carry soft_deleted_at; two arrive with attachment edges, and container_remote_manifests brings a fifth version-level scan, gated on S16's Step 1c |
| S22 emit sites | every call site: increments per format, Δartifacts at the tombstone write, Δsize/Δcomponents at the purger |
| Schema | two indexes — ADR-007's on repositories, and a new one on npm_packages (handbook!20690) |
| CI | the purger's package joins test:storage-failpoints' enable list and changes: rule, or every gofail criterion skips and reports green |
S17 amendment
One change: DELETE /api/v1/:slug/repositories/:repository_name gains a request contract
where it takes no parameters today. 204 unchanged for the empty case, 202 added for
destructive delete of a non-empty repository, 400 for a missing or non-boolean value.
Delete section, API-contract row, three acceptance criteria and two error rows.
api/openapi/** and api/bruno/** belong to the implementation plan, not this MR.
Research corrections
Re-derived against main; where research contradicts a source, the spec says so at that
point rather than substituting the fact silently.
npm_tagsis not a purger gap.CascadeSoftDeleteNpmPackage's final leg hard-deletes every tag row for the package, and the single-version path callsDeleteNpmTagsForVersion. Issue #470 (closed) and the mechanism note both predate that.npm_metadata_filesis the real FK stall, on the opposite path. Single-version unpublish force-expires the cache but soft-deletes the parent package when the last active version goes, leaving live rows under a tombstoned package and a23503onDELETE FROM npm_packages. Whole-package unpublish is the clean one.- Container's walk hits an application guard, not only a foreign key.
ContainerManifestDeleterrefuses a manifest still referenced by an active index, so the walk reaps manifests parent-first. RegisterPeriodicalready has a feature caller — S13's health sweep. S22's plan text claiming none is stale; this spec is the third registrant.- Counts: 14 parent tables carry
soft_deleted_atand 15 partial unique indexes gate onIS NULL, not #472 (closed)'s ten —maven_remote_filesandcontainer_remote_imageslanded after the issue was filed. The container family's absence is not total. - Option A's foreign-key counts are dropped, not corrected. They re-derive to 27 and 13 against the note's "about two dozen" and 10, and neither number carries the argument.
- S12 permits rather than assigns. It says once that "S20 may add the columns and partial indexes back"; ADR-007 is what settles the direction.
- The S22 dependency runs both ways. Its criterion 21 is gated on #464 (closed), so neither spec is upstream of the other.
- S22 criteria 19 and 20 are restated rather than copied, with the reasons given.
Open Questions are populated, deliberately
Issue #472 (closed)'s decision 2 — now scoped to hosted container, since S16 owns the remote
columns — and the Δartifacts emit site, addressed to S22's author, are genuinely open.
Three more carry confirmations rather than decisions.
Testing
Documentation, plus a three-line comment fix in internal/datastore/maven_repositories.go
correcting stub TODOs that attributed the shared buffered-write client to S22.
markdownlint-cli2, vale and lychee pass; go build ./... clean.
Related to #533 (closed)