Packet: P0.8.3 η Merkle Root Finalization Tools

Task: P0.8.3 — execution plan Contract: docs/contracts/merkle-tools-contract.md Audit: docs/audits/merkle-tools-audit.md


1. File-by-file plan

1.1 New: src/db/migrations/006_eta.sql

Create file with the exact body from contract §6. No TypeScript — pure SQL. After write, npm test should apply the migration automatically on in-memory databases because the test helpers load the .sql files directly (src/__tests__/domains/trail/repository.test.ts:52-64 shows the pattern).

1.2 New: src/tools/merkle.ts

Module layout:

1. File header (doc block)
2. Imports: node:crypto, zod, better-sqlite3 types, db/index (getDb), server (registerColibriTool, ColibriServerContext),
   domains/proof/merkle (buildMerkleTree, EMPTY_TREE_ROOT, MerkleProof unused but re-exported if needed)
3. Types: AuditSession, MerkleRootRow, handler error shapes
4. Zod schemas (3 inputs)
5. Repository-style functions:
     - startAuditSession(db, input, options) → AuditSession
     - finalizeMerkleRoot(db, session_id, options) → MerkleRootRow
     - getMerkleRoot(db, session_id) → MerkleRootRow | null
6. Helpers:
     - fetchSessionHashes(db, session_id) → string[]
     - readAuditSession(db, session_id) → AuditSession | null
7. Error classes (3): AuditSessionNotFoundError, AuditSessionExistsError, AlreadyFinalizedError
8. registerMerkleTools(ctx) — wires all 3 tools

All functions take db as first argument for testability. The MCP handlers lazy-resolve getDb() at call time.

1.3 New: src/__tests__/tools/merkle.test.ts

Test organization (one describe per concern, ~35 tests total):

describe('migration 005 — schema shape')
describe('startAuditSession — happy path')
describe('startAuditSession — idempotence / uniqueness')
describe('finalizeMerkleRoot — happy path')
describe('finalizeMerkleRoot — AC4 zero-record rejection')
describe('finalizeMerkleRoot — already-finalized rejection')
describe('finalizeMerkleRoot — unknown session rejection')
describe('finalizeMerkleRoot — 5-record manual computation (AC)')
describe('finalizeMerkleRoot — order independence')
describe('getMerkleRoot — happy path')
describe('getMerkleRoot — unfinalized session')
describe('registerMerkleTools — registration')
describe('Zod schema validation')

1.4 Edit: src/server.ts

Add one import and one call in bootstrap():

+import { registerMerkleTools } from './tools/merkle.js';
...
    registerTaskTools(ctx);
+   // P0.8.3: register η Proof Store tools — audit_session_start, merkle_finalize, merkle_root.
+   registerMerkleTools(ctx);
    await start(ctx);

1.5 Edit: src/db/schema.sql

Add to the existing η section (-- P0.8 η Proof Store → migrations/006_eta.sql):

-- η Proof Store — introduced by 006_eta.sql (P0.8.3).
--   audit_sessions — one row per audit_session_start call. PK: session_id.
--                    Columns: intent, task_id (opaque), started_at.
--   merkle_roots   — one row per finalized session. PK: session_id.
--                    Columns: root (64-char hex), record_count, finalized_at.
--   thought_records.session_id (new nullable column) — groups records into
--                    audit sessions for merkle_finalize's collection query.

Pure commentary — schema.sql is not executed.


2. Test plan (exhaustive)

2.1 Migration 005 schema shape

Test Assertion
005 adds audit_sessions table sqlite_master contains audit_sessions
005 adds merkle_roots table sqlite_master contains merkle_roots
005 adds session_id column to thought_records PRAGMA table_info('thought_records') returns a row for session_id
005 adds idx_trail_session index sqlite_master contains idx_trail_session

2.2 startAuditSession (repository function)

Test Assertion
returns a UUID-shaped session_id matches /^[0-9a-f-]{36}$/i
persists exactly one row SELECT COUNT(*) = 1
writes intent verbatim row.intent === input.intent
task_id optional task_id: null when not provided
started_at is ISO-8601 parses + re-serializes to same string
accepts injected session_id (test seam) returned matches provided
rejects duplicate session_id second call with same id throws AuditSessionExistsError

2.3 finalizeMerkleRoot

Test Assertion
happy path — writes merkle_roots row SELECT COUNT(*) FROM merkle_roots = 1
returns root matching manual buildMerkleTree equal hex string
returns record_count equal to inserted records integer
returns finalized_at ISO-8601 parses successfully
AC4: zero records → throws ERR_NO_RECORDS error.code === ‘ERR_NO_RECORDS’
unknown session → throws ERR_SESSION_NOT_FOUND error.code matches
already finalized → throws ERR_ALREADY_FINALIZED second call errors
AC5: 5-record session → root matches manual computation headline acceptance test
order independence — 5 records inserted in reverse → same root equality
uses ORDER BY rowid ASC not created_at two records with identical timestamp still produce deterministic root

2.4 getMerkleRoot

Test Assertion
happy path returns all 4 fields session_id, root, record_count, finalized_at
unfinalized session returns null getMerkleRoot(...) === null

2.5 MCP handler envelope shape (called via InMemoryTransport)

Test Assertion
audit_session_start returns {ok:true, data:{session_id, ...}} JSON wire envelope
merkle_finalize on zero records returns {ok:false, error:{code:'ERR_NO_RECORDS',...}}  
merkle_root on unfinalized returns {ok:false, error:{code:'ERR_NOT_FINALIZED',...}}  

2.6 Tool registration

Test Assertion
registerMerkleTools(ctx) adds 3 names to ctx._registeredToolNames Set has all 3
second call throws duplicate-name guard triggers
server_ping + server_health still work in the same ctx no collision

2.7 Zod validation

Test Assertion
empty intent rejected safeParse fails
empty session_id rejected safeParse fails
wrong type on task_id rejected safeParse fails

3. Migration plan

3.1 Apply path

  • initDb(path) in src/db/index.ts reads every NNN_*.sql in src/db/migrations/, sorts numerically, applies in a transaction per file while bumping PRAGMA user_version.
  • Adding 006_eta.sql as the 5th migration file causes user_version to advance from 4 → 5 on next boot.
  • Migration is idempotent: re-running the same initDb(path) is a no-op (step skipped because user_version = 5).

3.2 Test-suite path

  • Test helpers pattern: read .sql file via fs.readFileSync, pass to db.exec(). The new test file MUST read ALL prior migrations + 005 together, in the right order: 002 → 003 → 004 → 005. (In-memory tests do not go through initDb; they hand-load only the migrations they need.)
  • Because 006_eta.sql has an ALTER TABLE thought_records ADD COLUMN, it depends on 003_thought_records.sql being applied first. The test helper enforces that order.

3.3 No migration backtracking

  • SQLite has no DROP COLUMN in this version; once shipped, session_id is permanent on thought_records. This is fine — the column is nullable and the P0.7.2 API ignores it.

4. Rollback strategy

4.1 Per-commit rollback

  • Each of the 5 steps (audit / contract / packet / implement / verify) is a separate commit. Rolling back a step is a git revert <sha> away.

4.2 Migration rollback

  • We do NOT ship a down-migration. If 006_eta.sql lands in production and needs removal:
    • New migration 006_drop_eta.sql that drops the two tables + the index.
    • The session_id column on thought_records cannot be dropped (SQLite limitation) — it stays as a harmless NULL-column. This is acceptable.

4.3 Tool-registration rollback

  • If the PR lands and then needs to be rolled back without a schema revert, simply revert the src/server.ts change. The migration stays in place; the tables become dormant (no code reads/writes them). No functional impact.

4.4 Data integrity under partial rollback

  • merkle_roots entries are immutable by design. A rollback of the tool code does not invalidate any finalized root already persisted — it just makes it unreadable via MCP until the tool is re-registered.

5. Implementation order (commit 4 — feat(p0-8-3))

Single commit with all code + migration + test:

  1. Write src/db/migrations/006_eta.sql.
  2. Write src/tools/merkle.ts.
  3. Edit src/server.ts (import + call).
  4. Edit src/db/schema.sql (commentary).
  5. Write src/__tests__/tools/merkle.test.ts.
  6. Run npm run lint:fix if available, then npm test locally.
  7. If green, commit.
  8. If red, iterate until green — each code-diff is edit+re-run, single commit at end.

6. Risk register

R Risk Mitigation
R-1 ALTER TABLE in SQLite is restrictive Only ADD COLUMN used — fully supported in every SQLite ≥3.2.0 (better-sqlite3 ships far newer).
R-2 Test flake from CLI rowid vs. created_at All queries use ORDER BY rowid ASC explicitly.
R-3 merkletreejs sort options forgotten We consume P0.8.1’s buildMerkleTree which already sets both sortPairs: true + sortLeaves: true. No direct merkletreejs import in this task.
R-4 Concurrent merkle_finalize calls produce duplicate rows Each finalize runs inside db.transaction. The PK UNIQUE on merkle_roots.session_id short-circuits the second call to ERR_ALREADY_FINALIZED.
R-5 getDb() called at tool registration time (not call time) Pattern copied from src/domains/trail/repository.ts:377 — handler body calls getDb(), not the outer closure.
R-6 Cross-worktree file leak (Wave C lesson) git status was clean before Step 1; every edit is inside the worktree; no main-checkout edits.
R-7 Jest + Zod mocking hazard We do not mock Zod — we only use it as documented. jest.isolateModulesAsync is not used anywhere in this test file.
R-8 Lint rule on :memory: connection leak We add afterEach(() => db.close()) in every describe that opens a DB.
R-9 Writeback ordering violation in PR body The PR body records the summary BEFORE any theoretical merkle_finalize call. There is no live MCP client during this executor run, so the ordering is trivially satisfied.

7. Commit plan

# Commit message Files
1 audit(merkle-tools): inventory surface docs/audits/merkle-tools-audit.md
2 contract(merkle-tools): behavioral contract docs/contracts/merkle-tools-contract.md
3 packet(merkle-tools): execution plan docs/packets/merkle-tools-packet.md (this file)
4 feat(p0-8-3): η merkle tools — merkle_finalize/merkle_root/audit_session_start src/tools/merkle.ts, src/__tests__/tools/merkle.test.ts, src/db/migrations/006_eta.sql, src/server.ts, src/db/schema.sql
5 verify(merkle-tools): test evidence docs/verification/merkle-tools-verification.md

Back to top

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

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