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). reputationstable — PK(node_id, domain),score INTEGER NOT NULL DEFAULT 0bounded[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_historytable —id INTEGER PRIMARY KEY AUTOINCREMENT, plusnode_id,domain,epoch,delta INTEGER(signed bps),reason,event_id. Append-only: no UPDATE or DELETE statements anywhere insrc/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:
Domainunion,DOMAINSreadonly array,ReputationRowinterface,ReputationHistoryRowinterface, Zod validators for both, plusselectReputation,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
-
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. -
Migration number collision. Listed
src/db/migrations/first;007is free. Locked. -
node_idforeign-key. Source prompt does NOT specify a FK to any existing table. Thenodes/identitiestable is Phase 2+ (ξ Soul Vector); not shipped yet. Sonode_idis a plain TEXT with no FK constraint. This matches the Phase 0 trail / skills pattern (theirtask_id/agent_idcolumns also lack FK constraints in Phase 0). -
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. -
epochsemantics. 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 onepoch. -
Test layout. Existing convention is
src/__tests__/domains/<domain>/(CLAUDE.md §9.1). New:src/__tests__/domains/reputation/schema.test.ts. The directorysrc/__tests__/domains/reputation/does not exist yet — Jest ESM will pick up the new path automatically. -
Symbolic
as consttuple 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_idtype:string(TEXT in SQL, no FK).scoreJS type:number.ban_until_epochJS 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: importBPS_MINandBPS_MAXfor the Zod validator (min(Number(BPS_MIN)).max(Number(BPS_MAX))) so the constraint is single-sourced. Do not duplicate0/10000as bare literals in the TS file.
Carrying forward to contract step.