Packet — P0.7.3 ζ Chain Verification Tool
1. Objective
Implement src/domains/trail/verifier.ts (pure verifier + MCP tool registration)
and src/__tests__/domains/trail/verifier.test.ts (test suite), then wire the
new tool into src/server.ts. No migration. Closes the ζ Decision Trail axis.
2. Files to create or modify
| Action | Path |
|---|---|
| CREATE | src/domains/trail/verifier.ts |
| CREATE | src/__tests__/domains/trail/verifier.test.ts |
| MODIFY | src/server.ts (add import + registerVerifyChainTool call) |
3. Implementation plan
3.1 src/domains/trail/verifier.ts
Imports:
import type Database from 'better-sqlite3';
import { z } from 'zod';
import { getDb } from '../../db/index.js';
import type { ColibriServerContext } from '../../server.js';
import { registerColibriTool } from '../../server.js';
import { computeHash, ZERO_HASH, type ThoughtRecord } from './schema.js';
import { listThoughtRecords, type ListThoughtRecordsFilters } from './repository.js';
VerifyChainResult interface:
export interface VerifyChainResult {
readonly valid: boolean;
readonly broken_count: number;
readonly first_broken_at?: string;
}
verifyChain(records: ThoughtRecord[]): VerifyChainResult:
- Loop
i = 0..n-1. expectedPrevHash = (i === 0) ? ZERO_HASH : records[i-1].hash.recomputedHash = computeHash(records[i]).- Broken if
recomputedHash !== records[i].hashORrecords[i].prev_hash !== expectedPrevHash. - Track
brokenCount, setfirstBrokenAton first broken record. - Return
{ valid: brokenCount === 0, broken_count: brokenCount, first_broken_at: firstBrokenAt }.
AuditVerifyChainToolInputSchema:
export const AuditVerifyChainToolInputSchema = z.object({
task_id: z.string().min(1).optional(),
});
registerVerifyChainTool(ctx: ColibriServerContext): void:
- Registers
audit_verify_chainviaregisterColibriTool. - Handler: lazy
getDb(),listThoughtRecords(db, filters),verifyChain(records), return{ ...result, record_count: records.length }.
3.2 src/server.ts modifications
Add one import:
import { registerVerifyChainTool } from './domains/trail/verifier.js';
Add one call in the tool-registration section alongside registerThoughtTools(ctx):
registerVerifyChainTool(ctx);
3.3 src/__tests__/domains/trail/verifier.test.ts
Test groups:
Group A — verifyChain pure function:
- Empty array →
{ valid: true, broken_count: 0 }, nofirst_broken_at. - Single-record intact chain → valid.
- Two-record intact chain → valid.
- Content tampered on record[0] →
valid: false,broken_count: 1,first_broken_at = record[0].id. - Content tampered on record[1] (not record[0]) →
valid: false,broken_count: 1,first_broken_at = record[1].id. prev_hashbroken on record[1] (prev_hash does not match record[0].hash) →valid: false.- Multiple broken records →
broken_countequals number of broken records,first_broken_atis first. - 100-record intact chain →
valid: true, completes in < 500ms.
Group B — registerVerifyChainTool + DB integration:
audit_verify_chainvia MCP transport: empty DB →{ valid: true, broken_count: 0, record_count: 0 }.- Insert 3 records for same
task_id, call tool withtask_idfilter → valid. - Insert 3 records, tamper content via raw SQL update, call tool →
valid: falsewith correctfirst_broken_at. - Insert records for two different task_ids, call without filter → valid.
- Call with
task_idfilter that has no matching records →{ valid: true, broken_count: 0, record_count: 0 }.
Helper pattern: reuse 003_thought_records.sql migration + in-memory DB,
same pattern as repository.test.ts.
4. Key invariants to test
computeHashis called with the same 6-field subset as during creation — the verifier must NOT includeagent_idorhashin its recomputed input.prev_hashfor record at index 0 must beZERO_HASH.- Tamper of
prev_hashfield itself (not content) also triggers broken detection.
5. No migration
No new SQL file. No user_version bump. Purely additive TypeScript.
6. Commit template (Step 4)
feat(p0-7-3): ζ audit_verify_chain tool — verifier.ts + MCP wiring + tests
7. Test gate
npm run build && npm test
Known pre-existing flake: startup-subprocess smoke test may fail intermittently on CI (predates this task). Note in PR body if it hits; do not chase it.