P2.3.1 — Experience Tokens (L0–L2b) — Step 1 Audit

§1. Surface inventory

Greenfield slice. No experience_tokens / mcp_witnesses tables exist at the baseline (origin/main = 7e5cf9d3). The reputation domain currently ships four library-only files:

File Lines Role
src/domains/reputation/schema.ts 329 P2.1.1 — reputations / reputation_history schema + readers + insertHistoryEvent (the only write path).
src/domains/reputation/compute.ts P2.1.2 — bps composition.
src/domains/reputation/decay.ts P2.2.1 — per-domain epoch decay.
src/domains/reputation/penalties.ts P2.2.2 — five-band offence penalties + scar + ban.

No existing token / witness code paths to refactor. No prior mcp_tokens rows. No feature_hash index. The schema stub mentioned in s05 §Phase 0 posture (mcp_tokens) was never actually created — git ls-files src/db/migrations/ shows 001 → 007 only, with 007 covering reputation. The next free slot is 008.

§2. Database migration sequence

001_init.sql
002_tasks.sql
003_thought_records.sql
004_skills.sql
005_retention.sql
006_eta.sql
007_reputation.sql        ← P2.1.1 reputations + reputation_history
008_experience_tokens.sql ← THIS TASK (new)

The migration runner (src/db/index.ts) discovers .sql files alphabetically and idempotently applies them; the only constraint on the number is uniqueness + ordering. No existing column needs to be altered.

§3. λ token spec re-read against shipped reality

docs/spec/s05-experience-tokens.md is fully spec-only at baseline; the implementation-status table at the bottom lists every claim as Spec-only except the AMS-heritage mcp_memory_frame chain-hash row.

Five token levels (L3 explicitly out of scope, deferred to π Phase 6):

Level Promotion source Persistence Implementation surface
L0 mint_L0(node_id, domain, action, outcome) Ephemeral per spec; stored as a row in Phase 0 (see §6) one-shot mint
L1 promote_to_L1(L0_token, cycle_proof) Persistent — requires β GSD cycle complete append new row
L1.5 promote_to_L1_5(L1_token, witnesses) Persistent — requires ≥1 witness witness validation + new row
L2a promote_to_L2a(tokens[]) Persistent — diversity gate feature_hash group + scenarios×counterparties
L2b promote_to_L2b(L2a_token) Persistent — invariance check κ engine replay with context zeroed

§4. Witness registry constants

From s05 §Witness registry + p2.1 §P2.3.1 acceptance criteria:

  • reputation_at_witness ≥ 200 (frozen integer bps; floor enforced at registration time).
  • Per-witness weight_cap ≤ 0.3 represented as integer 30 in bps × 100 units (so weight_cap ≤ 30). Float-free per CLAUDE.md §5 (forbidden: floats).
  • Sum cap per target episode: Σ weights ≤ 0.4 × MIN_EPISODES. MIN_EPISODES is named but undefined in s05 — define MIN_EPISODES = 5n per p2.1 §P2.3.1 “Common gotchas” → sum cap encoded as integer 0.4 × 5 × 100 = 200 units.
  • Independence: ≤1 witness per counterparty class per rolling 7-day window for the same target_node_id.

§5. Pattern matching / SHA-256 algorithm

From s05 §Pattern-matching algorithm:

  1. Normalize context. domain / scenario / counterparty canonicalized; unknown values → *.
  2. Feature extract. feature_hash = SHA-256(canonical_context || action_type || outcome_class).
  3. Pattern match. All tokens sharing feature_hash form a candidate set.
  4. Diversity gate (L2a). ≥3 distinct scenario values AND ≥3 distinct counterparty classes (in candidate set of ≥5 tokens).
  5. Invariance gate (L2b). Replay each token via κ engine with context zeroed; resulting outcome_class must equal the original.

SHA-256 hex via node:crypto.createHash('sha256') — already a dependency through crypto.randomUUID usage elsewhere in the codebase (src/__tests__/db-init.test.ts).

Canonical JSON serializer must be deterministic. JSON.stringify with a sorted-keys replacer is sufficient and pure (no float emission for our bigint-free input shape: all string keys + string/number values).

§6. Append-only semantics

AX-01: tokens are append-only. Promotion creates a new row with promoted_from pointing at the upstream token. There is no UPDATE / DELETE on experience_tokens or mcp_witnesses. The shipped reputation module already enforces the same pattern at the export surface (no updateReputation, no deleteHistory) — copy the contract.

Per s05 §Append-only vs garbage collection: the event log is canonical; the derived token store may be pruned. Phase 2 does not implement pruning. We ship only the event-log shape (one row per token, no derived cache).

§7. κ engine integration for L2b

src/domains/rules/engine.ts exports:

export function executeRuleset(
  registry: RuleRegistry,
  event: Readonly<Record<string, unknown>>,
  state: Readonly<Record<string, unknown>>,
  rule_version: string,
  epoch: bigint,
): TransitionResult;

The L2b promotion takes an engine binding (dependency-injected) so the token module never imports executeRuleset at module scope. This keeps:

  • circular-import risk at zero (engine.ts has zero reputation/* deps),
  • testability high (mock the binding with a deterministic stub that produces a known outcome_class),
  • the token module pure-library when invariance replay is not requested.

TokenInvariancePredicate interface in tokens.ts:

export interface TokenInvariancePredicate {
  // Given a token's original (context, action, outcome_class), return the
  // outcome_class produced by replaying through κ with context zeroed.
  // Caller wires this to an executeRuleset closure that decodes the token
  // into (event, state) per their domain.
  replay(token: Token): string;
}

The L2b promoter passes the predicate; the caller (tests, real wiring) constructs it.

§8. Determinism

ULID generation: per gotcha (p2.1 §P2.3.1), Math.random is forbidden. Use crypto.randomBytes + monotonic timestamp prefix. ULID format: tok_<26 chars Crockford base32> per s05 example (tok_01HXYZ...).

created_at: passed in by the caller (epoch seconds). The token module is clock-free — no Date.now(). This mirrors the rules engine’s determinism guardrail. The minting helper accepts created_at as a parameter.

feature_hash: deterministic via canonical JSON sort. Tested via two distinct invocations producing identical hex output.

§9. Acceptance criteria checklist (from source prompt)

  • 5 token levels L0 | L1 | L1.5 | L2a | L2b (no L3).
  • experience_tokens table with ULID PK + level CHECK + indexes.
  • mcp_witnesses table with rep≥200 CHECK + weight_cap≤30 CHECK + indexes.
  • mint_L0.
  • promote_to_L1 (cycle_proof gate).
  • promote_to_L1_5 (witness rules + downgrade-on-violation per s05).
  • promote_to_L2a (≥5 same feature_hash, ≥3 scenarios, ≥3 counterparties).
  • promote_to_L2b (κ invariance replay; outcome_class must match).
  • feature_hash = SHA-256(canonical_context || action_type || outcome_class).
  • Append-only (no UPDATE/DELETE).
  • Non-transferable (no node_id mutator).
  • count_tokens_by_level.

§10. Risks

  1. κ engine signature drift. The token module passes through a TokenInvariancePredicate rather than importing executeRuleset directly — mitigates breaking changes when the engine signature evolves in Phase 1.5+.
  2. Witness independence window. The 7-day rolling window is enforced at registration time via the indexed (target_node_id, counterparty_class, created_at) clause. We treat created_at as caller-supplied (epoch seconds); the module is clock-free.
  3. MIN_EPISODES drift. s05 names but does not define the constant. Defined here as MIN_EPISODES = 5n per p2.1 gotcha — adding to the existing bps-constants.ts would be a cross-domain import (reputation reaches into rules), so we keep MIN_EPISODES local to tokens.ts with a doc cross-reference.
  4. L3 temptation. Out of scope. The TokenLevel union explicitly omits "L3"; there is no promote_to_L3 export. Phase 6 (π governance) will land it.

§11. Out-of-scope (deferred / forbidden)

  • L3 aggregate level — defer to π Phase 6.
  • Token GC / derived-state pruning — s05 calls this a Phase-1.5+ concern.
  • MCP tool surface (token_mint, token_promote, etc.) — library-only per Phase 2 λ posture.
  • Direct integration with mcp_witnesses from existing reputation modules — that’s P2.5.1’s job (reputation_get exposes token counts).
  • Witness reputation re-validation on subsequent rep changes — s05 explicitly freezes reputation_at_witness at witness time.

Audit done. Proceed to contract.


Back to top

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

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