## Context The `restore-crypto-keychain-packer-umbrella` change was sliced into four sequential children on 2026-05-03 (`restore-crypto-cleanup-duplicates`, `restore-crypto-fold-keys-vault`, `restore-crypto-opinionated-layer`, `restore-crypto-cli-and-allowlist`). Each child shipped its source-code delta. The umbrella itself was archived to `openspec/archive/2026-05-03-restore-crypto-keychain-packer-umbrella/` with its 20-requirement spec under `specs/crypto/spec.md`. The slice plan implicitly assumed each child would carry its share of the umbrella's spec deltas into `openspec/specs/crypto/spec.md`. That did not happen. The current live spec at `openspec/specs/crypto/spec.md` carries primitive-substrate requirements, the `keychain` CLI surface, the streaming-AEAD façade, and password-blob helpers — but the central "every key carries `(masterVersion, slotVersion)`" invariant is absent at the requirement layer. The supporting source code shipped (verified by direct inspection of `packages/crypto/crypto/src/derivation/{coordinate,layouts}/{coordinate,layouts}.ts` and the test files alongside them). In parallel, `openspec/specs/crypto-keys/spec.md` exists as a stub: `Purpose: TBD - created by archiving change crypto-completion`. That stub describes a `BaseKeySchema` device-key surface for a `@suaveplan/crypto-keys` package that no longer exists on genesis (`packages/crypto/crypto-keys/` is absent). Live `openspec/specs/crypto/spec.md:204-207` asserts the package must be deleted; the stub contradicts it. This is a documentation-debt change. The code is correct and tested. The spec corpus is incomplete and self-contradictory. ## Goals / Non-Goals **Goals:** - Restore the versioned-key vocabulary (`DerivationCoordinate`, `DerivationLayout`, `masterVersion`, `slotVersion`, eight-slot `Keychain`, validity windows, parent-signed bundles, hierarchical identity, revocation, audit, packer, replay protection, trust-anchor) as named requirements in `openspec/specs/crypto/spec.md`. - Every new requirement carries `Verified by:` pointers to existing test files at concrete `path::"test name"` granularity. - Eliminate the contradiction between `openspec/specs/crypto/spec.md` (says crypto-keys is gone) and `openspec/specs/crypto-keys/spec.md` (still describes it) by removing the orphan stub. - After archive, `openspec validate --strict` is green for the live `crypto` capability and `openspec/specs/crypto-keys/` no longer exists. **Non-Goals:** - No source-code changes. No new tests. No deleted tests. No moved files in `packages/`. - No new requirements that the existing test suite does not already cover. If the umbrella had a requirement (e.g. a specific CRL fetch endpoint) that has no current test, it is **not** folded — it gets a follow-up implementation change. - No reopening of any of the four archived child changes (`restore-crypto-cleanup-duplicates`, `restore-crypto-fold-keys-vault`, `restore-crypto-opinionated-layer`, `restore-crypto-cli-and-allowlist`). Their archives remain authoritative for their respective slices. - No reorganisation of unrelated requirements (primitive substrate, AEAD façade, password blob, allowlist gate). Their text is unchanged. - No spec changes for `bitpacker-crypto`, `crypto-cli`, `crypto-core`, `slsa`. The umbrella touched their specs too; those folds are out of scope here and tracked as separate follow-ups if found necessary. ## Decisions ### Decision 1: spec-only change, zero code touch Rationale: the source already implements every requirement to be folded. Any diff outside `openspec/specs/crypto/` and `openspec/specs/crypto-keys/` would expand scope into a re-audit of `restore-crypto-opinionated-layer`'s archive. By constraining the change to spec text plus deletion of the stub, the review surface is bounded and the risk of accidentally re-implementing already-shipped logic is zero. Alternative considered: a combined "spec restore + missing-test backfill" change. Rejected — coverage is already at 100% on this package per the gate, so any "missing test" would be a requirement that was never shipped, which makes it a new feature change, not a fold. ### Decision 2: fold by direct copy, not by reference Each folded requirement's text lives in `openspec/specs/crypto/spec.md` as-written. We do **not** add `See archived umbrella` cross-references. Reasons: 1. Live specs are the contract. A live spec that delegates to an archived spec for its central invariants is dead-on-arrival when the archive is read in a different context. 2. `openspec validate` does not traverse archive paths. 3. The umbrella contained more than this change is folding (e.g. `Domain-separation tag on every signed payload` cross-cuts requirements that are out of this change's scope). Direct-copy lets us select cleanly. ### Decision 3: every folded requirement carries `Verified by:` `Verified by: ::""` is mandatory on every new requirement. The format is the same one already used in newer parts of `openspec/specs/crypto/spec.md` (e.g. lines 311, 367). The validation step in `tasks.md` greps each cited test file for the exact `it(` / `test(` string before the change is allowed to archive. Rationale: a folded requirement without a `Verified by:` pointer is indistinguishable from a forward-looking spec, which is exactly the failure mode that produced the current contradictory corpus. ### Decision 4: requirements are folded at umbrella granularity, not split The umbrella named 14 requirements that are missing from the live spec. Each becomes a single requirement here, with the same name. We do not split a single umbrella requirement into multiple smaller ones (e.g. one per scenario). Reasons: 1. Bigger requirements with multiple `#### Scenario:` blocks are the existing house style in `openspec/specs/crypto/spec.md`. 2. Splitting would reformat the umbrella's authorial intent without adding signal. 3. A single requirement per umbrella concept makes the diff between umbrella archive and live spec trivially auditable. ### Decision 5: delete `openspec/specs/crypto-keys/spec.md`, do not rewrite it The stub describes `BaseKeySchema` device keys (`{s, v, m, u, d, o}`) for a deleted package. Two alternatives were considered: a. Rewrite the stub as a `crypto-keys` capability that documents the post-fold "the package no longer exists" reality. Rejected — non-existent capabilities are noise, and `openspec/specs/crypto/spec.md` already asserts non-existence. b. Move the stub's content (the device-key requirements) into `openspec/specs/crypto/spec.md` under a new requirement. Rejected — the underlying device-key derivation API is **not implemented** on genesis (per `archive/2026-05-03-crypto-completion/tasks.md:60`, the device-key suite is deferred to a `crypto-keys-port` follow-up). Folding it now would re-introduce the same problem this change is fixing: a requirement with no `Verified by:` pointer. Chosen: delete the stub. If `crypto-keys-port` ever ships, it gets to add its own requirements with its own implementing tests. ### Decision 6: name the requirements verbatim from the umbrella, drop the unbacked ones The 13 requirement headings will read exactly as they did in the umbrella's `specs/crypto/spec.md`: 1. Eight-slot Keychain with full-width derivation coordinates 2. DerivationLayout registry — pluggable u64 layout 3. Validity windows enforced at every verify call 4. Domain-separation tag on every signed payload 5. Public bundles are parent-signed and carry an issuer reference 6. Chain bundle traversal — concatenated parent-signed links 7. Wire-format schemas exposed and committed 8. Authority — signing abstraction over hierarchical identity 9. Hierarchical identity — Root and Server 10. RevocationList — root-signed CRL 11. AuditLog — append-only Merkle log 12. Packer — encrypt-and-sign envelope 13. ChunkedPacker — per-chunk-signed P2P fragmentation The umbrella's `Trust-anchor bootstrap` requirement is **dropped** from this fold. Direct inspection of `packages/crypto/crypto/src/` confirms there is no `trust-anchors/` directory, no `loadTrustAnchor()` / `loadTrustAnchorFromEnv()` symbol, and no `SUAVEPLAN_TRUST_ANCHOR_PATH` consumer. Folding the requirement now would create a spec entry whose `Verified by:` pointer cites a test that does not exist — exactly the failure mode this change exists to fix. Trust-anchor bootstrap needs an implementation change first; this change leaves it explicitly out of scope. The umbrella's `Vault` and `Replay protection via issuedAt + nonce` requirements are **not** added either — their content overlaps with the existing live requirement `Opinionated layer modules — Identity, Revocation, Audit, Packer, Vault` (line 209) plus the replay scenarios that ride on the new `Packer` requirement. Folding them as separate requirements would create cross-cutting duplication. The live `Opinionated layer modules` requirement is left intact as a higher-level overview; the 13 new requirements add the missing per-module detail. The umbrella's `Wire-format KAT vectors`, `Property-based round-trip coverage`, `OpenTelemetry instrumentation with key-byte redaction`, and `Structured error codes with stable identifiers` requirements are **not** folded either: KAT and property-coverage are implementation gates already enforced by the existing 100%-coverage requirement; OTel redaction is covered by the live "Structured errors never leak secrets" requirement at line 139; structured error codes are covered by the same live requirement. Adding any of them would duplicate. ## Risks / Trade-offs - [Risk] A `Verified by:` pointer cites a test that does not actually assert what the requirement says. → Mitigation: the validation step in `tasks.md` greps each cited test file for the exact `it(` / `test(` string and reports any miss as a hard failure before archive. A second mitigation: every requirement's scenario block is hand-copied from the umbrella, where each scenario was originally written to map to a planned test name. - [Risk] The umbrella's wording references symbols (`KEYCHAIN.UNKNOWN_LAYOUT`, `TRUST_ANCHOR.MISMATCH`, `DOMAIN_PUBLIC_KEYCHAIN_V1`) that may have been renamed in source after archive. → Mitigation: `tasks.md` includes a grep step that confirms each cited symbol exists in `packages/crypto/crypto/src/`. A miss either reveals a stale-spec bug (which fails the change) or a stale-source bug (which gets a separate fix). - [Risk] Folding 14 requirements expands `openspec/specs/crypto/spec.md` substantially, raising the chance of merge conflicts with parallel changes touching `crypto`. → Mitigation: this change is small in absolute terms (~600 lines of spec text) and ships before any other open `crypto` change can land. We do not block on adjacent capabilities. - [Risk] Removing `openspec/specs/crypto-keys/spec.md` breaks any external doc/link that references it. → Mitigation: a grep over the genesis tree will be run as part of `tasks.md`. If any consumer references the path, the change blocks until those references are updated. Empirically (per the search done while drafting this proposal), only `archive/2026-05-03-crypto-completion/` references `crypto-keys`, and archives are immutable. - [Trade-off] Spec-only changes are cheap to write and risky to review — they look low-stakes but encode contracts. → Counterweight: every requirement has a verifiable test pointer, so reviewers can run `bunx vitest run --reporter=verbose` and confirm the cited scenarios pass. The contract is testable, not just readable. ## Migration Plan 1. **Add deltas**: write `specs/crypto/spec.md` (`## ADDED Requirements` section, 14 entries) and `specs/crypto-keys/spec.md` (`## REMOVED Requirements` section listing every requirement currently in `openspec/specs/crypto-keys/spec.md`). The delta files live under `openspec/changes/crypto-spec-restore-versioning/specs/`. 2. **Validate**: `bun openspec validate crypto-spec-restore-versioning --strict` passes. 3. **Verified-by audit**: for each new requirement, grep its `Verified by:` pointer against the cited test file and assert match. Block on any miss. 4. **Symbol audit**: grep each cited symbol (`DERIVATION_LAYOUT_V1`, `DOMAIN_VAULT_V1`, `PUBLIC_BUNDLE_MAGIC`, etc.) against `packages/crypto/crypto/src/` and assert presence. 5. **External-reference audit**: grep `crypto-keys` against the live tree (excluding `openspec/archive/`) and assert no consumer breaks. 6. **Apply**: fold the deltas into `openspec/specs/crypto/spec.md`; delete `openspec/specs/crypto-keys/`; archive `openspec/changes/crypto-spec-restore-versioning/` to `openspec/archive/-crypto-spec-restore-versioning/`. 7. **Post-archive validation**: `bun openspec validate --strict` passes for the entire spec corpus. **Rollback strategy**: revert the single archive commit. The change is one commit by design. ## Open Questions - Should `Domain-separation tag on every signed payload` be folded here, or already covered by the existing live "Structured errors never leak secrets" + "Opinionated layer modules" pair? Default per Decision 6: fold it. Reviewer can pull it if the duplication is judged worse than the explicit mention. - Should `Wire-format schemas exposed and committed` reference the KAT-fixture directory `packages/crypto/crypto/test-fixtures/wire-format/` if it exists, or stay symbol-only? Default: stay symbol-only; if a `test-fixtures/` dir is found during the symbol-audit step, add a scenario that asserts byte-stability against it.