Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
# Enterprise hardening — `@suaveplan/packer` integrity cluster
**Package**: `@suaveplan/packer`
**Status**: PROPOSED
**Type**: feature
**Design**: (inline below — small scope)
**Spec delta**: ./specs/packer/spec.md
## Why
`@suaveplan/packer` currently ships ed25519 signing and a SHA-256 per-chunk wire
hash, but neither the whole-file nor the chunked paths emit a content-addressable
digest scheme or a bundle-level Merkle root. There is also no OpenTelemetry
instrumentation on the public entrypoints (no counters, no histograms, no spans)
and every user-visible error carries a hard-coded English string. The
`enterprise/packer.md` audit calls all three out as gaps blocking the
ENTERPRISE label.
This is cluster #1 of a 3-cluster enterprise hardening of `@suaveplan/packer`
(integrity, resilience, extensibility). Clusters #2 and #3 are deliberately out
of scope here — see "Out of scope" below.
## What changes
- ADDED: `src/integrity/integrity.ts` — `entryDigestBlake3(data)` and
`merkleRootBlake3(digests)` primitives over `@noble/hashes/blake3`.
- ADDED: `integrity?: PackedDataIntegrity` field on `PackedDataMetadata`
carrying `{ entryDigest, merkleRoot }`. Always populated by `Packer.pack`.
- ADDED: `PACKER_INTEGRITY_FAILED` error code; thrown by `Unpacker.unpack` when
integrity is missing or does not match the recomputed digests.
- ADDED: `entryDigest` field on `ManifestChunkEntry`; `merkleRoot` field on
`Manifest`. `buildManifest` populates both; `verifyManifest` continues to
cover them via the existing canonical-encode signature.
- ADDED: `unpackChunksWithManifest(chunks, manifest, opts)` — chunked unpacker
variant that verifies per-chunk BLAKE3 against `manifest.chunks[i].entryDigest`
and the manifest's Merkle root before yielding plaintext.
- ADDED: `src/observability/observability.ts` — OTel instruments
(`packer_bundle_size_bytes`, `packer_entry_count`, `packer_pack_total`,
`packer_unpack_total`, `packer_integrity_failure_total`,
`packer_pack_duration_ms`, `packer_unpack_duration_ms`,
`packer_entry_decode_ms`) and `withPackerSpan` helper.
- ADDED: `PackerObservability` injection on `PackerOptions` and on
`PackChunkedOptions` / `UnpackChunkedOptions` — defaults to a no-op recorder
pulled from `@opentelemetry/api`.
- ADDED: `src/i18n/messages.ts` — English-defaults message catalog keyed by
`packer.error.<code>.<scenario>`. Constructor option
`PackerOptions.messageResolver?: MessageResolver` (and equivalent on chunked
option bundles); default resolver looks up the local catalog.
- MODIFIED: every `throw new PackerError(CODE, "english string")` site now
resolves the message through the injected `MessageResolver`.
## Out of scope
- `@suaveplan/retry` / `@suaveplan/idempotency` integration → cluster #2 (resilience).
- Clock / Rng injection audit (no `Date.now()` / `Math.random()` at module
scope) → cluster #2.
- Whole-file `AbortSignal` propagation → cluster #2.
- `@suaveplan/schema-registry` integration → cluster #3 (extensibility).
- `@suaveplan/feature-flags` integration → cluster #3.
- Plugin / extension interface → cluster #3.
- `fast-check` property tests for round-trip invariants → cluster #3.
- mitata benchmarks → cluster #3.
## Source mapping
This change derives from `../suaveplan/enterprise/packer.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| PACKER-18 (BLAKE3 per-entry + Merkle root, mandatory verify) | L40 | Verbatim — "per-entry BLAKE3 plus bundle-level Merkle root; verification is mandatory on read". |
| PACKER-19 (`PACKER_INTEGRITY_FAILED`) | L45 | Reworded — audit line: "Structured errors … with stable error codes for integrity failures". |
| PACKER-20 (OpenTelemetry counters / histograms / spans) | L44 | Verbatim — "OpenTelemetry instrumentation via `@suaveplan/opentelemetry` for all public entrypoints (bundle size, entry count, pack/unpack latency)". |
| PACKER-21 (i18n-keyed messages, `MessageResolver` injection) | L49 | Verbatim — "i18n-ready — no hard-coded user-visible strings, all messages keyed". |
### Requirements with no source line
None — every requirement in this cluster maps directly to an audit bullet.
## Verification approach
- `src/integrity/integrity.test.ts` — pins BLAKE3 KAT + Merkle root vectors
(PACKER-18 primitive correctness).
- `src/integrity/integrity.spec-conformance.test.ts` — pins the wire-level
integrity-field shape on `PackedDataMetadata` and `ManifestChunkEntry`
(PACKER-18 surface).
- `src/packer/packer.spec-conformance.test.ts` (extended) — `pack` always
emits `integrity`; `unpack` rejects with `PACKER.INTEGRITY_FAILED` on
mismatch and absence (PACKER-18, PACKER-19).
- `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts` —
`unpackChunksWithManifest` verifies per-chunk BLAKE3 + manifest Merkle root
and rejects mutations (PACKER-18, PACKER-19).
- `src/observability/observability.spec-conformance.test.ts` — uses a real
in-memory `MeterProvider` + `InMemoryMetricExporter` and a real
`BasicTracerProvider` + `InMemorySpanExporter`, asserts every named counter
/ histogram / span fires from production paths (PACKER-20).
- `src/i18n/messages.spec-conformance.test.ts` — pins the catalog shape and
proves a custom `MessageResolver` is consulted at every throw site
(PACKER-21).
## Migration impact
Public-surface additions only. Two consumer packages (`@suaveplan/drive-plugin-chunks`,
`@suaveplan/drive-webrtc`) are audited and the sweep is part of the gates; no
existing call site changes shape. Whole-file `unpack` now rejects packed data
that lacks the new `integrity` field — this is the only consumer-visible
behavior change and is intentional per the cluster scope ("verification is
mandatory on read"). A `## Migration` section below names the rollover.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/packer` to enumerate.
- `@suaveplan/drive-plugin-chunks` — [x] tests run / [x] docs reviewed / [ ] no action needed
- `@suaveplan/drive-webrtc` — [x] tests run / [x] docs reviewed / [ ] no action needed
If `tests run` is unchecked for any dependent, the change MUST NOT be folded
into the canonical spec.
## Migration
- **Old behavior**: `Unpacker.unpack(packed)` succeeded on any `PackedData`
whose envelope verified through the existing signature / AEAD pipeline,
regardless of whether `metadata.integrity` was present.
- **New behavior**: `Unpacker.unpack(packed)` throws
`PackerError(PACKER.INTEGRITY_FAILED)` when `metadata.integrity` is missing
OR when the recomputed `(entryDigest, merkleRoot)` does not match the
envelope's recorded values.
- **Search command**: `grep -rln "Unpacker\|@suaveplan/packer" packages/ | grep -v "/dist/\|/coverage/"`.
- **Replacement**: regenerate stale packed bundles with `Packer.pack` (which
always emits `integrity`), or — for callers that intentionally ship
pre-integrity envelopes — pass `verifyIntegrity: false` on
`UnpackOptions` (additive, additive, additive: this opt-out exists to make
the rollover one-line; the default is verify).

View File

@@ -0,0 +1,190 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: packer (enterprise-packer-integrity)
**Package**: `@suaveplan/packer`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: PACKER-18 — BLAKE3 per-entry digest and bundle-level Merkle root
The package SHALL compute and surface a content-addressable BLAKE3 digest for
every entry written to a bundle, plus a bundle-level Merkle root computed over
the sorted entry digests. The Merkle root MUST be a 32-byte BLAKE3 digest of
the concatenation of the sorted entry digests in `chunkId`-ascending order;
for a single-entry bundle the Merkle root equals the lone entry digest; for an
empty bundle the Merkle root MUST be `BLAKE3("")` (the 32-byte all-zero-input
digest). Whole-file bundles carry `(entryDigest, merkleRoot)` on
`PackedDataMetadata.integrity`; chunked bundles carry per-chunk `entryDigest`
on every `ManifestChunkEntry` and `merkleRoot` at the manifest root. The
production paths `Packer.pack` and `buildManifest` MUST always populate these
fields.
#### Scenario: whole-file pack emits integrity
- **WHEN** `Packer.pack(data)` runs
- **THEN** the returned `PackedData.metadata.integrity` is present, its
`entryDigest` equals `blake3(payload)` of the wire-shaped payload bytes,
and its `merkleRoot` equals `entryDigest`
#### Scenario: chunked manifest emits per-entry digest and Merkle root
- **WHEN** `buildManifest({chunks, ...}, signKey)` runs
- **THEN** every `manifest.chunks[i].entryDigest` equals `blake3(wireBytes_i)`
and `manifest.merkleRoot` equals `blake3(concat(sort_by_chunkId(digests)))`
#### Scenario: empty bundle Merkle root is BLAKE3 of zero bytes
- **WHEN** `merkleRootBlake3([])` is called
- **THEN** the result equals `blake3(new Uint8Array(0))`
**Verified by:** `src/integrity/integrity.test.ts::"computes the BLAKE3 entry digest of payload bytes"` AND `src/integrity/integrity.test.ts::"computes the Merkle root as BLAKE3 of concatenated sorted digests"` AND `src/integrity/integrity.test.ts::"returns BLAKE3 of zero bytes for an empty digest list"` AND `src/packer/packer.spec-conformance.test.ts::"populates metadata.integrity on every pack"` AND `src/chunked/manifest/manifest.spec-conformance.test.ts::"populates entryDigest on every chunk entry and a manifest merkleRoot"`
---
### Requirement: PACKER-19 — Mandatory integrity verification on unpack
The package SHALL reject any unpack of a bundle whose recomputed integrity
digests do not match the bundle's recorded values. `Unpacker.unpack` MUST
throw `PackerError(PACKER.INTEGRITY_FAILED)` when `metadata.integrity` is
absent OR when the recomputed `entryDigest` or `merkleRoot` does not match.
`unpackChunksWithManifest` MUST throw `PackerError(PACKER.INTEGRITY_FAILED)`
for any chunk whose `blake3(wireBytes)` does not equal the matching
`manifest.chunks[i].entryDigest`, OR when the manifest's `merkleRoot` does
not equal `merkleRootBlake3(manifest.chunks.map(c => c.entryDigest))`.
Integrity verification MUST run on **both** the whole-file and chunked paths;
ed25519 signing remains in force in parallel.
#### Scenario: whole-file unpack rejects missing integrity
- **WHEN** `Unpacker.unpack(packed)` is called on a `PackedData` whose
`metadata.integrity` field is absent
- **THEN** it throws `PackerError(PACKER.INTEGRITY_FAILED)` and the resolved
message is the catalog value for `packer.error.INTEGRITY_FAILED.absent`
#### Scenario: whole-file unpack rejects entry-digest mutation
- **WHEN** `Unpacker.unpack(packed)` is called on a `PackedData` whose
`metadata.integrity.entryDigest` has been mutated by one bit
- **THEN** it throws `PackerError(PACKER.INTEGRITY_FAILED)` before yielding
the plaintext
#### Scenario: whole-file unpack rejects merkle-root mutation
- **WHEN** `Unpacker.unpack(packed)` is called on a `PackedData` whose
`metadata.integrity.merkleRoot` has been mutated by one bit
- **THEN** it throws `PackerError(PACKER.INTEGRITY_FAILED)`
#### Scenario: chunked unpack rejects per-chunk BLAKE3 mismatch
- **WHEN** `unpackChunksWithManifest(chunks, manifest, opts)` is consumed
fully and one chunk's wire bytes differ from `manifest.chunks[i].entryDigest`
- **THEN** the iterator throws `PackerError(PACKER.INTEGRITY_FAILED)` before
yielding plaintext for the mismatched chunk
#### Scenario: chunked unpack rejects merkle-root mismatch
- **WHEN** `unpackChunksWithManifest(chunks, manifest, opts)` is given a
manifest whose `merkleRoot` does not equal `merkleRootBlake3(...)` of its
entries
- **THEN** the iterator throws `PackerError(PACKER.INTEGRITY_FAILED)` before
consuming any chunk
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"rejects unpack when metadata.integrity is absent"` AND `src/packer/packer.spec-conformance.test.ts::"rejects unpack on mutated entryDigest"` AND `src/packer/packer.spec-conformance.test.ts::"rejects unpack on mutated merkleRoot"` AND `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts::"rejects per-chunk BLAKE3 mismatch with INTEGRITY_FAILED"` AND `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts::"rejects manifest with inconsistent merkleRoot"`
---
### Requirement: PACKER-20 — OpenTelemetry instrumentation on every public entrypoint
The package SHALL emit OpenTelemetry counters, histograms, and spans on every
public entrypoint via an injected `PackerObservability` bundle. The named
instruments are:
| Instrument | Type | Unit |
|---|---|---|
| `packer_bundle_size_bytes` | counter | `By` |
| `packer_entry_count` | counter | `1` |
| `packer_pack_total` | counter | `1` |
| `packer_unpack_total` | counter | `1` |
| `packer_integrity_failure_total` | counter | `1` |
| `packer_pack_duration_ms` | histogram | `ms` |
| `packer_unpack_duration_ms` | histogram | `ms` |
| `packer_entry_decode_ms` | histogram | `ms` |
Spans MUST wrap `Packer.pack`, `Unpacker.unpack`, `packChunks`,
`unpackChunks`, and `unpackChunksWithManifest`. When no observability bundle
is injected, the package MUST fall back to the no-op meter and tracer obtained
from `@opentelemetry/api` (`metrics.getMeter` / `trace.getTracer`); no
instrument call may throw.
#### Scenario: pack emits counters, histogram, and span
- **WHEN** `Packer.pack(data)` runs under a real in-memory `MeterProvider`
and `BasicTracerProvider`
- **THEN** the collected metrics include a `packer_pack_total` increment,
a `packer_bundle_size_bytes` increment equal to the output byte length,
a `packer_entry_count` increment of `1`, and a `packer_pack_duration_ms`
histogram observation; the collected spans include one whose name is
`packer.pack`
#### Scenario: unpack emits counters, histogram, and span
- **WHEN** `Unpacker.unpack(packed)` runs under a real in-memory provider
- **THEN** the collected metrics include `packer_unpack_total` (+1) and a
`packer_unpack_duration_ms` histogram observation; spans include `packer.unpack`
#### Scenario: integrity failure increments failure counter
- **WHEN** `Unpacker.unpack` rejects with `PACKER.INTEGRITY_FAILED`
- **THEN** `packer_integrity_failure_total` is incremented by 1 and the span
is recorded with status ERROR
#### Scenario: chunked paths emit spans and per-entry decode histogram
- **WHEN** `packChunks` / `unpackChunks` / `unpackChunksWithManifest` are
iterated to completion
- **THEN** spans `packer.pack_chunks` / `packer.unpack_chunks` /
`packer.unpack_chunks_with_manifest` are recorded and one
`packer_entry_decode_ms` observation is recorded per consumed chunk
**Verified by:** `src/observability/observability.spec-conformance.test.ts::"records packer_pack_total and packer_bundle_size_bytes on every pack"` AND `src/observability/observability.spec-conformance.test.ts::"records packer_unpack_total and a duration histogram on every unpack"` AND `src/observability/observability.spec-conformance.test.ts::"increments packer_integrity_failure_total when unpack rejects on integrity"` AND `src/observability/observability.spec-conformance.test.ts::"emits a packer.pack span captured by an in-memory span exporter"` AND `src/observability/observability.spec-conformance.test.ts::"emits a packer.unpack_chunks_with_manifest span and per-entry decode histogram observations"`
---
### Requirement: PACKER-21 — i18n-keyed error messages via injected MessageResolver
The package SHALL route every user-visible error message through an injected
`MessageResolver` (`@suaveplan/types`). A per-package English-defaults catalog
lives at `src/i18n/messages.ts` keyed by
`packer.error.<CODE>.<scenario>`. Constructor options
(`PackerOptions.messageResolver`, `PackChunkedOptions.messageResolver`,
`UnpackChunkedOptions.messageResolver`) accept a caller-supplied resolver;
when absent, the package falls back to `defaultPackerResolver` which reads
the local catalog. Every `throw new PackerError(CODE, message)` MUST pass a
resolver-returned string, never a hard-coded English literal.
#### Scenario: every catalog key is reachable from a throw site
- **WHEN** the test suite walks the catalog at `i18n/messages.ts`
- **THEN** every key is also a string literal passed to the resolver in at
least one `throw` site in `src/`
#### Scenario: custom resolver intercepts every throw
- **WHEN** an `Unpacker` is constructed with a custom `messageResolver` that
returns `"LOCALE_X:" + key` and `unpack` is called on inputs that trigger
every error path
- **THEN** every thrown `PackerError.message` is prefixed with `"LOCALE_X:"`
#### Scenario: default resolver returns the English catalog value
- **WHEN** a `Packer` is constructed without `messageResolver`
- **THEN** errors thrown by `pack` carry the English catalog string for the
triggered key
**Verified by:** `src/i18n/messages.spec-conformance.test.ts::"every key in the catalog is referenced from at least one throw site in src/"` AND `src/i18n/messages.spec-conformance.test.ts::"custom MessageResolver is consulted by Unpacker.unpack on every error path"` AND `src/i18n/messages.spec-conformance.test.ts::"default resolver returns the English catalog value when no resolver is injected"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,85 @@
# Tasks — Enterprise hardening (integrity cluster) for `@suaveplan/packer`
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/enterprise/packer.md` end-to-end
- [ ] 1.2 Read `openspec/specs/packer/spec.md`
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range
- [ ] 1.6 Draft `specs/packer/spec.md` from `openspec/templates/spec-delta.md`
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Implementation — Integrity (I-1)
- [ ] 2.1 Add `@noble/hashes` to `packer/package.json` deps (catalog:crypto)
- [ ] 2.2 New `src/integrity/integrity.ts` with `entryDigestBlake3(data)` and `merkleRootBlake3(digests)` + `.types.ts` + `.md` + barrel
- [ ] 2.3 Extend `PackedDataMetadata` with optional `integrity?: { entryDigest, merkleRoot }`
- [ ] 2.4 `Packer.pack` always populates `metadata.integrity`
- [ ] 2.5 `Unpacker.unpack` always verifies; throws `PackerError(PACKER.INTEGRITY_FAILED)` on mismatch / absence (unless `verifyIntegrity: false` is passed)
- [ ] 2.6 Extend `ManifestChunkEntry` with `entryDigest` (BLAKE3 of wire bytes), extend `Manifest` with `merkleRoot`
- [ ] 2.7 `buildManifest` populates both; `verifyManifest` covers them via canonical signature
- [ ] 2.8 Add `unpackChunksWithManifest(chunks, manifest, opts)` — verifies per-chunk BLAKE3 and manifest Merkle root before yielding plaintext
- [ ] 2.9 Register `PACKER_INTEGRITY_FAILED` in `defineCodes(...)` plus metadata row
## 3. Implementation — OTel (I-2)
- [ ] 3.1 Add `@opentelemetry/sdk-trace-base` + `@opentelemetry/sdk-metrics` as devDependencies (catalog:observability)
- [ ] 3.2 Add `@suaveplan/opentelemetry` to deps
- [ ] 3.3 New `src/observability/observability.ts` exporting `createPackerObservability(meter?, tracer?)` returning a `PackerObservability` with named instruments
- [ ] 3.4 Wire counters: `packer_bundle_size_bytes`, `packer_entry_count`, `packer_pack_total`, `packer_unpack_total`, `packer_integrity_failure_total`
- [ ] 3.5 Wire histograms: `packer_pack_duration_ms`, `packer_unpack_duration_ms`, `packer_entry_decode_ms`
- [ ] 3.6 Wrap public entrypoints `Packer.pack`, `Unpacker.unpack`, `packChunks`, `unpackChunks`, `unpackChunksWithManifest` in spans via `withSpan`
## 4. Implementation — i18n (I-3)
- [ ] 4.1 New `src/i18n/messages.ts` — English-defaults catalog keyed `packer.error.<code>.<scenario>`
- [ ] 4.2 Add `messageResolver?: MessageResolver` to `PackerOptions` and to chunked option bundles
- [ ] 4.3 Default resolver = `defaultPackerResolver` that walks the local catalog
- [ ] 4.4 Replace every hard-coded English string in `throw new PackerError(...)` across `src/` with a resolver call
## 5. Tests
- [ ] 5.1 `src/integrity/integrity.test.ts` — BLAKE3 + Merkle KAT vectors, edge cases (empty, single entry, 1 / 2 / 3 / N entries)
- [ ] 5.2 `src/integrity/integrity.spec-conformance.test.ts` — surface invariants
- [ ] 5.3 `src/packer/packer.spec-conformance.test.ts` extended — pack always writes integrity; unpack rejects on missing / mismatched / mutated integrity
- [ ] 5.4 `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts` — verifies per-chunk BLAKE3, Merkle root, rejects mutation
- [ ] 5.5 `src/observability/observability.spec-conformance.test.ts` — REAL in-memory `MeterProvider` + `InMemoryMetricExporter`, REAL `BasicTracerProvider` + `InMemorySpanExporter`; assert every named counter / histogram / span fires
- [ ] 5.6 `src/i18n/messages.spec-conformance.test.ts` — custom `MessageResolver` is consulted at every throw site; catalog covers every code+scenario keyed in src
- [ ] 5.7 Every `it(...)` carries `{ timeout: 5000 }`
## 6. Documentation
- [ ] 6.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
- [ ] 6.2 Update `README.md` Modules list with the three new feature folders
- [ ] 6.3 Update existing `.md`s touched by signature changes
## 7. Pre-archive verification (the 5-gate triple)
- [ ] 7.1 `bun run check` — Biome 0/0/0
- [ ] 7.2 `bunx turbo lint typecheck test --filter=@suaveplan/packer` — green
- [ ] 7.3 `bunx turbo test:coverage --filter=@suaveplan/packer` — 100/100/100/100
- [ ] 7.4 `bun scripts/openspec-conformance.ts @suaveplan/packer` — exit 0, no `error` findings
- [ ] 7.5 `bunx turbo build --filter=@suaveplan/packer` — green
## 8. Consumer sweep
- [ ] 8.1 `bunx turbo lint typecheck test --filter=@suaveplan/drive-plugin-chunks --filter=@suaveplan/drive-webrtc` — green
## 9. Pre-archive 9-row checklist
- [ ] 9.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim
- [ ] 9.2 Zod schemas wired at every declared boundary
- [ ] 9.3 Every error code in `error-codes.ts` thrown by at least one production path
- [ ] 9.4 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider` (NO `vi.mock`)
- [ ] 9.5 No `export *` in feature barrels
- [ ] 9.6 No direct `vitest` / `bun:test` imports
- [ ] 9.7 No retroactive tick — every box above reflects work done in this change
- [ ] 9.8 Sibling `.md` for every new `.ts`
- [ ] 9.9 Items I-1, I-2, I-3 each have ≥1 production code path and ≥1 test pinning every named scenario
## 10. Archive
- [ ] 10.1 Fold `specs/packer/spec.md` into `openspec/specs/packer/spec.md`
- [ ] 10.2 Move `openspec/changes/enterprise-packer-integrity/` → `openspec/archive/<yyyy-mm-dd>-enterprise-packer-integrity/`
- [ ] 10.3 Refresh `MANIFEST.md`