Verification — P2.2.1 Exponential Decay

Task ID: 7ab3f352-7ba5-4132-9db4-aeda2affb2a0 Branch: feature/p2-2-1-decay Step: 5 of 5 (audit ✓ → contract ✓ → packet ✓ → implement ✓ → verify) Audit: docs/audits/p2-2-1-decay-audit.md Contract: docs/contracts/p2-2-1-decay-contract.md Packet: docs/packets/p2-2-1-decay-packet.md

§1. Shipped surface

src/domains/reputation/decay.ts         — 156 LOC source (rate_for, apply_decay, apply_decay_batch)
src/__tests__/domains/reputation/decay.test.ts — 278 LOC tests (T1–T20)

No edits to existing files. No new npm dependency. No schema migration.

§2. Gate evidence

§2.1 npm run build

> colibri@0.0.1 build
> tsc

> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs

copy-migrations: copied 7 migration(s) ... -> .../dist/db/migrations

Status: PASS — TypeScript clean, no diagnostics, never-narrowed exhaustiveness check satisfied at compile time.

§2.2 npm run lint

> colibri@0.0.1 lint
> eslint src

Status: PASS — ESLint clean (no output = no warnings/errors).

§2.3 npm test

Test Suites: 49 passed, 49 total
Tests:       2464 passed, 2464 total
Snapshots:   0 total
Time:        54.077 s

Status: PASS — 2464/2464 across 49 suites.

  • Baseline at origin/main (994db1e4): 2444 tests.
  • Delta: +20 tests added by P2.2.1 (T1–T20).
  • Regression: zero — every prior suite still passes.

§2.4 Coverage for decay.ts

src/domains/reputation/decay.ts | 88.23% statements | 88.88% branches | 100% functions | 87.5% lines
Uncovered: lines 83–84

Lines 83–84 are the never-narrowed default branch in rate_for:

const _exhaustive: never = domain;
throw new Error(`rate_for: unhandled domain ${String(_exhaustive)}`);

This branch is unreachable by design — TS rejects any caller that could hit it. The 88% coverage is the maximum achievable without a test that deliberately defeats the type system (which would itself be a defect).

§3. Acceptance criteria — checked against the contract

AC (contract §7) Test Status
T1: execution → DECAY_EXECUTION (500n) rate_for · T1 PASS
T2: commissioning → 300n rate_for · T2 PASS
T3: arbitration → 1_000n rate_for · T3 PASS
T4: governance → 200n rate_for · T4 PASS
T5: social → 100n rate_for · T5 PASS
T6: zero-inactive identity short-circuit apply_decay · T6 PASS
T7: clock-skew identity short-circuit apply_decay · T7 PASS
T8: 10-epoch execution decay matches decay() apply_decay · T8 PASS
T9: score = 0 stays 0 (floor) apply_decay · T9 PASS
T10: last_activity_epoch unchanged apply_decay · T10 PASS
T11: Object.freeze input — fresh row returned apply_decay · T11 PASS
T12: each domain decays at its rate apply_decay · T12 PASS
T13: 10k rows perf smoke (<500ms) apply_decay_batch · T13 PASS
T14: empty array → empty array apply_decay_batch · T14 PASS
T15: mixed-domain batch apply_decay_batch · T15 PASS
T16: length + order preserved apply_decay_batch · T16 PASS
T17: compound ≠ linear (9025 vs 9000) properties · T17 PASS
T18: determinism — deep-equal properties · T18 PASS
T19: EpochCeilingError propagates apply_decay · T19 PASS
T20: rate_for accepts every DOMAINS member properties · T20 PASS

§4. Invariant audit (contract §4)

ID Invariant Verification
AX-01 Pure functions; no I/O. grep confirms: no better-sqlite3, no fs, no path, no http imports.
AX-02 No Math.*, Date.*, Math.random. grep -nE 'Math\.|Date\.' returns no hits in decay.ts.
AX-03 decay() is imported, never re-implemented. Single import statement; no local function decay definition.
AX-04 No mutation of input. T11 (Object.freeze) proves it at runtime.
AX-05 last_activity_epoch preserved. T10 asserts equality across input/output.
AX-06 rate_for exhaustive. TS compile-time check via never branch; tests T1–T5 + T20 cover all five domains at runtime.
AX-07 NodeNext ESM .js suffix. All imports use .js suffix.
AX-08 bigint ↔ number conversion bounded. The exported function signatures expose bigint only for current_epoch (which is bigint to match decay() input type); all ReputationRow.score reads/writes use Number() boundary conversion.

§5. Anomalies and follow-ups

§5.1 T17 — corrected during the packet step

The original prompt suggested testing decay(decay(x, r, e1), r, e2) !== decay(x, r, e1+e2) as a “compound effect” property. We verified empirically during the packet step that this is in fact equal — same-rate composition is associative because per-step flooring is idempotent across cuts. The genuine non-linearity of compound decay is that it diverges from the linear projection: 5% × 2 epochs reduces value by 9.75%, not 10%. T17 tests that real property and is documented in contract §7 and packet §6 R1.

This is a correctness improvement on the prompt, not a deviation from the underlying intent (which was to show that compounding matters).

§5.2 88% line coverage on decay.ts

Lines 83–84 (the never-narrowed default branch in rate_for) are intentionally unreachable. 100% line coverage would require a test that casts away the type system to construct an invalid Domain, which would itself be a defect. The never check provides stronger guarantees than runtime coverage could.

§5.3 No blockers

  • decay() primitive signature matches: decay(value: bigint, rate_bps: bigint, epochs: bigint): bigint.
  • All five DECAY_* constants present and correct in bps-constants.ts.
  • ReputationRow shape from P2.1.1 is the source of truth.
  • All three gates green; zero regressions; +20 tests.

§6. Commit timeline

Step SHA Subject
1 — audit ba86a120 audit(p2-2-1-decay): inventory surface
2 — contract 6fc6e044 contract(p2-2-1-decay): behavioral contract
3 — packet c7a82fbb packet(p2-2-1-decay): execution plan
3.1 — fixup 706ac660 contract+packet(p2-2-1-decay): correct T17 — same-rate decay composition is associative, real non-linearity is vs linear projection
4 — implement 351f1368 feat(p2-2-1-decay): per-domain decay using κ integer-math primitive
5 — verify (this commit) verify(p2-2-1-decay): test evidence

§7. Writeback (for PM)

task_id: 7ab3f352-7ba5-4132-9db4-aeda2affb2a0
branch: feature/p2-2-1-decay
worktree: .worktrees/claude/p2-2-1-decay
commits: ba86a120, 6fc6e044, c7a82fbb, 706ac660, 351f1368, <verify SHA>
tests: npm run build && npm run lint && npm test → 49 suites, 2464/2464 PASS (+20 from 2444 baseline)
summary: Per-domain reputation decay using κ integer-math decay() primitive
  and DECAY_* constants from bps-constants. apply_decay (single-row, pure)
  and apply_decay_batch (10k+ rows pure map). Exhaustive rate_for over the
  5 canonical Domain values with `never`-narrowed default for compile-time
  safety. Score floor at 0 delegated to the κ primitive; last_activity_epoch
  preserved byte-for-byte (B3, AX-05); zero-inactive short-circuit returns
  input reference; 10k-row perf smoke completed well under spec target.
  T17 corrected during packet step: same-rate decay composition is associative,
  so the meaningful non-linearity property is compound vs linear (9025 vs 9000),
  not split vs combined. Documented in contract §7 + packet §6 R1.
blockers: none

Verification complete. Ready for PR open.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.