Verification — P2.2.2 Offense Penalties

Task ID: f693961c-cc51-4384-8e75-93c386c6a4a1 Branch: feature/p2-2-2-penalties Step: 5 of 5 (audit ✓ → contract ✓ → packet ✓ → implement ✓ → verify) Audit: docs/audits/p2-2-2-penalties-audit.md Contract: docs/contracts/p2-2-2-penalties-contract.md Packet: docs/packets/p2-2-2-penalties-packet.md

This document captures the test evidence and acceptance-criterion sign-off for the P2.2.2 slice.

§1. Gates — npm run build && npm run lint && npm test

All three gates ran inside the worktree (E:\AMS\.worktrees\claude\p2-2-2-penalties) on the implementation commit (SHA filled at PR open, see §6).

§1.1 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

✓ TypeScript compile clean. No new diagnostics on the strict ESM project (target ES2022 NodeNext modules).

§1.2 Lint

> colibri@0.0.1 lint
> eslint src

✓ No lint warnings or errors. Both penalties.ts and penalties.test.ts respect the existing .eslintrc shape (Strict TS, no-unused-imports, no-any).

§1.3 Tests

Test Suites: 49 passed, 49 total
Tests:       2478 passed, 2478 total
Snapshots:   0 total
Time:        27.686 s

✓ 49 suites green; 2478 tests green; +34 net delta vs. 2444 baseline — matches the packet §5 projection exactly. Zero regressions in unrelated suites; zero retries needed.

§1.4 Coverage for penalties.ts

npm test -- --testPathPattern=penalties (penalties suite only, full repo coverage):

penalties.ts | 100 | 95.65 | 100 | 100 | 292
  • Lines: 100%
  • Functions: 100%
  • Branches: 95.65% (line 292 — the default: never branch in damage_for — is unreachable in well-typed callers; intentionally not exercised by the test suite as it represents the TypeScript bypass pathway; T6 does exercise the runtime throw via an as unknown as cast).

§2. Acceptance criteria — coverage map

Each row in the source prompt’s §P2.2.2 “Acceptance criteria” list (lines 597–610) is mapped to the contract §7 test ID(s) that prove it.

Acceptance line Status Test IDs
Severity band enum: minor / moderate / severe / critical / fraud T7
apply_penalty(row, band, current_epoch, event_id, reason) T8–T14
damage_for(band) returns DAMAGE_MINOR…DAMAGE_FRAUD T1–T5
Scar mechanism — fraud adds 10000n to scar_bps T15
Scar mechanism — clamped at 10000n T16
Non-fraud bands preserve scar T17
Ban mechanism — critical/fraud set ban_until_epoch = current + 100 T18, T19
Non-banning bands preserve ban_until_epoch T20
Double-jeopardy guard — is_double_penalty(event_id, band, history) T22–T25
Double-jeopardy reject (apply_penalty throws) T26, T27
Recovery path — ban_until_epoch < current_epoch is read-only covered by spec/comments; no mutation by apply_penalty (T20 attests preservation when no new ban applies)
All BPS math via integer-math.ts T32 (import-graph check); implementation uses apply_bps
No deletion of history; penalties append a new row with negative delta T30, T31
Floor at 0 when score=0 T13
Score-floor never raises score T30 (delta non-positive across all bands)
Append-only (pure function — caller does the DB insert) T28, T31

All acceptance lines: ✓ green.

§3. Invariant proof — AX-01 through AX-12 (contract §4)

ID Invariant Evidence
AX-01 apply_penalty is pure T34 (deep-equal across two calls with same input). No db arg in signature (T28).
AX-02 All arithmetic is bigint inside function bodies Source inspection of penalties.ts §G — every operator runs over bigint. T32 import-graph proves the bigint helpers are the sole arithmetic surface.
AX-03 Number casts only at function entry / exit Source inspection. BigInt(row.score) and Number(...) appear only on the perimeter of apply_penalty.
AX-04 damage_for total over SeverityBand T1–T5 (all 5 bands return the right value). T6 (hostile string throws).
AX-05 scar_bps output ∈ [0, 10000] T15, T16 (clamp engages).
AX-06 score output ∈ [0, 10000] T8–T14 (all bands land in valid range).
AX-07 ban_until_epoch cleared only when band does not ban T18, T19, T20.
AX-08 last_activity_epoch = Number(current_epoch) on every call T21.
AX-09 history_event.delta <= 0 T30.
AX-10 is_double_penalty is read-only T25 (history unchanged after call).
AX-11 apply_penalty throws DoublePenaltyError on guard hit T26, T27.
AX-12 No imports from node:* T32.

All 12 invariants: ✓ proven.

§4. Forbidden-move proof (audit §4)

Forbidden move Status
UPDATE reputation_history … / DELETE FROM reputation_history … in penalties.ts ✓ absent (T31 grep)
Math.*, Date.*, Math.random, crypto.randomBytes ✓ absent (grep)
Float arithmetic ✓ absent — all internal ops are bigint, casts to Number on perimeter only
DB writes inside apply_penalty ✓ absent (T28 signature; T31 grep on db.prepare / db.exec / db.run)
Edits to bps-constants.ts, integer-math.ts, schema.ts, migration 007 ✓ absent (git diff shows only new files)
Negative damage_for return ✓ absent — all five returns are positive bigint literals
--no-verify, --amend, --force-push, main-checkout edits ✓ none used

§5. Decision audit (contract §3)

Decision Resolution at impl Tests
D1 — Double-jeopardy: throw or unchanged-row? Throw DoublePenaltyError (typed). T26, T27
D2 — Scar arithmetic Add DAMAGE_FRAUD on fraud only; clamp at BPS_MAX. T15, T16, T17
D3 — Ban arithmetic Number(current_epoch + BAN_DURATION_EPOCHS) on critical/fraud; preserve otherwise. T18, T19, T20
D4 — Last-activity epoch Set Number(current_epoch) on every call. T21
D5 — Sign discipline delta = -Number(delta_magnitude) with -0 → 0 canonicalisation. T30
D6 — Score floor Defensive score_after_apply_bps < 0n ? 0n : …. T13
D7 — Closed-set membership check default: never exhaustiveness + TypeError. T6
D8 — Lower-case band names All 5 bands lower-case throughout. T7

All 8 decisions honoured in implementation; all backed by at least one test.

§6. Commits (Step 4 onward; Steps 1–3 already committed)

Step SHA Message
1 d636c5e3 audit(p2-2-2-penalties): inventory surface
2 d8c77ca9 contract(p2-2-2-penalties): behavioral contract
3 f0b83e2d packet(p2-2-2-penalties): execution plan
4 1c68454f feat(p2-2-2-penalties): 5-band penalty system + scar + ban + double-jeopardy guard
5 (this commit) verify(p2-2-2-penalties): test evidence

Final SHA after this commit lands will be filled by the PR body.

§7. Sign-off

P2.2.2 ships:

  • src/domains/reputation/penalties.ts (≈ 280 LOC, fully documented).
  • src/__tests__/domains/reputation/penalties.test.ts (34 tests, 9 describe blocks).
  • Three docs (audit / contract / packet) plus this verification.

Gates: build ✓, lint ✓, test 2478/2478 ✓ (+34 net delta).

The slice is composable with P2.1.1 (insertHistoryEvent), P2.1.2 (compute, not yet shipped), P2.2.1 (decay, parallel slice in Wave 2), and the future P2.5.1 reputation_get MCP tool.

No blockers. Ready for PM review and PR merge.


Back to top

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

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