source axis = #62 (frame transformation cache layer — content-addressed cache; lookup before AI call + save after visual_check PASS + 사용자 OK or --auto-cache).
pair = #61 IMP-33 (closed at 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.
depend = IMP-33 AI hook (now landed); declared consumer = every route_ai_fallback call site (currently only gather_step12_ai_repair_proposals).
raiseNotImplementedError("IMP-46 persistent cache storage is not implemented yet; ""this is the IMP-33 u6 stub marker.")
read_proposal returns None for any key (cache.py:36-45); save_proposal enforces both gates then raises NotImplementedError (cache.py:48-82). IMP-46 = replace the marker with a real backend (read + write JSON under data/frame_cache/{frame_id}/{signature_hash}.json per 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)
source_section_ids is the MDX section identifier (e.g., "02.mdx::sec-0"). This means:
a fresh MDX with the same structural shape → guaranteed cache MISS;
two MDX with different identifiers but identical signature → never hit;
adding/renaming a sample → orphans the cache entry.
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.
top / bottom_l / bottom_r — zone topology position
yes
None 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:
source
path
scope
frame contract
templates/phase_z2/catalog/frame_contracts.yaml (loaded by src/phase_z2_mapper.py:49 load_frame_contracts with _CATALOG_CACHE global)
per-template subtree
partial template (family)
templates/phase_z2/families/*.html (12 files at HEAD)
per-template file
partial template (frame)
templates/phase_z2/frames/*.html (2 files at HEAD: process_product_two_way.html, three_parallel_requirements.html)
per-template file
catalog update
structural changes to frame_contracts.yaml (added sub_zones, accepted_content_types, etc.)
global if structural
Two strategies under consideration (Stage 2 lock target):
(I1) per-entry embedded fingerprint — each cache entry stores SHA-256 of: (a) its template subtree of frame_contracts.yaml, (b) the family/frame partial HTML it references. On read, recompute and compare; mismatch → miss + log; never serve stale.
(I2) global manifest version — single counter bumped on any catalog/partial change. Simpler but coarse (one frame change invalidates everything).
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):
issue spec field
u2 ProposalKind
reusability across samples
builder_options
BUILDER_OPTIONS_PATCH
high — structural (parser switch, knob value)
partial_overrides
PARTIAL_OVERRIDES
ambiguous — payload slots may contain text content
slot mapping
SLOT_MAPPING_PROPOSAL
high — content-unit → slot decision (no raw text)
slide-level CSS
NOT in IMP-33 u2 whitelist
out-of-scope under FORBIDDEN_KINDS (raw_css) — schema.py:28-30
The slide-level CSS branch is forbidden by IMP-33 schema (raw_css in FORBIDDEN_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_OVERRIDES payload may contain text. Two interpretations:
(D1) verbatim cache — store the AI proposal as-is (text included). Then signature MUST include a content fingerprint of the MDX text to avoid wrong-text false hits. This collapses cache hits to "same MDX" → no cross-sample generalization → cache hit rate ≈ re-render same file.
(D2) structural cache — store only structural decisions (builder_options + slot-mapping structure). Text content NOT cached; on hit, AI is not re-called, but text re-flows through the deterministic pipeline using the cached structural decisions. Cross-sample generalization works.
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
axis
today (HEAD c864fe0)
after IMP-46
read_proposal(key)
returns None for any key (cache.py:42-45)
reads data/frame_cache/{frame_id}/{signature_hash}.json if present + fingerprint valid; else None
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)
{"schema_version":1,"signature":"<16-char hex>","signature_axes":{"frame_id":"1171281190","v4_label":"light_edit","cardinality":3,"source_shape":"top_bullets","h3_count":3,"char_count_bucket":"100-300","layout_preset":"two-column","zone_position":"top"},"fingerprints":{"frame_contract_subtree_sha256":"...","partial_template_sha256":"..."// null if no partial referenced
},"proposal":{"proposal_kind":"...","payload":{...},"rationale":"..."},"created_at_utc":"2026-05-21T07:23:11Z","visual_check_passed":true,"user_approved":true,"auto_cache_flag":false}
Stored at data/frame_cache/{frame_id}/{signature}.json. On read: load → verify fingerprints.frame_contract_subtree_sha256 matches current catalog subtree → verify fingerprints.partial_template_sha256 matches current partial template content → if mismatch, return None and log invalidation reason. Pydantic schema validation on parse.
(d) Save trigger flow
Currently no site calls save_proposal. IMP-46 introduces the producer:
pipeline render → visual_check (existing, src/phase_z2_classifier.py:495)
→ user approval gate (new; settings.ai_fallback_auto_cache OR explicit slide_status.user_approved)
→ if both True AND proposal exists: save_proposal(signature, proposal, ...)
Save site = a new orchestration helper called from the pipeline AFTER slide_status.visual_check_passed and AFTER the (existing) user-OK signal. Stage 2 must lock the exact wiring point. Candidate site: end of phase_z2_pipeline.run_pipeline post-visual-check loop.
(e) --auto-cache flag
Issue spec mentions --auto-cache as user_approved bypass. Plumbing: new src/config.py Settings field ai_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.
signature MUST be structural only — NO source_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 signature
feedback_no_hardcoding, issue spec "signature hash 가 sample-specific case 만들지 않게"
G3
cache hit = deterministic — same signature MUST return byte-identical proposal across runs (test: read same entry twice + json.loads().model_dump() == ...)
issue spec "cache hit 결과 = 결정론적"
G4
save gate = visual_check_passed AND (user_approved OR auto_cache_flag). Existing AiFallbackCacheGateError semantics preserved
cache.py:69-78, issue spec
G5
invalidation on partial / contract change — read-time fingerprint comparison; on mismatch, return None and log reason. No serving stale entries
issue spec "contract/partial 변경 시 invalidate"
G6
slide-level CSS out of cache value — raw_css is in IMP-33 u2 FORBIDDEN_KINDS (schema.py:28-30); IMP-46 does NOT expand the whitelist
feedback_ai_isolation_contract, IMP-17 carve-out
G7
AST isolation preserved — IMP-46 cache backend imports ONLY stdlib (json, hashlib, pathlib) + IMP-33 u2 schema. NO Phase Q / Kei / pipeline runtime imports. Verified via test_ast_isolation.py rerun
IMP-33 u10 contract
G8
data/frame_cache/ MUST be gitignored or under explicit .gitignore rule (cache is runtime artifact, not source-of-truth). Catalog promotion (cache → committed) deferred to R4
issue spec "cache → catalog promote = R4"
G9
no-hardcoding — bucket boundaries, signature axis list, fingerprint sources ALL in src/config.py or catalog. No sample-specific (mdx 03/04/05) branches in cache module
RULE 0, feedback_no_hardcoding
G10
u8 step12 cache_key build site MUST be updated to call the new build_cache_signature helper. Leaving template_id::source_section_ids would render IMP-46 inert
Axis B above
G11
RULE 0 — signature axes evaluated against ALL 32 frames + ALL aligned MDX shapes. No frame-specific branching in signature builder
RULE 0 PIPELINE-CONSTRUCTION
G12
auto_cache_flag default False at HEAD merge. CI default preserves zero-write behavior
feedback_auto_pipeline_first — no review_required injection between AI call and cache save. Save gate decision is auto (visual_check + user_approved/auto_cache); failure paths emit clear reason strings, not queues
feedback_auto_pipeline_first
G15
post-IMP-46 backlog row in PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md added with status close + reference to commit. Currently stale (IMP-33/IMP-46 rows missing — already flagged in #61 Stage 1)
anchor sync
5. Implementation slicing sketch (Stage 2 input — NOT binding)
Tests: determinism (same input → same hash), axis sensitivity (each axis change → different hash), cross-sample collision (2 different source_section_ids with same structural axes → same hash).
Update src/phase_z2_ai_fallback/cache.py — read_proposal reads JSON from data/frame_cache/{frame_id}/{signature}.json; save_proposal writes JSON (still gated by the existing visual_check_passed AND user_approved conditions).
Tests: round-trip read/write, invalidation on fingerprint mismatch, gate enforcement preserved (existing 6 gate tests must still pass).
U5 — Step12 integration
src/phase_z2_ai_fallback/step12.py:109-111 — replace sample-specific cache_key with build_cache_signature(...) using V4 result + frame contract + unit shape.
Tests: 2 samples × same shape × different identifiers → same signature → cache hit on 2nd run.
U6 — Save site
New helper apply_fallback_save_decision(slide_status, proposal, ...) -> None in src/phase_z2_ai_fallback/save_site.py (or pipeline-side module).
Called from pipeline after visual_check + user_approval.
Honors --auto-cache flag via config.
Tests: visual_check False → no write; user_approved False AND auto_cache False → no write; both True (or auto_cache True) → JSON written at canonical path.
U7 — .gitignore + invalidation log
Add data/frame_cache/ to .gitignore.
Optional: emit invalidation reason to debug.json (cache_invalidation_log additive field).
U8 — Tests + AST isolation rerun
Full tests/phase_z2_ai_fallback/ rerun (existing 116 tests must pass).
New tests under tests/phase_z2_ai_fallback/test_cache_persistent.py.
AST isolation guard rerun against new modules.
U9 — Docs sync
cache.py module docstring update.
IMP-17-CARVE-OUT.md:54 IMP-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.md row add.
6. Open questions (Codex round 1차 review)
Q1 — Cache value semantics: D1 (verbatim, text included) vs D2 (structural only, text re-flowed)? Claude #1 preference = D2 (cross-sample generalization, no hidden text leak). Codex view?
Q2 — Invalidation strategy: I1 (per-entry embedded fingerprint) vs I2 (global manifest counter)? Claude #1 preference = I1 (surgical, audit-clear).
Q3 — --auto-cache flag plumbing: settings (env / .env) vs CLI argument vs both? Claude #1 strawman = settings only (env-overridable). CLI-only adds new arg-parser surface.
Q4 — Signature axis list completeness: are 8 axes (frame_id / v4_label / cardinality / source_shape / h3_count / char_count_bucket / layout_preset / zone_position) sufficient? Should internal_region_id be added (Layer A SPEC v1 surface)? Claude #1 view: leave for v2; today Internal Region is trace-only.
Q5 — Save site placement: end of pipeline run_pipeline post-visual-check vs separate explicit save_fallback_proposals(...) helper invoked by caller? Claude #1 preference = explicit helper (clearer audit + composability with --auto-cache).
Q6 — Char count bucket boundaries: (100, 300, 700) vs other? Stage 2 lock target. Claude #1 strawman based on typical Korean text density at 16px / 600px-wide zones.
Q7 — Hash truncation: 16-char SHA-256 hex (default) vs longer? Collision probability at 16 chars ≈ 2^-64; for 1k frames × 10 v4_labels × 8 cardinalities ≈ 80k entries → negligible. Claude #1 preference = 16.
Q8 — Should IMP-46 also fix the symbolic cache_key parameter type at route_ai_fallback(cache_key: str, ...) in src/phase_z2_ai_fallback/router.py:45? Today the router treats it as opaque string; with structural signature, type stays str (no shape change). Claude #1 view: no router-side change required; signature build happens caller-side (step12) and passes the result.
Q9 — Pair atomicity: IMP-46 in single PR vs split (signature first, backend second)? Claude #1 preference = single PR with u1~u9 (atomic; gates preserved).
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/ = no frame_cache/ directory at HEAD (confirmed via ls 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.
Pair issue #62 body (current task input) — signature axes + value scope + invalidation rules verified.
Gitea API:
#61 (IMP-33) closed; #62 (IMP-46) open — current Stage 1.
.orchestrator/drafts/61_stage_problem-review_claude_r1.md re-read as reference (paired axis context, especially Axis E G11 about cache visual_check + 사용자 OK gating).
No prior .orchestrator/drafts/62_* — confirmed first Stage 1 round.
No code change made — Stage 1 problem-review only.
rewind_target: 해당 없음 (YES 제안)
FINAL_CONSENSUS: YES
[Claude #1] Stage 1 problem-review — IMP-46
## 1. Source re-read + pair context
- source axis = #62 (frame transformation cache layer — content-addressed cache; lookup before AI call + save after `visual_check PASS + 사용자 OK` or `--auto-cache`).
- pair = #61 IMP-33 (closed at `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.
- depend = IMP-33 AI hook (now landed); declared consumer = every `route_ai_fallback` call site (currently only `gather_step12_ai_repair_proposals`).
- out-of-scope (explicit) = AI call itself (IMP-33), cache → catalog promotion (R4).
- roadmap axis = R3 (AI 보정/재구성 보조), wave 1 (실질 구동 필수).
## 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):
```python
raise NotImplementedError(
"IMP-46 persistent cache storage is not implemented yet; "
"this is the IMP-33 u6 stub marker."
)
```
`read_proposal` returns `None` for any key (`cache.py:36-45`); `save_proposal` enforces both gates then raises NotImplementedError (`cache.py:48-82`). IMP-46 = replace the marker with a real backend (read + write JSON under `data/frame_cache/{frame_id}/{signature_hash}.json` per 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):
```python
cache_key = "::".join(
[template_id, ",".join(sorted(record["source_section_ids"]))]
)
```
`source_section_ids` is the MDX section identifier (e.g., `"02.mdx::sec-0"`). This means:
- a fresh MDX with the same structural shape → guaranteed cache MISS;
- two MDX with different identifiers but identical signature → never hit;
- adding/renaming a sample → orphans the cache entry.
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):
| signature axis | source at HEAD | structural? |
|---|---|---|
| `frame_id` | `frame_contracts.yaml` per-template `frame_id:` (e.g. `1171281190`) — `templates/phase_z2/catalog/frame_contracts.yaml:23` | yes |
| `v4_label` | V4 result `label` (`light_edit` / `restructure`) — passed as `record["label"]` | yes |
| `cardinality` | frame contract `cardinality.strict` OR V4 result `cardinality_signature` — `frame_contracts.yaml:27-29` | yes |
| `source_shape` | `frame_contract.source_shape` (`top_bullets` / `paragraph` / `table`) — `frame_contracts.yaml:26` | yes |
| `h3_count` | source MDX subsection count (derivable from `unit.raw_content`) | yes |
| `char_count_bucket` | discretized text length (e.g., `<100`, `100-300`, `300-700`, `700+`) | yes |
| `layout_preset` | `sidebar-right` / `two-column` / `hero-detail` / `single-column` — Phase Z layout choice | yes |
| `zone_position` | `top` / `bottom_l` / `bottom_r` — zone topology position | yes |
None 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:
| source | path | scope |
|---|---|---|
| frame contract | `templates/phase_z2/catalog/frame_contracts.yaml` (loaded by `src/phase_z2_mapper.py:49 load_frame_contracts` with `_CATALOG_CACHE` global) | per-template subtree |
| partial template (family) | `templates/phase_z2/families/*.html` (12 files at HEAD) | per-template file |
| partial template (frame) | `templates/phase_z2/frames/*.html` (2 files at HEAD: `process_product_two_way.html`, `three_parallel_requirements.html`) | per-template file |
| catalog update | structural changes to `frame_contracts.yaml` (added `sub_zones`, `accepted_content_types`, etc.) | global if structural |
Two strategies under consideration (Stage 2 lock target):
- **(I1) per-entry embedded fingerprint** — each cache entry stores SHA-256 of: (a) its template subtree of `frame_contracts.yaml`, (b) the family/frame partial HTML it references. On read, recompute and compare; mismatch → miss + log; never serve stale.
- **(I2) global manifest version** — single counter bumped on any catalog/partial change. Simpler but coarse (one frame change invalidates everything).
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`):
| issue spec field | u2 ProposalKind | reusability across samples |
|---|---|---|
| `builder_options` | `BUILDER_OPTIONS_PATCH` | high — structural (parser switch, knob value) |
| `partial_overrides` | `PARTIAL_OVERRIDES` | **ambiguous** — payload `slots` may contain text content |
| `slot mapping` | `SLOT_MAPPING_PROPOSAL` | high — content-unit → slot decision (no raw text) |
| `slide-level CSS` | NOT in IMP-33 u2 whitelist | **out-of-scope under FORBIDDEN_KINDS (`raw_css`)** — `schema.py:28-30` |
**The slide-level CSS branch is forbidden by IMP-33 schema** (`raw_css` in `FORBIDDEN_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_OVERRIDES` payload may contain text. Two interpretations:
- **(D1) verbatim cache** — store the AI proposal as-is (text included). Then signature MUST include a content fingerprint of the MDX text to avoid wrong-text false hits. This collapses cache hits to "same MDX" → no cross-sample generalization → cache hit rate ≈ re-render same file.
- **(D2) structural cache** — store only structural decisions (builder_options + slot-mapping structure). Text content NOT cached; on hit, AI is not re-called, but text re-flows through the deterministic pipeline using the cached structural decisions. Cross-sample generalization works.
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
| axis | today (HEAD `c864fe0`) | after IMP-46 |
|---|---|---|
| `read_proposal(key)` | returns `None` for any key (`cache.py:42-45`) | reads `data/frame_cache/{frame_id}/{signature_hash}.json` if present + fingerprint valid; else `None` |
| `save_proposal(key, proposal, *, visual_check_passed, user_approved)` | both gates → `NotImplementedError`; either gate False → `AiFallbackCacheGateError` | both gates True → JSON write at canonical path; gate semantics **unchanged** |
| step12 cache key | `template_id::source_section_ids` (sample-specific) | structural signature hash (frame_id + v4_label + cardinality + source_shape + h3_count + char_bucket + layout_preset + zone_position) |
| AiFallbackProposal schema (u2) | 3 kinds (`BUILDER_OPTIONS_PATCH` / `PARTIAL_OVERRIDES` / `SLOT_MAPPING_PROPOSAL`); FORBIDDEN_KINDS includes `raw_css` | **unchanged** — IMP-46 does NOT expand the whitelist. Slide-level CSS dropped from cache value |
| `route_ai_fallback` flow (u7) | flag-off OR route-mismatch → None; else cache_read → prompt → client → validate | **unchanged** call shape. Cache read step now backed by real storage; cache hit → return validated proposal without API call |
| normal-path AI call count | 0 (PZ-1 lock) | **0 (locked)**. Cache only activates inside fallback path; flag default still OFF |
| user_approved signal source | placeholder kwarg in `save_proposal` (no producer yet) | new producer: pipeline `slide_status.user_approved` field OR `--auto-cache` CLI flag (single-shot per run) → propagated to caller |
| `data/frame_cache/` directory | does NOT exist | created on first write; per-frame subdirectory (`{frame_id}/`) |
| AST isolation guard | `tests/phase_z2_ai_fallback/test_ast_isolation.py` (IMP-33 u10) — package may NOT import Phase Q / Kei / pipeline runtime symbols | **unchanged** — IMP-46 cache backend imports nothing new from Phase Q / Kei; only stdlib (`json`, `hashlib`, `pathlib`) |
| existing IMP-33 tests | 9 tests in `tests/phase_z2_ai_fallback/test_cache.py` (gate enforcement, NotImplementedError marker) | gate tests **preserved verbatim**; NotImplementedError test deleted and replaced with persistent-storage tests (signature determinism, hit/miss, invalidation, write-path) |
### (b) Signature (the key novel surface)
```python
def build_cache_signature(
*,
frame_id: str, # frame_contract.frame_id (str)
v4_label: str, # "light_edit" | "restructure"
cardinality: int,
source_shape: str, # "top_bullets" | "paragraph" | "table"
h3_count: int,
char_count_bucket: str, # discretized: "<100" | "100-300" | "300-700" | "700+"
layout_preset: str, # "sidebar-right" | "two-column" | "hero-detail" | "single-column"
zone_position: str, # "top" | "bottom_l" | "bottom_r"
) -> str:
"""Return short SHA-256 hex digest (16 chars) of the canonical-form tuple."""
```
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)
```jsonc
{
"schema_version": 1,
"signature": "<16-char hex>",
"signature_axes": {
"frame_id": "1171281190",
"v4_label": "light_edit",
"cardinality": 3,
"source_shape": "top_bullets",
"h3_count": 3,
"char_count_bucket": "100-300",
"layout_preset": "two-column",
"zone_position": "top"
},
"fingerprints": {
"frame_contract_subtree_sha256": "...",
"partial_template_sha256": "..." // null if no partial referenced
},
"proposal": { "proposal_kind": "...", "payload": { ... }, "rationale": "..." },
"created_at_utc": "2026-05-21T07:23:11Z",
"visual_check_passed": true,
"user_approved": true,
"auto_cache_flag": false
}
```
Stored at `data/frame_cache/{frame_id}/{signature}.json`. On read: load → verify `fingerprints.frame_contract_subtree_sha256` matches current catalog subtree → verify `fingerprints.partial_template_sha256` matches current partial template content → if mismatch, return `None` and log invalidation reason. Pydantic schema validation on parse.
### (d) Save trigger flow
Currently no site calls `save_proposal`. IMP-46 introduces the producer:
```
pipeline render → visual_check (existing, src/phase_z2_classifier.py:495)
→ user approval gate (new; settings.ai_fallback_auto_cache OR explicit slide_status.user_approved)
→ if both True AND proposal exists: save_proposal(signature, proposal, ...)
```
Save site = a new orchestration helper called from the pipeline AFTER `slide_status.visual_check_passed` and AFTER the (existing) user-OK signal. Stage 2 must lock the exact wiring point. Candidate site: end of `phase_z2_pipeline.run_pipeline` post-visual-check loop.
### (e) `--auto-cache` flag
Issue spec mentions `--auto-cache` as user_approved bypass. Plumbing: new `src/config.py` Settings field `ai_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)
- **on-read** (mandatory) — fingerprint comparison; mismatch → miss.
- **on-write** (mandatory) — embed current fingerprints in entry.
- **on-catalog-change** (deferred to R4) — explicit purge command; out-of-scope for IMP-46.
## 4. Guardrails (Stage 2 binding)
| # | guardrail | source |
|---|---|---|
| G1 | normal-path AI call count = **0**. Cache only operates inside fallback path; cache miss + flag-OFF route = no AI call | `feedback_ai_isolation_contract`, PZ-1, `IMP-17-CARVE-OUT.md` |
| G2 | signature MUST be structural only — NO `source_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 signature | `feedback_no_hardcoding`, issue spec "signature hash 가 sample-specific case 만들지 않게" |
| G3 | cache hit = deterministic — same signature MUST return byte-identical proposal across runs (test: read same entry twice + `json.loads().model_dump() == ...`) | issue spec "cache hit 결과 = 결정론적" |
| G4 | save gate = visual_check_passed AND (user_approved OR auto_cache_flag). Existing `AiFallbackCacheGateError` semantics **preserved** | `cache.py:69-78`, issue spec |
| G5 | invalidation on partial / contract change — read-time fingerprint comparison; on mismatch, return None and log reason. No serving stale entries | issue spec "contract/partial 변경 시 invalidate" |
| G6 | slide-level CSS **out** of cache value — `raw_css` is in IMP-33 u2 `FORBIDDEN_KINDS` (`schema.py:28-30`); IMP-46 does NOT expand the whitelist | `feedback_ai_isolation_contract`, IMP-17 carve-out |
| G7 | AST isolation **preserved** — IMP-46 cache backend imports ONLY stdlib (`json`, `hashlib`, `pathlib`) + IMP-33 u2 schema. NO Phase Q / Kei / pipeline runtime imports. Verified via `test_ast_isolation.py` rerun | IMP-33 u10 contract |
| G8 | `data/frame_cache/` MUST be gitignored or under explicit `.gitignore` rule (cache is runtime artifact, not source-of-truth). Catalog promotion (cache → committed) deferred to R4 | issue spec "cache → catalog promote = R4" |
| G9 | no-hardcoding — bucket boundaries, signature axis list, fingerprint sources ALL in `src/config.py` or catalog. No sample-specific (mdx 03/04/05) branches in cache module | RULE 0, `feedback_no_hardcoding` |
| G10 | u8 step12 cache_key build site **MUST** be updated to call the new `build_cache_signature` helper. Leaving `template_id::source_section_ids` would render IMP-46 inert | Axis B above |
| G11 | RULE 0 — signature axes evaluated against ALL 32 frames + ALL aligned MDX shapes. No frame-specific branching in signature builder | RULE 0 PIPELINE-CONSTRUCTION |
| G12 | `auto_cache_flag` default **False** at HEAD merge. CI default preserves zero-write behavior | safety + IMP-33 default-OFF symmetry |
| G13 | docstring + IMP-17/IMP-31 doc sync — `cache.py` module docstring updated (NotImplementedError marker removed); `IMP-17-CARVE-OUT.md:54` IMP-46 row updated from "stub" to "active backend, gate semantics preserved" | doc-sync, anchor sync |
| G14 | `feedback_auto_pipeline_first` — no `review_required` injection between AI call and cache save. Save gate decision is auto (visual_check + user_approved/auto_cache); failure paths emit clear reason strings, not queues | `feedback_auto_pipeline_first` |
| G15 | post-IMP-46 backlog row in `PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md` added with status close + reference to commit. Currently stale (IMP-33/IMP-46 rows missing — already flagged in #61 Stage 1) | anchor sync |
## 5. Implementation slicing sketch (Stage 2 input — NOT binding)
Suggested wave-1 ordering:
1. **U1 — Config plumbing (zero behavior change)**
- `src/config.py` Settings : `ai_fallback_auto_cache: bool = False`, `ai_fallback_cache_bucket_boundaries: tuple = (100, 300, 700)` (declared values, env-overridable).
- .env.example update; no committed .env.
2. **U2 — Signature builder + axes**
- New module `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, ...]) -> str` helper.
- Tests: determinism (same input → same hash), axis sensitivity (each axis change → different hash), cross-sample collision (2 different `source_section_ids` with same structural axes → same hash).
3. **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`.
- Tests: stable across reads; sensitive to catalog edit; None when no partial.
4. **U4 — Persistent backend (replaces NotImplementedError)**
- Update `src/phase_z2_ai_fallback/cache.py` — `read_proposal` reads JSON from `data/frame_cache/{frame_id}/{signature}.json`; `save_proposal` writes JSON (still gated by the existing `visual_check_passed` AND `user_approved` conditions).
- Path resolver respects `frame_id` axis (subdirectory).
- Tests: round-trip read/write, invalidation on fingerprint mismatch, gate enforcement preserved (existing 6 gate tests must still pass).
5. **U5 — Step12 integration**
- `src/phase_z2_ai_fallback/step12.py:109-111` — replace sample-specific `cache_key` with `build_cache_signature(...)` using V4 result + frame contract + unit shape.
- Add axis derivation helpers (h3 count, char count bucket, layout preset, zone position).
- Tests: 2 samples × same shape × different identifiers → same signature → cache hit on 2nd run.
6. **U6 — Save site**
- New helper `apply_fallback_save_decision(slide_status, proposal, ...) -> None` in `src/phase_z2_ai_fallback/save_site.py` (or pipeline-side module).
- Called from pipeline after visual_check + user_approval.
- Honors `--auto-cache` flag via config.
- Tests: visual_check False → no write; user_approved False AND auto_cache False → no write; both True (or auto_cache True) → JSON written at canonical path.
7. **U7 — `.gitignore` + invalidation log**
- Add `data/frame_cache/` to `.gitignore`.
- Optional: emit invalidation reason to `debug.json` (`cache_invalidation_log` additive field).
8. **U8 — Tests + AST isolation rerun**
- Full `tests/phase_z2_ai_fallback/` rerun (existing 116 tests must pass).
- New tests under `tests/phase_z2_ai_fallback/test_cache_persistent.py`.
- AST isolation guard rerun against new modules.
9. **U9 — Docs sync**
- `cache.py` module docstring update.
- `IMP-17-CARVE-OUT.md:54` IMP-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.md` row add.
## 6. Open questions (Codex round 1차 review)
- **Q1 — Cache value semantics: D1 (verbatim, text included) vs D2 (structural only, text re-flowed)?** Claude #1 preference = D2 (cross-sample generalization, no hidden text leak). Codex view?
- **Q2 — Invalidation strategy: I1 (per-entry embedded fingerprint) vs I2 (global manifest counter)?** Claude #1 preference = I1 (surgical, audit-clear).
- **Q3 — `--auto-cache` flag plumbing: settings (env / .env) vs CLI argument vs both?** Claude #1 strawman = settings only (env-overridable). CLI-only adds new arg-parser surface.
- **Q4 — Signature axis list completeness: are 8 axes (frame_id / v4_label / cardinality / source_shape / h3_count / char_count_bucket / layout_preset / zone_position) sufficient?** Should `internal_region_id` be added (Layer A SPEC v1 surface)? Claude #1 view: leave for v2; today Internal Region is trace-only.
- **Q5 — Save site placement: end of pipeline `run_pipeline` post-visual-check vs separate explicit `save_fallback_proposals(...)` helper invoked by caller?** Claude #1 preference = explicit helper (clearer audit + composability with `--auto-cache`).
- **Q6 — Char count bucket boundaries: (100, 300, 700) vs other?** Stage 2 lock target. Claude #1 strawman based on typical Korean text density at 16px / 600px-wide zones.
- **Q7 — Hash truncation: 16-char SHA-256 hex (default) vs longer?** Collision probability at 16 chars ≈ 2^-64; for 1k frames × 10 v4_labels × 8 cardinalities ≈ 80k entries → negligible. Claude #1 preference = 16.
- **Q8 — Should IMP-46 also fix the symbolic `cache_key` parameter type at `route_ai_fallback(cache_key: str, ...)` in `src/phase_z2_ai_fallback/router.py:45`?** Today the router treats it as opaque string; with structural signature, type stays `str` (no shape change). Claude #1 view: no router-side change required; signature build happens caller-side (step12) and passes the result.
- **Q9 — Pair atomicity: IMP-46 in single PR vs split (signature first, backend second)?** Claude #1 preference = single PR with u1~u9 (atomic; gates preserved).
=== EVIDENCE ===
Files read (path:line):
- `src/phase_z2_ai_fallback/cache.py` — full file (`read_proposal` returns None; `save_proposal` gates → NotImplementedError marker).
- `src/phase_z2_ai_fallback/step12.py` — full file (`cache_key` build 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_KINDS` includes `raw_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 under `PARTIAL_OVERRIDES`).
- `src/phase_z2_ai_fallback/step17.py` L1-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 (existing `ai_fallback_*` 8 fields; no `ai_fallback_auto_cache` yet).
- `src/phase_z2_mapper.py` L1-80 (catalog loader at L49; `_CATALOG_CACHE` global; `CATALOG_PATH` at L34).
- `templates/phase_z2/catalog/frame_contracts.yaml` L1-60 (single yaml; per-template `frame_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 across `cache.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:495` producer, `phase_z2_pipeline.py:2635/5086` consumer). Producer of `user_approved` = none today (new surface).
- `grep -n "load_frame_contracts" src/phase_z2_mapper.py` = L49 (loader); `get_contract` at 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/` = no `frame_cache/` directory at HEAD (confirmed via `ls 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.
- Pair issue #62 body (current task input) — signature axes + value scope + invalidation rules verified.
Gitea API:
- #61 (IMP-33) **closed**; #62 (IMP-46) **open** — current Stage 1.
- `.orchestrator/drafts/61_stage_problem-review_claude_r1.md` re-read as reference (paired axis context, especially Axis E G11 about cache visual_check + 사용자 OK gating).
- No prior `.orchestrator/drafts/62_*` — confirmed first Stage 1 round.
No code change made — Stage 1 problem-review only.
rewind_target: 해당 없음 (YES 제안)
FINAL_CONSENSUS: YES
Backend axis: verified. src/phase_z2_ai_fallback/cache.py:36 returns None for every valid key, and src/phase_z2_ai_fallback/cache.py:48 preserves the two write gates before raising the IMP-46 NotImplementedError at src/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:109 builds cache_key from template_id plus sorted source_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:32 does not accept layout_preset or zone_position, but both are required signature axes. src/phase_z2_ai_fallback/step12.py:113 also passes v4_result["cardinality"] = None, while src/phase_z2_ai_fallback/prompts.py:66 only 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:
No existing persistent cache was found: no data/frame_cache directory and no frame_cache/signature_hash implementation in src, templates, tests, or data.
route_ai_fallback has one source call site, src/phase_z2_ai_fallback/step12.py:120; tests call it directly, but I found no production call to gather_step12_ai_repair_proposals outside tests. Do not assume the pipeline already invokes the Step 12 AI helper.
Current frame contracts contain 32 frames and expose structural axes such as frame_id, source_shape, and cardinality in templates/phase_z2/catalog/frame_contracts.yaml, but source_shape values are currently repo terms such as top_bullets and h3_subsections, not only the issue text's table/bullet/paragraph. The signature builder should canonicalize from existing repo data rather than hardcoding sample-specific or issue-wording-specific labels.
save_proposal is only called by tests today; the production write path after visual_check PASS plus user OK/--auto-cache still needs a concrete caller-owned integration point. The router intentionally imports only read_proposal, so write-after-approval must remain outside route_ai_fallback.
Cache value scope should stay within the existing AiFallbackProposal contract (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 by src/phase_z2_ai_fallback/schema.py and 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.
D:\ad-hoc\kei\design_agent\templates\phase_z2\catalog\frame_contracts.yaml
Test results:
Not run. Current stage explicitly says NO pytest.
Verified facts:
frame_contracts.yaml currently has 32 template_id entries.
read_proposal is a miss-only stub and save_proposal raises the IMP-46 storage marker only after both gates pass.
Current Step 12 cache key is source-section-ID-based, not structural/content-addressed.
No persistent data/frame_cache implementation was found.
The current source tree has no production caller for gather_step12_ai_repair_proposals.
FINAL_CONSENSUS: YES
[Codex #1] Stage 1 problem-review - IMP-46
Status 3-axis:
1. Backend axis: verified. `src/phase_z2_ai_fallback/cache.py:36` returns `None` for every valid key, and `src/phase_z2_ai_fallback/cache.py:48` preserves the two write gates before raising the IMP-46 `NotImplementedError` at `src/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.
2. Signature axis: verified, with a scope correction. `src/phase_z2_ai_fallback/step12.py:109` builds `cache_key` from `template_id` plus sorted `source_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.
3. Integration/scope axis: scope is larger than a backend swap. `src/phase_z2_ai_fallback/step12.py:32` does not accept `layout_preset` or `zone_position`, but both are required signature axes. `src/phase_z2_ai_fallback/step12.py:113` also passes `v4_result["cardinality"] = None`, while `src/phase_z2_ai_fallback/prompts.py:66` only 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:
- No existing persistent cache was found: no `data/frame_cache` directory and no `frame_cache`/`signature_hash` implementation in `src`, `templates`, `tests`, or `data`.
- `route_ai_fallback` has one source call site, `src/phase_z2_ai_fallback/step12.py:120`; tests call it directly, but I found no production call to `gather_step12_ai_repair_proposals` outside tests. Do not assume the pipeline already invokes the Step 12 AI helper.
- Current frame contracts contain 32 frames and expose structural axes such as `frame_id`, `source_shape`, and `cardinality` in `templates/phase_z2/catalog/frame_contracts.yaml`, but `source_shape` values are currently repo terms such as `top_bullets` and `h3_subsections`, not only the issue text's `table/bullet/paragraph`. The signature builder should canonicalize from existing repo data rather than hardcoding sample-specific or issue-wording-specific labels.
- `save_proposal` is only called by tests today; the production write path after visual_check PASS plus user OK/`--auto-cache` still needs a concrete caller-owned integration point. The router intentionally imports only `read_proposal`, so write-after-approval must remain outside `route_ai_fallback`.
- Cache value scope should stay within the existing `AiFallbackProposal` contract (`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 by `src/phase_z2_ai_fallback/schema.py` and 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_fallback`
- `rg -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 tests`
- `Get-Content -LiteralPath 'src\phase_z2_ai_fallback\cache.py' | Select-Object -First 120`
- `Get-Content -LiteralPath 'src\phase_z2_ai_fallback\step12.py' | Select-Object -First 170`
- `Get-Content -LiteralPath 'src\phase_z2_ai_fallback\router.py' | Select-Object -First 140`
- `rg -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 --short`
Files checked:
- `D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\cache.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\step12.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\router.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\schema.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\prompts.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_composition.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.py`
- `D:\ad-hoc\kei\design_agent\templates\phase_z2\catalog\frame_contracts.yaml`
Test results:
- Not run. Current stage explicitly says NO pytest.
Verified facts:
- `frame_contracts.yaml` currently has 32 `template_id` entries.
- `read_proposal` is a miss-only stub and `save_proposal` raises the IMP-46 storage marker only after both gates pass.
- Current Step 12 cache key is source-section-ID-based, not structural/content-addressed.
- No persistent `data/frame_cache` implementation was found.
- The current source tree has no production caller for `gather_step12_ai_repair_proposals`.
FINAL_CONSENSUS: YES
id: u2
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:
u1: Covers the 8-axis exact structural signature and removes section/sample id leakage. Needs one explicit same-structure/different-section-id determinism test in Stage 3.
u2: Backend path, schema_version, corrupt/missing miss behavior, and write gate are in scope. Plan is missing explicit cache value shape coverage for builder_options, partial_overrides, slot mapping, and optional slide-level CSS.
u3: Invalidation axis is named, but not implementation-ready: the plan does not say where contract_sha, partial_sha, and catalog_sha are computed or how read_proposal receives/recomputes them. Unit files only cover cache.py tests, so integration with Step 12/router inputs is under-specified.
u4: Structural key replacement and cardinality/layout/zone propagation are in scope. Plan does not cover exact-first lookup versus optional fuzzy lookup; fuzzy may be deferred, but the plan must say so and test exact deterministic hit behavior.
u5: This does not satisfy the issue body as written. The issue asks for a --auto-cache flag; the plan only adds settings.ai_fallback_auto_cache and names no CLI/entrypoint file or existing flag surface.
u6: Size/file-count rule passes. The .gitignore/.gitkeep choice is acceptable if generated JSON stays untracked and the directory anchor is tracked.
Out-of-scope notes:
AI invocation internals, cache-to-catalog promotion, new route_ai_fallback call sites, and Stage 0 mdx_normalizer remain out of scope.
Fuzzy lookup can be out of scope only if explicitly deferred as optional; current plan neither implements nor defers it.
Rollback strategy:
Revert u1-u6 touched files and delete generated data/frame_cache JSON. No destructive sweep is required because cache misses fall back to the existing AI path.
D:\ad-hoc\kei\design_agent\docs\architecture\IMP-17-CARVE-OUT.md
Test results:
pytest -q tests: 526 passed in 41.83s
Verified facts:
Current cache.py read_proposal always returns None and save_proposal still raises NotImplementedError after both gates pass.
Current step12.py builds cache_key from frame_template_id plus sorted source_section_ids, so the Stage 1 defect is real.
Current router.py reads cache before prompt/client and does not import save_proposal.
Current src/config.py has ai_fallback_enabled defaults but no ai_fallback_auto_cache field.
Existing tests assert the old sample-specific Step 12 cache key and the cache NotImplementedError marker, so Stage 3 must update those tests.
Claude r1 implementation_units block exists and every unit is <=50 estimated lines and <=3 files; size rule passes.
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Codex #2] Stage 2 simulation-plan review IMP-46
=== IMPLEMENTATION_UNITS ===
- id: u1
summary: Add deterministic structural signature module over the 8 declared axes, with char-count buckets and source-shape encoding.
files:
- src/phase_z2_ai_fallback/signature.py
- tests/phase_z2_ai_fallback/test_signature.py
tests:
- tests/phase_z2_ai_fallback/test_signature.py
estimate_lines: 50
- id: u2
summary: Replace cache.py NotImplementedError stub with persistent JSON backend under data/frame_cache/{frame_id}/{signature_hash}.json.
files:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
estimate_lines: 50
- id: u3
summary: Add invalidation check for frame contract, partial template, and catalog fingerprints.
files:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
tests:
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
estimate_lines: 45
- id: u4
summary: Extend Step 12 helper inputs and replace template/section-id cache key with structural signature.
files:
- src/phase_z2_ai_fallback/step12.py
- tests/phase_z2_ai_fallback/test_step12.py
tests:
- tests/phase_z2_ai_fallback/test_step12.py
estimate_lines: 50
- id: u5
summary: Add auto-cache override setting while preserving visual_check_passed gate.
files:
- src/config.py
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
estimate_lines: 30
- id: u6
summary: Ignore generated frame cache data and refresh IMP-17 carve-out documentation.
files:
- .gitignore
- data/frame_cache/.gitkeep
- docs/architecture/IMP-17-CARVE-OUT.md
tests: []
estimate_lines: 20
Per-unit rationale:
- u1: Covers the 8-axis exact structural signature and removes section/sample id leakage. Needs one explicit same-structure/different-section-id determinism test in Stage 3.
- u2: Backend path, schema_version, corrupt/missing miss behavior, and write gate are in scope. Plan is missing explicit cache value shape coverage for builder_options, partial_overrides, slot mapping, and optional slide-level CSS.
- u3: Invalidation axis is named, but not implementation-ready: the plan does not say where contract_sha, partial_sha, and catalog_sha are computed or how read_proposal receives/recomputes them. Unit files only cover cache.py tests, so integration with Step 12/router inputs is under-specified.
- u4: Structural key replacement and cardinality/layout/zone propagation are in scope. Plan does not cover exact-first lookup versus optional fuzzy lookup; fuzzy may be deferred, but the plan must say so and test exact deterministic hit behavior.
- u5: This does not satisfy the issue body as written. The issue asks for a --auto-cache flag; the plan only adds settings.ai_fallback_auto_cache and names no CLI/entrypoint file or existing flag surface.
- u6: Size/file-count rule passes. The .gitignore/.gitkeep choice is acceptable if generated JSON stays untracked and the directory anchor is tracked.
Out-of-scope notes:
- AI invocation internals, cache-to-catalog promotion, new route_ai_fallback call sites, and Stage 0 mdx_normalizer remain out of scope.
- Fuzzy lookup can be out of scope only if explicitly deferred as optional; current plan neither implements nor defers it.
Rollback strategy:
- Revert u1-u6 touched files and delete generated data/frame_cache JSON. No destructive sweep is required because cache misses fall back to the existing AI path.
=== EVIDENCE ===
Commands run:
- Get-Content D:\ad-hoc\kei\design_agent\.orchestrator\tmp\codex_prompt.txt
- git status --short
- rg over src/tests/docs for cache, Step 12, signature, and config surfaces
- Get-Content src/phase_z2_ai_fallback/cache.py
- Get-Content src/phase_z2_ai_fallback/step12.py
- Get-Content src/phase_z2_ai_fallback/prompts.py
- Get-Content src/phase_z2_ai_fallback/router.py
- Get-Content src/phase_z2_ai_fallback/schema.py
- Get-Content src/config.py
- Get-Content tests/phase_z2_ai_fallback/test_cache.py
- Get-Content tests/phase_z2_ai_fallback/test_step12.py
- Get-Content tests/phase_z2_ai_fallback/test_router.py
- Select-String docs/architecture/IMP-17-CARVE-OUT.md for cache/fallback anchors
- pytest -q tests
Files checked:
- D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\cache.py
- D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\step12.py
- D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\prompts.py
- D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\router.py
- D:\ad-hoc\kei\design_agent\src\phase_z2_ai_fallback\schema.py
- D:\ad-hoc\kei\design_agent\src\config.py
- D:\ad-hoc\kei\design_agent\tests\phase_z2_ai_fallback\test_cache.py
- D:\ad-hoc\kei\design_agent\tests\phase_z2_ai_fallback\test_step12.py
- D:\ad-hoc\kei\design_agent\tests\phase_z2_ai_fallback\test_router.py
- D:\ad-hoc\kei\design_agent\docs\architecture\IMP-17-CARVE-OUT.md
Test results:
- pytest -q tests: 526 passed in 41.83s
Verified facts:
- Current cache.py read_proposal always returns None and save_proposal still raises NotImplementedError after both gates pass.
- Current step12.py builds cache_key from frame_template_id plus sorted source_section_ids, so the Stage 1 defect is real.
- Current router.py reads cache before prompt/client and does not import save_proposal.
- Current src/config.py has ai_fallback_enabled defaults but no ai_fallback_auto_cache field.
- Existing tests assert the old sample-specific Step 12 cache key and the cache NotImplementedError marker, so Stage 3 must update those tests.
- Claude r1 implementation_units block exists and every unit is <=50 estimated lines and <=3 files; size rule passes.
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
id: u2
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:
id: u6
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:
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.
Test-Path checks for src/phase_z2_ai_fallback/signature.py, tests/phase_z2_ai_fallback/test_signature.py, tests/phase_z2_ai_fallback/test_cache_invalidation.py, data/frame_cache
pytest -q tests
Files checked:
src/phase_z2_ai_fallback/cache.py
src/phase_z2_ai_fallback/step12.py
src/phase_z2_ai_fallback/prompts.py
src/phase_z2_ai_fallback/schema.py
src/phase_z2_ai_fallback/router.py
src/config.py
src/phase_z2_pipeline.py
tests/phase_z2_ai_fallback/test_cache.py
tests/phase_z2_ai_fallback/test_step12.py
tests/test_phase_z2_ai_fallback_config.py
docs/architecture/IMP-17-CARVE-OUT.md
.gitignore
Test results:
pytest -q tests: 526 passed in 42.03s
Verified facts:
cache.py still returns None on read and raises NotImplementedError after both gates, matching the IMP-46 target.
step12.py still builds cache_key from template_id plus sorted source_section_ids and passes cardinality None, so u4 directly fixes the known defect.
prompts.py reads cardinality/cardinality_signature only if present, so Step 12 contract extension is required.
signature.py, test_signature.py, test_cache_invalidation.py, and data/frame_cache are absent now and correctly planned for Stage 3.
.gitignore currently ignores data/ at line 11, so u6 needs an exception for data/frame_cache/.gitkeep.
IMPLEMENTATION_UNITS exists; every unit is <= 50 estimated lines and <= 3 files.
Side effects before posting: no source edits made for implementation; only this required draft artifact is created.
Commit SHA: n/a for Stage 2
FINAL_CONSENSUS: YES
[Codex #3] Stage 2 simulation-plan review - IMP-46
=== IMPLEMENTATION_UNITS ===
- id: u1
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:
- src/phase_z2_ai_fallback/signature.py
- tests/phase_z2_ai_fallback/test_signature.py
tests:
- tests/phase_z2_ai_fallback/test_signature.py
estimate_lines: 50
- id: u2
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:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
estimate_lines: 50
- id: u3
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:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
tests:
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
estimate_lines: 45
- id: u4
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:
- src/phase_z2_ai_fallback/step12.py
- tests/phase_z2_ai_fallback/test_step12.py
tests:
- tests/phase_z2_ai_fallback/test_step12.py
estimate_lines: 50
- id: u5
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:
- src/phase_z2_pipeline.py
- src/config.py
- src/phase_z2_ai_fallback/cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
- tests/test_phase_z2_ai_fallback_config.py
estimate_lines: 40
- id: u6
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:
- .gitignore
- data/frame_cache/.gitkeep
- docs/architecture/IMP-17-CARVE-OUT.md
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:
- Get-Content .orchestrator/tmp/codex_prompt.txt
- rg --files
- git status --short
- Get-Content src/phase_z2_ai_fallback/cache.py
- Get-Content src/phase_z2_ai_fallback/step12.py
- Get-Content src/phase_z2_ai_fallback/prompts.py
- Get-Content src/phase_z2_ai_fallback/schema.py
- Get-Content src/phase_z2_ai_fallback/router.py
- Get-Content tests/phase_z2_ai_fallback/test_cache.py
- Get-Content tests/phase_z2_ai_fallback/test_step12.py
- rg -n "ai_fallback|auto-cache|auto_cache|argparse|ArgumentParser" src/phase_z2_pipeline.py src/config.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 docs/architecture/IMP-17-CARVE-OUT.md .gitignore
- rg -n "def read_proposal|def save_proposal|NotImplementedError|cache_key|route_ai_fallback|cardinality|build_ai_fallback_prompt|data/|frame_cache|IMP-46|AI_FALLBACK|ai_fallback" src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/prompts.py src/config.py .gitignore docs/architecture/IMP-17-CARVE-OUT.md
- Test-Path checks for src/phase_z2_ai_fallback/signature.py, tests/phase_z2_ai_fallback/test_signature.py, tests/phase_z2_ai_fallback/test_cache_invalidation.py, data/frame_cache
- pytest -q tests
Files checked:
- src/phase_z2_ai_fallback/cache.py
- src/phase_z2_ai_fallback/step12.py
- src/phase_z2_ai_fallback/prompts.py
- src/phase_z2_ai_fallback/schema.py
- src/phase_z2_ai_fallback/router.py
- src/config.py
- src/phase_z2_pipeline.py
- tests/phase_z2_ai_fallback/test_cache.py
- tests/phase_z2_ai_fallback/test_step12.py
- tests/test_phase_z2_ai_fallback_config.py
- docs/architecture/IMP-17-CARVE-OUT.md
- .gitignore
Test results:
- pytest -q tests: 526 passed in 42.03s
Verified facts:
- cache.py still returns None on read and raises NotImplementedError after both gates, matching the IMP-46 target.
- step12.py still builds cache_key from template_id plus sorted source_section_ids and passes cardinality None, so u4 directly fixes the known defect.
- prompts.py reads cardinality/cardinality_signature only if present, so Step 12 contract extension is required.
- signature.py, test_signature.py, test_cache_invalidation.py, and data/frame_cache are absent now and correctly planned for Stage 3.
- .gitignore currently ignores data/ at line 11, so u6 needs an exception for data/frame_cache/.gitkeep.
- IMPLEMENTATION_UNITS exists; every unit is <= 50 estimated lines and <= 3 files.
- Side effects before posting: no source edits made for implementation; only this required draft artifact is created.
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>` entry
- Each unit MUST include `tests: [<path>, ...]` (NOT `tests: []`)
- Implementation + tests = same unit (no deferring tests to later units)
Example:
=== IMPLEMENTATION_UNITS ===
- id: u1
summary: ...
files: [...]
tests: [tests/.../test_xxx.py]
estimate_lines: <int>
id: u6
summary: Track data/frame_cache/.gitkeep via .gitignore exception (!data/frame_cache/.gitkeep under existing data/ 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:
AI body (IMP-33 c864fe0); fuzzy lookup (deferred); cache→catalog (R4); new router call sites; Stage 0 normalizer; prompts.py redesign beyond cardinality field exposure.
Rollback:
Per-unit revert. cache.py shared (u2/u3/u5) → reverse order u5→u3→u2. u1/u6 additive. Generated JSON under data/frame_cache/ stays gitignored — delete working tree only.
=== EVIDENCE ===
Files verified present:
D:/ad-hoc/kei/design_agent/docs/architecture/IMP-17-CARVE-OUT.md (target for u6 doc refresh)
D:/ad-hoc/kei/design_agent/tests/phase_z2_ai_fallback/ (target dir for new test file; existing test_docs_sync.py provides docs-marker pattern)
Comments reviewed:
id: u2
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:
id: u5
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:
id: u6
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:
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
[Codex #4] Stage 2 simulation-plan review - IMP-46
=== IMPLEMENTATION_UNITS ===
- id: u1
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:
- src/phase_z2_ai_fallback/signature.py
- tests/phase_z2_ai_fallback/test_signature.py
tests:
- tests/phase_z2_ai_fallback/test_signature.py
estimate_lines: 50
- id: u2
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:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
estimate_lines: 50
- id: u3
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:
- src/phase_z2_ai_fallback/cache.py
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
tests:
- tests/phase_z2_ai_fallback/test_cache_invalidation.py
estimate_lines: 45
- id: u4
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:
- src/phase_z2_ai_fallback/step12.py
- tests/phase_z2_ai_fallback/test_step12.py
tests:
- tests/phase_z2_ai_fallback/test_step12.py
estimate_lines: 50
- id: u5
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:
- src/phase_z2_pipeline.py
- src/config.py
- src/phase_z2_ai_fallback/cache.py
tests:
- tests/phase_z2_ai_fallback/test_cache.py
- tests/test_phase_z2_ai_fallback_config.py
estimate_lines: 40
- id: u6
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:
- .gitignore
- data/frame_cache/.gitkeep
- docs/architecture/IMP-17-CARVE-OUT.md
tests:
- tests/phase_z2_ai_fallback/test_cache_repo_layout.py
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
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
AI isolation contract: signature.py performs zero AI/network/MDX I/O. Pure stdlib (hashlib, json, enum).
No-hardcoding lock: the 8-axis surface accepts no sample/section/template identifiers. The public-surface test enforces this structurally.
Scope lock honored: u1 alone — no edits to 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 versioning: SCHEMA_VERSION is 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
u2 — cache.py persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json
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 hashlib import needed by this unit.
build_signature public parameters are exactly frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position.
rg found no template_id, source_section_ids, mdx_path, section_id, MDX 03, MDX 04, or MDX 05 leakage in the u1 signature files.
test_ast_isolation.py only adds hashlib to the allowed top-level imports for this unit.
Remaining units: [u2, u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[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 `hashlib` import needed by this unit.
=== EVIDENCE ===
Commands run:
- `Get-Content -LiteralPath src/phase_z2_ai_fallback/signature.py`
- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_signature.py`
- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_ast_isolation.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_signature.py tests/phase_z2_ai_fallback/test_ast_isolation.py -q`
- `rg -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.py`
- PowerShell Python introspection for `SCHEMA_VERSION`, `CHAR_COUNT_BUCKET_LABELS`, and `build_signature` parameters
Files checked:
- `src/phase_z2_ai_fallback/signature.py`
- `tests/phase_z2_ai_fallback/test_signature.py`
- `tests/phase_z2_ai_fallback/test_ast_isolation.py`
Test results:
- `42 passed in 0.12s`
Verified facts:
- `SCHEMA_VERSION == 1`.
- `CHAR_COUNT_BUCKET_LABELS == ("0-50", "51-150", "151-400", "401-1000", "1001+")`.
- `build_signature` public parameters are exactly `frame_id`, `v4_label`, `cardinality`, `source_shape`, `h3_count`, `char_count_bucket`, `layout_preset`, `zone_position`.
- `rg` found no `template_id`, `source_section_ids`, `mdx_path`, `section_id`, `MDX 03`, `MDX 04`, or `MDX 05` leakage in the u1 signature files.
- `test_ast_isolation.py` only adds `hashlib` to the allowed top-level imports for this unit.
Remaining units: [u2, u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
summary: Replace the IMP-33 u6 NotImplementedError marker in src/phase_z2_ai_fallback/cache.py with a content-addressed JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json. Stored payload = {schema_version: 1, proposal, slide_css, fingerprints}. Missing / corrupt files → read_proposal returns None; mkdir(parents=True, exist_ok=True) on save. Both write gates (visual_check_passed AND user_approved) preserved BEFORE any filesystem touch.
■ files_changed
src/phase_z2_ai_fallback/cache.py (rewrite, 137 SLOC — was 82 SLOC stub)
Module-level constants: SCHEMA_VERSION = 1, KEY_DELIMITER = "::", CACHE_ROOT: pathlib.Path = pathlib.Path("data/frame_cache") (mutable for monkeypatch-based test isolation; tests redirect to tmp_path).
AiFallbackCacheGateError(RuntimeError) — class preserved exactly (loud gate violation surface).
_parse_key(key) -> tuple[str, str] | None — splits "{frame_id}::{signature_hash}"; returns None if 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").
Legacy key (no ::) → silent None. This is router back-compat — test_router.py:58 still passes cache_key="frame:1171281190:cardinality:many" and must continue to miss safely until u4 replaces the cache_key at the step12 call site.
File missing → None. Corrupt JSON (OSError / json.JSONDecodeError) → None. Non-dict root, missing proposal key, non-dict proposal value, or AiFallbackProposal.model_validate failure (e.g. forbidden proposal_kind like mdx_text smuggled into a stale payload) → None. Read errors NEVER propagate — the cache is a hint, never a hard dependency.
Gate order preserved: empty key → ValueError; wrong type → TypeError; visual_check_passed=False → AiFallbackCacheGateError; user_approved=False → AiFallbackCacheGateError. Both gates raise BEFORE the parent directory is touched (verified by test_save_gate_violation_does_not_touch_filesystem).
slide_css validated (str | None), fingerprints validated (dict | None), then key parsed via _parse_key; legacy key format → ValueError (loud — writes must use the structural form).
On success: path.parent.mkdir(parents=True, exist_ok=True), JSON payload written with sort_keys=True, ensure_ascii=False, indent=2. Returns the resolved pathlib.Path.
fingerprints is persisted but NOT compared in u2 — comparison / invalidation is u3's responsibility (explicit module docstring note).
Module docstring documents the on-disk layout, the legacy-key silent-miss back-compat, the u3 fingerprint deferral, and the AI-isolation invariant (no Anthropic / MDX / Phase Z runtime imports).
tests/phase_z2_ai_fallback/test_cache.py — 31 cases. @pytest.fixture(autouse=True) _isolated_cache_root redirects cache_mod.CACHE_ROOT to tmp_path / "frame_cache" via monkeypatch.setattr for every test, so the production directory is never touched. Coverage:
write gates: visual_check=False rejects; user_approved=False rejects; both False rejects; gate violation does NOT create the frame_id directory; empty key → ValueError; non-proposal object → TypeError; legacy key format → ValueError; non-string slide_css → TypeError; non-dict fingerprints → TypeError; AiFallbackCacheGateError is not a NotImplementedError subclass.
persistence + round-trip: creates parent directories; returns resolved path; payload includes schema_version=1; payload includes proposal.model_dump(mode="json"); default slide_css=None and default fingerprints={}; round-trip with slide_css set; round-trip with fingerprints dict; read returns the same proposal after save; parametrized round-trip across all three ProposalKind values (BUILDER_OPTIONS_PATCH, PARTIAL_OVERRIDES, SLOT_MAPPING_PROPOSAL); overwrite an existing entry; file layout uses frame_id/ directory + signature_hash.json file; two distinct signature hashes under the same frame are isolated; triple-delimiter key rejected.
No changes to tests/phase_z2_ai_fallback/test_ast_isolation.py — json and pathlib were already in the _ALLOWED_TOP_LEVEL whitelist, so the new backend introduces no new top-level imports.
python -m pytest tests/phase_z2_ai_fallback/ -q → 156 passed in 0.27s (full AI fallback regression — includes test_router.py which still calls read_proposal("frame:1171281190:cardinality:many") and continues to get None because 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.
AiFallbackCacheGateError MRO ends at RuntimeError (NOT a NotImplementedError — the IMP-33 marker is gone).
■ scope_locks_honoured
u3 deferred: fingerprints field is persisted on save and the field is read back on disk, but read_proposal does NOT compare supplied fingerprints against the stored value. That mismatch-comparison logic is u3.
u4 deferred: router.py, step12.py, and test_router.py are NOT modified. The router still calls read_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 returning None (the prior stub behavior). u4 will replace the cache_key at the step12 call site with the structural frame_id::signature_hash form.
u5 deferred: --auto-cache CLI flag, settings.ai_fallback_auto_cache, and the auto_cache parameter on save_proposal are NOT introduced here. Both gates remain mandatory; neither can be bypassed in u2.
u6 deferred: .gitignore exception for data/frame_cache/.gitkeep and the IMP-17-CARVE-OUT.md row refresh are NOT performed here.
AI isolation contract preserved: no Anthropic, MDX, or Phase Z runtime imports; only stdlib (json, pathlib) + intra-package (schema).
■ remaining_units
u3 (cache invalidation by fingerprints — read_proposal returns None on mismatch; new tests/phase_z2_ai_fallback/test_cache_invalidation.py).
(none — Stage 1 scope_lock holds; no axis discovered beyond the unit decomposition).
■ STOP
No commit, no push. The Gitea comment is the deliverable.
[Claude #2] Stage 3 code-edit IMP-46 — u2 persistent JSON cache backend
Executing unit: u2
■ unit_executed
- id: u2
- summary: Replace the IMP-33 u6 `NotImplementedError` marker in `src/phase_z2_ai_fallback/cache.py` with a content-addressed JSON backend at `data/frame_cache/{frame_id}/{signature_hash}.json`. Stored payload = `{schema_version: 1, proposal, slide_css, fingerprints}`. Missing / corrupt files → `read_proposal` returns `None`; `mkdir(parents=True, exist_ok=True)` on save. Both write gates (`visual_check_passed` AND `user_approved`) preserved BEFORE any filesystem touch.
■ files_changed
- src/phase_z2_ai_fallback/cache.py (rewrite, 137 SLOC — was 82 SLOC stub)
- tests/phase_z2_ai_fallback/test_cache.py (rewrite, ~290 SLOC — 31 test cases covering gates, round-trip, persistence layout, corruption tolerance)
■ diff_summary
- `src/phase_z2_ai_fallback/cache.py`
- Module-level constants: `SCHEMA_VERSION = 1`, `KEY_DELIMITER = "::"`, `CACHE_ROOT: pathlib.Path = pathlib.Path("data/frame_cache")` (mutable for monkeypatch-based test isolation; tests redirect to `tmp_path`).
- `AiFallbackCacheGateError(RuntimeError)` — class preserved exactly (loud gate violation surface).
- `_parse_key(key) -> tuple[str, str] | None` — splits `"{frame_id}::{signature_hash}"`; returns `None` if 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)`:
- Empty / non-string `key` → `ValueError` (loud; matches prior contract).
- Legacy key (no `::`) → silent `None`. This is router back-compat — `test_router.py:58` still passes `cache_key="frame:1171281190:cardinality:many"` and must continue to miss safely until u4 replaces the cache_key at the step12 call site.
- File missing → `None`. Corrupt JSON (`OSError` / `json.JSONDecodeError`) → `None`. Non-dict root, missing `proposal` key, non-dict `proposal` value, or `AiFallbackProposal.model_validate` failure (e.g. forbidden `proposal_kind` like `mdx_text` smuggled 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)`:
- Gate order preserved: empty key → `ValueError`; wrong type → `TypeError`; `visual_check_passed=False` → `AiFallbackCacheGateError`; `user_approved=False` → `AiFallbackCacheGateError`. Both gates raise BEFORE the parent directory is touched (verified by `test_save_gate_violation_does_not_touch_filesystem`).
- `slide_css` validated (`str | None`), `fingerprints` validated (`dict | None`), then key parsed via `_parse_key`; legacy key format → `ValueError` (loud — writes must use the structural form).
- On success: `path.parent.mkdir(parents=True, exist_ok=True)`, JSON payload written with `sort_keys=True, ensure_ascii=False, indent=2`. Returns the resolved `pathlib.Path`.
- `fingerprints` is persisted but NOT compared in u2 — comparison / invalidation is u3's responsibility (explicit module docstring note).
- Module docstring documents the on-disk layout, the legacy-key silent-miss back-compat, the u3 fingerprint deferral, and the AI-isolation invariant (no Anthropic / MDX / Phase Z runtime imports).
- `tests/phase_z2_ai_fallback/test_cache.py` — 31 cases. `@pytest.fixture(autouse=True) _isolated_cache_root` redirects `cache_mod.CACHE_ROOT` to `tmp_path / "frame_cache"` via `monkeypatch.setattr` for every test, so the production directory is never touched. Coverage:
- read: missing file → None; empty key → ValueError; non-string key → ValueError; legacy key format → None (router back-compat); corrupt JSON → None; non-dict root → None; payload missing `proposal` → None; forbidden `proposal_kind="mdx_text"` → None (validator rejects).
- write gates: visual_check=False rejects; user_approved=False rejects; both False rejects; gate violation does NOT create the `frame_id` directory; empty key → ValueError; non-proposal object → TypeError; legacy key format → ValueError; non-string `slide_css` → TypeError; non-dict `fingerprints` → TypeError; `AiFallbackCacheGateError` is not a `NotImplementedError` subclass.
- persistence + round-trip: creates parent directories; returns resolved path; payload includes `schema_version=1`; payload includes `proposal.model_dump(mode="json")`; default `slide_css=None` and default `fingerprints={}`; round-trip with `slide_css` set; round-trip with fingerprints dict; read returns the same proposal after save; **parametrized round-trip across all three `ProposalKind` values** (BUILDER_OPTIONS_PATCH, PARTIAL_OVERRIDES, SLOT_MAPPING_PROPOSAL); overwrite an existing entry; file layout uses `frame_id/` directory + `signature_hash.json` file; two distinct signature hashes under the same frame are isolated; triple-delimiter key rejected.
- No changes to `tests/phase_z2_ai_fallback/test_ast_isolation.py` — `json` and `pathlib` were already in the `_ALLOWED_TOP_LEVEL` whitelist, 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 — includes `test_router.py` which still calls `read_proposal("frame:1171281190:cardinality:many")` and continues to get `None` because 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**.
- Public-surface introspection:
- `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`.
- `AiFallbackCacheGateError` MRO ends at `RuntimeError` (NOT a `NotImplementedError` — the IMP-33 marker is gone).
■ scope_locks_honoured
- u3 deferred: `fingerprints` field is persisted on save and the field is read back on disk, but `read_proposal` does NOT compare supplied fingerprints against the stored value. That mismatch-comparison logic is u3.
- u4 deferred: `router.py`, `step12.py`, and `test_router.py` are NOT modified. The router still calls `read_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 returning `None` (the prior stub behavior). u4 will replace the cache_key at the step12 call site with the structural `frame_id::signature_hash` form.
- u5 deferred: `--auto-cache` CLI flag, `settings.ai_fallback_auto_cache`, and the `auto_cache` parameter on `save_proposal` are NOT introduced here. Both gates remain mandatory; neither can be bypassed in u2.
- u6 deferred: `.gitignore` exception for `data/frame_cache/.gitkeep` and the `IMP-17-CARVE-OUT.md` row refresh are NOT performed here.
- AI isolation contract preserved: no Anthropic, MDX, or Phase Z runtime imports; only stdlib (`json`, `pathlib`) + intra-package (`schema`).
■ remaining_units
- u3 (cache invalidation by fingerprints — `read_proposal` returns None on mismatch; new `tests/phase_z2_ai_fallback/test_cache_invalidation.py`).
- u4 (Step 12 helper inputs + structural cache_key replacement — `src/phase_z2_ai_fallback/step12.py` + `tests/phase_z2_ai_fallback/test_step12.py`).
- u5 (`--auto-cache` CLI flag + `settings.ai_fallback_auto_cache` + `auto_cache` kwarg on `save_proposal` + 2³ gate truth-table test).
- u6 (`data/frame_cache/.gitkeep` + `.gitignore` exception + `IMP-17-CARVE-OUT.md` cache row refresh + `tests/phase_z2_ai_fallback/test_cache_repo_layout.py`).
■ follow_up_issue_candidates
- (none — Stage 1 scope_lock holds; no axis discovered beyond the unit decomposition).
■ STOP
- No commit, no push. The Gitea comment is the deliverable.
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, stores schema_version, proposal, slide_css, and fingerprints, returns None for 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 call Path.resolve() when CACHE_ROOT is relative. This does not violate the u2 contract.
tests/phase_z2_ai_fallback/test_cache.py: 33 passed in 0.12s
tests/phase_z2_ai_fallback/test_router.py: 9 passed in 0.03s
Verified facts:
SCHEMA_VERSION == 1.
KEY_DELIMITER == "::".
CACHE_ROOT == data/frame_cache.
save_proposal accepts slide_css and fingerprints, writes JSON only after visual_check_passed=True and user_approved=True, and returns the target storage path.
read_proposal returns None for missing files, malformed legacy keys without ::, corrupt JSON, non-dict roots, missing/non-dict proposal payloads, and proposal validation failures.
Saved payload includes schema_version, proposal, slide_css, and fingerprints.
Parent directories are created lazily on successful save.
Gate failures are AiFallbackCacheGateError, not NotImplementedError, and the targeted test verifies no frame directory is created on gate rejection.
Current router still imports only read_proposal; no save_proposal call site was introduced in src.
Fingerprint persistence is present, but read-side fingerprint comparison is not implemented in u2, matching the u3 boundary.
rg found no anthropic, Claude, MDX 03, MDX 04, MDX 05, source_section_ids, mdx_path, or template_id leakage in cache.py; NotImplementedError remains only in explanatory/test text.
Remaining units: [u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[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`, stores `schema_version`, `proposal`, `slide_css`, and `fingerprints`, returns `None` for 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 call `Path.resolve()` when `CACHE_ROOT` is relative. This does not violate the u2 contract.
=== EVIDENCE ===
Commands run:
- `Get-Content -LiteralPath src\phase_z2_ai_fallback\cache.py`
- `Get-Content -LiteralPath tests\phase_z2_ai_fallback\test_cache.py`
- `git diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py -q`
- `rg -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.py`
- PowerShell Python introspection for `SCHEMA_VERSION`, `KEY_DELIMITER`, `CACHE_ROOT`, `save_proposal`, and `read_proposal`
- `rg -n "save_proposal\(|read_proposal\(|cache_key\s*=|route_ai_fallback\(" src tests/phase_z2_ai_fallback/test_step12.py`
- `Get-Content -LiteralPath src\phase_z2_ai_fallback\step12.py`
- `Get-Content -LiteralPath src\phase_z2_ai_fallback\router.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
- `git status --short`
Files checked:
- `src/phase_z2_ai_fallback/cache.py`
- `tests/phase_z2_ai_fallback/test_cache.py`
- `src/phase_z2_ai_fallback/router.py`
- `src/phase_z2_ai_fallback/step12.py`
Test results:
- `tests/phase_z2_ai_fallback/test_cache.py`: 33 passed in 0.12s
- `tests/phase_z2_ai_fallback/test_router.py`: 9 passed in 0.03s
Verified facts:
- `SCHEMA_VERSION == 1`.
- `KEY_DELIMITER == "::"`.
- `CACHE_ROOT == data/frame_cache`.
- `save_proposal` accepts `slide_css` and `fingerprints`, writes JSON only after `visual_check_passed=True` and `user_approved=True`, and returns the target storage path.
- `read_proposal` returns `None` for missing files, malformed legacy keys without `::`, corrupt JSON, non-dict roots, missing/non-dict proposal payloads, and proposal validation failures.
- Saved payload includes `schema_version`, `proposal`, `slide_css`, and `fingerprints`.
- Parent directories are created lazily on successful save.
- Gate failures are `AiFallbackCacheGateError`, not `NotImplementedError`, and the targeted test verifies no frame directory is created on gate rejection.
- Current router still imports only `read_proposal`; no `save_proposal` call site was introduced in `src`.
- Fingerprint persistence is present, but read-side fingerprint comparison is not implemented in u2, matching the u3 boundary.
- `rg` found no `anthropic`, `Claude`, `MDX 03`, `MDX 04`, `MDX 05`, `source_section_ids`, `mdx_path`, or `template_id` leakage in `cache.py`; `NotImplementedError` remains only in explanatory/test text.
Remaining units: [u3, u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
summary: Add fingerprint-based read-side invalidation to src/phase_z2_ai_fallback/cache.py. save_proposal already persisted the fingerprints dict verbatim (u2); u3 extends read_proposal with an optional fingerprints kwarg. When supplied, the stored fingerprints dict 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 no fingerprints field) returns None. Default fingerprints=None performs no comparison (back-compat). Fingerprint computation stays outside cache.py: the module is a content-addressed comparator, not a hasher — preserves AI isolation and keeps the cache schema-agnostic.
Rationale for keeping computation outside: AI isolation + schema-agnostic (new fingerprint axes don't require editing cache.py)
read_proposal signature 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.
New input validation: fingerprints is not None and not isinstance(fingerprints, dict) → TypeError (symmetric with save_proposal's fingerprints validation).
New comparison block placed AFTER file existence + JSON load + non-dict-root checks, but BEFORE the proposal validation: if fingerprints is not None and stored = data.get("fingerprints") is not a dict OR stored != fingerprints → return None. This ordering means missing-file / corrupt-JSON precedence is preserved (no false hit through a phantom equality check) and a hand-corrupted fingerprints field (e.g. serialized as a list) is treated as an invalidation, not as a TypeError.
No new module-level imports. No new constants. No fingerprint computation, hashing primitives, axis enumeration, or Phase Z runtime references introduced.
tests/phase_z2_ai_fallback/test_cache_invalidation.py (new — 21 cases, all pass)
Save-side: test_save_persists_fingerprints_verbatim re-asserts the u2 round-trip foundation u3 depends on.
Back-compat (read without kwarg):
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_empty_fingerprints_matches_empty_stored — both sides {} is a valid match, not a special-case None.
Invalidation (the 11-case core):
Parametrized over the three declared shas: 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_field_missing — legacy payload (no fingerprints field at all) invalidates under fingerprint-aware lookup.
Module isolation lock: test_cache_module_has_no_fingerprint_computer — public surface of cache.py is grepped for hash/sha/fingerprint substrings; 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.
Cross-signature isolation: test_distinct_fingerprint_sets_isolated_per_signature — two entries with different sig_hashes hold their own fingerprints; crossed lookups miss, aligned lookups hit.
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).
No src/** files touched outside src/phase_z2_ai_fallback/cache.py.
AST isolation guard re-verified: no new top-level imports added; the existing hashlib whitelist from u1 is NOT used by cache.py (u3 does not import hashlib — comparison is plain dict equality, not hash recomputation).
■ guardrail / contract evidence
AI isolation contract: cache.py still imports only json, pathlib, and src.phase_z2_ai_fallback.schema. No Anthropic, no MDX, no Phase Z runtime references.
No-hardcoding: the three sha axis names (contract_sha / partial_sha / catalog_sha) do NOT appear in cache.py source — they are caller-supplied dict keys; cache.py only compares dicts. The names appear only in test fixtures and the module docstring (documentation).
PZ-1 (normal-path AI=0): unchanged — read_proposal performs no AI invocation.
Both write gates (visual_check_passed AND user_approved) preserved untouched. u3 only modifies the read path.
Gate precedence preserved: u2's test_save_gate_violation_does_not_touch_filesystem and the 33-case u2 suite all still green.
Strict equality (NOT subset) chosen explicitly to ensure invalidation on additive axis drift: if a future patch introduces a 4th fingerprint axis, every existing cache entry will invalidate on the next fingerprint-aware lookup until re-saved with the new axis. Subset semantics would silently honor stale entries — rejected.
■ remaining_units
[u4, u5, u6]
■ follow_up_issue_candidates
None. u3 is scope-locked to read-side comparator + tests; fingerprint computation (axis SHA derivation from live contract YAML / partial templates / catalog payloads) is u4's responsibility per Stage 2 plan and stays out of cache.py by design. No new axis discovered.
[Claude #3] Stage 3 code-edit IMP-46 — u3 fingerprint invalidation
Executing unit: u3
■ unit_executed
- id: u3
- summary: Add fingerprint-based read-side invalidation to `src/phase_z2_ai_fallback/cache.py`. `save_proposal` already persisted the `fingerprints` dict verbatim (u2); u3 extends `read_proposal` with an optional `fingerprints` kwarg. When supplied, the stored `fingerprints` dict 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 no `fingerprints` field) returns `None`. Default `fingerprints=None` performs no comparison (back-compat). Fingerprint *computation* stays outside `cache.py`: the module is a content-addressed *comparator*, not a *hasher* — preserves AI isolation and keeps the cache schema-agnostic.
■ files_changed
- src/phase_z2_ai_fallback/cache.py (modified, +47 / -10 — module docstring expanded with u3 contract; `read_proposal` gains keyword-only `fingerprints: dict | None = None`)
- tests/phase_z2_ai_fallback/test_cache_invalidation.py (new, 21 test cases, 305 SLOC)
■ diff_summary
- `src/phase_z2_ai_fallback/cache.py`
- Module docstring header renamed `u2` → `u2 + u3`; new section "u3 invalidation contract (this module is a *comparator*, not a *computer*)" documents:
- `save_proposal` persists `fingerprints` verbatim — no fingerprint axis names are hardcoded in cache.py
- `read_proposal(fingerprints=...)` enforces strict equality (NOT subset); default `None` skips comparison
- Rationale for keeping computation outside: AI isolation + schema-agnostic (new fingerprint axes don't require editing cache.py)
- `read_proposal` signature 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.
- New input validation: `fingerprints is not None and not isinstance(fingerprints, dict)` → `TypeError` (symmetric with `save_proposal`'s fingerprints validation).
- New comparison block placed AFTER file existence + JSON load + non-dict-root checks, but BEFORE the `proposal` validation: if `fingerprints is not None` and `stored = data.get("fingerprints")` is not a dict OR `stored != fingerprints` → return `None`. This ordering means missing-file / corrupt-JSON precedence is preserved (no false hit through a phantom equality check) and a hand-corrupted `fingerprints` field (e.g. serialized as a list) is treated as an invalidation, not as a `TypeError`.
- No new module-level imports. No new constants. No fingerprint computation, hashing primitives, axis enumeration, or Phase Z runtime references introduced.
- `tests/phase_z2_ai_fallback/test_cache_invalidation.py` (new — 21 cases, all pass)
- Save-side: `test_save_persists_fingerprints_verbatim` re-asserts the u2 round-trip foundation u3 depends on.
- Back-compat (read without kwarg):
- `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.
- Matching path:
- `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.
- Invalidation (the 11-case core):
- Parametrized over the three declared shas: `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 (no `fingerprints` field at all) invalidates under fingerprint-aware lookup.
- Precedence locks:
- `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.
- Input validation symmetry: `test_read_rejects_non_dict_fingerprints[list|string|int]` (3 cases) — TypeError matches save-side surface.
- Module isolation lock: `test_cache_module_has_no_fingerprint_computer` — public surface of `cache.py` is grepped for `hash`/`sha`/`fingerprint` substrings; 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.
- Cross-signature isolation: `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).
- No `src/**` files touched outside `src/phase_z2_ai_fallback/cache.py`.
- AST isolation guard re-verified: no new top-level imports added; the existing `hashlib` whitelist from u1 is NOT used by cache.py (u3 does not import `hashlib` — comparison is plain `dict` equality, not hash recomputation).
■ guardrail / contract evidence
- AI isolation contract: `cache.py` still imports only `json`, `pathlib`, and `src.phase_z2_ai_fallback.schema`. No Anthropic, no MDX, no Phase Z runtime references.
- No-hardcoding: the three sha axis names (`contract_sha` / `partial_sha` / `catalog_sha`) do NOT appear in `cache.py` source — they are caller-supplied dict keys; cache.py only compares dicts. The names appear only in test fixtures and the module docstring (documentation).
- PZ-1 (normal-path AI=0): unchanged — `read_proposal` performs no AI invocation.
- Both write gates (`visual_check_passed` AND `user_approved`) preserved untouched. u3 only modifies the read path.
- Gate precedence preserved: u2's `test_save_gate_violation_does_not_touch_filesystem` and the 33-case u2 suite all still green.
- Strict equality (NOT subset) chosen explicitly to ensure invalidation on additive axis drift: if a future patch introduces a 4th fingerprint axis, every existing cache entry will invalidate on the next fingerprint-aware lookup until re-saved with the new axis. Subset semantics would silently honor stale entries — rejected.
■ remaining_units
- [u4, u5, u6]
■ follow_up_issue_candidates
- None. u3 is scope-locked to read-side comparator + tests; fingerprint computation (axis SHA derivation from live contract YAML / partial templates / catalog payloads) is u4's responsibility per Stage 2 plan and stays out of `cache.py` by design. No new axis discovered.
Scope: verified only the single executed unit, u3.
Verdict: u3 is correct. read_proposal now accepts a keyword-only fingerprints comparator, strict equality invalidates mismatched cache entries, missing/corrupt/stale payloads still return None, and save_proposal continues to persist caller-supplied fingerprints without computing them inside cache.py. The cache module remains a storage/comparison layer and does not invoke AI or add Phase Z runtime coupling.
PowerShell Python introspection for read_proposal and save_proposal signatures
git status --short
Files checked:
src/phase_z2_ai_fallback/cache.py
tests/phase_z2_ai_fallback/test_cache_invalidation.py
Test results:
tests/phase_z2_ai_fallback/test_cache_invalidation.py: 20 passed in 0.09s
Verified facts:
read_proposal signature is (key: str, *, fingerprints: dict | None = None) -> AiFallbackProposal | None; the new parameter is keyword-only.
save_proposal still accepts fingerprints: dict | None = None and stores dict(fingerprints) in the JSON payload.
When caller fingerprints are supplied, stored fingerprints must be a dict and must equal the caller dict exactly; mismatch, missing field, non-dict stored value, missing file, or corrupt JSON returns None.
fingerprints=None on read skips comparison for back-compat.
Non-dict caller fingerprints raise TypeError.
The targeted grep found no hashlib, sha256, fingerprint computation function, Anthropic/Claude import, MDX sample IDs, source_section_ids, mdx_path, or template_id leakage in cache.py; fingerprint axis names appear only as documented opaque keys/test fixtures.
The working tree contains many pre-existing unrelated changes; u3 verification was limited to cache.py and test_cache_invalidation.py.
Remaining units: [u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Codex #3] Stage 3 code-edit review IMP-46 u3
Scope: verified only the single executed unit, u3.
Verdict: u3 is correct. `read_proposal` now accepts a keyword-only `fingerprints` comparator, strict equality invalidates mismatched cache entries, missing/corrupt/stale payloads still return `None`, and `save_proposal` continues to persist caller-supplied fingerprints without computing them inside `cache.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.py`
- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_cache_invalidation.py`
- `git diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_cache_invalidation.py -q`
- `rg -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.py`
- PowerShell Python introspection for `read_proposal` and `save_proposal` signatures
- `git status --short`
Files checked:
- `src/phase_z2_ai_fallback/cache.py`
- `tests/phase_z2_ai_fallback/test_cache_invalidation.py`
Test results:
- `tests/phase_z2_ai_fallback/test_cache_invalidation.py`: 20 passed in 0.09s
Verified facts:
- `read_proposal` signature is `(key: str, *, fingerprints: dict | None = None) -> AiFallbackProposal | None`; the new parameter is keyword-only.
- `save_proposal` still accepts `fingerprints: dict | None = None` and stores `dict(fingerprints)` in the JSON payload.
- When caller fingerprints are supplied, stored fingerprints must be a dict and must equal the caller dict exactly; mismatch, missing field, non-dict stored value, missing file, or corrupt JSON returns `None`.
- `fingerprints=None` on read skips comparison for back-compat.
- Non-dict caller fingerprints raise `TypeError`.
- The targeted grep found no `hashlib`, `sha256`, fingerprint computation function, Anthropic/Claude import, MDX sample IDs, `source_section_ids`, `mdx_path`, or `template_id` leakage in `cache.py`; fingerprint axis names appear only as documented opaque keys/test fixtures.
- The working tree contains many pre-existing unrelated changes; u3 verification was limited to `cache.py` and `test_cache_invalidation.py`.
Remaining units: [u4, u5, u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
summary: Replace the sample-identity cache_key (template_id::sorted(source_section_ids)) in src/phase_z2_ai_fallback/step12.py with a structural signature key "{frame_id}::{signature_hash}" derived from the 8 IMP-46 axes via build_signature (u1). Extend Step 12 helper inputs (unit attributes) for layout_preset, zone_position, source_shape, h3_count, char_count, and cardinality; project char_count through bucket_char_count (u1). Compute the three invalidation fingerprints (contract_sha, partial_sha, catalog_sha) and attach them — with the new cache_key — to each AI-eligible record for downstream save_proposal use. Exact-match lookup only (no fuzzy fallback added). v4_result.cardinality now mirrors the unit's cardinality (was hardcoded None).
tests/phase_z2_ai_fallback/test_step12.py (modified, +247 / -16 — replaces the legacy test_cache_key_includes_template_and_section_ids test with 14 new u4 cases; FakeUnit extended with 6 new signature-input fields)
■ diff_summary
src/phase_z2_ai_fallback/step12.py
Module docstring expanded with an IMP-46 u4 — structural cache key + fingerprints section 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).
New stdlib imports: hashlib, json (both already in the AST isolation whitelist — test_ast_isolation.py:39-42).
New intra-package import: bucket_char_count, build_signature from src.phase_z2_ai_fallback.signature (u1).
New module-level helper _sha256_of(payload: Any) -> str: deterministic SHA256 over json.dumps(payload, sort_keys=True, ensure_ascii=False). Used only for contract_sha and partial_sha.
gather_step12_ai_repair_proposals signature gains one new keyword-only argument:
catalog_sha_loader: Callable[[], str] | None = None — called once per gather invocation (verified by test_catalog_sha_loader_called_once_per_gather). When None, catalog_sha defaults to "" (sentinel — always present, so fingerprints is always a 3-key dict).
Record schema gains two fields, both initialised to 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 left None for not_provisional / design_reference_only_no_ai / route_not_ai_adaptation:* records.
"fingerprints": dict | None — same population rule.
Inside the AI-eligible branch (after route gates pass):
Read signature inputs from unit attributes via getattr with 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" — valid SourceShape enum member), h3_count (default 0), char_count (default 0).
v4_result["cardinality"] now reads the unit's cardinality attribute instead of the hardcoded None from 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.py
Module docstring updated to declare the IMP-46 u4 coverage axis alongside the IMP-33 gates.
FakeUnit dataclass 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 construct FakeUnit(label=..., provisional=...) without modification.
New helper _ai_unit(**overrides): builds an AI-eligible (provisional=True, label="restructure") FakeUnit with realistic signature axes — keeps the u4 test bodies readable without mutating the existing test surface.
Legacy test_cache_key_includes_template_and_section_ids REMOVED — it asserted the broken template_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.
Existing test_record_shape_contract_is_stable renamed to test_record_shape_contract_is_stable_with_u4_fields and updated to assert exactly 12 keys (the original 10 + cache_key + fingerprints).
14 new u4 cases:
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 same cache_key (no sample leakage).
test_cache_key_invariant_to_template_id_changes — frame_template_id is NOT in the signature surface (only frame_id is).
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 mutates cache_key. char_count=500 is chosen specifically to cross the 151-400 → 401-1000 bucket boundary (verifies bucketing, not raw count).
test_char_count_bucket_collapses_within_bucket — char_count=160 and char_count=399 both fall in "151-400" and produce identical keys; char_count=401 differs.
test_fingerprints_attached_to_ai_record — fingerprints dict has exactly the 3 declared keys, all string values; contract_sha and partial_sha match an inline recomputation of hashlib.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 — no catalog_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} mutates contract_sha, leaves partial_sha unchanged.
test_fingerprints_change_when_partial_changes — symmetric for figma_partial_json.
test_v4_result_cardinality_uses_unit_value — unit cardinality=7 flows into v4_result["cardinality"]=7; cardinality=None stays None. Closes the IMP-33 u8 hardcoded-None gap noted in Stage 1.
test_skipped_records_have_no_cache_key_or_fingerprints — not_provisional, reject, and light_edit paths all keep cache_key=None and fingerprints=None.
test_catalog_sha_loader_called_once_per_gather — MagicMock loader 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 produced cache_key through cache._parse_key; asserts frame_id == "fid_123", len(signature_hash) == 64, no extra KEY_DELIMITER. Cross-module structural lock — the u4 producer and the u2 consumer agree on the key shape.
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).
Grep audit of src/phase_z2_ai_fallback/step12.py for source_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 to build_signature), or the template_id variable that is used only as a key for the existing get_contract_fn / frame_visual_loader / figma_partial_loader callbacks. template_id and source_section_ids do NOT appear in build_signature(...) arguments or in the cache_key f-string. No-hardcoding lock honored.
AI isolation contract: no new module imports outside the existing whitelist; hashlib and json were already allowed by test_ast_isolation.py:39-42. No Anthropic/MDX/Phase Q/Phase Z2 runtime imports added.
■ remaining_units
[u5, u6]
■ scope_lock observations (NOT acted on)
Router-side read fingerprint validation: the router's existing call is read_proposal(cache_key) — it does NOT yet pass the new fingerprints dict to read_proposal (the u3 invalidation comparator is therefore inert through the router today). u4's files list (per Stage 2 plan) is step12.py + test_step12.py only; router.py is 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
router read-side fingerprint validation — src/phase_z2_ai_fallback/router.py:66 calls read_proposal(cache_key) without fingerprints=. To complete the u3 invalidation chain end-to-end (so a stale cache entry with changed contract_sha / partial_sha / catalog_sha misses through the router), the router needs to accept a fingerprints kwarg from gather_step12_ai_repair_proposals and forward it to read_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 source-of-truth — catalog_sha_loader is a Callable[[], str]. The actual catalog content (Step 2 plan says the invalidation trigger is templates/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).
[Claude #4] Stage 3 code-edit IMP-46 — u4 structural cache key + fingerprints
Executing unit: u4
■ unit_executed
- id: u4
- summary: Replace the sample-identity cache_key (`template_id::sorted(source_section_ids)`) in `src/phase_z2_ai_fallback/step12.py` with a structural signature key `"{frame_id}::{signature_hash}"` derived from the 8 IMP-46 axes via `build_signature` (u1). Extend Step 12 helper inputs (unit attributes) for `layout_preset`, `zone_position`, `source_shape`, `h3_count`, `char_count`, and `cardinality`; project `char_count` through `bucket_char_count` (u1). Compute the three invalidation fingerprints (`contract_sha`, `partial_sha`, `catalog_sha`) and attach them — with the new `cache_key` — to each AI-eligible record for downstream `save_proposal` use. Exact-match lookup only (no fuzzy fallback added). v4_result.cardinality now mirrors the unit's cardinality (was hardcoded `None`).
■ files_changed
- src/phase_z2_ai_fallback/step12.py (modified, +82 / -2 — adds `_sha256_of` helper, `catalog_sha_loader` kwarg, structural signature + fingerprint block, record fields `cache_key` + `fingerprints`)
- tests/phase_z2_ai_fallback/test_step12.py (modified, +247 / -16 — replaces the legacy `test_cache_key_includes_template_and_section_ids` test with 14 new u4 cases; FakeUnit extended with 6 new signature-input fields)
■ diff_summary
- `src/phase_z2_ai_fallback/step12.py`
- Module docstring expanded with an `IMP-46 u4 — structural cache key + fingerprints` section 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).
- New stdlib imports: `hashlib`, `json` (both already in the AST isolation whitelist — `test_ast_isolation.py:39-42`).
- New intra-package import: `bucket_char_count`, `build_signature` from `src.phase_z2_ai_fallback.signature` (u1).
- New module-level helper `_sha256_of(payload: Any) -> str`: deterministic SHA256 over `json.dumps(payload, sort_keys=True, ensure_ascii=False)`. Used only for `contract_sha` and `partial_sha`.
- `gather_step12_ai_repair_proposals` signature gains one new keyword-only argument:
- `catalog_sha_loader: Callable[[], str] | None = None` — called once per gather invocation (verified by `test_catalog_sha_loader_called_once_per_gather`). When `None`, `catalog_sha` defaults to `""` (sentinel — always present, so `fingerprints` is always a 3-key dict).
- Record schema gains two fields, both initialised to `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 left `None` for `not_provisional` / `design_reference_only_no_ai` / `route_not_ai_adaptation:*` records.
- `"fingerprints": dict | None` — same population rule.
- Inside the AI-eligible branch (after route gates pass):
- Read signature inputs from unit attributes via `getattr` with 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"` — valid `SourceShape` enum member), `h3_count` (default `0`), `char_count` (default `0`).
- `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_key` format (`KEY_DELIMITER = "::"`); validated by `test_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's `cardinality` attribute instead of the hardcoded `None` from 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.py`
- Module docstring updated to declare the IMP-46 u4 coverage axis alongside the IMP-33 gates.
- `FakeUnit` dataclass 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 construct `FakeUnit(label=..., provisional=...)` without modification.
- New helper `_ai_unit(**overrides)`: builds an AI-eligible (`provisional=True`, `label="restructure"`) `FakeUnit` with realistic signature axes — keeps the u4 test bodies readable without mutating the existing test surface.
- Legacy `test_cache_key_includes_template_and_section_ids` REMOVED — it asserted the broken `template_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.
- Existing `test_record_shape_contract_is_stable` renamed to `test_record_shape_contract_is_stable_with_u4_fields` and updated to assert exactly 12 keys (the original 10 + `cache_key` + `fingerprints`).
- 14 new u4 cases:
- `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 same `cache_key` (no sample leakage).
- `test_cache_key_invariant_to_template_id_changes` — `frame_template_id` is NOT in the signature surface (only `frame_id` is).
- `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 mutates `cache_key`. `char_count=500` is chosen specifically to cross the `151-400` → `401-1000` bucket boundary (verifies bucketing, not raw count).
- `test_char_count_bucket_collapses_within_bucket` — `char_count=160` and `char_count=399` both fall in `"151-400"` and produce identical keys; `char_count=401` differs.
- `test_fingerprints_attached_to_ai_record` — fingerprints dict has exactly the 3 declared keys, all string values; `contract_sha` and `partial_sha` match an inline recomputation of `hashlib.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` — no `catalog_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}` mutates `contract_sha`, leaves `partial_sha` unchanged.
- `test_fingerprints_change_when_partial_changes` — symmetric for `figma_partial_json`.
- `test_v4_result_cardinality_uses_unit_value` — unit `cardinality=7` flows into `v4_result["cardinality"]=7`; `cardinality=None` stays `None`. Closes the IMP-33 u8 hardcoded-`None` gap noted in Stage 1.
- `test_skipped_records_have_no_cache_key_or_fingerprints` — `not_provisional`, `reject`, and `light_edit` paths all keep `cache_key=None` and `fingerprints=None`.
- `test_catalog_sha_loader_called_once_per_gather` — `MagicMock` loader 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 produced `cache_key` through `cache._parse_key`; asserts `frame_id == "fid_123"`, `len(signature_hash) == 64`, no extra `KEY_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).
- `Grep` audit of `src/phase_z2_ai_fallback/step12.py` for `source_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 to `build_signature`), or the `template_id` variable that is used **only** as a key for the existing `get_contract_fn` / `frame_visual_loader` / `figma_partial_loader` callbacks. **`template_id` and `source_section_ids` do NOT appear in `build_signature(...)` arguments or in the `cache_key` f-string.** No-hardcoding lock honored.
- AI isolation contract: no new module imports outside the existing whitelist; `hashlib` and `json` were already allowed by `test_ast_isolation.py:39-42`. No Anthropic/MDX/Phase Q/Phase Z2 runtime imports added.
■ remaining_units
- [u5, u6]
■ scope_lock observations (NOT acted on)
- Router-side read fingerprint validation: the router's existing call is `read_proposal(cache_key)` — it does NOT yet pass the new `fingerprints` dict to `read_proposal` (the u3 invalidation comparator is therefore inert through the router today). u4's files list (per Stage 2 plan) is `step12.py + test_step12.py` only; `router.py` is 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
- **router read-side fingerprint validation** — `src/phase_z2_ai_fallback/router.py:66` calls `read_proposal(cache_key)` without `fingerprints=`. To complete the u3 invalidation chain end-to-end (so a stale cache entry with changed `contract_sha` / `partial_sha` / `catalog_sha` misses through the router), the router needs to accept a `fingerprints` kwarg from `gather_step12_ai_repair_proposals` and forward it to `read_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 source-of-truth** — `catalog_sha_loader` is a `Callable[[], str]`. The actual catalog content (Step 2 plan says the invalidation trigger is `templates/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).
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 into v4_result, computes the three invalidation fingerprints outside cache.py, and attaches cache_key plus fingerprints only 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.
tests/phase_z2_ai_fallback/test_ast_isolation.py
Test results:
tests/phase_z2_ai_fallback/test_step12.py: 21 passed in 0.04s
tests/phase_z2_ai_fallback/test_ast_isolation.py: 22 passed in 0.12s
u1-u4 focused regression set (test_signature.py, test_cache.py, test_cache_invalidation.py, test_step12.py): 94 passed in 0.25s
Verified facts:
gather_step12_ai_repair_proposals accepts catalog_sha_loader and calls it once per gather invocation when supplied.
AI-eligible records receive cache_key and fingerprints; skipped records keep both fields as None.
The generated cache key uses the frame id prefix plus a 64-character lowercase SHA256 digest and round-trips through cache._parse_key.
frame_template_id and source_section_ids no longer influence the generated cache key; changing those values with the same structural axes preserves the key.
Changing structural axes changes the key, including a focused check for the v4_label axis with routing held constant.
char_count is bucketed before signature construction; values within the same bucket preserve the key and crossing a bucket boundary changes it.
Fingerprints contain exactly contract_sha, partial_sha, and catalog_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 to None.
The targeted grep found no MDX sample IDs or mdx_path leakage in the u4 Step 12 files; legacy template_id / source_section_ids references 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
[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 into `v4_result`, computes the three invalidation fingerprints outside `cache.py`, and attaches `cache_key` plus `fingerprints` only 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.py`
- `Get-Content -LiteralPath tests\\phase_z2_ai_fallback\\test_step12.py`
- `git diff -- src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.py`
- `rg -n "gather_step12_ai_repair_proposals\\(" src tests -g "*.py"`
- `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`
- `rg -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.py`
- PowerShell Python focused check that changing `v4_label` mutates the generated cache key while preserving the AI route.
- `python -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -q`
- `python -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 -q`
Files checked:
- `src/phase_z2_ai_fallback/step12.py`
- `tests/phase_z2_ai_fallback/test_step12.py`
- `tests/phase_z2_ai_fallback/test_ast_isolation.py`
Test results:
- `tests/phase_z2_ai_fallback/test_step12.py`: 21 passed in 0.04s
- `tests/phase_z2_ai_fallback/test_ast_isolation.py`: 22 passed in 0.12s
- u1-u4 focused regression set (`test_signature.py`, `test_cache.py`, `test_cache_invalidation.py`, `test_step12.py`): 94 passed in 0.25s
Verified facts:
- `gather_step12_ai_repair_proposals` accepts `catalog_sha_loader` and calls it once per gather invocation when supplied.
- AI-eligible records receive `cache_key` and `fingerprints`; skipped records keep both fields as `None`.
- The generated cache key uses the frame id prefix plus a 64-character lowercase SHA256 digest and round-trips through `cache._parse_key`.
- `frame_template_id` and `source_section_ids` no longer influence the generated cache key; changing those values with the same structural axes preserves the key.
- Changing structural axes changes the key, including a focused check for the `v4_label` axis with routing held constant.
- `char_count` is bucketed before signature construction; values within the same bucket preserve the key and crossing a bucket boundary changes it.
- Fingerprints contain exactly `contract_sha`, `partial_sha`, and `catalog_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 to `None`.
- The targeted grep found no MDX sample IDs or `mdx_path` leakage in the u4 Step 12 files; legacy `template_id` / `source_section_ids` references 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
id: u5
summary: Wire IMP-46 u5 auto-cache opt-in across three layers — (1) add ai_fallback_auto_cache: bool = False to src/config.py Settings; (2) extend src/phase_z2_ai_fallback/cache.py::save_proposal with a keyword-only auto_cache: bool = False parameter that bypasses ONLY the user_approved gate (visual_check_passed remains mandatory and non-bypassable); (3) add --auto-cache CLI flag in src/phase_z2_pipeline.py that mutates settings.ai_fallback_auto_cache=True at parse time. The 2^3 gate truth table over (visual_check_passed, user_approved, auto_cache) is now exhaustively enumerated in tests/phase_z2_ai_fallback/test_cache.py with exactly three persisting rows: (T,T,F), (T,T,T), (T,F,T). Every other row raises AiFallbackCacheGateError before any filesystem touch.
New field ai_fallback_auto_cache: bool = False placed alongside the existing IMP-33 u1 ai_fallback_* block. Docstring explains the gate semantics ("bypasses user_approved only; visual_check_passed never bypassable") and locks the default OFF + CLI mutation contract.
src/phase_z2_ai_fallback/cache.py
Module docstring header: IMP-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.
Guardrails block updated: "Both write gates preserved — visual_check_passed=False always raises … user_approved=False also raises by default; the IMP-46 u5 auto_cache=True override bypasses ONLY the user_approved gate (visual_check_passed is never bypassed). Gate violation never silently no-ops."
save_proposal signature gains keyword-only auto_cache: bool = False at the end of the kwargs block (after fingerprints). Position-only callers cannot accidentally pass it.
New type-check: if not isinstance(auto_cache, bool): raise TypeError("auto_cache must be a bool"). Symmetric with slide_css / fingerprints validation.
Gate logic change — single line:
Before: if not user_approved: raise AiFallbackCacheGateError(...)
After: if not user_approved and not auto_cache: raise AiFallbackCacheGateError(...)
The visual_check_passed gate is unchanged — auto_cache does 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).
No new imports. No src.config import inside cache.py — the cache module remains a pure comparator/storage layer that takes the resolved boolean from the caller (preserves AI isolation contract tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69).
src/phase_z2_pipeline.py
New --auto-cache flag added to the existing if __name__ == "__main__" argparse block (after --override-section-assignment, before args = parser.parse_args()). action="store_true", default=False. Help text states the gate semantics + setting wiring.
After parse_args(): a 3-line conditional imports settings from src.config and sets ai_fallback_auto_cache=True when the flag is present. The deferred import (inside if args.auto_cache:) keeps src.config off the unconditional pipeline import surface — only operators who opt in pull it in.
The mutation is in-process only — no .env write, no side effects between runs. A subsequent pipeline run without --auto-cache starts back at the Settings default (False).
tests/phase_z2_ai_fallback/test_cache.py
New section header "IMP-46 u5: auto_cache gate (2^3 truth table)".
_GATE_TRUTH_TABLE constant — 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_table parametrised — for each row, either calls save_proposal and asserts the file exists (persist row), or asserts AiFallbackCacheGateError raises AND the frame_id directory is absent (gate row — symmetric with the existing test_save_gate_violation_does_not_touch_filesystem).
test_auto_cache_default_off_preserves_dual_gate_semantics — calling without the auto_cache kwarg keeps the IMP-46 u2 behaviour (user_approved=False raises 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 mentions visual_check_passed (regression sentinel against accidentally moving auto_cache into the visual-check branch).
test_auto_cache_bypass_user_approved_persists — (V=True, U=False, A=True) round-trip: persists, file exists, read_proposal returns the original payload.
test_auto_cache_rejects_non_bool — passing auto_cache="yes" raises TypeError (symmetric with the existing non-string / non-dict guards for slide_css and fingerprints).
test_auto_cache_is_keyword_only — inspect.signature(save_proposal).parameters["auto_cache"] is KEYWORD_ONLY and default False. Locks the public surface so a future refactor can't accidentally make it positional or default-True.
tests/test_phase_z2_ai_fallback_config.py
New section explaining the u5 CLI / settings contract.
test_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.
python -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.59s
python -m src.phase_z2_pipeline --help → --auto-cache flag surfaced with the documented help text
AST isolation guard re-confirmed: cache.py imports unchanged (json, pathlib, src.phase_z2_ai_fallback.schema); no src.config import inside src/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 contract tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69)
✅ No save_proposal call site changes outside tests — there is no production save_proposal invocation yet (verified by rg "save_proposal\\(" src — only cache.py definition + docstring references in router.py).
✅visual_check_passed gate is non-bypassable — auto_cache only relaxes user_approved. Truth-table row (V=False, U=True, A=True) raises, locked by test_auto_cache_cannot_bypass_visual_check.
✅ Settings default OFF — Settings().ai_fallback_auto_cache is False (test_ai_fallback_auto_cache_default_off). Honours feedback_ai_isolation_contract (auto-cache is an explicit operator opt-in, not silent).
✅ No-hardcoding lock — no sample / MDX / template_id specific literal added. auto_cache is a pure axis of the cache write gate.
■ remaining_units
[u6] — Track data/frame_cache/.gitkeep via .gitignore exception under the existing data/ ignore, add repo-layout coverage (tests/phase_z2_ai_fallback/test_cache_repo_layout.py), and refresh docs/architecture/IMP-17-CARVE-OUT.md cache row from stub status → persistent backend status.
■ follow_up_issue_candidates
None. u5 scope-lock held; no axis discovered outside (V, U, A) × cache write surface.
[Claude #5] Stage 3 code-edit IMP-46 — u5 auto-cache CLI flag + 2^3 gate truth table
Executing unit: u5
■ unit_executed
- id: u5
- summary: Wire IMP-46 u5 auto-cache opt-in across three layers — (1) add `ai_fallback_auto_cache: bool = False` to `src/config.py` Settings; (2) extend `src/phase_z2_ai_fallback/cache.py::save_proposal` with a keyword-only `auto_cache: bool = False` parameter that bypasses ONLY the `user_approved` gate (`visual_check_passed` remains mandatory and non-bypassable); (3) add `--auto-cache` CLI flag in `src/phase_z2_pipeline.py` that mutates `settings.ai_fallback_auto_cache=True` at parse time. The 2^3 gate truth table over `(visual_check_passed, user_approved, auto_cache)` is now exhaustively enumerated in `tests/phase_z2_ai_fallback/test_cache.py` with exactly three persisting rows: `(T,T,F)`, `(T,T,T)`, `(T,F,T)`. Every other row raises `AiFallbackCacheGateError` before any filesystem touch.
■ files_changed
- src/config.py (+8 lines — `ai_fallback_auto_cache: bool = False` Settings field + docstring)
- src/phase_z2_ai_fallback/cache.py (+~30/-10 lines — `save_proposal(auto_cache=False)` keyword-only param, docstring expanded with u5 contract section, module docstring `u2 + u3` → `u2 + u3 + u5`)
- src/phase_z2_pipeline.py (+~20 lines — `--auto-cache` argparse flag + in-process `settings.ai_fallback_auto_cache=True` mutation)
- tests/phase_z2_ai_fallback/test_cache.py (+~110 lines — 8-row parametrised truth-table test + 5 targeted u5 cases)
- tests/test_phase_z2_ai_fallback_config.py (+~12 lines — `test_ai_fallback_auto_cache_default_off`)
■ diff_summary
- `src/config.py`
- New field `ai_fallback_auto_cache: bool = False` placed alongside the existing IMP-33 u1 ai_fallback_* block. Docstring explains the gate semantics ("bypasses `user_approved` only; `visual_check_passed` never bypassable") and locks the default OFF + CLI mutation contract.
- `src/phase_z2_ai_fallback/cache.py`
- Module docstring header: `IMP-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.
- Guardrails block updated: "Both write gates preserved — `visual_check_passed=False` always raises … `user_approved=False` also raises by default; the IMP-46 u5 `auto_cache=True` override bypasses ONLY the `user_approved` gate (`visual_check_passed` is never bypassed). Gate violation never silently no-ops."
- `save_proposal` signature gains keyword-only `auto_cache: bool = False` at the end of the kwargs block (after `fingerprints`). Position-only callers cannot accidentally pass it.
- New type-check: `if not isinstance(auto_cache, bool): raise TypeError("auto_cache must be a bool")`. Symmetric with `slide_css` / `fingerprints` validation.
- Gate logic change — single line:
- Before: `if not user_approved: raise AiFallbackCacheGateError(...)`
- After: `if not user_approved and not auto_cache: raise AiFallbackCacheGateError(...)`
- The `visual_check_passed` gate is unchanged — `auto_cache` does 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).
- No new imports. No `src.config` import inside cache.py — the cache module remains a pure comparator/storage layer that takes the resolved boolean from the caller (preserves AI isolation contract `tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69`).
- `src/phase_z2_pipeline.py`
- New `--auto-cache` flag added to the existing `if __name__ == "__main__"` argparse block (after `--override-section-assignment`, before `args = parser.parse_args()`). `action="store_true"`, `default=False`. Help text states the gate semantics + setting wiring.
- After `parse_args()`: a 3-line conditional imports `settings` from `src.config` and sets `ai_fallback_auto_cache=True` when the flag is present. The deferred import (inside `if args.auto_cache:`) keeps `src.config` off the unconditional pipeline import surface — only operators who opt in pull it in.
- The mutation is in-process only — no .env write, no side effects between runs. A subsequent pipeline run without `--auto-cache` starts back at the Settings default (`False`).
- `tests/phase_z2_ai_fallback/test_cache.py`
- New section header "IMP-46 u5: auto_cache gate (2^3 truth table)".
- `_GATE_TRUTH_TABLE` constant — 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_table` parametrised — for each row, either calls `save_proposal` and asserts the file exists (persist row), or asserts `AiFallbackCacheGateError` raises AND the frame_id directory is absent (gate row — symmetric with the existing `test_save_gate_violation_does_not_touch_filesystem`).
- `test_auto_cache_default_off_preserves_dual_gate_semantics` — calling without the `auto_cache` kwarg keeps the IMP-46 u2 behaviour (`user_approved=False` raises 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 mentions `visual_check_passed` (regression sentinel against accidentally moving `auto_cache` into the visual-check branch).
- `test_auto_cache_bypass_user_approved_persists` — `(V=True, U=False, A=True)` round-trip: persists, file exists, `read_proposal` returns the original payload.
- `test_auto_cache_rejects_non_bool` — passing `auto_cache="yes"` raises `TypeError` (symmetric with the existing non-string / non-dict guards for `slide_css` and `fingerprints`).
- `test_auto_cache_is_keyword_only` — `inspect.signature(save_proposal).parameters["auto_cache"]` is `KEYWORD_ONLY` and default `False`. Locks the public surface so a future refactor can't accidentally make it positional or default-True.
- `tests/test_phase_z2_ai_fallback_config.py`
- New section explaining the u5 CLI / settings contract.
- `test_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.22s
- `python -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.59s
- `python -m src.phase_z2_pipeline --help` → `--auto-cache` flag surfaced with the documented help text
- AST isolation guard re-confirmed: cache.py imports unchanged (`json`, `pathlib`, `src.phase_z2_ai_fallback.schema`); no `src.config` import inside `src/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 contract `tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69`)
■ scope-lock verification
- ✅ u5 declared files only (config.py / cache.py / phase_z2_pipeline.py / test_cache.py / test_phase_z2_ai_fallback_config.py).
- ✅ No `route_ai_fallback` call site changes — IMP-33 AI hook untouched.
- ✅ No `read_proposal` signature change — u3 fingerprint comparator surface preserved.
- ✅ No `save_proposal` call site changes outside tests — there is no production `save_proposal` invocation yet (verified by `rg "save_proposal\\(" src` — only `cache.py` definition + docstring references in `router.py`).
- ✅ `visual_check_passed` gate is non-bypassable — `auto_cache` only relaxes `user_approved`. Truth-table row `(V=False, U=True, A=True)` raises, locked by `test_auto_cache_cannot_bypass_visual_check`.
- ✅ Settings default OFF — `Settings().ai_fallback_auto_cache is False` (`test_ai_fallback_auto_cache_default_off`). Honours `feedback_ai_isolation_contract` (auto-cache is an explicit operator opt-in, not silent).
- ✅ No-hardcoding lock — no sample / MDX / template_id specific literal added. `auto_cache` is a pure axis of the cache write gate.
■ remaining_units
- [u6] — Track `data/frame_cache/.gitkeep` via `.gitignore` exception under the existing `data/` ignore, add repo-layout coverage (`tests/phase_z2_ai_fallback/test_cache_repo_layout.py`), and refresh `docs/architecture/IMP-17-CARVE-OUT.md` cache row from stub status → persistent backend status.
■ follow_up_issue_candidates
- None. u5 scope-lock held; no axis discovered outside `(V, U, A)` × cache write surface.
FINAL_CONSENSUS_TARGET: continue_same (u6 remains)
Scope: verified only the single executed unit, u5.
Verdict: u5 is correct. Settings.ai_fallback_auto_cache defaults to False, save_proposal now accepts a keyword-only auto_cache: bool = False, and the gate logic preserves visual_check_passed as non-bypassable while allowing auto_cache=True to bypass only user_approved=False. The CLI adds --auto-cache as a store_true flag and mutates settings.ai_fallback_auto_cache=True after argument parsing. I did not find an added AI invocation or cache-module dependency on Phase Z runtime settings.
The truth table test covers all eight (visual_check_passed, user_approved, auto_cache) combinations; exactly (True, True, False), (True, True, True), and (True, False, True) persist.
visual_check_passed=False raises AiFallbackCacheGateError even when auto_cache=True, and rejected rows do not create the frame cache directory.
--auto-cache is registered with dest="auto_cache", action="store_true", and default=False.
When parsed args.auto_cache is true, src.phase_z2_pipeline mutates _settings.ai_fallback_auto_cache = True.
cache.py does not import src.config, Anthropic, or Phase Z runtime modules for this unit; callers still pass the resolved boolean explicitly.
The working tree contains many pre-existing unrelated changes; u5 verification was limited to the declared u5 file set and targeted isolation test.
Remaining units: [u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[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_cache` defaults to `False`, `save_proposal` now accepts a keyword-only `auto_cache: bool = False`, and the gate logic preserves `visual_check_passed` as non-bypassable while allowing `auto_cache=True` to bypass only `user_approved=False`. The CLI adds `--auto-cache` as a `store_true` flag and mutates `settings.ai_fallback_auto_cache=True` after 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.py`
- `Get-Content -LiteralPath src\phase_z2_ai_fallback\cache.py`
- `Get-Content -LiteralPath src\phase_z2_pipeline.py`
- `Get-Content -LiteralPath tests\phase_z2_ai_fallback\test_cache.py`
- `Get-Content -LiteralPath tests\test_phase_z2_ai_fallback_config.py`
- `rg -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.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py -q`
- PowerShell Python introspection for `save_proposal` signature and `Settings().ai_fallback_auto_cache`
- `python -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -q`
- `git 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`
- PowerShell Python AST check for the `--auto-cache` argparse entry and `settings.ai_fallback_auto_cache` mutation
- `git status --short`
Files checked:
- `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`
- `tests/phase_z2_ai_fallback/test_ast_isolation.py`
Test results:
- `tests/phase_z2_ai_fallback/test_cache.py` plus `tests/test_phase_z2_ai_fallback_config.py`: 51 passed in 0.14s
- `tests/phase_z2_ai_fallback/test_ast_isolation.py`: 22 passed in 0.14s
Verified facts:
- `Settings().ai_fallback_auto_cache is False`.
- `save_proposal` signature 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_cache` is keyword-only and defaults to `False`.
- Non-bool `auto_cache` raises `TypeError`.
- The truth table test covers all eight `(visual_check_passed, user_approved, auto_cache)` combinations; exactly `(True, True, False)`, `(True, True, True)`, and `(True, False, True)` persist.
- `visual_check_passed=False` raises `AiFallbackCacheGateError` even when `auto_cache=True`, and rejected rows do not create the frame cache directory.
- `--auto-cache` is registered with `dest="auto_cache"`, `action="store_true"`, and `default=False`.
- When parsed `args.auto_cache` is true, `src.phase_z2_pipeline` mutates `_settings.ai_fallback_auto_cache = True`.
- `cache.py` does not import `src.config`, Anthropic, or Phase Z runtime modules for this unit; callers still pass the resolved boolean explicitly.
- The working tree contains many pre-existing unrelated changes; u5 verification was limited to the declared u5 file set and targeted isolation test.
Remaining units: [u6]
rewind_target: continue_same
FINAL_CONSENSUS: NO
Commit 1186ad8 (#76 IMP-47B activation) absorbed substantially all of the originally-planned IMP-46 implementation_units. Per memory rule project_imp46_carveout_caveat, Stage 1 must enumerate what is already merged and bound new work to NON-overlapping territory only.
_persist_ai_repair_proposals_to_cache called after Step 14 visual check, threading visual_check_passed from overflow result and auto_cache from settings
src/phase_z2_pipeline.py:718-775, 5534-5550
Residual root cause (1 axis)
Axis R — Router read-path fingerprint enforcement gap.
route_ai_fallback(cache_key, ...) accepts no fingerprints parameter (src/phase_z2_ai_fallback/router.py:43-53).
Inside the router, cached = read_proposal(cache_key) is called WITHOUT fingerprints=... (src/phase_z2_ai_fallback/router.py:66).
Step 12 already computes the three fingerprints (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.
Self-acknowledged TODO at src/phase_z2_ai_fallback/step12.py:48-51: "the router's existing read_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-44 is enforceable only at the caller of read_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)
Re-stirring cache.py, signature.py, save-side pipeline wiring, --auto-cache semantics, .gitignore, or data/frame_cache/.gitkeep (all merged in 1186ad8 — see carve-out table).
AI call itself / prompt redesign (IMP-33 closed at c864fe0).
New route_ai_fallback call sites beyond current production site src/phase_z2_ai_fallback/step12.py:194-203.
Fuzzy match secondary tier (issue marks "옵션" — deferred per Stage 2 plan).
user_approved UX gate (documented design choice — pipeline relies on auto_cache opt-in per IMP-46 u5; see src/phase_z2_pipeline.py:5538-5542).
Step 17 cache integration (Step 17 entry is structurally blocked behind IMP-34/35 prerequisites per IMP-17-CARVE-OUT.md:52).
Proposed scope-lock (residual axis only)
Extend route_ai_fallback signature with an optional fingerprints: dict | None = None keyword-only kwarg. Default None preserves current router behavior and back-compat with existing test_router.py cases.
Pass fingerprints through to read_proposal(cache_key, fingerprints=fingerprints) (cache.py:108 already supports this kwarg; no cache.py edit needed).
Thread fingerprints from src/phase_z2_ai_fallback/step12.py:194-203route_ai_fallback(...) call. Source = the existing fingerprints dict already computed at step12.py:179-185. No new computation — pure reuse.
Update tests/phase_z2_ai_fallback/test_router.py with three new cases:
cache-hit with matching fingerprints → returns cached (validates short-circuit preserved)
cache-hit with mismatched fingerprints → read_proposal returns None → falls through to client path (validates strict-equality invalidation reaches production)
cache-hit with fingerprints=None (legacy back-compat) → returns cached without comparison (preserves existing router test surface)
Update tests/phase_z2_ai_fallback/test_step12.py to assert fingerprints is forwarded to route_ai_fallback (mock router, inspect kwargs).
No new modules, no new files. Touched files: 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)
carve-out caveat (project_imp46_carveout_caveat): files modified in 1186ad8 are NOT in scope for content rewrites. Only the call site in step12.py:194-203 (the route_ai_fallback(...) invocation) gets one kwarg added. No re-touch of the fingerprints computation block (step12.py:179-185).
AI isolation (feedback_ai_isolation_contract): cache layer adds no AI call; router still does not import save_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.
PZ-1 invariant: flag-off and route-mismatch short-circuits execute BEFORE fingerprint comparison (router lines 61-65 unchanged). AI=0 normal-path count remains 0.
No hardcoding (feedback_no_hardcoding): fingerprints sourced exclusively from existing step12.py compute (frame_contract / figma_partial_json / catalog_sha_loader). No new per-sample literals; no MDX section ids re-enter the signature surface.
Process-first: the gap is a missing thread, not a wrong value. Fix is "wire the kwarg," not "patch the symptom downstream."
Artifact-status naming (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.
1 turn = 1 step (feedback_one_step_per_turn): Stage 1 = problem-review comment only; no code edit, no plan unit enumeration. Stage 2 enumerates units.
Scope-qualified verification (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
q1: should route_ai_fallback's fingerprints kwarg be strictly keyword-only (recommended for forward compat) or positional? Default recommendation: keyword-only, matching the cache.py pattern (read_proposal(key, *, fingerprints=None) at cache.py:108-112).
q2: when fingerprints=None is passed AND a cache entry exists with stored non-empty fingerprints, current read_proposal behavior = return entry (no comparison). Stage 2 to confirm: keep legacy back-compat (router default None → 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 honor cache.py:38-44 strict-equality semantics (caller opt-in to invalidation).
q3: should the catalog_sha source be wired from step12.py:116-118 (current: optional catalog_sha_loader defaulting 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 until catalog_sha_loader is wired upstream, so invalidation behaves consistently within a release; tighten in a follow-up axis when B4 lands.
_persist_ai_repair_proposals_to_cache invocation: src/phase_z2_pipeline.py:5545-5550 (visual_check_passed from overflow.get("passed"), auto_cache from settings.ai_fallback_auto_cache, user_approved hardcoded False per documented design choice)
existing router tests that must continue to pass: tests/phase_z2_ai_fallback/test_router.py:68-157
carve-out doc cross-reference of merged surface: docs/architecture/IMP-17-CARVE-OUT.md:54
commit anchor for absorbed scope: 1186ad8
IMP-33 close anchor (AI call path): c864fe0
[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 rule `project_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):**
| Unit | Scope | Lands in |
|---|---|---|
| u1 | 8-axis structural signature (`build_signature`, `bucket_char_count`) | `src/phase_z2_ai_fallback/signature.py:1-92` |
| u2 | Persistent JSON backend at `data/frame_cache/{frame_id}/{signature_hash}.json` | `src/phase_z2_ai_fallback/cache.py:108-243` |
| u3 | Fingerprints invalidation (strict-equality over `contract_sha` / `partial_sha` / `catalog_sha`) | `src/phase_z2_ai_fallback/cache.py:38-44, 145-148` |
| u4 (compute side) | Step 12 builds structural cache_key + fingerprints from unit attributes | `src/phase_z2_ai_fallback/step12.py:160-185` |
| u5 | `--auto-cache` CLI + `settings.ai_fallback_auto_cache` (bypasses ONLY `user_approved`) | `src/config.py:29`, `src/phase_z2_pipeline.py:6025-6037`, `src/phase_z2_ai_fallback/cache.py:158-217` |
| u6 | `data/frame_cache/.gitkeep` + `.gitignore` payload exclusion + carve-out doc | `data/frame_cache/.gitkeep`, `.gitignore`, `docs/architecture/IMP-17-CARVE-OUT.md:54` |
| save-side wiring | `_persist_ai_repair_proposals_to_cache` called after Step 14 visual check, threading `visual_check_passed` from overflow result and `auto_cache` from settings | `src/phase_z2_pipeline.py:718-775, 5534-5550` |
# Residual root cause (1 axis)
**Axis R — Router read-path fingerprint enforcement gap.**
- `route_ai_fallback(cache_key, ...)` accepts no `fingerprints` parameter (`src/phase_z2_ai_fallback/router.py:43-53`).
- Inside the router, `cached = read_proposal(cache_key)` is called WITHOUT `fingerprints=...` (`src/phase_z2_ai_fallback/router.py:66`).
- Step 12 already computes the three fingerprints (`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.
- Self-acknowledged TODO at `src/phase_z2_ai_fallback/step12.py:48-51`: *"the router's existing `read_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-44` is enforceable only at the *caller* of `read_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)
- Re-stirring `cache.py`, `signature.py`, save-side pipeline wiring, `--auto-cache` semantics, `.gitignore`, or `data/frame_cache/.gitkeep` (all merged in `1186ad8` — see carve-out table).
- AI call itself / prompt redesign (IMP-33 closed at `c864fe0`).
- New `route_ai_fallback` call sites beyond current production site `src/phase_z2_ai_fallback/step12.py:194-203`.
- Fuzzy match secondary tier (issue marks "옵션" — deferred per Stage 2 plan).
- Cache → catalog promotion (R4 wave).
- `mdx_normalizer` Stage 0 integration (separate lock 2026-05-08).
- `user_approved` UX gate (documented design choice — pipeline relies on `auto_cache` opt-in per IMP-46 u5; see `src/phase_z2_pipeline.py:5538-5542`).
- Step 17 cache integration (Step 17 entry is structurally blocked behind IMP-34/35 prerequisites per `IMP-17-CARVE-OUT.md:52`).
# Proposed scope-lock (residual axis only)
- Extend `route_ai_fallback` signature with an optional `fingerprints: dict | None = None` keyword-only kwarg. Default `None` preserves current router behavior and back-compat with existing `test_router.py` cases.
- Pass `fingerprints` through to `read_proposal(cache_key, fingerprints=fingerprints)` (cache.py:108 already supports this kwarg; no `cache.py` edit needed).
- Thread `fingerprints` from `src/phase_z2_ai_fallback/step12.py:194-203` `route_ai_fallback(...)` call. Source = the existing `fingerprints` dict already computed at `step12.py:179-185`. No new computation — pure reuse.
- Update `tests/phase_z2_ai_fallback/test_router.py` with three new cases:
1. cache-hit with matching fingerprints → returns cached (validates short-circuit preserved)
2. cache-hit with mismatched fingerprints → `read_proposal` returns `None` → falls through to client path (validates strict-equality invalidation reaches production)
3. cache-hit with `fingerprints=None` (legacy back-compat) → returns cached without comparison (preserves existing router test surface)
- Update `tests/phase_z2_ai_fallback/test_step12.py` to assert `fingerprints` is forwarded to `route_ai_fallback` (mock router, inspect kwargs).
- No new modules, no new files. Touched files: `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)
- **carve-out caveat** (`project_imp46_carveout_caveat`): files modified in `1186ad8` are NOT in scope for content rewrites. Only the *call site* in `step12.py:194-203` (the `route_ai_fallback(...)` invocation) gets one kwarg added. No re-touch of the fingerprints computation block (`step12.py:179-185`).
- **AI isolation** (`feedback_ai_isolation_contract`): cache layer adds no AI call; router still does not import `save_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.
- **PZ-1 invariant**: flag-off and route-mismatch short-circuits execute BEFORE fingerprint comparison (router lines 61-65 unchanged). AI=0 normal-path count remains 0.
- **No hardcoding** (`feedback_no_hardcoding`): fingerprints sourced exclusively from existing `step12.py` compute (frame_contract / figma_partial_json / catalog_sha_loader). No new per-sample literals; no MDX section ids re-enter the signature surface.
- **Process-first**: the gap is a *missing thread*, not a wrong value. Fix is "wire the kwarg," not "patch the symptom downstream."
- **Artifact-status naming** (`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.
- **1 turn = 1 step** (`feedback_one_step_per_turn`): Stage 1 = problem-review comment only; no code edit, no plan unit enumeration. Stage 2 enumerates units.
- **Scope-qualified verification** (`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
- q1: should `route_ai_fallback`'s `fingerprints` kwarg be strictly keyword-only (recommended for forward compat) or positional? Default recommendation: **keyword-only**, matching the cache.py pattern (`read_proposal(key, *, fingerprints=None)` at `cache.py:108-112`).
- q2: when `fingerprints=None` is passed AND a cache entry exists with stored non-empty fingerprints, current `read_proposal` behavior = return entry (no comparison). Stage 2 to confirm: keep legacy back-compat (router default `None` → 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 honor `cache.py:38-44` strict-equality semantics (caller opt-in to invalidation).
- q3: should the `catalog_sha` source be wired from `step12.py:116-118` (current: optional `catalog_sha_loader` defaulting 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 until `catalog_sha_loader` is wired upstream, so invalidation behaves consistently within a release; tighten in a follow-up axis when B4 lands.
# Evidence
- router signature gap: `src/phase_z2_ai_fallback/router.py:43-66`
- step12 compute + missing thread: `src/phase_z2_ai_fallback/step12.py:160-185, 194-203`
- self-acknowledged TODO comment: `src/phase_z2_ai_fallback/step12.py:48-51`
- cache.py read-side fingerprints support already present: `src/phase_z2_ai_fallback/cache.py:108-148`
- save-side wiring already in production: `src/phase_z2_pipeline.py:5534-5550`
- save-side helper definition: `src/phase_z2_pipeline.py:718-775`
- `_persist_ai_repair_proposals_to_cache` invocation: `src/phase_z2_pipeline.py:5545-5550` (visual_check_passed from `overflow.get("passed")`, auto_cache from `settings.ai_fallback_auto_cache`, user_approved hardcoded False per documented design choice)
- existing router tests that must continue to pass: `tests/phase_z2_ai_fallback/test_router.py:68-157`
- carve-out doc cross-reference of merged surface: `docs/architecture/IMP-17-CARVE-OUT.md:54`
- commit anchor for absorbed scope: `1186ad8`
- IMP-33 close anchor (AI call path): `c864fe0`
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-148 already supports read_proposal(key, fingerprints=...) and invalidates on strict stored-vs-supplied mismatch. src/phase_z2_ai_fallback/step12.py:179-185 computes and records contract_sha, partial_sha, and catalog_sha. But src/phase_z2_ai_fallback/router.py:43-53 accepts no fingerprints argument and src/phase_z2_ai_fallback/router.py:66 calls read_proposal(cache_key) with the default fingerprints=None, which src/phase_z2_ai_fallback/cache.py:38-44 documents as no comparison. src/phase_z2_ai_fallback/step12.py:195-203 calls 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:
Add router read-path fingerprint enforcement only: extend route_ai_fallback to accept fingerprints: dict | None = None, pass it to read_proposal(cache_key, fingerprints=fingerprints), and thread record["fingerprints"] from Step 12 into the router call.
Add focused router/Step 12 regression coverage proving mismatched fingerprints miss cache and matching fingerprints hit cache through the router path.
Do not alter signature axes, JSON path format, save gates, auto-cache semantics, fuzzy lookup, AI request behavior, catalog promotion, or sample-specific logic.
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, and docs/architecture/IMP-17-CARVE-OUT.md. No pytest was run, per Stage 1 instruction.
line-window reads for 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, and src/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
src/phase_z2_ai_fallback/signature.py
src/phase_z2_pipeline.py
src/config.py
.gitignore
data/frame_cache/.gitkeep
docs/architecture/IMP-17-CARVE-OUT.md
Test results: not run; Stage 1 task explicitly says grep/find and NO pytest.
Verified facts:
read_proposal supports optional fingerprint comparison and returns None on mismatch when fingerprints are supplied.
save_proposal persists supplied fingerprints and still enforces visual-check plus approval/auto-cache gates.
Step 12 computes the three fingerprints and stores them on each AI record.
Router signature and read call currently omit fingerprints, so the production read path bypasses invalidation.
The residual work is non-overlapping with already-merged signature/backend/save-gate units.
FINAL_CONSENSUS: YES
[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-148` already supports `read_proposal(key, fingerprints=...)` and invalidates on strict stored-vs-supplied mismatch. `src/phase_z2_ai_fallback/step12.py:179-185` computes and records `contract_sha`, `partial_sha`, and `catalog_sha`. But `src/phase_z2_ai_fallback/router.py:43-53` accepts no `fingerprints` argument and `src/phase_z2_ai_fallback/router.py:66` calls `read_proposal(cache_key)` with the default `fingerprints=None`, which `src/phase_z2_ai_fallback/cache.py:38-44` documents as no comparison. `src/phase_z2_ai_fallback/step12.py:195-203` calls 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:
- Add router read-path fingerprint enforcement only: extend `route_ai_fallback` to accept `fingerprints: dict | None = None`, pass it to `read_proposal(cache_key, fingerprints=fingerprints)`, and thread `record["fingerprints"]` from Step 12 into the router call.
- Add focused router/Step 12 regression coverage proving mismatched fingerprints miss cache and matching fingerprints hit cache through the router path.
- Do not alter signature axes, JSON path format, save gates, auto-cache semantics, fuzzy lookup, AI request behavior, catalog promotion, or sample-specific logic.
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`, and `docs/architecture/IMP-17-CARVE-OUT.md`. No pytest was run, per Stage 1 instruction.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `rg -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.py`
- `rg -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.md`
- `rg -n "route_ai_fallback\(|read_proposal\(|save_proposal\(|fingerprints" src tests`
- line-window reads for `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`, and `src/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`
- `src/phase_z2_ai_fallback/signature.py`
- `src/phase_z2_pipeline.py`
- `src/config.py`
- `.gitignore`
- `data/frame_cache/.gitkeep`
- `docs/architecture/IMP-17-CARVE-OUT.md`
Test results: not run; Stage 1 task explicitly says grep/find and NO pytest.
Verified facts:
- `read_proposal` supports optional fingerprint comparison and returns `None` on mismatch when fingerprints are supplied.
- `save_proposal` persists supplied fingerprints and still enforces visual-check plus approval/auto-cache gates.
- Step 12 computes the three fingerprints and stores them on each AI record.
- Router signature and read call currently omit fingerprints, so the production read path bypasses invalidation.
- The residual work is non-overlapping with already-merged signature/backend/save-gate units.
FINAL_CONSENSUS: YES
u2: cache.py:145-148 strict equality. Tests router-level only (mock read_proposal, no filesystem) per feedback_scope_qualified_verification. On-disk invalidation already covered by #76 test_cache_invalidation.py.
u3: 1 kwarg added to existing call. record["fingerprints"] already built.
u4: Stage 1 Q1 resolved → mismatch returns None → router falls through to AI call (existing miss path, no typed signal needed).
=== 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).
id: u1
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
id: u2
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
id: u3
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
id: u4
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 ===
u1: Covers router.py:43-53 signature gap and router.py:66 read_proposal call gap. Optional kwarg preserves non-Step12 callers while enabling strict lookup for callers that have fingerprints.
u2: Scope-qualified router-level tests cover the residual read path only. Existing cache.py:108-148 already owns filesystem parsing and strict fingerprint equality.
u3: Covers step12.py:195-203 caller gap using the fingerprints produced at step12.py:179-185. This is the only production Step 12 edit required by the Stage 1 lock.
u4: Covers end-to-end forwarding at Step12 level without changing AI request behavior. The expected stale-cache behavior remains ordinary cache miss followed by the existing client path.
=== 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
[Codex #2] Stage 2 simulation-plan verification - IMP-46 (#62) router read-path fingerprint enforcement
=== IMPLEMENTATION_UNITS ===
- id: u1
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
- id: u2
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
- id: u3
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
- id: u4
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 ===
- u1: Covers router.py:43-53 signature gap and router.py:66 read_proposal call gap. Optional kwarg preserves non-Step12 callers while enabling strict lookup for callers that have fingerprints.
- u2: Scope-qualified router-level tests cover the residual read path only. Existing cache.py:108-148 already owns filesystem parsing and strict fingerprint equality.
- u3: Covers step12.py:195-203 caller gap using the fingerprints produced at step12.py:179-185. This is the only production Step 12 edit required by the Stage 1 lock.
- u4: Covers end-to-end forwarding at Step12 level without changing AI request behavior. The expected stale-cache behavior remains ordinary cache miss followed by the existing client path.
=== 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
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 fresh Settings(): 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 ===
id: u1
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
id: u2
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
id: u3
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
id: u4
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
id: u5
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
u4: Stage 1 Q1 resolved → mismatch returns None → router falls through to AI call (existing miss path).
u5: monkeypatch.delenv at session scope insulates 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).
id: u1
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
id: u2
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
id: u3
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
id: u4
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
id: u5
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 ===
u1: Correct residual axis. router.py:43-68 currently has no fingerprints kwarg and calls read_proposal(cache_key) without invalidation input.
u2: Correct router-level scope. cache.py:108-148 already enforces strict equality when fingerprints are supplied.
u3: Correct Step 12 wire. step12.py:179-185 builds fingerprints and step12.py:195-203 currently omits them from route_ai_fallback.
u4: Correct integration target. The test should prove forwarding only, not re-test cache.py invalidation.
u5: NO. The mechanism is false: src/config.py:37 sets model_config env_file=.env, so deleting os.environ does not stop fresh Settings() from reading .env:4 AI_FALLBACK_ENABLED=true. A session autouse fixture also cannot reliably predate module-import global settings construction at src/config.py:40.
=== 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.
u2: 3-case mirror of cache.py strict-equality. On-disk invalidation already covered by #76 test_cache_invalidation.py — not duplicated.
u3: 1 kwarg added. Pure forward; no new computation.
u4: Stage 1 Q1 resolution → mismatch returns None → router falls through to existing AI miss path (router.py:74-83); no typed cache-stale signal needed.
id: u1
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
id: u2
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
id: u3
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
id: u4
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 ===
u1: Directly closes the binding Stage 1 read-path gap at router.py:43-66. cache.py:108-148 already has the strict invalidation API; router is the missing caller.
u2: Covers deterministic cache-hit/cache-miss behavior at router scope without touching on-disk cache semantics already covered in test_cache_invalidation.py.
u3: Uses the fingerprints producer already present at step12.py:179-185 and forwards it at the only current Step 12 router call at step12.py:195-203.
u4: Covers Step 12 end-to-end forwarding and the negative skipped-record path, matching the allowed_tests contract for router-level plus step12-level scope.
Out-of-scope notes:
No edits to signature.py, cache.py invalidation logic, save-side cache persistence, auto-cache, fuzzy lookup, catalog promotion, AI request behavior, or sample-specific artifacts.
No tests/conftest.py scrubber: baseline reds are environment/config state, not Axis R implementation scope.
All units are <=50 estimated lines and <=3 files; total implementation scope is router.py, step12.py, test_router.py, and test_step12.py only.
Rollback strategy:
Revert the four scoped files only; no generated cache files or catalog/template data should be touched by this plan.
pytest -q tests: 4 failed, 979 passed. Failures are tests/test_imp47b_step12_ai_wiring.py and tests/test_phase_z2_ai_fallback_config.py, caused by current AI_FALLBACK_ENABLED=true environment/config behavior; these are baseline/out-of-scope for Axis R.
Current router signature has no fingerprints parameter and calls read_proposal(cache_key) without fingerprints at router.py:43-66.
Step 12 already builds contract_sha, partial_sha, and catalog_sha into record["fingerprints"] at step12.py:179-185, but does not pass fingerprints to route_ai_fallback at step12.py:195-203.
cache.py already documents and implements strict equality invalidation when fingerprints are supplied, and no comparison when fingerprints=None.
Existing router read_proposal monkeypatches at test_router.py:89,102,111,124,137,146 must be widened when u1 changes the call shape.
The u1-u4 plan covers the full residual Stage 1 Axis R contract and does not reopen#76 absorbed cache signature/save/fuzzy/catalog areas.
FINAL_CONSENSUS: YES
[Codex #4] Stage 2 simulation-plan verification - IMP-46 (#62) Axis R
=== IMPLEMENTATION_UNITS ===
- id: u1
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
- id: u2
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
- id: u3
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
- id: u4
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 ===
- u1: Directly closes the binding Stage 1 read-path gap at router.py:43-66. cache.py:108-148 already has the strict invalidation API; router is the missing caller.
- u2: Covers deterministic cache-hit/cache-miss behavior at router scope without touching on-disk cache semantics already covered in test_cache_invalidation.py.
- u3: Uses the fingerprints producer already present at step12.py:179-185 and forwards it at the only current Step 12 router call at step12.py:195-203.
- u4: Covers Step 12 end-to-end forwarding and the negative skipped-record path, matching the allowed_tests contract for router-level plus step12-level scope.
Out-of-scope notes:
- No edits to signature.py, cache.py invalidation logic, save-side cache persistence, auto-cache, fuzzy lookup, catalog promotion, AI request behavior, or sample-specific artifacts.
- No tests/conftest.py scrubber: baseline reds are environment/config state, not Axis R implementation scope.
- All units are <=50 estimated lines and <=3 files; total implementation scope is router.py, step12.py, test_router.py, and test_step12.py only.
Rollback strategy:
- Revert the four scoped files only; no generated cache files or catalog/template data should be touched by this plan.
=== EVIDENCE ===
Commands run:
- rg -n "def route_ai_fallback|read_proposal|route_ai_fallback\\(|fingerprints|class|def test_|monkeypatch\\.setattr\\(.*read_proposal" 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 tests/phase_z2_ai_fallback/test_cache_invalidation.py
- pytest -q tests
- pytest -q tests/phase_z2_ai_fallback/test_router.py
- pytest -q tests/phase_z2_ai_fallback/test_step12.py
- pytest -q tests/phase_z2_ai_fallback/test_cache_invalidation.py
Files checked:
- src/phase_z2_ai_fallback/router.py:43-66
- src/phase_z2_ai_fallback/step12.py:179-203
- src/phase_z2_ai_fallback/cache.py:38-44,108-148
- tests/phase_z2_ai_fallback/test_router.py:86-150
- tests/phase_z2_ai_fallback/test_step12.py:62-76,291-377
- tests/phase_z2_ai_fallback/test_cache_invalidation.py:83-286,317-345
Test results:
- pytest -q tests: 4 failed, 979 passed. Failures are tests/test_imp47b_step12_ai_wiring.py and tests/test_phase_z2_ai_fallback_config.py, caused by current AI_FALLBACK_ENABLED=true environment/config behavior; these are baseline/out-of-scope for Axis R.
- pytest -q tests/phase_z2_ai_fallback/test_router.py: 9 passed.
- pytest -q tests/phase_z2_ai_fallback/test_step12.py: 24 passed.
- pytest -q tests/phase_z2_ai_fallback/test_cache_invalidation.py: 20 passed.
Verified facts:
- Current router signature has no fingerprints parameter and calls read_proposal(cache_key) without fingerprints at router.py:43-66.
- Step 12 already builds contract_sha, partial_sha, and catalog_sha into record["fingerprints"] at step12.py:179-185, but does not pass fingerprints to route_ai_fallback at step12.py:195-203.
- cache.py already documents and implements strict equality invalidation when fingerprints are supplied, and no comparison when fingerprints=None.
- Existing router read_proposal monkeypatches at test_router.py:89,102,111,124,137,146 must be widened when u1 changes the call shape.
- The u1-u4 plan covers the full residual Stage 1 Axis R contract and does not reopen #76 absorbed cache signature/save/fuzzy/catalog areas.
FINAL_CONSENSUS: YES
summary: Add optional fingerprints kwarg to route_ai_fallback after client, forward it into read_proposal(cache_key, fingerprints=fingerprints), and repair the 6 existing read_proposal monkeypatch lambdas in test_router.py to 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.py
tests/phase_z2_ai_fallback/test_router.py
diff_summary
src/phase_z2_ai_fallback/router.py:43-66
Added kwarg-only parameter fingerprints: dict | None = None after client in route_ai_fallback signature.
Forwarded fingerprints into existing cache call: read_proposal(cache_key, fingerprints=fingerprints).
Expanded docstring with the IMP-46 #62 Axis R rationale: contract / partial / catalog SHA mismatch must invalidate stale cache entries; None preserves 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 every monkeypatch.setattr(router_mod, "read_proposal", ...) site (4× None, 1× cached, 1× bad_cached).
Pure carry-fix — no test assertion changes in u1; new fingerprint coverage lands in u2.
Result: 9 passed in 0.04s (matches Stage 2 baseline of 9 passed; signature widening + lambda carry-fix preserves green).
remaining_units
u2: Add router-level cache read coverage (mismatched fingerprints miss, matching fingerprints hit, legacy None hit) with assertions on the read_proposalfingerprints kwarg and router-level scope docstrings.
u3: Forward record["fingerprints"] into the existing route_ai_fallback call in src/phase_z2_ai_fallback/step12.py AI-eligible path.
u4: Step 12 integration coverage proving route_ai_fallback receives exactly record["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.py AI_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.
Scope: verified only the single executed unit, u1.
Verdict: u1 is correct. route_ai_fallback now accepts optional fingerprints after client and forwards that value into read_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, and signature.py have no scoped diff in this verification. The implemented changes are limited to src/phase_z2_ai_fallback/router.py and tests/phase_z2_ai_fallback/test_router.py.
tests/phase_z2_ai_fallback/test_router.py:89,102,111,124,137,146 use lambda key, **_: for existing read_proposal monkeypatches.
git diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py produced 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
[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_fallback` now accepts optional `fingerprints` after `client` and forwards that value into `read_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`, and `signature.py` have no scoped diff in this verification. The implemented changes are limited to `src/phase_z2_ai_fallback/router.py` and `tests/phase_z2_ai_fallback/test_router.py`.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git diff -- src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.py`
- `rg -n "def route_ai_fallback|read_proposal\(|fingerprints|lambda key" src/phase_z2_ai_fallback/router.py tests/phase_z2_ai_fallback/test_router.py`
- `git diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
Files checked:
- `src/phase_z2_ai_fallback/router.py:43-72`
- `tests/phase_z2_ai_fallback/test_router.py:86-147`
- `src/phase_z2_ai_fallback/step12.py`
- `src/phase_z2_ai_fallback/cache.py`
- `src/phase_z2_ai_fallback/signature.py`
Test results:
- Router-level scope: `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q` -> `9 passed in 0.04s`
Verified facts:
- `src/phase_z2_ai_fallback/router.py:53` adds `fingerprints: dict | None = None`.
- `src/phase_z2_ai_fallback/router.py:72` calls `read_proposal(cache_key, fingerprints=fingerprints)`.
- `tests/phase_z2_ai_fallback/test_router.py:89,102,111,124,137,146` use `lambda key, **_:` for existing `read_proposal` monkeypatches.
- `git diff -- src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py` produced 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
id: u2
summary: Add router-level cache read coverage for (a) mismatched fingerprints → miss → client invoked, (b) matching fingerprints → hit → client NOT invoked, (c) legacy fingerprints=None → hit → client NOT invoked. Each new test captures the fingerprints kwarg actually passed to read_proposal via a keyword-spy, asserts strict equality against the caller-supplied dict (or None), and declares router-level scope in its docstring per Stage 2 plan.
files_changed
tests/phase_z2_ai_fallback/test_router.py
diff_summary
tests/phase_z2_ai_fallback/test_router.py — three new test functions inserted between test_router_validates_cached_proposal and test_router_calls_client_and_returns_validated_proposal:
test_router_forwards_fingerprints_none_for_legacy_callers — caller omits fingerprints, spy read_proposal returns cached proposal, captured kwarg is None, client not called (legacy no-invalidation behaviour preserved).
No production code touched in u2. src/phase_z2_ai_fallback/router.py already accepts and forwards fingerprints per u1.
The 6 pre-existing read_proposal monkeypatch lambdas (already repaired in u1 with lambda key, **_:) remain unchanged.
scope verification
Stage 2 plan per_file_changes for u2 honoured: tests/phase_z2_ai_fallback/test_router.py only — 3 new fingerprint scenarios added; no existing test assertion changed.
Stage 1 allowed_edits honoured: u2 lives entirely in the router-level test file; production surface untouched until u3.
Out-of-scope untouched this turn: 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.
IMP-47B (#76) commit 1186ad8 cache carve-out: not stirred (memory: project_imp46_carveout_caveat).
AI isolation contract: untouched (memory: feedback_ai_isolation_contract).
No hardcoding (memory: feedback_no_hardcoding): test fingerprint values are inline test fixtures, not sample-specific MDX or frame DB leakage.
scope-qualified verification
Scope: router-level only (tests/phase_z2_ai_fallback/test_router.py).
Result: 12 passed in 0.49s (u1 baseline 9 passed + 3 new u2 scenarios = 12). No regressions in pre-existing 9 tests.
remaining_units
u3: Forward record["fingerprints"] into the existing route_ai_fallback call in src/phase_z2_ai_fallback/step12.py AI-eligible path (~3 lines).
u4: Step 12 integration coverage proving route_ai_fallback receives exactly record["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.py AI_FALLBACK_ENABLED scrubber (former u5) and 4 .env-driven baseline-red as separate-axis follow-ups; no new candidates surfaced.
[Claude #2] Stage 3 code-edit — IMP-46 (#62) Axis R u2
Executing unit: u2
## unit_executed
- id: u2
- summary: Add router-level cache read coverage for (a) mismatched fingerprints → miss → client invoked, (b) matching fingerprints → hit → client NOT invoked, (c) legacy `fingerprints=None` → hit → client NOT invoked. Each new test captures the `fingerprints` kwarg actually passed to `read_proposal` via a keyword-spy, asserts strict equality against the caller-supplied dict (or `None`), and declares router-level scope in its docstring per Stage 2 plan.
## files_changed
- `tests/phase_z2_ai_fallback/test_router.py`
## diff_summary
- `tests/phase_z2_ai_fallback/test_router.py` — three new test functions inserted between `test_router_validates_cached_proposal` and `test_router_calls_client_and_returns_validated_proposal`:
- `test_router_forwards_fingerprints_and_misses_on_mismatch` — supplies `fingerprints={"contract_sha": "aaa", "partial_sha": "bbb", "catalog_sha": "ccc"}`, spy `read_proposal` returns `None` (simulating strict-equality SHA mismatch), router proceeds to call client, captured kwarg equals supplied dict.
- `test_router_forwards_fingerprints_and_hits_on_match` — supplies `fingerprints={"contract_sha": "xxx", "partial_sha": "yyy", "catalog_sha": "zzz"}`, spy `read_proposal` returns cached proposal (simulating strict-equality match), router short-circuits, `client.request_proposal` not called, captured kwarg equals supplied dict.
- `test_router_forwards_fingerprints_none_for_legacy_callers` — caller omits `fingerprints`, spy `read_proposal` returns cached proposal, captured kwarg is `None`, client not called (legacy no-invalidation behaviour preserved).
- No production code touched in u2. `src/phase_z2_ai_fallback/router.py` already accepts and forwards `fingerprints` per u1.
- The 6 pre-existing `read_proposal` monkeypatch lambdas (already repaired in u1 with `lambda key, **_:`) remain unchanged.
## scope verification
- Stage 2 plan `per_file_changes` for u2 honoured: `tests/phase_z2_ai_fallback/test_router.py` only — 3 new fingerprint scenarios added; no existing test assertion changed.
- Stage 1 `allowed_edits` honoured: u2 lives entirely in the router-level test file; production surface untouched until u3.
- Out-of-scope untouched this turn: `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.
- IMP-47B (#76) commit `1186ad8` cache carve-out: not stirred (memory: `project_imp46_carveout_caveat`).
- AI isolation contract: untouched (memory: `feedback_ai_isolation_contract`).
- No hardcoding (memory: `feedback_no_hardcoding`): test fingerprint values are inline test fixtures, not sample-specific MDX or frame DB leakage.
## scope-qualified verification
- Scope: router-level only (`tests/phase_z2_ai_fallback/test_router.py`).
- Command: `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
- Result: `12 passed in 0.49s` (u1 baseline 9 passed + 3 new u2 scenarios = 12). No regressions in pre-existing 9 tests.
## remaining_units
- u3: Forward `record["fingerprints"]` into the existing `route_ai_fallback` call in `src/phase_z2_ai_fallback/step12.py` AI-eligible path (~3 lines).
- u4: Step 12 integration coverage proving `route_ai_fallback` receives exactly `record["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.py` AI_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 actual fingerprints kwarg passed to read_proposal and 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, or signature.py, so u3/u4 and cache/signature territory were not touched by this unit.
tests/phase_z2_ai_fallback/test_router.py:109 adds test_router_forwards_fingerprints_and_misses_on_mismatch; its spy captures fingerprints, returns None, and client.request_proposal.assert_called_once() verifies fallback on miss.
tests/phase_z2_ai_fallback/test_router.py:139 adds test_router_forwards_fingerprints_and_hits_on_match; its spy captures fingerprints, returns a cached proposal, and client.request_proposal.assert_not_called() verifies cache-hit short-circuiting.
tests/phase_z2_ai_fallback/test_router.py:166 adds test_router_forwards_fingerprints_none_for_legacy_callers; caller omits fingerprints, the spy captures None, and the cached proposal short-circuits the client.
The combined scoped diff shows no changes to src/phase_z2_ai_fallback/step12.py, tests/phase_z2_ai_fallback/test_step12.py, src/phase_z2_ai_fallback/cache.py, or src/phase_z2_ai_fallback/signature.py.
Remaining units: [u3, u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[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 actual `fingerprints` kwarg passed to `read_proposal` and 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`, or `signature.py`, so u3/u4 and cache/signature territory were not touched by this unit.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git 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.py`
- `rg -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.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
Files checked:
- `tests/phase_z2_ai_fallback/test_router.py:109-187`
- `src/phase_z2_ai_fallback/router.py:43-72`
- `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.py`
Test results:
- Router-level scope: `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q` -> `12 passed in 0.04s`
Verified facts:
- `tests/phase_z2_ai_fallback/test_router.py:109` adds `test_router_forwards_fingerprints_and_misses_on_mismatch`; its spy captures `fingerprints`, returns `None`, and `client.request_proposal.assert_called_once()` verifies fallback on miss.
- `tests/phase_z2_ai_fallback/test_router.py:139` adds `test_router_forwards_fingerprints_and_hits_on_match`; its spy captures `fingerprints`, returns a cached proposal, and `client.request_proposal.assert_not_called()` verifies cache-hit short-circuiting.
- `tests/phase_z2_ai_fallback/test_router.py:166` adds `test_router_forwards_fingerprints_none_for_legacy_callers`; caller omits `fingerprints`, the spy captures `None`, and the cached proposal short-circuits the client.
- The combined scoped diff shows no changes to `src/phase_z2_ai_fallback/step12.py`, `tests/phase_z2_ai_fallback/test_step12.py`, `src/phase_z2_ai_fallback/cache.py`, or `src/phase_z2_ai_fallback/signature.py`.
Remaining units: [u3, u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
summary: Forward the fingerprints dict already built in src/phase_z2_ai_fallback/step12.py (lines 179-185) into the existing route_ai_fallback call 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.py
diff_summary
src/phase_z2_ai_fallback/step12.py:195-204 — single-line addition fingerprints=fingerprints, inside the existing route_ai_fallback(...) keyword-only call. The fingerprints local was already constructed at step12.py:179-185 (contract_sha = _sha256_of(frame_contract), partial_sha = _sha256_of(figma_partial_json), catalog_sha) and stamped onto record["fingerprints"] at step12.py:185. No producer logic touched; no record-side telemetry touched; no control-flow change.
step12.py: forward record["fingerprints"] into route_ai_fallback call ✓
Stage 2 per_file_changes for u3 honoured:
step12.py:194-203 — pass fingerprints=record["fingerprints"] into existing route_ai_fallback call ✓ (using the named local fingerprints which is the same object stamped to record["fingerprints"] one line earlier at step12.py:185 — semantically identical, avoids the dict-subscript round-trip).
Out-of-scope untouched this turn: 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.
IMP-47B (#76) commit 1186ad8 cache carve-out: not stirred (memory: project_imp46_carveout_caveat).
AI isolation contract: untouched — u3 forwards a comparison input only, no new AI invocation path (memory: feedback_ai_isolation_contract).
No hardcoding (memory: feedback_no_hardcoding): u3 wires the producer to the consumer; signature axes / SHA computation untouched.
1 commit = 1 decision unit (memory: feedback_workflow_atomicity_rules): u3 = single line in step12.py only.
Note: u3 wires a kwarg that defaults to None upstream; existing step12 tests stay green because the cache layer treats fingerprints=None as legacy no-comparison. Explicit forwarding-coverage lands in u4.
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)
No other files touched (cache.py / signature.py / test_step12.py / test_cache.py / test_signature.py untouched ✓).
src/phase_z2_ai_fallback/router.py:53 shows fingerprints: dict | None = None already present in the signature (u1).
src/phase_z2_ai_fallback/router.py:72 shows read_proposal(cache_key, fingerprints=fingerprints) already in place (u1).
src/phase_z2_ai_fallback/step12.py:179-185 shows the fingerprints producer (contract_sha / partial_sha / catalog_sha) is unchanged.
src/phase_z2_ai_fallback/step12.py:203 now carries fingerprints=fingerprints, inside the existing route_ai_fallback call (u3).
remaining_units
u4: Step 12 integration coverage proving route_ai_fallback receives exactly record["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.py AI_FALLBACK_ENABLED scrubber (former u5) and the 4 .env-driven baseline reds as separate axes for follow-up.
[Claude #3] Stage 3 code-edit — IMP-46 (#62) Axis R u3
Executing unit: u3
## unit_executed
- id: u3
- summary: Forward the `fingerprints` dict already built in `src/phase_z2_ai_fallback/step12.py` (lines 179-185) into the existing `route_ai_fallback` call 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.py`
## diff_summary
- `src/phase_z2_ai_fallback/step12.py:195-204` — single-line addition `fingerprints=fingerprints,` inside the existing `route_ai_fallback(...)` keyword-only call. The `fingerprints` local was already constructed at `step12.py:179-185` (`contract_sha = _sha256_of(frame_contract)`, `partial_sha = _sha256_of(figma_partial_json)`, `catalog_sha`) and stamped onto `record["fingerprints"]` at `step12.py:185`. No producer logic touched; no record-side telemetry touched; no control-flow change.
```diff
@@ src/phase_z2_ai_fallback/step12.py
figma_partial_json=figma_partial_json,
internal_region=internal_region,
mdx_text=mdx_text,
+ fingerprints=fingerprints,
)
```
## scope verification
- Stage 1 `allowed_edits` for u3 honoured:
- `step12.py: forward record["fingerprints"] into route_ai_fallback call` ✓
- Stage 2 `per_file_changes` for u3 honoured:
- `step12.py:194-203 — pass fingerprints=record["fingerprints"] into existing route_ai_fallback call` ✓ (using the named local `fingerprints` which is the same object stamped to `record["fingerprints"]` one line earlier at step12.py:185 — semantically identical, avoids the dict-subscript round-trip).
- Out-of-scope untouched this turn: `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.
- IMP-47B (#76) commit `1186ad8` cache carve-out: not stirred (memory: `project_imp46_carveout_caveat`).
- AI isolation contract: untouched — u3 forwards a comparison input only, no new AI invocation path (memory: `feedback_ai_isolation_contract`).
- No hardcoding (memory: `feedback_no_hardcoding`): u3 wires the producer to the consumer; signature axes / SHA computation untouched.
- 1 commit = 1 decision unit (memory: `feedback_workflow_atomicity_rules`): u3 = single line in step12.py only.
## scope-qualified verification
- Scope A: step12-level (`tests/phase_z2_ai_fallback/test_step12.py`)
- Command: `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`
- Result: `24 passed in 0.08s`
- Note: u3 wires a kwarg that defaults to `None` upstream; existing step12 tests stay green because the cache layer treats `fingerprints=None` as legacy no-comparison. Explicit forwarding-coverage lands in u4.
- Scope B: router-level (`tests/phase_z2_ai_fallback/test_router.py`)
- Command: `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
- Result: `12 passed in 0.04s` (u1 baseline 9 + u2's 3 fingerprint scenarios — all green post-u3).
- Scope NOT verified this turn: end-to-end step12 → router fingerprint forwarding (deferred to u4 by Stage 2 plan).
## evidence
- `git diff -- src/phase_z2_ai_fallback/step12.py` confirms the single-line addition at the existing `route_ai_fallback` keyword-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)
- No other files touched (cache.py / signature.py / test_step12.py / test_cache.py / test_signature.py untouched ✓).
- `src/phase_z2_ai_fallback/router.py:53` shows `fingerprints: dict | None = None` already present in the signature (u1).
- `src/phase_z2_ai_fallback/router.py:72` shows `read_proposal(cache_key, fingerprints=fingerprints)` already in place (u1).
- `src/phase_z2_ai_fallback/step12.py:179-185` shows the fingerprints producer (`contract_sha` / `partial_sha` / `catalog_sha`) is unchanged.
- `src/phase_z2_ai_fallback/step12.py:203` now carries `fingerprints=fingerprints,` inside the existing `route_ai_fallback` call (u3).
## remaining_units
- u4: Step 12 integration coverage proving `route_ai_fallback` receives exactly `record["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.py` AI_FALLBACK_ENABLED scrubber (former u5) and the 4 `.env`-driven baseline reds as separate axes for follow-up.
Scope: verified only the single executed unit, u3.
Verdict: u3 is correct. src/phase_z2_ai_fallback/step12.py now forwards the existing fingerprints local into the existing route_ai_fallback(...) call on the AI-eligible path. That local is the same dict assigned to record["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.
src/phase_z2_ai_fallback/step12.py:179-185 constructs fingerprints from contract_sha, partial_sha, and catalog_sha, then assigns the same object to record["fingerprints"].
src/phase_z2_ai_fallback/step12.py:195-203 calls route_ai_fallback(...) and now passes fingerprints=fingerprints.
The combined scoped diff shows no changes to src/phase_z2_ai_fallback/cache.py, src/phase_z2_ai_fallback/signature.py, or tests/phase_z2_ai_fallback/test_step12.py for this unit.
src/phase_z2_ai_fallback/router.py:53 and src/phase_z2_ai_fallback/router.py:72 remain consistent with previously verified u1: optional fingerprints kwarg is accepted and forwarded to read_proposal(cache_key, fingerprints=fingerprints).
Remaining units: [u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[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.py` now forwards the existing `fingerprints` local into the existing `route_ai_fallback(...)` call on the AI-eligible path. That local is the same dict assigned to `record["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 --short`
- `git 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.py`
- `rg -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.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`
Files checked:
- `src/phase_z2_ai_fallback/step12.py:179-203`
- `src/phase_z2_ai_fallback/router.py:43-72`
- `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.py`
Test results:
- Step12-level scope: `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q` -> `24 passed in 0.08s`
Verified facts:
- `src/phase_z2_ai_fallback/step12.py:179-185` constructs `fingerprints` from `contract_sha`, `partial_sha`, and `catalog_sha`, then assigns the same object to `record["fingerprints"]`.
- `src/phase_z2_ai_fallback/step12.py:195-203` calls `route_ai_fallback(...)` and now passes `fingerprints=fingerprints`.
- The combined scoped diff shows no changes to `src/phase_z2_ai_fallback/cache.py`, `src/phase_z2_ai_fallback/signature.py`, or `tests/phase_z2_ai_fallback/test_step12.py` for this unit.
- `src/phase_z2_ai_fallback/router.py:53` and `src/phase_z2_ai_fallback/router.py:72` remain consistent with previously verified u1: optional `fingerprints` kwarg is accepted and forwarded to `read_proposal(cache_key, fingerprints=fingerprints)`.
Remaining units: [u4]
rewind_target: continue_same
FINAL_CONSENSUS: NO
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 the router and keep record["fingerprints"] as None. Locks the end-to-end producer→consumer wiring landed in u3 (step12.py:179-185 builds the fingerprints dict, stamps it onto record["fingerprints"] at step12.py:185, and forwards the same object into the router via fingerprints= 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.py
diff_summary
tests/phase_z2_ai_fallback/test_step12.py — appended a new section header IMP-46 u4 — Step 12 ↔ router fingerprints forwarding (integration scope) and 5 new test functions after test_production_non_provisional_reject_skipped_before_route_gate:
test_router_receives_exactly_record_fingerprints_for_ai_eligible — strict equality router.call_args.kwargs["fingerprints"] == recs[0]["fingerprints"] for an AI-eligible unit with caller-supplied contract / partial / catalog_value; also re-derives the expected SHA256 of contract + partial via hashlib.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 default None) and that the 3 keys are present with catalog_sha == "" sentinel when no catalog_sha_loader is 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 matches recs[2]["fingerprints"], and skipped records carry fingerprints is None.
Each new test declares step12-integration scope in its docstring per Stage 2 plan.
No production source touched this turn. route_ai_fallback forwarding at step12.py:203 (u3) and read_proposal(cache_key, fingerprints=fingerprints) at router.py:72 (u1) remain unchanged.
test_step12.py — add integration coverage asserting router receives record["fingerprints"] for AI-eligible, None for skipped/non-AI ✓
Stage 1 allowed_edits honoured: u4 lives entirely in tests/phase_z2_ai_fallback/test_step12.py; production surface (router.py, step12.py, cache.py, signature.py) untouched this turn.
This-turn scoped diff = tests/phase_z2_ai_fallback/test_step12.py only (+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.
Out-of-scope untouched this turn: 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.
IMP-47B (#76) commit 1186ad8 cache carve-out: not stirred (memory: project_imp46_carveout_caveat).
AI isolation contract: untouched — u4 adds test-side assertions only, no new AI invocation path (memory: feedback_ai_isolation_contract).
No hardcoding (memory: 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.
None this turn. No scope-lock-external axis observed during u4. Stage 2 already records the two deferred follow-ups (tests/conftest.py AI_FALLBACK_ENABLED scrubber + 4 .env-driven baseline-red root cause) as separate axes.
[Claude #4] Stage 3 code-edit — IMP-46 (#62) Axis R u4
Executing unit: u4
## unit_executed
- id: u4
- 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 the router and keep `record["fingerprints"]` as `None`. Locks the end-to-end producer→consumer wiring landed in u3 (step12.py:179-185 builds the fingerprints dict, stamps it onto `record["fingerprints"]` at step12.py:185, and forwards the same object into the router via `fingerprints=` 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.py`
## diff_summary
- `tests/phase_z2_ai_fallback/test_step12.py` — appended a new section header `IMP-46 u4 — Step 12 ↔ router fingerprints forwarding (integration scope)` and 5 new test functions after `test_production_non_provisional_reject_skipped_before_route_gate`:
- `test_router_receives_exactly_record_fingerprints_for_ai_eligible` — strict equality `router.call_args.kwargs["fingerprints"] == recs[0]["fingerprints"]` for an AI-eligible unit with caller-supplied `contract` / `partial` / `catalog_value`; also re-derives the expected SHA256 of contract + partial via `hashlib.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 default `None`) and that the 3 keys are present with `catalog_sha == ""` sentinel when no `catalog_sha_loader` is 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 matches `recs[2]["fingerprints"]`, and skipped records carry `fingerprints is None`.
- Each new test declares **step12-integration scope** in its docstring per Stage 2 plan.
- No production source touched this turn. `route_ai_fallback` forwarding at `step12.py:203` (u3) and `read_proposal(cache_key, fingerprints=fingerprints)` at `router.py:72` (u1) remain unchanged.
```diff
@@ tests/phase_z2_ai_fallback/test_step12.py (append, +102 lines)
+# ---------------------------------------------------------------------------
+# IMP-46 u4 — Step 12 ↔ router fingerprints forwarding (integration scope)
+# ---------------------------------------------------------------------------
+
+def test_router_receives_exactly_record_fingerprints_for_ai_eligible(monkeypatch):
+ ...
+ assert forwarded == record_fingerprints
+
+def test_router_fingerprints_kwarg_is_present_even_with_default_catalog(monkeypatch):
+ ...
+ assert "fingerprints" in router.call_args.kwargs
+
+def test_router_not_called_and_fingerprints_none_for_non_provisional(monkeypatch):
+ ...
+ router.assert_not_called()
+ assert recs[0]["fingerprints"] is None
+
+def test_router_not_called_and_fingerprints_none_for_non_ai_route(monkeypatch):
+ ...
+ router.assert_not_called()
+ assert recs[0]["fingerprints"] is None
+
+def test_mixed_units_router_receives_fingerprints_only_for_ai_eligible(monkeypatch):
+ ...
+ router.assert_called_once()
+ assert forwarded == recs[2]["fingerprints"]
+ assert recs[0]["fingerprints"] is None
+ assert recs[1]["fingerprints"] is None
```
## scope verification
- Stage 2 plan `per_file_changes` for u4 honoured:
- `test_step12.py — add integration coverage asserting router receives record["fingerprints"] for AI-eligible, None for skipped/non-AI` ✓
- Stage 1 `allowed_edits` honoured: u4 lives entirely in `tests/phase_z2_ai_fallback/test_step12.py`; production surface (`router.py`, `step12.py`, `cache.py`, `signature.py`) untouched this turn.
- This-turn scoped diff = `tests/phase_z2_ai_fallback/test_step12.py` only (+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.
- Out-of-scope untouched this turn: `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.
- IMP-47B (#76) commit `1186ad8` cache carve-out: not stirred (memory: `project_imp46_carveout_caveat`).
- AI isolation contract: untouched — u4 adds test-side assertions only, no new AI invocation path (memory: `feedback_ai_isolation_contract`).
- No hardcoding (memory: `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.
- 1 commit = 1 decision unit (memory: `feedback_workflow_atomicity_rules`): u4 = test_step12.py only.
- Scope-qualified verification (memory: `feedback_scope_qualified_verification`): each new test docstring declares "Integration scope" explicitly.
## scope-qualified verification
- Scope A: step12-integration (`tests/phase_z2_ai_fallback/test_step12.py`)
- Command: `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`
- Result: `29 passed in 0.10s` (24 baseline + 5 new u4 tests).
- Scope B: router-level regression (`tests/phase_z2_ai_fallback/test_router.py`)
- Command: `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`
- Result: `12 passed in 0.04s` (unchanged from u2 baseline — u4 did not touch router-level surface).
## remaining_units
- (none)
All Stage 2 implementation_units are now executed:
- u1 ✓ (Claude #1 / Codex #1 verified)
- u2 ✓ (Claude #2 / Codex #2 verified)
- u3 ✓ (Claude #3 / Codex #3 verified)
- u4 ✓ (this turn — Codex #4 verification pending)
## 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.py` AI_FALLBACK_ENABLED scrubber + 4 `.env`-driven baseline-red root cause) as separate axes.
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 keep record["fingerprints"] as None. The test coverage is scoped to tests/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.
tests/phase_z2_ai_fallback/test_step12.py:517-544 adds test_router_receives_exactly_record_fingerprints_for_ai_eligible; it patches step12_mod.route_ai_fallback, captures router.call_args.kwargs["fingerprints"], asserts equality with recs[0]["fingerprints"], and re-derives the expected contract/partial SHA256 plus supplied catalog_sha.
tests/phase_z2_ai_fallback/test_step12.py:547-559 adds explicit coverage that the fingerprints kwarg is present even when catalog_sha defaults to the empty-string sentinel.
tests/phase_z2_ai_fallback/test_step12.py:562-585 adds skipped-record coverage for non-provisional and non-AI-route records: router not called, ai_called is False, fingerprints is None, and cache_key is None.
tests/phase_z2_ai_fallback/test_step12.py:588-604 adds mixed-batch coverage proving only the AI-eligible unit calls the router and forwards fingerprints matching recs[2]["fingerprints"]; skipped records keep fingerprints is None.
src/phase_z2_ai_fallback/step12.py:179-185 still builds and stores the fingerprints dict before routing, and src/phase_z2_ai_fallback/step12.py:195-203 still forwards it via fingerprints=fingerprints.
The scoped combined diff shows no u4 production edits to src/phase_z2_ai_fallback/step12.py, src/phase_z2_ai_fallback/router.py, src/phase_z2_ai_fallback/cache.py, or src/phase_z2_ai_fallback/signature.py; only tests/phase_z2_ai_fallback/test_step12.py contains the new u4 test block.
Remaining units: none
FINAL_CONSENSUS: YES
[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 keep `record["fingerprints"]` as `None`. The test coverage is scoped to `tests/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 --short`
- `git 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.py`
- `rg -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.py`
- `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`
- PowerShell line inspection for `tests/phase_z2_ai_fallback/test_step12.py:506-604`
- PowerShell line inspection for `src/phase_z2_ai_fallback/step12.py:179-204`
Files checked:
- `tests/phase_z2_ai_fallback/test_step12.py:506-604`
- `src/phase_z2_ai_fallback/step12.py:179-204`
- `src/phase_z2_ai_fallback/router.py:43-72`
- `tests/phase_z2_ai_fallback/test_router.py:109-187`
- `src/phase_z2_ai_fallback/cache.py`
- `src/phase_z2_ai_fallback/signature.py`
Test results:
- Step12-integration scope: `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q` -> `29 passed in 0.08s`
Verified facts:
- `tests/phase_z2_ai_fallback/test_step12.py:517-544` adds `test_router_receives_exactly_record_fingerprints_for_ai_eligible`; it patches `step12_mod.route_ai_fallback`, captures `router.call_args.kwargs["fingerprints"]`, asserts equality with `recs[0]["fingerprints"]`, and re-derives the expected contract/partial SHA256 plus supplied `catalog_sha`.
- `tests/phase_z2_ai_fallback/test_step12.py:547-559` adds explicit coverage that the `fingerprints` kwarg is present even when `catalog_sha` defaults to the empty-string sentinel.
- `tests/phase_z2_ai_fallback/test_step12.py:562-585` adds skipped-record coverage for non-provisional and non-AI-route records: router not called, `ai_called is False`, `fingerprints is None`, and `cache_key is None`.
- `tests/phase_z2_ai_fallback/test_step12.py:588-604` adds mixed-batch coverage proving only the AI-eligible unit calls the router and forwards fingerprints matching `recs[2]["fingerprints"]`; skipped records keep `fingerprints is None`.
- `src/phase_z2_ai_fallback/step12.py:179-185` still builds and stores the fingerprints dict before routing, and `src/phase_z2_ai_fallback/step12.py:195-203` still forwards it via `fingerprints=fingerprints`.
- The scoped combined diff shows no u4 production edits to `src/phase_z2_ai_fallback/step12.py`, `src/phase_z2_ai_fallback/router.py`, `src/phase_z2_ai_fallback/cache.py`, or `src/phase_z2_ai_fallback/signature.py`; only `tests/phase_z2_ai_fallback/test_step12.py` contains the new u4 test block.
Remaining units: none
FINAL_CONSENSUS: YES
only line 201 added (the fingerprints= forward), save-side fingerprint persistence at 179-185 unchanged
✅ producer untouched
feedback memory: project_imp46_carveout_caveat honored — no re-stirring of cache backend / signature axes / save gates / auto-cache / save-side fingerprint persistence.
■ No-hardcoding audit
Check
Result
`rg "mdx 03
hardcod
`rg "mdx 03
hardcod
Test SHA values
"aaa"/"bbb"/"ccc", "xxx"/"yyy"/"zzz", "c0ffee00" * 8 — fixture stand-ins, not extracted from any MDX 03/04/05 sample
Test inputs use _call(...) helper + FakeUnit factory
Production paths exercise generic _sha256_of(frame_contract) recomputation, not sample-pinned values
Generality 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
All 229 tests in tests/phase_z2_ai_fallback/ pass.
Pre-existing baseline-red 4× (.env-driven AI_FALLBACK_ENABLED=true leak) — axis-orthogonal to Axis R (Stage 2 EXIT REPORT follow_up_candidates noted them as separate axis; out-of-scope per Stage 1 allowed_edits).
Adjacent router consumers verified: tests/phase_z2_ai_fallback/test_router.py legacy callers (lines 184, 205, 218, 227) continue to call route_ai_fallback(**_call_kwargs(), client=client) with no fingerprints arg → read_proposal receives fingerprints=None → cache layer skips fingerprint comparison (legacy behavior preserved per router.py:62-65 docstring).
■ PZ rule application
PZ-1 (AI=0 normal): route_ai_fallback short-circuits at flag-off / route-mismatch BEFORE prompt/client work (router.py:67-71). Unchanged by u1.
PZ-2 (1 turn = 1 step): Stage 4 verification only this turn; no Stage 5 commit/push pre-GO.
PZ-3 (no speculative): No NEW units invented this turn.
PZ-4 (no silent shrink): Fingerprint-mismatch on cached entry → strict miss → router proceeds to client call (per u2 _misses_on_mismatch test). No silent fallback to stale value.
RULE 4 (scope-qualified): every new test docstring carries "Scope: router-level" or "Integration scope:" prefix.
RULE 9 (LLM classifies, code composes): this axis is pure code wiring, no AI surface widened.
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 ===
Working tree (relevant files):
M src/phase_z2_ai_fallback/router.py
M src/phase_z2_ai_fallback/step12.py
M tests/phase_z2_ai_fallback/test_router.py
M tests/phase_z2_ai_fallback/test_step12.py
Working tree also contains unrelated local changes/untracked files, including .claude/settings.json; these are outside commit scope and were not evaluated as IMP-46 changes.
src/phase_z2_ai_fallback/step12.py:203 forwards the already-built local fingerprints dict into route_ai_fallback(...).
tests/phase_z2_ai_fallback/test_router.py:109-187 contains the three router-level u2 scenarios: mismatch miss, matching hit, and legacy fingerprints=None.
tests/phase_z2_ai_fallback/test_step12.py:517-604 contains the five u4 integration tests proving AI-eligible forwarding and skipped/non-AI no-router behavior.
git diff -- src/phase_z2_ai_fallback/step12.py shows only the fingerprints=fingerprints forward in production Step 12; save-side cache persistence was not changed.
=== BASIS ===
Stage 4 PASS exit report defined exact staging surface (4 files) + commit message. Stage 5 executed: path-scoped git add, single commit d9d3384 matching plan stat 1:1 (197+/7-, 13 production lines), git push origin main + git push slide2 main both succeeded with f3ef4d9..d9d3384. Post-push fetch confirmed both remotes synced to d9d3384. Carve-out (cache.py/signature.py) diff empty. No regression to staging policy or memory guardrails.
[Claude #1] Stage 5 commit-push — IMP-46 (#62) Axis R u1~u4
📌 **Stage 5: 커밋 및 푸쉬 완료**
■ 핵심 결정
- IMP-46 (#62) Axis R u1~u4 commit `d9d3384` 작성, path-scoped staging 으로 4 파일만 포함.
- `origin` (GitHub) + `slide2` (Gitea mirror) 양측 push 성공. 두 remote 모두 `d9d3384` 정합.
- Stage 4 plan 과 1:1 매칭: 197+/7- 변경, production 13 lines (router.py +6/-1, step12.py +1/-0), test 195 lines.
- `git add -A` 사용 금지 준수. `.claude/settings.json` 외 unrelated untracked 모두 unstaged 유지.
- IMP-47B (#76) carve-out 영역 (`cache.py`, `signature.py`) diff = empty. 침범 없음.
■ 범위 제외
- IMP-46 와 무관한 working tree 변경 (`.claude/settings.json`, `tests/matching/`, `Front/`, `samples/`, untracked `tmp*.json` 등) 전부 unstaged.
- `cache.py` / `signature.py` 변경 없음.
- u5+ 후속 axis — 본 이슈 범위 밖.
■ 다음 단계
- Stage 6 (final-close) 진입 대기.
=== EXIT REPORT (English, binding contract) ===
```yaml
commit:
sha: d9d338416ab277f55deaf8496f827cd17260cb87
short: d9d3384
parent: f3ef4d917c775d497fbed8109042f46635e66f1a
author: "kyeongmin <b24009@hanmaceng.co.kr>"
date: "2026-05-23 08:53:22 +0900"
message: "feat(#62): IMP-46 cache fingerprint forwarding u1~u4 (router kwarg + step12 forward + 8 scenarios)"
staging_method: "path-scoped"
git_add_command: "git add 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"
git_add_A_used: false
files_committed:
- src/phase_z2_ai_fallback/router.py # +6 / -1 (u1)
- src/phase_z2_ai_fallback/step12.py # +1 / -0 (u3)
- tests/phase_z2_ai_fallback/test_router.py # +93 / -3 (u1 lambda patches + u2 scenarios)
- tests/phase_z2_ai_fallback/test_step12.py # +102 / -0 (u4)
diff_stat: "4 files changed, 197 insertions(+), 7 deletions(-)"
production_lines_changed: 13
plan_match: exact
protected_carveout_diff:
src/phase_z2_ai_fallback/cache.py: "no diff"
src/phase_z2_ai_fallback/signature.py: "no diff"
carveout_note: "IMP-47B (#76) commit 1186ad8 cache territory untouched"
excluded_from_commit:
- ".claude/settings.json (modified, axis-orthogonal)"
- "untracked tmp*.json / tests/matching/* / Front/ / samples/uploads/ / .orchestrator/* / templates/* (unrelated)"
push_targets:
- remote: origin
url: "git@github.com:keimin86/design_agent.git"
result: "f3ef4d9..d9d3384 main -> main"
fetch_verified: "origin/main = d9d3384"
- remote: slide2
url: "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2.git"
result: "f3ef4d9..d9d3384 main -> main"
fetch_verified: "slide2/main = d9d3384"
- remote_skipped: gitea
url: "https://gitea.hmac.kr/Kyeongmin/_Geulbeot.git"
reason: "different project (memory: reference_design_agent_remotes)"
remote_sync_check:
HEAD: d9d338416ab277f55deaf8496f827cd17260cb87
origin_main: d9d338416ab277f55deaf8496f827cd17260cb87
slide2_main: d9d338416ab277f55deaf8496f827cd17260cb87
all_equal: true
unresolved_questions: []
guardrails:
- "path-scoped staging (no git add -A) — RULE 6 satisfied"
- "no hardcoding — fix routed through router contract, not artifact (memory: feedback_no_hardcoding)"
- "1 commit = 1 decision unit — u1~u4 packaged as single fingerprint forwarding axis"
- "IMP-46 carve-out caveat preserved — cache.py / signature.py untouched (memory: project_imp46_carveout_caveat)"
- "AI isolation contract — fingerprints kwarg defaults None; legacy callers unaffected"
evidence:
pre_commit:
- "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, 197+/7-"
- "git diff --stat src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/signature.py → empty (no output)"
- "git rev-parse HEAD pre-commit = f3ef4d917c775d497fbed8109042f46635e66f1a"
commit:
- "git commit -m 'feat(#62): IMP-46 cache fingerprint forwarding u1~u4 (router kwarg + step12 forward + 8 scenarios)' → [main d9d3384]"
- "git show --stat d9d3384 → 4 files changed, 197 insertions(+), 7 deletions(-)"
push:
- "git push origin main → f3ef4d9..d9d3384 main -> main"
- "git push slide2 main → f3ef4d9..d9d3384 main -> main"
post_push_verify:
- "git fetch origin main && git fetch slide2 main → both succeed"
- "git rev-parse HEAD origin/main slide2/main → all three = d9d338416ab277f55deaf8496f827cd17260cb87"
source_comment_ids:
- "[Claude #1] Stage 5 commit-push — IMP-46 (#62) Axis R u1~u4"
commit_sha: d9d338416ab277f55deaf8496f827cd17260cb87
```
=== BASIS ===
Stage 4 PASS exit report defined exact staging surface (4 files) + commit message. Stage 5 executed: path-scoped `git add`, single commit `d9d3384` matching plan stat 1:1 (197+/7-, 13 production lines), `git push origin main` + `git push slide2 main` both succeeded with `f3ef4d9..d9d3384`. Post-push fetch confirmed both remotes synced to `d9d3384`. Carve-out (`cache.py`/`signature.py`) diff empty. No regression to staging policy or memory guardrails.
Invalidation: contract / partial / catalog SHA 변경 시 폐기
cache-side absorbed by #76 (strict equality at cache.py:108-148); router read-path enforcement gap = residual Axis R
Stage 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.py territory untouched.
Carve-out diff in d9d3384 for src/phase_z2_ai_fallback/cache.py and src/phase_z2_ai_fallback/signature.py = empty. IMP-47B (#76) 1186ad8 territory untouched.
📌 Goal vs result
Goal (issue body): cache layer 가 strict invalidation 능력을 갖되, contract/partial/catalog SHA 변경 시 폐기가 end-to-end 로 작동.
Residual gap (Stage 1): cache.py 자체는 strict 했지만 router (route_ai_fallback) 가 fingerprints 인자를 받지 않아 step12 producer 가 만든 SHA 3종이 read-path 까지 도달하지 못함 → stale entry 위험.
Result (u1~u4): router 가 fingerprints: dict | None = None kwarg 을 노출하고 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.
결정론적 hit 보장: 같은 input → 같은 output (cache.py 의 strict equality + 새로 닫힌 router read-path).
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:
The broad IMP-46 cache backend/signature/save-gate work is treated as absorbed by IMP-47B (#76) commit 1186ad8, per the binding exit reports.
The remaining Stage 1 root cause was the router read-path fingerprint enforcement gap: step12.py produced contract/partial/catalog fingerprints, but route_ai_fallback did not forward them into read_proposal.
Commit d9d3384 addresses that residual scope only: router accepts optional fingerprints, forwards it to cache read, and Step 12 passes the record fingerprints on AI-eligible routes.
Verified commit state:
HEAD, origin/main, and slide2/main all resolve to d9d338416ab277f55deaf8496f827cd17260cb87.
Commit file scope is exactly four files: src/phase_z2_ai_fallback/router.py, src/phase_z2_ai_fallback/step12.py, tests/phase_z2_ai_fallback/test_router.py, and tests/phase_z2_ai_fallback/test_step12.py.
Commit shortstat is 4 files changed, 197 insertions(+), 7 deletions(-).
Minor correction to prior per-file attribution: git diff --numstat reports router.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.py and src/phase_z2_ai_fallback/signature.py have no diff in d9d3384, so the IMP-47B carve-out remains untouched.
The unrelated dirty/untracked working-tree artifacts shown by git status --short are not in the commit.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
관련 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