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

9.4 KiB

bitpacker-hardening — Zod surface, OTel, property tests, benchmarks

Packages: @suaveplan/bitpacker, @suaveplan/bitpacker-strategies, @suaveplan/bitpacker-validation, @suaveplan/bitpacker-game, @suaveplan/bitpacker-streaming, @suaveplan/bitpacker-compressed, @suaveplan/bitpacker-network, @suaveplan/bitpacker-schema, @suaveplan/bitpacker-delta Status: PROPOSED Type: hardening (no behaviour changes; no public-surface breaks) Carved from: bitpacker-full-port triage on 2026-05-03 Scope decisions baked in (2026-05-03):

  • Feature-flags integration is DROPPED from this change. Every policy choice in the bitpacker family is already JSON-configurable (compression algorithm name + options object, network simulation profile, named adapter registration, registry / storage adapter selection). Feature-flags would only add value for runtime per-user / per-tenant rollout, which is not a legitimate concern for deterministic codecs. If a future risky primitive arrives, gate it at adapter-registration time on the consumer's bootstrap, not via runtime flags.

Why

The bitpacker family is functionally complete and at 100/100/100/100 coverage across every package. Six hardening items from the original bitpacker-full-port proposal remain genuinely undone — they would have been done as part of the full port but the modular work prioritised behaviour over instrumentation. Each is small in isolation; bundled together they amount to one focused hardening pass that makes the family enterprise-ready by genesis policy.

None of these items change any wire format, public API shape, or behaviour. Every item is additive instrumentation or test infrastructure.

What changes

1. Zod schemas for every public type

Every public input/output type across the family gets a Zod schema. Today only bitpacker-crypto, bitpacker-delta, and bitpacker-schema have them; bitpacker core, bitpacker-strategies, bitpacker-validation, bitpacker-game, bitpacker-streaming, bitpacker-compressed, and bitpacker-network do not.

  • ADDED: per-package *.schema.ts modules co-located with the implementation file (schema-name.ts → schema-name.schema.ts + schema-name.schema.md).
  • ADDED: validate* helpers around boundaries that today accept unknown from external callers (pack(data: unknown, schema: BitPackerSchema), registry register(adapter), unpack input bytes).
  • ENFORCED: input validation runs only at package boundaries, not internal recursion (per CLAUDE.md "validate at boundaries only").

2. Register Zod schemas with @suaveplan/schema-registry

Once the Zod schemas exist, register the canonical wire-format schemas with @suaveplan/schema-registry so cross-version consumers can negotiate.

  • ADDED: per-package register-schemas.ts entry that contributes its public schemas to the registry under namespace "bitpacker" with the package name as the schema name and version: 1.
  • ADDED: tests/registered.test.ts per package asserting the schemas are present after import "@suaveplan/<pkg>/register-schemas".

3. OTel instrumentation across core

Only bitpacker-crypto has OTel spans today. The bitpacker core, strategy chain, and the four runtime packages (-streaming, -compressed, -network, -game) get spans + counters + histograms via @suaveplan/opentelemetry.

  • ADDED: per-package OTel module (<pkg>/src/otel/instruments.ts) declaring Counter, Histogram, and span names.
  • ADDED: hot-path emission at pack/unpack boundaries (one span per top-level call; per-chunk events nested inside).
  • ASSERTED: emission is verified with a real MeterProvider from @suaveplan/testing — never vi.mock.

4. fast-check property tests

The family today verifies behaviour with concrete examples. Property tests close the gap on round-trip soundness for every schema kind.

  • ADDED: fast-check as a devDependency on every bitpacker package.
  • ADDED: per-package tests/property/*.property.test.ts files exercising round-trip pack/unpack against generated input.
  • ADDED: arbitrary generators for each schema kind in @suaveplan/testing/bitpacker-arbitraries so tests across packages share the same generator surface.

5. mitata benchmark suite + CI regression gate

Add steady-state benchmarks for every public encoder/decoder, with baselines committed to the repo.

  • ADDED: mitata as a devDependency on every bitpacker package.
  • ADDED: per-package bench/*.bench.ts files with bench(name, fn) per public operation.
  • ADDED: scripts/bench/bitpacker-baselines.json baseline file.
  • ADDED: CI gate (Turbo task bench:check) that fails if any benchmark regresses by more than 10% from baseline.

6. @suaveplan/feature-flags integration — DROPPED

Resolved 2026-05-03 (see scope decisions at top of proposal). Every policy choice in the bitpacker family is already configurable via plain JSON option bundles or named-adapter registration; runtime per-user / per-tenant rollout is not a legitimate concern for deterministic codecs. If a future risky primitive arrives, gate it at adapter-registration time at the consumer's bootstrap rather than via runtime flag evaluation. No code change in this section.

Out of scope

  • 47 source files in the bitpacker family lack a sibling .md (bitpacker 30, bitpacker-validation 9, bitpacker-game 7, bitpacker-crypto 1). Documentation pass tracked as a separate bitpacker-doc-pass change.
  • Wire-format-version forward-compat semantics for _version strategy. Today the strategy enforces version match; "tolerate newer version with default-ed missing fields" is a feature decision, not hardening. Tracked separately as a future bitpacker-version-tolerance change.
  • Tree-shake assertion test on the rewritten index.ts. Vite already verifies tree-shaking via dts-only outputs; an explicit assertion adds little value.

Source mapping

This change derives entirely from the unchecked items in openspec/changes/bitpacker-full-port/tasks.md Phase 3 (the "Enterprise Hardening" section). No upstream source lines — every item is an above-baseline genesis-policy addition.

Requirement Source Notes
Zod schemas for every public type bitpacker-full-port/tasks.md Phase 3 #2 Above-baseline; genesis policy
Register with schema-registry bitpacker-full-port/tasks.md Phase 3 #3 Above-baseline; genesis policy
OTel across core bitpacker-full-port/tasks.md Phase 3 #5 Above-baseline; genesis policy
fast-check property tests bitpacker-full-port/tasks.md Phase 3 #8 Above-baseline; genesis policy
mitata benchmark suite + gate bitpacker-full-port/tasks.md Phase 3 #9-10 Above-baseline; genesis policy
feature-flags integration bitpacker-full-port/tasks.md Phase 3 #11 DROPPED — JSON-configurable boundaries cover every legitimate need

Verification approach

  • Per-package tests/registered.test.ts for the schema-registry items.
  • Per-package OTel emission tests using a real MeterProvider from @suaveplan/testing.
  • Per-package tests/property/*.property.test.ts files using fast-check.
  • Per-package bench/*.bench.ts files with mitata baselines committed.
  • bunx turbo bench:check --filter=...@suaveplan/bitpacker* as the CI gate.
  • The 100/100/100/100 coverage gate continues to apply at package level.

Migration impact

None — additive only. No public surface changes; no behaviour changes; no consumer is affected. Every change here is instrumentation or test infrastructure.

Estimated scope

  • ~30-40 new files (one Zod schema module + one OTel module + one property-test file + one bench file per package)
  • ~2-3 new dependencies per package (fast-check, mitata, possibly @suaveplan/opentelemetry if not already wired)
  • Zero source-file modifications outside of the new modules
  • One new Turbo task (bench:check)
  • One CI workflow update

Migration

Phase 8 introduces a single behaviour-affecting addition to @suaveplan/bitpacker-strategies:

_version strategy now accepts optional _tolerance and _allowedVersions fields

Old behaviour: { _version: number; _schema: BitPackerSchema } — unpack throws BitpackerError(INVALID_SCHEMA) on any wire-version mismatch.

New behaviour: Same default. New optional fields:

  • _tolerance?: "strict" | "forward" — defaults to "strict". Set to "forward" to accept alternate wire-versions on unpack.
  • _allowedVersions?: readonly number[] — when paired with _tolerance: "forward", restricts acceptance to the listed versions; otherwise any version [0, 65535] is accepted.

Search command for downstream consumers:

# Find all _version-strategy usages that may benefit from forward tolerance:
grep -rn "_version" packages/ apps/ --include="*.ts" | grep -v "/dist/\|/coverage/\|node_modules" | grep -E "_version:\s*[0-9]"

Recommended replacement: No replacement is needed for existing strict-mode consumers — the change is purely additive and the default behaviour is unchanged. Consumers that need to read newer-version payloads from a server they don't control can opt in by adding _tolerance: "forward" and (optionally) _allowedVersions.

Failure-mode change: When _tolerance: "forward" is set with _allowedVersions, the error message on rejection now includes the allowedVersions list in the error context. Consumers asserting on the old error message text (which mentioned only expected/received) must update their assertions.