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,192 @@
# Capability Spec: packer
- **Capability**: `packer`
- **Package**: `@suaveplan/packer`
- **Change**: packer-port
- **Date**: 2026-04-28
---
## Requirements
All requirements in this section are new for genesis. The package is a partial rewrite of `../suaveplan/packages/universal/packer` v2.0.0; whole-file mode preserves the source's pipeline shape with a simplified envelope, while chunked mode is rewritten to emit the locked v1 byte format pinned below.
### PACKER-01 — Whole-file pack/unpack round-trip
`Packer.pack(data, filename?)` followed by `Unpacker.unpack(packed, key, pubKey?)` MUST yield bytes equal to the original `data`. Optional MIME detection MUST surface in the returned metadata.
**Verified by:** `src/unpacker/unpacker.spec-conformance.test.ts::"round-trips uncompressed unencrypted bytes"`.
### PACKER-02 — Whole-file gzip compression
When `compressionLevel > 0`, `pack` MUST gzip the payload before AEAD; `unpack` MUST gunzip after AEAD. The `metadata.compressionAlgorithm` MUST surface as `"gzip"` when compression ran.
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"compresses when compressionLevel > 0"`; `src/unpacker/unpacker.spec-conformance.test.ts::"round-trips compressed bytes"`.
### PACKER-03 — Whole-file signing
When a `signing.key` is provided to `pack`, the produced `PackedData.metadata` MUST carry `signed: true` and a 64-byte ed25519 signature over the post-encryption ciphertext. `unpack` with `verifySignature: true` and a 32-byte `verifyKey` MUST verify the signature and throw `PackerError(PACKER.VERIFICATION_FAILED)` on mismatch.
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"appends an ed25519 signature when signing.key supplied"`; `src/unpacker/unpacker.spec-conformance.test.ts::"verifySignature with wrong key returns VERIFICATION_FAILED"`.
### PACKER-04 — Whole-file event lifecycle
`Packer` MUST emit typed events via `EventBus<PackerEventMap>`: `pack:start`, `pack:compressed`, `pack:encrypted`, `pack:signed`, `pack:complete`, with corresponding `unpack:*` events on the unpacker. Events MUST fire in deterministic order on completion.
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"emits pack:start, pack:compressed, pack:encrypted, pack:signed, pack:complete in order"`; `src/unpacker/unpacker.spec-conformance.test.ts::"emits unpack:start, unpack:verified, unpack:decrypted, unpack:decompressed, unpack:complete in order"`.
### PACKER-05 — Chunked v1 byte layout
A v1 chunk produced by `ChunkedPacker` MUST be the byte concatenation `metadata(100) ‖ sig(64) ‖ ciphertext(payloadLen)`, total `164 + payloadLen` bytes, big-endian. Field offsets within metadata MUST be exactly: `version` 0–2, `fileId` 2–34, `chunkId` 34–38, `totalChunks` 38–42, `fileSize` 42–50, `chunkSize` 50–54, `payloadLen` 54–58, `flags` 58–60, `nonce` 60–84, `aeadTag` 84–100. Sig occupies 100–164. Ciphertext starts at 164.
**Verified by:** `src/chunked/codec/chunk.spec-conformance.test.ts::"metadata field offsets match the locked layout"`.
### PACKER-06 — `flags` reserved in v1
`flags` MUST be `0x0000` in v1. `parseHeader` MUST reject non-zero `flags` with `PackerError(PACKER.FLAGS_RESERVED_NONZERO)` before any further parse work.
**Verified by:** `src/chunked/codec/chunk.spec-conformance.test.ts::"parseHeader rejects flags != 0 with FLAGS_RESERVED_NONZERO"`.
### PACKER-07 — `payloadLen` invariants
For non-final chunks (`chunkId < totalChunks - 1`), `payloadLen` MUST equal `chunkSize - 164`. For the final chunk, `payloadLen` MUST equal `fileSize - chunkId * (chunkSize - 164)`. Mismatch MUST be rejected with `PackerError(PACKER.PAYLOAD_LEN_INVALID)` before sig verification.
**Verified by:** `src/chunked/codec/chunk.spec-conformance.test.ts::"non-final chunk MUST have payloadLen === chunkSize - 164"`; `src/chunked/codec/chunk.spec-conformance.test.ts::"final chunk MUST have payloadLen === fileSize - chunkId * (chunkSize - 164)"`.
### PACKER-08 — Streaming pipeline produces v1 chunks
`ChunkedPacker.pack(source, opts)` MUST stream-process the input via `normalizeSource → accumulate → encrypt → sign → emit`. The output MUST be an `AsyncIterable<{ chunkId, bytes, metadata }>` where each `bytes` is a v1-format chunk. The full source MUST NOT be buffered as a single in-memory copy.
**Verified by:** `src/chunked/packer/packer.spec-conformance.test.ts::"round-trips multi-chunk file with short final chunk"`.
### PACKER-09 — Non-final chunks are exactly `chunkSize` bytes
Every emitted chunk with `chunkId < totalChunks - 1` MUST be exactly `chunkSize` bytes on the wire. No zero padding is added.
**Verified by:** `src/chunked/packer/packer.spec-conformance.test.ts::"round-trips multi-chunk file with short final chunk"`; `src/chunked/packer/packer.spec-conformance.test.ts::"round-trips file whose size is exact multiple of payloadPer"`.
### PACKER-10 — Final chunk is naturally short
The final chunk MUST be `164 + lastPayloadLen` bytes, where `lastPayloadLen = fileSize - (totalChunks - 1) * (chunkSize - 164)`. The final chunk MUST NOT be zero-padded to `chunkSize`.
**Verified by:** `src/chunked/packer/packer.spec-conformance.test.ts::"round-trips multi-chunk file with short final chunk"`.
### PACKER-11 — Round-trip parity
`ChunkedUnpacker.unpack(chunks, fileKey, serverPubKey)` over the chunks emitted by `ChunkedPacker.pack(...)` MUST yield a plaintext stream byte-equal to the original input. Order MUST be `chunkId` ascending; out-of-order delivery is the consumer's responsibility to reorder before passing to unpacker.
**Verified by:** `src/chunked/unpacker/unpacker.spec-conformance.test.ts::"decrypts multi-chunk stream in order"`.
### PACKER-12 — Mutation detection
Any mutation to any signed byte MUST cause `ChunkedUnpacker.unpack` to throw `PackerError(PACKER.VERIFICATION_FAILED)` from the per-chunk sig-verify step before AEAD decryption is attempted.
**Verified by:** `src/chunked/unpacker/unpacker.spec-conformance.test.ts::"rejects chunk signed by a different key"`.
### PACKER-13 — Verifier without file key
`ChunkVerifier.verify(chunkBytes, serverPubKey)` MUST validate the structural invariants and the ed25519 signature without the file decryption key. It MUST return `{ ok: true, metadata }` for valid chunks and `{ ok: false, code }` for invalid chunks. It MUST NOT attempt AEAD decryption.
**Verified by:** `src/chunked/verifier/chunk-verifier.spec-conformance.test.ts::"returns ok for a properly-signed chunk"`.
### PACKER-14 — Verifier rejects forged sig
`ChunkVerifier.verify` MUST return `{ ok: false, code: "PACKER.VERIFICATION_FAILED" }` for a chunk whose signature does not validate under `serverPubKey`. Verifier MUST NOT consult the chunks plugin or any persistence layer.
**Verified by:** `src/chunked/verifier/chunk-verifier.spec-conformance.test.ts::"returns VERIFICATION_FAILED for tampered metadata"`; `src/chunked/verifier/chunk-verifier.spec-conformance.test.ts::"returns VERIFICATION_FAILED for chunk signed by a different key"`.
### PACKER-15 — Manifest signature
The manifest produced by `buildManifest(...)` MUST be ed25519-signed via the canonical encoding of every field except `sig` itself. `verifyManifest(manifest, serverPubKey)` MUST return `false` for any manifest whose signature does not validate; mutation of any signed field MUST cause verification failure.
**Verified by:** `src/chunked/manifest/manifest.spec-conformance.test.ts::"signs and verifies a manifest"`; `src/chunked/manifest/manifest.spec-conformance.test.ts::"verify returns false on tampered field"`.
### PACKER-16 — Manifest matches chunk metadata
`buildManifest` MUST produce a manifest where `totalChunks === chunks.length`, `version === 1`, and the per-chunk entries (`chunkId`, `hash`, `payloadLen`) match the inputs. The `sig` field MUST be 64 bytes.
**Verified by:** `src/chunked/manifest/manifest.spec-conformance.test.ts::"signs and verifies a manifest"`.
### PACKER-17 — Error registry namespaced and frozen
`PACKER_CODES` MUST be declared via `defineCodes("PACKER", [...])` from `@suaveplan/error`. `Object.isFrozen(PACKER_CODES)` MUST be `true`. Every code value MUST start with `PACKER.`. `PackerError` MUST extend `SuaveplanError<PackerErrorCode>`.
**Verified by:** `src/error-codes.spec-conformance.test.ts::"PACKER_CODES is frozen"`; `src/error-codes.spec-conformance.test.ts::"every code value starts with PACKER."`; `src/error-codes.spec-conformance.test.ts::"PackerError extends SuaveplanError"`.
### 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 `blake3(entryDigest)` (domain-separated wrap of the lone leaf); for an empty bundle the Merkle root MUST be `blake3("")` (the 32-byte digest of zero input bytes). 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.
**Verified by:** `src/integrity/integrity.test.ts::"computes the BLAKE3 entry digest of payload bytes"`; `src/integrity/integrity.test.ts::"computes the Merkle root as BLAKE3 of concatenated sorted digests"`; `src/integrity/integrity.test.ts::"returns BLAKE3 of zero bytes for an empty digest list"`; `src/packer/packer.spec-conformance.test.ts::"populates metadata.integrity on every pack"`; `src/chunked/manifest/manifest.spec-conformance.test.ts::"populates entryDigest on every chunk entry and a manifest merkleRoot"`.
### 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. Callers MAY pass `verifyIntegrity: false` on `UnpackOptions` as a documented escape hatch for legacy bundles produced before the integrity layer existed.
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"rejects unpack when metadata.integrity is absent"`; `src/packer/packer.spec-conformance.test.ts::"rejects unpack on mutated entryDigest"`; `src/packer/packer.spec-conformance.test.ts::"rejects unpack on mutated merkleRoot"`; `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts::"rejects per-chunk BLAKE3 mismatch with INTEGRITY_FAILED"`; `src/chunked/unpacker/unpacker-with-manifest.spec-conformance.test.ts::"rejects manifest with inconsistent merkleRoot"`.
### 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 `packer_bundle_size_bytes`, `packer_entry_count`, `packer_pack_total`, `packer_unpack_total`, `packer_integrity_failure_total` (counters), and `packer_pack_duration_ms`, `packer_unpack_duration_ms`, `packer_entry_decode_ms` (histograms, unit `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.
**Verified by:** `src/observability/observability.spec-conformance.test.ts::"records packer_pack_total and packer_bundle_size_bytes on every pack"`; `src/observability/observability.spec-conformance.test.ts::"records packer_unpack_total and a duration histogram on every unpack"`; `src/observability/observability.spec-conformance.test.ts::"increments packer_integrity_failure_total when unpack rejects on integrity"`; `src/observability/observability.spec-conformance.test.ts::"emits a packer.pack span captured by an in-memory span exporter"`; `src/observability/observability.spec-conformance.test.ts::"emits a packer.unpack_chunks_with_manifest span and per-entry decode histogram observations"`.
### 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)` site MUST pass a resolver-returned string, never a hard-coded English literal. The catalog MUST be `Object.freeze`d and every catalog key MUST be reachable from at least one `throw` site in `src/`.
**Verified by:** `src/i18n/messages.spec-conformance.test.ts::"every key in the catalog is referenced from at least one throw site in src/"`; `src/i18n/messages.spec-conformance.test.ts::"custom MessageResolver is consulted by Unpacker.unpack on every error path"`; `src/i18n/messages.spec-conformance.test.ts::"default resolver returns the English catalog value when no resolver is injected"`.
### PACKER-22 — `@suaveplan/retry` on IO-backed source reads
The package SHALL route every IO-backed source pull (every `reader.read()` call against a `ReadableStream` consumed by `normalizeSource` or the abstract `collect` helper) through `@suaveplan/retry`'s `withRetry`. The retry policy MUST be configurable per call via a `retryConfig?: RetryConfig` option on `PackerOptions`, `PackChunkedOptions`, and `UnpackChunkedOptions`. When `retryConfig` is absent the package MUST default to a single-attempt policy (`maxAttempts: 1`) so wire behaviour matches cluster #1. When `retryConfig` is supplied the retry loop MUST honour the caller's `AbortSignal`: cancellation MUST abort the retry loop immediately (no further attempts, no further sleeps) and surface as a `RetryAbortedError`.
**Verified by:** `src/retry/retryable-read.test.ts::"retries up to maxAttempts on a failing reader"`; `src/retry/retryable-read.test.ts::"cancels the retry loop when AbortSignal fires mid-backoff"`; `src/retry/retryable-read.spec-conformance.test.ts::"default policy is a single attempt with no retries"`; `src/retry/retryable-read.spec-conformance.test.ts::"retryRead surfaces RetryAbortedError when signal fires before first read"`.
### PACKER-23 — Idempotent repeat-pack short-circuit
The package SHALL expose an injectable idempotency cache on `PackerOptions.idempotency` (`{ store, ttlMs?, enabled? }`). When enabled, `Packer.pack(data, opts?)` MUST compute a content-derived dedup key from `blake3(data)` plus the packer's pipeline parameters (`compressionLevel`, `encryption.algorithm`, `signing` enabled bit), look the key up in the supplied `IdempotencyStoreAdapter`, and on hit return the previously-built `PackedData` without re-running the gzip / AEAD / sign / integrity pipeline. On miss the pipeline MUST run as before and the resulting `PackedData` MUST be written back to the store under the same key with the configured TTL (default `86_400_000` ms / 24 h). Idempotency MUST be opt-in: callers that do not pass `idempotency` see the legacy non-cached behaviour.
**Verified by:** `src/idempotency/idempotency.test.ts::"second pack with identical input short-circuits to cached PackedData"`; `src/idempotency/idempotency.test.ts::"cache miss populates the store with the resulting PackedData"`; `src/idempotency/idempotency.spec-conformance.test.ts::"idempotency is disabled by default"`; `src/idempotency/idempotency.spec-conformance.test.ts::"custom IdempotencyStoreAdapter is consulted exactly once per pack"`.
### PACKER-24 — Whole-file `AbortSignal` propagation
The package SHALL accept an `AbortSignal` on every async public entrypoint and MUST check the signal at every yield point. `Unpacker.unpack` accepts `UnpackOptions.signal?` and MUST check it before integrity verify, before signature verify, before AEAD decrypt, before gunzip, and before the final emit. `Packer.pack` accepts an additional overload `pack(data, opts: PackOptions)` where `PackOptions.signal?` is checked before compression, before encryption, before signing, before integrity compute, and before the final return. `packChunks` accepts `PackChunkedOptions.signal?` and MUST check it on every loop iteration before yielding the next packed chunk. Aborted operations MUST throw `PackerError(PACKER.ABORTED)` with the i18n catalog message `packer.error.ABORTED.<entrypoint>`. The original `Packer.pack(data, filename?)` overload MUST remain a valid call shape for back-compat.
**Verified by:** `src/unpacker/unpacker.spec-conformance.test.ts::"rejects with PACKER.ABORTED when signal is pre-aborted"`; `src/unpacker/unpacker.spec-conformance.test.ts::"rejects with PACKER.ABORTED when signal fires between pipeline steps"`; `src/packer/packer.spec-conformance.test.ts::"rejects with PACKER.ABORTED when opts.signal is pre-aborted"`; `src/chunked/packer/packer.spec-conformance.test.ts::"aborts mid-stream when signal fires after the second chunk"`; `src/packer/packer.spec-conformance.test.ts::"legacy pack(data, filename) overload still surfaces mimeType"`.
### PACKER-25 — Injected `Clock` and `Rng` everywhere
The package SHALL accept `clock?: Clock` and `rng?: Rng` (types from `@suaveplan/types`) on every public option bundle (`PackerOptions`, `PackChunkedOptions`, `UnpackChunkedOptions`). Every production read of wall-clock time MUST go through the resolved `Clock` (no direct `Date.now()`); every production source of randomness MUST go through the resolved `Rng` (no direct `Math.random()` / `crypto.getRandomValues`); every elapsed-time measurement MUST go through an injected `monotonicClock?: MonotonicClock` (defaults to a wrapper around `performance.now()`). The defaults MUST produce byte-identical wire output to the cluster #1 behaviour. Determinism: two `Packer.pack` calls with identical inputs, identical options, and a seeded `Rng` MUST produce byte-identical `PackedData.data` and identical `metadata.integrity` values.
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"timestamp is sourced from the injected Clock"`; `src/packer/packer.spec-conformance.test.ts::"AEAD nonce is sourced from the injected Rng"`; `src/packer/packer.spec-conformance.test.ts::"two packs with identical inputs and seeded Rng produce byte-identical output"`; `src/runtime/runtime.spec-conformance.test.ts::"no production file calls Date.now / Math.random / crypto.getRandomValues directly"`.
### PACKER-26 — `@suaveplan/schema-registry` integration
The package SHALL register every Zod schema in `src/schemas/packer.schemas.ts` (`PackerOptionsSchema`, `UnpackOptionsSchema`, `ChunkedPackerOptionsSchema`, `ManifestSchema`) plus a `packer-chunk-format` subject capturing the wire-format version byte, into a process-singleton `SchemaRegistry` exposed via `getPackerSchemaRegistry()`. The `packer-chunk-format` subject MUST register V1 (the current wire format) under a `BACKWARD` compatibility policy with evolution metadata `{ formatVersion: "1", encoding: "binary", description: "v1 fixed-width 164-byte header + AEAD payload" }` so a future V2 entry can be added without disturbing V1. Every other schema MUST be registered under its own subject (`PackerOptions`, `UnpackOptions`, `ChunkedPackerOptions`, `Manifest`) carrying the metadata `{ formatVersion: "1" }`. A `registerPackerSchemas(registry)` function MUST allow callers to register the same schemas into any external `SchemaRegistry` instance for cross-package validation.
**Verified by:** `src/schemas/registration.test.ts::"registers every option schema as a subject with formatVersion 1 metadata"`; `src/schemas/registration.test.ts::"records packer-chunk-format V1 with binary encoding and BACKWARD compatibility"`; `src/schemas/registration.spec-conformance.test.ts::"getPackerSchemaRegistry returns a process-singleton populated with the canonical subjects"`; `src/schemas/registration.spec-conformance.test.ts::"registerPackerSchemas writes into a caller-supplied registry without mutating the global singleton"`.
### PACKER-27 — `@suaveplan/feature-flags` for format-version rollouts
The package SHALL accept a `flags?: PackerFlagsOptions` bundle on `PackerOptions` and route the wire-format selection through the resolved `PackerFeatureFlags` provider. The exported constant `PACKER_FLAG_KEYS.FORMAT_V2 === "packer.format.v2"` MUST identify the V2 rollout flag. When no provider is supplied, `defaultPackerFlags()` MUST act as a no-op that returns the V1 default (`false`) for every flag. When the resolved provider reports `packer.format.v2 === true`, `Packer.pack` MUST throw `PackerError(PACKER.FORMAT_VERSION_UNSUPPORTED)` with the catalog message `packer.error.FORMAT_VERSION_UNSUPPORTED.v2-not-implemented`; this is a versioning gate — the V2 codec is a future change. A caller-supplied `@suaveplan/feature-flags` `Evaluator` MUST be acceptable as the provider via `resolvePackerFlags({ evaluator, context? })`.
**Verified by:** `src/flags/flags.test.ts::"defaultPackerFlags returns false for packer.format.v2"`; `src/flags/flags.test.ts::"resolvePackerFlags adapts a feature-flags Evaluator into a PackerFeatureFlags provider"`; `src/flags/flags.spec-conformance.test.ts::"Packer.pack throws PACKER.FORMAT_VERSION_UNSUPPORTED when packer.format.v2 is enabled"`; `src/flags/flags.spec-conformance.test.ts::"Packer.pack succeeds in V1 when flags option is omitted"`; `src/flags/flags.spec-conformance.test.ts::"a caller-supplied feature-flags Evaluator drives the format-version gate"`.
### PACKER-28 — Plugin / extension interface
The package SHALL expose a `PackerPlugin` contract carrying six lifecycle hooks (`onPackStart(ctx)`, `onEntry(ctx)`, `onPackEnd(ctx)`, `onUnpackStart(ctx)`, `onEntryDecoded(ctx)`, `onUnpackEnd(ctx)`), each optional and each invoked in plugin-declaration order. Plugins are registered via `plugins?: ReadonlyArray<PackerPlugin>` on `PackerOptions`. `Packer.pack` MUST invoke `onPackStart` exactly once before the pipeline, `onEntry` exactly once per entry packed (one for whole-file mode), and `onPackEnd` exactly once after the final result is composed. `Unpacker.unpack` MUST invoke `onUnpackStart` / `onEntryDecoded` / `onUnpackEnd` symmetrically. Any plugin error MUST be caught and re-thrown as `PackerError(PACKER.PLUGIN_FAILED)` with a catalog message `packer.error.PLUGIN_FAILED.<hook>` and the original error carried on `error.cause`.
**Verified by:** `src/plugins/plugins.test.ts::"runs onPackStart / onEntry / onPackEnd in declaration order"`; `src/plugins/plugins.test.ts::"runs onUnpackStart / onEntryDecoded / onUnpackEnd in declaration order"`; `src/plugins/plugins.test.ts::"wraps thrown plugin errors as PackerError(PACKER.PLUGIN_FAILED) with cause"`; `src/plugins/plugins.spec-conformance.test.ts::"a noop-logger plugin observes every lifecycle hook"`; `src/plugins/plugins.spec-conformance.test.ts::"a double-integrity sidecar plugin contributes SHA-256 alongside BLAKE3"`.
### PACKER-29 — `fast-check` property-based round-trip + determinism
The package SHALL include `fast-check` property tests pinning the pack/unpack round-trip identity, streaming↔batch equivalence, and seeded-Rng determinism invariants. Property assertions MUST run over arbitrary `Uint8Array` inputs of length 0..1024 (at least 25 runs per property). Round-trip identity: for every `data`, `unpack(pack(data))` returns bytes byte-equal to `data`. Streaming equivalence: the concatenation of every `PackedChunk.bytes` emitted by `packChunks(asyncIter(data), { ...same fileKey/signKey/baseNonce })` MUST byte-equal the wire bytes of `packChunks(data, { ...same opts })` consumed from a whole-buffer source. Determinism: two `Packer.pack` runs with identical inputs, identical options, the same seeded `Rng` and the same fixed `Clock` MUST produce byte-identical `PackedData` (both `data` and every byte of `metadata.integrity`).
**Verified by:** `src/__property__/round-trip.property.test.ts::"pack then unpack recovers the original bytes for arbitrary inputs"`; `src/__property__/round-trip.property.test.ts::"chunked streaming pack matches whole-buffer chunked pack byte-for-byte"`; `src/__property__/determinism.property.test.ts::"two packs with identical inputs and seeded Rng produce byte-identical bundles"`.
### PACKER-30 — mitata benchmark suite
The package SHALL ship a runnable mitata benchmark suite at `src/__benchmarks__/packer.bench.ts` covering: (a) whole-file `Packer.pack` over a fixed 1 KiB payload, (b) whole-file `Unpacker.unpack` over the result of (a), (c) chunked `packChunks` over a single-chunk source, (d) chunked `packChunks` over a 1000-chunk source, (e) `computeWholeFileIntegrity` over a fixed 1 KiB payload, and (f) a plugin-overhead comparison (`Packer.pack` with zero plugins vs. with a no-op plugin). The benchmark MUST be runnable via `bun run benchmark` declared in `package.json` and MUST execute without error. Baseline numbers MUST be published in `README.md` under a `## Benchmarks` section with a hardware/runtime context line.
**Verified by:** `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"declares the six named benchmark cases"`; `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"package.json declares a bun run benchmark script"`; `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"README.md publishes a Benchmarks section with a hardware context line"`.