docs(specs): retire S04-C and its implementation plan
🗑️ What this does
Retires S04-C (column-level encryption wiring) and its implementing plan
2026-08-02-s04c-encryption-wiring, and repairs every reference they leave
behind. First application of the retention rule merged in c2bf2b29, so the
resolutions below are the precedent for later retirements.
✅ Eligibility
| Finding | Re-verified |
|---|---|
| One implementing plan | 2026-08-02-s04c-encryption-wiring.md alone declares S04-C on its - Spec: bullet. 2026-08-03-s04b-rotation-tooling.md declares S04-B and 2026-09-03-s04a-per-format-credential-columns.md declares S04-A (with S13 named for a preserved policy), so neither is an implementing plan |
| That plan is completed | |
| No blocking open issue | S04-C returns 13 hits, 12 closed; the only open one is work item 1338 itself |
| No ADR cites it | grep -rn 'S04-C' docs/adr/ is empty, so no handbook amendment is owed and no lychee-excluded link is left dangling |
| Destination coverage |
🔍 Divergences from the pre-flight findings
Two, both reported rather than worked around:
### Deployment posture(spec lines 125–135) is already carried, contrary to the pre-flight reading that paired it with the counter as uncarried. Its FIPS half sits indocs/dev/fips.md## CI gate scope(thetest:crypto-fipsgate scoped to./internal/crypto/..., and why) and## Startup assertion(whose closing paragraph already states that the assertion, log and gauge belong to service-wide compliance work rather than this wiring). Its consumer-table half sits indocs/dev/column-encryption.md## Consumer-table contract, which carries all five items the spec listed. Nothing from that section needed authoring; only### Fallback-unwrap counterdid.- The reference census is 10 files / 31 occurrences over 24 lines, not 12
files / 25 occurrences. Six sites in
2026-09-03-s04a-per-format-credential-columns.mdwere not in the pre-flight list: lines 19, 127, 137, 1825, 3394 and 3407 at the old numbering. Five of those six are line-pinned citations of the same class as the two that were listed. Nothing merged tomainbetween the pre-flight basis and this branch that added a site, so the difference is in the counting, not in the tree.
📚 Spec section by section
Read against the destination rather than checked off a list.
| Spec section | Where it lands |
|---|---|
Title, **Status:** Approved |
Dropped: point-in-time. The index row carries the status, now Retired |
## Summary |
Dropped: point-in-time framing ("this spec makes deployable…"). Its FIPS scoping is in docs/dev/fips.md ## CI gate scope |
## References |
Dropped: point-in-time. Every durable target (ADR 023/024/007, S04-A, S03-B, storage-composition.md, fips.md, gitlab-com-infrastructure.md) is reachable from the destination or the live specs |
## Dependencies |
Dropped: a delivery-state table. Its integration points are the ## Quick reference rows in docs/dev/column-encryption.md |
## Data Model |
Nothing to carry: "No database tables" |
### Boot sequence |
docs/dev/column-encryption.md ## What the composition root wires (the four ordered steps, boot-failure rule, skip-when-absent) |
### The wireEncryption tier |
## What the composition root wires (including the one Info record per boot naming the tier's outcome) and ### The enablement signal |
### Fallback-unwrap counter |
Authored in this MR: ### The fallback-unwrap signal |
### Deployment posture |
Already carried — docs/dev/fips.md ## CI gate scope and ## Startup assertion, plus ## Consumer-table contract, the opening's migration exclusion, and ### Rows the move leaves behind in column-encryption.md. The documentation-pairing assignments in it are point-in-time MR obligations, dropped |
## API Contracts |
Nothing to carry: no endpoints, no new Go API |
## Configuration |
Already carried by docs/dev/configuration-reference.md ## encryption: the block's presence, the local arm requirement, the 10m default and the 1m–1h bounds, and the shred-cooldown derivation |
## Acceptance Criteria |
Dropped: point-in-time. Ten criteria that describe the delivery's test matrix, which merged with !1295 (merged)/!1297 (merged)/!1298 (merged) |
## Error Cases |
Boot taxonomy. The reader-facing half ("any failure on the enabled path fails boot; no degraded mode") is in ## What the composition root wires; the per-sentinel mapping is a code contract of internal/crypto pinned by wire_encryption_test.go, dropped as point-in-time |
## Security Considerations |
Zeroization and the fail-closed posture are in ## What the composition root wires; the label-coercion bound is in the new ### The fallback-unwrap signal; key-material confinement is S04-A's, which stays; the review gates on work item 8 are delivery state, dropped |
## Out of Scope |
Dropped: point-in-time. The live disclaimers it mirrored survive in S04-B's own Out of Scope, repaired below |
## Follow-ups, ## Open Questions |
Both None |
## Resolutions 2 |
The hook contract, authored into ### The fallback-unwrap signal — resolution 2 existed only to say the contract had no other home |
## Resolutions 3, 5 |
Already carried: the absent-block behavior in ## What the composition root wires, the enablement signal in ### The enablement signal |
## Resolutions 1, 4 |
Dropped: 1 is the FIPS scoping now in docs/dev/fips.md, 4 is the work item 8 gate timing, which is delivery state |
📝 What column-encryption.md gained, and who owns which half
One new section, ### The fallback-unwrap signal, holding what the spec alone
stated: the hook fires on a GetKey fetch of an active namespace key wrapped
under a non-active root key; the manager calls it synchronously on the cold-miss
path alone, holding no lock, passing the record and active URIs and no namespace
id; an implementation must be fast, must not panic and must be concurrency-safe;
and the coercion rule with its two reasons and the resulting
configured-keys-plus-one bound.
docs/dev/observability.md line 117 already carried the
counter's operational semantics — warm-cache behavior, concurrent cold
misses, what non-zero proves and what zero does not, the absent() caveat, and
that an out-of-set URI reads unknown. That stays where it is. The split is by
reader: column-encryption.md tells whoever wires the hook what the contract
obliges, observability.md tells whoever reads the samples what they mean. Each
page states the split, so neither drifts into restating the other.
🔗 Reference sites and how each was resolved
24 reference lines across 10 files, of which three are kept as written by rule
(closed-beta.md:90 and :660, …s04a…:19). The rule row that governs each is
named, since this is the precedent run.
Sites that redden docs:lint — a relative link to a deleted path fails lychee --offline
| Site | Resolution | Rule row |
|---|---|---|
docs/specs/S13-virtual-remote-foundation.md:660 |
Inline spec link → column-encryption.md#the-enablement-signal, which is the contract the sentence reaches for |
A live document's reference to a retired spec |
docs/specs/S32-container-virtual.md:46 |
Dependency-row first cell → [Column-level encryption wiring](../dev/column-encryption.md#what-the-composition-root-wires), id dropped. Verdict cell lost its Approved status word and now states the contract and the remaining gap alone |
A live document's reference to a retired spec; a dev-doc table column whose values are spec ids, by analogy |
docs/specs/README.md:97 |
Retired row: unlinked id, Retired status, one-line scope, the MR, the dev doc |
The index row in docs/specs/README.md |
docs/plans/README.md:95 |
Retired row: unlinked Plan cell, Description led by the retirement marker, and — per the spec README's rule, because the same MR deletes the spec — the Spec cell's S04-C link dropped to plain text | The index row in docs/plans/README.md |
Dead text CI cannot see
| Site | Resolution | Rule row |
|---|---|---|
docs/dev/observability.md:117 |
Owner cell S04-C → [column-encryption.md](column-encryption.md#the-fallback-unwrap-signal). The ## Metrics preamble gained one sentence saying the cell names a dev doc where the spec has retired, so the column explains itself, and that the linked page owns the contract while the row owns how to read the samples |
A dev-doc table column whose values are spec ids — the rule's own worked example |
docs/specs/S04-b-encryption-key-rotation-tooling.md:21 |
References entry relabelled to the dev doc with the delivery epic named after it. The epic link resolves, but it is delivery tracking and does not carry the wiring contract, which is what a reader following this entry came for | A live document's reference to a retired spec |
docs/specs/S04-b-…:451 |
not S04-C's → nor the composition-root encryption wiring's; the surface, not the id |
A live spec never carries a bare id for a retired spec |
docs/specs/S04-b-…:456 |
Out-of-scope entry → the dev-doc section, with the epic kept as the delivery pointer | A live document's reference to a retired spec |
docs/specs/S32-container-virtual.md:128 |
S04-C's production encryption wiring → The production encryption wiring, with the dev-doc link |
A live spec never carries a bare id for a retired spec |
internal/metrics/cardinality.go:181 |
Id dropped; the comment now names the metric vector and the cardinality rule it justifies. Compressed from 8 lines to 2 because the gate charges every segment an edit touches (cap 2 here) — the block keeps the configured-keys-plus-one bound with its coercion, why the labels are not in closedSetValues, and the mirrored fallbackUnwrapURIBudget with its full path; a trailing comment on each budget entry says 5 is headroom that nothing refuses past |
Code sites are not in the table — ## Comments do not cite process state governs |
docs/roadmap/closed-beta.md:90, :660 |
Bare id kept as written, and the S04-A plan links on :90 left alone (that plan is not being deleted) |
A dated narrative row |
docs/roadmap/closed-beta.md:185 and its section |
Re-derived over the index, not decremented — see below | A live tally that counts index rows |
docs/plans/2026-08-03-s04b-rotation-tooling.md:7 |
is S04-C's → the dev-doc section |
A live plan never carries a bare id for a retired spec |
docs/plans/…s04b…:26, :169 |
The S04-C encryption wiring → The encryption wiring, and the S04-C chain → the encryption tier's chain. Both sentences already name cmd/artifact-registry/wire_encryption.go or the tier, so the resolvable coordinate was there and the id was the only rot |
A live plan never carries a bare id for a retired spec |
docs/plans/2026-09-03-s04a-per-format-credential-columns.md:19 |
Plan slug left as plain text, unlinked already | A reference to a retired plan keeps the plan's slug |
…s04a…:104, :112 |
The S04-C:199 citation becomes a commit-pinned permalink at the basis the bullet declares; S04-C's FIPS resolution names the spec by surface and links docs/dev/fips.md#startup-assertion, which carries the posture it settled |
A line-pinned citation into either file |
…s04a…:127, :137, :1825, :3394, :3407 |
Each becomes a commit-pinned permalink at the basis its own bullet declares (ee9e5063, 45b52905, 7606fdb5, and 85530efe for the Step 5b entry at :1825). Every line number was verified to still resolve to the quoted text at that commit |
A line-pinned citation into either file, third repair: the retired text itself is what the reader needs |
📊 The tally on closed-beta.md
Re-derived over the three index tables rather than decremented, at
bb10ea3a (2026-09-23), the basis main's own W38 sync of this section uses.
docs/specs/README.md is unchanged since c2bf2b29, so that commit reproduces
the count.
- Status table:
Approveddrops S04-C, 34 → 33; a newRetired | 1 | S04-Crow records the retirement. - The service table stays at 41 rows (6
Implemented, 23Approved, 1In Progress, 1Draft, 9Planned, 1Retired). - Approved or better 41 of 56 → 40 of 55, and
Approvedalone 34 of 56 → 33 of 55, withRetiredcounted on neither side perdocs/specs/README.md. The 56th row is the retired one, so 55 are tracked scope. Excluding the descoped table, closed-beta scope is40 of 50. - The basis line names bb10ea3a and the retirement this change records, and one paragraph says the drop is a retirement, not lost scope or a regression.
💭 Notes for review
- Duo
spec-status-changeswill flag theRetiredrow for having no spec file to match its status. That finding is expected on every retirement, anddocs/specs/README.mdholds the answer in the paragraph directly under the status legend. - No pre-existing retired row names S04-C. Checked both indexes: this is the first retirement in the tree, so there is no earlier row to amend and no earlier MR to preserve in a marker.
- The end-of-plan
docs-authorrun is not wanted for this plan, and is therefore not landed ahead of the deletion. The plan wired the composition root and changed no customer-visible surface, so the run's outcome would beno docs needed, which leaves nothing in the tree for a later reader. Deleting the plan file ends the run's availability rather than postponing it, and that is the intended outcome here. - Nothing under
docs/user/is affected. A retirement changes no customer-visible surface: no endpoint, no GraphQL field, no auth flow, no client behavior, no role, setting or limit moves. The one user-facing page this change came near,configuration-reference.md, already carries the wholeencryptionblock and needed no edit. - Commit prefix.
docs(specs):rather thandocs(plans):, although the MR touches both trees: the spec's completion is what authorizes the retirement and the plan goes with it. Worth a maintainer's view, since the convention assignsdocs(plans):by directory and no rule covers an MR that retires both. - Size. 91 insertions, 563 deletions over 13 files. 514 of the deletions are
the two retired files; the reviewable surface is the 20-line
column-encryption.mdaddition, the 28-lineclosed-beta.mdrecomputation, and 92 lines of short repairs across 10 files. - E2e catalogs need no change:
docs/testing/references no S04-C scenario and this MR changes no behavior. - The retired-row templates are amended here. Both spelled the marker
Retired in {retiring MR URL}, and a bare URL in a table cell is anMD034/no-bare-urlserror undermarkdownlint-cli2; the fenced templates themselves are exempt, so nothing caught it until a real row was written. The three template lines indocs/specs/README.mdanddocs/plans/README.mdnow spell itRetired in [!{iid}]({retiring MR URL}), the form both rows here use and the plan Status table's MR column already prescribes.
Related to #1338