ADR-0005: RetryLedger startup verifies Phase 4 chain-head compatibility¶
Status: Accepted Date: 2026-05-12 Tags: audit-chain · phase-boundary · cross-phase-test Related: ADR-0002, ADR-0007, production ADR-0014
Context¶
Phase 4 emits BLAKE3-chained audit events (solved_example.duplicate_skipped, engine_used stamping). Phase 5's RetryLedger extends that same chain: every attempt's chain_hash = blake3(prev_hash || canonical_json(attempt)). The critic's roadmap §6 attack was that "none of the three designs verified that Phase 4's chain events produce entries Phase 5 will consume." Without an explicit chain-compatibility test, Phase 4 can mutate its event shape, and Phase 5 will read corrupt or unparseable predecessor entries — likely silently. See final-design.md §Synthesis ledger and phase-arch-design.md §Component design — RetryLedger.
Options considered¶
- Trust the chain implicitly — Phase 5 reads the chain head; if BLAKE3 verifies, proceed. Catches tampering but not schema drift in Phase 4's event payloads (a re-named field passes BLAKE3 verification fine).
- Re-implement chain primitives in Phase 5 — Decouples Phase 5 from Phase 4's chain. Loses end-to-end tamper-evidence; reviewer can no longer trust the chain spans the LLM-decision boundary.
- Golden Phase 4 chain-head fixture + Phase 5 startup test — Capture a known-good Phase 4 chain head as a binary fixture; Phase 5's
RetryLedger.__init__reads.codegenie/remediation/<run-id>/chain_head.binand refuses to start if mismatch;tests/schema/test_phase4_chain_compat.pyregenerates the fixture and would fail loudly if Phase 4's shape drifts.
Decision¶
RetryLedger.__init__ accepts prev_chain_head: bytes | None, reads it from .codegenie/remediation/<run-id>/chain_head.bin (Phase 4's last write), and raises AuditChainCorrupted on mismatch. A binary fixture tests/golden/phase4_chain_head.bin plus an integration test verifies Phase 4's last entry produces a chain head whose shape Phase 5 can read. Startup test refuses to run any gate if compatibility fails.
Tradeoffs¶
| Gain | Cost |
|---|---|
| The audit chain spans Phase 4's LLM-decision boundary all the way into Phase 5's gate verdicts | Cross-phase coupling: Phase 4 cannot change its chain event shape without regenerating Phase 5's fixture |
| Startup refusal is loud and fail-loud per global rule 12 — silent chain drift is impossible | Operators who hand-edit attempts.jsonl or chain_head.bin (e.g., for forensics) will trigger AuditChainCorrupted |
| Tamper detection extends through every gate retry | Per-attempt record() fsyncs (~10 ms) — durability over throughput |
tests/golden/phase4_chain_head.bin is regenerated as part of any Phase 4 chain-shape PR — Phase 4 cannot drift silently |
The fixture is a tiny binary file checked into Git; its byte-stability is part of the test contract |
Consequences¶
RetryLedger.__init__is the load-bearing chain-compat enforcement point.tests/schema/test_phase4_chain_compat.pyregenerates the fixture; Phase 4 PR that changes event shape must include a Phase 5 fixture update — the diff signals the cross-phase change.codegenie sandbox inspect <gate-run-id>re-verifies the chain on every invocation;AuditChainCorruptedsurfaces with a clear remediation message.- The chain is the contract Phase 11 (handoff) consumes for evidence bundles.
- New invariant: any Phase 4 event-shape change requires regenerating
phase4_chain_head.binand an ADR amendment on either side that surfaces the cross-phase break. - The
attempts.jsonlfile is append-only with BLAKE3 per-line chain — secondrecord(Attempt(attempt_id=1, ...))raisesLedgerAttemptOutOfOrder.
Reversibility¶
Medium. The chain-compat test is independent of the chain primitive; the primitive could be replaced (e.g., move to Merkle tree). Removing the startup refusal would re-open silent drift — that part is hard to reverse without forfeiting tamper-evidence. The fixture/test pattern is portable.