Audit — P0.7.3 ζ Chain Verification Tool
1. Scope
Inventory the existing ζ Decision Trail surface in preparation for implementing
audit_verify_chain — the final tool that closes the ζ axis.
2. Existing surface (ζ axis files)
| Path | Purpose | Phase |
|---|---|---|
src/domains/trail/schema.ts |
Hash primitives: computeHash, canonicalize, ZERO_HASH, ThoughtRecordSchema |
P0.7.1 |
src/domains/trail/repository.ts |
CRUD: createThoughtRecord, getThoughtRecord, listThoughtRecords + thought_record / thought_record_list MCP tools |
P0.7.2 |
src/db/migrations/003_thought_records.sql |
thought_records table DDL |
P0.7.2 |
src/__tests__/domains/trail/repository.test.ts |
~835 baseline tests (partial count) | P0.7.2 |
Not yet created (this task’s outputs):
src/domains/trail/verifier.tssrc/__tests__/domains/trail/verifier.test.ts
3. Schema.ts — relevant primitives
computeHash(record) hashes the 6-field subset: {id, type, task_id, content, timestamp, prev_hash}.
agent_idis excluded (author metadata, not chain-integrity).hashis excluded (it is the output).- Output: 64-char lowercase hex SHA-256.
ZERO_HASH="0".repeat(64)— the genesisprev_hash.
Hash algorithm (canonical from P0.7.1 schema.ts):
canonical_input = canonicalize({id, type, task_id, content, timestamp, prev_hash})
hash = SHA-256(canonical_input, utf8)
Chain invariant: record.prev_hash === previous_record.hash for all non-genesis records.
4. Repository.ts — relevant functions
listThoughtRecords(db, filters) returns records ORDER BY rowid ASC — true insertion order.
- Optional
task_idfilter (string). - Optional
limitfilter (positive int).
Both task_id and session_id are relevant for filtering. However, the current schema has task_id (not session_id) as the scope column. The zeta extraction doc’s Python pseudocode uses session_id, but the Phase 0 implementation uses task_id. The audit_verify_chain tool should filter by task_id.
The audit_session_start tool (P0.8.3 / src/tools/merkle.ts) uses a separate audit_sessions table with task_id. The session context is preserved there. For audit_verify_chain, filtering by task_id is the correct Colibri Phase 0 scoping.
5. Server.ts registration pattern
Tools are registered via registerColibriTool(ctx, name, config, handler) where:
config.inputSchemais a Zod object schema.handler(input)returns the data payload (middleware wraps in{ok, data}envelope).
Existing tool registrations (P0.7.2 and P0.8.3) are loaded by src/server.ts at import time via named registration functions. The audit_verify_chain tool follows the same pattern:
- A new
registerVerifyChainTool(ctx)exported fromverifier.ts. - Imported and called from
server.tsalongsideregisterThoughtTools.
6. Migration status
NO migration required. audit_verify_chain is read-only against thought_records. No new tables, no new columns, no user_version bump.
7. Dependencies
src/domains/trail/schema.ts—computeHash,ZERO_HASH,ThoughtRecordsrc/domains/trail/repository.ts—listThoughtRecordssrc/db/index.ts—getDb()src/server.ts—registerColibriTool,ColibriServerContext
All are existing; no new packages.
8. Acceptance criteria (from spec)
verifyChain(records[])— iterates chain, recomputes each hash, checksprev_hashlinking.- Returns
{ valid: boolean, first_broken_at?: string, broken_count: number }. audit_verify_chainMCP tool — callsverifyChainon DB chain; optionaltask_idfilter.- Test: tamper one record’s content →
valid: falseat correct position. - Test: intact 100-record chain →
valid: truein < 500ms.
9. Risks and mitigations
| Risk | Mitigation |
|---|---|
| Hash input mismatch (verifier uses different fields than creator) | Use computeHash from schema.ts — same function the creator uses |
Session-vs-task filter confusion (extraction doc says session_id) |
Phase 0 table has task_id; use task_id; note in contract |
| Empty chain edge case | Return { valid: true, broken_count: 0 } |
| Performance for large chains | O(n) walk is sufficient; 100 records « 500ms threshold |