Initial Commit
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<!-- 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 -->
|
||||
Reference in New Issue
Block a user