Audit — P2.1.1 Reputation Record Schema

Task ID: c4bd3540-34a6-478a-b02b-567aa4aa2df6 Branch: feature/p2-1-1-rep-schema Base: main @ 310cd078 (post-R89 Wave 0) Date: 2026-05-12 Step: 1 of 5 (audit → contract → packet → implement → verify)

Purpose

Inventory the surface area touched by P2.1.1 (the foundational λ Reputation slice): what the spec demands, what shipped Phase 0 code already provides, what gaps exist, and what naming / numeric / structural decisions must be made before contract drafting.

This task is R89 Wave 1 — the foundation slice for Phase 2 λ. It ships the reputations + reputation_history tables, the 5-domain enum, Zod validators, and the append-only insert helper. No score computation, no decay, no penalties — those are P2.1.2 / P2.2.1 / P2.2.2.

§1. Spec inputs

§1.1 docs/guides/implementation/task-prompts/p2.1-lambda-reputation.md lines 130–276

Authoritative source prompt. Lists exactly what must ship:

  • 5-domain enum: execution | commissioning | arbitration | governance | social (no sixth value permitted; Zod must reject).
  • reputations table — PK (node_id, domain), score INTEGER NOT NULL DEFAULT 0 bounded [0, 10000] bps via SQL CHECK, scar_bps INTEGER NOT NULL DEFAULT 0, ban_until_epoch INTEGER (nullable), last_activity_epoch INTEGER NOT NULL.
  • reputation_history table — id INTEGER PRIMARY KEY AUTOINCREMENT, plus node_id, domain, epoch, delta INTEGER (signed bps), reason, event_id. Append-only: no UPDATE or DELETE statements anywhere in src/domains/reputation/**.
  • Three indexes: idx_reputations_lookup (node_id, domain), idx_reputations_leaderboard (domain, score DESC), idx_history_node (node_id, domain, epoch DESC).
  • TypeScript: Domain union, DOMAINS readonly array, ReputationRow interface, ReputationHistoryRow interface, Zod validators for both, plus selectReputation, selectHistory, insertHistoryEvent. No mutator exports.

§1.2 docs/guides/implementation/task-breakdown.md §P2.1.1 (lines 765–774)

Matches the source prompt except for one minor wording divergence: task-breakdown says “scars (bitmask)” but the source prompt says scar_bps INTEGER NOT NULL DEFAULT 0 (cumulative scar reduction). Per CLAUDE.md the source prompt is authoritative — scar_bps it is. The “bitmask” phrasing is a legacy artefact from when scars were imagined as discrete event flags; the integer-bps model is cleaner and matches the P2.2.2 penalty schedule (which adds bps, not bits).

§1.3 docs/3-world/social/reputation.md (concept doc)

Confirms the 5-domain enum (Execution, Commissioning, Arbitration, Governance, Social) and decay rates per domain. Confirms λ is colibri_code: none in Phase 0 — this task graduates it to a database surface but does not yet read or write scores (P2.1.2 will). Confirms non-transferability: the table key is the agent’s identity (ξ Soul Vector id, surfaced here as node_id). Phase 0 posture explicitly says “The reputations, experience_tokens, and penalty_events tables exist in the schema but are not populated” — this task creates the first of those tables.

§1.4 docs/spec/s04-reputation.md

Confirms basis-point arithmetic (“All arithmetic uses 64-bit signed integers with basis points (1 bp = 0.01%)”) and acknowledges the spec lists only 3 domains in the headline (Execution, Commissioning, Arbitration) but the concept doc + source prompt + bps-constants.ts all enumerate 5 (adding Governance, Social). The 5-domain model is the canonical Phase 2 target.

§1.5 src/domains/rules/bps-constants.ts (P1.1.3 — already shipped)

Already exports the 5 decay rates as bigint named constants:

  • DECAY_EXECUTION = 500n (5.00%)
  • DECAY_COMMISSIONING = 300n (3.00%)
  • DECAY_ARBITRATION = 1_000n (10.00%)
  • DECAY_GOVERNANCE = 200n (2.00%)
  • DECAY_SOCIAL = 100n (1.00%)

And the bps bounds: BPS_MIN = 0n, BPS_MAX = 10_000n. This file is the single source of truth for bps arithmetic and ranges. P2.1.1 schema CHECK constraint uses 0 / 10000 as plain SQL integers but those must agree with BPS_MIN / BPS_MAX semantically.

Critical: bps-constants exposes bigints; the schema layer uses plain JS number because SQLite’s INTEGER column maps cleanly to number via better-sqlite3 (no bigint serialization headache). The runtime bridge (P2.1.2 compute layer) will convert between bigint arithmetic and number storage at the IO boundary. P2.1.1 stays in number-land.

§1.6 src/db/index.ts (migration runner — already shipped)

Migrations discovered by NNN_*.sql filename prefix. Numeric collisions throw. Existing migrations: 001_init.sql, 002_tasks.sql, 003_thought_records.sql, 004_skills.sql, 005_retention.sql, 006_eta.sql. Next free number: 007.

Migration body is wrapped in a transaction; db.exec runs the whole file; the runner short-circuits on empty/comment-only bodies. Idempotency comes from user_version — re-running initDb skips migrations whose version is ≤ current user_version. We do not need CREATE TABLE IF NOT EXISTS for idempotency at the runner level, but the migration smoke test in P2.1.1 invokes initDb twice — that path exercises the version check, not the SQL re-application.

§2. Existing code layout

§2.1 src/domains/ (existing axes)

src/domains/
├── integrations/   (ν)
├── proof/          (η)
├── router/         (δ)
├── rules/          (κ — P1.x BPS + DSL + admission)
├── skills/         (ε)
├── tasks/          (β)
└── trail/          (ζ)

P2.1.1 introduces src/domains/reputation/ — the first λ directory. No existing files conflict. Pattern matches src/domains/<concept>/schema.ts.

§2.2 Reference patterns

  • src/domains/trail/schema.ts — Zod schema + types + pure helpers. Good shape model for what P2.1.1 must look like at the top of the file.
  • src/domains/skills/schema.ts — Zod + readonly constant arrays for closed enums (GREEK_LETTERS pattern is exactly what 5-domain enum needs).
  • src/db/migrations/003_thought_records.sql — CREATE TABLE + CREATE INDEX pattern; comments-first; no DML.
  • src/db/migrations/004_skills.sql — single-table migration with one index. PK on a TEXT column.

§2.3 No existing reputation references

grep "reputation" src/ returns zero hits (modulo docs/). Greenfield surface.

§3. Naming + numeric decisions

Question Decision Rationale
Migration filename 007_reputation.sql Next free number after 006_eta.sql; suffix matches domain dir
Migration user_version 7 Equals numeric prefix per migration runner contract
Domain enum order [execution, commissioning, arbitration, governance, social] Same order as source prompt + decay constants in bps-constants.ts
Domain type spelling Domain (not ReputationDomain) Matches source prompt verbatim; the name is local to src/domains/reputation/ so collision risk is minimal
DOMAINS export readonly Domain[] from as const literal tuple Matches THOUGHT_TYPES + GREEK_LETTERS pattern
score JS type number (not bigint) better-sqlite3 returns INTEGER as number ≤ 2^53; bps domain is [0, 10000] ⊂ safe integer range
scar_bps JS type number (same reason)  
ban_until_epoch number \| null Nullable column; null means “not banned”
Append-only enforcement No exported mutator + grep test Compile-time: no updateReputation / deleteReputation; runtime: smoke test scans source for UPDATE/DELETE keywords inside the domain dir
SQL CHECK form CHECK (score >= 0 AND score <= 10000) Matches source prompt §Acceptance criteria; typeof(score) = 'integer' is not added because the Zod validator + insert prepared-statement bindings enforce integer-only at the TS boundary
Index column DESC idx_reputations_leaderboard ON (domain, score DESC) SQLite 3.39+ supports DESC; better-sqlite3 14.x bundles 3.42; pre-cleared

§4. Acceptance-criteria mapping

Criterion (from source prompt) Implementation plan
5-domain enum, sixth-value rejected DOMAINS tuple + z.enum(DOMAINS)
reputations PK (node_id, domain) SQL PRIMARY KEY (node_id, domain)
score bounds [0, 10000] SQL CHECK + Zod .int().min(0).max(10000)
reputation_history AUTOINCREMENT PK SQL id INTEGER PRIMARY KEY AUTOINCREMENT
Append-only No mutator export; grep-style smoke test
Three indexes (lookup, leaderboard, history_node) Three CREATE INDEX statements
TS types exported export type Domain, export interface ReputationRow, export interface ReputationHistoryRow
Migration idempotent initDb called twice in test; second call no-ops via user_version
selectReputation / selectHistory / insertHistoryEvent only Three named exports; nothing else with delete / update in the name

§5. Risks + gotchas

  1. Float silently accepted by SQLite. Source prompt §Common gotchas: SQLite’s INTEGER column accepts floats unless explicitly CHECK’d. The Zod validator on the TS side + integer-only prepared-statement parameter binding closes this at the TS boundary; we do NOT add typeof(score) = 'integer' to the CHECK because that would also reject raw INSERTs from SQL tools (e.g. a future migration script that seeds rows). The TS layer is the authority.

  2. Migration number collision. Listed src/db/migrations/ first; 007 is free. Locked.

  3. node_id foreign-key. Source prompt does NOT specify a FK to any existing table. The nodes / identities table is Phase 2+ (ξ Soul Vector); not shipped yet. So node_id is a plain TEXT with no FK constraint. This matches the Phase 0 trail / skills pattern (their task_id / agent_id columns also lack FK constraints in Phase 0).

  4. AUTOINCREMENT vs ROWID. Source prompt says id INTEGER PRIMARY KEY AUTOINCREMENT. SQLite distinguishes ROWID (recycled) from AUTOINCREMENT (never reused). Append-only semantics + audit grade favor AUTOINCREMENT. Locked.

  5. epoch semantics. Source prompt + bps-constants are silent on whether epoch is “global wall-clock epoch” or “per-domain decay-clock epoch”. For P2.1.1 (schema only) we store it as plain INTEGER and let P2.1.2 / P2.2.1 define the semantics. No CHECK on epoch.

  6. Test layout. Existing convention is src/__tests__/domains/<domain>/ (CLAUDE.md §9.1). New: src/__tests__/domains/reputation/schema.test.ts. The directory src/__tests__/domains/reputation/ does not exist yet — Jest ESM will pick up the new path automatically.

  7. Symbolic as const tuple ordering. The 5-domain enum must be stable across the codebase: if a downstream test hardcodes 4 domains in a fixture, it breaks. Greenfield; nothing downstream exists yet.

§6. Decision tree before contract step

Locked at audit close:

  • Files: 3 source files (schema.ts, migration SQL, test file).
  • Migration number: 007.
  • node_id type: string (TEXT in SQL, no FK).
  • score JS type: number.
  • ban_until_epoch JS type: number | null.
  • Mutator policy: zero update* / delete* exports in the domain module.
  • Test runtime: in-memory better-sqlite3 db; runs full migration pipeline.
  • Bps consumption from src/domains/rules/bps-constants.ts: import BPS_MIN and BPS_MAX for the Zod validator (min(Number(BPS_MIN)).max(Number(BPS_MAX))) so the constraint is single-sourced. Do not duplicate 0 / 10000 as bare literals in the TS file.

Carrying forward to contract step.


Back to top

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

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