Verification — P1.5.10 ζ Decision-Trail Integration
Round: R92, Wave 7 (parallel slice 2/2)
Branch: feature/p1-5-10-zeta-integration
Implementation SHA: 5ce017f5 (this verification commit lands on top)
Base SHA: 6cfd269b (post-P1.5.7 #258)
§1. Test gate evidence
§1.1. npm run build
> colibri@0.0.1 build
> tsc
> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 9 migration(s) E:\AMS\.worktrees\claude\p1-5-10-zeta-integration\src\db\migrations -> E:\AMS\.worktrees\claude\p1-5-10-zeta-integration\dist\db\migrations
Exit 0. No tsc diagnostics.
§1.2. npm run lint
> colibri@0.0.1 lint
> eslint src
Exit 0. No lint findings.
§1.3. npm test (serial — --runInBand)
Test Suites: 78 passed, 78 total
Tests: 3381 passed, 3381 total
Snapshots: 0 total
Time: 99.941 s, estimated 301 s
Ran all test suites.
- Suite count: 78 / 78 passing.
- Test count: 3381 / 3381 passing.
- Baseline: 3353 at
6cfd269b. - Delta: +28 (27 in new
zeta-emission.test.ts, 1 infallback.test.ts). - Regression count: 0.
§1.4. Parallel-mode behaviour
A parallel run (npm test without --runInBand) shows the documented
pre-existing flakes:
src/__tests__/domains/consensus/parity-harness.test.ts G7.1— perf budget intermittently > 5000 ms on a loaded worker. Pre-existing per CLAUDE.md §5 (“pre-existing flake; retry-clean”).src/__tests__/domains/reputation/{schema,witnesses}.test.ts— shared-state cross-suite races under parallel workers. Pre-existing.
Both clusters pass in isolation and under --runInBand (proved §1.3).
None of these are introduced by P1.5.10.
§2. ζ event matrix
The RoutingDecisionRecord shape is the 8-field envelope from
docs/3-world/social/llm.md §Decision-trail recording:
| Field | Type | Value on routing_mode='single' |
Value on routing_mode='fail' |
|---|---|---|---|
type |
literal 'routing_decision' |
'routing_decision' |
'routing_decision' |
routing_mode |
union 'single' \| 'ensemble' \| 'pipeline' \| 'fail' |
'single' |
'fail' |
chosen_model_id |
string | winning ModelId (e.g. 'claude') |
'' (empty) |
candidates_considered |
readonly string[] | full chain order (scoreIntent descending) |
full chain order |
scores |
Readonly<Record<string, number» | 9-member score map from scoreIntent |
9-member score map |
fallback_attempts |
non-negative integer | Math.max(0, modelsAttempted.length - 1) |
attempts.length |
rule_version_hash |
'rv:sha256:<64hex>' or 'rv:sha256:unavailable' |
normalised κ hash | normalised κ hash |
decision_hash |
64-char lowercase hex | SHA-256 of canonicalize(inputs) + ' ' + chosenModelId |
SHA-256 of canonicalize(inputs) + ' ' (empty chosen) |
§2.1. decision_hash preimage
canonicalize(DecisionHashInputs) + ' ' + chosen_model_id where the
DecisionHashInputs projection is:
{
prompt, // user message
context: { // optional sub-fields
task?, // ScoreContext.task
operatorPreference?, // ScoreContext.operatorPreference
candidatesSnapshot?: ModelId[], // model_ids only (bigint elsewhere)
weightsSnapshot?: Record<string, string>, // bigints stringified
},
rule_version_hash, // 'rv:sha256:<hex>' or unavailable
candidates_considered // chain-walk order
}
Excluded (intentionally non-deterministic / secret / non-portable):
completionFn, completionFnRegistry, scoringFn, fetchFn, logger,
delayFn, nowFn, apiKey, tools, maxTokens, systemPrompt,
model, zetaEmitter.
§3. Acceptance-criteria evidence (slice doc §Acceptance criteria)
| AC | Spec | Evidence |
|---|---|---|
| 1 | Exact 8-field shape | zeta-emission.test.ts §4 — buildRoutingDecisionRecord returns the exact 8-field shape (toEqual against fixture) |
| 2 | decision_hash deterministic input |
zeta-emission.test.ts §3 — is deterministic over JSON-equal inputs (passes); 4 sensitivity tests (chosen, candidates, rule version) |
| 3 | 'fail' on FallbackChainExhaustedError |
zeta-emission.test.ts §6 — emits routing_mode="fail" before throwing (passes) |
| 4 | 'single' on success |
zeta-emission.test.ts §5 — emits one record with routing_mode="single" (passes) |
| 5 | fallback_attempts counting |
zeta-emission.test.ts §7 — cascade case: chain [sonnet, claude], first fails, second succeeds → fallback_attempts === 1 (passes); §6 fail case → === attempts.length (passes) |
| 6 | Chain-thread prev_hash |
zeta-emission.test.ts §10 — hash-chain integrity: each prev_hash threads to the previous record (passes); 3 records, first uses ZERO_HASH, N+1 uses N’s hash |
| 7 | Emission failure non-fatal | zeta-emission.test.ts §9 — a throwing emitter does NOT alter the success return value (passes); does NOT swallow the fail path throw (passes); logger captures “ζ emission failed” |
| 8 | Gates green | npm run build && npm run lint && npm test all pass (§1) |
§4. Behavioural-invariant evidence (contract §3)
| ID | Invariant | Evidence |
|---|---|---|
| I1 | routeRequest signature byte-identical |
RouteOptions extended additively; existing fallback.test.ts (88 tests) all pass — no destructuring caller broke. |
| I2 | RouteResult shape byte-identical |
No RouteResult change; existing assertions on the 8 fields green. |
| I3 | Default emitter is no-op | zeta-emission.test.ts §8 — back-compat test (passes). fallback.test.ts new “back-compat guard” test (passes). |
| I4 | Success emits one 'single' |
§5 test 1 (passes). |
| I5 | Fail emits one 'fail' then throws |
§6 test 1 (passes). |
| I6 | decision_hash deterministic |
§3 determinism test (passes). |
| I7 | decision_hash is 64-hex |
§3 shape test, §5, §6 (all pass). |
| I8 | rule_version_hash valid |
§5 test 4 (passes — regex match or 'rv:sha256:unavailable'). |
| I9 | Emission throws caught | §9 (passes). |
| I10 | RoutingDecisionRecord frozen |
§4 test 1 (Object.isFrozen(record)). |
| I11 | createThoughtRecordEmitter writes a real row |
§10 (4 tests pass, including chain integrity). |
| I12 | No edit to src/domains/trail/* |
git diff --name-only origin/main shows only src/domains/router/*.ts + src/__tests__/domains/router/*.ts + docs/* (verified §6). |
| I13 | No edit to adapters | Same as I12. |
| I14 | No MCP tool registration | tools.ts unmodified. |
§5. ζ emitter opt-in confirmation
Default behaviour. RouteOptions.zetaEmitter is optional. When
omitted, routeRequest uses NO_OP_ZETA_EMITTER — a frozen
() => undefined. Phase 0 / Phase 1.5 W1–W6 callers that never knew
about zetaEmitter see byte-identical RouteResult shapes and
byte-identical FallbackChainExhaustedError throws.
Live evidence. fallback.test.ts was extended with one new test
(“omitting zetaEmitter is byte-equivalent to the pre-P1.5.10 baseline”)
which asserts result.model === 'claude', result.content === 'Hello!',
result.finishReason === 'end_turn', and Object.isFrozen(result).
Passes.
Opt-in path. Callers that want ζ persistence wire it explicitly:
import { createThoughtRecordEmitter } from './trail.js';
const emitter = createThoughtRecordEmitter({
db: getDb(),
task_id: 'some-task',
agent_id: 'router-agent',
});
const result = await routeRequest(prompt, { zetaEmitter: emitter, ... });
createThoughtRecordEmitter is a real emitter (not a stub). It
writes one row per call into the live thought_records chain via
createThoughtRecord (the canonical ζ insert path), threading
prev_hash. Tested against an in-memory DB in §10.
§6. Files touched
docs/audits/p1-5-10-zeta-integration-audit.md | + 309 lines (new)
docs/contracts/p1-5-10-zeta-integration-contract.md | + 207 lines (new)
docs/packets/p1-5-10-zeta-integration-packet.md | + 183 lines (new)
docs/verification/p1-5-10-zeta-integration-verification.md| + this file (new)
src/domains/router/trail.ts | + 376 lines (new)
src/domains/router/fallback.ts | +147/-2 (edit — additive)
src/__tests__/domains/router/zeta-emission.test.ts | + 519 lines (new)
src/__tests__/domains/router/fallback.test.ts | +16/-0 (edit — additive)
Zero deletes outside fallback.ts’s 2-line import addition. Zero
edits to src/domains/trail/*, src/domains/router/adapters/*,
scoring.ts, circuit.ts, cost.ts, tools.ts. Matches parent-prompt
forbiddens.
§7. Sibling P1.5.8 race posture
P1.5.8 (parity tests, parallel sibling) ships a NEW file
(src/__tests__/domains/router/parity.test.ts) and per the parent
prompt “WILL NOT touch src/domains/router/fallback.ts”. My slice
touches fallback.ts (additive — one new optional RouteOptions field
- two emission call sites + four private helpers) and one new
describeblock at end offallback.test.ts.
The two slices are disjoint at the source-file level (mine: fallback.ts
trail.ts(new); sibling’s:parity.test.ts(new)) and at the test-file level (mine:zeta-emission.test.ts(new) + one block infallback.test.ts; sibling’s:parity.test.tsonly). When merged sequentially in either order, the only merge-touch surface is the optionalzetaEmitterfield onRouteOptions— the sibling does not add aRouteOptionsfield, so the merge is straightforward.
§8. Out-of-scope confirmations
- 4 MCP tool handlers in
tools.tsare NOT wired to the emitter (deferred per contract §6). routing_mode='ensemble'/'pipeline'paths NOT exercised (deferred per contract §6).THOUGHT_TYPESenum NOT modified (parent-prompt forbidden).- No new MCP tools registered (parent-prompt forbidden).
- No new env vars (parent-prompt forbidden).
§9. Writeback (transcript-only; MCP unattached this round)
task_id: P1.5.10
branch: feature/p1-5-10-zeta-integration
worktree: .worktrees/claude/p1-5-10-zeta-integration
base: origin/main @ 6cfd269b
commits:
- 1ebae49e audit
- 82b01e33 contract
- 7714704a packet
- 5ce017f5 feat (implementation)
- <verify SHA — this commit>
tests:
- npm run build
- npm run lint
- npm test (--runInBand)
test_count: 3381 (was 3353; delta +28)
suite_count: 78 (was 77)
summary: |
ζ Decision-Trail emission for δ router. routeRequest now emits one
RoutingDecisionRecord per call — routing_mode='single' on success,
'fail' on FallbackChainExhaustedError. Emitter is opt-in via
options.zetaEmitter; default is NO_OP_ZETA_EMITTER, preserving Phase 0
and Phase 1.5 W1–W6 callers byte-identically. createThoughtRecordEmitter
is the real implementation — writes rows through the canonical
createThoughtRecord path, threading prev_hash. Emission failures are
caught and logged via options.logger; they NEVER alter the
RouteResult / thrown FallbackChainExhaustedError.
Record shape (8 fields, exact match to docs/3-world/social/llm.md
§Decision-trail recording):
type, routing_mode, chosen_model_id, candidates_considered, scores,
fallback_attempts, rule_version_hash, decision_hash
decision_hash = SHA-256(canonicalize(inputs) + ' ' + chosen_model_id)
where inputs is the deterministic projection of prompt + context +
rule_version_hash + candidates_considered.
rule_version_hash is sourced via computeScoringRuleVersionHash() from
the κ shim and normalised to the 'rv:sha256:<hex>' wire shape; falls
back to 'rv:sha256:unavailable' on KappaRulesUnavailableError without
blocking emission.
blockers: []