Files
suaveplan/openspec/specs/packer/spec.md
2026-05-27 23:29:53 +01:00

26 KiB
Raw Blame History

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.freezed 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".