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_tags is not a purger gap. CascadeSoftDeleteNpmPackage's final leg hard-deletes every tag row for the package, and the single-version path calls DeleteNpmTagsForVersion. Issue #470 (closed) and the mechanism note both predate that.
  • npm_metadata_files is 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 a 23503 on DELETE FROM npm_packages. Whole-package unpublish is the clean one.
  • Container's walk hits an application guard, not only a foreign key. ContainerManifestDeleter refuses a manifest still referenced by an active index, so the walk reaps manifests parent-first.
  • RegisterPeriodic already 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_at and 15 partial unique indexes gate on IS NULL, not #472 (closed)'s ten — maven_remote_files and container_remote_images landed 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)

Edited by Pawel Rozlach

Merge request reports

Loading
Loading