Attempt log — S2-01 (Semantic checkpoints)¶
Attempt 1 — 2026-05-25 — GREEN¶
Executor: phase-story-executor (Claude Opus 4.7) Outcome: All 17 acceptance criteria satisfied; full Phase-6 + checkpoint test suite green (268 new + amended tests pass). Total new tests landed: 56 unit + property + integration + fence tests across 10 new files + 3 amended files.
Files landed¶
Source (new):
- src/codegenie/workflows/checkpoints.py — CheckpointStore Protocol (runtime_checkable, 5 methods), _SEMANTIC_BOUNDARY_KINDS (closed frozenset of 6), _MAX_EVENT_BYTES = 65_536, _GENESIS_CHAIN_HEAD, _assert_boundary helper, _canonical_event_bytes helper.
- src/codegenie/workflows/sqlite_checkpoints.py — production WAL adapter, per-workflow SQLite file under root/<workflow_id>/checkpoints.sqlite, per-workflow fcntl.flock lock file, clock injection, __slots__, sanitizer wired.
- src/codegenie/workflows/in_memory_checkpoints.py — test adapter mirroring the same Protocol; same sanitizer/cap/boundary discipline so AC-6 byte-equality holds.
- src/codegenie/workflows/errors.py — CheckpointPayloadTooLargeError with error_id = "workflows.checkpoint_payload_too_large".
Source (amended):
- src/codegenie/output/sanitizer.py — added sanitize_for_persistence(bytes) -> bytes, a thin wrapper over redact_raw_artifact_bytes so the canonical regex set has a single declaration site.
Tests (new):
- tests/unit/workflows/test_checkpoint_store_protocol.py — AC-1 (4 tests).
- tests/unit/workflows/test_semantic_boundary_set.py — AC-3 + AC-4 (12 tests).
- tests/unit/workflows/test_checkpoint_sqlite_schema.py — AC-5 (2 tests).
- tests/unit/workflows/test_checkpoint_payload_cap.py — AC-10 (4 tests).
- tests/unit/workflows/test_checkpoint_clock_and_between_boundary.py — AC-13 (3 tests).
- tests/unit/workflows/test_all_unchanged_after_s2_01.py — AC-2 (2 tests).
- tests/property/_phase6_event_strategies.py — shared Hypothesis strategy + factory parametrize lists.
- tests/property/test_checkpoint_chain_forward.py — AC-7 (6 tests, parametrized over both adapters).
- tests/property/test_checkpoint_read_ordering.py — AC-8 (4 tests, parametrized).
- tests/property/test_checkpoint_partial_write.py — AC-11 (2 tests).
- tests/integration/test_checkpoint_store_parity.py — AC-6 (3 tests).
- tests/integration/test_checkpoint_store_parity_meta.py — AC-17 (1 test).
- tests/integration/test_checkpoint_golden_ordering.py — AC-9 (2 tests).
- tests/fence/test_checkpoint_sanitizer_imports.py — AC-12 (3 tests, AST + runtime).
- tests/fence/test_checkpoint_adapter_slots.py — AC-14 (3 tests, AST + runtime).
Tests (amended):
- tests/integration/test_phase6_sut_contract_snapshot.py — AC-15 extension: added checkpoint_store_protocol, checkpoint_store_is_runtime_protocol, semantic_boundary_kinds, max_event_bytes, checkpoint_sqlite_schema to the snapshot; extended classifier with checkpoint-substrate diff rules.
- tests/integration/test_phase6_sut_contract_snapshot_meta.py — AC-15 meta: 6 new synthetic deltas (additive new Protocol method, breaking removed boundary kind, breaking narrowed _MAX_EVENT_BYTES, additive widened cap, breaking runtime_checkable removed, breaking signature changed).
Goldens:
- tests/golden/phase6-checkpoint/sqlite_schema.sql — AC-5 byte-equal sidecar.
- tests/golden/phase6-checkpoint/clean_completion_chain.json — AC-9 chain golden.
- tests/golden/phase6-contract/snapshot.json — regenerated under PHASE6_CONTRACT_GOLDEN_REWRITE=1 after AC-15 extension.
Mutation-resistance checks performed¶
- AC-1 method-set: dropping any of the five methods fails
test_checkpoint_store_exposes_exactly_five_methodsbyte-loud. - AC-3 boundary set: dropping
failed_unrecoverablefailstest_ac3_terminal_kinds_are_boundary_kinds(terminal partition discipline); addingneeds_planfailstest_ac3_needs_plan_is_not_a_boundary. - AC-4 directive substring: the parametrized non-boundary test checks for "Phase-6 checkpoint policy violation" + "ADR-0003" + the literal
next_state_id. - AC-5 schema golden: any whitespace drift between
_CHECKPOINT_SCHEMA_SQLand the.sqlsidecar fails byte-loud. - AC-6 parity: a buggy adapter using
hash()or_GENESIS_CHAIN_HEADfor every append fails the byte-equality assertion across both stores. - AC-7 chain-forward: a store that swallows
prior_headfails onmin_size=2Hypothesis examples; a store that shares heads across workflows fails the cross-workflow-isolation sub-property. - AC-8 ordering: a store that sorts by
transition_idornext_headfails the append-order property; one that omits the workflow filter fails cross-workflow. - AC-9 golden: any change to
_compute_chain_headbyte-stability orTransitionEventschema fails the byte-equal assertion with a clear additive-vs-breaking directive. - AC-10 cap: a swap to cap at the model layer would break the forensic EventLog;
test_ac10_cap_lives_at_store_layer_not_model_layerconstructs an oversized event the model accepts but the store rejects. - AC-11 detection-only: an executor "helpfully" recomputing inside
tail_chain_headwould return a recomputed-different-from-persisted value; my test tampers the row and asserts the tampered value is returned. - AC-12 sanitizer: any local
re.compilein either adapter trips the AST fence; the secret-shape property test injects anAKIA...token and asserts redaction in persisted bytes. - AC-13 clock determinism: any executor that calls
datetime.now()directly cannot inject a frozen clock, breaking the golden ordering test. - AC-14
__slots__: a typo'dself._connetcion = ...assignment raisesAttributeErrorat runtime (third test asserts this directly). - AC-15 contract snapshot: any
==→!=mutation in the additive/breaking classifier is caught by the 6 new meta-tests exercising checkpoint-shaped deltas. - AC-17 parity meta: a reversed-reader adapter passing the parity test would be a tautology; the meta-test asserts the parity invariant fires.
Decisions of record¶
_GENESIS_CHAIN_HEADdeclared locally (not re-exported from Phase-3 events.py). The Phase-3GENESIS_CHAIN_HEADis aBlobDigest, not aChainHead; the two are different newtypes. Phase-3 declared its own; Phase-6 declares its own. Both are bare 64-hex zeros — convention preserved.- Sanitization is write-time only; chain head is computed over the live event. AC-12 says "the store's serialization path ... MUST pass through sanitize_for_persistence" — that's the bytes written to the row. The chain head input is the live
event(which retains the cleartext) so byte-equality between adapters holds even when the on-disk bytes are redacted. The S2-02 verifier will recompute against the persisted bytes; if a redaction triggers, that's a deliberate divergence the verifier policy can flag — but detection-substrate-only is preserved here. - Per-example tmp subdir for Hypothesis tests. Without a fresh sub-directory per example, the SQLite file accumulates state across
@givenexamples within one test invocation. The_EXAMPLE_COUNTERmodule-level dict +_fresh_subdir(tmp_path)helper isolates each example. - Unique
transition_idwithin Hypothesis lists. Usedunique_by=lambda e: e.transition_idon thest.listsstrategy so the SQLite UNIQUE constraint never trips within a single example draw. InMemoryCheckpointStoreaccepts (and ignores) arootargument. Required by the parity contract fixture which constructsfactory(tmp_path)for both adapters. Mirrors the Phase-3InMemorySink()convenience (no-arg) but extended for parity-test ergonomics.- In-memory adapter stores sanitized bytes + parses on read. Mirrors the SQLite adapter exactly so the parity contract test (AC-6) compares byte-for-byte across adapters. An alternative — storing the live event in-memory and skipping the round-trip — would have been faster but would have broken byte-parity when sanitization triggers.
- Scenario #2 path corrected to 6 transitions / 5 boundary writes. Initial sequence had no non-boundary transition target so
len(boundary_writes) == 6; corrected to include thegate_failed_retryable → needs_planretry-branch (the one non-boundary edge today) so the test asserts the structural invariant per the story prose.
Refactor decisions¶
- Composition over inheritance for adapter sharing. Both
SqliteCheckpointStoreandInMemoryCheckpointStorecall free functions_assert_boundary,_canonical_event_bytesincheckpoints.py— noBaseCheckpointStoreABC, no mixin. Anti-refactor #1 honored. - No
CheckpointStoreRegistry. Two adapters today; rule-of-three threshold reached when Phase-9 landsPostgresCheckpointStore. Anti-refactor #2 honored. - No
CheckpointTransactioncontext manager. Theappend()body is six lines of imperative-shell SQL; wrapping it in a Command-pattern abstraction is over-design for one call site. Anti-refactor #3 honored. Callable[[], datetime]as the clock type, not aClockProtocol. AC-13 names the callable shape directly; no ceremony. Anti-refactor #6 honored.
Test counts touched¶
- Suite-level: 7321 passed, 44 skipped, 9 xfailed, 4 pre-existing flakes (lint-imports/pre-commit/mkdocs PATH issues; tsconfig perf flake) — all four flakes pass when re-run with the venv on PATH (env-related, not introduced by this story).
- Phase-6 checkpoint suite: 49 tests across 9 new files + 7 new tests on amended files (
test_phase6_sut_contract_snapshot.pyAC-15 row, meta-test 6 new cases). - Mypy: 243 source files clean under
--strict. - Ruff: all checks passed; ruff format clean.
- Import-linter: 12 contracts kept, 0 broken.
Notes for downstream stories¶
- S2-02 (replay verifier) dispatches on
store.tail_chain_head(wf)+store.read_all_for_workflow(wf); recomputes the chain via_compute_chain_headand rejects any divergence withFailedUnrecoverable(reason="checkpoint_integrity"). The AC-11 partial-write tamper test is exactly the failure mode S2-02 must surface. - S3-01 (plugin-local subgraph) constructs a
SqliteCheckpointStore(via constructor injection from the SUT adapter S5-01) and emitsTransitionEvents through the conditional edges. The boundary policy is enforced byappend()— the subgraph cannot accidentally checkpoint atneeds_plan. - S4-01 (HITL) reads the chain head + the latest
awaiting_human_reviewrow viastore.read_all_for_workflow(wf)filtered bynext_state_id; legalawaiting_human_review → plan_readytransition is already in S1-02's_LEGAL_TRANSITIONS. - S5-01 (LocalVulnRemediationSut) constructs the store via
SqliteCheckpointStore(root, clock=...)and passes it through the subgraph. The Postgres adapter swap (Phase-9 S5-01) is a one-line constructor change. - Phase-9 G5 byte-equality. The Protocol + parity-contract test is the substrate Phase 9 will assert byte-identical across SQLite and Postgres backends. Adding the Postgres adapter is one line in
ADAPTER_FACTORIES+ one new filepostgres_checkpoints.py.
Sanitization design point — surfaced for downstream review¶
The chain head is computed over the live event (which can contain cleartext), while the on-disk row is the sanitized bytes. On replay, S2-02 will recompute the chain over the persisted (sanitized) bytes, which would NOT match the persisted chain head if sanitization triggered. This is intentional:
- The detection (S2-01's
tail_chain_headreads what was persisted; never recomputes). - The policy (S2-02's verifier folds the persisted bytes and compares; if they differ, the workflow transitions to
FailedUnrecoverable(reason="checkpoint_integrity")).
If S2-02 wants to surface secret-redaction as a benign divergence vs. a tamper, it will need to know how to distinguish them. Two approaches: (i) S2-02 also runs sanitize_for_persistence on the live event before recomputing (mirrors the write path); (ii) we add a "redaction occurred" sentinel column to the SQLite schema. Both are S2-02 design decisions, deferred to that story. Surfaced for the S2-02 executor.
Follow-ups surfaced this attempt¶
- S2-02 sanitization-aware replay. Per the design point above, S2-02 must decide how to handle the live-event-vs-persisted-bytes divergence when sanitization triggered. Recommend mirroring the write path: S2-02's recomputation pipeline calls
sanitize_for_persistenceon the live event before folding. (Minor; S2-02 design decision.) PostgresCheckpointStore(Phase-9 S5-01). The Protocol + parity-contract test is the substrate. One line addition totests/property/_phase6_event_strategies.pyADAPTER_FACTORIES+ a newpostgres_checkpoints.pyadapter. (Out of scope; just the precedent.)