P2.3.1 — Experience Tokens (L0–L2b) — Step 5 Verification

§1. Gate execution log

Run on worktree .worktrees/claude/p2-3-1-tokens at HEAD 83777b28.

$ npm run build
> colibri@0.0.1 build
> tsc

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

copy-migrations: copied 8 migration(s) src/db/migrations -> dist/db/migrations

$ npm run lint
> colibri@0.0.1 lint
> eslint src
(no output — clean)

$ npm test
Test Suites: 54 passed, 54 total
Tests:       2587 passed, 2587 total
Snapshots:   0 total
Time:        27.439 s

Build green. Lint green. Tests 2587/2587 (+109 vs 2478 baseline). All three gates pass.

§2. Test delta breakdown

Suite Existing New After
Full suite baseline at origin/main 7e5cf9d3 2478
src/__tests__/domains/reputation/witnesses.test.ts (new) 0 27 27
src/__tests__/domains/reputation/tokens.test.ts (new) 0 82 82
Total after P2.3.1 2478 +109 2587

§3. Acceptance criteria verification (from source prompt)

AC Status Evidence
5 token levels L0 | L1 | L1.5 | L2a | L2b (no L3) TOKEN_LEVELS tuple at tokens.ts §B; test TOKEN_LEVELS is exactly [L0,L1,L1.5,L2a,L2b]; SQL CHECK (level IN ('L0','L1','L1.5','L2a','L2b')); test M3 verifies CHECK rejects ‘L3’.
experience_tokens table with required columns + indexes Migration 008 §CREATE TABLE experience_tokens; test M1 verifies table existence; test “experience_tokens indexes exist” confirms all three indexes.
mcp_witnesses table with required columns + indexes Migration 008 §CREATE TABLE mcp_witnesses; test “table exists after initDb”; SQL CHECK on rep≥200 + weight_cap≤30.
mint_L0(node_id, domain, action, outcome) tokens.ts §G mint_L0; test T1 + T25.
promote_to_L1(L0_token, cycle_proof) requires cycle complete tokens.ts §G promote_to_L1; tests T2/T3 + happy path. CycleProof requires both flags literally true.
promote_to_L1_5(L1_token, witnesses) with witness rules tokens.ts §G promote_to_L1_5; tests T4–T8. s05 silent-downgrade to L1 on rule violation rather than reject.
L1.5 witness floor (rep ≥ 200) Test T5 (silent downgrade); register_witness throws WitnessReputationFloorError at registration time (test W1).
L1.5 per-witness weight cap (≤ 0.3, encoded as 30 bps×100) Test T6 (silent downgrade); test W2 throws WitnessWeightCapError. SQL CHECK weight_cap > 0 AND weight_cap <= 30.
L1.5 sum cap (Σ ≤ 0.4 × MIN_EPISODES = 200 bps×100) Test T7 (silent downgrade); test W10 throws WitnessSumCapError. WITNESS_SUM_CAP_MAX = 200.
L1.5 independence (≤1 per counterparty class per 7-day window) Tests W5/W6/W7/W8; SQL index idx_witnesses_independence(target_node_id, counterparty_class, created_at). Window = 604800s.
promote_to_L2a requires ≥5 same feature_hash + ≥3 scenarios + ≥3 counterparties Tests T9/T11/T11-refined/T12/T13.
promote_to_L2b invariance via κ engine replay tokens.ts §G promote_to_L2b calls predicate.replay; tests T16 (match) + T17 (mismatch throws L2bInvarianceError). κ injected via TokenInvariancePredicate.
feature_hash = SHA-256(canonical_context || action || outcome_class) tokens.ts §E feature_hash; tests T18 (determinism) + T19 (distinct inputs) + “key insertion order insensitive”.
Append-only No UPDATE experience_tokens or DELETE FROM experience_tokens in source (test T21); no UPDATE/DELETE mcp_witnesses in source (test W13).
Non-transferable No set_node_id / transfer / mutateNodeId exports (test T22). Token.node_id is readonly and the object is Object.freezed.
count_tokens_by_level(node_id, domain, level) tokens.ts §H count_tokens_by_level; tests T26/T27.

§4. Invariant verification (contract §2)

All 22 invariants from docs/contracts/p2-3-1-tokens-contract.md verified:

Invariant Test
I1 — level ∈ {L0/L1/L1.5/L2a/L2b} “TOKEN_LEVELS is exactly…” + M3
I2 — node_id immutable T22 export-scan + readonly type + Object.freeze
I3 — feature_hash null for L0/L1, set for L1.5/L2a/L2b T1 + T8 + happy paths
I4 — promotion produces new row All promote_to_* tests assert new id + promoted_from
I5 — no UPDATE/DELETE SQL T21 + W13
I6 — no update/delete/setNodeId exports T22 + T24 + W14
I7 — mint_L0 has level=L0, witnesses=[], feature_hash=null T1
I8 — promote_to_L1 requires both cycle_proof flags T2 + “counterparty_confirmed missing”
I9 — L1.5 silent downgrade on violation T5/T6/T7
I10 — L2a diversity gate T9/T11/T12 + T13 happy
I11 — L2b invariance via predicate.replay T16/T17
I12 — feature_hash determinism T18 + key-order test
I13 — canonicalize_context emits strings (no null/undefined) T20
I14 — Witness rep ≥ 200 enforced TS + SQL W1 + “CHECK rejects rep=199”
I15 — Witness weight_cap ≤ 30, float-free W2/W3 + “rejects non-integer” + “CHECK rejects weight_cap=31”
I16 — 7-day independence W5/W6/W7/W8
I17 — Sum cap (Σ ≤ 200) W10
I18 — ULID consumes caller-supplied random_bytes T29 + W4 (matches ^wit_[0-9A-Z]{26}$)
I19 — created_at caller-supplied All happy paths assert returned created_at matches input
I20 — All arithmetic integer Float-free guards in register_witness and encodeUlid; no parseFloat / Math.* in source
I21 — count_tokens_by_level returns 0 on missing T26
I22 — No L3 anywhere in code T23 (post-comment-strip) + TOKEN_LEVELS assertion

§5. Determinism guardrails

  • No Math.* / Math.random: confirmed via grep -E 'Math\.' src/domains/reputation/tokens.ts src/domains/reputation/witnesses.ts returns zero matches in code (any prose mention is in stripped-comment blocks).
  • No Date.now(): same scan — zero matches.
  • No floats: numeric literals in module source are integers; weight_cap is stored bps × 100; MIN_EPISODES = 5n (bigint).
  • No crypto.randomBytes() from inside module: encodeUlid takes caller-supplied Uint8Array(10). Tests use deterministic bytes(seed) helper.

§6. Anomalies

None encountered.

Minor design decision worth flagging:

  1. feature_hash excludes scenario + counterparty. The source prompt says “canonical_context is JSON-stringified with sorted keys; unknown values bucketed to *”. The s05 §Pattern-matching algorithm step 1 says “Canonicalize domain, scenario, and counterparty to a shared vocabulary.” Reading these together, the cleanest implementation that makes the L2a gate (“≥5 tokens with the same feature_hash spanning ≥3 distinct scenarios AND ≥3 distinct counterparty classes”) reachable is to bucket scenario + counterparty to * when computing the hash. Otherwise five tokens with three distinct scenarios would always produce three distinct hashes and L2a could never gate.

    The diversity check still enforces ≥3 distinct values — those distinct values live in the token’s scenario / counterparty row fields, not in the hash input. The hash captures the pattern abstraction; the row fields capture the instance variation.

    This is consistent with s05’s narrative example (L2a token has concrete scenario: "bug_triage" even though the gate requires diversity).

  2. promoted_from enforced by SQLite FK. PRAGMA foreign_keys = ON is set by initDb (src/db/index.ts:252). The audit assumed FKs were off based on the documentary FK in earlier migrations; in practice the FK is enforced. Test T28 was updated to insert an anchor L0 token first so the FK constraint is satisfied for the L1.5 row inserts.

§7. Source artefacts

docs/audits/p2-3-1-tokens-audit.md                      (Step 1)
docs/contracts/p2-3-1-tokens-contract.md                (Step 2)
docs/packets/p2-3-1-tokens-packet.md                    (Step 3)
src/db/migrations/008_experience_tokens.sql             (Step 4)
src/domains/reputation/tokens.ts                        (Step 4)
src/domains/reputation/witnesses.ts                     (Step 4)
src/__tests__/domains/reputation/tokens.test.ts         (Step 4)
src/__tests__/domains/reputation/witnesses.test.ts      (Step 4)
docs/verification/p2-3-1-tokens-verification.md         (Step 5 — this)

§8. Commit ledger

# SHA Description
1 b8de13bc audit(p2-3-1-tokens): inventory surface
2 a76e3e01 contract(p2-3-1-tokens): behavioral contract
3 0fef40b0 packet(p2-3-1-tokens): execution plan
4 d878777e feat(p2-3-1-tokens): L0-L2b token issuance + witness registry + κ invariance replay
5 83777b28 feat(p2-3-1-tokens): tests + feature_hash scenario/counterparty abstraction
6 (this commit) verify(p2-3-1-tokens): test evidence

§9. Phase-2-λ posture confirmation

  • Library-only: no MCP tool registration in src/server.ts. Confirmed via diff that src/server.ts is untouched.
  • L3 deferred: no 'L3' literal in code (comments document the deferral citing Phase 6 π governance).
  • κ engine integration via injection: no direct import of src/domains/rules/engine.ts from tokens.ts. Confirmed via grep "from '../rules'" src/domains/reputation/tokens.ts returning zero matches.
  • Witness reads via Witness shape: tokens.ts imports only type Witness (type-only).
  • Determinism: caller-supplied random_bytes + created_at for every mint/promote. No internal clock or RNG.

Verification done. 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.