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

226 lines
12 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: packer (enterprise-packer-extensibility)
**Package**: `@suaveplan/packer`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: 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.
#### Scenario: getPackerSchemaRegistry returns a populated singleton
- **WHEN** `getPackerSchemaRegistry()` is called and then
`registry.listSubjects()` is read
- **THEN** the list contains exactly `PackerOptions`, `UnpackOptions`,
`ChunkedPackerOptions`, `Manifest`, and `packer-chunk-format`
#### Scenario: packer-chunk-format records V1 with evolution metadata
- **WHEN** `registry.getLatest("packer-chunk-format")` is read
- **THEN** `metadata.formatVersion === "1"`, `metadata.encoding ===
"binary"`, and registering an identical V1 candidate succeeds under
`BACKWARD` compatibility
#### Scenario: registerPackerSchemas writes into a caller-supplied registry
- **WHEN** `registerPackerSchemas(externalRegistry)` is called against
a fresh `createRegistry()`
- **THEN** `externalRegistry.listSubjects()` contains the same five
subjects with `formatVersion: "1"` metadata, and no mutation to the
global singleton has occurred
**Verified by:** `src/schemas/registration.test.ts::"registers every option schema as a subject with formatVersion 1 metadata"` AND `src/schemas/registration.test.ts::"records packer-chunk-format V1 with binary encoding and BACKWARD compatibility"` AND `src/schemas/registration.spec-conformance.test.ts::"getPackerSchemaRegistry returns a process-singleton populated with the canonical subjects"` AND `src/schemas/registration.spec-conformance.test.ts::"registerPackerSchemas writes into a caller-supplied registry without mutating the global singleton"`
---
### Requirement: 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? })`.
#### Scenario: default flags resolve V2 to false and pack succeeds in V1
- **WHEN** `new Packer().pack(data)` is called with no `flags` option
- **THEN** the call returns a `PackedData` whose `metadata` carries the
V1 shape (no V2-specific fields), and `defaultPackerFlags().get("packer.format.v2") === false`
#### Scenario: enabling packer.format.v2 throws FORMAT_VERSION_UNSUPPORTED
- **WHEN** `Packer.pack(data)` is called with a `flags` provider whose
`get("packer.format.v2")` returns `true`
- **THEN** the call rejects with `PackerError(PACKER.FORMAT_VERSION_UNSUPPORTED)`
and `error.code === "PACKER.FORMAT_VERSION_UNSUPPORTED"`
#### Scenario: a caller-supplied feature-flags Evaluator drives the gate
- **WHEN** `Packer.pack(data)` is called with `flags: { evaluator,
context: { userId: "u-1" } }` where `evaluator` resolves
`packer.format.v2` to `false` for `userId: "u-1"`
- **THEN** the call returns a V1 bundle and the evaluator's internal
counter records one evaluation for `flag_key: "packer.format.v2"`
**Verified by:** `src/flags/flags.test.ts::"defaultPackerFlags returns false for packer.format.v2"` AND `src/flags/flags.test.ts::"resolvePackerFlags adapts a feature-flags Evaluator into a PackerFeatureFlags provider"` AND `src/flags/flags.spec-conformance.test.ts::"Packer.pack throws PACKER.FORMAT_VERSION_UNSUPPORTED when packer.format.v2 is enabled"` AND `src/flags/flags.spec-conformance.test.ts::"Packer.pack succeeds in V1 when flags option is omitted"` AND `src/flags/flags.spec-conformance.test.ts::"a caller-supplied feature-flags Evaluator drives the format-version gate"`
---
### Requirement: 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`.
#### Scenario: plugin hooks fire in declaration order
- **WHEN** `Packer.pack(data)` is called with two plugins whose
`onPackStart` / `onEntry` / `onPackEnd` push their name onto a
shared array
- **THEN** the array equals `["A:start", "B:start", "A:entry",
"B:entry", "A:end", "B:end"]`
#### Scenario: an unpack pass fires onUnpackStart / onEntryDecoded / onUnpackEnd
- **WHEN** `Unpacker.unpack(packed)` is called with a plugin that
records each unpack hook
- **THEN** the recorded order is `["unpackStart", "entryDecoded",
"unpackEnd"]`
#### Scenario: a plugin error surfaces as PACKER.PLUGIN_FAILED
- **WHEN** `Packer.pack(data)` is called with a plugin whose
`onEntry` throws `new Error("boom")`
- **THEN** the call rejects with
`PackerError(PACKER.PLUGIN_FAILED)`, `error.cause.message === "boom"`,
and `error.message` is the resolved
`packer.error.PLUGIN_FAILED.onEntry` catalog string
#### Scenario: a double-integrity sidecar plugin contributes SHA-256
- **WHEN** `Packer.pack(data)` is called with a plugin whose `onEntry`
computes SHA-256 of the payload and stashes it on the plugin's
internal map, then `Unpacker.unpack` is called with the same plugin
whose `onEntryDecoded` reads back and asserts equality
- **THEN** both calls succeed and the plugin observed identical SHA-256
bytes on the way out and the way in
**Verified by:** `src/plugins/plugins.test.ts::"runs onPackStart / onEntry / onPackEnd in declaration order"` AND `src/plugins/plugins.test.ts::"runs onUnpackStart / onEntryDecoded / onUnpackEnd in declaration order"` AND `src/plugins/plugins.test.ts::"wraps thrown plugin errors as PackerError(PACKER.PLUGIN_FAILED) with cause"` AND `src/plugins/plugins.spec-conformance.test.ts::"a noop-logger plugin observes every lifecycle hook"` AND `src/plugins/plugins.spec-conformance.test.ts::"a double-integrity sidecar plugin contributes SHA-256 alongside BLAKE3"`
---
### Requirement: 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 50 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`).
#### Scenario: round-trip identity over arbitrary Uint8Array inputs
- **WHEN** `fc.assert(fc.asyncProperty(fc.uint8Array({ maxLength: 1024 }), async (data) => { … pack/unpack … }))` runs
- **THEN** every drawn `data` is recovered byte-identical from
`unpack(pack(data))`
#### Scenario: streaming chunked pack matches whole-buffer chunked pack
- **WHEN** the same input is fed to `packChunks` once as a
whole-buffer source and once as a 1-byte-at-a-time async-iterable
source, with identical options
- **THEN** concatenated wire bytes are byte-identical between the two
runs
#### Scenario: seeded Rng + fixed Clock produces byte-identical bundles
- **WHEN** two `Packer.pack(data)` calls run with the same `data`, the
same seeded `Rng`, the same fixed `Clock`, and identical encryption
options
- **THEN** both `result.data` and `result.metadata.integrity.entryDigest`
bytes are equal
**Verified by:** `src/__property__/round-trip.property.test.ts::"pack then unpack recovers the original bytes for arbitrary inputs"` AND `src/__property__/round-trip.property.test.ts::"chunked streaming pack matches whole-buffer chunked pack byte-for-byte"` AND `src/__property__/determinism.property.test.ts::"two packs with identical inputs and seeded Rng produce byte-identical bundles"`
---
### Requirement: 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.
#### Scenario: bun run benchmark executes the mitata suite
- **WHEN** `bun run benchmark` is invoked at the package root
- **THEN** the suite completes without throwing and emits mitata's
standard ops/sec summary for every named case
#### Scenario: every named case is registered with mitata
- **WHEN** the benchmark module is statically inspected for `bench(`
invocations
- **THEN** there are at least six distinct `bench("...")` calls
matching the named cases (whole-file pack, whole-file unpack,
chunked single, chunked 1000, integrity verify, plugin overhead)
**Verified by:** `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"declares the six named benchmark cases"` AND `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"package.json declares a bun run benchmark script"` AND `src/benchmarks-conformance/packer.bench.spec-conformance.test.ts::"README.md publishes a Benchmarks section with a hardware context line"`
<!-- markdownlint-enable MD024 -->