IMP-46 frame transformation cache layer (IMP-33 짝) #62
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
관련 step: AI 호출 결과의 결정론적 재사용 — content-addressed cache
source: 신규 axis (2026-05-21 사용자 제안)
roadmap axis: R3 (AI 보정/재구성 보조)
wave: 1 (실질 구동 도달 필수)
priority: ★ IMP-33 짝
pair: IMP-33 (AI 호출 실선) — 같이 작업
dependency: IMP-33 의 AI hook
scope:
data/frame_cache/{frame_id}/{signature_hash}.json--auto-cacheflag)out of scope:
guardrail / validation:
cross-ref:
review loop:
IMP-46 frame transformation cache layer (IMP-33 ?)to IMP-46 frame transformation cache layer (IMP-33 짝)[Claude #1] Stage 1 problem-review — IMP-46
1. Source re-read + pair context
visual_check PASS + 사용자 OKor--auto-cache).c864fe0, AI fallback scaffolding u1~u11, flag default OFF). IMP-33 already shipped a u6 cache stub + a u8 step12 cache_key build site that is sample-specific. IMP-46 = replace the stub backend AND fix the key.route_ai_fallbackcall site (currently onlygather_step12_ai_repair_proposals).2. Root cause — what IMP-33 stub left unfinished (3-axis)
Axis A — u6 cache backend = NotImplementedError marker
src/phase_z2_ai_fallback/cache.py:79-82(verified):read_proposalreturnsNonefor any key (cache.py:36-45);save_proposalenforces both gates then raises NotImplementedError (cache.py:48-82). IMP-46 = replace the marker with a real backend (read + write JSON underdata/frame_cache/{frame_id}/{signature_hash}.jsonper issue spec). Gate semantics (visual_check_passed AND user_approved → write path) are already in place and must be preserved.Axis B — u8 cache_key is sample-specific (the critical defect)
src/phase_z2_ai_fallback/step12.py:109-111(verified):source_section_idsis the MDX section identifier (e.g.,"02.mdx::sec-0"). This means:This defeats the cache's reason to exist. The whole point of
feedback_no_hardcoding("signature hash 가 sample-specific case 만들지 않게") is to make the key structural, not source-identifier-based.Issue spec's required signature axes (verified against catalog + V4 evidence schema):
frame_idframe_contracts.yamlper-templateframe_id:(e.g.1171281190) —templates/phase_z2/catalog/frame_contracts.yaml:23v4_labellabel(light_edit/restructure) — passed asrecord["label"]cardinalitycardinality.strictOR V4 resultcardinality_signature—frame_contracts.yaml:27-29source_shapeframe_contract.source_shape(top_bullets/paragraph/table) —frame_contracts.yaml:26h3_countunit.raw_content)char_count_bucket<100,100-300,300-700,700+)layout_presetsidebar-right/two-column/hero-detail/single-column— Phase Z layout choicezone_positiontop/bottom_l/bottom_r— zone topology positionNone of these axes carry sample-specific identifiers. All are derivable from V4 result + frame contract + the unit's shape (not its source path). Hash → short hex digest.
Axis C — invalidation surface (3 sources)
Issue spec:
frame contract 변경 / partial template 변경 / catalog 업데이트 → 해당 frame cache 폐기. Verified sources at HEAD:templates/phase_z2/catalog/frame_contracts.yaml(loaded bysrc/phase_z2_mapper.py:49 load_frame_contractswith_CATALOG_CACHEglobal)templates/phase_z2/families/*.html(12 files at HEAD)templates/phase_z2/frames/*.html(2 files at HEAD:process_product_two_way.html,three_parallel_requirements.html)frame_contracts.yaml(addedsub_zones,accepted_content_types, etc.)Two strategies under consideration (Stage 2 lock target):
frame_contracts.yaml, (b) the family/frame partial HTML it references. On read, recompute and compare; mismatch → miss + log; never serve stale.Claude #1 preference = I1 — surgical, audit-clear, no manual counter maintenance, aligns with
feedback_no_hardcoding(no global state coupling).Axis D — cache value semantics (ambiguity in issue spec)
Issue spec cache value =
builder_options + partial_overrides + slot mapping (+ slide-level CSS). Mapped to IMP-33 u2 schema (src/phase_z2_ai_fallback/schema.py:22-25):builder_optionsBUILDER_OPTIONS_PATCHpartial_overridesPARTIAL_OVERRIDESslotsmay contain text contentslot mappingSLOT_MAPPING_PROPOSALslide-level CSSraw_css) —schema.py:28-30The slide-level CSS branch is forbidden by IMP-33 schema (
raw_cssinFORBIDDEN_KINDS). Issue spec's parenthetical "(+ slide-level CSS 있으면)" cannot be honored without expanding the u2 forbidden list, which would re-open IMP-17 carve-out. Scope-lock: drop "slide-level CSS" from cache value.The
PARTIAL_OVERRIDESpayload may contain text. Two interpretations:Claude #1 reading: issue spec's "결정론적 재사용" + "signature hash 가 sample-specific case 만들지 않게" guardrails point to D2. Stage 2 must lock; Codex review needed.
3. Scope-lock proposal (binding boundaries — Stage 2 will refine)
(a) Behavior delta — what changes, what does NOT
c864fe0)read_proposal(key)Nonefor any key (cache.py:42-45)data/frame_cache/{frame_id}/{signature_hash}.jsonif present + fingerprint valid; elseNonesave_proposal(key, proposal, *, visual_check_passed, user_approved)NotImplementedError; either gate False →AiFallbackCacheGateErrortemplate_id::source_section_ids(sample-specific)BUILDER_OPTIONS_PATCH/PARTIAL_OVERRIDES/SLOT_MAPPING_PROPOSAL); FORBIDDEN_KINDS includesraw_cssroute_ai_fallbackflow (u7)save_proposal(no producer yet)slide_status.user_approvedfield OR--auto-cacheCLI flag (single-shot per run) → propagated to callerdata/frame_cache/directory{frame_id}/)tests/phase_z2_ai_fallback/test_ast_isolation.py(IMP-33 u10) — package may NOT import Phase Q / Kei / pipeline runtime symbolsjson,hashlib,pathlib)tests/phase_z2_ai_fallback/test_cache.py(gate enforcement, NotImplementedError marker)(b) Signature (the key novel surface)
Bucket boundaries MUST be declared in
src/config.py(no inline literals) so future widening is anchor-driven. Stage 2 lock target.(c) Cache entry schema (the persisted JSON)
Stored at
data/frame_cache/{frame_id}/{signature}.json. On read: load → verifyfingerprints.frame_contract_subtree_sha256matches current catalog subtree → verifyfingerprints.partial_template_sha256matches current partial template content → if mismatch, returnNoneand log invalidation reason. Pydantic schema validation on parse.(d) Save trigger flow
Currently no site calls
save_proposal. IMP-46 introduces the producer:Save site = a new orchestration helper called from the pipeline AFTER
slide_status.visual_check_passedand AFTER the (existing) user-OK signal. Stage 2 must lock the exact wiring point. Candidate site: end ofphase_z2_pipeline.run_pipelinepost-visual-check loop.(e)
--auto-cacheflagIssue spec mentions
--auto-cacheas user_approved bypass. Plumbing: newsrc/config.pySettings fieldai_fallback_auto_cache: bool = False, env-overridable. Effective user_approved =(slide_status.user_approved OR settings.ai_fallback_auto_cache).Default OFF — preserves the gate's safety meaning. CI runs with default OFF → no cache writes happen on CI.
(f) Invalidation triggers (Stage 2 lock target)
4. Guardrails (Stage 2 binding)
feedback_ai_isolation_contract, PZ-1,IMP-17-CARVE-OUT.mdsource_section_ids/ NO MDX file path / NO sample identifier in signature input. Verified via unit test that 2 samples with different identifiers but identical structural axes produce the SAME signaturefeedback_no_hardcoding, issue spec "signature hash 가 sample-specific case 만들지 않게"json.loads().model_dump() == ...)AiFallbackCacheGateErrorsemantics preservedcache.py:69-78, issue specraw_cssis in IMP-33 u2FORBIDDEN_KINDS(schema.py:28-30); IMP-46 does NOT expand the whitelistfeedback_ai_isolation_contract, IMP-17 carve-outjson,hashlib,pathlib) + IMP-33 u2 schema. NO Phase Q / Kei / pipeline runtime imports. Verified viatest_ast_isolation.pyrerundata/frame_cache/MUST be gitignored or under explicit.gitignorerule (cache is runtime artifact, not source-of-truth). Catalog promotion (cache → committed) deferred to R4src/config.pyor catalog. No sample-specific (mdx 03/04/05) branches in cache modulefeedback_no_hardcodingbuild_cache_signaturehelper. Leavingtemplate_id::source_section_idswould render IMP-46 inertauto_cache_flagdefault False at HEAD merge. CI default preserves zero-write behaviorcache.pymodule docstring updated (NotImplementedError marker removed);IMP-17-CARVE-OUT.md:54IMP-46 row updated from "stub" to "active backend, gate semantics preserved"feedback_auto_pipeline_first— noreview_requiredinjection between AI call and cache save. Save gate decision is auto (visual_check + user_approved/auto_cache); failure paths emit clear reason strings, not queuesfeedback_auto_pipeline_firstPHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.mdadded with status close + reference to commit. Currently stale (IMP-33/IMP-46 rows missing — already flagged in #61 Stage 1)5. Implementation slicing sketch (Stage 2 input — NOT binding)
Suggested wave-1 ordering:
U1 — Config plumbing (zero behavior change)
src/config.pySettings :ai_fallback_auto_cache: bool = False,ai_fallback_cache_bucket_boundaries: tuple = (100, 300, 700)(declared values, env-overridable).U2 — Signature builder + axes
src/phase_z2_ai_fallback/cache_signature.py(stdlib only).build_cache_signature(...)→ 16-char SHA-256 hex.bucket_char_count(n: int, boundaries: tuple[int, ...]) -> strhelper.source_section_idswith same structural axes → same hash).U3 — Fingerprint helpers
src/phase_z2_ai_fallback/cache_fingerprint.py—fingerprint_frame_contract_subtree(template_id) -> str,fingerprint_partial_template(template_id) -> str | None.U4 — Persistent backend (replaces NotImplementedError)
src/phase_z2_ai_fallback/cache.py—read_proposalreads JSON fromdata/frame_cache/{frame_id}/{signature}.json;save_proposalwrites JSON (still gated by the existingvisual_check_passedANDuser_approvedconditions).frame_idaxis (subdirectory).U5 — Step12 integration
src/phase_z2_ai_fallback/step12.py:109-111— replace sample-specificcache_keywithbuild_cache_signature(...)using V4 result + frame contract + unit shape.U6 — Save site
apply_fallback_save_decision(slide_status, proposal, ...) -> Noneinsrc/phase_z2_ai_fallback/save_site.py(or pipeline-side module).--auto-cacheflag via config.U7 —
.gitignore+ invalidation logdata/frame_cache/to.gitignore.debug.json(cache_invalidation_logadditive field).U8 — Tests + AST isolation rerun
tests/phase_z2_ai_fallback/rerun (existing 116 tests must pass).tests/phase_z2_ai_fallback/test_cache_persistent.py.U9 — Docs sync
cache.pymodule docstring update.IMP-17-CARVE-OUT.md:54IMP-46 row update.IMP-31-GATE-AUDIT.md(if any cache reference).PHASE-Z-PIPELINE-STATUS-BOARD.md(IMP-46 line).PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.mdrow add.6. Open questions (Codex round 1차 review)
--auto-cacheflag plumbing: settings (env / .env) vs CLI argument vs both? Claude #1 strawman = settings only (env-overridable). CLI-only adds new arg-parser surface.internal_region_idbe added (Layer A SPEC v1 surface)? Claude #1 view: leave for v2; today Internal Region is trace-only.run_pipelinepost-visual-check vs separate explicitsave_fallback_proposals(...)helper invoked by caller? Claude #1 preference = explicit helper (clearer audit + composability with--auto-cache).cache_keyparameter type atroute_ai_fallback(cache_key: str, ...)insrc/phase_z2_ai_fallback/router.py:45? Today the router treats it as opaque string; with structural signature, type staysstr(no shape change). Claude #1 view: no router-side change required; signature build happens caller-side (step12) and passes the result.=== EVIDENCE ===
Files read (path:line):
src/phase_z2_ai_fallback/cache.py— full file (read_proposalreturns None;save_proposalgates → NotImplementedError marker).src/phase_z2_ai_fallback/step12.py— full file (cache_keybuild at L109-111 confirmed sample-specific).src/phase_z2_ai_fallback/router.py— full file (cache_read at L66; no save call).src/phase_z2_ai_fallback/schema.py— full file (3 ProposalKind values;FORBIDDEN_KINDSincludesraw_css).src/phase_z2_ai_fallback/prompts.py— full file (V4_ROUTE_AI_ADAPTATION = "ai_adaptation_required"; user payload structure).src/phase_z2_ai_fallback/validate.py— full file (4 guards; payload may contain slot text underPARTIAL_OVERRIDES).src/phase_z2_ai_fallback/step17.pyL1-80 (overflow cascade order; AI_REPAIR stage definition).src/phase_z2_ai_fallback/__init__.py— full file (exports = schema only; cache NOT re-exported).src/config.py— full file (existingai_fallback_*8 fields; noai_fallback_auto_cacheyet).src/phase_z2_mapper.pyL1-80 (catalog loader at L49;_CATALOG_CACHEglobal;CATALOG_PATHat L34).templates/phase_z2/catalog/frame_contracts.yamlL1-60 (single yaml; per-templateframe_id+source_shape+cardinality.strict).tests/phase_z2_ai_fallback/test_cache.py— full file (9 tests covering gate + NotImplementedError marker; 1 test must be replaced when backend lands).Grep / count results:
grep -rn "frame_cache\|IMP-46\|cache_key" src/phase_z2_ai_fallback/= 5 hits acrosscache.py,router.py,step12.py,step17.py(all consistent with stub status).grep -rn "visual_check_passed" src/= 4 sites (cache.py× 4,phase_z2_classifier.py:495producer,phase_z2_pipeline.py:2635/5086consumer). Producer ofuser_approved= none today (new surface).grep -n "load_frame_contracts" src/phase_z2_mapper.py= L49 (loader);get_contractat L56 (per-template subtree access).ls templates/phase_z2/frames/= 2 files (process_product_two_way.html,three_parallel_requirements.html).ls templates/phase_z2/families/= 12 family partials.ls data/= noframe_cache/directory at HEAD (confirmed vials data/).grep -rln "auto_cache\|--auto-cache" src/= 0 (new surface).Commits cross-checked:
c864fe0(IMP-33 u1~u11) — current HEAD; cache stub landed.1efbf67(IMP-30 first-render invariant) — provisional zone synthesis upstream of step12 cache_key build.Gitea API:
.orchestrator/drafts/61_stage_problem-review_claude_r1.mdre-read as reference (paired axis context, especially Axis E G11 about cache visual_check + 사용자 OK gating)..orchestrator/drafts/62_*— confirmed first Stage 1 round.No code change made — Stage 1 problem-review only.
rewind_target: 해당 없음 (YES 제안)
FINAL_CONSENSUS: YES
[Codex #1] Stage 1 problem-review - IMP-46
Status 3-axis:
Backend axis: verified.
src/phase_z2_ai_fallback/cache.py:36returnsNonefor every valid key, andsrc/phase_z2_ai_fallback/cache.py:48preserves the two write gates before raising the IMP-46NotImplementedErroratsrc/phase_z2_ai_fallback/cache.py:79. IMP-46 must replace this marker with persistent JSON read/write while preserving the visual_check and user_approved gates.Signature axis: verified, with a scope correction.
src/phase_z2_ai_fallback/step12.py:109buildscache_keyfromtemplate_idplus sortedsource_section_ids; those IDs are source/sample identifiers, so structurally identical content with different MDX section IDs will miss. This violates the issue's no-hardcoding/content-addressed intent. The fix must introduce a deterministic structural signature hash using frame_id, v4 label, cardinality, source shape, h3 count, character-count bucket, layout preset, and zone position.Integration/scope axis: scope is larger than a backend swap.
src/phase_z2_ai_fallback/step12.py:32does not acceptlayout_presetorzone_position, but both are required signature axes.src/phase_z2_ai_fallback/step12.py:113also passesv4_result["cardinality"] = None, whilesrc/phase_z2_ai_fallback/prompts.py:66only reads cardinality/cardinality_signature if present. Stage 2 must choose a call/integration point where unit raw_content, frame contract, resolved layout preset, and zone position are all available, or extend the Step 12 helper contract explicitly.Missing files / wrong assumptions flagged:
data/frame_cachedirectory and noframe_cache/signature_hashimplementation insrc,templates,tests, ordata.route_ai_fallbackhas one source call site,src/phase_z2_ai_fallback/step12.py:120; tests call it directly, but I found no production call togather_step12_ai_repair_proposalsoutside tests. Do not assume the pipeline already invokes the Step 12 AI helper.frame_id,source_shape, andcardinalityintemplates/phase_z2/catalog/frame_contracts.yaml, butsource_shapevalues are currently repo terms such astop_bulletsandh3_subsections, not only the issue text'stable/bullet/paragraph. The signature builder should canonicalize from existing repo data rather than hardcoding sample-specific or issue-wording-specific labels.save_proposalis only called by tests today; the production write path after visual_check PASS plus user OK/--auto-cachestill needs a concrete caller-owned integration point. The router intentionally imports onlyread_proposal, so write-after-approval must remain outsideroute_ai_fallback.AiFallbackProposalcontract (builder_options_patch,partial_overrides,slot_mapping_proposal) plus any slide-level CSS metadata only if represented as a validated/cache-owned extension. Raw HTML/CSS or frame changes remain forbidden bysrc/phase_z2_ai_fallback/schema.pyand validation contracts.Scope-lock recommendation:
Proceed to Stage 2 only if the plan includes: a content-addressed signature builder, persistent JSON storage at
data/frame_cache/{frame_id}/{signature_hash}.json, exact lookup first and optional fuzzy lookup as a separate deterministic layer, guarded save semantics, invalidation tied to frame contracts/partials/catalog changes, and focused tests for deterministic key stability across different source_section_ids. No sample-specific MDX 03/04/05 keys should survive.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath 'D:\ad-hoc\kei\design_agent\.orchestrator\tmp\codex_prompt.txt'rg -n "NotImplementedError|read_proposal|save_proposal|FrameCache|cache_key" src/phase_z2_ai_fallbackrg -n "frame_id:|source_shape:|cardinality:|strict:|partial|template" templates/phase_z2/catalog/frame_contracts.yaml templates/phase_z2/catalog -g "*.yaml"rg -n "route_ai_fallback|gather_step12_ai_repair_proposals|visual_check|auto-cache|auto_cache|user_approved|builder_options|partial_overrides|slot" src testsGet-Content -LiteralPath 'src\phase_z2_ai_fallback\cache.py' | Select-Object -First 120Get-Content -LiteralPath 'src\phase_z2_ai_fallback\step12.py' | Select-Object -First 170Get-Content -LiteralPath 'src\phase_z2_ai_fallback\router.py' | Select-Object -First 140rg -n "frame_cache|signature_hash|signature|data/frame_cache|source_section_ids|cardinality_signature|layout_preset|zone_position" src templates tests data -g "*.py" -g "*.yaml" -g "*.md"(rg -c "^ template_id:" templates/phase_z2/catalog/frame_contracts.yaml)rg -n "route_ai_fallback\(" src tests -g "*.py"rg -n "save_proposal\(" src tests -g "*.py"rg -n "gather_step12_ai_repair_proposals\(" src tests -g "*.py"git status --shortFiles checked:
D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\cache.pyD:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\step12.pyD:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\router.pyD:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\schema.pyD:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\prompts.pyD:\ad-hoc\kei\design_agent\src\phase_z2_composition.pyD:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.pyD:\ad-hoc\kei\design_agent\templates\phase_z2\catalog\frame_contracts.yamlTest results:
Verified facts:
frame_contracts.yamlcurrently has 32template_identries.read_proposalis a miss-only stub andsave_proposalraises the IMP-46 storage marker only after both gates pass.data/frame_cacheimplementation was found.gather_step12_ai_repair_proposals.FINAL_CONSENSUS: YES
[Claude #2] Stage 2 simulation-plan IMP-46 frame transformation cache layer
=== IMPLEMENTATION_UNITS ===
summary: Add signature module — deterministic hash over 8 structural axes; char_count bin + source_shape enum helpers; no sample-id leakage
files:
tests:
estimate_lines: 50
summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json (schema_version=1); read None on missing/corrupt JSON
files:
tests:
estimate_lines: 50
summary: Add cache invalidation — fingerprint of frame_contract + partial template + catalog version stored in cache value; mismatch on read returns None
files:
tests:
estimate_lines: 45
summary: Extend Step 12 helper — accept layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; build cache_key via u1 signature; remove section_id leakage
files:
tests:
estimate_lines: 50
summary: Add --auto-cache override — settings.ai_fallback_auto_cache (default False); save_proposal honours flag in place of user_approved; visual_check_passed gate preserved
files:
tests:
estimate_lines: 30
summary: .gitignore data/frame_cache + .gitkeep marker + IMP-17-CARVE-OUT.md cache-row refresh (stub → persistent JSON backend)
files:
tests: []
estimate_lines: 20
Per-unit rationale:
{frame_id}::{signature_hash}so router signature untouched. Replaces test_save_raises_not_implemented with round-trip.Out of scope:
c864fe0)Rollback: each unit single-commit revertable; u2/u3/u5 share cache.py → revert reverse order (u5→u3→u2); u1 + u6 additive and safely deletable.
=== EVIDENCE ===
c864fe0FINAL_CONSENSUS: YES
[Codex #2] Stage 2 simulation-plan review IMP-46
=== IMPLEMENTATION_UNITS ===
summary: Add deterministic structural signature module over the 8 declared axes, with char-count buckets and source-shape encoding.
files:
tests:
estimate_lines: 50
summary: Replace cache.py NotImplementedError stub with persistent JSON backend under data/frame_cache/{frame_id}/{signature_hash}.json.
files:
tests:
estimate_lines: 50
summary: Add invalidation check for frame contract, partial template, and catalog fingerprints.
files:
tests:
estimate_lines: 45
summary: Extend Step 12 helper inputs and replace template/section-id cache key with structural signature.
files:
tests:
estimate_lines: 50
summary: Add auto-cache override setting while preserving visual_check_passed gate.
files:
tests:
estimate_lines: 30
summary: Ignore generated frame cache data and refresh IMP-17 carve-out documentation.
files:
tests: []
estimate_lines: 20
Per-unit rationale:
Out-of-scope notes:
Rollback strategy:
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Claude #2] Stage 2 simulation-plan IMP-46 frame transformation cache (rewind r1)
Axis→unit: AI hook=u2/u5; path=u2/u6; 8-axis sig=u1; cache value=u2; exact lookup=u4; gate+--auto-cache=u5; invalidation=u3; guardrails=u1/u2/u5 tests.
=== IMPLEMENTATION_UNITS ===
summary: signature.py — SHA256 over 8-axis JSON dict (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position); char_count bins {0-50,51-150,151-400,401-1000,1001+}; source_shape enum {bullet,paragraph,table,mixed}; schema_version=1.
files:
tests:
estimate_lines: 50
summary: cache.py — replace NotImplementedError marker with persistent JSON at data/frame_cache/{frame_id}/{sig}.json. Stored shape
{schema_version, proposal, slide_css, fingerprints}. Missing/corrupt → None. Round-trip per ProposalKind + slide_css set/None.files:
tests:
estimate_lines: 50
summary: cache.py — extend read/save with fingerprints kwarg {contract_sha,partial_sha,catalog_sha}; save stores, read compares → mismatch=None. Compute at caller (u4), not cache module.
files:
tests:
estimate_lines: 45
summary: step12.py — extend helper inputs (layout_preset, zone_position, source_shape, h3_count, char_count, cardinality); inline-compute 3 fingerprints; replace sample cache_key with u1 signature. Exact-match only; fuzzy explicitly deferred (Stage 6 follow-up).
files:
tests:
estimate_lines: 50
summary: --auto-cache CLI flag in src/phase_z2_pipeline.py (argparse line 5208, action='store_true'); wired to settings.ai_fallback_auto_cache (new, default False); save_proposal accepts auto_cache=bool, bypasses user_approved only (visual_check_passed mandatory).
files:
tests:
estimate_lines: 40
summary: Track data/frame_cache/.gitkeep (data/ gitignored line 11 → add
!data/frame_cache/.gitkeepexception); refresh IMP-17-CARVE-OUT.md cache row stub→persistent.files:
tests: []
estimate_lines: 20
Per-unit rationale (Codex r1 gaps closed):
sha256(json.dumps(.,sort_keys=True))for contract+partial,sha256(catalog_yaml.read_bytes())for catalog. Cache stores opaquely; mismatch=None.!exception keeps .gitkeep tracked; JSON untracked.Out of scope:
c864fe0); fuzzy lookup (deferred); cache→catalog (R4); new router call sites; Stage 0 normalizer; prompts.py redesign beyond cardinality.Rollback:
=== EVIDENCE ===
data/→ u6 adds!data/frame_cache/.gitkeep.ls data/→ frame_cache/ absent (u6 creates).Verified: all units ≤50 lines + ≤3 files; Codex r1 NO gaps closed (u1 determinism, u2 value shape, u3 fp compute site, u4 exact-match decl + fuzzy deferred, u5 real CLI); rejected tests rewritten not deleted; flag defaults OFF; PZ-1 AI=0 intact.
FINAL_CONSENSUS: YES
[Codex #3] Stage 2 simulation-plan review - IMP-46
=== IMPLEMENTATION_UNITS ===
summary: Add signature.py with deterministic SHA256 over the 8 declared structural axes: frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position. Use fixed char-count buckets 0-50, 51-150, 151-400, 401-1000, 1001+; source_shape enum bullet, paragraph, table, mixed; schema_version=1.
files:
tests:
estimate_lines: 50
summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json. Stored value includes schema_version, proposal, slide_css, fingerprints; missing/corrupt files return None; round-trip all ProposalKind values and slide_css None/set.
files:
tests:
estimate_lines: 50
summary: Add invalidation by fingerprints: contract_sha, partial_sha, catalog_sha. save stores fingerprints, read compares supplied fingerprints and returns None on mismatch; fingerprint computation stays outside cache.py.
files:
tests:
estimate_lines: 45
summary: Extend Step 12 helper inputs for layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; derive char_count_bucket via u1; compute the three fingerprints; replace template/section-id cache_key with structural signature. Exact-match lookup only; fuzzy is explicitly deferred as optional follow-up.
files:
tests:
estimate_lines: 50
summary: Add --auto-cache CLI flag in src/phase_z2_pipeline.py, wire settings.ai_fallback_auto_cache default False, and let save_proposal accept auto_cache. auto_cache bypasses user_approved only; visual_check_passed remains mandatory.
files:
tests:
estimate_lines: 40
summary: Track data/frame_cache/.gitkeep by adding a .gitignore exception under the existing data/ ignore, and refresh IMP-17-CARVE-OUT.md cache row from stub status to persistent backend status.
files:
tests: []
estimate_lines: 20
Per-unit rationale:
u1 covers the no-hardcoding structural signature lock and removes source_section_id/sample leakage. Tests must assert same 8 axes produce same hash and any axis change changes it.
u2 covers the content-addressed persistent path and cache value shape. Tests must cover read miss, corrupt JSON miss, path creation, and ProposalKind round-trips.
u3 covers contract/partial/catalog invalidation without coupling cache.py to loaders. Tests must prove matching fingerprints hit and each mismatch misses.
u4 covers automatic lookup before the AI hook at the current production call site and supplies all missing signature inputs. Tests must prove router receives structural cache_key, not template plus section ids.
u5 covers the save gate and auto-cache override while preserving default OFF governance. Tests must prove visual_check is never bypassed.
u6 covers repo hygiene for the ignored data/ path and docs sync. No runtime test is needed; verify by git status and doc diff.
Out-of-scope notes:
AI invocation/client behavior remains IMP-33-owned and untouched; cache never invokes AI. Cache-to-catalog promotion remains R4. New route_ai_fallback call sites beyond Step 12 are excluded. Fuzzy lookup is optional and deferred; exact deterministic lookup is required for this stage.
Rollback strategy:
Revert the six unit file sets; remove data/frame_cache/.gitkeep and restore the data/ ignore-only state. This returns cache.py to IMP-33 stub behavior and Step 12 to its existing cache_key path.
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
Commit SHA: n/a for Stage 2
FINAL_CONSENSUS: YES
⚠️ [Orchestrator] Stage 2 FINAL_CONSENSUS: YES rejected: unit with
tests: [](forbidden — implementation + tests = same unit).Requirements (strict):
=== IMPLEMENTATION_UNITS ===block with at least one- id: u<N>entrytests: [<path>, ...](NOTtests: [])Example:
=== IMPLEMENTATION_UNITS ===
summary: ...
files: [...]
tests: [tests/.../test_xxx.py]
estimate_lines:
[Claude #3] Stage 2 simulation-plan IMP-46 (r2 — u6 test gap closed)
Single delta vs r1: u6 had
tests: [](orchestrator rejected). Now u6 shipstest_cache_repo_layout.py. Other 5 units unchanged from r1 (Codex #3 YES).Axis→unit: AI hook=u2/u5; path=u2/u6; 8-axis sig=u1; cache value=u2; exact lookup=u4; gate+--auto-cache=u5; invalidation=u3; guardrails=u1/u2/u5/u6 tests.
=== IMPLEMENTATION_UNITS ===
summary: signature.py — SHA256 over 8-axis JSON dict (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position); char_count bins {0-50,51-150,151-400,401-1000,1001+}; source_shape enum {bullet,paragraph,table,mixed}; schema_version=1.
files:
tests:
estimate_lines: 50
summary: cache.py — replace NotImplementedError marker with persistent JSON at data/frame_cache/{frame_id}/{sig}.json. Stored shape {schema_version, proposal, slide_css, fingerprints}. Missing/corrupt → None. Round-trip all ProposalKind values + slide_css None/set; mkdir parents on save.
files:
tests:
estimate_lines: 50
summary: cache.py — extend read/save with fingerprints kwarg {contract_sha, partial_sha, catalog_sha}; save stores, read compares supplied fingerprints → mismatch=None. Fingerprint computation stays outside cache.py (caller computes at u4).
files:
tests:
estimate_lines: 45
summary: step12.py — extend helper inputs (layout_preset, zone_position, source_shape, h3_count, char_count, cardinality); derive char_count_bucket via u1; inline-compute 3 fingerprints (sha256 of contract dict + partial template bytes + catalog yaml bytes); replace sample cache_key with u1 signature. Exact-match lookup only; fuzzy explicitly deferred (Stage 6 follow-up candidate).
files:
tests:
estimate_lines: 50
summary: --auto-cache CLI flag in src/phase_z2_pipeline.py argparse, wired to settings.ai_fallback_auto_cache (new, default False); save_proposal accepts auto_cache=bool which bypasses user_approved only — visual_check_passed remains mandatory. Gate truth table 2^3=8 cells tested; visual_check_passed=False ALWAYS rejects.
files:
tests:
estimate_lines: 40
summary: Track data/frame_cache/.gitkeep via .gitignore exception (
!data/frame_cache/.gitkeepunder existingdata/ignore at line 11); refresh IMP-17-CARVE-OUT.md cache row from stub→persistent backend. Test asserts .gitkeep exists as a tracked file path AND IMP-17-CARVE-OUT.md contains the persistent-backend marker string (e.g., "data/frame_cache/{frame_id}/{signature_hash}.json").files:
tests:
estimate_lines: 30
Per-unit rationale (deltas from r1):
tests/phase_z2_ai_fallback/test_cache_repo_layout.py— two invariants: (1) data/frame_cache/.gitkeep exists (repo layout); (2) IMP-17-CARVE-OUT.md contains persistent-backend marker substring (mirrors test_docs_sync.py pattern). estimate_lines 20→30.Out of scope:
c864fe0); fuzzy lookup (deferred); cache→catalog (R4); new router call sites; Stage 0 normalizer; prompts.py redesign beyond cardinality field exposure.Rollback:
=== EVIDENCE ===
Files verified present:
Comments reviewed:
Scope-lock referenced from Stage 1 exit report (binding contract above).
FINAL_CONSENSUS: YES
[Codex #4] Stage 2 simulation-plan review - IMP-46
=== IMPLEMENTATION_UNITS ===
summary: Add signature.py with deterministic SHA256 over the 8 declared structural axes: frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position. Fixed char-count buckets 0-50, 51-150, 151-400, 401-1000, 1001+; source_shape enum bullet, paragraph, table, mixed; schema_version=1.
files:
tests:
estimate_lines: 50
summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json. Stored value includes schema_version, proposal, slide_css, fingerprints; missing/corrupt files return None; mkdir parents on save; round-trip all ProposalKind values and slide_css None/set.
files:
tests:
estimate_lines: 50
summary: Add invalidation by fingerprints contract_sha, partial_sha, catalog_sha. save stores fingerprints, read compares supplied fingerprints and returns None on mismatch; fingerprint computation stays outside cache.py.
files:
tests:
estimate_lines: 45
summary: Extend Step 12 helper inputs for layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; derive char_count_bucket via u1; compute the three fingerprints; replace template/section-id cache_key with structural signature. Exact-match lookup only; fuzzy is deferred.
files:
tests:
estimate_lines: 50
summary: Add --auto-cache CLI flag in src/phase_z2_pipeline.py, wire settings.ai_fallback_auto_cache default False, and let save_proposal accept auto_cache. auto_cache bypasses user_approved only; visual_check_passed remains mandatory; test the 2^3 gate truth table.
files:
tests:
estimate_lines: 40
summary: Track data/frame_cache/.gitkeep via .gitignore exception under the existing data/ ignore, add repo-layout coverage, and refresh IMP-17-CARVE-OUT.md cache row from stub status to persistent backend status.
files:
tests:
estimate_lines: 20
Per-unit rationale:
u1 covers the no-hardcoding structural signature lock and removes sample/source_section_id leakage; tests must prove same 8 axes give same hash and any axis change changes it.
u2 covers the required cache path and cache value shape while preserving read-path determinism and cache-miss fallback for missing/corrupt files.
u3 covers contract/partial/catalog invalidation without making cache.py know how to compute project-specific hashes.
u4 covers the sole production Step 12 integration point and supplies the missing layout/cardinality/source-shape inputs before signature build.
u5 covers the issue write gate and auto-cache override while keeping the default flag off and visual_check_passed as a non-bypassable gate.
u6 covers the previously missing repo-layout/test gap: data/ is ignored today, data/frame_cache is absent, so .gitkeep needs an explicit exception plus a layout test.
Out-of-scope notes:
AI invocation logic, prompt redesign, cache-to-catalog promotion, new route_ai_fallback call sites, fuzzy lookup implementation, and Stage 0 mdx_normalizer integration remain out of scope.
Rollback strategy:
Revert the six unit filesets by unit if a unit regresses; cache files are content-addressed under data/frame_cache and can be removed without affecting AI hook behavior.
=== EVIDENCE ===
Commands run: git status --short; rg --files; Get-Content src/phase_z2_ai_fallback/cache.py; Get-Content src/phase_z2_ai_fallback/step12.py; rg -n ai_fallback|auto_cache|auto-cache|ArgumentParser|add_argument src/config.py src/phase_z2_pipeline.py tests/test_phase_z2_ai_fallback_config.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_step12.py; Get-Content .gitignore; Test-Path data/frame_cache; Get-Content tests/phase_z2_ai_fallback/test_cache.py; Get-Content tests/phase_z2_ai_fallback/test_step12.py; pytest -q tests
Files checked: src/phase_z2_ai_fallback/cache.py; src/phase_z2_ai_fallback/step12.py; src/config.py; src/phase_z2_pipeline.py; .gitignore; tests/phase_z2_ai_fallback/test_cache.py; tests/phase_z2_ai_fallback/test_step12.py; tests/test_phase_z2_ai_fallback_config.py
Test results: pytest -q tests => 526 passed in 41.71s
Verified facts: current cache.py still returns None on read and raises NotImplementedError after both gates; current step12.py cache_key still uses template_id plus sorted source_section_ids; no auto-cache setting or CLI flag exists; .gitignore ignores data/ and data/frame_cache is absent; revised units are all at or under 50 estimated lines and at or under 3 files; u6 now includes tests/phase_z2_ai_fallback/test_cache_repo_layout.py; issue axes map to u1 signature, u2 path/value/backend, u3 invalidation, u4 lookup/integration, u5 gate/auto-cache, u6 repo layout/docs.
FINAL_CONSENSUS: YES
[Claude #1] Stage 3 code-edit IMP-46 — u1 signature builder
Executing unit: u1
■ unit_executed
signature.pywith deterministic SHA256 over the 8 declared structural axes (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position). Fixed char-count buckets 0-50 / 51-150 / 151-400 / 401-1000 / 1001+;source_shapeenum (bullet/paragraph/table/mixed);SCHEMA_VERSION=1embedded in hashed payload.■ files_changed
hashlib; stdlib peer ofjson/pathlib)■ diff_summary
src/phase_z2_ai_fallback/signature.py:SCHEMA_VERSION = 1constant, embedded inside the hashed JSON payload so a future schema bump auto-invalidates digests.SourceShape(str, Enum)with membersBULLET / PARAGRAPH / TABLE / MIXED.CHAR_COUNT_BUCKET_LABELS = ("0-50", "51-150", "151-400", "401-1000", "1001+")exposed as a public tuple for cross-module reuse.bucket_char_count(int) -> str: maps a non-negative int char count to one of the 5 fixed buckets; rejectsbool, non-int, or negative input.build_signature(*, frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position) -> str: keyword-only API restricted to the 8 declared axes. Coercessource_shapefromstrviaSourceShape(...); validateschar_count_bucket ∈ CHAR_COUNT_BUCKET_LABELS. Computes SHA256 overjson.dumps(payload, sort_keys=True, ensure_ascii=False)→ returns 64-char hex digest.tests/phase_z2_ai_fallback/test_signature.py(20 cases, all pass):source_shapestring ⇔ enum equivalence.source_shapeand unknownchar_count_bucket.cardinality=Noneyields a valid digest distinct fromcardinality=0.inspect.signature(build_signature).parametersis exactly the 8-axis set (notemplate_id/source_section_ids/mdx_pathleakage).bucket_char_count(3.14)andbucket_char_count(True).test_ast_isolation.py): added"hashlib"to_ALLOWED_TOP_LEVEL(alphabetical placement betweenenumandjson). Stdlib peer of already-whitelistedjson/pathlib/dataclasses. No forbidden surface widened — Phase Q / Kei / phase_z2_* runtime imports still rejected.■ validation
python -m pytest tests/phase_z2_ai_fallback/test_signature.py -q→ 20 passed.python -m pytest tests/phase_z2_ai_fallback/ tests/test_family_contract_baseline.py -q→ 136 passed (full fallback suite + baseline contract test green; AST isolation, schema, cache gate, step12, router, prompts, client mock all unaffected).■ guardrails
signature.pyperforms zero AI/network/MDX I/O. Pure stdlib (hashlib,json,enum).cache.py,step12.py, pipeline, or config. Cache backend (u2), invalidation (u3), Step 12 wiring (u4), gate flag (u5), repo layout (u6) all untouched.SCHEMA_VERSIONis part of the hashed payload, so any future axis rename / addition can bump it for deterministic invalidation (will be the invariant that u3 fingerprints layer on top of, not replace).■ remaining_units
data/frame_cache/{frame_id}/{signature_hash}.json--auto-cacheCLI flag +settings.ai_fallback_auto_cache+ 2^3 gate truth table.gitignoreexception +data/frame_cache/.gitkeep+ IMP-17-CARVE-OUT.md refresh + repo-layout test■ follow_up_issue_candidates
hashlibwhitelist addition is in-scope u1 plumbing (stdlib parity withjson); not a scope-creep axis. No other off-scope surface discovered.stopping here per "1 turn = 1 step" rule; no commit, no push.
[Codex #1] Stage 3 code-edit review IMP-46 u1
Scope: verified only the single executed unit, u1.
Verdict: u1 is correct. The new signature builder is deterministic, content-addressed over only the 8 declared structural axes, embeds schema version 1 in the hashed payload, exposes the fixed char-count buckets, rejects invalid bucket/source-shape inputs, and does not include template, section, MDX, or sample identifiers in the signature surface. The AST whitelist update is limited to the stdlib
hashlibimport needed by this unit.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath src/phase_z2_ai_fallback/signature.pyGet-Content -LiteralPath tests/phase_z2_ai_fallback/test_signature.pyGet-Content -LiteralPath tests/phase_z2_ai_fallback/test_ast_isolation.pypython -m pytest tests/phase_z2_ai_fallback/test_signature.py tests/phase_z2_ai_fallback/test_ast_isolation.py -qrg -n "template_id|source_section_ids|mdx_path|section_id|MDX 03|MDX 04|MDX 05" src/phase_z2_ai_fallback/signature.py tests/phase_z2_ai_fallback/test_signature.pySCHEMA_VERSION,CHAR_COUNT_BUCKET_LABELS, andbuild_signatureparametersFiles checked:
src/phase_z2_ai_fallback/signature.pytests/phase_z2_ai_fallback/test_signature.pytests/phase_z2_ai_fallback/test_ast_isolation.pyTest results:
42 passed in 0.12sVerified facts:
SCHEMA_VERSION == 1.CHAR_COUNT_BUCKET_LABELS == ("0-50", "51-150", "151-400", "401-1000", "1001+").build_signaturepublic parameters are exactlyframe_id,v4_label,cardinality,source_shape,h3_count,char_count_bucket,layout_preset,zone_position.rgfound notemplate_id,source_section_ids,mdx_path,section_id,MDX 03,MDX 04, orMDX 05leakage in the u1 signature files.test_ast_isolation.pyonly addshashlibto the allowed top-level imports for this unit.Remaining units: [u2, u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #2] Stage 3 code-edit IMP-46 — u2 persistent JSON cache backend
Executing unit: u2
■ unit_executed
NotImplementedErrormarker insrc/phase_z2_ai_fallback/cache.pywith a content-addressed JSON backend atdata/frame_cache/{frame_id}/{signature_hash}.json. Stored payload ={schema_version: 1, proposal, slide_css, fingerprints}. Missing / corrupt files →read_proposalreturnsNone;mkdir(parents=True, exist_ok=True)on save. Both write gates (visual_check_passedANDuser_approved) preserved BEFORE any filesystem touch.■ files_changed
■ diff_summary
src/phase_z2_ai_fallback/cache.pySCHEMA_VERSION = 1,KEY_DELIMITER = "::",CACHE_ROOT: pathlib.Path = pathlib.Path("data/frame_cache")(mutable for monkeypatch-based test isolation; tests redirect totmp_path).AiFallbackCacheGateError(RuntimeError)— class preserved exactly (loud gate violation surface)._parse_key(key) -> tuple[str, str] | None— splits"{frame_id}::{signature_hash}"; returnsNoneif delimiter missing or component empty or extra::present. Centralizes the on-disk layout decision in one place._cache_path(frame_id, signature_hash) -> pathlib.Path— single source of truth for the file layout (CACHE_ROOT / frame_id / f"{signature_hash}.json").read_proposal(key):key→ValueError(loud; matches prior contract).::) → silentNone. This is router back-compat —test_router.py:58still passescache_key="frame:1171281190:cardinality:many"and must continue to miss safely until u4 replaces the cache_key at the step12 call site.None. Corrupt JSON (OSError/json.JSONDecodeError) →None. Non-dict root, missingproposalkey, non-dictproposalvalue, orAiFallbackProposal.model_validatefailure (e.g. forbiddenproposal_kindlikemdx_textsmuggled into a stale payload) →None. Read errors NEVER propagate — the cache is a hint, never a hard dependency.save_proposal(key, proposal, *, visual_check_passed, user_approved, slide_css=None, fingerprints=None):ValueError; wrong type →TypeError;visual_check_passed=False→AiFallbackCacheGateError;user_approved=False→AiFallbackCacheGateError. Both gates raise BEFORE the parent directory is touched (verified bytest_save_gate_violation_does_not_touch_filesystem).slide_cssvalidated (str | None),fingerprintsvalidated (dict | None), then key parsed via_parse_key; legacy key format →ValueError(loud — writes must use the structural form).path.parent.mkdir(parents=True, exist_ok=True), JSON payload written withsort_keys=True, ensure_ascii=False, indent=2. Returns the resolvedpathlib.Path.fingerprintsis persisted but NOT compared in u2 — comparison / invalidation is u3's responsibility (explicit module docstring note).tests/phase_z2_ai_fallback/test_cache.py— 31 cases.@pytest.fixture(autouse=True) _isolated_cache_rootredirectscache_mod.CACHE_ROOTtotmp_path / "frame_cache"viamonkeypatch.setattrfor every test, so the production directory is never touched. Coverage:proposal→ None; forbiddenproposal_kind="mdx_text"→ None (validator rejects).frame_iddirectory; empty key → ValueError; non-proposal object → TypeError; legacy key format → ValueError; non-stringslide_css→ TypeError; non-dictfingerprints→ TypeError;AiFallbackCacheGateErroris not aNotImplementedErrorsubclass.schema_version=1; payload includesproposal.model_dump(mode="json"); defaultslide_css=Noneand defaultfingerprints={}; round-trip withslide_cssset; round-trip with fingerprints dict; read returns the same proposal after save; parametrized round-trip across all threeProposalKindvalues (BUILDER_OPTIONS_PATCH, PARTIAL_OVERRIDES, SLOT_MAPPING_PROPOSAL); overwrite an existing entry; file layout usesframe_id/directory +signature_hash.jsonfile; two distinct signature hashes under the same frame are isolated; triple-delimiter key rejected.tests/phase_z2_ai_fallback/test_ast_isolation.py—jsonandpathlibwere already in the_ALLOWED_TOP_LEVELwhitelist, so the new backend introduces no new top-level imports.■ validation
python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_ast_isolation.py -q→ 55 passed in 0.26s.python -m pytest tests/phase_z2_ai_fallback/ -q→ 156 passed in 0.27s (full AI fallback regression — includestest_router.pywhich still callsread_proposal("frame:1171281190:cardinality:many")and continues to getNonebecause the legacy key format is now silently treated as a miss; PZ-1 unaffected).python -m pytest tests/test_family_contract_baseline.py -q→ 4 passed in 0.59s.SCHEMA_VERSION == 1,KEY_DELIMITER == "::",CACHE_ROOT == pathlib.Path("data/frame_cache").read_proposal(key: str) -> AiFallbackProposal | None.save_proposal(key: str, proposal: AiFallbackProposal, *, visual_check_passed: bool, user_approved: bool, slide_css: str | None = None, fingerprints: dict | None = None) -> pathlib.Path.AiFallbackCacheGateErrorMRO ends atRuntimeError(NOT aNotImplementedError— the IMP-33 marker is gone).■ scope_locks_honoured
fingerprintsfield is persisted on save and the field is read back on disk, butread_proposaldoes NOT compare supplied fingerprints against the stored value. That mismatch-comparison logic is u3.router.py,step12.py, andtest_router.pyare NOT modified. The router still callsread_proposal(cache_key)with the IMP-33 u7 legacy string. u2's parser silently misses on legacy keys so the router's cache path keeps returningNone(the prior stub behavior). u4 will replace the cache_key at the step12 call site with the structuralframe_id::signature_hashform.--auto-cacheCLI flag,settings.ai_fallback_auto_cache, and theauto_cacheparameter onsave_proposalare NOT introduced here. Both gates remain mandatory; neither can be bypassed in u2..gitignoreexception fordata/frame_cache/.gitkeepand theIMP-17-CARVE-OUT.mdrow refresh are NOT performed here.json,pathlib) + intra-package (schema).■ remaining_units
read_proposalreturns None on mismatch; newtests/phase_z2_ai_fallback/test_cache_invalidation.py).src/phase_z2_ai_fallback/step12.py+tests/phase_z2_ai_fallback/test_step12.py).--auto-cacheCLI flag +settings.ai_fallback_auto_cache+auto_cachekwarg onsave_proposal+ 2³ gate truth-table test).data/frame_cache/.gitkeep+.gitignoreexception +IMP-17-CARVE-OUT.mdcache row refresh +tests/phase_z2_ai_fallback/test_cache_repo_layout.py).■ follow_up_issue_candidates
■ STOP
[Codex #2] Stage 3 code-edit review IMP-46 u2
Scope: verified only the single executed unit, u2.
Verdict: u2 is correct. The cache stub has been replaced with a persistent JSON backend at
data/frame_cache/{frame_id}/{signature_hash}.json, storesschema_version,proposal,slide_css, andfingerprints, returnsNonefor missing/corrupt/unparseable cached reads, preserves the dual write gate before filesystem writes, and keeps fingerprint comparison out of u2. No AI invocation path or save call site was added.Minor wording note: the implementation returns the storage
Path; it does not explicitly callPath.resolve()whenCACHE_ROOTis relative. This does not violate the u2 contract.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath src\phase_z2_ai_fallback\cache.pyGet-Content -LiteralPath tests\phase_z2_ai_fallback\test_cache.pygit diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache.pypython -m pytest tests/phase_z2_ai_fallback/test_cache.py -qrg -n "NotImplementedError|anthropic|Claude|MDX 03|MDX 04|MDX 05|source_section_ids|mdx_path|template_id" src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache.pySCHEMA_VERSION,KEY_DELIMITER,CACHE_ROOT,save_proposal, andread_proposalrg -n "save_proposal\(|read_proposal\(|cache_key\s*=|route_ai_fallback\(" src tests/phase_z2_ai_fallback/test_step12.pyGet-Content -LiteralPath src\phase_z2_ai_fallback\step12.pyGet-Content -LiteralPath src\phase_z2_ai_fallback\router.pypython -m pytest tests/phase_z2_ai_fallback/test_router.py -qgit status --shortFiles checked:
src/phase_z2_ai_fallback/cache.pytests/phase_z2_ai_fallback/test_cache.pysrc/phase_z2_ai_fallback/router.pysrc/phase_z2_ai_fallback/step12.pyTest results:
tests/phase_z2_ai_fallback/test_cache.py: 33 passed in 0.12stests/phase_z2_ai_fallback/test_router.py: 9 passed in 0.03sVerified facts:
SCHEMA_VERSION == 1.KEY_DELIMITER == "::".CACHE_ROOT == data/frame_cache.save_proposalacceptsslide_cssandfingerprints, writes JSON only aftervisual_check_passed=Trueanduser_approved=True, and returns the target storage path.read_proposalreturnsNonefor missing files, malformed legacy keys without::, corrupt JSON, non-dict roots, missing/non-dict proposal payloads, and proposal validation failures.schema_version,proposal,slide_css, andfingerprints.AiFallbackCacheGateError, notNotImplementedError, and the targeted test verifies no frame directory is created on gate rejection.read_proposal; nosave_proposalcall site was introduced insrc.rgfound noanthropic,Claude,MDX 03,MDX 04,MDX 05,source_section_ids,mdx_path, ortemplate_idleakage incache.py;NotImplementedErrorremains only in explanatory/test text.Remaining units: [u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #3] Stage 3 code-edit IMP-46 — u3 fingerprint invalidation
Executing unit: u3
■ unit_executed
src/phase_z2_ai_fallback/cache.py.save_proposalalready persisted thefingerprintsdict verbatim (u2); u3 extendsread_proposalwith an optionalfingerprintskwarg. When supplied, the storedfingerprintsdict must equal the caller's dict by strict equality — any mismatch (single-axis drift, extra key, missing key, non-dict stored value, legacy entry with nofingerprintsfield) returnsNone. Defaultfingerprints=Noneperforms no comparison (back-compat). Fingerprint computation stays outsidecache.py: the module is a content-addressed comparator, not a hasher — preserves AI isolation and keeps the cache schema-agnostic.■ files_changed
read_proposalgains keyword-onlyfingerprints: dict | None = None)■ diff_summary
src/phase_z2_ai_fallback/cache.pyu2→u2 + u3; new section "u3 invalidation contract (this module is a comparator, not a computer)" documents:save_proposalpersistsfingerprintsverbatim — no fingerprint axis names are hardcoded in cache.pyread_proposal(fingerprints=...)enforces strict equality (NOT subset); defaultNoneskips comparisonread_proposalsignature changed from(key: str)to(key: str, *, fingerprints: dict | None = None). The new kwarg is keyword-only so positional callers cannot accidentally pass an axis-extension dict.fingerprints is not None and not isinstance(fingerprints, dict)→TypeError(symmetric withsave_proposal's fingerprints validation).proposalvalidation: iffingerprints is not Noneandstored = data.get("fingerprints")is not a dict ORstored != fingerprints→ returnNone. This ordering means missing-file / corrupt-JSON precedence is preserved (no false hit through a phantom equality check) and a hand-corruptedfingerprintsfield (e.g. serialized as a list) is treated as an invalidation, not as aTypeError.tests/phase_z2_ai_fallback/test_cache_invalidation.py(new — 21 cases, all pass)test_save_persists_fingerprints_verbatimre-asserts the u2 round-trip foundation u3 depends on.test_read_without_fingerprints_kwarg_returns_proposal— legacy callers still hit.test_read_without_fingerprints_kwarg_ignores_stored_mismatch— entry saved with{"contract_sha": "old"}still readable by legacy reader.test_read_with_matching_fingerprints_returns_proposal— exact dict equality hits.test_read_with_empty_fingerprints_matches_empty_stored— both sides{}is a valid match, not a special-case None.test_read_invalidates_on_single_axis_drift[contract_sha|partial_sha|catalog_sha]— each axis individually breaks the match.test_read_invalidates_when_caller_supplies_extra_key— strict equality (not subset) is the locked semantic.test_read_invalidates_when_caller_supplies_subset— same, from the other direction.test_read_invalidates_when_entry_saved_without_fingerprints— caller demands proof of freshness; empty stored set is not "compatible by default".test_read_invalidates_when_stored_fingerprints_not_dict— hand-corrupted payload (fingerprints: [...]) misses cleanly.test_read_invalidates_when_stored_fingerprints_field_missing— legacy payload (nofingerprintsfield at all) invalidates under fingerprint-aware lookup.test_read_with_matching_fingerprints_still_loses_to_missing_file— file-missing wins.test_read_with_matching_fingerprints_still_loses_to_corrupt_json— JSON-corrupt wins.test_read_rejects_non_dict_fingerprints[list|string|int](3 cases) — TypeError matches save-side surface.test_cache_module_has_no_fingerprint_computer— public surface ofcache.pyis grepped forhash/sha/fingerprintsubstrings; passes iff zero functions named for fingerprint computation exist. This is the structural defence against later patches accidentally pulling fingerprint computation into the cache module.test_distinct_fingerprint_sets_isolated_per_signature— two entries with different sig_hashes hold their own fingerprints; crossed lookups miss, aligned lookups hit.■ validation
python -m pytest tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_ast_isolation.py tests/phase_z2_ai_fallback/test_router.py -q→ 84 passed in 0.32s.python -m pytest tests/phase_z2_ai_fallback/ -q→ 176 passed in 0.37s (whole package green, no regressions on u1 signature / u2 cache backend / IMP-33 router / AST guard).src/**files touched outsidesrc/phase_z2_ai_fallback/cache.py.hashlibwhitelist from u1 is NOT used by cache.py (u3 does not importhashlib— comparison is plaindictequality, not hash recomputation).■ guardrail / contract evidence
cache.pystill imports onlyjson,pathlib, andsrc.phase_z2_ai_fallback.schema. No Anthropic, no MDX, no Phase Z runtime references.contract_sha/partial_sha/catalog_sha) do NOT appear incache.pysource — they are caller-supplied dict keys; cache.py only compares dicts. The names appear only in test fixtures and the module docstring (documentation).read_proposalperforms no AI invocation.visual_check_passedANDuser_approved) preserved untouched. u3 only modifies the read path.test_save_gate_violation_does_not_touch_filesystemand the 33-case u2 suite all still green.■ remaining_units
■ follow_up_issue_candidates
cache.pyby design. No new axis discovered.[Codex #3] Stage 3 code-edit review IMP-46 u3
Scope: verified only the single executed unit, u3.
Verdict: u3 is correct.
read_proposalnow accepts a keyword-onlyfingerprintscomparator, strict equality invalidates mismatched cache entries, missing/corrupt/stale payloads still returnNone, andsave_proposalcontinues to persist caller-supplied fingerprints without computing them insidecache.py. The cache module remains a storage/comparison layer and does not invoke AI or add Phase Z runtime coupling.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath src/phase_z2_ai_fallback/cache.pyGet-Content -LiteralPath tests/phase_z2_ai_fallback/test_cache_invalidation.pygit diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.pypython -m pytest tests/phase_z2_ai_fallback/test_cache_invalidation.py -qrg -n "hashlib|sha256|def .*fingerprint|compute_.*finger|contract_sha|partial_sha|catalog_sha|anthropic|Claude|MDX 03|MDX 04|MDX 05|source_section_ids|mdx_path|template_id" src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.pyread_proposalandsave_proposalsignaturesgit status --shortFiles checked:
src/phase_z2_ai_fallback/cache.pytests/phase_z2_ai_fallback/test_cache_invalidation.pyTest results:
tests/phase_z2_ai_fallback/test_cache_invalidation.py: 20 passed in 0.09sVerified facts:
read_proposalsignature is(key: str, *, fingerprints: dict | None = None) -> AiFallbackProposal | None; the new parameter is keyword-only.save_proposalstill acceptsfingerprints: dict | None = Noneand storesdict(fingerprints)in the JSON payload.None.fingerprints=Noneon read skips comparison for back-compat.TypeError.hashlib,sha256, fingerprint computation function, Anthropic/Claude import, MDX sample IDs,source_section_ids,mdx_path, ortemplate_idleakage incache.py; fingerprint axis names appear only as documented opaque keys/test fixtures.cache.pyandtest_cache_invalidation.py.Remaining units: [u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #4] Stage 3 code-edit IMP-46 — u4 structural cache key + fingerprints
Executing unit: u4
■ unit_executed
template_id::sorted(source_section_ids)) insrc/phase_z2_ai_fallback/step12.pywith a structural signature key"{frame_id}::{signature_hash}"derived from the 8 IMP-46 axes viabuild_signature(u1). Extend Step 12 helper inputs (unit attributes) forlayout_preset,zone_position,source_shape,h3_count,char_count, andcardinality; projectchar_countthroughbucket_char_count(u1). Compute the three invalidation fingerprints (contract_sha,partial_sha,catalog_sha) and attach them — with the newcache_key— to each AI-eligible record for downstreamsave_proposaluse. Exact-match lookup only (no fuzzy fallback added). v4_result.cardinality now mirrors the unit's cardinality (was hardcodedNone).■ files_changed
_sha256_ofhelper,catalog_sha_loaderkwarg, structural signature + fingerprint block, record fieldscache_key+fingerprints)test_cache_key_includes_template_and_section_idstest with 14 new u4 cases; FakeUnit extended with 6 new signature-input fields)■ diff_summary
src/phase_z2_ai_fallback/step12.pyIMP-46 u4 — structural cache key + fingerprintssection that names every signature axis read from unit attributes and explains why fingerprint computation lives here (cache.py is a comparator per u3 — keeps the cache module schema-agnostic).hashlib,json(both already in the AST isolation whitelist —test_ast_isolation.py:39-42).bucket_char_count,build_signaturefromsrc.phase_z2_ai_fallback.signature(u1)._sha256_of(payload: Any) -> str: deterministic SHA256 overjson.dumps(payload, sort_keys=True, ensure_ascii=False). Used only forcontract_shaandpartial_sha.gather_step12_ai_repair_proposalssignature gains one new keyword-only argument:catalog_sha_loader: Callable[[], str] | None = None— called once per gather invocation (verified bytest_catalog_sha_loader_called_once_per_gather). WhenNone,catalog_shadefaults to""(sentinel — always present, sofingerprintsis always a 3-key dict).None:"cache_key": str | None— populated only on the AI-eligible code path; the structural axes are not guaranteed for skipped units, so the field is leftNonefornot_provisional/design_reference_only_no_ai/route_not_ai_adaptation:*records."fingerprints": dict | None— same population rule.getattrwith safe defaults (so existing test fixtures and pre-IMP-46 units survive):frame_id_value,cardinality,layout_preset(default""),zone_position(default""),source_shape(default"paragraph"— validSourceShapeenum member),h3_count(default0),char_count(default0).char_count_bucket = bucket_char_count(char_count)— u1 fixed-bin projection.signature_hash = build_signature(frame_id=..., v4_label=label or "", cardinality=..., source_shape=..., h3_count=..., char_count_bucket=..., layout_preset=..., zone_position=...)— 8-axis SHA256.cache_key = f"{frame_id_value}::{signature_hash}"— matches cache.py_parse_keyformat (KEY_DELIMITER = "::"); validated bytest_cache_key_is_compatible_with_cache_parse_key.fingerprints = {"contract_sha": _sha256_of(frame_contract), "partial_sha": _sha256_of(figma_partial_json), "catalog_sha": catalog_sha}.v4_result["cardinality"]now reads the unit'scardinalityattribute instead of the hardcodedNonefrom IMP-33 u8.route_ai_fallback(cache_key=cache_key, ...)now receives the structural key (the router's existing read-side path is unchanged —read_proposal(cache_key)continues to perform exact-match lookup only, as required by the u4 contract).tests/phase_z2_ai_fallback/test_step12.pyFakeUnitdataclass extended with 6 new fields (all with safe defaults):cardinality: int | None = None,layout_preset: str = "",zone_position: str = "",source_shape: str = "paragraph",h3_count: int = 0,char_count: int = 0. All pre-existing tests continue to constructFakeUnit(label=..., provisional=...)without modification._ai_unit(**overrides): builds an AI-eligible (provisional=True,label="restructure")FakeUnitwith realistic signature axes — keeps the u4 test bodies readable without mutating the existing test surface.test_cache_key_includes_template_and_section_idsREMOVED — it asserted the brokentemplate_id::sorted(section_ids)format that u4 explicitly replaces. Removing it (rather than xfailing) is consistent with the no-hardcoding lock: that key shape is now a defect, not a contract.test_record_shape_contract_is_stablerenamed totest_record_shape_contract_is_stable_with_u4_fieldsand updated to assert exactly 12 keys (the original 10 +cache_key+fingerprints).test_cache_key_format_is_frame_id_plus_sha256—cache_key.startswith("fid_123::"), suffix is 64-char lowercase hex; asserts the legacy substrings"tmpl_x"and"02-1"are absent.test_cache_key_invariant_to_section_id_changes—source_section_ids=["02-1"]and["05-2","07-3"]produce the samecache_key(no sample leakage).test_cache_key_invariant_to_template_id_changes—frame_template_idis NOT in the signature surface (onlyframe_idis).test_cache_key_changes_when_any_signature_axis_changes— parametrised-style loop over{frame_id, layout_preset, zone_position, source_shape, h3_count, char_count, cardinality}; each single-axis flip mutatescache_key.char_count=500is chosen specifically to cross the151-400→401-1000bucket boundary (verifies bucketing, not raw count).test_char_count_bucket_collapses_within_bucket—char_count=160andchar_count=399both fall in"151-400"and produce identical keys;char_count=401differs.test_fingerprints_attached_to_ai_record— fingerprints dict has exactly the 3 declared keys, all string values;contract_shaandpartial_shamatch an inline recomputation ofhashlib.sha256(json.dumps(..., sort_keys=True, ensure_ascii=False).encode()). Stable-fixture lock against silent fingerprint-format drift.test_fingerprints_default_catalog_sha_is_empty_string— nocatalog_sha_loader→catalog_sha == ""AND the key still appears in the dict (3-key invariant).test_fingerprints_change_when_contract_changes—frame_contract={"a":1}vs{"a":2}mutatescontract_sha, leavespartial_shaunchanged.test_fingerprints_change_when_partial_changes— symmetric forfigma_partial_json.test_v4_result_cardinality_uses_unit_value— unitcardinality=7flows intov4_result["cardinality"]=7;cardinality=NonestaysNone. Closes the IMP-33 u8 hardcoded-Nonegap noted in Stage 1.test_skipped_records_have_no_cache_key_or_fingerprints—not_provisional,reject, andlight_editpaths all keepcache_key=Noneandfingerprints=None.test_catalog_sha_loader_called_once_per_gather—MagicMockloader is called exactly once across 3 AI-eligible units (not once per unit).test_record_shape_contract_is_stable_with_u4_fields— record key set is exactly{unit_index, source_section_ids, frame_template_id, label, route_hint, provisional, ai_called, skip_reason, proposal, error, cache_key, fingerprints}.test_cache_key_is_compatible_with_cache_parse_key— round-trips the producedcache_keythroughcache._parse_key; assertsframe_id == "fid_123",len(signature_hash) == 64, no extraKEY_DELIMITER. Cross-module structural lock — the u4 producer and the u2 consumer agree on the key shape.■ validation
python -m pytest tests/phase_z2_ai_fallback/test_step12.py tests/phase_z2_ai_fallback/test_ast_isolation.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_signature.py -q→ 125 passed in 0.32s.python -m pytest tests/test_family_contract_baseline.py tests/phase_z2_ai_fallback/ -q→ 192 passed in 1.02s (Stage 2 baseline regression suite green pre- and post-u4).git diff --stat→step12.py +84/-2,test_step12.py +247/-16(315 insertions, 16 deletions, 2 files).Grepaudit ofsrc/phase_z2_ai_fallback/step12.pyforsource_section_ids|template_id|mdx_path|section_id|MDX 0[345]: every hit is either the docstring describing the LEGACY format, the record-schema docstring, the record initialisation (telemetry — never fed tobuild_signature), or thetemplate_idvariable that is used only as a key for the existingget_contract_fn/frame_visual_loader/figma_partial_loadercallbacks.template_idandsource_section_idsdo NOT appear inbuild_signature(...)arguments or in thecache_keyf-string. No-hardcoding lock honored.hashlibandjsonwere already allowed bytest_ast_isolation.py:39-42. No Anthropic/MDX/Phase Q/Phase Z2 runtime imports added.■ remaining_units
■ scope_lock observations (NOT acted on)
read_proposal(cache_key)— it does NOT yet pass the newfingerprintsdict toread_proposal(the u3 invalidation comparator is therefore inert through the router today). u4's files list (per Stage 2 plan) isstep12.py + test_step12.pyonly;router.pyis out of scope. Step 12 produces and exposes the fingerprints; wiring them into the router's read call is a follow-up axis. Recording below.■ follow_up_issue_candidates
src/phase_z2_ai_fallback/router.py:66callsread_proposal(cache_key)withoutfingerprints=. To complete the u3 invalidation chain end-to-end (so a stale cache entry with changedcontract_sha/partial_sha/catalog_shamisses through the router), the router needs to accept afingerprintskwarg fromgather_step12_ai_repair_proposalsand forward it toread_proposal. This is not required by the Stage 2 plan's u4 scope, but the cache-invalidation behaviour is incomplete without it. Suggest filing as a small follow-up issue (or folding into u5 if the orchestrator consents — but strictly outside the unit list as written).catalog_sha_loaderis aCallable[[], str]. The actual catalog content (Step 2 plan says the invalidation trigger istemplates/phase_z2/catalog/frame_contracts.yaml) is not yet wired here — only the loader hook exists. Production wiring of a real catalog sha calculator at the pipeline integration site is a separate task.DO NOT commit or push (Stage 3 unit policy).
[Codex #4] Stage 3 code-edit review IMP-46 u4
Scope: verified only the single executed unit, u4.
Verdict: u4 is correct. Step 12 now builds cache keys as
{frame_id}::{signature_hash}using the u1 structural signature helper, derives the char-count bucket before hashing, forwards unit cardinality intov4_result, computes the three invalidation fingerprints outsidecache.py, and attachescache_keyplusfingerprintsonly for AI-eligible records. The legacy template/section-id cache key is gone from the router call path, fuzzy lookup was not added, and the change does not introduce AI invocation outside the existing Step 12 router call.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath src\\phase_z2_ai_fallback\\step12.pyGet-Content -LiteralPath tests\\phase_z2_ai_fallback\\test_step12.pygit diff -- src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.pyrg -n "gather_step12_ai_repair_proposals\\(" src tests -g "*.py"python -m pytest tests/phase_z2_ai_fallback/test_step12.py -qrg -n "template_id|source_section_ids|mdx_path|MDX 03|MDX 04|MDX 05|read_proposal\\(|fingerprints=" src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.pyv4_labelmutates the generated cache key while preserving the AI route.python -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -qpython -m pytest tests/phase_z2_ai_fallback/test_signature.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_step12.py -qFiles checked:
src/phase_z2_ai_fallback/step12.pytests/phase_z2_ai_fallback/test_step12.pytests/phase_z2_ai_fallback/test_ast_isolation.pyTest results:
tests/phase_z2_ai_fallback/test_step12.py: 21 passed in 0.04stests/phase_z2_ai_fallback/test_ast_isolation.py: 22 passed in 0.12stest_signature.py,test_cache.py,test_cache_invalidation.py,test_step12.py): 94 passed in 0.25sVerified facts:
gather_step12_ai_repair_proposalsacceptscatalog_sha_loaderand calls it once per gather invocation when supplied.cache_keyandfingerprints; skipped records keep both fields asNone.cache._parse_key.frame_template_idandsource_section_idsno longer influence the generated cache key; changing those values with the same structural axes preserves the key.v4_labelaxis with routing held constant.char_countis bucketed before signature construction; values within the same bucket preserve the key and crossing a bucket boundary changes it.contract_sha,partial_sha, andcatalog_sha; contract/partial hashes are deterministic JSON-sorted SHA256 values, and the default catalog sentinel is an empty string.v4_result["cardinality"]now mirrors the unit value instead of being hardcoded toNone.mdx_pathleakage in the u4 Step 12 files; legacytemplate_id/source_section_idsreferences remain only as record fields, loader inputs, and tests proving they are excluded from the cache key.Remaining units: [u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #5] Stage 3 code-edit IMP-46 — u5 auto-cache CLI flag + 2^3 gate truth table
Executing unit: u5
■ unit_executed
ai_fallback_auto_cache: bool = Falsetosrc/config.pySettings; (2) extendsrc/phase_z2_ai_fallback/cache.py::save_proposalwith a keyword-onlyauto_cache: bool = Falseparameter that bypasses ONLY theuser_approvedgate (visual_check_passedremains mandatory and non-bypassable); (3) add--auto-cacheCLI flag insrc/phase_z2_pipeline.pythat mutatessettings.ai_fallback_auto_cache=Trueat parse time. The 2^3 gate truth table over(visual_check_passed, user_approved, auto_cache)is now exhaustively enumerated intests/phase_z2_ai_fallback/test_cache.pywith exactly three persisting rows:(T,T,F),(T,T,T),(T,F,T). Every other row raisesAiFallbackCacheGateErrorbefore any filesystem touch.■ files_changed
ai_fallback_auto_cache: bool = FalseSettings field + docstring)save_proposal(auto_cache=False)keyword-only param, docstring expanded with u5 contract section, module docstringu2 + u3→u2 + u3 + u5)--auto-cacheargparse flag + in-processsettings.ai_fallback_auto_cache=Truemutation)test_ai_fallback_auto_cache_default_off)■ diff_summary
src/config.pyai_fallback_auto_cache: bool = Falseplaced alongside the existing IMP-33 u1 ai_fallback_* block. Docstring explains the gate semantics ("bypassesuser_approvedonly;visual_check_passednever bypassable") and locks the default OFF + CLI mutation contract.src/phase_z2_ai_fallback/cache.pyIMP-46 u2 + u3→IMP-46 u2 + u3 + u5. New "u5 auto-cache contract" section enumerates the truth table (3 cells succeed, 5 raise) and pins the keyword-only / default-False shape.visual_check_passed=Falsealways raises …user_approved=Falsealso raises by default; the IMP-46 u5auto_cache=Trueoverride bypasses ONLY theuser_approvedgate (visual_check_passedis never bypassed). Gate violation never silently no-ops."save_proposalsignature gains keyword-onlyauto_cache: bool = Falseat the end of the kwargs block (afterfingerprints). Position-only callers cannot accidentally pass it.if not isinstance(auto_cache, bool): raise TypeError("auto_cache must be a bool"). Symmetric withslide_css/fingerprintsvalidation.if not user_approved: raise AiFallbackCacheGateError(...)if not user_approved and not auto_cache: raise AiFallbackCacheGateError(...)visual_check_passedgate is unchanged —auto_cachedoes NOT appear in that branch. Error messages updated to reflect the new bypass surface ("auto_cache cannot bypass this gate" on visual-check failure; "Pass auto_cache=True (or --auto-cache on the CLI) to bypass" on user-approved failure).src.configimport inside cache.py — the cache module remains a pure comparator/storage layer that takes the resolved boolean from the caller (preserves AI isolation contracttests/phase_z2_ai_fallback/test_ast_isolation.py:48-69).src/phase_z2_pipeline.py--auto-cacheflag added to the existingif __name__ == "__main__"argparse block (after--override-section-assignment, beforeargs = parser.parse_args()).action="store_true",default=False. Help text states the gate semantics + setting wiring.parse_args(): a 3-line conditional importssettingsfromsrc.configand setsai_fallback_auto_cache=Truewhen the flag is present. The deferred import (insideif args.auto_cache:) keepssrc.configoff the unconditional pipeline import surface — only operators who opt in pull it in.--auto-cachestarts back at the Settings default (False).tests/phase_z2_ai_fallback/test_cache.py_GATE_TRUTH_TABLEconstant — 8 rows, exhaustive Cartesian product of(V, U, A)×expect_persist. Hand-locked, NOT generated from the implementation, so a regression in the gate logic surfaces as a row failure (3 persist + 5 raise).test_save_gate_truth_tableparametrised — for each row, either callssave_proposaland asserts the file exists (persist row), or assertsAiFallbackCacheGateErrorraises AND the frame_id directory is absent (gate row — symmetric with the existingtest_save_gate_violation_does_not_touch_filesystem).test_auto_cache_default_off_preserves_dual_gate_semantics— calling without theauto_cachekwarg keeps the IMP-46 u2 behaviour (user_approved=Falseraises with"user_approved"in the message; no directory created).test_auto_cache_cannot_bypass_visual_check—(V=False, U=True, A=True)row promoted to its own assertion that also checks the error message mentionsvisual_check_passed(regression sentinel against accidentally movingauto_cacheinto the visual-check branch).test_auto_cache_bypass_user_approved_persists—(V=True, U=False, A=True)round-trip: persists, file exists,read_proposalreturns the original payload.test_auto_cache_rejects_non_bool— passingauto_cache="yes"raisesTypeError(symmetric with the existing non-string / non-dict guards forslide_cssandfingerprints).test_auto_cache_is_keyword_only—inspect.signature(save_proposal).parameters["auto_cache"]isKEYWORD_ONLYand defaultFalse. Locks the public surface so a future refactor can't accidentally make it positional or default-True.tests/test_phase_z2_ai_fallback_config.pytest_ai_fallback_auto_cache_default_off—Settings().ai_fallback_auto_cache is False. The CLI flag mutates the in-process settings instance; the default must stay OFF so the dual-gate contract survives without an operator opt-in.■ validation
python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py -q→ 51 passed in 0.22spython -m pytest tests/phase_z2_ai_fallback/ -q→ 201 passed in 0.40s (u1-u4 regression incl. test_ast_isolation, test_signature, test_cache_invalidation, test_step12 all green)python -m pytest tests/test_family_contract_baseline.py -q→ 4 passed in 0.59spython -m src.phase_z2_pipeline --help→--auto-cacheflag surfaced with the documented help textjson,pathlib,src.phase_z2_ai_fallback.schema); nosrc.configimport insidesrc/phase_z2_ai_fallback/(the setting is read by the CLI entry point and the resolved bool is passed in by callers, preserving the AI-isolation contracttests/phase_z2_ai_fallback/test_ast_isolation.py:48-69)■ scope-lock verification
route_ai_fallbackcall site changes — IMP-33 AI hook untouched.read_proposalsignature change — u3 fingerprint comparator surface preserved.save_proposalcall site changes outside tests — there is no productionsave_proposalinvocation yet (verified byrg "save_proposal\\(" src— onlycache.pydefinition + docstring references inrouter.py).visual_check_passedgate is non-bypassable —auto_cacheonly relaxesuser_approved. Truth-table row(V=False, U=True, A=True)raises, locked bytest_auto_cache_cannot_bypass_visual_check.Settings().ai_fallback_auto_cache is False(test_ai_fallback_auto_cache_default_off). Honoursfeedback_ai_isolation_contract(auto-cache is an explicit operator opt-in, not silent).auto_cacheis a pure axis of the cache write gate.■ remaining_units
data/frame_cache/.gitkeepvia.gitignoreexception under the existingdata/ignore, add repo-layout coverage (tests/phase_z2_ai_fallback/test_cache_repo_layout.py), and refreshdocs/architecture/IMP-17-CARVE-OUT.mdcache row from stub status → persistent backend status.■ follow_up_issue_candidates
(V, U, A)× cache write surface.FINAL_CONSENSUS_TARGET: continue_same (u6 remains)
[Codex #5] Stage 3 code-edit review IMP-46 u5
Scope: verified only the single executed unit, u5.
Verdict: u5 is correct.
Settings.ai_fallback_auto_cachedefaults toFalse,save_proposalnow accepts a keyword-onlyauto_cache: bool = False, and the gate logic preservesvisual_check_passedas non-bypassable while allowingauto_cache=Trueto bypass onlyuser_approved=False. The CLI adds--auto-cacheas astore_trueflag and mutatessettings.ai_fallback_auto_cache=Trueafter argument parsing. I did not find an added AI invocation or cache-module dependency on Phase Z runtime settings.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath src\config.pyGet-Content -LiteralPath src\phase_z2_ai_fallback\cache.pyGet-Content -LiteralPath src\phase_z2_pipeline.pyGet-Content -LiteralPath tests\phase_z2_ai_fallback\test_cache.pyGet-Content -LiteralPath tests\test_phase_z2_ai_fallback_config.pyrg -n "auto_cache|auto-cache|ai_fallback_auto_cache|save_proposal\(" src\config.py src\phase_z2_ai_fallback\cache.py src\phase_z2_pipeline.py tests\phase_z2_ai_fallback\test_cache.py tests\test_phase_z2_ai_fallback_config.pypython -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py -qsave_proposalsignature andSettings().ai_fallback_auto_cachepython -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -qgit diff -- src/config.py src/phase_z2_ai_fallback/cache.py src/phase_z2_pipeline.py tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py--auto-cacheargparse entry andsettings.ai_fallback_auto_cachemutationgit status --shortFiles checked:
src/config.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_pipeline.pytests/phase_z2_ai_fallback/test_cache.pytests/test_phase_z2_ai_fallback_config.pytests/phase_z2_ai_fallback/test_ast_isolation.pyTest results:
tests/phase_z2_ai_fallback/test_cache.pyplustests/test_phase_z2_ai_fallback_config.py: 51 passed in 0.14stests/phase_z2_ai_fallback/test_ast_isolation.py: 22 passed in 0.14sVerified facts:
Settings().ai_fallback_auto_cache is False.save_proposalsignature is(key: 'str', proposal: 'AiFallbackProposal', *, visual_check_passed: 'bool', user_approved: 'bool', slide_css: 'str | None' = None, fingerprints: 'dict | None' = None, auto_cache: 'bool' = False) -> 'pathlib.Path'.auto_cacheis keyword-only and defaults toFalse.auto_cacheraisesTypeError.(visual_check_passed, user_approved, auto_cache)combinations; exactly(True, True, False),(True, True, True), and(True, False, True)persist.visual_check_passed=FalseraisesAiFallbackCacheGateErroreven whenauto_cache=True, and rejected rows do not create the frame cache directory.--auto-cacheis registered withdest="auto_cache",action="store_true", anddefault=False.args.auto_cacheis true,src.phase_z2_pipelinemutates_settings.ai_fallback_auto_cache = True.cache.pydoes not importsrc.config, Anthropic, or Phase Z runtime modules for this unit; callers still pass the resolved boolean explicitly.Remaining units: [u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #1] Stage 1 problem-review — IMP-46 (#62) residual scope after #76 absorption
Carve-out status (#76 absorption)
Commit
1186ad8(#76 IMP-47B activation) absorbed substantially all of the originally-planned IMP-46 implementation_units. Per memory ruleproject_imp46_carveout_caveat, Stage 1 must enumerate what is already merged and bound new work to NON-overlapping territory only.Already merged in
1186ad8(DO NOT re-stir):build_signature,bucket_char_count)src/phase_z2_ai_fallback/signature.py:1-92data/frame_cache/{frame_id}/{signature_hash}.jsonsrc/phase_z2_ai_fallback/cache.py:108-243contract_sha/partial_sha/catalog_sha)src/phase_z2_ai_fallback/cache.py:38-44, 145-148src/phase_z2_ai_fallback/step12.py:160-185--auto-cacheCLI +settings.ai_fallback_auto_cache(bypasses ONLYuser_approved)src/config.py:29,src/phase_z2_pipeline.py:6025-6037,src/phase_z2_ai_fallback/cache.py:158-217data/frame_cache/.gitkeep+.gitignorepayload exclusion + carve-out docdata/frame_cache/.gitkeep,.gitignore,docs/architecture/IMP-17-CARVE-OUT.md:54_persist_ai_repair_proposals_to_cachecalled after Step 14 visual check, threadingvisual_check_passedfrom overflow result andauto_cachefrom settingssrc/phase_z2_pipeline.py:718-775, 5534-5550Residual root cause (1 axis)
Axis R — Router read-path fingerprint enforcement gap.
route_ai_fallback(cache_key, ...)accepts nofingerprintsparameter (src/phase_z2_ai_fallback/router.py:43-53).cached = read_proposal(cache_key)is called WITHOUTfingerprints=...(src/phase_z2_ai_fallback/router.py:66).contract_sha/partial_sha/catalog_sha) and stores them in the per-unit record (src/phase_z2_ai_fallback/step12.py:179-185), but never threads them through to the router.src/phase_z2_ai_fallback/step12.py:48-51: "the router's existingread_proposal(cache_key)continues to perform exact-match lookup only (fuzzy is deferred per Stage 2 plan); read-side fingerprint validation through the router is a follow-up axis."Consequence: When a frame contract / partial template / catalog payload changes, the save-side correctly tags new entries with fresh fingerprints, but lookups continue to return stale entries because the read path does not compare fingerprints. The invalidation contract documented in
cache.py:28-44is enforceable only at the caller ofread_proposal, and the production caller (router) does not pass fingerprints. Issue body explicit guardrail "contract/partial 변경 시 invalidate (cache 일관성)" therefore is half-enforced: tested in unit isolation, not enforced in production read path.Out of scope (explicit exclusions, carve-out boundary)
cache.py,signature.py, save-side pipeline wiring,--auto-cachesemantics,.gitignore, ordata/frame_cache/.gitkeep(all merged in1186ad8— see carve-out table).c864fe0).route_ai_fallbackcall sites beyond current production sitesrc/phase_z2_ai_fallback/step12.py:194-203.mdx_normalizerStage 0 integration (separate lock 2026-05-08).user_approvedUX gate (documented design choice — pipeline relies onauto_cacheopt-in per IMP-46 u5; seesrc/phase_z2_pipeline.py:5538-5542).IMP-17-CARVE-OUT.md:52).Proposed scope-lock (residual axis only)
route_ai_fallbacksignature with an optionalfingerprints: dict | None = Nonekeyword-only kwarg. DefaultNonepreserves current router behavior and back-compat with existingtest_router.pycases.fingerprintsthrough toread_proposal(cache_key, fingerprints=fingerprints)(cache.py:108 already supports this kwarg; nocache.pyedit needed).fingerprintsfromsrc/phase_z2_ai_fallback/step12.py:194-203route_ai_fallback(...)call. Source = the existingfingerprintsdict already computed atstep12.py:179-185. No new computation — pure reuse.tests/phase_z2_ai_fallback/test_router.pywith three new cases:read_proposalreturnsNone→ falls through to client path (validates strict-equality invalidation reaches production)fingerprints=None(legacy back-compat) → returns cached without comparison (preserves existing router test surface)tests/phase_z2_ai_fallback/test_step12.pyto assertfingerprintsis forwarded toroute_ai_fallback(mock router, inspect kwargs).src/phase_z2_ai_fallback/router.py(signature + one call),src/phase_z2_ai_fallback/step12.py(one call site),tests/phase_z2_ai_fallback/test_router.py(3 cases),tests/phase_z2_ai_fallback/test_step12.py(1-2 assertions).Guardrails (Stage 2 binding)
project_imp46_carveout_caveat): files modified in1186ad8are NOT in scope for content rewrites. Only the call site instep12.py:194-203(theroute_ai_fallback(...)invocation) gets one kwarg added. No re-touch of the fingerprints computation block (step12.py:179-185).feedback_ai_isolation_contract): cache layer adds no AI call; router still does not importsave_proposal;test_router_does_not_import_save_proposal(test_router.py:153) invariant holds. AST isolation test (tests/phase_z2_ai_fallback/test_ast_isolation.py) keeps the import whitelist intact.feedback_no_hardcoding): fingerprints sourced exclusively from existingstep12.pycompute (frame_contract / figma_partial_json / catalog_sha_loader). No new per-sample literals; no MDX section ids re-enter the signature surface.feedback_artifact_status_naming): cache hit/miss must remain a distinct axis from rendered / visual_check / coverage. Step 12 record schema (step12.py:122-135) already keeps them separate — no merging.feedback_one_step_per_turn): Stage 1 = problem-review comment only; no code edit, no plan unit enumeration. Stage 2 enumerates units.feedback_scope_qualified_verification): Stage 4 verify must scope to router-fingerprint axis only — no "all tests pass" generic claim.Unresolved questions for Stage 2
route_ai_fallback'sfingerprintskwarg be strictly keyword-only (recommended for forward compat) or positional? Default recommendation: keyword-only, matching the cache.py pattern (read_proposal(key, *, fingerprints=None)atcache.py:108-112).fingerprints=Noneis passed AND a cache entry exists with stored non-empty fingerprints, currentread_proposalbehavior = return entry (no comparison). Stage 2 to confirm: keep legacy back-compat (router defaultNone→ no invalidation enforcement, opt-in via step12 thread) OR tighten to "fingerprints=None ⇒ never trust stored entry with fingerprints"? Default recommendation: keep legacy back-compat to honorcache.py:38-44strict-equality semantics (caller opt-in to invalidation).catalog_shasource be wired fromstep12.py:116-118(current: optionalcatalog_sha_loaderdefaulting to"") up through the production pipeline call site, or is""acceptable until B4 frame_selection evidence integration formalizes the catalog snapshot? Default recommendation: defer —""is the same fingerprint on both write and read untilcatalog_sha_loaderis wired upstream, so invalidation behaves consistently within a release; tighten in a follow-up axis when B4 lands.Evidence
src/phase_z2_ai_fallback/router.py:43-66src/phase_z2_ai_fallback/step12.py:160-185, 194-203src/phase_z2_ai_fallback/step12.py:48-51src/phase_z2_ai_fallback/cache.py:108-148src/phase_z2_pipeline.py:5534-5550src/phase_z2_pipeline.py:718-775_persist_ai_repair_proposals_to_cacheinvocation:src/phase_z2_pipeline.py:5545-5550(visual_check_passed fromoverflow.get("passed"), auto_cache fromsettings.ai_fallback_auto_cache, user_approved hardcoded False per documented design choice)tests/phase_z2_ai_fallback/test_router.py:68-157docs/architecture/IMP-17-CARVE-OUT.md:541186ad8c864fe0[Codex #1] Stage 1 problem-review -- IMP-46 (#62)
Scope-lock result: Claude r1 is correct. The #76 absorption already landed the cache backend, structural signature, save gates, auto-cache flag, and save-side fingerprint persistence. The remaining IMP-46 work should not re-open those areas.
Residual root cause verified: the production cache read path is not fingerprint-aware.
src/phase_z2_ai_fallback/cache.py:108-148already supportsread_proposal(key, fingerprints=...)and invalidates on strict stored-vs-supplied mismatch.src/phase_z2_ai_fallback/step12.py:179-185computes and recordscontract_sha,partial_sha, andcatalog_sha. Butsrc/phase_z2_ai_fallback/router.py:43-53accepts nofingerprintsargument andsrc/phase_z2_ai_fallback/router.py:66callsread_proposal(cache_key)with the defaultfingerprints=None, whichsrc/phase_z2_ai_fallback/cache.py:38-44documents as no comparison.src/phase_z2_ai_fallback/step12.py:195-203calls the router without passing the already-computed fingerprints.Consequence: stale cache entries can be returned after contract / partial / catalog changes whenever the structural signature is unchanged. The cache layer can enforce invalidation, but the production router caller is not supplying the invalidation inputs.
Scope-locked implementation axis:
route_ai_fallbackto acceptfingerprints: dict | None = None, pass it toread_proposal(cache_key, fingerprints=fingerprints), and threadrecord["fingerprints"]from Step 12 into the router call.Missing-file / wrong-assumption check: no missing required files found for this residual axis. The relevant files exist at
src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/step12.py,src/phase_z2_ai_fallback/cache.py,src/phase_z2_ai_fallback/signature.py,src/phase_z2_pipeline.py,src/config.py,data/frame_cache/.gitkeep,.gitignore, anddocs/architecture/IMP-17-CARVE-OUT.md. No pytest was run, per Stage 1 instruction.=== EVIDENCE ===
Commands run:
git status --shortrg -n "def route_ai_fallback|read_proposal\(|fingerprints|TODO|cache_key" src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.pyrg -n "auto_cache|_persist_ai_repair_proposals_to_cache|visual_check_passed|ai_fallback_auto_cache|frame_cache|build_signature|bucket_char_count" src data .gitignore docs/architecture/IMP-17-CARVE-OUT.mdrg -n "route_ai_fallback\(|read_proposal\(|save_proposal\(|fingerprints" src testssrc/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/step12.py,src/phase_z2_ai_fallback/cache.py,src/phase_z2_ai_fallback/signature.py,src/phase_z2_pipeline.py, andsrc/config.pyFiles checked:
src/phase_z2_ai_fallback/router.pysrc/phase_z2_ai_fallback/step12.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pysrc/phase_z2_pipeline.pysrc/config.py.gitignoredata/frame_cache/.gitkeepdocs/architecture/IMP-17-CARVE-OUT.mdTest results: not run; Stage 1 task explicitly says grep/find and NO pytest.
Verified facts:
read_proposalsupports optional fingerprint comparison and returnsNoneon mismatch when fingerprints are supplied.save_proposalpersists supplied fingerprints and still enforces visual-check plus approval/auto-cache gates.FINAL_CONSENSUS: YES
[Claude #2] Stage 2 simulation-plan — IMP-46 (#62) router read-path fingerprint enforcement (post-#76-absorption, Axis R only)
Scope = Stage 1 allowed_edits + allowed_tests verbatim. All #76-absorbed territory (signature axes / JSON cache path / cache.py invalidation comparator / save-side wiring / --auto-cache / catalog promotion) FROZEN.
=== AXIS COVERAGE ===
=== IMPLEMENTATION_UNITS ===
summary: Add fingerprints=None kwarg to route_ai_fallback signature, forward to read_proposal, repair 6 existing router test lambdas to accept the kwarg via
lambda key, **_:.files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Add router-level miss + hit cases — mismatched fingerprints → cache miss → client called; matching fingerprints → cache hit → client NOT called.
files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 30
summary: Step 12 forwards record["fingerprints"] into route_ai_fallback(fingerprints=...) at step12.py:195-203.
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 4
summary: Step12 integration test patching step12_mod.route_ai_fallback with MagicMock; assert call_args.kwargs["fingerprints"] equals record["fingerprints"] dict built at step12.py:179-185.
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
=== PER-UNIT RATIONALE ===
fingerprints=Noneno-op default. Router gap = missing kwarg + forward only. Optional kwarg (not positional) preserves non-step12 callers per Stage 1 Q2.=== OUT OF SCOPE (Stage 1 lock) ===
signature.py, JSON cache path format, cache.py comparator, save-side _persist_ai_repair_proposals_to_cache, --auto-cache CLI / settings.ai_fallback_auto_cache, fuzzy lookup, sample-specific logic, AI request behaviour, catalog promotion, u1~u6 of original IMP-46 plan (all absorbed in commit
1186ad8).=== ROLLBACK ===
Two-file revert (router.py + step12.py) restores pre-change behaviour. No on-disk cache schema change; existing cache files remain readable under default fingerprints=None.
=== SIDE EFFECTS ===
=== EVIDENCE ===
read_proposal(cache_key)lacks fingerprints arg.FINAL_CONSENSUS: YES
[Codex #2] Stage 2 simulation-plan verification - IMP-46 (#62) router read-path fingerprint enforcement
=== IMPLEMENTATION_UNITS ===
summary: Add optional fingerprints kwarg to route_ai_fallback and forward it into read_proposal; update existing router test read_proposal monkeypatch lambdas to accept the new kwarg.
files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Add router-level cache miss/hit coverage for mismatched versus matching fingerprints, proving stale entries miss and matching entries return without client call.
files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 30
summary: Forward record["fingerprints"] from Step 12 into route_ai_fallback at the existing AI-eligible call site.
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 4
summary: Add Step 12 integration coverage proving the router call receives the exact fingerprints dict already attached to the AI repair record.
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
=== PER-UNIT RATIONALE ===
=== OUT OF SCOPE ===
signature.py structural axes, data/frame_cache path format, cache.py invalidation comparator, save-side cache persistence, --auto-cache semantics, fuzzy lookup, AI request behavior, catalog promotion, sample-specific handling, and all #76-absorbed IMP-46 units remain frozen.
=== ROLLBACK STRATEGY ===
Revert only the four planned files if Stage 3 regresses. No data files, cache artifacts, templates, catalog files, or settings should be committed for this issue.
=== EVIDENCE ===
Commands run: rg -n "def route_ai_fallback|read_proposal\(|route_ai_fallback\(|fingerprints|class|def test|lambda key" src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py src/phase_z2_ai_fallback/cache.py; git status --short; rg --files tests src/phase_z2_ai_fallback | sort; pytest -q tests; Select-String -LiteralPath ".env" -Pattern "AI_FALLBACK|ai_fallback"; line-range reads for router.py, step12.py, test_router.py, cache.py, src/config.py, tests/test_phase_z2_ai_fallback_config.py.
Files checked: src/phase_z2_ai_fallback/router.py; src/phase_z2_ai_fallback/step12.py; src/phase_z2_ai_fallback/cache.py; tests/phase_z2_ai_fallback/test_router.py; tests/phase_z2_ai_fallback/test_step12.py; src/config.py; tests/test_phase_z2_ai_fallback_config.py; .env.
Test results: pytest -q tests failed before implementation: 979 passed, 4 failed. Failures are tests/test_imp47b_step12_ai_wiring.py::test_mixed_units_classified_by_route_and_provisional_flag, ::test_reject_provisional_unit_reaches_router_short_circuit, ::test_step12_ai_repair_artifact_writes_json_serialisable_records, and tests/test_phase_z2_ai_fallback_config.py::test_ai_fallback_master_flag_default_off.
Verified facts: router.py:43-53 has no fingerprints parameter; router.py:66 calls read_proposal(cache_key) without fingerprints; step12.py:179-185 builds record["fingerprints"]; step12.py:195-203 does not forward it; cache.py:38-44 and cache.py:108-148 already define optional strict fingerprint comparison; test_router.py:89/102/111/124/137/146 have read_proposal lambdas that would need kwarg-compatible signatures; each implementation unit is <=50 estimated lines and <=3 files.
Side effects: the required baseline is red because .env:4 sets AI_FALLBACK_ENABLED=true, overriding the locked Settings default at src/config.py:19 and violating the expectation asserted at tests/test_phase_z2_ai_fallback_config.py:22-26. This must be cleared or explicitly handled before Stage 3/4 can produce meaningful regression evidence.
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Claude #3] Stage 2 simulation-plan — IMP-46 (#62) router fingerprint enforcement (Axis R, r2)
Delta vs prior r1 (Codex #2 NO, rewind_target=stage_2_plan): adds u5 to clear pre-existing baseline-red Codex flagged (.env:4
AI_FALLBACK_ENABLED=true→ 4 tests fail on freshSettings(): test_phase_z2_ai_fallback_config.py::test_ai_fallback_master_flag_default_off + 3 test_imp47b_step12_ai_wiring.py cases). u5 = test-side isolation via autouse conftest fixture; .env untouched (feedback_demo_env_toggle_policy preserved). u1~u4 unchanged.=== IMPLEMENTATION_UNITS ===
summary: Add fingerprints=None kwarg to route_ai_fallback signature; forward to read_proposal(cache_key, fingerprints=fingerprints); repair 6 router test lambdas @:89/102/111/124/137/146 to accept the kwarg (
lambda key, **_:).files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Add router-level miss + hit cases — mismatched fingerprints → cache miss → client called; matching fingerprints → cache hit → client NOT called (mock read_proposal, no filesystem).
files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 30
summary: Step 12 forwards record["fingerprints"] into route_ai_fallback(fingerprints=...) at step12.py:195-203.
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 4
summary: Step12 integration test patching step12_mod.route_ai_fallback with MagicMock; assert call_args.kwargs["fingerprints"] equals record["fingerprints"] dict built at step12.py:179-185.
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
summary: Create tests/conftest.py with an autouse session fixture that deletes AI_FALLBACK_ENABLED from os.environ before any Settings() construction, restoring the locked default (False) for the 4 baseline-red tests; .env file untouched.
files: [tests/conftest.py]
tests: [tests/test_phase_z2_ai_fallback_config.py, tests/test_imp47b_step12_ai_wiring.py]
estimate_lines: 20
=== PER-UNIT RATIONALE ===
fingerprints=Noneno-op default. Optional kwarg (not positional) preserves non-step12 callers per Stage 1 Q2.Settings()from demo .env. Does NOT touch .env (feedback_demo_env_toggle_policy preserved). Codex #2 ask satisfied.=== OUT OF SCOPE (Stage 1 lock) ===
signature.py / JSON cache path / cache.py comparator / save-side _persist_ai_repair_proposals_to_cache / --auto-cache CLI / settings.ai_fallback_auto_cache / fuzzy lookup / sample-specific logic / AI request behaviour / catalog promotion / u1~u6 of original IMP-46 plan (absorbed in
1186ad8). .env file content (demo toggle lock).=== ROLLBACK ===
Three-file revert (router.py + step12.py + tests/conftest.py). No on-disk cache schema change; existing #76 cache files remain readable under default fingerprints=None.
=== EVIDENCE ===
read_proposal(cache_key)lacks fingerprints arg._call+ MagicMock pattern available for u4.AI_FALLBACK_ENABLED=trueconfirmed (Bash cat).FINAL_CONSENSUS: YES
[Codex #3] Stage 2 simulation-plan verification - IMP-46 router fingerprint enforcement
=== IMPLEMENTATION_UNITS ===
summary: Add optional fingerprints kwarg to route_ai_fallback and forward it into read_proposal; update existing router read_proposal monkeypatches to accept the new kwarg.
files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Add router-level cache miss/hit coverage for mismatched versus matching fingerprints, proving stale entries miss and matching entries return without client call.
files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 30
summary: Forward record["fingerprints"] from Step 12 into route_ai_fallback at the existing AI-eligible call site.
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 4
summary: Add Step 12 integration coverage proving route_ai_fallback receives the exact fingerprints dict already attached to the AI repair record.
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
summary: Proposed baseline-red fix creates tests/conftest.py with an autouse session fixture deleting AI_FALLBACK_ENABLED from os.environ before Settings() construction.
files: [tests/conftest.py]
tests: [tests/test_phase_z2_ai_fallback_config.py, tests/test_imp47b_step12_ai_wiring.py]
estimate_lines: 20
=== PER-UNIT RATIONALE ===
=== OUT OF SCOPE ===
signature.py axes, JSON cache path, cache.py invalidation comparator, save-side cache gates, --auto-cache semantics, fuzzy lookup, AI request behavior, catalog promotion, and all #76 absorbed units remain out of scope.
=== ROLLBACK STRATEGY ===
Revert only the router.py, step12.py, and focused test-file edits for this axis. Do not edit .env; replace u5 with a plan that neutralizes Settings env_file or patches the imported settings objects inside the affected tests.
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Claude #4] Stage 2 simulation-plan — IMP-46 (#62) Axis R r3 (post Codex #3 NO)
Delta vs prior r1/r2: u5 (
tests/conftest.pyAI_FALLBACK_ENABLED scrubber) DROPPED. Stage 1allowed_edits= router.py + step12.py only;allowed_tests= router-level + step12 integration only. conftest.py is scope creep both axes. The 4.env-driven baseline reds (.env:4 AI_FALLBACK_ENABLED=true) are pre-existing, axis-orthogonal, out-of-scope; router testsmonkeypatch.setattr(router_mod.settings, "ai_fallback_enabled", ...)so .env is irrelevant for u1u4 (verified:u4 unchanged.pytest tests/phase_z2_ai_fallback/test_router.py= 9 passed). u1=== IMPLEMENTATION_UNITS ===
summary: Add
fingerprints: dict | None = Nonekwarg-only param toroute_ai_fallback(router.py:43-53, appended afterclient); forward viaread_proposal(cache_key, fingerprints=fingerprints)(router.py:66); repair 6 router-test lambdas at test_router.py:89/102/111/124/137/146 fromlambda key: ...tolambda key, **_: .... Carry-fix shares the same decision unit (intermediate state baseline-red).files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Router-level fingerprint coverage in test_router.py — (a) mismatched fingerprints → read_proposal returns None → client.request_proposal IS called; (b) matching → cached returned, client NOT called; (c)
fingerprints=Nonelegacy → cached returned. Each mocks read_proposal with a spy asserting thefingerprintskwarg. Each test docstring declares scope = router-level.files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 35
summary: Forward
record["fingerprints"]intoroute_ai_fallbackat step12.py:194-203 (addfingerprints=fingerprints,kwarg; record["fingerprints"] already built at step12.py:179-185).files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 3
summary: Step 12 integration coverage in test_step12.py — assert
router.call_args.kwargs["fingerprints"]equals dict atrecord["fingerprints"]. Positive case (AI-eligible_ai_unit()→ forward, shape{contract_sha, partial_sha, catalog_sha}) + negative case (skipped unit → router not called,record["fingerprints"] is None). Mocksstep12_mod.route_ai_fallbackper_callpattern at test_step12.py:62-76.files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
=== PER-UNIT RATIONALE ===
fingerprints=Noneno-op default; strict-equality comparator at cache.py:145-148. Kwarg + default None preserves any non-step12 callers (Stage 1 Q2 lock).=== OUT OF SCOPE (Stage 1 lock + #76 absorbed) ===
signature.py / data/frame_cache JSON path / cache.py comparator / save-side
_persist_ai_repair_proposals_to_cache/--auto-cacheCLI / settings.ai_fallback_auto_cache / fuzzy lookup / catalog promotion /.env:4toggle + induced 4 baseline reds (feedback_demo_env_toggle_policy; out-of-scope follow-up).=== ROLLBACK ===
Per-unit revert; u3 depends on u1 (revert u3 first). u2+u4 additive. No new files. No on-disk cache schema change; #76 cache JSONs remain readable under default
fingerprints=None.=== EVIDENCE ===
fingerprints; read_proposal call lacks forward (Read).fingerprintskwarg + strict-equality already present (Read).record["fingerprints"]built; step12.py:194-203 call site does not forward (Read).lambda key:monkeypatches (Grep, 6 hits)._call+ MagicMock pattern for u4 (Read)..env:4 AI_FALLBACK_ENABLED=true(Bash grep).pytest tests/test_phase_z2_ai_fallback_config.py tests/test_imp47b_step12_ai_wiring.py= 4 failed (pre-existing) / 6 passed. Axis-orthogonal.pytest tests/phase_z2_ai_fallback/test_router.py= 9 passed (router scope clean).FINAL_CONSENSUS: YES
[Codex #4] Stage 2 simulation-plan verification - IMP-46 (#62) Axis R
=== IMPLEMENTATION_UNITS ===
summary: Add optional fingerprints kwarg to src/phase_z2_ai_fallback/router.py route_ai_fallback after client, and forward it into read_proposal as fingerprints=fingerprints. Update existing read_proposal monkeypatches in tests/phase_z2_ai_fallback/test_router.py to accept the new kwarg.
files: [src/phase_z2_ai_fallback/router.py, tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 25
summary: Add router-level cache read coverage for mismatched fingerprints miss, matching fingerprints hit, and legacy fingerprints=None hit. Each case must assert the read_proposal fingerprints kwarg and declare router-level scope.
files: [tests/phase_z2_ai_fallback/test_router.py]
tests: [tests/phase_z2_ai_fallback/test_router.py]
estimate_lines: 35
summary: Forward the fingerprints dict already built in src/phase_z2_ai_fallback/step12.py into the existing route_ai_fallback call at the AI-eligible path.
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 3
summary: Add Step 12 integration coverage proving route_ai_fallback receives exactly record["fingerprints"] for an AI-eligible unit, and proving skipped/non-AI records do not call router and keep fingerprints as None.
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 25
=== PER-UNIT RATIONALE ===
Out-of-scope notes:
Rollback strategy:
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
FINAL_CONSENSUS: YES
[Claude #1] Stage 3 code-edit — IMP-46 (#62) Axis R u1
Executing unit: u1
unit_executed
fingerprintskwarg toroute_ai_fallbackafterclient, forward it intoread_proposal(cache_key, fingerprints=fingerprints), and repair the 6 existingread_proposalmonkeypatch lambdas intest_router.pyto accept the new kwarg (carry-fix bundled in u1 per Stage 2 plan to avoid mid-state baseline-red).files_changed
src/phase_z2_ai_fallback/router.pytests/phase_z2_ai_fallback/test_router.pydiff_summary
src/phase_z2_ai_fallback/router.py:43-66fingerprints: dict | None = Noneafterclientinroute_ai_fallbacksignature.fingerprintsinto existing cache call:read_proposal(cache_key, fingerprints=fingerprints).Nonepreserves legacy no-comparison behaviour.tests/phase_z2_ai_fallback/test_router.py(6 lambdas at lines 89, 102, 111, 124, 137, 146 in baseline)lambda key: ...→lambda key, **_: ...for everymonkeypatch.setattr(router_mod, "read_proposal", ...)site (4×None, 1×cached, 1×bad_cached).scope verification
allowed_editshonoured:router.py: extendroute_ai_fallback(..., fingerprints: dict | None = None)✓router.py: passfingerprintstoread_proposal(cache_key, fingerprints=fingerprints)✓step12.py: untouched (deferred to u3) ✓cache.py,signature.py,_persist_ai_repair_proposals_to_cache,--auto-cache,Settings.ai_fallback_auto_cache, AI request behaviour, catalog promotion, all #76-absorbed units.1186ad8cache carve-out: not stirred (memory:project_imp46_carveout_caveat).scope-qualified verification
tests/phase_z2_ai_fallback/test_router.py).python -m pytest tests/phase_z2_ai_fallback/test_router.py -q9 passed in 0.04s(matches Stage 2 baseline of 9 passed; signature widening + lambda carry-fix preserves green).remaining_units
Nonehit) with assertions on theread_proposalfingerprintskwarg and router-level scope docstrings.record["fingerprints"]into the existingroute_ai_fallbackcall insrc/phase_z2_ai_fallback/step12.pyAI-eligible path.route_ai_fallbackreceives exactlyrecord["fingerprints"]for AI-eligible units and skipped / non-AI records do not call the router.follow_up_issue_candidates
None this turn. No scope-lock-external axis observed during u1. Stage 2 already records
tests/conftest.pyAI_FALLBACK_ENABLED scrubber and the 4.env-driven baseline-red items as separate follow-up axes.guardrails honoured
feedback_workflow_atomicity_rules: 1 commit = 1 decision unit (u1 carry-fix bundled to avoid mid-state baseline-red).feedback_one_step_per_turn: only u1 executed; u2~u4 deferred.feedback_no_hardcoding: read-path contract widened (process), not artifact patched.feedback_scope_qualified_verification: pytest scope declared router-level.feedback_factual_verification: every change cites file + line range.[Codex #1] Stage 3 code-edit review -- IMP-46 (#62) Axis R u1
Scope: verified only the single executed unit, u1.
Verdict: u1 is correct.
route_ai_fallbacknow accepts optionalfingerprintsafterclientand forwards that value intoread_proposal(cache_key, fingerprints=fingerprints). The six pre-existing router test monkeypatches now accept the new keyword, preserving router-test compatibility without adding u2 coverage early.Scope lock checked:
step12.py,cache.py, andsignature.pyhave no scoped diff in this verification. The implemented changes are limited tosrc/phase_z2_ai_fallback/router.pyandtests/phase_z2_ai_fallback/test_router.py.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.pyrg -n "def route_ai_fallback|read_proposal\(|fingerprints|lambda key" src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.pygit diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pypython -m pytest tests/phase_z2_ai_fallback/test_router.py -qFiles checked:
src/phase_z2_ai_fallback/router.py:43-72tests/phase_z2_ai_fallback/test_router.py:86-147src/phase_z2_ai_fallback/step12.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pyTest results:
python -m pytest tests/phase_z2_ai_fallback/test_router.py -q->9 passed in 0.04sVerified facts:
src/phase_z2_ai_fallback/router.py:53addsfingerprints: dict | None = None.src/phase_z2_ai_fallback/router.py:72callsread_proposal(cache_key, fingerprints=fingerprints).tests/phase_z2_ai_fallback/test_router.py:89,102,111,124,137,146uselambda key, **_:for existingread_proposalmonkeypatches.git diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyproduced no diff, so u3/u4 and cache/signature territory were not touched in this unit.Remaining units: [u2, u3, u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #2] Stage 3 code-edit — IMP-46 (#62) Axis R u2
Executing unit: u2
unit_executed
fingerprints=None→ hit → client NOT invoked. Each new test captures thefingerprintskwarg actually passed toread_proposalvia a keyword-spy, asserts strict equality against the caller-supplied dict (orNone), and declares router-level scope in its docstring per Stage 2 plan.files_changed
tests/phase_z2_ai_fallback/test_router.pydiff_summary
tests/phase_z2_ai_fallback/test_router.py— three new test functions inserted betweentest_router_validates_cached_proposalandtest_router_calls_client_and_returns_validated_proposal:test_router_forwards_fingerprints_and_misses_on_mismatch— suppliesfingerprints={"contract_sha": "aaa", "partial_sha": "bbb", "catalog_sha": "ccc"}, spyread_proposalreturnsNone(simulating strict-equality SHA mismatch), router proceeds to call client, captured kwarg equals supplied dict.test_router_forwards_fingerprints_and_hits_on_match— suppliesfingerprints={"contract_sha": "xxx", "partial_sha": "yyy", "catalog_sha": "zzz"}, spyread_proposalreturns cached proposal (simulating strict-equality match), router short-circuits,client.request_proposalnot called, captured kwarg equals supplied dict.test_router_forwards_fingerprints_none_for_legacy_callers— caller omitsfingerprints, spyread_proposalreturns cached proposal, captured kwarg isNone, client not called (legacy no-invalidation behaviour preserved).src/phase_z2_ai_fallback/router.pyalready accepts and forwardsfingerprintsper u1.read_proposalmonkeypatch lambdas (already repaired in u1 withlambda key, **_:) remain unchanged.scope verification
per_file_changesfor u2 honoured:tests/phase_z2_ai_fallback/test_router.pyonly — 3 new fingerprint scenarios added; no existing test assertion changed.allowed_editshonoured: u2 lives entirely in the router-level test file; production surface untouched until u3.src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/step12.py,tests/phase_z2_ai_fallback/test_step12.py,cache.py,signature.py,_persist_ai_repair_proposals_to_cache,--auto-cache,Settings.ai_fallback_auto_cache, AI request behaviour, catalog promotion, all #76-absorbed units.1186ad8cache carve-out: not stirred (memory:project_imp46_carveout_caveat).feedback_ai_isolation_contract).feedback_no_hardcoding): test fingerprint values are inline test fixtures, not sample-specific MDX or frame DB leakage.scope-qualified verification
tests/phase_z2_ai_fallback/test_router.py).python -m pytest tests/phase_z2_ai_fallback/test_router.py -q12 passed in 0.49s(u1 baseline 9 passed + 3 new u2 scenarios = 12). No regressions in pre-existing 9 tests.remaining_units
record["fingerprints"]into the existingroute_ai_fallbackcall insrc/phase_z2_ai_fallback/step12.pyAI-eligible path (~3 lines).route_ai_fallbackreceives exactlyrecord["fingerprints"]for AI-eligible units and skipped / non-AI records do not call the router.follow_up_issue_candidates
None this turn. No scope-lock-external axis observed during u2. Stage 2 already records
tests/conftest.pyAI_FALLBACK_ENABLED scrubber (former u5) and 4.env-driven baseline-red as separate-axis follow-ups; no new candidates surfaced.[Codex #2] Stage 3 code-edit review -- IMP-46 (#62) Axis R u2
Scope: verified only the single executed unit, u2.
Verdict: u2 is correct. The router-level test file adds the three required fingerprint scenarios: mismatch simulated as cache miss with client invocation, match simulated as cache hit with no client invocation, and legacy omitted fingerprints forwarded as
None. Each new test captures the actualfingerprintskwarg passed toread_proposaland declares router-level scope in its docstring.Scope lock checked: production code changes remain limited to the previously verified u1 router forwarding. This u2 verification found no scoped diff in
step12.py,test_step12.py,cache.py, orsignature.py, so u3/u4 and cache/signature territory were not touched by this unit.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- tests/phase_z2_ai_fallback/test_router.py src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyrg -n "fingerprints|def test_router|read_proposal|request_proposal|route_ai_fallback" tests/phase_z2_ai_fallback/test_router.py src/phase_z2_ai_fallback/router.pypython -m pytest tests/phase_z2_ai_fallback/test_router.py -qFiles checked:
tests/phase_z2_ai_fallback/test_router.py:109-187src/phase_z2_ai_fallback/router.py:43-72src/phase_z2_ai_fallback/step12.pytests/phase_z2_ai_fallback/test_step12.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pyTest results:
python -m pytest tests/phase_z2_ai_fallback/test_router.py -q->12 passed in 0.04sVerified facts:
tests/phase_z2_ai_fallback/test_router.py:109addstest_router_forwards_fingerprints_and_misses_on_mismatch; its spy capturesfingerprints, returnsNone, andclient.request_proposal.assert_called_once()verifies fallback on miss.tests/phase_z2_ai_fallback/test_router.py:139addstest_router_forwards_fingerprints_and_hits_on_match; its spy capturesfingerprints, returns a cached proposal, andclient.request_proposal.assert_not_called()verifies cache-hit short-circuiting.tests/phase_z2_ai_fallback/test_router.py:166addstest_router_forwards_fingerprints_none_for_legacy_callers; caller omitsfingerprints, the spy capturesNone, and the cached proposal short-circuits the client.src/phase_z2_ai_fallback/step12.py,tests/phase_z2_ai_fallback/test_step12.py,src/phase_z2_ai_fallback/cache.py, orsrc/phase_z2_ai_fallback/signature.py.Remaining units: [u3, u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #3] Stage 3 code-edit — IMP-46 (#62) Axis R u3
Executing unit: u3
unit_executed
fingerprintsdict already built insrc/phase_z2_ai_fallback/step12.py(lines 179-185) into the existingroute_ai_fallbackcall at the AI-eligible path (lines 195-203). u1 widened the router signature; u3 connects the producer to the consumer so contract / partial / catalog SHA mismatches actually reach the cache invalidation gate end-to-end.files_changed
src/phase_z2_ai_fallback/step12.pydiff_summary
src/phase_z2_ai_fallback/step12.py:195-204— single-line additionfingerprints=fingerprints,inside the existingroute_ai_fallback(...)keyword-only call. Thefingerprintslocal was already constructed atstep12.py:179-185(contract_sha = _sha256_of(frame_contract),partial_sha = _sha256_of(figma_partial_json),catalog_sha) and stamped ontorecord["fingerprints"]atstep12.py:185. No producer logic touched; no record-side telemetry touched; no control-flow change.scope verification
allowed_editsfor u3 honoured:step12.py: forward record["fingerprints"] into route_ai_fallback call✓per_file_changesfor u3 honoured:step12.py:194-203 — pass fingerprints=record["fingerprints"] into existing route_ai_fallback call✓ (using the named localfingerprintswhich is the same object stamped torecord["fingerprints"]one line earlier at step12.py:185 — semantically identical, avoids the dict-subscript round-trip).src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/cache.py,src/phase_z2_ai_fallback/signature.py,tests/phase_z2_ai_fallback/test_router.py,tests/phase_z2_ai_fallback/test_step12.py,_persist_ai_repair_proposals_to_cache,--auto-cache,Settings.ai_fallback_auto_cache, AI request behaviour, catalog promotion, all #76-absorbed units.1186ad8cache carve-out: not stirred (memory:project_imp46_carveout_caveat).feedback_ai_isolation_contract).feedback_no_hardcoding): u3 wires the producer to the consumer; signature axes / SHA computation untouched.feedback_workflow_atomicity_rules): u3 = single line in step12.py only.scope-qualified verification
tests/phase_z2_ai_fallback/test_step12.py)python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q24 passed in 0.08sNoneupstream; existing step12 tests stay green because the cache layer treatsfingerprints=Noneas legacy no-comparison. Explicit forwarding-coverage lands in u4.tests/phase_z2_ai_fallback/test_router.py)python -m pytest tests/phase_z2_ai_fallback/test_router.py -q12 passed in 0.04s(u1 baseline 9 + u2's 3 fingerprint scenarios — all green post-u3).evidence
git diff -- src/phase_z2_ai_fallback/step12.pyconfirms the single-line addition at the existingroute_ai_fallbackkeyword-only call site.git diff --stat -- src/phase_z2_ai_fallback/ tests/phase_z2_ai_fallback/:src/phase_z2_ai_fallback/router.py | 8 ++-(u1 carry, unchanged this turn)src/phase_z2_ai_fallback/step12.py | 1 +(u3 this turn)tests/phase_z2_ai_fallback/test_router.py | 93 +++++++++++++++++++++++++++++--(u2 carry, unchanged this turn)src/phase_z2_ai_fallback/router.py:53showsfingerprints: dict | None = Nonealready present in the signature (u1).src/phase_z2_ai_fallback/router.py:72showsread_proposal(cache_key, fingerprints=fingerprints)already in place (u1).src/phase_z2_ai_fallback/step12.py:179-185shows the fingerprints producer (contract_sha/partial_sha/catalog_sha) is unchanged.src/phase_z2_ai_fallback/step12.py:203now carriesfingerprints=fingerprints,inside the existingroute_ai_fallbackcall (u3).remaining_units
route_ai_fallbackreceives exactlyrecord["fingerprints"]for an AI-eligible unit and skipped / non-AI records do not call the router (file:tests/phase_z2_ai_fallback/test_step12.py, estimate 25 lines).follow_up_issue_candidates
None this turn. No scope-lock-external axis observed during u3. Stage 2 already records
tests/conftest.pyAI_FALLBACK_ENABLED scrubber (former u5) and the 4.env-driven baseline reds as separate axes for follow-up.[Codex #3] Stage 3 code-edit review -- IMP-46 (#62) Axis R u3
Scope: verified only the single executed unit, u3.
Verdict: u3 is correct.
src/phase_z2_ai_fallback/step12.pynow forwards the existingfingerprintslocal into the existingroute_ai_fallback(...)call on the AI-eligible path. That local is the same dict assigned torecord["fingerprints"]immediately before the router call, so the Step 12 producer is now connected to the router read-path invalidation input without changing fingerprint computation, cache semantics, or AI request behavior.Scope lock checked: this unit's scoped production diff is the one-line Step 12 forwarding change. The already-present u1/u2 router and router-test changes remain in the combined diff, but u3 did not add cache, signature, save-side, auto-cache, or test-step12 changes.
=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_step12.py tests/phase_z2_ai_fallback/test_router.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyrg -n "fingerprints|route_ai_fallback\\(|read_proposal\\(" src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_step12.py tests/phase_z2_ai_fallback/test_router.pypython -m pytest tests/phase_z2_ai_fallback/test_step12.py -qFiles checked:
src/phase_z2_ai_fallback/step12.py:179-203src/phase_z2_ai_fallback/router.py:43-72tests/phase_z2_ai_fallback/test_step12.pytests/phase_z2_ai_fallback/test_router.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pyTest results:
python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q->24 passed in 0.08sVerified facts:
src/phase_z2_ai_fallback/step12.py:179-185constructsfingerprintsfromcontract_sha,partial_sha, andcatalog_sha, then assigns the same object torecord["fingerprints"].src/phase_z2_ai_fallback/step12.py:195-203callsroute_ai_fallback(...)and now passesfingerprints=fingerprints.src/phase_z2_ai_fallback/cache.py,src/phase_z2_ai_fallback/signature.py, ortests/phase_z2_ai_fallback/test_step12.pyfor this unit.src/phase_z2_ai_fallback/router.py:53andsrc/phase_z2_ai_fallback/router.py:72remain consistent with previously verified u1: optionalfingerprintskwarg is accepted and forwarded toread_proposal(cache_key, fingerprints=fingerprints).Remaining units: [u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #4] Stage 3 code-edit — IMP-46 (#62) Axis R u4
Executing unit: u4
unit_executed
route_ai_fallbackreceives exactlyrecord["fingerprints"]for an AI-eligible unit, and proving skipped / non-AI records do not call the router and keeprecord["fingerprints"]asNone. Locks the end-to-end producer→consumer wiring landed in u3 (step12.py:179-185 builds the fingerprints dict, stamps it ontorecord["fingerprints"]at step12.py:185, and forwards the same object into the router viafingerprints=at step12.py:203). u1+u2 covered router-level surface; u4 covers the Step 12 integration surface.files_changed
tests/phase_z2_ai_fallback/test_step12.pydiff_summary
tests/phase_z2_ai_fallback/test_step12.py— appended a new section headerIMP-46 u4 — Step 12 ↔ router fingerprints forwarding (integration scope)and 5 new test functions aftertest_production_non_provisional_reject_skipped_before_route_gate:test_router_receives_exactly_record_fingerprints_for_ai_eligible— strict equalityrouter.call_args.kwargs["fingerprints"] == recs[0]["fingerprints"]for an AI-eligible unit with caller-suppliedcontract/partial/catalog_value; also re-derives the expected SHA256 of contract + partial viahashlib.sha256(json.dumps(..., sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()and asserts the forwarded dict matches the canonical form.test_router_fingerprints_kwarg_is_present_even_with_default_catalog— asserts"fingerprints" in router.call_args.kwargs(not relying on the router's defaultNone) and that the 3 keys are present withcatalog_sha == ""sentinel when nocatalog_sha_loaderis supplied; proves step12.py:203 forwards explicitly rather than letting the router-side default apply.test_router_not_called_and_fingerprints_none_for_non_provisional— non-provisional unit →router.assert_not_called(),rec["fingerprints"] is None,rec["cache_key"] is None,rec["skip_reason"] == "not_provisional".test_router_not_called_and_fingerprints_none_for_non_ai_route—light_edit(non-AI route) →router.assert_not_called(),rec["fingerprints"] is None,rec["cache_key"] is None,rec["skip_reason"] == "route_not_ai_adaptation:deterministic_minor_adjustment".test_mixed_units_router_receives_fingerprints_only_for_ai_eligible— mixed batch of[not_provisional, light_edit, _ai_unit()]→router.assert_called_once(), forwarded fingerprints kwarg matchesrecs[2]["fingerprints"], and skipped records carryfingerprints is None.route_ai_fallbackforwarding atstep12.py:203(u3) andread_proposal(cache_key, fingerprints=fingerprints)atrouter.py:72(u1) remain unchanged.scope verification
per_file_changesfor u4 honoured:test_step12.py — add integration coverage asserting router receives record["fingerprints"] for AI-eligible, None for skipped/non-AI✓allowed_editshonoured: u4 lives entirely intests/phase_z2_ai_fallback/test_step12.py; production surface (router.py,step12.py,cache.py,signature.py) untouched this turn.tests/phase_z2_ai_fallback/test_step12.pyonly (+102 lines, 5 new test functions + 1 section header). Pre-existing deltas from earlier rounds (router.py+8 from u1,step12.py+1 from u3,test_router.py+93 from u1+u2) are NOT re-touched in u4.src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/step12.py,src/phase_z2_ai_fallback/cache.py,src/phase_z2_ai_fallback/signature.py,tests/phase_z2_ai_fallback/test_router.py,_persist_ai_repair_proposals_to_cache,--auto-cache,Settings.ai_fallback_auto_cache, AI request behaviour, catalog promotion, all #76-absorbed units.1186ad8cache carve-out: not stirred (memory:project_imp46_carveout_caveat).feedback_ai_isolation_contract).feedback_no_hardcoding): test fixtures are inline (contract = {"frame_id": "fid_123", ...},partial = {"deeper": [9,8,7], ...},catalog_value = "c0ffee00" * 8); no sample MDX or frame DB leakage. SHA256 expectations are re-derived from the same canonical JSON-sort form used by the producer (json.dumps(..., sort_keys=True, ensure_ascii=False).encode("utf-8")) rather than hardcoded hex literals.feedback_workflow_atomicity_rules): u4 = test_step12.py only.feedback_scope_qualified_verification): each new test docstring declares "Integration scope" explicitly.scope-qualified verification
tests/phase_z2_ai_fallback/test_step12.py)python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q29 passed in 0.10s(24 baseline + 5 new u4 tests).tests/phase_z2_ai_fallback/test_router.py)python -m pytest tests/phase_z2_ai_fallback/test_router.py -q12 passed in 0.04s(unchanged from u2 baseline — u4 did not touch router-level surface).remaining_units
All Stage 2 implementation_units are now executed:
follow_up_issue_candidates
None this turn. No scope-lock-external axis observed during u4. Stage 2 already records the two deferred follow-ups (
tests/conftest.pyAI_FALLBACK_ENABLED scrubber + 4.env-driven baseline-red root cause) as separate axes.[Codex #4] Stage 3 code-edit review -- IMP-46 (#62) Axis R u4
Scope: verified only the single executed unit, u4.
Verdict: u4 is correct. The Step 12 integration test file adds the required coverage proving that AI-eligible records forward the produced fingerprints dict into
route_ai_fallback, and that skipped/non-AI records do not call the router and keeprecord["fingerprints"]asNone. The test coverage is scoped totests/phase_z2_ai_fallback/test_step12.py; the combined working diff still includes the already-verified u1-u3 changes, but u4 itself adds only Step 12 integration tests.Scope lock checked: no u4 changes were made to production source, cache invalidation logic, signature generation, save-side cache behavior, auto-cache settings, AI request behavior, or catalog promotion. The production wiring from u3 remains present at
src/phase_z2_ai_fallback/step12.py:195-203.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- tests/phase_z2_ai_fallback/test_step12.py src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyrg -n "IMP-46|fingerprints|route_ai_fallback\\(|def test_" tests/phase_z2_ai_fallback/test_step12.py src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.pypython -m pytest tests/phase_z2_ai_fallback/test_step12.py -qtests/phase_z2_ai_fallback/test_step12.py:506-604src/phase_z2_ai_fallback/step12.py:179-204Files checked:
tests/phase_z2_ai_fallback/test_step12.py:506-604src/phase_z2_ai_fallback/step12.py:179-204src/phase_z2_ai_fallback/router.py:43-72tests/phase_z2_ai_fallback/test_router.py:109-187src/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pyTest results:
python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q->29 passed in 0.08sVerified facts:
tests/phase_z2_ai_fallback/test_step12.py:517-544addstest_router_receives_exactly_record_fingerprints_for_ai_eligible; it patchesstep12_mod.route_ai_fallback, capturesrouter.call_args.kwargs["fingerprints"], asserts equality withrecs[0]["fingerprints"], and re-derives the expected contract/partial SHA256 plus suppliedcatalog_sha.tests/phase_z2_ai_fallback/test_step12.py:547-559adds explicit coverage that thefingerprintskwarg is present even whencatalog_shadefaults to the empty-string sentinel.tests/phase_z2_ai_fallback/test_step12.py:562-585adds skipped-record coverage for non-provisional and non-AI-route records: router not called,ai_called is False,fingerprints is None, andcache_key is None.tests/phase_z2_ai_fallback/test_step12.py:588-604adds mixed-batch coverage proving only the AI-eligible unit calls the router and forwards fingerprints matchingrecs[2]["fingerprints"]; skipped records keepfingerprints is None.src/phase_z2_ai_fallback/step12.py:179-185still builds and stores the fingerprints dict before routing, andsrc/phase_z2_ai_fallback/step12.py:195-203still forwards it viafingerprints=fingerprints.src/phase_z2_ai_fallback/step12.py,src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/cache.py, orsrc/phase_z2_ai_fallback/signature.py; onlytests/phase_z2_ai_fallback/test_step12.pycontains the new u4 test block.Remaining units: none
FINAL_CONSENSUS: YES
[Claude #1] Stage 4 test-verify — IMP-46 (#62) Axis R u1~u4
📌 검증 결정: PASS
■ Targeted test runs (scope-qualified)
python -m pytest tests/phase_z2_ai_fallback/test_router.py -vpython -m pytest tests/phase_z2_ai_fallback/test_step12.py -v-vphase_z2_ai_fallback)python -m pytest tests/phase_z2_ai_fallback/ -q■ Diff vs Stage 2 plan — exact match
Production-line change count = 13 lines (router.py +6/-1, step12.py +1/-0) — within u1+u3 estimate (25+3).
router.py:43-66addfingerprintskwarg-only afterclient, forward toread_proposal(cache_key, fingerprints=fingerprints)key, **_(lines 89, 102, 184, 205, 218, 227)_forwards_fingerprints_and_misses_on_mismatch/_and_hits_on_match/_forwards_fingerprints_none_for_legacy_callers), each docstring declares "Scope: router-level (IMP-46 #62 Axis R u2)"step12.py:194-203forwardrecord["fingerprints"]intoroute_ai_fallback(...)fingerprints=fingerprints) — uses the same local dict already stamped ontorecord["fingerprints"]at step12.py:185record["fingerprints"]for AI-eligible; None for skipped/non-AIforwarded == recs[i]["fingerprints"]viarouter.call_args.kwargs["fingerprints"]■ Carve-out boundary verification (vs #76 commit
1186ad8)git diffagainst the protected territory — clean:src/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pysrc/phase_z2_ai_fallback/step12.py_persist_ai_repair_proposals_to_cachefingerprints=forward), save-side fingerprint persistence at 179-185 unchangedfeedback memory: project_imp46_carveout_caveathonored — no re-stirring of cache backend / signature axes / save gates / auto-cache / save-side fingerprint persistence.■ No-hardcoding audit
"aaa"/"bbb"/"ccc","xxx"/"yyy"/"zzz","c0ffee00" * 8— fixture stand-ins, not extracted from any MDX 03/04/05 sample_call(...)helper +FakeUnitfactory_sha256_of(frame_contract)recomputation, not sample-pinned valuesGenerality check (RULE 0 / PIPELINE-CONSTRUCTION): the wiring is sample-agnostic — any frame_contract / figma_partial_json the upstream supplies produces fingerprints that flow through identical strict-equality cache invalidation. No frame-specific carve-out.
■ Regression check
tests/phase_z2_ai_fallback/pass..env-drivenAI_FALLBACK_ENABLED=trueleak) — axis-orthogonal to Axis R (Stage 2 EXIT REPORTfollow_up_candidatesnoted them as separate axis; out-of-scope per Stage 1allowed_edits).tests/phase_z2_ai_fallback/test_router.pylegacy callers (lines 184, 205, 218, 227) continue to callroute_ai_fallback(**_call_kwargs(), client=client)with no fingerprints arg →read_proposalreceivesfingerprints=None→ cache layer skips fingerprint comparison (legacy behavior preserved per router.py:62-65 docstring).■ PZ rule application
_misses_on_mismatchtest). No silent fallback to stale value."Scope: router-level"or"Integration scope:"prefix.■ Decision
PASS — Stage 4 round #1.
All 4 implementation units (u1 + u2 + u3 + u4) match Stage 2 plan exactly. Production touch surface = 13 lines across 2 files (router.py + step12.py). Test coverage = 8 new tests (3 router-level + 5 integration) all green. Carve-out boundary intact (cache.py / signature.py / save-side path untouched). No hardcoding regressions. Adjacent tests / full module regression green.
Ready for Stage 5 (commit + push) pending Codex consensus + user GO.
=== EVIDENCE ===
4 files changed, 197 insertions(+), 7 deletions(-).src/phase_z2_ai_fallback/router.py:53addsfingerprints: dict | None = Nonekwarg-only.src/phase_z2_ai_fallback/router.py:72forwardsread_proposal(cache_key, fingerprints=fingerprints).src/phase_z2_ai_fallback/router.py:62-65docstring documents legacyNonesemantics.src/phase_z2_ai_fallback/step12.py:203addsfingerprints=fingerprints,to existingroute_ai_fallback(...)call.src/phase_z2_ai_fallback/step12.py:179-185(fingerprints producer) unchanged.tests/phase_z2_ai_fallback/test_router.py:109-187— 3 new u2 tests with router-level scope docstrings.tests/phase_z2_ai_fallback/test_router.py:89, 102, 184, 205, 218, 227— 6 lambdas patchedkey→key, **_(carry-fix per Stage 2 plan).tests/phase_z2_ai_fallback/test_step12.py:506-604— 5 new u4 integration tests with integration-scope docstrings.git diff src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py→ empty output.python -m pytest tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py -v→ 41 passed in 0.10s.python -m pytest tests/phase_z2_ai_fallback/ -q→ 229 passed in 0.44s.rg "mdx 03|hardcod|sample-specific"in both touched test files → no new sample-specific paths.f3ef4d917c775d497fbed8109042f46635e66f1a(f3ef4d9 feat(#64): IMP-35 ...); baseline absorbed =1186ad8(#76 IMP-47B).FINAL_CONSENSUS: YES
[Codex #1] Stage 4 test-verify -- IMP-46 (#62) Axis R u1-u4
Verification decision: PASS. Commit OK signal for the scoped IMP-46 Axis R files.
Scope-qualified test results:
python -m pytest tests/phase_z2_ai_fallback/test_router.py -v-> 12 passed.python -m pytest tests/phase_z2_ai_fallback/test_step12.py -v-> 29 passed.python -m pytest tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py -v-> 41 passed.python -m pytest tests/phase_z2_ai_fallback/ -q-> 229 passed.Scoped diff verification:
git diff --stat -- src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py-> 4 files changed, 197 insertions(+), 7 deletions(-)..claude/settings.json; these are outside commit scope and were not evaluated as IMP-46 changes.Verified implementation facts:
src/phase_z2_ai_fallback/router.py:53adds optionalfingerprints: dict | None = None.src/phase_z2_ai_fallback/router.py:72forwardsread_proposal(cache_key, fingerprints=fingerprints).src/phase_z2_ai_fallback/step12.py:203forwards the already-built localfingerprintsdict intoroute_ai_fallback(...).tests/phase_z2_ai_fallback/test_router.py:109-187contains the three router-level u2 scenarios: mismatch miss, matching hit, and legacyfingerprints=None.tests/phase_z2_ai_fallback/test_step12.py:517-604contains the five u4 integration tests proving AI-eligible forwarding and skipped/non-AI no-router behavior.git diff -- src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyis empty; protected cache backend/signature territory remains untouched.git diff -- src/phase_z2_ai_fallback/step12.pyshows only thefingerprints=fingerprintsforward in production Step 12; save-side cache persistence was not changed.=== EVIDENCE ===
Commands run:
git status --shortgit diff --statgit diff -- src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.pyrg -n "fingerprints|route_ai_fallback\(|read_proposal\(|IMP-46|def test_" src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.pypython -m pytest tests/phase_z2_ai_fallback/test_router.py -vpython -m pytest tests/phase_z2_ai_fallback/test_step12.py -vpython -m pytest tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py -vpython -m pytest tests/phase_z2_ai_fallback/ -qgit diff --stat -- src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.pygit diff -- src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pygit diff -- src/phase_z2_ai_fallback/step12.py | Select-String -Pattern '_persist_ai_repair_proposals_to_cache|fingerprints=fingerprints|record\["fingerprints"\]' -Context 3,3Files checked:
src/phase_z2_ai_fallback/router.pysrc/phase_z2_ai_fallback/step12.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pytests/phase_z2_ai_fallback/test_router.pytests/phase_z2_ai_fallback/test_step12.pyTest results:
tests/phase_z2_ai_fallback/test_router.py: 12 passed.tests/phase_z2_ai_fallback/test_step12.py: 29 passed.tests/phase_z2_ai_fallback/: 229 passed.FINAL_CONSENSUS: YES
[Claude #1] Stage 5 commit-push — IMP-46 (#62) Axis R u1~u4
📌 Stage 5: 커밋 및 푸쉬 완료
■ 핵심 결정
d9d3384작성, path-scoped staging 으로 4 파일만 포함.origin(GitHub) +slide2(Gitea mirror) 양측 push 성공. 두 remote 모두d9d3384정합.git add -A사용 금지 준수..claude/settings.json외 unrelated untracked 모두 unstaged 유지.cache.py,signature.py) diff = empty. 침범 없음.■ 범위 제외
.claude/settings.json,tests/matching/,Front/,samples/, untrackedtmp*.json등) 전부 unstaged.cache.py/signature.py변경 없음.■ 다음 단계
=== EXIT REPORT (English, binding contract) ===
=== BASIS ===
Stage 4 PASS exit report defined exact staging surface (4 files) + commit message. Stage 5 executed: path-scoped
git add, single commitd9d3384matching plan stat 1:1 (197+/7-, 13 production lines),git push origin main+git push slide2 mainboth succeeded withf3ef4d9..d9d3384. Post-push fetch confirmed both remotes synced tod9d3384. Carve-out (cache.py/signature.py) diff empty. No regression to staging policy or memory guardrails.[Codex #1] Stage 5 commit-push verification -- IMP-46 (#62)
Stage 5 verification result: PASS.
I verified the commit and both pushed remotes from git state, without reading Gitea comments.
Commit verified:
d9d338416af3ef4d917cCommit scope verified:
Remote sync verified after fetch:
d9d338416ad9d338416ad9d338416aUnintended-file check:
d9d3384.Remote target check:
=== EVIDENCE ===
Commands run:
d9d338416ad9d338416ad9d338416ad9d338416ad9d338416a-- src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pyFiles checked:
Commit SHA:
d9d338416aVerified facts:
d9d338416ais local HEAD.d9d338416aafter fetch.d9d3384contains only the four scoped files.FINAL_CONSENSUS: YES
[Claude #1] Stage 6 final-close — IMP-46 (#62)
📌 Issue re-read vs commit on origin
1186ad8data/frame_cache/{frame_id}/{signature_hash}.jsonpath format--auto-cachecache.py:108-148); router read-path enforcement gap = residual Axis RStage 1 carved the residual into Axis R (router read-path fingerprint forwarding). Stages 2–5 executed u1~u4 against that carve-out only — IMP-47B (#76)
cache.py/signature.pyterritory untouched.📌 Commit verification
Diff stat: 4 files changed, 197 insertions(+), 7 deletions(-)
Carve-out diff in
d9d3384forsrc/phase_z2_ai_fallback/cache.pyandsrc/phase_z2_ai_fallback/signature.py= empty. IMP-47B (#76)1186ad8territory untouched.📌 Goal vs result
route_ai_fallback) 가fingerprints인자를 받지 않아 step12 producer 가 만든 SHA 3종이 read-path 까지 도달하지 못함 → stale entry 위험.fingerprints: dict | None = Nonekwarg 을 노출하고read_proposal(cache_key, fingerprints=fingerprints)로 그대로 흘려보냄. step12 는 이미 보유한record["fingerprints"]를 동일 객체로 전달. router-level 3 시나리오 (mismatch-miss / match-hit / legacy-None) + step12 integration 5 시나리오 (AI-eligible forwarding, default-catalog kwarg presence, non-provisional no-router, skipped/non-AI 변형) 로 invariant lock.📌 Scope-qualified verification
Out-of-scope (declared):
.env-driven baseline reds (4 건) — axis-orthogonal, 별도 evidence-based 이슈 후보.tests/conftest.pyAI_FALLBACK_ENABLEDscrubber (구 u5) — Stage 1 allowed_edits 밖, separate axis.📌 Follow-up links
.env-driven 4 baseline-red root causetests/conftest.pyAI_FALLBACK_ENABLED scrubber📌 Labels / Close decision
IMP-46,R3,wave-1,done(또는 프로젝트 컨벤션 매핑).Close decision: CLOSE (resolved)
Rationale:
d9d3384exists on both origin (GitHub) and slide2 (Gitea mirror), heads identical.1186ad8cache territory) untouched.=== EVIDENCE ===
git rev-parse HEAD origin/main slide2/main→ alld9d338416ab277f55deaf8496f827cd17260cb87git show --stat d9d3384→ 4 files, 197+/7-, matches Stage 4/5 plangit diff d9d3384^ d9d3384 -- src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py→ empty (carve-out untouched)python -m pytest tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py -q→ 41 passed in 0.09sFINAL_CONSENSUS: YES
[Codex #1] Stage 6 final-close verify -- IMP-46 (#62)
Verdict: PASS. I independently verified the pushed commit, scoped diff, protected carve-out, and current router/step12 regression surface for the residual Axis R fix.
Issue reread vs implemented scope:
1186ad8, per the binding exit reports.step12.pyproduced contract/partial/catalog fingerprints, butroute_ai_fallbackdid not forward them intoread_proposal.d9d3384addresses that residual scope only: router accepts optionalfingerprints, forwards it to cache read, and Step 12 passes the record fingerprints on AI-eligible routes.Verified commit state:
HEAD,origin/main, andslide2/mainall resolve tod9d338416ab277f55deaf8496f827cd17260cb87.feat(#62): IMP-46 cache fingerprint forwarding u1~u4 (router kwarg + step12 forward + 8 scenarios).src/phase_z2_ai_fallback/router.py,src/phase_z2_ai_fallback/step12.py,tests/phase_z2_ai_fallback/test_router.py, andtests/phase_z2_ai_fallback/test_step12.py.4 files changed, 197 insertions(+), 7 deletions(-).git diff --numstatreportsrouter.py +7/-1,step12.py +1/-0,test_router.py +87/-6,test_step12.py +102/-0. The total and four-file scope remain as reported.src/phase_z2_ai_fallback/cache.pyandsrc/phase_z2_ai_fallback/signature.pyhave no diff ind9d3384, so the IMP-47B carve-out remains untouched.git status --shortare not in the commit.Verified behavior:
src/phase_z2_ai_fallback/router.py:53definesfingerprints: dict | None = None.src/phase_z2_ai_fallback/router.py:72callsread_proposal(cache_key, fingerprints=fingerprints).src/phase_z2_ai_fallback/step12.py:179-185builds the fingerprint dict and stores it onrecord["fingerprints"].src/phase_z2_ai_fallback/step12.py:195-203forwardsfingerprints=fingerprintsintoroute_ai_fallback.Nonebehavior attests/phase_z2_ai_fallback/test_router.py:109-187.tests/phase_z2_ai_fallback/test_step12.py:517-604.41 passed in 0.09s.=== EVIDENCE ===
Commands run:
git status --shortgit remote -vgit show --no-renames --format=fuller --stat --name-status d9d3384git diff --name-only d9d3384^ d9d3384git fetch origin maingit fetch slide2 maingit rev-parse HEAD origin/main slide2/maingit diff --name-only d9d3384^ d9d3384 -- src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.pygit diff --shortstat d9d3384^ d9d3384git diff --numstat d9d3384^ d9d3384 -- src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.pyrg -n "fingerprints|read_proposal|route_ai_fallback\(" src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/step12.pyrg -n "IMP-46|fingerprints|mismatch|matching|legacy|router" tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.pypython -m pytest tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_step12.py -qFiles checked:
src/phase_z2_ai_fallback/router.pysrc/phase_z2_ai_fallback/step12.pysrc/phase_z2_ai_fallback/cache.pysrc/phase_z2_ai_fallback/signature.pytests/phase_z2_ai_fallback/test_router.pytests/phase_z2_ai_fallback/test_step12.pyCommit SHA:
d9d338416ab277f55deaf8496f827cd17260cb87Verified facts:
HEAD == origin/main == slide2/main == d9d338416ab277f55deaf8496f827cd17260cb87.cache.pyorsignature.pydiff exists in the commit.fingerprints=None.41 passed in 0.09s.FINAL_CONSENSUS: YES
Kyeongmin referenced this issue2026-05-24 03:04:15 +09:00