Files
suaveplan/openspec/archive/2026-05-03-bitpacker-hardening/specs/bitpacker/spec.md
2026-05-27 23:29:53 +01:00

95 lines
6.2 KiB
Markdown

# Capability Spec Delta: bitpacker (bitpacker-hardening)
**Package:** `@suaveplan/bitpacker` and the bitpacker family
**Status:** PROPOSED
> Every requirement carries a `**Verified by:**` line. All requirements are above-baseline genesis-policy additions; there is no upstream source for them. Every requirement is additive instrumentation or test infrastructure with no behaviour change.
## ADDED Requirements
### Requirement: Public types are Zod-validated at package boundaries
Every package in the bitpacker family SHALL expose a Zod schema for each public input type and SHALL validate untrusted inputs at the package boundary (the entry method that accepts a value from outside the package). Internal recursion MUST NOT re-validate.
#### Scenario: invalid input rejected at boundary
- **WHEN** a caller passes a value that does not match the boundary's Zod schema
- **THEN** the package throws a `BitpackerError` whose `cause` is the Zod error and whose code is `INVALID_SCHEMA`
#### Scenario: internal recursion does not re-validate
- **WHEN** the strategy chain recurses into a sub-schema that has already been validated at the entry boundary
- **THEN** no Zod schema is parsed during the recursion (verified by spying on the schema parser's call count)
**Verified by:** `packages/bitpacker/<each-pkg>/tests/zod-boundary.test.ts::"throws BitpackerError with INVALID_SCHEMA on bad input"` AND `::"does not re-validate during internal recursion"`
### Requirement: Public schemas registered with schema-registry
Every package in the bitpacker family SHALL register its public Zod schemas with `@suaveplan/schema-registry` under namespace `"bitpacker"`, with the package name as the schema name and `version: 1`.
#### Scenario: schemas present after import
- **WHEN** a consumer imports the package's `register-schemas` subpath
- **THEN** `schemaRegistry.get("bitpacker", "<pkg-name>", 1)` returns a populated entry
**Verified by:** `packages/bitpacker/<each-pkg>/tests/registered.test.ts::"registers schemas under namespace 'bitpacker'"`
### Requirement: OTel spans emitted at hot-path boundaries
Every package in the bitpacker family SHALL emit one OTel span per top-level pack/unpack call, named `suaveplan.bitpacker.<pkg>.<operation>`. Per-chunk events MUST be nested span events, not standalone spans.
#### Scenario: span emitted on pack
- **WHEN** a consumer calls a top-level pack/unpack method
- **THEN** exactly one span is emitted with the canonical name and attributes `{schema.kind, byte.count, success}`
**Verified by:** `packages/bitpacker/<each-pkg>/tests/otel.test.ts::"emits one span per top-level pack/unpack call"`
### Requirement: fast-check property tests cover round-trip soundness
Every package that exposes a pack/unpack pair SHALL ship a `tests/property/*.property.test.ts` file using `fast-check` to assert that for every value matching the schema's arbitrary, `unpack(pack(v)) ≡ v`.
#### Scenario: round-trip property holds for every public schema kind
- **WHEN** a fast-check arbitrary generates 100 values for a schema kind
- **THEN** every generated value round-trips byte-identically
**Verified by:** `packages/bitpacker/<each-pkg>/tests/property/round-trip.property.test.ts::"round-trip is identity for <kind>"`
### Requirement: mitata benchmarks have committed baselines and CI gate
Every package that exposes a public encoder/decoder SHALL ship a `bench/*.bench.ts` file using `mitata`, with baseline timings committed to `scripts/bench/bitpacker-baselines.json`. CI SHALL fail the `bench:check` task if any benchmark regresses by more than 10% from the committed baseline.
#### Scenario: regression caught by CI gate
- **WHEN** a code change makes a benchmarked operation 11%+ slower than its committed baseline
- **THEN** `bunx turbo bench:check` exits non-zero with the regressed operation's name and percentage
**Verified by:** `scripts/bench/check-baselines.test.ts::"flags regressions over 10%"`
### Requirement: `_version` strategy supports opt-in forward tolerance
The `VersionPackingStrategy` in `@suaveplan/bitpacker-strategies` SHALL accept an optional `_tolerance: "strict" | "forward"` field on the version-schema shape. When `_tolerance` is absent or `"strict"`, the strategy SHALL throw `BitpackerError(INVALID_SCHEMA)` on any wire-version mismatch (current behaviour). When `_tolerance: "forward"` is set and the schema also carries an `_allowedVersions: number[]`, the strategy SHALL accept any wire version listed in that array and proceed with the schema's `_schema` field for decoding. When `_tolerance: "forward"` is set without `_allowedVersions`, the strategy SHALL accept any wire version >= 0 and proceed with `_schema`. The on-wire byte format is unchanged: a 16-bit unsigned big-endian version followed by the schema-encoded payload.
#### Scenario: strict mode rejects mismatched wire version
- **WHEN** a consumer unpacks bytes whose wire version differs from the schema's `_version` and `_tolerance` is absent or `"strict"`
- **THEN** the strategy throws `BitpackerError(INVALID_SCHEMA)` with the expected and received versions in the error context
#### Scenario: forward-tolerant mode accepts a listed version
- **WHEN** a consumer unpacks bytes whose wire version is listed in `_allowedVersions` and `_tolerance: "forward"` is set
- **THEN** the strategy decodes the payload via `_schema` without throwing
#### Scenario: forward-tolerant mode without allowlist accepts any version
- **WHEN** a consumer unpacks bytes with `_tolerance: "forward"` and no `_allowedVersions` field
- **THEN** the strategy decodes any wire version >= 0 via `_schema`
#### Scenario: forward-tolerant mode rejects a version outside the allowlist
- **WHEN** a consumer unpacks bytes whose wire version is NOT in the supplied `_allowedVersions`, with `_tolerance: "forward"`
- **THEN** the strategy throws `BitpackerError(INVALID_SCHEMA)` with the expected version, the received version, and the allowed-versions list in the error context
**Verified by:** `packages/bitpacker/bitpacker-strategies/src/strategies/version/version.test.ts::"throws on strict mismatch"` AND `::"accepts a listed version when _tolerance: forward"` AND `::"accepts any version when _tolerance: forward without allowlist"` AND `::"rejects an unlisted version under _tolerance: forward"`