# 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"`.