Files
2026-05-27 23:29:53 +01:00

106 lines
6.3 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: bitpacker-delta (bitpacker-stub-completion)
**Package**: `@suaveplan/bitpacker-delta`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: Field-level change detection
The package SHALL expose a `DeltaManager` class that, given a current object, a `BitPackerSchema` describing it, and a previous state retrieved by state key, returns the ordered list of fields whose values changed since that previous state. Field order in the returned `FieldChange[]` SHALL match `Object.keys(schema)` order so encoders can rely on field indexes.
#### Scenario: first call returns empty changes
- **WHEN** `detectChanges` is called for a state key with no stored previous state
- **THEN** the result is an empty array AND the current state is stored under that key
#### Scenario: changed fields surface as ordered FieldChange[]
- **WHEN** the previous state had `{ a: 1, b: "x" }` and the current state is `{ a: 1, b: "y" }` under schema `{ a: "u8", b: "-32" }`
- **THEN** the result is `[{ index: 1, key: "b", previousValue: "x", currentValue: "y" }]`
#### Scenario: maxFieldCount cap enforced
- **WHEN** the schema has more entries than `DeltaConfig.maxFieldCount`
- **THEN** the manager throws `BitpackerError` with code `BITPACKER.DELTA_FIELD_LIMIT_EXCEEDED` and the offending field count in `context.fieldCount`
**Verified by:** `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.test.ts::"detects changed fields in schema order"` AND `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.test.ts::"returns empty array on first call for a state key"` AND `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.test.ts::"throws DELTA_FIELD_LIMIT_EXCEEDED when schema exceeds maxFieldCount"`
---
### Requirement: Configurable delta modes via DeltaConfig
The package SHALL accept a `DeltaConfig` object (`mode`, `xorNumbers`, `diffStrings`, `threshold`, `maxFieldCount`, `stateKey`, `equalityMode`) validated by Zod. Unknown fields and invalid `mode` values SHALL be rejected at construction time with `BitpackerError` code `BITPACKER.DELTA_INVALID_CONFIG`.
#### Scenario: unknown mode rejected
- **WHEN** `new DeltaManager({ mode: "magic" } as never)` is constructed
- **THEN** it throws `BitpackerError` with code `BITPACKER.DELTA_INVALID_CONFIG` and `context.mode === "magic"`
#### Scenario: `equalityMode: "json"` opts into stringify-based equality
- **WHEN** `equalityMode` is `"json"`
- **THEN** the manager uses `JSON.stringify` for equality (parity with upstream behavior)
#### Scenario: structural equality is the default
- **WHEN** `equalityMode` is omitted
- **THEN** equality is computed by walking the schema tree, not by `JSON.stringify`, so key-order differences in nested objects do not register as changes
**Verified by:** `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.spec-conformance.test.ts::"rejects invalid DeltaConfig with DELTA_INVALID_CONFIG"` AND `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.spec-conformance.test.ts::"uses structural equality by default"` AND `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.spec-conformance.test.ts::"opts into JSON equality when equalityMode is 'json'"`
---
### Requirement: State storage with LRU eviction
The package SHALL expose a `DeltaStateManager` class that stores previous states keyed by string, deep-clones on store to prevent caller mutation, and evicts in insertion order when the configured `maxStates` (default 100) is exceeded.
#### Scenario: stored state is isolated from mutation
- **WHEN** a caller stores a state, then mutates the original object
- **THEN** `getPreviousState` returns the unmutated snapshot
#### Scenario: oldest state evicted at capacity
- **WHEN** `maxStates` is 3 and a 4th key is stored
- **THEN** the first key is evicted; `hasPreviousState(firstKey)` returns false
#### Scenario: re-store under existing key does not advance LRU position
- **WHEN** a key is stored, then re-stored after intervening writes
- **THEN** the key remains at its original LRU position (re-store updates value, not order — matches upstream behavior in `delta-state.ts` L22-L37)
**Verified by:** `packages/bitpacker/bitpacker-delta/src/delta-state/delta-state.test.ts::"deep-clones stored states to isolate from caller mutation"` AND `packages/bitpacker/bitpacker-delta/src/delta-state/delta-state.test.ts::"evicts oldest state when maxStates exceeded"` AND `packages/bitpacker/bitpacker-delta/src/delta-state/delta-state.test.ts::"re-store under existing key preserves LRU position"`
---
### Requirement: Deterministic clock injection
The package SHALL accept an optional `Clock` from `@suaveplan/types`; all timestamps recorded internally (e.g. for OTel histograms) SHALL come from the injected clock. The package SHALL NOT call `Date.now()` at module scope or inside hot paths.
#### Scenario: injected clock observed in OTel histogram
- **WHEN** a fake `Clock` advancing 7ms per `now()` call is injected and `detectChanges` is called twice
- **THEN** the OTel histogram `bitpacker.delta.changes_detected` records two samples whose timestamps differ by exactly the fake clock's delta
#### Scenario: no module-scope `Date.now()`
- **WHEN** `grep -r "Date\.now()" packages/bitpacker/bitpacker-delta/src/` is run
- **THEN** there are zero matches in implementation files
**Verified by:** `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.spec-conformance.test.ts::"records OTel timestamps from injected Clock"` AND `packages/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.spec-conformance.test.ts::"no module-scope Date.now in implementation files"`
## REMOVED Requirements
### Requirement: Inline 35-line DeltaManager in `index.ts`
**Reason:** Stub. Used `Date.now()` and `JSON.stringify` (CLAUDE.md determinism rule violation), tracked no field-level diffs, and held only one global previous state. Replaced wholesale by the `DeltaManager` + `DeltaStateManager` pair above.
**Migration:** Existing call sites doing `new DeltaManager()` continue to work — the new class accepts an optional config and clock. Call sites that read `manager.getPreviousState()` (no arg) should switch to `manager.getPreviousState(stateKey)`; the default `stateKey` is `"default"`.
<!-- markdownlint-enable MD024 -->