Files
C.E.L_Slide_test2/.orchestrator/tmp/codex_last_stderr.txt
T

3255 lines
180 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
OpenAI Codex v0.130.0
--------
workdir: D:\ad-hoc\kei\design_agent
model: gpt-5.5
provider: openai
approval: never
sandbox: danger-full-access
reasoning effort: medium
reasoning summaries: none
session id: 019e4a97-7537-7d20-90e9-0023f72d2209
--------
user
Read the file D:\ad-hoc\kei\design_agent\.orchestrator\tmp\codex_prompt.txt and follow the instructions inside it exactly.
codex
Ill read the orchestrator prompt first, then Ill follow the workflow it defines.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'D:\\ad-hoc\\kei\\design_agent\\.orchestrator\\tmp\\codex_prompt.txt'" in D:\ad-hoc\kei\design_agent
succeeded in 167ms:
ISSUE #76: IMP-47B reject-as-AI-adaptation activation (reject route AI ?ш뎄???쒖꽦??
URL: https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/76
=== ISSUE BODY ===
**愿€??step**: Step 12 reject route ??reject = AI ?ш뎄???€???뺤콉 ?쒖꽦??
**source**: ?ъ슜???뺤콉 ?뺤젙 (2026-05-21) ??#61 ??design_reference_only_no_ai ?뺤콉 ?뺤젙
**roadmap axis**: R3 (AI 蹂댁젙/?ш뎄?? ??destination 蹂몄껜
**wave**: 1 (?ㅼ쭏 援щ룞 ?꾨떖 ?꾩닔)
**priority**: ??destination ?쒖뿰 path 蹂몄껜
**dependency**: **IMP-47A** verified (frontend ?덉젙???좏뻾), **#62 IMP-46** verified (cache 寃곗젙濡?蹂댁옣), #61 IMP-33 verified (AI scaffold), #39 IMP-30 verified (first-render invariant ??provisional zone)
**pair**: IMP-47A (frontend ?덉젙???좏뻾)
**理쒖쥌 ?뺤콉 (?ъ슜??紐낆떆 2026-05-21)**:
1. `use_as_is` / `light_edit` ??肄붾뱶 湲곕컲 ?먮룞 ?뚮뜑
2. `restructure` ??AI ?ш뎄???€??
3. **`reject` (all reject ?먮뒗 ?ъ슜?먭? reject frame ?좏깮) ??rank 1 reject frame ?좎? + AI ?ш뎄??*
4. ?덈? 湲덉?: ?ㅻⅨ rank frame ?쇰줈 ?먮룞 援먯껜 (frame swap) / MDX ?댁슜 ??젣 / frame visual ?꾩쓽 蹂€寃?
**scope** (7 ?묒뾽):
1. **reject route ?뺤젙** ??`design_reference_only_no_ai` ??`ai_adaptation_required` 怨꾩뿴
- `src/phase_z2_pipeline.py` ??`APPLICATION_MODE_BY_V4_LABEL` ?먮뒗 `_IMP05_ROUTE_HINTS` ?뺤젙
- `reject` label ??AI fallback hook ?€?곸쑝濡?routing
2. **all reject ??rank 1 reject 瑜?provisional unit ?쇰줈 ?대┝**
- ?꾩옱: V4 媛€ ?대뼡 section ?먮룄 rank 1 留ㅼ묶 紐삵븿 ??no_v4_candidate ??__empty__ provisional shell
- ?뺤젙: 紐⑤뱺 ?꾨낫媛€ reject ?쇰룄 rank 1 reject 瑜?provisional unit ?쇰줈 梨꾪깮 (IMP-30 first-render invariant ?€ ?곌껐)
3. **?ъ슜??override reject ??AI ?€??*
- frontend FramePanel ?먯꽌 ?ъ슜?먭? reject 移대뱶 紐낆떆 ?대┃ ??backend `--override-frame` ?꾨떖 ???대떦 reject frame ?좎? + AI ?ш뎄??
4. **AI proposal ?앹꽦 ??slot_payload ?곸슜**
- ?낅젰: ?좏깮??reject frame ??visual / contract / cardinality + MDX ?먮Ц
- 異쒕젰: builder_options / partial_overrides / slot_mapping (MDX ?댁슜??frame slot ??100% 蹂댁〈?섎㈃??諛곗튂)
- ?대? #61 scaffold ??`phase_z2_ai_fallback` ??hook 議댁옱 ??flag ON + reject route 異붽?留?
5. **final.html ?щ젋??*
- AI proposal ?곸슜 ??Step 13 render ?ъ떎??
- frontend iframe ?먮룞 ?щ줈??
6. **content 100% coverage 寃€利?*
- Step 20 slide_status ??`covered` set ??紐⑤뱺 section ?ы븿?섎뒗吏€ 寃€利?
- text_block / table / image / details ??젣 X (dropped ?덈? 猷?
7. **?ㅽ뙣 ??human_review / user override 紐낇솗 ?쒖떆**
- AI ?몄텧 ?ㅽ뙣 / proposal validation ?ㅽ뙣 / coverage 誘몃떖 ??frontend ??紐낇솗??notification ("??frame ?쇰줈 AI ?ш뎄???ㅽ뙣. ?ㅻⅨ frame ?좏깮 ?먮뒗 manual ?몄쭛 ?꾩슂")
**out of scope** (蹂?IMP):
- frontend stabilization ??**IMP-47A** (?좏뻾)
- cache ?먯껜 ??**#62 IMP-46** (?좏뻾)
- HTML 吏곸젒 ?몄쭛 ??R6 (異뷀썑)
- overflow chain (zone resize / popup / responsive fit) ??IMP-34 / 35 / 36
- mdx04 / 05 ??raw HTML 泥섎━ ??IMP-37 (stage0 normalizer)
**guardrail / validation**:
- ??**AI ?몄텧 = fallback path only**. normal path (use_as_is / light_edit) AI ?몄텧 = 0 (memory `feedback_ai_isolation_contract`)
- ??**MDX ?먮Ц 100% 蹂댁〈**. AI proposal 異쒕젰?€ slot mapping / builder_options / partial_overrides 留???MDX text 蹂€寃?X
- ??**?먮룞 frame swap 湲덉?**. V4 rank 1 reject frame ?좎? (?ㅻⅨ rank frame ?쇰줈 援먯껜 X)
- ??**frame visual ?꾩쓽 蹂€寃?湲덉?**. frame partial / contract 臾대? ??slot 梨꾩슦湲?/ cardinality 諛곗튂留?
- ??**dropped ?덈? 猷?*: text_block / table / image / details ??젣 X
- ??AI fallback ON + route gate + per-run budget + circuit breaker + cache + validation 紐⑤몢 ?곸슜 (runaway 諛⑹?)
- no-hardcoding: reject 泥섎━ path 媛€ sample-specific X
- mdx03 reject ?쇰꺼 frame ?쇰줈 end-to-end 寃€利? ?ъ슜???대┃ ??AI ?몄텧 ??cache hit/miss ??proposal apply ??final.html ??iframe ?쒖떆
**cross-ref**:
- source: ?ъ슜???뺤콉 ?뺤젙 (2026-05-21)
- depend: IMP-47A (?좏뻾 ??frontend ?덉젙??, #62 IMP-46 (?좏뻾 ??cache), #61 IMP-33 (AI scaffold), #39 IMP-30 (provisional zone), #38 IMP-29 (frontend evidence bridge)
- chain: IMP-34/35/36 (overflow chain ??IMP-47B ?€ 蹂?axis, restructure/reject ?€ overflow ??遺꾨━)
- ?곹뼢 ?뚯씪: `src/phase_z2_pipeline.py` (route hint), `src/phase_z2_ai_fallback/*` (#61 scaffold ?쒖슜), `src/config.py` (`ai_fallback_enabled = True`), `Front/client/src/components/FramePanel.tsx` (reject ?대┃ ??confirmation), frontend notification
**?쒖뿰 紐⑺몴**:
- mdx03 ??reject ?쇰꺼 frame ?ъ슜???대┃ ??AI ?ш뎄????寃곗젙濡좎쟻 ?ъ떎??(cache) ??final.html ?쒖떆
- ?쒖뿰 硫섑듃: "湲곗〈 ?꾨젅??留ㅼ묶?€ 肄붾뱶濡??먮룞, **誘몃ℓ移?reject ??AI 媛€ frame ??援ъ“??留욊쾶 ?댁슜 100% 蹂댁〈?섎㈃???ш뎄??*"
**review loop**:
- [ ] Codex 1李?review
- [ ] Claude ?ш???
- [ ] Codex ?ш?利?
- [ ] scope-locked
- [ ] ready-for-implementation
- [ ] implemented
- [ ] verified
=== COMPLETED STAGE EXIT REPORTS (binding contracts) ===
=== Stage 1: 臾몄젣 寃€??Exit Report ===
?뱦 **[?ㅼ??ㅽ듃?덉씠?? Stage 1: 臾몄젣 寃€???꾨즺**
??**?듭떖 寃곗젙 (Korean)**
- 洹쇰낯 ?먯씤?€ ???덉씠?댁쓽 reject 李⑤떒 + ??媛??꾨씫???몄텧 ?€?댁뼱濡??뺤젙. (1) `_IMP05_ROUTE_HINTS["reject"] = "design_reference_only"` ?뺤콉???섎せ?? (2) step12.py:137-140 ?먯꽌 `design_reference_only` 媛€?쒓? AI ?몄텧???⑤씫, (3) `gather_step12_ai_repair_proposals` 媛€ pipeline runtime ?먯꽌 ?몄텧?섏? ?딆쓬.
- Codex 蹂댁젙 ?섏슜: `ai_fallback_enabled=False` 媛€ default ?대?濡?Stage 2 ??reject fallback 留??쒖꽦?뷀븯??硫붿빱?덉쬁??紐낆떆?댁빞 ??(normal `use_as_is`/`light_edit` 寃쎈줈??AI ?몄텧 湲덉?).
- override?뭨eject 寃쎈줈??`provisional=True` 媛€ ?명똿?섏? ?딆쑝硫?step12.py:133-136 ??`not_provisional` 媛€?쒖뿉???ъ쟾??留됲옒 ??Stage 2 ??override 吏꾩엯 ??provisional 留덊궧 ?묒뾽 ?ы븿.
- AI 寃⑸━ contract (memory: feedback_ai_isolation_contract) 以€????frame ?먮룞 swap 湲덉?, frame ?좎? + AI ?щ같移섎쭔.
??**踰붿쐞 ?쒖쇅**
- `restructure` 寃쎈줈??AI ?쒖꽦?붾뒗 蹂?issue 踰붿쐞 諛?(蹂꾨룄 IMP).
- `ai_fallback_enabled` global default 蹂€寃쎌? 踰붿쐞 諛?(per-route ?쒖꽦?붾줈 ?쒖젙).
- Step 13 ?뚮뜑留?/ Step 14+ ?꾩냽 寃뚯씠???섏젙 ?놁쓬 (proposal.payload consume ??怨노쭔).
- ?꾨줎?몄뿏??override UI ?쒓컖 蹂€寃??놁쓬 ??湲곗〈 FramePanel?묱ome.tsx ?€?댁뼱 洹몃?濡?
??**?ㅼ쓬 ?④퀎**
- Stage 2: 援ы쁽 怨꾪쉷 ???쒖꽦??硫붿빱?덉쬁 ?좏깮 (per-route allowlist vs reject-only flag), provisional 留덊궧 吏€?? pipeline call site, regression ?뚯뒪?????뺤젙.
```
=== EXIT REPORT (English, binding contract) ===
root_cause:
- layer_1_route_policy: _IMP05_ROUTE_HINTS["reject"] = "design_reference_only"
blocks reject from reaching AI adaptation route.
- layer_2_step12_gate: step12.py:137-140 short-circuits route_hint ==
"design_reference_only" with skip_reason="design_reference_only_no_ai"
BEFORE the AI call path.
- layer_3_missing_call: gather_step12_ai_repair_proposals
(step12.py:69) has no runtime caller in src/phase_z2_pipeline.py;
Step 12 currently terminates at deterministic map_mdx_to_slots
(pipeline.py:3855) and writes step12_slot_payload.json
(pipeline.py:4263-4281) without AI consultation.
- layer_4_provisional_gate: step12.py:133-136 skips
non-provisional units first; override?뭨eject path must set
unit.provisional=True for AI to be reached after route-hint flip.
key_files:
- src/phase_z2_pipeline.py (route hint map L572-577; provisional
selection L717-740; Step 12 call site L3855; payload writer
L4263-4281; override entry L3621)
- src/phase_z2_ai_fallback/step12.py (route gates L60, L133-144;
gather_step12_ai_repair_proposals L69)
- src/config.py (ai_fallback_enabled default OFF, L19)
- src/phase_z2_ai_fallback/router.py (short-circuit when flag OFF,
L61-63)
- tests/test_phase_z2_ai_fallback_config.py (default-off lock,
L4-24)
scope_lock:
- flip reject route hint to ai_adaptation_required (or add
reject-specific AI route key) ??minimal route policy change.
- flip / remove step12.py design_reference_only short-circuit so
reject reaches AI path.
- wire gather_step12_ai_repair_proposals into Step 12 of pipeline
runtime; consume proposal.payload into slot_payload prior to
Step 13 render.
- ensure override?뭨eject path marks the unit provisional=True so
the existing provisional gate admits it.
- activation mechanism MUST preserve AI=0 on use_as_is /
light_edit normal paths (default-off contract intact).
out_of_scope:
- restructure route AI activation (separate IMP).
- global ai_fallback_enabled default flip.
- Step 13+ render / visual gate logic.
- frontend override UI visual redesign.
- cache schema changes (IMP-46 backend reused as-is).
guardrails:
- AI isolation contract: frame auto-swap forbidden; AI re-places
content into the existing frame only.
- auto-pipeline first: no review_required / review_queue
insertion; explicit selected / filtered_* / adapter_needed.
- factual verification: every Stage 2 path/line citation must be
grep-verified before commit.
- hardcoding ban: activation gating must source from Settings,
never inline literals.
- 1 turn = 1 step: Stage 1 closes here; no Stage 2 drafting in
this turn.
unresolved_questions:
- activation mechanism shape: per-route allowlist
(ai_fallback_routes: set[str]) vs reject-only boolean flag
(ai_fallback_reject_enabled: bool)?
- provisional marking point: at override entry (pipeline.py:3621)
or downstream in selection (pipeline.py:717-740)?
- when AI proposal absent/errored, fall back to deterministic
map_mdx_to_slots or surface filtered_no_ai status?
evidence:
- pipeline.py:572-577 (route hint map, verified inline)
- step12.py:133-144 (provisional + design_reference gates,
verified inline)
- config.py:19 (ai_fallback_enabled=False, verified inline)
- rg: no runtime call of gather_step12_ai_repair_proposals from
src/phase_z2_pipeline.py (Codex #1).
source_comment_ids:
- Claude #1 (Stage 1 problem review)
- Codex #1 (Stage 1 verification + corrections)
commit_sha: 15ef7c6 (current HEAD; pre-IMP-47B baseline)
```
Stage 1 closed. Next turn = Stage 2 援ы쁽 怨꾪쉷 only when user issues GO.
=== Stage 2: ?쒕? 湲곕컲 怨꾪쉷 ?섎┰ Exit Report ===
?뱦 **[?ㅼ??ㅽ듃?덉씠?? Stage 2: ?쒕? 湲곕컲 怨꾪쉷 ?섎┰ ?꾨즺**
???듭떖 寃곗젙
- IMP-47B reject 猷⑦듃瑜?`ai_adaptation_required` 濡??꾪솚?섍퀬 Step 12 ??design_reference_only skip ?쒓굅. ?뺤긽 寃쎈줈 AI=0 ?뚭???config.py:19 + router.py:61-65 媛€?쒕줈 蹂댁〈.
- AI repair ?먮쫫 = gather ??PARTIAL_OVERRIDES apply (?ㅻⅨ kind ??loudly fail) ??audit artifact ??coverage invariant ??slide_status surfacing ??cache ?€??(u4~u8, u13).
- Override ?좏깮 reject ??provisional ?쒖떆 (u3), ?쇳빀 direct+reject ?щ씪?대뱶??泥??뚮뜑?먯꽌 `allow_provisional_fill` 濡??몄젙 (u12). ???곸뿭 reject ??IMP-30 retry 媛€ 而ㅻ쾭.
- ?꾨윴?몄뿏?쒕뒗 `ai_repair_status` ?좎뒪??+ reject-click confirm (u11). ??unit ??0以?+ ???뚯씪 mechanical check ?듦낵.
??踰붿쐞 ?쒖쇅
- restructure ?쒖꽦?? ?꾩뿭 ?뚮옒洹?default flip, cache ?ㅽ궎留?蹂€寃? overflow chain, ?꾨윴?몄뿏??visual redesign, mdx04/05 normalizer, HTML 吏곸젒 ?몄쭛, Step 13+ visual 濡쒖쭅.
- builder-options / slot-mapping kind ??reject 踰붿쐞 諛뽰씠吏€留?u5 ?먯꽌 `kind_not_supported_for_reject` 濡?紐낆떆.
???ㅼ쓬 ?④퀎
- Stage 3 (援ы쁽) 吏꾩엯. u1~u13 ?쒖감 ?ㅽ뻾, 媛?unit ?⑥쐞 commit. Codex ?⑹쓽 ?꾨즺.
=== EXIT REPORT (English, binding contract) ===
enumerated_axes:
- reject-route activation (u1, u2)
- override admission (u3, u12)
- AI repair runtime wiring (u4, u5, u6, u13)
- coverage / failure surfacing (u7, u8)
- frontend human_review surfacing (u11)
- regression + E2E coverage (u9, u10)
files:
- src/phase_z2_pipeline.py
- src/phase_z2_ai_fallback/step12.py
- tests/test_phase_z2_ai_fallback_config.py
- tests/phase_z2_ai_fallback/test_step12.py
- tests/test_imp47b_override_provisional.py
- tests/test_imp47b_step12_ai_wiring.py
- tests/test_imp47b_payload_apply.py
- tests/test_imp47b_coverage_invariant.py
- tests/test_imp47b_failure_surface.py
- tests/test_imp47b_end_to_end.py
- tests/test_imp47b_mixed_reject_fill.py
- tests/test_imp47b_cache_save_gate.py
- Front/client/src/services/designAgentApi.ts
- Front/client/src/pages/Home.tsx
- Front/client/src/components/FramePanel.tsx
- Front/client/tests/imp47b_human_review_toast.test.tsx
per_file_changes: See IMPLEMENTATION_UNITS YAML below ??each `summary` describes the precise per-file change; `files` lists the scope.
test_plan: Per-unit test files listed in YAML `tests:`. Aggregate: config flip (u1), step12 reject AI-eligible + normal AI=0 (u2, u9), override provisional (u3), gather/apply/audit (u4-u6), coverage invariant (u7), failure surfacing (u8), E2E reject smoke with mocked client + final.html + full coverage + no text loss (u10), frontend toast/confirm (u11), mixed reject fill (u12), cache save gate (u13).
rollback: Revert the IMP-47B implementation commit(s) for touched backend/frontend/test files; no migration rollback expected.
baseline_tests: Existing `tests/test_phase_z2_ai_fallback_config.py`, `tests/phase_z2_ai_fallback/test_step12.py`, `tests/test_family_contract_baseline.py` (modified files in tree).
follow_up_candidates: restructure activation, global flag default flip, builder-options / slot-mapping kind support, cache schema evolution, mdx04/05 normalizer.
implementation_units: 13 units (u1~u13), all ??0 lines and ?? files (mechanical check pass).
unresolved_questions: None blocking. (builder-options/slot-mapping support deferred per out-of-scope; u5 will surface `kind_not_supported_for_reject`.)
guardrails:
- Normal-path AI=0 must remain (config.py:19 + router.py:61-65 unchanged).
- MDX ?먮Ц 臾댁넀??蹂댁〈 (u7 coverage invariant + u10 no-text-loss assertion).
- reject = AI ?ш뎄???€?? frame ?좎? (?먮룞 frame swap 湲덉?) per [[feedback_ai_isolation_contract]].
- ?먮룞 ?뚯씠?꾨씪???곗꽑 ??human_review surfacing ??selected/filtered_* 遺꾨쪟濡??먮룞 寃곗젙 ([[feedback_auto_pipeline_first]]).
- 1 commit = 1 unit ([[feedback_workflow_atomicity_rules]]).
evidence: src/phase_z2_pipeline.py:3621-3670 (override admission site), src/phase_z2_pipeline.py:4262 (Step12 write site), src/config.py:19 (AI flag default), src/phase_z2_ai_fallback/router.py:61-65 (normal-route guard).
source_comment_ids: Claude #4, Codex #5 (issue #76).
commit_sha: pending Stage 3 (no implementation commit yet for IMP-47B).
=== IMPLEMENTATION_UNITS ===
reviewed_units:
- id: u1
summary: Flip reject route hint to ai_adaptation_required
files: [src/phase_z2_pipeline.py]
tests: [tests/test_phase_z2_ai_fallback_config.py]
estimate_lines: 10
- id: u2
summary: Remove Step 12 design_reference_only reject skip
files: [src/phase_z2_ai_fallback/step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 12
- id: u3
summary: Mark override-selected reject units provisional
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_override_provisional.py]
estimate_lines: 28
- id: u4
summary: Wire gather_step12_ai_repair_proposals into Step 12 runtime
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_step12_ai_wiring.py]
estimate_lines: 48
- id: u5
summary: Apply PARTIAL_OVERRIDES; unsupported proposal kinds fail loudly
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_payload_apply.py]
estimate_lines: 38
- id: u6
summary: Write step12_ai_repair.json audit artifact
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_step12_ai_wiring.py]
estimate_lines: 25
- id: u7
summary: Post-AI source_section_ids coverage invariant
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_coverage_invariant.py]
estimate_lines: 22
- id: u8
summary: Surface slide_status.ai_repair_status failures
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_failure_surface.py]
estimate_lines: 32
- id: u9
summary: Step 12 reject eligibility and normal-path AI=0 regression
files: [tests/phase_z2_ai_fallback/test_step12.py]
tests: [tests/phase_z2_ai_fallback/test_step12.py]
estimate_lines: 28
- id: u10
summary: E2E reject smoke with mocked client, final.html, full coverage, no text loss
files: [tests/test_imp47b_end_to_end.py]
tests: [tests/test_imp47b_end_to_end.py]
estimate_lines: 48
- id: u11
summary: Frontend ai_repair_status notification and reject-click confirm
files: [Front/client/src/services/designAgentApi.ts, Front/client/src/pages/Home.tsx, Front/client/src/components/FramePanel.tsx]
tests: [Front/client/tests/imp47b_human_review_toast.test.tsx]
estimate_lines: 45
- id: u12
summary: Initial plan_composition allow_provisional_fill for mixed direct+reject
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_mixed_reject_fill.py]
estimate_lines: 24
- id: u13
summary: Persist validated proposals through save_proposal after visual/cache gates
files: [src/phase_z2_pipeline.py]
tests: [tests/test_imp47b_cache_save_gate.py]
estimate_lines: 38
rationale:
- u1/u2 activate reject routing while config.py:19 and router.py:61-65 keep normal AI=0.
- u12 covers mixed direct+reject first-render admission; IMP-30 retry covers all-empty fallback.
- u3 admits override-selected reject frames; current override only mutates frame metadata.
- u4/u5/u6/u13 cover gather, apply/audit, and cache persistence before render.
- u7/u10 cover 100% content coverage/no text loss; u8/u11 cover human_review surfacing.
- Mechanical check passes: implementation_units exists; every unit is <=50 lines and <=3 files.
out-of-scope:
- Valid: restructure activation, global flag default flip, cache schema changes, overflow chain, frontend visual redesign, mdx04/05 normalizer, HTML direct edit, Step 13+ visual logic.
- Builder-options and slot-mapping application stay outside reject scope, but u5 must mark them unsupported/human_review.
rollback:
- Revert the IMP-47B implementation commit for touched backend/frontend/test files; no migration rollback expected.
=== REWIND: FAILURE REPORT (from code-edit) ===
[Verification Failure Report]
Issue: #76
Failed stage: code-edit
Rewind to: code-edit
Failure evidence:
[Codex #15] Stage 3 code-edit verification - IMP-47B u10
Verified the single executed unit u10 only.
Result: u10 is incomplete against the binding Stage 2 unit contract.
Findings:
- `tests/test_imp47b_end_to_end.py` does exercise the synthetic gather -> apply -> coverage invariant -> `ai_repair_status` helper chain with a mocked `route_ai_fallback`, and that narrow test passes.
- However, u10's binding summary is "E2E reject smoke with mocked client, final.html, full coverage, no text loss." The new test never calls `render_slide`, never writes a `final.html`, and never asserts rendered HTML output. It only asserts that `zones_data[0]["slot_payload"]` contains AI proposal values, then treats that as structural proof that `final.html` would render those values.
- The implementation report also says the heavy `run_phase_z2_mvp1` + Selenium path is deferred, but the u10 contract still requires a `final.html` smoke. This is a Stage 3 implementation gap, not a plan correction.
- Secondary scope concern: the new test file is 179 lines, while the Stage 2 implementation-unit plan described u10 as a bounded 48-line unit. The functional blocker is the missing `final.html` assertion.
Expected fix:
- Keep the mocked-client approach, but extend u10 so it renders or otherwise creates a real `final.html` artifact and asserts that the AI-repaired slot content appears in that HTML while
Fix the issues above before re-attempting.
=== CURRENT: Stage 3: 肄붾뱶 ?섏젙 / ?댁뒋 遺꾧린 Round #3 ===
Task: Verify only the SINGLE unit executed in this turn. If correct AND remaining_units non-empty ??rewind_target: continue_same / FINAL_CONSENSUS: NO. If correct AND remaining_units empty (all units complete) ??FINAL_CONSENSUS: YES. If incorrect ??rewind_target: stage_2_plan (plan wrong) / stage_3_edit (this unit incomplete) / retry_same (technical).
MANDATORY EVIDENCE LINE FORMAT ??include EXACTLY one of:
Remaining units: [u2, u3, u4] (bracketed list when units remain)
Remaining units: none (when all units complete)
Free-form sentences mixing other words on this line will fail orchestrator parse.
=== MID-STAGE COMPACTION ===
# IMP-47B Stage 3 Code-Edit Discussion Summary (u1?뱔5)
## Agreed (Claude executed + Codex verified)
**u1 ??Flip reject route hint**
- `_IMP05_ROUTE_HINTS["reject"]: "design_reference_only" ??"ai_adaptation_required"` in `src/phase_z2_pipeline.py`
- Test added: `test_reject_route_hint_routes_to_ai_adaptation`; v4-fallback baseline string synced
- AI=0 normal-path preserved by master flag default OFF + router short-circuit
**u2 ??Remove Step 12 design_reference_only skip**
- Deleted `_DESIGN_REFERENCE_ROUTE` constant + bespoke skip branch in `step12.py`
- Legacy `design_reference_only` now falls through to catch-all `route_not_ai_adaptation:<hint>`
- Test renamed; local mapping intentionally left until u9
**u3 ??Promote override-selected reject units to provisional**
- New helper `_apply_frame_override_to_unit(unit, new_tid, v4)` with 3-tier probe: v4_candidates ??full 32-judgment reject probe (sets `provisional=True`, meta `v4_reject_judgment_provisional`) ??raw fallback
- Non-reject + unknown-template paths byte-identical to prior behaviour
**u4 ??Wire gather into Step 12 runtime**
- Import `gather_step12_ai_repair_proposals`; added `_load_frame_partial_html` + `_run_step12_ai_repair`
- Call site placed right before Step 12 slot payload artifact write
- Records only gathered, not yet consumed (PARTIAL_OVERRIDES apply = u5 scope)
**u5 ??Apply PARTIAL_OVERRIDES, fail loudly on unsupported kinds**
- `_REJECT_SUPPORTED_PROPOSAL_KINDS = {"partial_overrides"}` + `_apply_ai_repair_proposals_to_zones`
- Five terminal `apply_status` states: `no_proposal`, `unsupported_kind_for_reject_route:<kind>`, `no_zone_match`, `applied:partial_overrides`
- Merge via `dict.update` into existing `zone["slot_payload"]`; frame visual/contract untouched (auto frame-swap forbidden per AI isolation contract)
- Stderr human-review line on unsupported kinds
## Rejected / out-of-scope
- Auto frame-swap on reject (forbidden ??`feedback_ai_isolation_contract`, 2026-05-21 correction)
- `builder_options_patch` / `slot_mapping_proposal` proposal kinds (deferred; not applied even if returned)
- Unrelated pre-existing IMP-46 cache-key hunks + dirty working tree (Codex explicitly excluded from each u's verification scope)
- Production reject eligibility mapping in test `_ROUTE_HINTS` (u9 scope)
## Open / pending
- u6: `step12_ai_repair.json` audit artifact write (u5 applied records but no audit file yet)
- u7?뱔10 (presumed): activation gating story, production reject eligibility flip, frontend override surface, end-to-end test with `ai_fallback_enabled=True`
- Runtime activation never triggered in u1?뱔5 ??master flag still default OFF
## Evidence
- Verification commands per unit: `git status --short`, scoped `git diff`, targeted `rg` grep, `pytest -q` on per-unit tests + adjacent baselines
- Key invariants confirmed each turn: `src/config.py:19` (`ai_fallback_enabled=False`), `src/phase_z2_ai_fallback/router.py:61-65` (short-circuit), `validate.py:61-74` (declared-slot completeness)
- All new tests passing in scope; cross-axis regressions not claimed (scope-qualified per `feedback_scope_qualified_verification`)
=== RECENT COMMENTS (current stage) ===
[claude r1] [Claude #17] Stage 3 code-edit ??IMP-47B u11
Executing unit: u11
??unit_executed
- id: u11
- summary: Frontend ai_repair_status notification and reject-click confirm
- status: implemented per Stage 2 binding contract ??three source-file edits + one test file at the contract-declared path. All four functional contract items met: (a) `ai_repair_status` surfaces from `step20_slide_status.json` via `RunMeta`; (b) pure helper `formatAiRepairHumanReviewMessage(...)` maps the three failure axes (error / coverage_violated / unsupported_kind) to user-facing notification text and returns null on success / no-AI paths; (c) `Home.tsx` calls `toast.error(msg)` after `setRunMeta(...)` when the helper returns a non-null string; (d) `FramePanel.tsx` adds `window.confirm` gate before forwarding a reject-label frame click to `onFrameSelect`. AI isolation contract preserved ??confirm is UX-only, no frame auto-swap, parent owns override apply.
??files_changed
- Front/client/src/services/designAgentApi.ts (modified ??62 inserts, 0 deletes)
- Front/client/src/pages/Home.tsx (modified ?? 8 inserts, 0 deletes)
- Front/client/src/components/FramePanel.tsx (modified ??22 inserts, 1 delete)
- Front/client/tests/imp47b_human_review_toast.test.tsx (new ??135 lines, contract-declared path)
??diff_summary
1. **Front/client/src/services/designAgentApi.ts** ??Two additions and one in-line wiring:
- `AiRepairStatus` interface (lines 226??51). Verbatim mirror of `_summarize_ai_repair_status(...)` return shape at `src/phase_z2_pipeline.py:768-776` (u8). Fields: `status` (open-vocab union with the five known enums + string fallback), `counts` (`total / applied / no_proposal / no_zone_match / unsupported_kind / error`), `unsupported_kind_records[]` (`unit_index? / source_section_ids[] / apply_status`), `error_records[]` (`unit_index? / source_section_ids[] / error`), `coverage_status`, `dropped_section_ids[]`, `human_review_required` boolean. No frontend redefinition ??enum names match `src/phase_z2_pipeline.py:758-767` (status mapping ladder).
- `RunMeta.ai_repair_status: AiRepairStatus | null` field appended (line 269-271). Nullable so legacy runs (pre-IMP-47B) and pipeline aborts before Step 12 still parse cleanly.
- `formatAiRepairHumanReviewMessage(ai: AiRepairStatus | null | undefined): string | null` exported helper (lines 282-301). Pure function ??no DOM, no `toast.*` call inside ??so the unit test exercises it without React Testing Library (which is NOT in `Front/package.json` devDependencies). Mapping:
- `null/undefined` ??`null` (no toast)
- `human_review_required === false` ??`null` (success / no-AI path)
- `status === "error"` ??`"AI ?ш뎄???몄텧 ?ㅽ뙣 (N嫄? ???ㅻⅨ frame ?좏깮 ?먮뒗 ?섎룞 ?몄쭛 ?꾩슂"`
- `status === "coverage_violated"` ??`"AI ?ш뎄????肄섑뀗痢??꾨씫 (dropped: 03-2) ???ㅻⅨ frame ?좏깮 ?먮뒗 ?섎룞 ?몄쭛 ?꾩슂"`
- `status === "unsupported_kind"` ??`"AI ?쒖븞 ?뺤떇 誘몄???(N嫄? ???ㅻⅨ frame ?좏깮 ?먮뒗 ?섎룞 ?몄쭛 ?꾩슂"`
- unknown future status with `human_review_required: tr
---
[codex r1] [Codex #18] Stage 3 code-edit verification - IMP-47B u11
Verified only the single executed unit u11 against the Stage 2 contract.
Result: u11 is correct and scoped.
Findings:
- `Front/client/src/services/designAgentApi.ts` adds `AiRepairStatus`, carries `RunMeta.ai_repair_status` from `step20_slide_status.json`, and maps the three required failure axes (`error`, `coverage_violated`, `unsupported_kind`) to human-review notification text while returning `null` for success/no-AI paths.
- `Front/client/src/pages/Home.tsx` calls `formatAiRepairHumanReviewMessage(runMeta.ai_repair_status)` after `setRunMeta(...)` and emits `toast.error(...)` only when the helper returns a message. This surfaces failures without adding a review queue or changing auto-pipeline behavior.
- `Front/client/src/components/FramePanel.tsx` gates explicit selection of a non-current `reject` frame through `window.confirm(...)` before forwarding to `onFrameSelect(candidate.id)`. It does not mutate frame state directly and does not introduce frame auto-swap behavior.
- `Front/client/tests/imp47b_human_review_toast.test.tsx` passes under the configured Vitest runner and covers the helper?셲 success/no-AI and three failure-axis message behavior. The reject-click confirm path is verified by code inspection, not by a DOM test.
- Broader `npm run check` is currently blocked by an unrelated pre-existing type error in `Front/client/src/components/BottomActions.tsx`: `serializeSlidePlan` is imported from `../utils/slidePlanUtils` but is not exported there. That file is outside the u11 diff and does not contradict this unit?셲 focused verification.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git diff -- Front/client/src/services/designAgentApi.ts Front/client/src/pages/Home.tsx Front/client/src/components/FramePanel.tsx Front/client/tests/imp47b_human_review_toast.test.tsx`
- `rg -n "AiRepairStatus|formatAiRepairHumanReviewMessage|ai_repair_status|human_review_required" Front/client/src/services/designAgentApi.ts Front/client/src/pages/Home.tsx Front/client/tests/imp47b_human_review_toast.test.tsx`
- `rg -n "confirm|reject|onFrameSelect" Front/client/src/components/FramePanel.tsx`
- `npx vitest run client/tests/imp47b_human_review_toast.test.tsx` from `D:\ad-hoc\kei\design_agent\Front`
- `npm run check` from `D:\ad-hoc\kei\design_agent\Front`
- `rg -n -C 3 "_summarize_ai_repair_status|unsupported_kind_records|human_review_required" src/phase_z2_pipeline.py`
- `rg -n "interface RunMeta|runMeta\s*[:=]|setRunMeta|ai_repair_status" Front/client/src Front/client/tests`
Files checked:
- `D:\ad-hoc\kei\design_agent\Front\client\src\services\designAgentApi.ts`
- `D:\ad-hoc\kei\design_agent\Front\client\src\pages\Home.tsx`
- `D:\ad-hoc\kei\design_agent\Front\client\src\components\FramePanel.tsx`
- `D:\ad-hoc\kei\design_agent\Front\client\tests\imp47b_human_review_toast.test.tsx`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.py`
Test results:
- `npx vitest run client/tests/imp4
---
[claude r2] [Claude #19] Stage 3 code-edit ??IMP-47B u12
Executing unit: u12
??unit_executed
- id: u12
- summary: Initial plan_composition allow_provisional_fill for mixed direct+reject
- status: implemented per Stage 2 binding contract ??one source-file inserted block (~50 lines including comments) at `src/phase_z2_pipeline.py` plus one new test file at the contract-declared path `tests/test_imp47b_mixed_reject_fill.py`. Within the unit's 24-line estimate envelope when comments are excluded (logic-only is ~30 lines; the doc block adds ~12 lines explaining the gate predicate, the no-op axis, and the relationship to the IMP-30 u4 retry below).
??files_changed
- src/phase_z2_pipeline.py (modified ??+50 lines, 0 deletions; inserted block only, no existing-line rewrites)
- tests/test_imp47b_mixed_reject_fill.py (new ??250 lines including the contract-mandated module docstring + 4 pytest cases; logic-only lines ~145, the rest is the IMP-47B `[[feedback_factual_verification]]` doc block + stub naming docstrings)
??diff_summary
1. **src/phase_z2_pipeline.py** ??One inserted block at the seam between the initial `plan_composition(...)` telemetry and the Step 7-A `--override-layout` block (right after the closing brace of `comp_debug["v4_fallback_summary"] = { ... "policy": (...) }` and right before the `# ?€?€ Step 7-A axis : layout override ?€?€` comment). No existing-line rewrites ??purely additive. The block:
- Gate predicate (3 boolean clauses + 1 list comprehension):
- `units and layout_preset is not None` ??short-circuits the all-reject case so the IMP-30 u4 retry block below (currently at the `if not units or layout_preset is None:` guard) keeps owning that path. Mirrors the existing retry's `section_assignment_plan is None` semantic.
- `not override_section_assignments` ??skips when the operator drove the assignment through `--override-section-assignments`, preserving the operator's plan and matching the IMP-30 u4 retry's identical skip. Mirrors `_apply_frame_override_to_unit`'s isolation contract ([[feedback_ai_isolation_contract]]).
- `_u12_uncovered_ids` non-empty ??built by subtracting the initial-pass `union(u.source_section_ids for u in units)` from `[s.section_id for s in sections]`. Empty list ??no-op (all-direct case).
- Retry seam ??a closure `_lookup_fn_mixed_admission(sid)` that calls `lookup_v4_match_with_fallback(...)` with `allow_provisional=True` (so chain_exhausted sections synthesize a provisional rank-1 `V4Match` per IMP-30 u1) and writes the trace into the shared `v4_fallback_traces` dict ??same shape as `_lookup_fn_provisional` at the IMP-30 u4 retry block.
- `plan_composition(...)` re-invocation with `allow_provisional_fill=True` (the kwarg added by IMP-30 u3 at `src/phase_z2_composition.py:809`) so `select_composition_units(...)` runs the last-resort provisional-fill pass for any sections still uncovered by the new (provisional-aware) lookup.
- Promotion branch ??when the retry
---
[codex r2] 癤?Codex #20] Stage 3 code-edit verification - IMP-47B u12
Verified only the single executed unit u12 against the Stage 2 contract.
Result: u12 is correct and scoped.
Findings:
- `src/phase_z2_pipeline.py` adds the mixed direct+reject admission gate immediately after initial `plan_composition(...)` telemetry and before layout override handling. The gate requires existing units, a non-null layout preset, no `override_section_assignments`, and at least one uncovered section, so all-direct slides are a no-op and operator section overrides are not overwritten.
- The retry path uses `lookup_v4_match_with_fallback(..., allow_provisional=True)` and calls `plan_composition(..., allow_provisional_fill=True)`, which is the required path for admitting uncovered reject sections as provisional rank-1 units while preserving the selected frame identity.
- The all-reject case is left to the existing IMP-30 retry path because the u12 gate short-circuits when the initial units list is empty. That matches the Stage 2 u12 scope.
- `tests/test_imp47b_mixed_reject_fill.py` exercises the core composition mechanic with synthetic sections: pre-u12 leaves the reject section uncovered, the u12-style retry covers both sections, marks the reject unit provisional, keeps `label="reject"`, and preserves the reject frame template id. It also covers the all-direct no-op gate and the all-reject empty-initial-units gate.
- The test file does not run the full pipeline, but for u12 this is acceptable: the source inspection confirms the pipeline glue uses the same two required switches (`allow_provisional=True` lookup and `allow_provisional_fill=True` composition retry) at the declared call site.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git diff -- src/phase_z2_pipeline.py tests/test_imp47b_mixed_reject_fill.py`
- `rg -n "allow_provisional_fill|mixed|_u12|override_section_assignments|lookup_v4_match_with_fallback|plan_composition" src/phase_z2_pipeline.py tests/test_imp47b_mixed_reject_fill.py`
- `pytest -q tests/test_imp47b_mixed_reject_fill.py`
- `Get-Content -LiteralPath 'tests\\test_imp47b_mixed_reject_fill.py' | Select-Object -First 340`
- `Get-Content -LiteralPath 'src\\phase_z2_composition.py' | Select-Object -Skip 760 -First 120`
- `Get-Content -LiteralPath 'src\\phase_z2_pipeline.py' | Select-Object -Skip 3500 -First 70`
Files checked:
- `D:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_composition.py`
- `D:\ad-hoc\kei\design_agent\tests\test_imp47b_mixed_reject_fill.py`
Test results:
- `pytest -q tests/test_imp47b_mixed_reject_fill.py` -> `4 passed in 0.03s`
Remaining units: [u6, u7, u8, u9, u10, u13]
rewind_target: continue_same
FINAL_CONSENSUS: NO
---
[claude r3] [Claude #21] Stage 3 code-edit ??IMP-47B u13
Executing unit: u13
??unit_executed
- id: u13
- summary: Persist validated proposals through save_proposal after visual/cache gates
- status: implemented per Stage 2 binding contract ??one new helper + one call site in `src/phase_z2_pipeline.py`, plus the contract-declared test file at `tests/test_imp47b_cache_save_gate.py`. Logic-only line count ~36 (helper body ~30 + call site ~6), within the unit's 38-line estimate envelope; the doc blocks add the IMP-46 dual-gate truth table reference and the IMP-47B AI-isolation tie-ins. Single new helper, never inlines `save_proposal` at the call site ??keeps the gate truth table in one place.
??files_changed
- src/phase_z2_pipeline.py (modified ??helper at lines 708-767, call site at lines 5268-5285)
- tests/test_imp47b_cache_save_gate.py (new ??213 lines including module docstring + 6 pytest cases)
??diff_summary
1. **src/phase_z2_pipeline.py** ??Two additive blocks, no rewrites of existing lines:
- **New helper `_persist_ai_repair_proposals_to_cache(...)`** inserted between `_check_post_ai_coverage_invariant` (line 705) and `_summarize_ai_repair_status` (line 770). The helper:
- Imports `cache.save_proposal`, `AiFallbackCacheGateError`, and `AiFallbackProposal` lazily inside the function so module-load-time AI=0 normal-path semantics stay unchanged (mirrors the IMP-47B u8 surface where the audit chain is kept off the import graph until pipeline runtime invokes it).
- For each record: gate-checks `apply_status.startswith("applied:")` AND a serialised `proposal` dict AND a non-empty `cache_key` AND a `fingerprints` dict ??any miss ??`cache_save_status="not_applied"` (skip; no `save_proposal` call ??no filesystem touch).
- Reconstructs `AiFallbackProposal.model_validate(record["proposal"])`; pydantic validation failure ??`cache_save_status="invalid_proposal:<ExcType>"` (loud surfacing, no raise).
- Calls `cache.save_proposal(cache_key, proposal_obj, visual_check_passed=..., user_approved=..., auto_cache=..., fingerprints=...)`. The IMP-46 dual-gate truth table (`src/phase_z2_ai_fallback/cache.py:69-73`) is honoured one-to-one ??three pass cells, five raise cells ??because the helper forwards the booleans directly without re-deriving them.
- Catches `AiFallbackCacheGateError` and writes `cache_save_status=f"gate_blocked:{exc}"` so the gate-block reason (visual_check / user_approved) flows into the same record dict; the cache module guarantees no filesystem write happened when the error fires (`save_proposal` raises BEFORE any `mkdir`/`write_text`, contract at `cache.py:181-182`).
- On success: `cache_save_status="saved"`. Records remain dicts; no new keys are required at the existing IMP-47B u4/u5/u7 callers downstream ??`_summarize_ai_repair_status` does not read `cache_save_status` (kept out of scope; cache layer is a hint, not part of the human-review surface).
- **New call site** after
DO NOT read all Gitea comments. Exit reports are binding contracts.
=== WORK PRINCIPLES ===
RULE 0 ??PIPELINE-CONSTRUCTION (overrides all)
Build GENERAL Phase Z pipeline, NOT sample-passing. Never hardcode MDX 03/04/05.
Evaluate against all 32 frames. Failure must be explainable.
RULE 1: English only. RULE 2: Auto pipeline. RULE 3: Status=3-axis.
RULE 4: Scope-qualified. pytest -q tests. COMMIT SCOPE only.
RULE 5: Factual: value+path+upstream. RULE 6: git add specific files only.
RULE 7: No hardcoding. RULE 8: AI finds 1px first. RULE 9: LLM classifies, code composes.
RULE 10: Don't uncritically accept. RULE 11: Checkpoint. RULE 12: Full paths. RULE 13: Anchor sync.
PZ-1: AI=0 normal. PZ-2: 1turn=1step. PZ-3: No speculative. PZ-4: No silent shrink.
=== COMMENT FORMAT (P5b 2026-05-20 ??STRICT, OVERRIDES ALL STAGE-SPECIFIC BODY RULES) ===
The FIRST non-empty line of EVERY Gitea comment MUST start with one of:
[Claude #N] <stage description>
[Codex #N] <stage description>
This rule applies to ALL stages (Stage 1 ~ Stage 6) and ALL issue types
(regular, execution-issue, audit-only). No prefix, no decoration, no banner,
no audit anchor before the agent header. Examples:
CORRECT:
[Codex #3] Stage 2 simulation-plan review ??IMP-24
?뱦 Verification table
...
WRONG (orchestrator detect_agent will fail; stage cannot advance):
?뱦 **[Claude #3] Stage 2 ...**
## [Codex #3] Stage 2 ...
=== IMPLEMENTATION_UNITS === (header missing entirely)
Audit anchor: ... (preface before header)
This first-line-strict rule OVERRIDES any stage-specific "body MUST contain
ONLY" rule (e.g., COMPACT_PLAN_RULE). Those body rules apply AFTER the
mandatory first-line agent header. Decorations / banners / anchors go on
line 2 or later.
=== CONSENSUS + REWIND (2026-05-16 lock) ===
Final line of every Codex review comment MUST be exactly one of:
FINAL_CONSENSUS: YES
FINAL_CONSENSUS: NO
YES REQUIRES === EVIDENCE === block (commands run, files checked, tests/commit/verified facts as stage requires).
NO evidence = REJECTED.
If NO, the comment MUST also include a line BEFORE FINAL_CONSENSUS:
rewind_target: <enum>
Allowed ENUM:
retry_same # technical_fail ONLY (push network/permission, hook reject). FORBIDDEN for test-verify / final-close.
continue_same # Stage 3 ONLY ??current unit verified OK, but more planned units remain. Not a failure.
stage_1_review # rewind to problem-review (root cause / scope-lock wrong)
stage_2_plan # rewind to simulation-plan (plan wrong / missing files / tests)
stage_3_edit # rewind to code-edit (implementation incomplete / scope creep)
stage_4_verify # rewind to test-verify (commit ok but missed regression)
stage_5_push # rewind to commit-push (post-push remote anomaly ??rare)
stage_4_verify (test-verify) NO and stage_6 final-close NO: retry_same / continue_same FORBIDDEN. Must rewind to earlier stage.
Stage 3 (code-edit) unit progress: use continue_same per unit; FINAL_CONSENSUS: YES only when ALL implementation_units complete.
TEMP / LOCAL ARTIFACTS: .orchestrator/tmp/ and .orchestrator/drafts/ only.
Codex (verifier). Verify EVERY claim. Use the verification level required by the CURRENT STAGE.
Do NOT run full pytest unless the stage task explicitly requires it.
With FINAL_CONSENSUS: YES, include === EVIDENCE === block :
=== EVIDENCE ===
Commands run: (list)
Files checked: (list)
Test results: (if Stage 2/4)
Commit SHA: (if Stage 5)
Verified facts: (list, if Stage 6)
With FINAL_CONSENSUS: NO, include rewind_target line BEFORE the consensus line :
rewind_target: stage_1_review | stage_2_plan | stage_3_edit | stage_4_verify | stage_5_push | retry_same | continue_same
FINAL_CONSENSUS: NO
Note: retry_same / continue_same FORBIDDEN for test-verify and final-close stages.
Stage 3 (code-edit): if the single executed unit is correct AND remaining_units is non-empty ??continue_same.
Stage 3: if ALL implementation_units complete and verified ??FINAL_CONSENSUS: YES.
Stage 3 EVIDENCE block MUST include EXACTLY one of these lines (strict format):
Remaining units: [u2, u3, u4] (bracketed list when units remain)
Remaining units: none (when all units complete)
[Codex #N].
POST comment: https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/76/comments | token $GITEA_TOKEN
=== ORCHESTRATOR DRAFT OUTPUT ===
After posting your normal Gitea comment, save the same comment body to:
D:\ad-hoc\kei\design_agent\.orchestrator\drafts\76_stage_code-edit_codex_r3.md
Do not summarize or shorten the draft; it must mirror the Gitea comment body.
The Gitea comment remains required in this transition phase.
codex
Ive got the binding prompt. The current task is Codex verification for Stage 3 code-edit on unit u13, so Im checking only the u13 scope and the files it claims to have changed.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'git status --short' in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'git diff -- src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py' in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'rg -n "_persist_ai_repair_proposals_to_cache|cache_save_status|save_proposal|AiFallbackCacheGateError|visual_check_passed|user_approved|auto_cache" src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py src/phase_z2_ai_fallback/cache.py' in D:\ad-hoc\kei\design_agent
succeeded in 98ms:
src/phase_z2_ai_fallback/cache.py:8:* ``read_proposal(key)`` / ``save_proposal(key, ...)`` accept a string ``key``
src/phase_z2_ai_fallback/cache.py:16:* ``save_proposal`` on a malformed key raises ``ValueError`` (loud, never
src/phase_z2_ai_fallback/cache.py:30:* ``save_proposal`` persists the ``fingerprints`` dict supplied by the
src/phase_z2_ai_fallback/cache.py:48:* Both write gates preserved — ``visual_check_passed=False`` always
src/phase_z2_ai_fallback/cache.py:49: raises ``AiFallbackCacheGateError`` BEFORE any filesystem touch.
src/phase_z2_ai_fallback/cache.py:50: ``user_approved=False`` also raises by default; the IMP-46 u5
src/phase_z2_ai_fallback/cache.py:51: ``auto_cache=True`` override bypasses ONLY the ``user_approved`` gate
src/phase_z2_ai_fallback/cache.py:52: (``visual_check_passed`` is never bypassed). Gate violation never
src/phase_z2_ai_fallback/cache.py:62:u5 auto-cache contract (CLI ``--auto-cache`` + ``settings.ai_fallback_auto_cache``):
src/phase_z2_ai_fallback/cache.py:64:* ``save_proposal(..., auto_cache=True)`` only bypasses the
src/phase_z2_ai_fallback/cache.py:65: ``user_approved`` gate; ``visual_check_passed`` remains mandatory.
src/phase_z2_ai_fallback/cache.py:66:* ``auto_cache`` is keyword-only and defaults to ``False`` — existing
src/phase_z2_ai_fallback/cache.py:69:* The truth table over ``(visual_check_passed, user_approved, auto_cache)``
src/phase_z2_ai_fallback/cache.py:73: ``AiFallbackCacheGateError``.
src/phase_z2_ai_fallback/cache.py:88:class AiFallbackCacheGateError(RuntimeError):
src/phase_z2_ai_fallback/cache.py:89: """Raised when ``save_proposal`` is called without both IMP-46 gates True."""
src/phase_z2_ai_fallback/cache.py:119: symmetric with :func:`save_proposal`);
src/phase_z2_ai_fallback/cache.py:158:def save_proposal(
src/phase_z2_ai_fallback/cache.py:162: visual_check_passed: bool,
src/phase_z2_ai_fallback/cache.py:163: user_approved: bool,
src/phase_z2_ai_fallback/cache.py:166: auto_cache: bool = False,
src/phase_z2_ai_fallback/cache.py:172: * ``visual_check_passed=False`` -> :class:`AiFallbackCacheGateError`
src/phase_z2_ai_fallback/cache.py:173: always (never bypassable; ``auto_cache`` cannot override).
src/phase_z2_ai_fallback/cache.py:174: * ``user_approved=False`` AND ``auto_cache=False`` ->
src/phase_z2_ai_fallback/cache.py:175: :class:`AiFallbackCacheGateError`.
src/phase_z2_ai_fallback/cache.py:176: * ``user_approved=False`` AND ``auto_cache=True`` -> bypass the
src/phase_z2_ai_fallback/cache.py:178: * Otherwise (``visual_check_passed=True`` AND either
src/phase_z2_ai_fallback/cache.py:179: ``user_approved=True`` OR ``auto_cache=True``) -> persist payload.
src/phase_z2_ai_fallback/cache.py:191: ``auto_cache`` is keyword-only and defaults to ``False``. It is wired
src/phase_z2_ai_fallback/cache.py:192: from :data:`src.config.settings.ai_fallback_auto_cache`, which the
src/phase_z2_ai_fallback/cache.py:205: if not isinstance(auto_cache, bool):
src/phase_z2_ai_fallback/cache.py:206: raise TypeError("auto_cache must be a bool")
src/phase_z2_ai_fallback/cache.py:207: if not visual_check_passed:
src/phase_z2_ai_fallback/cache.py:208: raise AiFallbackCacheGateError(
src/phase_z2_ai_fallback/cache.py:209: "IMP-46 gate: visual_check_passed=False; refusing to cache an "
src/phase_z2_ai_fallback/cache.py:210: "unverified proposal. (auto_cache cannot bypass this gate.)"
src/phase_z2_ai_fallback/cache.py:212: if not user_approved and not auto_cache:
src/phase_z2_ai_fallback/cache.py:213: raise AiFallbackCacheGateError(
src/phase_z2_ai_fallback/cache.py:214: "IMP-46 gate: user_approved=False and auto_cache=False; "
src/phase_z2_ai_fallback/cache.py:216: "auto_cache=True (or --auto-cache on the CLI) to bypass."
tests/test_imp47b_cache_save_gate.py:1:"""IMP-47B u13 — Persist validated proposals through ``save_proposal`` after gates.
tests/test_imp47b_cache_save_gate.py:4: Verify the new ``_persist_ai_repair_proposals_to_cache`` helper in
tests/test_imp47b_cache_save_gate.py:12: * ``visual_check_passed=False`` always blocks — never bypassable, even
tests/test_imp47b_cache_save_gate.py:13: when ``auto_cache=True`` (IMP-46 u5 truth table cell).
tests/test_imp47b_cache_save_gate.py:14: * ``user_approved=False`` AND ``auto_cache=False`` → gate blocked
tests/test_imp47b_cache_save_gate.py:17: * ``visual_check_passed=True`` AND ``auto_cache=True`` → proposal
tests/test_imp47b_cache_save_gate.py:19: via ``cache.save_proposal``.
tests/test_imp47b_cache_save_gate.py:21: error) → ``cache_save_status='not_applied'`` and NEVER reach
tests/test_imp47b_cache_save_gate.py:22: ``save_proposal`` (no filesystem touch).
tests/test_imp47b_cache_save_gate.py:23: * Settings axis — ``settings.ai_fallback_auto_cache`` sourced through
tests/test_imp47b_cache_save_gate.py:33:from src.phase_z2_ai_fallback.cache import AiFallbackCacheGateError
tests/test_imp47b_cache_save_gate.py:35:from src.phase_z2_pipeline import _persist_ai_repair_proposals_to_cache
tests/test_imp47b_cache_save_gate.py:73: """Redirect ``cache.CACHE_ROOT`` to a per-test tmp dir so save_proposal
tests/test_imp47b_cache_save_gate.py:79:def test_visual_check_failed_blocks_save_even_with_auto_cache(_isolate_cache_root):
tests/test_imp47b_cache_save_gate.py:80: """visual_check_passed=False is never bypassable — auto_cache cannot override."""
tests/test_imp47b_cache_save_gate.py:83: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:85: visual_check_passed=False,
tests/test_imp47b_cache_save_gate.py:86: user_approved=True,
tests/test_imp47b_cache_save_gate.py:87: auto_cache=True,
tests/test_imp47b_cache_save_gate.py:89: assert record["cache_save_status"].startswith("gate_blocked:")
tests/test_imp47b_cache_save_gate.py:90: assert "visual_check_passed=False" in record["cache_save_status"]
tests/test_imp47b_cache_save_gate.py:95:def test_user_not_approved_and_no_auto_cache_blocks_save(_isolate_cache_root):
tests/test_imp47b_cache_save_gate.py:96: """Default pipeline path (user_approved=False, auto_cache=False) → gate blocked."""
tests/test_imp47b_cache_save_gate.py:99: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:101: visual_check_passed=True,
tests/test_imp47b_cache_save_gate.py:102: user_approved=False,
tests/test_imp47b_cache_save_gate.py:103: auto_cache=False,
tests/test_imp47b_cache_save_gate.py:105: assert record["cache_save_status"].startswith("gate_blocked:")
tests/test_imp47b_cache_save_gate.py:106: assert "user_approved=False" in record["cache_save_status"]
tests/test_imp47b_cache_save_gate.py:110:def test_visual_passed_and_auto_cache_persists_proposal(_isolate_cache_root):
tests/test_imp47b_cache_save_gate.py:111: """Happy path — visual_check_passed=True + auto_cache=True persists JSON."""
tests/test_imp47b_cache_save_gate.py:114: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:116: visual_check_passed=True,
tests/test_imp47b_cache_save_gate.py:117: user_approved=False,
tests/test_imp47b_cache_save_gate.py:118: auto_cache=True,
tests/test_imp47b_cache_save_gate.py:120: assert record["cache_save_status"] == "saved"
tests/test_imp47b_cache_save_gate.py:129: """no_proposal / no_zone_match / unsupported_kind / error → never reach save_proposal."""
tests/test_imp47b_cache_save_gate.py:160: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:162: visual_check_passed=True,
tests/test_imp47b_cache_save_gate.py:163: user_approved=True,
tests/test_imp47b_cache_save_gate.py:164: auto_cache=True,
tests/test_imp47b_cache_save_gate.py:167: assert r["cache_save_status"] == "not_applied"
tests/test_imp47b_cache_save_gate.py:183: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:185: visual_check_passed=True,
tests/test_imp47b_cache_save_gate.py:186: user_approved=False,
tests/test_imp47b_cache_save_gate.py:187: auto_cache=True,
tests/test_imp47b_cache_save_gate.py:189: assert applied["cache_save_status"] == "saved"
tests/test_imp47b_cache_save_gate.py:190: assert not_applied["cache_save_status"] == "not_applied"
tests/test_imp47b_cache_save_gate.py:196: """Malformed ``proposal`` dict → ``cache_save_status='invalid_proposal:...'``,
tests/test_imp47b_cache_save_gate.py:206: _persist_ai_repair_proposals_to_cache(
tests/test_imp47b_cache_save_gate.py:208: visual_check_passed=True,
tests/test_imp47b_cache_save_gate.py:209: user_approved=True,
tests/test_imp47b_cache_save_gate.py:210: auto_cache=True,
tests/test_imp47b_cache_save_gate.py:212: assert bad_record["cache_save_status"].startswith("invalid_proposal:")
src/phase_z2_pipeline.py:709:def _persist_ai_repair_proposals_to_cache(
src/phase_z2_pipeline.py:712: visual_check_passed: bool,
src/phase_z2_pipeline.py:713: user_approved: bool,
src/phase_z2_pipeline.py:714: auto_cache: bool,
src/phase_z2_pipeline.py:718: Mutates each record in place with a ``cache_save_status`` axis.
src/phase_z2_pipeline.py:723: ``cache.save_proposal`` with the IMP-46 dual-gate truth table; the
src/phase_z2_pipeline.py:724: helper catches :class:`AiFallbackCacheGateError` so a gate block is
src/phase_z2_pipeline.py:727: cache.py contract). ``visual_check_passed`` is never bypassable;
src/phase_z2_pipeline.py:728: ``auto_cache=True`` bypasses ONLY the ``user_approved`` gate per
src/phase_z2_pipeline.py:732: AiFallbackCacheGateError,
src/phase_z2_pipeline.py:733: save_proposal,
src/phase_z2_pipeline.py:747: record["cache_save_status"] = "not_applied"
src/phase_z2_pipeline.py:752: record["cache_save_status"] = f"invalid_proposal:{type(exc).__name__}"
src/phase_z2_pipeline.py:755: save_proposal(
src/phase_z2_pipeline.py:758: visual_check_passed=visual_check_passed,
src/phase_z2_pipeline.py:759: user_approved=user_approved,
src/phase_z2_pipeline.py:760: auto_cache=auto_cache,
src/phase_z2_pipeline.py:763: except AiFallbackCacheGateError as gate_exc:
src/phase_z2_pipeline.py:764: record["cache_save_status"] = f"gate_blocked:{gate_exc}"
src/phase_z2_pipeline.py:766: record["cache_save_status"] = "saved"
src/phase_z2_pipeline.py:2856: - visual_check_passed : Selenium per-zone overflow / clipping 통과 여부
src/phase_z2_pipeline.py:3008: "visual_check_passed": visual_passed,
src/phase_z2_pipeline.py:5340: # check + per IMP-46 dual-gate. ``visual_check_passed`` reads the
src/phase_z2_pipeline.py:5341: # Selenium overflow result; ``auto_cache`` sourced from Settings
src/phase_z2_pipeline.py:5342: # (CLI --auto-cache wires settings.ai_fallback_auto_cache at parse
src/phase_z2_pipeline.py:5343: # time, src/phase_z2_pipeline.py:5631-5633). ``user_approved`` stays
src/phase_z2_pipeline.py:5344: # False — the pipeline has no UX approval gate; the auto_cache
src/phase_z2_pipeline.py:5346: # surface as ``cache_save_status='gate_blocked:<reason>'`` on the
src/phase_z2_pipeline.py:5349: _persist_ai_repair_proposals_to_cache(
src/phase_z2_pipeline.py:5351: visual_check_passed=bool(overflow.get("passed")),
src/phase_z2_pipeline.py:5352: user_approved=False,
src/phase_z2_pipeline.py:5353: auto_cache=bool(_ai_cache_settings.ai_fallback_auto_cache),
src/phase_z2_pipeline.py:5582: f'<tr><th>visual_check_passed</th><td>{slide_status.get("visual_check_passed")}</td></tr>'
src/phase_z2_pipeline.py:5650: if not slide_status["visual_check_passed"]:
src/phase_z2_pipeline.py:5654: if not fit_classification["visual_check_passed"]:
src/phase_z2_pipeline.py:5680: if not slide_status["visual_check_passed"]:
src/phase_z2_pipeline.py:5760: # IMP-46 u5 — auto-cache opt-in. When set, ``cache.save_proposal``
src/phase_z2_pipeline.py:5761: # bypasses the ``user_approved`` gate only (``visual_check_passed``
src/phase_z2_pipeline.py:5763: # ``settings.ai_fallback_auto_cache`` (src/config.py); this flag
src/phase_z2_pipeline.py:5768: dest="auto_cache",
src/phase_z2_pipeline.py:5772: "Allow cache.save_proposal to bypass the user_approved gate "
src/phase_z2_pipeline.py:5773: "(visual_check_passed remains mandatory). Sets "
src/phase_z2_pipeline.py:5774: "settings.ai_fallback_auto_cache=True for this run."
src/phase_z2_pipeline.py:5779: if args.auto_cache:
src/phase_z2_pipeline.py:5781: _settings.ai_fallback_auto_cache = True
succeeded in 134ms:
M .gitignore
M Front/client/src/components/FramePanel.tsx
M Front/client/src/pages/Home.tsx
M Front/client/src/services/designAgentApi.ts
M Front/client/src/types/designAgent.ts
M Front/vite.config.ts
M docs/architecture/IMP-17-CARVE-OUT.md
M src/config.py
M src/phase_z2_ai_fallback/cache.py
M src/phase_z2_ai_fallback/step12.py
M src/phase_z2_mapper.py
M src/phase_z2_pipeline.py
M tests/phase_z2_ai_fallback/test_ast_isolation.py
M tests/phase_z2_ai_fallback/test_cache.py
M tests/phase_z2_ai_fallback/test_step12.py
M tests/test_phase_z2_ai_fallback_config.py
M tests/test_phase_z2_v4_fallback.py
?? .claude/settings.local.json
?? .orchestrator/
?? "D\357\200\272tmp42_comments.json"
?? "D\357\200\272tmpissue38_comments.json"
?? Front/client/public/data/
?? Front/client/src/data/
?? Front/client/tests/
?? Front_test/
?? Front_test_v515/
?? ad-hockeidesign_agent.orchestratortmp28_r5_post_response.json
?? chromedriver/win64/147.0.7727.117/
?? data/
?? "orchestrator(\354\233\220\353\263\270).py"
?? "samples/mdx/05. \354\204\244\352\263\204 \353\260\251\354\213\235\354\235\230 \354\231\234\352\263\241.mdx"
?? samples/mdx_batch/05.mdx
?? samples/uploads/
?? scripts/post_gitea_comment.py
?? scripts/sync_phase_z_run_to_frontend.py
?? src/phase_z2_ai_fallback/signature.py
?? templates/catalog/
?? templates/phase_z2/catalog/v4_fallback_policy.yaml
?? templates/phase_z2/families/app_sw_package_vs_solution.html
?? templates/phase_z2/families/pre_construction_model_info_stacked.html
?? templates/phase_z2/frames/
?? tests/PIPELINE.md
?? tests/PLAN.md
?? tests/PROGRESS.md
?? tests/README.md
?? tests/matching/ANCHOR_SETS_OVERVIEW.html
?? tests/matching/ANCHOR_SETS_OVERVIEW.md
?? tests/matching/ATTACH_01_KEYWORD_ORGANIZATION.html
?? tests/matching/ATTACH_02_KEYWORD_MATCHING.html
?? tests/matching/ATTACH_03_CASCADE_RESULT.html
?? tests/matching/ATTACH_04_STRUCTURE_VOCAB.html
?? tests/matching/ATTACH_05_SAMPLES.html
?? tests/matching/ATTACH_06_DB_ORGANIZATION.html
?? tests/matching/ATTACH_07_WEIGHT_VALIDATION.html
?? tests/matching/BEPS_SUMMARY_REPORT.html
?? tests/matching/BEPS_SUMMARY_REPORT.md
?? tests/matching/CLEAN_ANCHOR_SETS_REPORT.html
?? tests/matching/CLEAN_ANCHOR_SETS_REPORT.md
?? tests/matching/CURRENT_STATUS.md
?? "tests/matching/DECK_01_TARGET_01_2(\354\265\234\354\242\205).html"
?? tests/matching/DECK_01_TARGET_01_2.html
?? tests/matching/DECK_02_HOLDOUT_02_2_1.html
?? "tests/matching/DECK_03_METHODS(\354\265\234\354\242\205).html"
?? tests/matching/DECK_03_METHODS.html
?? "tests/matching/DECK_04_DB_STRUCTURE(\354\265\234\354\242\205).html"
?? tests/matching/DECK_04_DB_STRUCTURE.html
?? "tests/matching/DECK_05_STRUCTURE_KEYWORD(\354\265\234\354\242\205).html"
?? tests/matching/DECK_05_STRUCTURE_KEYWORD.html
?? tests/matching/DECK_06_FIGMA_TO_DB.html
?? "tests/matching/DECK_07_KEYWORD_PIPELINE(\354\265\234\354\242\205).html"
?? tests/matching/DOMAIN_TERMS_REPORT.html
?? tests/matching/DOMAIN_TERMS_REPORT.md
?? tests/matching/FIGMA_AUDIT.html
?? tests/matching/FIGMA_AUDIT.md
?? tests/matching/FIGMA_KEYWORDS_REPORT.html
?? tests/matching/FIGMA_KEYWORDS_REPORT.md
?? tests/matching/FILTER_CANDIDATES.html
?? tests/matching/FILTER_CANDIDATES.md
?? tests/matching/FRAME_KEYWORD_REVIEW.html
?? tests/matching/FRAME_KEYWORD_REVIEW.md
?? tests/matching/HOLDOUT_EVALUATION.html
?? tests/matching/HOLDOUT_EVALUATION.md
?? tests/matching/HOLDOUT_LABELING.html
?? tests/matching/INTENT_DIAGNOSTIC_REPORT.html
?? tests/matching/INTENT_DIAGNOSTIC_REPORT.md
?? tests/matching/KEYWORD_BASE_DRAFT.html
?? tests/matching/KEYWORD_BASE_DRAFT.md
?? tests/matching/KEYWORD_INVENTORY.html
?? tests/matching/KEYWORD_INVENTORY.md
?? tests/matching/KEYWORD_REFINEMENT_REPORT.md
?? tests/matching/KEYWORD_REPORT.html
?? tests/matching/KEYWORD_REPORT.md
?? tests/matching/MATRIX.html
?? tests/matching/MATRIX.md
?? tests/matching/MATRIX_17.html
?? tests/matching/MATRIX_17.md
?? tests/matching/MATRIX_CASE1.html
?? tests/matching/MATRIX_CASE1.md
?? tests/matching/MATRIX_CASE2.html
?? tests/matching/MATRIX_CASE2.md
?? tests/matching/MATRIX_CASE3.html
?? tests/matching/MATRIX_CASE3.md
?? tests/matching/MATRIX_PHASE10.html
?? tests/matching/MATRIX_PHASE10.md
?? tests/matching/MATRIX_PHASE11.html
?? tests/matching/MATRIX_PHASE11.md
?? tests/matching/MATRIX_PHASE12.html
?? tests/matching/MATRIX_PHASE12.md
?? tests/matching/MATRIX_PHASE13.html
?? tests/matching/MATRIX_PHASE13.md
?? tests/matching/MATRIX_PHASE14.html
?? tests/matching/MATRIX_PHASE14.md
?? tests/matching/MATRIX_PHASE14_SAMPLE.html
?? tests/matching/MATRIX_PHASE14_SAMPLE.md
?? tests/matching/MATRIX_PHASE15.html
?? tests/matching/MATRIX_PHASE15.md
?? tests/matching/MATRIX_PHASE16.html
?? tests/matching/MATRIX_PHASE16.md
?? tests/matching/MATRIX_PHASE17.html
?? tests/matching/MATRIX_PHASE17.md
?? tests/matching/MATRIX_PHASE18.html
?? tests/matching/MATRIX_PHASE18.md
?? tests/matching/MATRIX_PHASE19.html
?? tests/matching/MATRIX_PHASE19.md
?? tests/matching/MATRIX_PHASE2.html
?? tests/matching/MATRIX_PHASE2.md
?? tests/matching/MATRIX_PHASE20.html
?? tests/matching/MATRIX_PHASE20.md
?? tests/matching/MATRIX_PHASE21.html
?? tests/matching/MATRIX_PHASE21.md
?? tests/matching/MATRIX_PHASE21b.html
?? tests/matching/MATRIX_PHASE21b.md
?? tests/matching/MATRIX_PHASE22.html
?? tests/matching/MATRIX_PHASE22.md
?? tests/matching/MATRIX_PHASE23.html
?? tests/matching/MATRIX_PHASE23.md
?? tests/matching/MATRIX_PHASE24.html
?? tests/matching/MATRIX_PHASE24.md
?? tests/matching/MATRIX_PHASE25.html
?? tests/matching/MATRIX_PHASE25.md
?? tests/matching/MATRIX_PHASE26.html
?? tests/matching/MATRIX_PHASE26.md
?? tests/matching/MATRIX_PHASE3.html
?? tests/matching/MATRIX_PHASE3.md
?? tests/matching/MATRIX_PHASE4.html
?? tests/matching/MATRIX_PHASE4.md
?? "tests/matching/MATRIX_PHASE4_1\354\210\234\354\234\204.html"
?? "tests/matching/MATRIX_PHASE4_1\354\210\234\354\234\204.md"
?? tests/matching/MATRIX_PHASE5.html
?? tests/matching/MATRIX_PHASE5.md
?? tests/matching/MATRIX_PHASE6.html
?? tests/matching/MATRIX_PHASE6.md
?? tests/matching/MATRIX_PHASE7.html
?? tests/matching/MATRIX_PHASE7.md
?? tests/matching/MATRIX_PHASE8.html
?? tests/matching/MATRIX_PHASE8.md
?? tests/matching/MATRIX_PHASE9.html
?? tests/matching/MATRIX_PHASE9.md
?? tests/matching/MDX_MATCHING_REPORT.html
?? tests/matching/MDX_MATCHING_REPORT.md
?? tests/matching/MEETING_BRIEF.html
?? tests/matching/NORMALIZED_TOKEN_INVENTORY.before_plain_line.md
?? tests/matching/PRIORITY_5_REVIEW.html
?? tests/matching/PRIORITY_5_REVIEW.md
?? tests/matching/PRIORITY_5_REVIEW_r2.html
?? tests/matching/PRIORITY_5_REVIEW_r2.md
?? tests/matching/PRIORITY_5_REVIEW_r3.html
?? tests/matching/PRIORITY_5_REVIEW_r3.md
?? tests/matching/RESULT.md
?? tests/matching/RESULT_BY_SECTION.html
?? tests/matching/RESULT_BY_SECTION.md
?? tests/matching/SAMPLE_01_2_V1_MATCHING.html
?? tests/matching/SLIDE_CONVERSION_PROCESS.html
?? tests/matching/SYNONYMS_REPORT.html
?? tests/matching/SYNONYMS_REPORT.md
?? tests/matching/SYNONYM_MINING_PROPOSALS.yaml
?? tests/matching/TEMPLATES_V1_AUDIT.html
?? tests/matching/TEMPLATES_V1_AUDIT.md
?? tests/matching/TEMPLATES_V2_DIFF.md
?? tests/matching/TEMPLATES_V2_DIFF_r2.md
?? tests/matching/TEMPLATES_V2_DIFF_r3.md
?? tests/matching/TEMPLATES_V2_SCHEMA_DRAFT.md
?? tests/matching/TEMPLATE_FIT_V1.md
?? tests/matching/TEXT_PIPELINE_REPORT.md
?? tests/matching/V2_DIAGNOSIS.md
?? tests/matching/V2_DIAGNOSIS.yaml
?? tests/matching/V4_SLOT_ABLATION.md
?? tests/matching/WEEKLY_REPORT_DASHBOARD.html
?? tests/matching/WEEKLY_REPORT_SYSTEM.md
?? tests/matching/_add_structure_intent.py
?? tests/matching/_archive/
?? tests/matching/_baseline_pre_ontology/
?? tests/matching/_phase18_21_results.pkl
?? tests/matching/_phase21b_results.pkl
?? tests/matching/_phase22_results.pkl
?? tests/matching/_phase23_results.pkl
?? tests/matching/_phase24_results.pkl
?? tests/matching/_phase25_results.pkl
?? tests/matching/actual_text_nodes.before_mdx_section.yaml
?? tests/matching/actual_text_nodes.before_plain_line.yaml
?? tests/matching/actual_text_nodes.yaml
?? tests/matching/actual_text_tokens.yaml
?? tests/matching/anchor_candidates.yaml
?? tests/matching/anchor_candidates_report.html
?? tests/matching/anchor_candidates_report.md
?? tests/matching/anchor_sets_draft.yaml
?? tests/matching/anchor_sets_input.ai_curated_legacy.yaml
?? tests/matching/anchor_sets_input.from_step7.todo.yaml
?? tests/matching/anchor_sets_input.yaml
?? tests/matching/anchor_sets_report.html
?? tests/matching/anchor_sets_report.md
?? tests/matching/auto_anchor_candidates.yaml
?? tests/matching/auto_anchor_candidates_report.html
?? tests/matching/auto_anchor_candidates_report.md
?? tests/matching/build_beps_summary.py
?? tests/matching/build_keyword_base.py
?? tests/matching/build_structure_ontology.py
?? tests/matching/by_section.py
?? tests/matching/case1_only.py
?? tests/matching/case3.py
?? tests/matching/clean_anchor_sets.py
?? tests/matching/common.py
?? tests/matching/compare_hybrids.py
?? tests/matching/detect_mdx.py
?? tests/matching/domain_terms.yaml
?? tests/matching/domain_terms_report.py
?? tests/matching/download_models.py
?? tests/matching/embeddings.py
?? tests/matching/extract_rare_keywords.py
?? tests/matching/extract_units.py
?? tests/matching/figma_audit.py
?? tests/matching/filter_candidates.yaml
?? tests/matching/generate_anchor_overview.py
?? tests/matching/generate_filter_candidates.py
?? tests/matching/generate_frame_keyword_review.py
?? tests/matching/generate_inventory_report.py
?? tests/matching/generate_keyword_inventory.py
?? tests/matching/ground_truth.yaml
?? tests/matching/holdout_labeling_template.yaml
?? tests/matching/holdout_labels.yaml
?? tests/matching/idf_only_test.py
?? tests/matching/inspect_keywords.py
?? tests/matching/intent_diagnostic.py
?? tests/matching/keyword_base.yaml
?? tests/matching/keyword_inventory_1.html
?? tests/matching/keyword_normalizer.py
?? tests/matching/keyword_refinement.py
?? tests/matching/keyword_report.py
?? tests/matching/keywords_from_texts.py
?? tests/matching/matrix_17.py
?? tests/matching/matrix_cases.py
?? tests/matching/matrix_simple.py
?? tests/matching/md_to_html.py
?? tests/matching/mdx_matching_report.before_mdx_section.html
?? tests/matching/mdx_matching_report.before_mdx_section.md
?? tests/matching/mdx_matching_result.before_mdx_section.yaml
?? tests/matching/mdx_matching_result.yaml
?? tests/matching/metadata_db.yaml
?? tests/matching/metadata_db_v2_sample.yaml
?? tests/matching/methods.py
?? tests/matching/normalized_text_tokens.before_mdx_section.yaml
?? tests/matching/normalized_text_tokens.before_plain_line.yaml
?? tests/matching/normalized_text_tokens.yaml
?? tests/matching/phase10.py
?? tests/matching/phase11.py
?? tests/matching/phase12.py
?? tests/matching/phase13.py
?? tests/matching/phase14.py
?? tests/matching/phase14_sample.py
?? tests/matching/phase15.py
?? tests/matching/phase16.py
?? tests/matching/phase17.py
?? tests/matching/phase18_21.py
?? tests/matching/phase2.py
?? tests/matching/phase21b.py
?? tests/matching/phase22.py
?? tests/matching/phase23.py
?? tests/matching/phase24.py
?? tests/matching/phase25.py
?? tests/matching/phase26.py
?? "tests/matching/phase2_MDX 03.pdf"
?? tests/matching/phase3.py
?? tests/matching/phase4.py
?? tests/matching/phase4_1st.py
?? tests/matching/phase5.py
?? tests/matching/phase6.py
?? tests/matching/phase7.py
?? tests/matching/phase8.py
?? tests/matching/phase9.py
?? tests/matching/phase_common.py
?? tests/matching/phase_domain_terms.py
?? tests/matching/phase_domain_terms_v2.py
?? tests/matching/pipeline_01_extract_nodes.py
?? tests/matching/pipeline_02_5_special_forms.py
?? tests/matching/pipeline_02_tokenize.py
?? tests/matching/pipeline_04_normalize.py
?? tests/matching/pipeline_04a_smoke_test.py
?? tests/matching/pipeline_05_anchor_candidates.py
?? tests/matching/pipeline_06_2_mdx_matching.py
?? tests/matching/pipeline_06_anchor_sets.py
?? tests/matching/pipeline_07_auto_anchor_candidates.py
?? tests/matching/pipeline_08_5_matching_report.py
?? tests/matching/pipeline_08_5_r2_matching_report.py
?? tests/matching/pipeline_08_v2_semantic_rerank.py
?? tests/matching/pipeline_08_v3_r2_structure_rerank.py
?? tests/matching/pipeline_08_v3_r3_structure_rerank.py
?? tests/matching/pipeline_08_v3_r4_structure_rerank.py
?? tests/matching/pipeline_08_v3_r5_structure_rerank.py
?? tests/matching/pipeline_08_v3_structure_rerank.py
?? tests/matching/pipeline_08_v4_r2_template_fit_judgment.py
?? tests/matching/pipeline_08_v4_template_fit_judgment.py
?? tests/matching/pipeline_09_v2_diagnosis.py
?? tests/matching/pipeline_10_5_holdout_eval.py
?? tests/matching/pipeline_10_holdout_labeling_prep.py
?? tests/matching/pipeline_11_templates_v1_audit.py
?? tests/matching/pipeline_12_finalize_v2.py
?? tests/matching/pipeline_12_generate_templates_v2.py
?? tests/matching/pipeline_12_promote_frame13.py
?? tests/matching/pipeline_12_r2_generate_templates_v2.py
?? tests/matching/pipeline_12_r3_generate_templates_v2.py
?? tests/matching/pipeline_13_generate_meeting_samples.py
?? tests/matching/pipeline_13_r2_meeting_docs.py
?? tests/matching/pipeline_14_single_sample.py
?? tests/matching/pipeline_15_bm25_comparison.py
?? tests/matching/pipeline_15_bm25_with_sets.py
?? tests/matching/pipeline_15_idf_comparison.py
?? tests/matching/pipeline_15_logistic_regression.py
?? tests/matching/pipeline_16_deck_4pages.py
?? tests/matching/pipeline_17_v4_full32.py
?? tests/matching/pipeline_18_slot_axis_ablation.py
?? tests/matching/replacement_report.yaml
?? tests/matching/run_all.py
?? tests/matching/special_forms_inventory.yaml
?? tests/matching/structure_ontology.yaml
?? tests/matching/structure_ontology.yaml.pre_intent.bak
?? tests/matching/structure_ontology.yaml.pre_milestone2.bak
?? tests/matching/structure_ontology_v2.yaml
?? tests/matching/structure_ontology_v2_final.yaml
?? tests/matching/structure_ontology_v2_final_r2.yaml
?? tests/matching/structure_ontology_v2_r2.yaml
?? tests/matching/structure_ontology_v2_r3.yaml
?? tests/matching/structure_v3.py
?? tests/matching/sync_analysis_from_ontology.py
?? tests/matching/synonym_mine.py
?? tests/matching/synonyms.yaml
?? tests/matching/synonyms_candidates.yaml
?? tests/matching/synonyms_production.yaml
?? tests/matching/synonyms_report.py
?? tests/matching/template_fit.py
?? tests/matching/test_mdx03_1_depth.py
?? tests/matching/text_canonical.yaml
?? tests/matching/text_pipeline.py
?? tests/matching/v2_semantic_rerank_result.yaml
?? tests/matching/v3_structure_rerank_r2_result.yaml
?? tests/matching/v3_structure_rerank_r3_result.yaml
?? tests/matching/v3_structure_rerank_r4_result.yaml
?? tests/matching/v3_structure_rerank_r5_result.yaml
?? tests/matching/v3_structure_rerank_result.prev_for_report.yaml
?? tests/matching/v3_structure_rerank_result.yaml
?? tests/matching/v4_template_fit_r2_result.yaml
?? tests/matching/v4_template_fit_result.prev_for_report.yaml
?? tests/matching/v4_template_fit_result.yaml
?? tests/matching/weekly_report_data.js
?? tests/phase_z2_ai_fallback/test_cache_invalidation.py
?? tests/phase_z2_ai_fallback/test_cache_repo_layout.py
?? tests/phase_z2_ai_fallback/test_signature.py
?? tests/pipeline/
?? tests/test_dynamic_max_rank.py
?? tests/test_imp47b_cache_save_gate.py
?? tests/test_imp47b_coverage_invariant.py
?? tests/test_imp47b_end_to_end.py
?? tests/test_imp47b_failure_surface.py
?? tests/test_imp47b_mixed_reject_fill.py
?? tests/test_imp47b_override_provisional.py
?? tests/test_imp47b_payload_apply.py
?? tests/test_imp47b_step12_ai_wiring.py
?? tests/test_phase_z2_max_rank_regression.py
?? tests/test_v4_fallback_policy_loader.py
?? tmpclosed_issues.json
succeeded in 171ms:
diff --git a/src/phase_z2_pipeline.py b/src/phase_z2_pipeline.py
index a4e9c20..06706c3 100644
--- a/src/phase_z2_pipeline.py
+++ b/src/phase_z2_pipeline.py
@@ -52,6 +52,7 @@ from phase_z2_mapper import (
compute_capacity_fit,
get_contract,
load_frame_contracts,
+ load_v4_fallback_policy,
map_with_contract,
)
from phase_z2_classifier import classify_visual_runtime_check
@@ -77,6 +78,12 @@ from phase_z2_failure_router import (
from phase_z2_content_extractor import extract_content_objects, extract_rich_content_objects
from phase_z2_placement_planner import plan_placement
+# IMP-47B u4 — Step 12 AI repair wiring. gather() short-circuits at the
+# router when settings.ai_fallback_enabled is False (default), so import
+# at module load is safe for the AI=0 normal path (PZ-1). Activation gate
+# stays in src/config.py + src/phase_z2_ai_fallback/router.py.
+from src.phase_z2_ai_fallback.step12 import gather_step12_ai_repair_proposals
+
# ─── Constants ──────────────────────────────────────────────────
@@ -568,12 +575,15 @@ def lookup_v4_match(
# use_as_is → Phase Z direct render
# light_edit → deterministic minor adjustment
# restructure → AI-assisted frame-aware adaptation (deferred to IMP-17 — carve-out, AI fallback only, normal path 밖)
-# reject → design reference only (deferred to IMP-29 frontend override)
+# reject → AI re-construction over the rank-1 reject frame (IMP-47B u1, 2026-05-21);
+# policy correction supersedes the legacy "design reference only" disposition.
+# Frame visual / contract stays untouched; AI only re-maps MDX content into
+# declared slots. Activation still gated by ai_fallback_enabled (default OFF).
_IMP05_ROUTE_HINTS: dict[str, str] = {
"use_as_is": "direct_render",
"light_edit": "deterministic_minor_adjustment",
"restructure": "ai_adaptation_required",
- "reject": "design_reference_only",
+ "reject": "ai_adaptation_required",
}
@@ -584,39 +594,354 @@ def _imp05_route_hint(label: Optional[str]) -> Optional[str]:
return _IMP05_ROUTE_HINTS.get(label)
+def _load_frame_partial_html(template_id: str) -> str:
+ """IMP-47B u4 — Read templates/phase_z2/families/{template_id}.html.
+
+ Missing partial (e.g., ``__empty__`` shell from IMP-30) returns an
+ empty string so gather_step12_ai_repair_proposals can still build a
+ record with skip_reason without raising on file IO.
+ """
+ partial_path = TEMPLATE_DIR / "families" / f"{template_id}.html"
+ if not partial_path.is_file():
+ return ""
+ return partial_path.read_text(encoding="utf-8")
+
+
+def _run_step12_ai_repair(units) -> list[dict]:
+ """IMP-47B u4 — Wire gather_step12_ai_repair_proposals into Step 12.
+
+ Routes provisional units whose IMP-05 hint maps to
+ ``ai_adaptation_required`` (``restructure`` + ``reject`` per u1)
+ through ``src.phase_z2_ai_fallback.router``. Normal-path units
+ (``use_as_is`` / ``light_edit`` / non-provisional) record a
+ skip_reason without invoking the router; flag-off runs short-circuit
+ at the router (``settings.ai_fallback_enabled=False`` default).
+ Returns the per-unit record list — u5 consumes records for
+ PARTIAL_OVERRIDES apply and u6 writes the audit artifact.
+ """
+ return gather_step12_ai_repair_proposals(
+ units,
+ route_for_label=_imp05_route_hint,
+ get_contract_fn=get_contract,
+ frame_visual_loader=_load_frame_partial_html,
+ )
+
+
+_REJECT_SUPPORTED_PROPOSAL_KINDS: frozenset[str] = frozenset({"partial_overrides"})
+
+
+def _apply_ai_repair_proposals_to_zones(
+ ai_repair_records: list[dict],
+ unit_positions: list[str],
+ zones_data: list[dict],
+) -> None:
+ """IMP-47B u5 — Apply PARTIAL_OVERRIDES into zones_data.slot_payload.
+
+ Mutates each record's ``apply_status`` in place and merges
+ ``proposal.payload.slots`` into the matching zone. Out-of-scope
+ kinds (``builder_options_patch``, ``slot_mapping_proposal``)
+ loud-fail with ``unsupported_kind_for_reject_route:<kind>`` — zones
+ untouched (human_review surfacing → u8). IMP-33 u5 validator
+ guarantees declared-slot completeness, so ``dict.update`` is the
+ structural merge (``feedback_ai_isolation_contract``).
+ """
+ zone_by_position = {z["position"]: z for z in zones_data}
+ for record in ai_repair_records:
+ proposal = record.get("proposal")
+ if proposal is None:
+ record["apply_status"] = "no_proposal"
+ continue
+ kind = proposal.get("proposal_kind")
+ if kind not in _REJECT_SUPPORTED_PROPOSAL_KINDS:
+ record["apply_status"] = f"unsupported_kind_for_reject_route:{kind}"
+ print(
+ f" [ai-repair-apply] unit {record['unit_index']} "
+ f"proposal_kind='{kind}' out-of-scope for reject route — "
+ "skipping apply; human_review required.",
+ file=sys.stderr,
+ )
+ continue
+ unit_index = record["unit_index"]
+ position = (
+ unit_positions[unit_index]
+ if 0 <= unit_index < len(unit_positions) else None
+ )
+ zone = zone_by_position.get(position) if position is not None else None
+ if zone is None:
+ record["apply_status"] = "no_zone_match"
+ continue
+ slots = (proposal.get("payload") or {}).get("slots") or {}
+ zone["slot_payload"].update(slots)
+ record["apply_status"] = "applied:partial_overrides"
+
+
+def _check_post_ai_coverage_invariant(
+ units,
+ ai_repair_records: list[dict],
+) -> dict:
+ """IMP-47B u7 — Verify AI repair preserved every source_section_id.
+
+ Compares the union of unit-level ``source_section_ids`` (pre-AI) to
+ the union present on ``ai_repair_records`` post-apply. Per the AI
+ isolation contract + dropped 절대 룰
+ (``feedback_ai_isolation_contract``), AI repair never removes a
+ unit's section coverage. Any divergence indicates a regression that
+ u8 surfaces through ``slide_status.ai_repair_status``. The check is
+ structural (set membership); the per-record ``source_section_ids``
+ list is a copy populated by ``gather_step12_ai_repair_proposals``
+ (``step12.py:124``) so apply mutations cannot silently drop it.
+ """
+ pre_ai_ids: set[str] = set()
+ for unit in units:
+ pre_ai_ids.update(getattr(unit, "source_section_ids", []) or [])
+ post_ai_ids: set[str] = set()
+ for record in ai_repair_records:
+ post_ai_ids.update(record.get("source_section_ids") or [])
+ dropped = sorted(pre_ai_ids - post_ai_ids)
+ return {
+ "pre_ai_section_ids": sorted(pre_ai_ids),
+ "post_ai_section_ids": sorted(post_ai_ids),
+ "dropped_section_ids": dropped,
+ "status": "ok" if not dropped else "violated",
+ }
+
+
+def _persist_ai_repair_proposals_to_cache(
+ ai_repair_records: list[dict],
+ *,
+ visual_check_passed: bool,
+ user_approved: bool,
+ auto_cache: bool,
+) -> None:
+ """IMP-47B u13 — Persist applied AI repair proposals through IMP-46 gates.
+
+ Mutates each record in place with a ``cache_save_status`` axis.
+ Only records whose ``apply_status`` starts with ``"applied:"`` and
+ that still carry the original ``cache_key`` + ``fingerprints`` + a
+ serialized ``proposal`` dict are eligible — everything else marked
+ ``not_applied``. Eligible records go through
+ ``cache.save_proposal`` with the IMP-46 dual-gate truth table; the
+ helper catches :class:`AiFallbackCacheGateError` so a gate block is
+ surfaced (``gate_blocked:<reason>``) without raising into the
+ pipeline runtime (the cache is a hint, never a hard dependency —
+ cache.py contract). ``visual_check_passed`` is never bypassable;
+ ``auto_cache=True`` bypasses ONLY the ``user_approved`` gate per
+ IMP-46 u5. Pure save layer: no AI call, no MDX touch.
+ """
+ from src.phase_z2_ai_fallback.cache import (
+ AiFallbackCacheGateError,
+ save_proposal,
+ )
+ from src.phase_z2_ai_fallback.schema import AiFallbackProposal
+ for record in ai_repair_records:
+ apply_status = record.get("apply_status") or ""
+ proposal_dict = record.get("proposal")
+ cache_key = record.get("cache_key")
+ fingerprints = record.get("fingerprints")
+ if (
+ not apply_status.startswith("applied:")
+ or not isinstance(proposal_dict, dict)
+ or not cache_key
+ or not isinstance(fingerprints, dict)
+ ):
+ record["cache_save_status"] = "not_applied"
+ continue
+ try:
+ proposal_obj = AiFallbackProposal.model_validate(proposal_dict)
+ except Exception as exc: # noqa: BLE001 — invalid payload → skip, never raise
+ record["cache_save_status"] = f"invalid_proposal:{type(exc).__name__}"
+ continue
+ try:
+ save_proposal(
+ cache_key,
+ proposal_obj,
+ visual_check_passed=visual_check_passed,
+ user_approved=user_approved,
+ auto_cache=auto_cache,
+ fingerprints=fingerprints,
+ )
+ except AiFallbackCacheGateError as gate_exc:
+ record["cache_save_status"] = f"gate_blocked:{gate_exc}"
+ continue
+ record["cache_save_status"] = "saved"
+
+
+def _summarize_ai_repair_status(
+ ai_repair_records: list[dict],
+ coverage_invariant: dict,
+) -> dict:
+ """IMP-47B u8 — Classify Step 12 AI repair outcomes for slide_status surfacing.
+
+ Reads u4 gather ``error`` + u5 ``apply_status`` + u7 coverage_invariant
+ to derive a single ``ai_repair_status`` axis attached to
+ ``slide_status``. Failure-axis priority (highest → lowest):
+ ``error`` > ``coverage_violated`` > ``unsupported_kind`` > ``applied`` > ``ok``.
+ ``human_review_required`` flips True on the three failure axes so the
+ frontend (u11) can surface a notification per the IMP-47B policy
+ ("AI 호출 실패 / proposal validation 실패 / coverage 미달 → frontend notification").
+ Pure: no IO, no AI call.
+ """
+ counts = {
+ "total": len(ai_repair_records),
+ "applied": 0,
+ "no_proposal": 0,
+ "no_zone_match": 0,
+ "unsupported_kind": 0,
+ "error": 0,
+ }
+ unsupported_records: list[dict] = []
+ error_records: list[dict] = []
+ for record in ai_repair_records:
+ if record.get("error"):
+ counts["error"] += 1
+ error_records.append({
+ "unit_index": record.get("unit_index"),
+ "source_section_ids": list(record.get("source_section_ids") or []),
+ "error": record.get("error"),
+ })
+ continue
+ apply_status = record.get("apply_status") or ""
+ if apply_status.startswith("applied:"):
+ counts["applied"] += 1
+ elif apply_status.startswith("unsupported_kind_for_reject_route:"):
+ counts["unsupported_kind"] += 1
+ unsupported_records.append({
+ "unit_index": record.get("unit_index"),
+ "source_section_ids": list(record.get("source_section_ids") or []),
+ "apply_status": apply_status,
+ })
+ elif apply_status == "no_zone_match":
+ counts["no_zone_match"] += 1
+ else:
+ counts["no_proposal"] += 1
+ coverage_status = (coverage_invariant or {}).get("status", "ok")
+ dropped = list((coverage_invariant or {}).get("dropped_section_ids") or [])
+ if counts["error"]:
+ status = "error"
+ elif coverage_status != "ok":
+ status = "coverage_violated"
+ elif counts["unsupported_kind"]:
+ status = "unsupported_kind"
+ elif counts["applied"]:
+ status = "applied"
+ else:
+ status = "ok"
+ return {
+ "status": status,
+ "counts": counts,
+ "unsupported_kind_records": unsupported_records,
+ "error_records": error_records,
+ "coverage_status": coverage_status,
+ "dropped_section_ids": dropped,
+ "human_review_required": status in {"error", "coverage_violated", "unsupported_kind"},
+ }
+
+
def lookup_v4_match_with_fallback(
v4: dict,
section_id: str,
*,
raw_content: Optional[str] = None,
- max_rank: int = 3,
+ max_rank: Optional[int] = None,
alias_keys: Optional[list] = None,
allow_provisional: bool = False,
) -> tuple[Optional[V4Match], dict]:
- """Select V4 rank-1, or promote rank-2/3 when rank-1 is not auto-renderable.
+ """Select V4 rank-1, or promote rank-2..N when rank-1 is not auto-renderable.
This is an IMP-05 selector only. It uses existing V4 labels, frame-contract
presence, and the Phase Z capacity precheck; it does not call calculate_fit.
- IMP-30 u1 — when ``allow_provisional=True`` and the rank-1..max_rank chain
- is exhausted (no candidate passes MVP1 filter + contract + capacity), the
- selector synthesizes a *provisional* V4Match from the rank-1 judgment so
+ IMP-30 u1 — when ``allow_provisional=True`` and the rank-1..effective_max_rank
+ chain is exhausted (no candidate passes MVP1 filter + contract + capacity),
+ the selector synthesizes a *provisional* V4Match from the rank-1 judgment so
the first-render invariant can be satisfied downstream. The synthesized
match carries ``provisional=True``, ``selection_path="provisional_rank_1"``,
and ``fallback_reason`` mirrors the existing chain-exhaust reason. The
candidate trace shape is unchanged (synthetic injection only updates the
top-level ``selection_path`` + ``selected_*`` mirrors). When the rank-1
judgment itself is missing (``empty_v4_judgments`` / ``no_v4_section``),
- no provisional is synthesized — the caller (u3 / u4) handles those cases
- with a placeholder zone or empty-shell.
+ no provisional is synthesized — the caller handles those cases with a
+ placeholder zone or empty-shell.
Default ``allow_provisional=False`` keeps the IMP-05 behavior byte-identical.
+
+ IMP-38 — dynamic effective max_rank via ``load_v4_fallback_policy()``
+ (4 round 합의 / Codex #1~#3 + Claude #1~#4 LOCK at #67 comment 23195):
+ - ``max_rank=None`` (default) → policy applied:
+ usable_count = candidates in rank 1..default_max_rank passing 3-tier
+ predicate (status in MVP1 + catalog registered + optional capacity).
+ usable_count >= usable_threshold → effective_max_rank = default_max_rank.
+ Otherwise → effective_max_rank = min(extended_max_rank,
+ len(judgments_full32)) = effective_extended_ceiling (Codex #2 정정).
+ - ``max_rank`` explicitly passed → caller_override: that value is used
+ as-is (backward compat for tests / explicit IMP-05/IMP-30 paths).
+
+ Trace gains 8 IMP-38 fields: ``requested_max_rank``, ``default_max_rank``,
+ ``configured_extended_max_rank``, ``judgments_count``,
+ ``effective_extended_ceiling``, ``effective_max_rank``, ``usable_count``,
+ ``policy_applied``. ``max_rank`` legacy field kept as alias for backward
+ compat (= effective_max_rank).
"""
resolved = _resolve_v4_section_key(v4, section_id, alias_keys=alias_keys)
sec = v4.get("mdx_sections", {}).get(resolved) if resolved else None
+ all_judgments = (sec.get("judgments_full32") if sec else None) or []
+ judgments_count = len(all_judgments)
+
+ # IMP-38 — load policy (graceful: yaml 없을 시 default_max_rank=3, extended=3)
+ _policy = load_v4_fallback_policy()
+ default_max_rank = int(_policy.get("default_max_rank", 3))
+ configured_extended_max_rank = int(_policy.get("extended_max_rank", default_max_rank))
+ usable_threshold = int(_policy.get("usable_threshold", 1))
+ # Codex #2 정정: min(configured, len(judgments_full32)) — yaml ceiling 무력화 방지
+ effective_extended_ceiling = min(configured_extended_max_rank, judgments_count) if judgments_count else default_max_rank
+
+ usable_count: Optional[int] = None # set only when policy path active
+ if max_rank is not None:
+ # caller override (backward compat — explicit IMP-05/IMP-30 paths, tests)
+ effective_max_rank = int(max_rank)
+ policy_applied = "caller_override"
+ elif judgments_count == 0:
+ # no judgments — slicing 빈 list 라 어차피 영향 X
+ effective_max_rank = default_max_rank
+ policy_applied = "no_judgments"
+ else:
+ # IMP-38 policy path — 3-tier predicate usable_count on default window
+ usable_count = 0
+ default_window = all_judgments[:default_max_rank]
+ for _j in default_window:
+ _m = _v4_match_from_judgment(section_id, _j, rank=0)
+ if to_phase_z_status(_m) not in MVP1_ALLOWED_STATUSES:
+ continue
+ if get_contract(_m.template_id) is None:
+ continue
+ if raw_content is not None:
+ _cap = compute_capacity_fit(_m.template_id, raw_content)
+ if _cap and _cap.get("fit_status") not in {
+ "ok", "no_contract", "unknown_source_shape",
+ }:
+ continue
+ usable_count += 1
+
+ if usable_count >= usable_threshold:
+ effective_max_rank = default_max_rank
+ policy_applied = "default_max_rank"
+ else:
+ effective_max_rank = effective_extended_ceiling
+ policy_applied = "extended_max_rank"
+
trace = {
"section_id": section_id,
- "max_rank": max_rank,
+ # IMP-38 — 8 trace fields (4 round LOCK)
+ "requested_max_rank": max_rank,
+ "default_max_rank": default_max_rank,
+ "configured_extended_max_rank": configured_extended_max_rank,
+ "judgments_count": judgments_count,
+ "effective_extended_ceiling": effective_extended_ceiling,
+ "effective_max_rank": effective_max_rank,
+ "usable_count": usable_count,
+ "policy_applied": policy_applied,
+ # legacy alias for backward compat (= effective_max_rank)
+ "max_rank": effective_max_rank,
"selection_path": "no_v4_candidate",
"selected_rank": None,
"selected_template_id": None,
@@ -630,7 +955,7 @@ def lookup_v4_match_with_fallback(
trace["fallback_reason"] = "no_v4_section"
return None, trace
- judgments = (sec.get("judgments_full32") or [])[:max_rank]
+ judgments = all_judgments[:effective_max_rank]
if not judgments:
trace["fallback_reason"] = "empty_v4_judgments"
return None, trace
@@ -712,7 +1037,7 @@ def lookup_v4_match_with_fallback(
trace["candidates"].append(candidate_trace)
trace["selection_path"] = "chain_exhausted"
- trace["fallback_reason"] = first_skip_reason or "no_auto_renderable_rank_1_to_3"
+ trace["fallback_reason"] = first_skip_reason or f"no_auto_renderable_rank_1_to_{effective_max_rank}"
# IMP-30 u1 — opt-in provisional first-render synthesis. When the caller
# signals allow_provisional, promote rank-1 judgment as a provisional
@@ -805,6 +1130,54 @@ def lookup_v4_candidates(
return candidates
+def _apply_frame_override_to_unit(unit, new_tid: str, v4: dict) -> str:
+ """IMP-47B u3 — apply a frame override to *unit* in place.
+
+ Returns a meta_source string for the override book-keeping. Three
+ probe layers, in order:
+
+ 1. ``unit.v4_candidates`` (non-reject, max_n bounded). Copies
+ frame_id / frame_number / confidence / label from the matching
+ candidate so Step 9 metadata stays consistent. Returns
+ ``"v4_candidates"``.
+ 2. Full 32 V4 judgments (reject inclusive). When the override
+ target matches a reject judgment for the unit's primary section,
+ the unit is promoted to ``provisional=True`` with ``label="reject"``
+ so Step 12 (IMP-47B u4) admits the AI repair path. Returns
+ ``"v4_reject_judgment_provisional"``.
+ 3. Raw fall-through. Updates only ``frame_template_id``; returns
+ ``"raw_template_id_only"``.
+
+ Frame visual / contract stay untouched per the AI isolation contract
+ (frame auto-swap forbidden — AI re-places content into the existing
+ frame only). The caller validates catalog contract presence before
+ invoking this helper.
+ """
+ for cand in (unit.v4_candidates or []):
+ if getattr(cand, "template_id", None) == new_tid:
+ unit.frame_template_id = cand.template_id
+ unit.frame_id = cand.frame_id
+ unit.frame_number = cand.frame_number
+ unit.confidence = cand.confidence
+ unit.label = cand.label
+ return "v4_candidates"
+ primary_sid = (
+ unit.source_section_ids[0] if unit.source_section_ids else None
+ )
+ if primary_sid:
+ for j in lookup_v4_all_judgments(v4, primary_sid):
+ if j.template_id == new_tid and j.label == "reject":
+ unit.frame_template_id = j.template_id
+ unit.frame_id = j.frame_id
+ unit.frame_number = j.frame_number
+ unit.confidence = j.confidence
+ unit.label = "reject"
+ unit.provisional = True
+ return "v4_reject_judgment_provisional"
+ unit.frame_template_id = new_tid
+ return "raw_template_id_only"
+
+
# ─── Content weight + zone layout 계산 ─────────────────────────
# layout preset 선택은 phase_z2_composition.select_layout_preset (composition v0) 가 담당.
# 본 모듈의 select_layout_preset 은 이전 단순 count-based 구현이었고 dead code 로 제거 (2026-04-29).
@@ -3218,7 +3591,6 @@ def run_phase_z2_mvp1(
v4,
sid,
raw_content=section_content_by_id.get(sid),
- max_rank=3,
alias_keys=section_alias_by_id.get(sid),
)
v4_fallback_traces[sid] = trace
@@ -3264,6 +3636,57 @@ def run_phase_z2_mvp1(
),
}
+ # IMP-47B u12 — mixed direct+reject first-render admission.
+ # When initial plan_composition produces a viable layout but at least one
+ # section remains uncovered (typically chain_exhausted / reject), re-run
+ # with allow_provisional in the lookup + allow_provisional_fill=True so
+ # reject sections gain a provisional rank-1 V4Match and a last-resort
+ # provisional candidate fill. This admits the mixed direct+reject case
+ # to the AI repair path (IMP-47B u4/u5) on first render. Skipped under
+ # --override-section-assignments to preserve the operator's plan and
+ # mirror the IMP-30 u4 retry's section_assignment_plan gate. All-direct
+ # slides have no uncovered sections so this is a no-op. The all-reject
+ # case is still handled by the IMP-30 u4 retry block below (initial
+ # plan_composition returns units=[]).
+ if units and layout_preset is not None and not override_section_assignments:
+ _u12_covered_ids: set[str] = set()
+ for _u in units:
+ _u12_covered_ids.update(_u.source_section_ids)
+ _u12_uncovered_ids = [
+ s.section_id for s in sections if s.section_id not in _u12_covered_ids
+ ]
+ if _u12_uncovered_ids:
+ def _lookup_fn_mixed_admission(sid: str) -> Optional[V4Match]:
+ match, trace = lookup_v4_match_with_fallback(
+ v4,
+ sid,
+ raw_content=section_content_by_id.get(sid),
+ alias_keys=section_alias_by_id.get(sid),
+ allow_provisional=True,
+ )
+ v4_fallback_traces[sid] = trace
+ return match
+
+ units_mixed, layout_preset_mixed, _comp_debug_mixed = plan_composition(
+ sections,
+ _lookup_fn_mixed_admission,
+ V4_LABEL_TO_PHASE_Z_STATUS,
+ MVP1_ALLOWED_STATUSES,
+ capacity_fit_fn=compute_capacity_fit,
+ v4_candidates_lookup_fn=candidates_lookup_fn,
+ allow_provisional_fill=True,
+ )
+ if units_mixed and layout_preset_mixed is not None:
+ units = units_mixed
+ layout_preset = layout_preset_mixed
+ comp_debug["v4_fallback_selections"] = list(v4_fallback_traces.values())
+ comp_debug["imp47b_u12_mixed_admission"] = {
+ "applied": True,
+ "uncovered_before": _u12_uncovered_ids,
+ "result_unit_count": len(units_mixed),
+ "result_layout_preset": layout_preset_mixed,
+ }
+
# ── Step 7-A axis : layout override ──
# 사용자가 LayoutPanel 에서 다른 preset 을 선택했을 때 자동 결정값을 강제 변경.
# 길이 mismatch (positions count vs unit count) 는 zone loop 의 fallback (zone_{i})
@@ -3434,7 +3857,6 @@ def run_phase_z2_mvp1(
v4,
sid,
raw_content=section_content_by_id.get(sid),
- max_rank=3,
alias_keys=section_alias_by_id.get(sid),
allow_provisional=True,
)
@@ -3613,7 +4035,10 @@ def run_phase_z2_mvp1(
# {unit_id: template_id} 형식. unit_id 매칭 시 unit.frame_template_id 강제 변경.
# v4_candidates 안에서 같은 template_id 를 가진 entry 를 찾으면 frame_id /
# frame_number / confidence / label 까지 그 entry 에서 가져와 갱신 — 그래야 step09
- # artifact 의 메타가 일관됨.
+ # artifact 의 메타가 일관됨. IMP-47B u3 (2026-05-21) : v4_candidates miss 시
+ # 전 32 judgments 까지 probe — reject 라벨 frame 을 사용자가 선택한 경우
+ # unit 을 provisional=True 로 승격해 Step 12 AI 재구성 게이트를 통과시킴
+ # (frame 유지, 자동 frame swap 금지 — [[feedback_ai_isolation_contract]]).
# frame contract 가 catalog 에 등록 안 된 template_id 면 skip + warning —
# crash 방지 (V4 score 는 매겨지지만 catalog partial 은 없는 후보 존재).
frame_overrides_applied: list[dict] = []
@@ -3642,21 +4067,7 @@ def run_phase_z2_mvp1(
file=sys.stderr,
)
continue
- match = None
- for cand in (unit.v4_candidates or []):
- if getattr(cand, "template_id", None) == new_tid:
- match = cand
- break
- if match is not None:
- unit.frame_template_id = match.template_id
- unit.frame_id = match.frame_id
- unit.frame_number = match.frame_number
- unit.confidence = match.confidence
- unit.label = match.label
- meta_source = "v4_candidates"
- else:
- unit.frame_template_id = new_tid
- meta_source = "raw_template_id_only"
+ meta_source = _apply_frame_override_to_unit(unit, new_tid, v4)
frame_overrides_applied.append({
"unit_id": unit_id,
"from": old_tid,
@@ -4258,6 +4669,58 @@ def run_phase_z2_mvp1(
note="B4 PlacementPlan slot_assignments — render path 미연결. 실제 render slot 매핑은 mapper.py 의 builder.",
)
+ # ─── Step 12 IMP-47B u4 — AI repair proposal gather ───
+ # Wire gather_step12_ai_repair_proposals so reject / restructure
+ # provisional units reach the AI fallback router. Normal-path units
+ # (use_as_is / light_edit / non-provisional) skip via the catch-all
+ # route gate; flag-off runs short-circuit at the router. Stored locally
+ # for u5 (PARTIAL_OVERRIDES apply) + u6 (step12_ai_repair.json audit).
+ ai_repair_records = _run_step12_ai_repair(units)
+
+ # ─── Step 12 IMP-47B u5 — Apply PARTIAL_OVERRIDES proposals ───
+ # Mirror the per-unit position derivation from the render loop above
+ # (L3789-3796); apply merges slots into zone slot_payload, loud-fails
+ # unsupported kinds via apply_status marker.
+ unit_positions: list[str] = []
+ for _i, _unit in enumerate(units):
+ _pos = positions[_i] if _i < len(positions) else f"zone_{_i}"
+ _plan_record = render_record_by_unit_id.get(id(_unit))
+ if _plan_record is not None and _plan_record.get("position"):
+ _pos = _plan_record["position"]
+ unit_positions.append(_pos)
+ _apply_ai_repair_proposals_to_zones(ai_repair_records, unit_positions, zones_data)
+
+ # ─── Step 12 IMP-47B u7 — Post-AI source_section_ids coverage invariant ───
+ # Structural defense: AI repair must not silently drop a unit's
+ # source_section_ids. dropped 절대 룰 — text_block / table / image /
+ # details deletion forbidden. Result feeds u6 audit (below) and
+ # u8 slide_status.ai_repair_status surfacing.
+ ai_repair_coverage_invariant = _check_post_ai_coverage_invariant(
+ units, ai_repair_records,
+ )
+
+ # ─── Step 12 IMP-47B u6 — AI repair audit artifact ───
+ # Persist per-unit gather/apply outcomes (route_hint, skip_reason,
+ # apply_status, ai_called, proposal kind, cache_key, fingerprints)
+ # so reviewers can audit which units reached the AI fallback router
+ # and what happened. Flag-off default → every record has
+ # ai_called=False + apply_status='no_proposal'; flag-on +
+ # provisional reject/restructure → router_short_circuit (cache miss
+ # without client) or applied:partial_overrides (cache hit / live AI).
+ # u7 coverage_invariant rides alongside per_unit for reviewers.
+ _write_step_artifact(
+ run_dir, 12, "ai_repair",
+ data={
+ "per_unit": ai_repair_records,
+ "coverage_invariant": ai_repair_coverage_invariant,
+ },
+ step_status="done",
+ pipeline_path_connected=True,
+ inputs=["step10_frame_contract.json", "step02_normalized.json"],
+ outputs=["step12_ai_repair.json"],
+ note="IMP-47B u6 — Step 12 AI repair gather + apply records per unit (route, skip_reason, apply_status, proposal). u7 coverage_invariant = pre/post AI source_section_ids set comparison.",
+ )
+
# ─── Step 12: Slot Payload (actual values, mapper.py 결과) ───
_write_step_artifact(
run_dir, 12, "slot_payload",
@@ -4872,6 +5335,24 @@ def run_phase_z2_mvp1(
),
)
+ # ─── IMP-47B u13: Persist validated AI repair proposals to cache ───
+ # Saves each applied PARTIAL_OVERRIDES proposal AFTER Step 14 visual
+ # check + per IMP-46 dual-gate. ``visual_check_passed`` reads the
+ # Selenium overflow result; ``auto_cache`` sourced from Settings
+ # (CLI --auto-cache wires settings.ai_fallback_auto_cache at parse
+ # time, src/phase_z2_pipeline.py:5631-5633). ``user_approved`` stays
+ # False — the pipeline has no UX approval gate; the auto_cache
+ # opt-in is the documented bypass per IMP-46 u5. Gate violations
+ # surface as ``cache_save_status='gate_blocked:<reason>'`` on the
+ # record (cache is a hint, never a hard dependency).
+ from src.config import settings as _ai_cache_settings
+ _persist_ai_repair_proposals_to_cache(
+ ai_repair_records,
+ visual_check_passed=bool(overflow.get("passed")),
+ user_approved=False,
+ auto_cache=bool(_ai_cache_settings.ai_fallback_auto_cache),
+ )
+
# 10. fit_classifier v0 (A1) — Selenium 결과 → spec §3 category 분류 layer.
# *분류만*. action / router / rerender X. behavior 변경 0.
fit_classification = classify_visual_runtime_check(overflow, debug_zones)
@@ -5055,6 +5536,16 @@ def run_phase_z2_mvp1(
debug_zones=debug_zones,
)
+ # IMP-47B u8 — Surface Step 12 AI repair outcomes through slide_status.
+ # Composes u4 gather errors + u5 apply_status + u7 coverage_invariant
+ # into a single ``ai_repair_status`` axis the frontend (u11) reads to
+ # render human_review notifications. Auto pipeline first
+ # ([[feedback_auto_pipeline_first]]) — no review_queue insertion;
+ # explicit status enum + human_review_required flag.
+ slide_status["ai_repair_status"] = _summarize_ai_repair_status(
+ ai_repair_records, ai_repair_coverage_invariant,
+ )
+
# ─── Step 20: Slide Status ───
_write_step_artifact(
run_dir, 20, "slide_status",
@@ -5076,6 +5567,11 @@ def run_phase_z2_mvp1(
_aligned = slide_status.get("aligned_section_ids") or []
_covered = slide_status.get("covered_section_ids") or []
_filtered = slide_status.get("filtered_section_ids") or []
+ _ai_repair = slide_status.get("ai_repair_status") or {}
+ _ai_repair_label = (
+ f'{_ai_repair.get("status", "?")} '
+ f'(human_review_required={_ai_repair.get("human_review_required", False)})'
+ )
_write_step_html(
run_dir, 20, "final_status",
title="Final Slide Status",
@@ -5090,6 +5586,7 @@ def run_phase_z2_mvp1(
f'<tr><th>filtered_section_ids</th><td>{_filtered}</td></tr>'
f'<tr><th>adapter_needed_count</th><td>{slide_status.get("adapter_needed_count", 0)}</td></tr>'
f'<tr><th>content_truncated_count</th><td>{slide_status.get("content_truncated_count", 0)}</td></tr>'
+ f'<tr><th>ai_repair_status</th><td>{_ai_repair_label}</td></tr>'
f'</table>'
f'<h2>Visual Fail Reasons</h2>{_vfs_html}'
f'<h2>Note</h2><p>{slide_status.get("note", "")}</p>'
@@ -5260,8 +5757,29 @@ if __name__ == "__main__":
"--override-section-assignment bottom=03-2,03-3"
),
)
+ # IMP-46 u5 — auto-cache opt-in. When set, ``cache.save_proposal``
+ # bypasses the ``user_approved`` gate only (``visual_check_passed``
+ # is never bypassable). Source of truth is
+ # ``settings.ai_fallback_auto_cache`` (src/config.py); this flag
+ # mutates the setting in-process so downstream callers read the
+ # same value through Settings rather than parsing args themselves.
+ parser.add_argument(
+ "--auto-cache",
+ dest="auto_cache",
+ action="store_true",
+ default=False,
+ help=(
+ "Allow cache.save_proposal to bypass the user_approved gate "
+ "(visual_check_passed remains mandatory). Sets "
+ "settings.ai_fallback_auto_cache=True for this run."
+ ),
+ )
args = parser.parse_args()
+ if args.auto_cache:
+ from src.config import settings as _settings
+ _settings.ai_fallback_auto_cache = True
+
overrides_frames: dict[str, str] = {}
for ov in args.override_frames:
if "=" not in ov:
codex
The u13 helper and call site are present. Im now checking the cache gate contract directly and running the focused u13 test, because this units correctness depends on the exact `save_proposal` failure behavior rather than broad pipeline tests.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\cache.py' | Select-Object -Skip 150 -First 90" in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_pipeline.py' | Select-Object -Skip 680 -First 100" in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_pipeline.py' | Select-Object -Skip 5310 -First 60" in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'tests\\test_imp47b_cache_save_gate.py' | Select-Object -First 260" in D:\ad-hoc\kei\design_agent
succeeded in 184ms:
return None
try:
return AiFallbackProposal.model_validate(proposal_dict)
except Exception: # noqa: BLE001 ??corrupt payload must miss, not raise
return None
def save_proposal(
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:
"""Persist ``proposal`` under ``key`` once the IMP-46 gates clear.
Gate contract (IMP-46 u5 truth table):
* ``visual_check_passed=False`` -> :class:`AiFallbackCacheGateError`
always (never bypassable; ``auto_cache`` cannot override).
* ``user_approved=False`` AND ``auto_cache=False`` ->
:class:`AiFallbackCacheGateError`.
* ``user_approved=False`` AND ``auto_cache=True`` -> bypass the
user-approval gate (IMP-46 u5 CLI / settings opt-in).
* Otherwise (``visual_check_passed=True`` AND either
``user_approved=True`` OR ``auto_cache=True``) -> persist payload.
Gate violations are raised BEFORE any filesystem touch ??no parent
directory is created, no file is written. When the gates clear the
JSON payload (schema_version + proposal + slide_css + fingerprints)
is written to ``data/frame_cache/{frame_id}/{signature_hash}.json``
and the resolved :class:`pathlib.Path` is returned.
``slide_css`` may be ``None`` (no slide-level CSS captured) or a
string. ``fingerprints`` may be ``None`` (treated as empty dict) or a
dict mapping fingerprint name to SHA hex digest.
``auto_cache`` is keyword-only and defaults to ``False``. It is wired
from :data:`src.config.settings.ai_fallback_auto_cache`, which the
``--auto-cache`` CLI flag in ``src/phase_z2_pipeline.py`` toggles at
parse time. The cache module never reads the setting itself ??the
caller passes the resolved boolean ??so AI-isolation contracts
(no Phase Z runtime / no Anthropic import) remain intact.
"""
if not isinstance(key, str) or not key:
raise ValueError("cache key must be a non-empty string")
if not isinstance(proposal, AiFallbackProposal):
raise TypeError(
"proposal must be an AiFallbackProposal instance "
f"(got {type(proposal).__name__})"
)
if not isinstance(auto_cache, bool):
raise TypeError("auto_cache must be a bool")
if not visual_check_passed:
raise AiFallbackCacheGateError(
"IMP-46 gate: visual_check_passed=False; refusing to cache an "
"unverified proposal. (auto_cache cannot bypass this gate.)"
)
if not user_approved and not auto_cache:
raise AiFallbackCacheGateError(
"IMP-46 gate: user_approved=False and auto_cache=False; "
"refusing to cache without explicit user approval. Pass "
"auto_cache=True (or --auto-cache on the CLI) to bypass."
)
if slide_css is not None and not isinstance(slide_css, str):
raise TypeError("slide_css must be a string or None")
if fingerprints is None:
fingerprints = {}
elif not isinstance(fingerprints, dict):
raise TypeError("fingerprints must be a dict or None")
parsed = _parse_key(key)
if parsed is None:
raise ValueError(
"cache key must be in "
f"'frame_id{KEY_DELIMITER}signature_hash' format; got {key!r}"
)
frame_id, signature_hash = parsed
path = _cache_path(frame_id, signature_hash)
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"schema_version": SCHEMA_VERSION,
"proposal": proposal.model_dump(mode="json"),
"slide_css": slide_css,
"fingerprints": dict(fingerprints),
}
path.write_text(
json.dumps(payload, sort_keys=True, ensure_ascii=False, indent=2),
succeeded in 185ms:
) -> dict:
"""IMP-47B u7 ??Verify AI repair preserved every source_section_id.
Compares the union of unit-level ``source_section_ids`` (pre-AI) to
the union present on ``ai_repair_records`` post-apply. Per the AI
isolation contract + dropped ?덈? 猷?
(``feedback_ai_isolation_contract``), AI repair never removes a
unit's section coverage. Any divergence indicates a regression that
u8 surfaces through ``slide_status.ai_repair_status``. The check is
structural (set membership); the per-record ``source_section_ids``
list is a copy populated by ``gather_step12_ai_repair_proposals``
(``step12.py:124``) so apply mutations cannot silently drop it.
"""
pre_ai_ids: set[str] = set()
for unit in units:
pre_ai_ids.update(getattr(unit, "source_section_ids", []) or [])
post_ai_ids: set[str] = set()
for record in ai_repair_records:
post_ai_ids.update(record.get("source_section_ids") or [])
dropped = sorted(pre_ai_ids - post_ai_ids)
return {
"pre_ai_section_ids": sorted(pre_ai_ids),
"post_ai_section_ids": sorted(post_ai_ids),
"dropped_section_ids": dropped,
"status": "ok" if not dropped else "violated",
}
def _persist_ai_repair_proposals_to_cache(
ai_repair_records: list[dict],
*,
visual_check_passed: bool,
user_approved: bool,
auto_cache: bool,
) -> None:
"""IMP-47B u13 ??Persist applied AI repair proposals through IMP-46 gates.
Mutates each record in place with a ``cache_save_status`` axis.
Only records whose ``apply_status`` starts with ``"applied:"`` and
that still carry the original ``cache_key`` + ``fingerprints`` + a
serialized ``proposal`` dict are eligible ??everything else marked
``not_applied``. Eligible records go through
``cache.save_proposal`` with the IMP-46 dual-gate truth table; the
helper catches :class:`AiFallbackCacheGateError` so a gate block is
surfaced (``gate_blocked:<reason>``) without raising into the
pipeline runtime (the cache is a hint, never a hard dependency ??
cache.py contract). ``visual_check_passed`` is never bypassable;
``auto_cache=True`` bypasses ONLY the ``user_approved`` gate per
IMP-46 u5. Pure save layer: no AI call, no MDX touch.
"""
from src.phase_z2_ai_fallback.cache import (
AiFallbackCacheGateError,
save_proposal,
)
from src.phase_z2_ai_fallback.schema import AiFallbackProposal
for record in ai_repair_records:
apply_status = record.get("apply_status") or ""
proposal_dict = record.get("proposal")
cache_key = record.get("cache_key")
fingerprints = record.get("fingerprints")
if (
not apply_status.startswith("applied:")
or not isinstance(proposal_dict, dict)
or not cache_key
or not isinstance(fingerprints, dict)
):
record["cache_save_status"] = "not_applied"
continue
try:
proposal_obj = AiFallbackProposal.model_validate(proposal_dict)
except Exception as exc: # noqa: BLE001 ??invalid payload ??skip, never raise
record["cache_save_status"] = f"invalid_proposal:{type(exc).__name__}"
continue
try:
save_proposal(
cache_key,
proposal_obj,
visual_check_passed=visual_check_passed,
user_approved=user_approved,
auto_cache=auto_cache,
fingerprints=fingerprints,
)
except AiFallbackCacheGateError as gate_exc:
record["cache_save_status"] = f"gate_blocked:{gate_exc}"
continue
record["cache_save_status"] = "saved"
def _summarize_ai_repair_status(
ai_repair_records: list[dict],
coverage_invariant: dict,
) -> dict:
"""IMP-47B u8 ??Classify Step 12 AI repair outcomes for slide_status surfacing.
Reads u4 gather ``error`` + u5 ``apply_status`` + u7 coverage_invariant
to derive a single ``ai_repair_status`` axis attached to
``slide_status``. Failure-axis priority (highest ??lowest):
``error`` > ``coverage_violated`` > ``unsupported_kind`` > ``applied`` > ``ok``.
``human_review_required`` flips True on the three failure axes so the
frontend (u11) can surface a notification per the IMP-47B policy
succeeded in 260ms:
inputs=["step07_layout.json", "step12_slot_payload.json", "templates/phase_z2/slide_base.html", "templates/phase_z2/families/*.html"],
outputs=["step13_render.json", "step13_draft_render.html", "final.html"],
note="Jinja2 render 寃곌낵 = final.html. step13_draft_render.html ?€ final.html ?숈씪 蹂듭궗 (寃€?좎슜).",
)
# Step 13 HTML ??final.html 洹몃?濡?蹂듭궗 (寃€?좎슜)
(run_dir / "steps" / "step13_draft_render.html").write_text(html, encoding="utf-8")
# 9. Selenium check
print(" visual : running per-zone overflow check ...")
overflow = run_overflow_check(out_path)
# ?€?€?€ Step 14: Visual Runtime Check (?ㅼ륫媛? ?€?€?€
_write_step_artifact(
run_dir, 14, "visual_check",
data=overflow,
step_status="partial",
pipeline_path_connected=True,
inputs=["step13_render.json", "final.html"],
outputs=["step14_visual_check.json"],
note=(
"Selenium ?ㅼ륫 ??clientHeight / scrollHeight / excess_y / frame_slot_metrics. "
"Step 8 ??怨꾪쉷媛믨낵 鍮꾧탳 ???대뒓 cell ??overflow ?덈뒗吏€ 諛뺥옒. "
"image_aspect_mismatch 寃€??異붽? (IMP-15 ?ㅽ뻾-1, issue #45) ??image_events[] + fail_reasons. "
"table 寃€??遺€??(?ㅽ뻾-2 ?붾쪟) ??Step 14 ??partial (table only)."
),
)
# ?€?€?€ IMP-47B u13: Persist validated AI repair proposals to cache ?€?€?€
# Saves each applied PARTIAL_OVERRIDES proposal AFTER Step 14 visual
# check + per IMP-46 dual-gate. ``visual_check_passed`` reads the
# Selenium overflow result; ``auto_cache`` sourced from Settings
# (CLI --auto-cache wires settings.ai_fallback_auto_cache at parse
# time, src/phase_z2_pipeline.py:5631-5633). ``user_approved`` stays
# False ??the pipeline has no UX approval gate; the auto_cache
# opt-in is the documented bypass per IMP-46 u5. Gate violations
# surface as ``cache_save_status='gate_blocked:<reason>'`` on the
# record (cache is a hint, never a hard dependency).
from src.config import settings as _ai_cache_settings
_persist_ai_repair_proposals_to_cache(
ai_repair_records,
visual_check_passed=bool(overflow.get("passed")),
user_approved=False,
auto_cache=bool(_ai_cache_settings.ai_fallback_auto_cache),
)
# 10. fit_classifier v0 (A1) ??Selenium 寃곌낵 ??spec 짠3 category 遺꾨쪟 layer.
# *遺꾨쪟留?. action / router / rerender X. behavior 蹂€寃?0.
fit_classification = classify_visual_runtime_check(overflow, debug_zones)
# ?€?€?€ Step 15: Fit Classification ?€?€?€
_write_step_artifact(
run_dir, 15, "fit_classification",
data=fit_classification,
step_status="done",
pipeline_path_connected=True,
inputs=["step14_visual_check.json"],
outputs=["step15_fit_classification.json"],
note="A1 ??visual_runtime_check 寃곌낵瑜?spec 짠3 category 濡?遺꾨쪟.",
)
succeeded in 170ms:
"""IMP-47B u13 ??Persist validated proposals through ``save_proposal`` after gates.
Scope (this slice):
Verify the new ``_persist_ai_repair_proposals_to_cache`` helper in
``src/phase_z2_pipeline.py`` honours the IMP-46 dual-gate truth table
on the post-Step-14 cache-save seam. The helper is exercised in
isolation (no Selenium, no full pipeline) with synthetic AI repair
records that mirror the gather ??apply ??coverage chain shape
produced by IMP-47B u4 / u5 / u7.
Guardrails proven by this test (IMP-46 + IMP-47B policy bullets):
* ``visual_check_passed=False`` always blocks ??never bypassable, even
when ``auto_cache=True`` (IMP-46 u5 truth table cell).
* ``user_approved=False`` AND ``auto_cache=False`` ??gate blocked
(default pipeline path has no UX approval gate; ``--auto-cache`` is
the documented bypass).
* ``visual_check_passed=True`` AND ``auto_cache=True`` ??proposal
persisted on disk under ``data/frame_cache/{frame_id}/{hash}.json``
via ``cache.save_proposal``.
* Non-applied records (no_proposal / no_zone_match / unsupported /
error) ??``cache_save_status='not_applied'`` and NEVER reach
``save_proposal`` (no filesystem touch).
* Settings axis ??``settings.ai_fallback_auto_cache`` sourced through
the helper kwargs, never inlined (hardcoding ban).
"""
from __future__ import annotations
import pathlib
import pytest
from src.phase_z2_ai_fallback import cache as cache_mod
from src.phase_z2_ai_fallback.cache import AiFallbackCacheGateError
from src.phase_z2_ai_fallback.schema import AiFallbackProposal, ProposalKind
from src.phase_z2_pipeline import _persist_ai_repair_proposals_to_cache
def _applied_record(
*,
cache_key: str = "MOCK_FRAME::deadbeef" + "0" * 56,
fingerprints: dict | None = None,
slots: dict | None = None,
) -> dict:
"""Build an IMP-47B u4/u5 shaped record marked ``applied:partial_overrides``."""
if fingerprints is None:
fingerprints = {"contract_sha": "c1", "partial_sha": "p1", "catalog_sha": "k1"}
if slots is None:
slots = {"title": "AI repaired", "bullets": ["b1", "b2"]}
proposal = AiFallbackProposal(
proposal_kind=ProposalKind.PARTIAL_OVERRIDES,
payload={"slots": slots},
rationale="cache save gate test",
)
return {
"unit_index": 0,
"source_section_ids": ["MOCK_S1"],
"frame_template_id": "MOCK_FRAME",
"label": "reject",
"route_hint": "ai_adaptation_required",
"provisional": True,
"ai_called": True,
"skip_reason": None,
"proposal": proposal.model_dump(),
"error": None,
"cache_key": cache_key,
"fingerprints": fingerprints,
"apply_status": "applied:partial_overrides",
}
@pytest.fixture(autouse=True)
def _isolate_cache_root(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch):
"""Redirect ``cache.CACHE_ROOT`` to a per-test tmp dir so save_proposal
writes never touch the real ``data/frame_cache/`` tree."""
monkeypatch.setattr(cache_mod, "CACHE_ROOT", tmp_path / "frame_cache")
yield tmp_path / "frame_cache"
def test_visual_check_failed_blocks_save_even_with_auto_cache(_isolate_cache_root):
"""visual_check_passed=False is never bypassable ??auto_cache cannot override."""
record = _applied_record()
records = [record]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=False,
user_approved=True,
auto_cache=True,
)
assert record["cache_save_status"].startswith("gate_blocked:")
assert "visual_check_passed=False" in record["cache_save_status"]
# No filesystem write occurred.
assert not _isolate_cache_root.exists() or not any(_isolate_cache_root.rglob("*.json"))
def test_user_not_approved_and_no_auto_cache_blocks_save(_isolate_cache_root):
"""Default pipeline path (user_approved=False, auto_cache=False) ??gate blocked."""
record = _applied_record()
records = [record]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=True,
user_approved=False,
auto_cache=False,
)
assert record["cache_save_status"].startswith("gate_blocked:")
assert "user_approved=False" in record["cache_save_status"]
assert not _isolate_cache_root.exists() or not any(_isolate_cache_root.rglob("*.json"))
def test_visual_passed_and_auto_cache_persists_proposal(_isolate_cache_root):
"""Happy path ??visual_check_passed=True + auto_cache=True persists JSON."""
record = _applied_record()
records = [record]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=True,
user_approved=False,
auto_cache=True,
)
assert record["cache_save_status"] == "saved"
written = list(_isolate_cache_root.rglob("*.json"))
assert len(written) == 1
# Layout = {CACHE_ROOT}/{frame_id}/{signature_hash}.json.
written_path = written[0]
assert written_path.parent.name == "MOCK_FRAME"
def test_non_applied_records_are_skipped_without_filesystem_touch(_isolate_cache_root):
"""no_proposal / no_zone_match / unsupported_kind / error ??never reach save_proposal."""
no_proposal_record = {
"unit_index": 0,
"apply_status": "no_proposal",
"proposal": None,
"cache_key": None,
"fingerprints": None,
}
no_zone_record = {
"unit_index": 1,
"apply_status": "no_zone_match",
"proposal": {"proposal_kind": "partial_overrides", "payload": {"slots": {}}, "rationale": ""},
"cache_key": "MOCK::abc",
"fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
}
unsupported_record = {
"unit_index": 2,
"apply_status": "unsupported_kind_for_reject_route:builder_options_patch",
"proposal": {"proposal_kind": "builder_options_patch", "payload": {}, "rationale": ""},
"cache_key": "MOCK::def",
"fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
}
error_record = {
"unit_index": 3,
"apply_status": None,
"proposal": None,
"cache_key": "MOCK::ghi",
"fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
"error": "RuntimeError: boom",
}
records = [no_proposal_record, no_zone_record, unsupported_record, error_record]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=True,
user_approved=True,
auto_cache=True,
)
for r in records:
assert r["cache_save_status"] == "not_applied"
# Zero JSON files written because none of the records were applied.
assert not _isolate_cache_root.exists() or not any(_isolate_cache_root.rglob("*.json"))
def test_mixed_records_only_persist_applied_ones(_isolate_cache_root):
"""Mixed batch ??only the ``applied:`` record is persisted."""
applied = _applied_record(cache_key="MOCK_FRAME::aaaaaaaa" + "0" * 56)
not_applied = {
"unit_index": 1,
"apply_status": "no_proposal",
"proposal": None,
"cache_key": None,
"fingerprints": None,
}
records = [applied, not_applied]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=True,
user_approved=False,
auto_cache=True,
)
assert applied["cache_save_status"] == "saved"
assert not_applied["cache_save_status"] == "not_applied"
written = list(_isolate_cache_root.rglob("*.json"))
assert len(written) == 1
def test_invalid_proposal_payload_surfaces_without_raising(_isolate_cache_root):
"""Malformed ``proposal`` dict ??``cache_save_status='invalid_proposal:...'``,
no filesystem write, no exception bubbling into the pipeline runtime."""
bad_record = {
"unit_index": 0,
"apply_status": "applied:partial_overrides",
"proposal": {"proposal_kind": "not_a_valid_enum_value", "payload": {}, "rationale": ""},
"cache_key": "MOCK::bad",
"fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
}
records = [bad_record]
_persist_ai_repair_proposals_to_cache(
records,
visual_check_passed=True,
user_approved=True,
auto_cache=True,
)
assert bad_record["cache_save_status"].startswith("invalid_proposal:")
assert not _isolate_cache_root.exists() or not any(_isolate_cache_root.rglob("*.json"))
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'pytest -q tests/test_imp47b_cache_save_gate.py' in D:\ad-hoc\kei\design_agent
succeeded in 4260ms:
...... [100%]Running teardown with pytest sessionfinish...
6 passed in 0.06s
codex
The focused u13 tests pass. Im doing one last contract check on the upstream records that feed the helper so I can distinguish a real gap from an impossible synthetic case.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'rg -n "cache_key|fingerprints|make_cache_key|signature" src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/signature.py tests/test_imp47b_step12_ai_wiring.py tests/test_imp47b_cache_save_gate.py' in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\step12.py' | Select-Object -Skip 60 -First 120" in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\router.py' | Select-Object -Skip 1 -First 130" in D:\ad-hoc\kei\design_agent
succeeded in 106ms:
tests/test_imp47b_cache_save_gate.py:40: cache_key: str = "MOCK_FRAME::deadbeef" + "0" * 56,
tests/test_imp47b_cache_save_gate.py:41: fingerprints: dict | None = None,
tests/test_imp47b_cache_save_gate.py:45: if fingerprints is None:
tests/test_imp47b_cache_save_gate.py:46: fingerprints = {"contract_sha": "c1", "partial_sha": "p1", "catalog_sha": "k1"}
tests/test_imp47b_cache_save_gate.py:65: "cache_key": cache_key,
tests/test_imp47b_cache_save_gate.py:66: "fingerprints": fingerprints,
tests/test_imp47b_cache_save_gate.py:123: # Layout = {CACHE_ROOT}/{frame_id}/{signature_hash}.json.
tests/test_imp47b_cache_save_gate.py:134: "cache_key": None,
tests/test_imp47b_cache_save_gate.py:135: "fingerprints": None,
tests/test_imp47b_cache_save_gate.py:141: "cache_key": "MOCK::abc",
tests/test_imp47b_cache_save_gate.py:142: "fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
tests/test_imp47b_cache_save_gate.py:148: "cache_key": "MOCK::def",
tests/test_imp47b_cache_save_gate.py:149: "fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
tests/test_imp47b_cache_save_gate.py:155: "cache_key": "MOCK::ghi",
tests/test_imp47b_cache_save_gate.py:156: "fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
tests/test_imp47b_cache_save_gate.py:174: applied = _applied_record(cache_key="MOCK_FRAME::aaaaaaaa" + "0" * 56)
tests/test_imp47b_cache_save_gate.py:179: "cache_key": None,
tests/test_imp47b_cache_save_gate.py:180: "fingerprints": None,
tests/test_imp47b_cache_save_gate.py:202: "cache_key": "MOCK::bad",
tests/test_imp47b_cache_save_gate.py:203: "fingerprints": {"contract_sha": "c", "partial_sha": "p", "catalog_sha": "k"},
src/phase_z2_ai_fallback/router.py:45: cache_key: str,
src/phase_z2_ai_fallback/router.py:66: cached = read_proposal(cache_key)
tests/test_imp47b_step12_ai_wiring.py:103: # cache_key / fingerprints populated only after the route + provisional
tests/test_imp47b_step12_ai_wiring.py:105: assert records[0]["cache_key"] is not None
tests/test_imp47b_step12_ai_wiring.py:106: assert records[0]["fingerprints"] is not None
src/phase_z2_ai_fallback/signature.py:1:"""IMP-46 u1 — Frame transformation cache signature builder.
src/phase_z2_ai_fallback/signature.py:8: * No sample/section identifiers in the signature surface (no-hardcoding lock).
src/phase_z2_ai_fallback/signature.py:11: via :func:`bucket_char_count` before being fed to :func:`build_signature`.
src/phase_z2_ai_fallback/signature.py:56:def build_signature(
src/phase_z2_ai_fallback/step12.py:25:IMP-46 u4 — structural cache key + fingerprints
src/phase_z2_ai_fallback/step12.py:28:The legacy ``cache_key`` was ``"{template_id}::{sorted(source_section_ids)}"``
src/phase_z2_ai_fallback/step12.py:32:``"{frame_id}::{signature_hash}"`` where ``signature_hash`` is the
src/phase_z2_ai_fallback/step12.py:34:``src.phase_z2_ai_fallback.signature``). Per-unit signature inputs are
src/phase_z2_ai_fallback/step12.py:44:In parallel the three invalidation fingerprints
src/phase_z2_ai_fallback/step12.py:49:``read_proposal(cache_key)`` continues to perform exact-match lookup only
src/phase_z2_ai_fallback/step12.py:60:from src.phase_z2_ai_fallback.signature import bucket_char_count, build_signature
src/phase_z2_ai_fallback/step12.py:99: "cache_key": str | None, # IMP-46 u4
src/phase_z2_ai_fallback/step12.py:100: "fingerprints": dict | None, # IMP-46 u4
src/phase_z2_ai_fallback/step12.py:103: ``cache_key`` and ``fingerprints`` are populated only when the unit
src/phase_z2_ai_fallback/step12.py:133: "cache_key": None,
src/phase_z2_ai_fallback/step12.py:134: "fingerprints": None,
src/phase_z2_ai_fallback/step12.py:168: signature_hash = build_signature(
src/phase_z2_ai_fallback/step12.py:178: cache_key = f"{frame_id_value}::{signature_hash}"
src/phase_z2_ai_fallback/step12.py:179: fingerprints = {
src/phase_z2_ai_fallback/step12.py:184: record["cache_key"] = cache_key
src/phase_z2_ai_fallback/step12.py:185: record["fingerprints"] = fingerprints
src/phase_z2_ai_fallback/step12.py:196: cache_key=cache_key,
succeeded in 181ms:
_AI_ADAPTATION_ROUTE = "ai_adaptation_required"
def _sha256_of(payload: Any) -> str:
"""Deterministic SHA256 hex digest over a JSON-serialisable payload."""
encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def gather_step12_ai_repair_proposals(
units: Iterable[Any],
*,
route_for_label: Callable[[str | None], str | None],
get_contract_fn: Callable[[str], dict | None],
frame_visual_loader: Callable[[str], str],
figma_partial_loader: Callable[[str], dict] | None = None,
internal_region_lookup: Callable[[Any], dict] | None = None,
mdx_text_loader: Callable[[Any], str] | None = None,
catalog_sha_loader: Callable[[], str] | None = None,
) -> list[dict]:
"""Return one record per unit describing the Step 12 AI repair decision.
The record schema is stable across all gate decisions so the Step 12
artifact consumer can rely on a single shape:
{
"unit_index": int,
"source_section_ids": list[str],
"frame_template_id": str,
"label": str | None,
"route_hint": str | None,
"provisional": bool,
"ai_called": bool,
"skip_reason": str | None,
"proposal": dict | None,
"error": str | None,
"cache_key": str | None, # IMP-46 u4
"fingerprints": dict | None, # IMP-46 u4
}
``cache_key`` and ``fingerprints`` are populated only when the unit
reaches the AI-eligible code path (provisional + ai_adaptation route).
Skipped units retain ``None`` for both ??the structural axes
(layout_preset / zone_position / source_shape / h3_count / char_count)
are not guaranteed to be set for non-AI paths.
``ai_called`` is True only when ``route_ai_fallback`` was invoked AND
returned a proposal OR raised. Flag-off / route-mismatch returns
``None`` from the router and is surfaced as ``ai_called=False`` with
``skip_reason="router_short_circuit"`` so the caller can distinguish
"router decided not to run" from "router ran and returned a proposal".
"""
records: list[dict] = []
catalog_sha = (
catalog_sha_loader() if catalog_sha_loader is not None else ""
)
for index, unit in enumerate(units):
label = getattr(unit, "label", None)
route_hint = route_for_label(label)
record: dict = {
"unit_index": index,
"source_section_ids": list(getattr(unit, "source_section_ids", []) or []),
"frame_template_id": getattr(unit, "frame_template_id", None),
"label": label,
"route_hint": route_hint,
"provisional": bool(getattr(unit, "provisional", False)),
"ai_called": False,
"skip_reason": None,
"proposal": None,
"error": None,
"cache_key": None,
"fingerprints": None,
}
if not record["provisional"]:
record["skip_reason"] = "not_provisional"
records.append(record)
continue
if route_hint != _AI_ADAPTATION_ROUTE:
record["skip_reason"] = f"route_not_ai_adaptation:{route_hint}"
records.append(record)
continue
template_id = record["frame_template_id"] or ""
frame_contract = get_contract_fn(template_id) or {}
frame_visual_html = frame_visual_loader(template_id)
figma_partial_json = (
figma_partial_loader(template_id) if figma_partial_loader is not None else {}
)
internal_region = (
internal_region_lookup(unit) if internal_region_lookup is not None else {}
)
mdx_text = (
mdx_text_loader(unit)
if mdx_text_loader is not None
else (getattr(unit, "raw_content", "") or "")
)
frame_id_value = getattr(unit, "frame_id", "") or ""
cardinality = getattr(unit, "cardinality", None)
layout_preset = getattr(unit, "layout_preset", "") or ""
zone_position = getattr(unit, "zone_position", "") or ""
source_shape = getattr(unit, "source_shape", "paragraph") or "paragraph"
h3_count = int(getattr(unit, "h3_count", 0) or 0)
char_count = int(getattr(unit, "char_count", 0) or 0)
char_count_bucket = bucket_char_count(char_count)
signature_hash = build_signature(
frame_id=frame_id_value,
v4_label=label or "",
cardinality=cardinality,
source_shape=source_shape,
h3_count=h3_count,
char_count_bucket=char_count_bucket,
layout_preset=layout_preset,
zone_position=zone_position,
)
cache_key = f"{frame_id_value}::{signature_hash}"
fingerprints = {
"contract_sha": _sha256_of(frame_contract),
succeeded in 229ms:
Composes the IMP-33 fallback flow:
1. flag gate (``settings.ai_fallback_enabled`` default OFF)
2. V4 route gate (route must equal ``ai_adaptation_required``)
3. cache read (u6 stub returns ``None`` until IMP-46 lands)
4. build prompt (u3)
5. call client (u4 ``request_proposal``)
6. validate (u5 ``validate_proposal``)
Returns the validated ``AiFallbackProposal``. Save to cache is NOT
performed here ??it is caller-driven AFTER ``visual_check_passed=True``
AND ``user_approved=True``, per the u6 IMP-46 gate. The router does not
import ``save_proposal``; this is the structural guarantee that the
router cannot persist a proposal before the caller's visual + user
checks (`feedback_artifact_status_naming`).
Guardrails:
* PZ-1 ??normal-path AI call count stays 0: flag-off OR route-mismatch
short-circuits BEFORE the prompt builder or client are touched.
* ``feedback_ai_isolation_contract`` ??MDX READ-ONLY (u3 enforces in
prompt; this module never reads or writes MDX).
* ``feedback_phase_z_spacing_direction`` ??V4 rank-1 protected (u5
enforces; router only forwards the contract).
"""
from __future__ import annotations
from typing import Any
from src.config import settings
from src.phase_z2_ai_fallback.cache import read_proposal
from src.phase_z2_ai_fallback.client import AiFallbackClient
from src.phase_z2_ai_fallback.prompts import (
V4_ROUTE_AI_ADAPTATION,
build_ai_fallback_prompt,
)
from src.phase_z2_ai_fallback.schema import AiFallbackProposal
from src.phase_z2_ai_fallback.validate import validate_proposal
def route_ai_fallback(
*,
cache_key: str,
v4_result: dict[str, Any],
frame_contract: dict[str, Any],
frame_visual_html: str,
figma_partial_json: dict[str, Any],
internal_region: dict[str, Any],
mdx_text: str,
client: AiFallbackClient | None = None,
) -> AiFallbackProposal | None:
"""Route a fallback request through cache ??prompt ??client ??validate.
Returns ``None`` when the master flag is OFF or when the V4 route is
not ``ai_adaptation_required`` ??both gates short-circuit BEFORE any
prompt/client work, so the normal-path AI call count stays at 0
(PZ-1).
"""
if not settings.ai_fallback_enabled:
return None
route = v4_result.get("route") or v4_result.get("imp05_route_hint")
if route != V4_ROUTE_AI_ADAPTATION:
return None
cached = read_proposal(cache_key)
if cached is not None:
validate_proposal(
cached,
frame_contract=frame_contract,
internal_region=internal_region,
)
return cached
prompt = build_ai_fallback_prompt(
v4_result=v4_result,
frame_contract=frame_contract,
frame_visual_html=frame_visual_html,
figma_partial_json=figma_partial_json,
internal_region=internal_region,
mdx_text=mdx_text,
)
active_client = client if client is not None else AiFallbackClient()
proposal = active_client.request_proposal(prompt)
validate_proposal(
proposal,
frame_contract=frame_contract,
internal_region=internal_region,
)
return proposal
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\step12.py' | Select-Object -Skip 150 -First 80" in D:\ad-hoc\kei\design_agent
succeeded in 167ms:
internal_region = (
internal_region_lookup(unit) if internal_region_lookup is not None else {}
)
mdx_text = (
mdx_text_loader(unit)
if mdx_text_loader is not None
else (getattr(unit, "raw_content", "") or "")
)
frame_id_value = getattr(unit, "frame_id", "") or ""
cardinality = getattr(unit, "cardinality", None)
layout_preset = getattr(unit, "layout_preset", "") or ""
zone_position = getattr(unit, "zone_position", "") or ""
source_shape = getattr(unit, "source_shape", "paragraph") or "paragraph"
h3_count = int(getattr(unit, "h3_count", 0) or 0)
char_count = int(getattr(unit, "char_count", 0) or 0)
char_count_bucket = bucket_char_count(char_count)
signature_hash = build_signature(
frame_id=frame_id_value,
v4_label=label or "",
cardinality=cardinality,
source_shape=source_shape,
h3_count=h3_count,
char_count_bucket=char_count_bucket,
layout_preset=layout_preset,
zone_position=zone_position,
)
cache_key = f"{frame_id_value}::{signature_hash}"
fingerprints = {
"contract_sha": _sha256_of(frame_contract),
"partial_sha": _sha256_of(figma_partial_json),
"catalog_sha": catalog_sha,
}
record["cache_key"] = cache_key
record["fingerprints"] = fingerprints
v4_result = {
"route": route_hint,
"label": label,
"frame_id": getattr(unit, "frame_id", None),
"rank": getattr(unit, "v4_rank", None),
"cardinality": cardinality,
}
try:
proposal = route_ai_fallback(
cache_key=cache_key,
v4_result=v4_result,
frame_contract=frame_contract,
frame_visual_html=frame_visual_html,
figma_partial_json=figma_partial_json,
internal_region=internal_region,
mdx_text=mdx_text,
)
except Exception as exc: # noqa: BLE001 ??record + continue, no AI re-raise
record["ai_called"] = True
record["error"] = f"{type(exc).__name__}: {exc}"
records.append(record)
continue
if proposal is None:
record["skip_reason"] = "router_short_circuit"
records.append(record)
continue
record["ai_called"] = True
record["proposal"] = proposal.model_dump()
records.append(record)
return records
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "rg -n \"overflow\\[|overflow\\.get\\(\\\"passed\\\"|\\\"passed\\\"\" src/phase_z2_pipeline.py src/phase_z2_visual_check.py src -g\"*.py\"" in D:\ad-hoc\kei\design_agent
2026-05-21T12:52:25.776513Z ERROR codex_core::tools::router: error=Exit code: 1
Wall time: 0.1 seconds
Output:
rg: regex parse error:
(?:overflow\[|overflow\.get\(\)
^
error: unclosed group
exited 1 in 99ms:
rg: regex parse error:
(?:overflow\[|overflow\.get\(\)
^
error: unclosed group
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "rg -n 'overflow\\.get\\(\"passed\"|\"passed\"' src/phase_z2_pipeline.py src -g '*.py'" in D:\ad-hoc\kei\design_agent
succeeded in 95ms:
src/phase_z2_pipeline.py:712: visual_check_passed: bool,
src/phase_z2_pipeline.py:727: cache.py contract). ``visual_check_passed`` is never bypassable;
src/phase_z2_pipeline.py:758: visual_check_passed=visual_check_passed,
src/phase_z2_pipeline.py:876: - ``max_rank`` explicitly passed → caller_override: that value is used
src/phase_z2_pipeline.py:2234: retry_passed : bool
src/phase_z2_pipeline.py:2237: post_retry_overflow : dict (retry_passed=True 일 때만)
src/phase_z2_pipeline.py:2238: post_retry_debug_zones : list (retry_passed=True 일 때만 — height_px 갱신본)
src/phase_z2_pipeline.py:2239: post_retry_layout_css : dict (retry_passed=True 일 때만)
src/phase_z2_pipeline.py:2246: "retry_passed": False,
src/phase_z2_pipeline.py:2259: base_trace["retry_skipped_reason"] = "router_active=False (visual check passed — no overflow)"
src/phase_z2_pipeline.py:2332: if candidate_overflow.get("passed", False):
src/phase_z2_pipeline.py:2346: base_trace["retry_passed"] = True
src/phase_z2_pipeline.py:2354: base_trace["retry_passed"] = False
src/phase_z2_pipeline.py:2360: "passed": False,
src/phase_z2_pipeline.py:2387: trace = {"salvage_attempted": False, "salvage_passed": False, "salvage_steps": []}
src/phase_z2_pipeline.py:2426: candidate_html, candidate_overflow, passed = None, None, False
src/phase_z2_pipeline.py:2433: passed = bool(candidate_overflow.get("passed", False))
src/phase_z2_pipeline.py:2434: step = {"action": next_action, "plan": plan, "passed": passed,
src/phase_z2_pipeline.py:2437: if passed:
src/phase_z2_pipeline.py:2441: trace["salvage_passed"] = True
src/phase_z2_pipeline.py:2529: return {"passed": False, "error": f"selenium init failed: {last_err or e}"}
src/phase_z2_pipeline.py:2760: return {"passed": False, **result}
src/phase_z2_pipeline.py:2827: result["passed"] = len(fail_reasons) == 0
src/phase_z2_pipeline.py:2856: - visual_check_passed : Selenium per-zone overflow / clipping 통과 여부
src/phase_z2_pipeline.py:2879: visual_passed = bool(overflow.get("passed", False))
src/phase_z2_pipeline.py:2967: if full_coverage and visual_passed:
src/phase_z2_pipeline.py:2969: elif full_coverage and not visual_passed:
src/phase_z2_pipeline.py:2971: elif not full_coverage and visual_passed:
src/phase_z2_pipeline.py:3008: "visual_check_passed": visual_passed,
src/phase_z2_pipeline.py:5340: # check + per IMP-46 dual-gate. ``visual_check_passed`` reads the
src/phase_z2_pipeline.py:5351: visual_check_passed=bool(overflow.get("passed")),
src/phase_z2_pipeline.py:5411: if retry_trace.get("retry_passed"):
src/phase_z2_pipeline.py:5475: if _salvage_trace.get("salvage_passed"):
src/phase_z2_pipeline.py:5487: # state. classify_retry_failure short-circuits on salvage_passed=True → both
src/phase_z2_pipeline.py:5496: "done" if retry_trace.get("retry_passed") or retry_trace.get("salvage_passed")
src/phase_z2_pipeline.py:5497: else "failed" if retry_trace.get("retry_attempted") and not retry_trace.get("retry_passed")
src/phase_z2_pipeline.py:5582: f'<tr><th>visual_check_passed</th><td>{slide_status.get("visual_check_passed")}</td></tr>'
src/phase_z2_pipeline.py:5650: if not slide_status["visual_check_passed"]:
src/phase_z2_pipeline.py:5654: if not fit_classification["visual_check_passed"]:
src/phase_z2_pipeline.py:5668: print(f" retry_passed = {retry_trace['retry_passed']}")
src/phase_z2_pipeline.py:5669: if not retry_trace["retry_passed"]:
src/phase_z2_pipeline.py:5680: if not slide_status["visual_check_passed"]:
src/phase_z2_pipeline.py:5761: # bypasses the ``user_approved`` gate only (``visual_check_passed``
src/phase_z2_pipeline.py:5773: "(visual_check_passed remains mandatory). Sets "
src\config.py:30: # `user_approved` gate only (`visual_check_passed` is never bypassed).
src\content_verifier.py:31: passed: bool
src\content_verifier.py:222: passed=True, area_name=area_name,
src\content_verifier.py:257: passed = score >= threshold
src\content_verifier.py:260: if not passed:
src\content_verifier.py:266: passed=passed,
src\content_verifier.py:268: checks={"text_preservation": passed},
src\content_verifier.py:365: passed = len(found) == 0
src\content_verifier.py:367: passed=passed,
src\content_verifier.py:369: checks={"no_forbidden": passed},
src\content_verifier.py:370: score=1.0 if passed else 0.0,
src\content_verifier.py:438: passed = len(missing) == 0
src\content_verifier.py:442: passed=passed,
src\content_verifier.py:444: checks={"structure": passed},
src\content_verifier.py:445: score=1.0 if passed else (1.0 - len(missing) / max(1, len(patterns))),
src\content_verifier.py:468: all_passed = all(r.passed for r in results)
src\content_verifier.py:481: passed=all_passed,
src\content_verifier.py:535: status = "PASS" if r.passed else "FAIL"
src\content_verifier.py:631: failed_areas = {name: r for name, r in verification.items() if not r.passed}
src\phase_z2_placement_planner.py:324: print("[OK] Test 1 (text-only → F13 → pillar_1) passed.")
src\phase_z2_placement_planner.py:388: print("[OK] Test 2 (text+transform → F29 → product_column / process_column) passed.")
src\phase_z2_failure_router.py:82:# retry_trace["salvage_steps"] with {action, passed, failure_reason}. classifier
src\phase_z2_failure_router.py:159: None : retry 가 *성공* 한 case (retry_passed=True). 분류할 failure 없음.
src\phase_z2_failure_router.py:163: if retry_trace.get("retry_passed"):
src\phase_z2_failure_router.py:167: if retry_trace.get("salvage_passed"):
src\phase_z2_failure_router.py:176: if not last.get("passed"):
src\phase_z2_failure_router.py:185: f"AND passed=False. raw failure_reason: {reason!r}"
src\phase_z2_failure_router.py:240: if retry_trace.get("rerender_attempted") and not retry_trace.get("retry_passed"):
src\phase_z2_failure_router.py:244: "plan.feasible=True AND rerender_attempted=True AND retry_passed=False"
src\phase_z2_failure_router.py:299: retry_passed=True 인 경우 → 두 필드 모두 None (failure 없음, escalation 없음).
src\phase_z2_content_extractor.py:484: print("[OK] Test 1 (text_block) passed.")
src\phase_z2_content_extractor.py:521: print("[OK] Test 2 (transform_table + text_block) passed.")
src\phase_z2_content_extractor.py:551: print("[OK] Rich Test 1 (popup → details) passed.")
src\phase_z2_content_extractor.py:567: print("[OK] Rich Test 2 (image) passed.")
src\phase_z2_content_extractor.py:584: print("[OK] Rich Test 3 (non-transform table) passed.")
src\phase_z2_content_extractor.py:597: print("[OK] Rich Test 4 (arrow table → skip) passed.")
src\phase_z2_content_extractor.py:604: print("[OK] Rich Test 5 (empty) passed.")
src\phase_z2_pipeline.py:712: visual_check_passed: bool,
src\phase_z2_pipeline.py:727: cache.py contract). ``visual_check_passed`` is never bypassable;
src\phase_z2_pipeline.py:758: visual_check_passed=visual_check_passed,
src\phase_z2_pipeline.py:876: - ``max_rank`` explicitly passed → caller_override: that value is used
src\phase_z2_pipeline.py:2234: retry_passed : bool
src\phase_z2_pipeline.py:2237: post_retry_overflow : dict (retry_passed=True 일 때만)
src\phase_z2_pipeline.py:2238: post_retry_debug_zones : list (retry_passed=True 일 때만 — height_px 갱신본)
src\phase_z2_pipeline.py:2239: post_retry_layout_css : dict (retry_passed=True 일 때만)
src\phase_z2_pipeline.py:2246: "retry_passed": False,
src\phase_z2_pipeline.py:2259: base_trace["retry_skipped_reason"] = "router_active=False (visual check passed — no overflow)"
src\phase_z2_pipeline.py:2332: if candidate_overflow.get("passed", False):
src\phase_z2_pipeline.py:2346: base_trace["retry_passed"] = True
src\phase_z2_pipeline.py:2354: base_trace["retry_passed"] = False
src\phase_z2_pipeline.py:2360: "passed": False,
src\phase_z2_pipeline.py:2387: trace = {"salvage_attempted": False, "salvage_passed": False, "salvage_steps": []}
src\phase_z2_pipeline.py:2426: candidate_html, candidate_overflow, passed = None, None, False
src\phase_z2_pipeline.py:2433: passed = bool(candidate_overflow.get("passed", False))
src\phase_z2_pipeline.py:2434: step = {"action": next_action, "plan": plan, "passed": passed,
src\phase_z2_pipeline.py:2437: if passed:
src\phase_z2_pipeline.py:2441: trace["salvage_passed"] = True
src\phase_z2_pipeline.py:2529: return {"passed": False, "error": f"selenium init failed: {last_err or e}"}
src\phase_z2_pipeline.py:2760: return {"passed": False, **result}
src\phase_z2_pipeline.py:2827: result["passed"] = len(fail_reasons) == 0
src\phase_z2_pipeline.py:2856: - visual_check_passed : Selenium per-zone overflow / clipping 통과 여부
src\phase_z2_pipeline.py:2879: visual_passed = bool(overflow.get("passed", False))
src\phase_z2_pipeline.py:2967: if full_coverage and visual_passed:
src\phase_z2_pipeline.py:2969: elif full_coverage and not visual_passed:
src\phase_z2_pipeline.py:2971: elif not full_coverage and visual_passed:
src\phase_z2_pipeline.py:3008: "visual_check_passed": visual_passed,
src\phase_z2_pipeline.py:5340: # check + per IMP-46 dual-gate. ``visual_check_passed`` reads the
src\phase_z2_pipeline.py:5351: visual_check_passed=bool(overflow.get("passed")),
src\phase_z2_pipeline.py:5411: if retry_trace.get("retry_passed"):
src\phase_z2_pipeline.py:5475: if _salvage_trace.get("salvage_passed"):
src\phase_z2_pipeline.py:5487: # state. classify_retry_failure short-circuits on salvage_passed=True → both
src\phase_z2_pipeline.py:5496: "done" if retry_trace.get("retry_passed") or retry_trace.get("salvage_passed")
src\phase_z2_pipeline.py:5497: else "failed" if retry_trace.get("retry_attempted") and not retry_trace.get("retry_passed")
src\phase_z2_pipeline.py:5582: f'<tr><th>visual_check_passed</th><td>{slide_status.get("visual_check_passed")}</td></tr>'
src\phase_z2_pipeline.py:5650: if not slide_status["visual_check_passed"]:
src\phase_z2_pipeline.py:5654: if not fit_classification["visual_check_passed"]:
src\phase_z2_pipeline.py:5668: print(f" retry_passed = {retry_trace['retry_passed']}")
src\phase_z2_pipeline.py:5669: if not retry_trace["retry_passed"]:
src\phase_z2_pipeline.py:5680: if not slide_status["visual_check_passed"]:
src\phase_z2_pipeline.py:5761: # bypasses the ``user_approved`` gate only (``visual_check_passed``
src\phase_z2_pipeline.py:5773: "(visual_check_passed remains mandatory). Sets "
src\phase_z2_internal_region_planner.py:368: print("[OK] Test 1 (text-only) passed.")
src\phase_z2_internal_region_planner.py:436: print("[OK] Test 2 (text+transform, vertical-stack, ratio 6:3) passed.")
src\phase_z2_classifier.py:342: overflow : run_overflow_check 결과 (passed, slide, zones[], ...)
src\phase_z2_classifier.py:347: visual_check_passed : Selenium 통과 여부 (overflow.passed AND no classifications)
src\phase_z2_classifier.py:354: count) — passed 여부 무관 항상 surface
src\phase_z2_classifier.py:363: # passed 여부 무관 항상 빌드 (B4 vs mapper divergence 가 passed 에서도 진단 가치).
src\phase_z2_classifier.py:373: # IMP-15 실행-3 (issue #47): no early-return on overflow.passed=True.
src\phase_z2_classifier.py:375: # visual_check_passed is widened to: overflow.passed AND no classifications.
src\phase_z2_classifier.py:493: # IMP-15 실행-3 (issue #47): widened semantic — overflow.passed alone is not
src\phase_z2_classifier.py:494: # enough; any image/table classification also flips visual_check_passed.
src\phase_z2_classifier.py:495: visual_check_passed = bool(overflow.get("passed", False)) and not classifications
src\phase_z2_classifier.py:497: "visual_check_passed": visual_check_passed,
src\kei_client.py:565: "passed": true/false,
src\kei_client.py:583: {"passed": bool, "score": int, "issues": [...], "fix_targets": [...]}
src\kei_client.py:628: passed = result.get("passed", score >= 50)
src\kei_client.py:629: result["passed"] = passed
src\kei_client.py:631: f"[Q-6] 품질 게이트: {score}/100 → {'PASS' if passed else 'FAIL'}"
src\phase_z2_verification_utils.py:28: passed: bool
src\phase_z2_verification_utils.py:252: sentence delegates to u7. Empty sentence list -> passed True,
src\phase_z2_verification_utils.py:258: return VerificationResult(passed=True, area_name=area_name,
src\phase_z2_verification_utils.py:270: passed = score >= threshold
src\phase_z2_verification_utils.py:272: if not passed:
src\phase_z2_verification_utils.py:282: passed=passed, area_name=area_name,
src\phase_z2_verification_utils.py:283: checks={"text_preservation": passed}, score=score,
src\phase_z2_retry.py:23: 7. debug trace 필수 (retry_attempted / target / donor / before/after / passed / reason)
src\phase_z2_router.py:12: - 실행 안 됨 → 현재 코드는 여전히 visual_check_passed=False 시 sys.exit(1)
src\phase_z2_router.py:120: router_active : True/False (visual_check_passed=False 일 때만 True)
src\phase_z2_router.py:128: if fit_classification.get("visual_check_passed", True):
src\phase_z2_router.py:136: "note": "visual check passed — no overflow to route",
src\pipeline.py:1689: area: {"passed": r.passed, "score": r.score, "errors": r.errors}
src\pipeline.py:1741: if not quality_result.get("passed", True):
src\phase_z2_ai_fallback\cache.py:48:* Both write gates preserved — ``visual_check_passed=False`` always
src\phase_z2_ai_fallback\cache.py:52: (``visual_check_passed`` is never bypassed). Gate violation never
src\phase_z2_ai_fallback\cache.py:65: ``user_approved`` gate; ``visual_check_passed`` remains mandatory.
src\phase_z2_ai_fallback\cache.py:69:* The truth table over ``(visual_check_passed, user_approved, auto_cache)``
src\phase_z2_ai_fallback\cache.py:162: visual_check_passed: bool,
src\phase_z2_ai_fallback\cache.py:172: * ``visual_check_passed=False`` -> :class:`AiFallbackCacheGateError`
src\phase_z2_ai_fallback\cache.py:178: * Otherwise (``visual_check_passed=True`` AND either
src\phase_z2_ai_fallback\cache.py:207: if not visual_check_passed:
src\phase_z2_ai_fallback\cache.py:209: "IMP-46 gate: visual_check_passed=False; refusing to cache an "
src\phase_z2_ai_fallback\router.py:13:performed here — it is caller-driven AFTER ``visual_check_passed=True``
src\phase_z2_ai_fallback\step12.py:23:``visual_check_passed=True`` AND ``user_approved=True`` (u6 IMP-46 gate).
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command 'rg -n "ai_fallback_auto_cache|ai_fallback_enabled" src/config.py src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py' in D:\ad-hoc\kei\design_agent
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\config.py' | Select-Object -First 60" in D:\ad-hoc\kei\design_agent
succeeded in 99ms:
src/config.py:19: ai_fallback_enabled: bool = False
src/config.py:35: ai_fallback_auto_cache: bool = False
tests/test_imp47b_cache_save_gate.py:23: * Settings axis — ``settings.ai_fallback_auto_cache`` sourced through
src/phase_z2_pipeline.py:82:# router when settings.ai_fallback_enabled is False (default), so import
src/phase_z2_pipeline.py:581:# declared slots. Activation still gated by ai_fallback_enabled (default OFF).
src/phase_z2_pipeline.py:618: at the router (``settings.ai_fallback_enabled=False`` default).
src/phase_z2_pipeline.py:5342: # (CLI --auto-cache wires settings.ai_fallback_auto_cache at parse
src/phase_z2_pipeline.py:5353: auto_cache=bool(_ai_cache_settings.ai_fallback_auto_cache),
src/phase_z2_pipeline.py:5763: # ``settings.ai_fallback_auto_cache`` (src/config.py); this flag
src/phase_z2_pipeline.py:5774: "settings.ai_fallback_auto_cache=True for this run."
src/phase_z2_pipeline.py:5781: _settings.ai_fallback_auto_cache = True
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\cache.py' | Select-Object -First 90" in D:\ad-hoc\kei\design_agent
succeeded in 239ms:
from __future__ import annotations
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Design Agent ?ㅼ젙."""
anthropic_api_key: str = ""
kei_api_url: str = "http://localhost:8000"
log_level: str = "DEBUG"
# ?щ씪?대뱶 ?ш린
slide_width: int = 1280
slide_height: int = 720
# IMP-33 u1 ??AI fallback policy. Fallback-path only; normal path AI=0.
# Defaults locked by Stage 2 plan; do NOT inline literals downstream.
ai_fallback_enabled: bool = False
ai_fallback_model: str = "claude-opus-4-6-20250415"
ai_fallback_timeout_s: float = 60.0
ai_fallback_max_retries: int = 3
ai_fallback_backoff_base_s: float = 1.0
ai_fallback_backoff_cap_s: float = 8.0
ai_fallback_backoff_jitter: float = 0.3
ai_fallback_budget_per_run: int = 10
ai_fallback_circuit_breaker_threshold: int = 5
# IMP-46 u5 ??auto-cache flag. When True, `save_proposal` bypasses the
# `user_approved` gate only (`visual_check_passed` is never bypassed).
# Default OFF preserves the dual-gate contract; the CLI flag
# `--auto-cache` in `src/phase_z2_pipeline.py` mutates this setting at
# parse time. Downstream callers MUST source the flag from Settings,
# never inline literals.
ai_fallback_auto_cache: bool = False
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()
succeeded in 182ms:
"""IMP-46 u2 + u3 + u5 ??Persistent JSON cache backend for AI fallback proposals.
Replaces the IMP-33 u6 ``NotImplementedError`` stub with a content-addressed
store at ``data/frame_cache/{frame_id}/{signature_hash}.json``.
Key format:
* ``read_proposal(key)`` / ``save_proposal(key, ...)`` accept a string ``key``
of the form ``"{frame_id}::{signature_hash}"``. The two components are
parsed inside this module so that upstream callers (router, step 12)
remain unaware of the on-disk layout.
* ``read_proposal`` on a malformed (legacy) key silently returns ``None``
??the IMP-33 u7 router currently passes a legacy ``cache_key`` string,
and u4 will switch to the structural form. Until then, all such reads
must miss safely (no exception, no false hit).
* ``save_proposal`` on a malformed key raises ``ValueError`` (loud, never
silent) ??writes are gated and must use the structural form.
Stored payload (one JSON file per (frame_id, signature_hash) pair):
{
"schema_version": 1,
"proposal": <AiFallbackProposal.model_dump(mode="json")>,
"slide_css": <str | null>,
"fingerprints": {"contract_sha": ..., "partial_sha": ..., "catalog_sha": ...}
}
u3 invalidation contract (this module is a *comparator*, not a *computer*):
* ``save_proposal`` persists the ``fingerprints`` dict supplied by the
caller verbatim. Cache.py never computes any fingerprint ??the three
declared shas (``contract_sha`` / ``partial_sha`` / ``catalog_sha``) are
computed by callers from the live contract YAML / partial templates /
catalog payloads and handed in. Keeping the computation out of cache.py
preserves AI isolation (no Phase Z runtime knowledge in the cache
module) and keeps the cache schema-agnostic ??additional fingerprint
axes can be added without editing cache.py.
* ``read_proposal`` accepts an optional ``fingerprints`` kwarg. When
supplied, the stored ``fingerprints`` dict must equal the caller's dict
exactly (strict equality, NOT subset). Any mismatch ??including a key
the caller demands but the stored entry lacks, OR a key the stored
entry has but the caller does not pass ??returns ``None``. Default
``fingerprints=None`` performs no comparison (back-compat for legacy
callers that have not yet adopted fingerprint-aware lookup).
Guardrails (locked by Stage 2 plan):
* Both write gates preserved ??``visual_check_passed=False`` always
raises ``AiFallbackCacheGateError`` BEFORE any filesystem touch.
``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.
* Missing or corrupt files cause ``read_proposal`` to return ``None`` ??
the cache is a hint, never a hard dependency. Errors are not propagated
to callers because the AI fallback path can always recompute.
* ``mkdir(parents=True, exist_ok=True)`` is performed lazily on save.
* No Anthropic / MDX / Phase Z runtime imports (AI isolation contract).
* Cache root is held as a module-level :data:`CACHE_ROOT` so tests can
redirect writes via ``monkeypatch.setattr`` without subclassing.
u5 auto-cache contract (CLI ``--auto-cache`` + ``settings.ai_fallback_auto_cache``):
* ``save_proposal(..., auto_cache=True)`` only bypasses the
``user_approved`` gate; ``visual_check_passed`` remains mandatory.
* ``auto_cache`` is keyword-only and defaults to ``False`` ??existing
callers (and the test suite) see the original dual-gate behaviour
unless they opt in explicitly.
* The truth table over ``(visual_check_passed, user_approved, auto_cache)``
has eight cells; exactly three succeed:
``(True, True, False)``, ``(True, True, True)``, and
``(True, False, True)``. Every other cell raises
``AiFallbackCacheGateError``.
"""
from __future__ import annotations
import json
import pathlib
from src.phase_z2_ai_fallback.schema import AiFallbackProposal
SCHEMA_VERSION = 1
KEY_DELIMITER = "::"
CACHE_ROOT: pathlib.Path = pathlib.Path("data/frame_cache")
class AiFallbackCacheGateError(RuntimeError):
"""Raised when ``save_proposal`` is called without both IMP-46 gates True."""
codex
u13 looks scoped and correct: the helper persists only applied proposals, forwards the IMP-46 gates, and the focused six-case test passes. Im posting the required Codex verification comment and mirroring it to the orchestrator draft path.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "@'
[Codex #22] Stage 3 code-edit verification - IMP-47B u13
Verified only the single executed unit u13 against the Stage 2 contract.
Result: u13 is correct and scoped.
Findings:
- "'`src/phase_z2_pipeline.py` adds `_persist_ai_repair_proposals_to_cache(...)`, which mutates each Step 12 AI repair record with `cache_save_status` and only sends records with `apply_status.startswith("applied:")`, a serialized proposal dict, a non-empty `cache_key`, and a fingerprints dict to `cache.save_proposal(...)`.
- The helper reconstructs `AiFallbackProposal` before saving, surfaces invalid proposal payloads as `invalid_proposal:<ExcType>`, and surfaces IMP-46 gate blocks as `gate_blocked:<reason>` without raising into pipeline runtime. Non-applied records are marked `not_applied` and do not touch the filesystem.
- The call site runs after Step 14 `run_overflow_check(...)` and forwards `visual_check_passed=bool(overflow.get("passed"))`, `user_approved=False`, and `auto_cache=bool(settings.ai_fallback_auto_cache)`. This preserves the visual gate and sources the auto-cache bypass from Settings instead of inline policy.
- `src/phase_z2_ai_fallback/cache.py` confirms the save gate truth table: visual failure always raises `AiFallbackCacheGateError`; user approval is bypassed only when `auto_cache=True`; successful cells persist under `data/frame_cache/{frame_id}/{signature_hash}.json`.
- `tests/test_imp47b_cache_save_gate.py` covers the u13 helper'"'s gate behavior: visual-fail block, user-not-approved/no-auto-cache block, auto-cache happy path persistence, non-applied skip behavior, mixed batch behavior, and invalid proposal payload surfacing.
- I did not run full pytest, per the current-stage instruction not to run broad test suites unless explicitly required.
=== EVIDENCE ===
Commands run:
- "'`git status --short`
- `git diff -- src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py`
- `rg -n "_persist_ai_repair_proposals_to_cache|cache_save_status|save_proposal|AiFallbackCacheGateError|visual_check_passed|user_approved|auto_cache" src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py src/phase_z2_ai_fallback/cache.py`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_ai_fallback\\\\cache.py' | Select-Object -Skip 150 -First 90"'`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_pipeline.py' | Select-Object -Skip 680 -First 100"'`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_pipeline.py' | Select-Object -Skip 5310 -First 60"'`
- `Get-Content -LiteralPath '"'tests\\\\test_imp47b_cache_save_gate.py' | Select-Object -First 260"'`
- `pytest -q tests/test_imp47b_cache_save_gate.py`
- `rg -n "cache_key|fingerprints|make_cache_key|signature" src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/signature.py tests/test_imp47b_step12_ai_wiring.py tests/test_imp47b_cache_save_gate.py`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_ai_fallback\\\\step12.py' | Select-Object -Skip 60 -First 120"'`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_ai_fallback\\\\router.py' | Select-Object -Skip 1 -First 130"'`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_ai_fallback\\\\step12.py' | Select-Object -Skip 150 -First 80"'`
- `rg -n '"'overflow\\\\.get\\\\(\\\"passed\\\"|\\\"passed\\\"' src/phase_z2_pipeline.py src -g '*.py'"'`
- `rg -n "ai_fallback_auto_cache|ai_fallback_enabled" src/config.py src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py`
- `Get-Content -LiteralPath '"'src\\\\config.py' | Select-Object -First 60"'`
- `Get-Content -LiteralPath '"'src\\\\phase_z2_ai_fallback\\\\cache.py' | Select-Object -First 90"'`
Files checked:
- `D:'"\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\phase_z2_pipeline.py"'`
- `D:'"\\\\ad-hoc\\\\kei\\\\design_agent\\\\tests\\\\test_imp47b_cache_save_gate.py"'`
- `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\\\\config.py"'`
Test results:
- `pytest -q tests/test_imp47b_cache_save_gate.py` -> `6 passed in 0.06s`
Remaining units: [u6, u7, u8, u9, u10]
rewind_target: continue_same
FINAL_CONSENSUS: NO
'"'@ | Set-Content -LiteralPath '.orchestrator\\drafts\\76_stage_code-edit_codex_r3.md' -Encoding UTF8
"'$body = Get-Content -LiteralPath '"'.orchestrator\\drafts\\76_stage_code-edit_codex_r3.md' -Raw
"'$payload = @{ body = $body } | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method Post -Uri '"'https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/76/comments' -Headers @{ Authorization = \"token "'$env:GITEA_TOKEN" } -ContentType '"'application/json; charset=utf-8' -Body "'$payload | ConvertTo-Json -Depth 5' in D:\ad-hoc\kei\design_agent
2026-05-21T12:53:09.618262Z ERROR codex_core::tools::router: error=Exit code: 1
Wall time: 0.4 seconds
Output:
Invoke-RestMethod : {"message":"[]: json: cannot unmarshal object into Go struct field CreateIssueCommentOption.Body of
type string","url":"https://gitea.hmac.kr/api/swagger"}
At line:53 char:1
+ Invoke-RestMethod -Method Post -Uri 'https://gitea.hmac.kr/api/v1/rep ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
exited 1 in 360ms:
Invoke-RestMethod : {"message":"[]: json: cannot unmarshal object into Go struct field CreateIssueCommentOption.Body of
type string","url":"https://gitea.hmac.kr/api/swagger"}
At line:53 char:1
+ Invoke-RestMethod -Method Post -Uri 'https://gitea.hmac.kr/api/v1/rep ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
codex
The draft was written, but the first Gitea POST encoded the body incorrectly for that API. Im retrying the POST with an explicit string payload; the local draft already contains the intended comment.
exec
"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command '$body = [System.IO.File]::ReadAllText('"'D:\\ad-hoc\\kei\\design_agent\\.orchestrator\\drafts\\76_stage_code-edit_codex_r3.md', [System.Text.Encoding]::UTF8)
"'$payload = @{ body = [string]$body } | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method Post -Uri '"'https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/76/comments' -Headers @{ Authorization = \"token "'$env:GITEA_TOKEN" } -ContentType '"'application/json; charset=utf-8' -Body "'$payload | ConvertTo-Json -Depth 5' in D:\ad-hoc\kei\design_agent
succeeded in 380ms:
{
"id": 23305,
"html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/76#issuecomment-23305",
"pull_request_url": "",
"issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/76",
"user": {
"id": 28,
"login": "Kyeongmin",
"login_name": "",
"source_id": 0,
"full_name": "이경민",
"email": "kyeongmin@noreply.localhost",
"avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e",
"html_url": "https://gitea.hmac.kr/Kyeongmin",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2025-12-30T10:51:04+09:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "",
"website": "",
"description": "",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 1,
"username": "Kyeongmin"
},
"original_author": "",
"original_author_id": 0,
"body": "[Codex #22] Stage 3 code-edit verification - IMP-47B u13\n\nVerified only the single executed unit u13 against the Stage 2 contract.\n\nResult: u13 is correct and scoped.\n\nFindings:\n- `src/phase_z2_pipeline.py` adds `_persist_ai_repair_proposals_to_cache(...)`, which mutates each Step 12 AI repair record with `cache_save_status` and only sends records with `apply_status.startswith(\"applied:\")`, a serialized proposal dict, a non-empty `cache_key`, and a fingerprints dict to `cache.save_proposal(...)`.\n- The helper reconstructs `AiFallbackProposal` before saving, surfaces invalid proposal payloads as `invalid_proposal:\u003cExcType\u003e`, and surfaces IMP-46 gate blocks as `gate_blocked:\u003creason\u003e` without raising into pipeline runtime. Non-applied records are marked `not_applied` and do not touch the filesystem.\n- The call site runs after Step 14 `run_overflow_check(...)` and forwards `visual_check_passed=bool(overflow.get(\"passed\"))`, `user_approved=False`, and `auto_cache=bool(settings.ai_fallback_auto_cache)`. This preserves the visual gate and sources the auto-cache bypass from Settings instead of inline policy.\n- `src/phase_z2_ai_fallback/cache.py` confirms the save gate truth table: visual failure always raises `AiFallbackCacheGateError`; user approval is bypassed only when `auto_cache=True`; successful cells persist under `data/frame_cache/{frame_id}/{signature_hash}.json`.\n- `tests/test_imp47b_cache_save_gate.py` covers the u13 helper\u0027s gate behavior: visual-fail block, user-not-approved/no-auto-cache block, auto-cache happy path persistence, non-applied skip behavior, mixed batch behavior, and invalid proposal payload surfacing.\n- I did not run full pytest, per the current-stage instruction not to run broad test suites unless explicitly required.\n\n=== EVIDENCE ===\nCommands run:\n- `git status --short`\n- `git diff -- src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py`\n- `rg -n \"_persist_ai_repair_proposals_to_cache|cache_save_status|save_proposal|AiFallbackCacheGateError|visual_check_passed|user_approved|auto_cache\" src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py src/phase_z2_ai_fallback/cache.py`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_ai_fallback\\\\cache.py\u0027 | Select-Object -Skip 150 -First 90`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_pipeline.py\u0027 | Select-Object -Skip 680 -First 100`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_pipeline.py\u0027 | Select-Object -Skip 5310 -First 60`\n- `Get-Content -LiteralPath \u0027tests\\\\test_imp47b_cache_save_gate.py\u0027 | Select-Object -First 260`\n- `pytest -q tests/test_imp47b_cache_save_gate.py`\n- `rg -n \"cache_key|fingerprints|make_cache_key|signature\" src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/router.py src/phase_z2_ai_fallback/signature.py tests/test_imp47b_step12_ai_wiring.py tests/test_imp47b_cache_save_gate.py`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_ai_fallback\\\\step12.py\u0027 | Select-Object -Skip 60 -First 120`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_ai_fallback\\\\router.py\u0027 | Select-Object -Skip 1 -First 130`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_ai_fallback\\\\step12.py\u0027 | Select-Object -Skip 150 -First 80`\n- `rg -n \u0027overflow\\\\.get\\\\(\\\"passed\\\"|\\\"passed\\\"\u0027 src/phase_z2_pipeline.py src -g \u0027*.py\u0027`\n- `rg -n \"ai_fallback_auto_cache|ai_fallback_enabled\" src/config.py src/phase_z2_pipeline.py tests/test_imp47b_cache_save_gate.py`\n- `Get-Content -LiteralPath \u0027src\\\\config.py\u0027 | Select-Object -First 60`\n- `Get-Content -LiteralPath \u0027src\\\\phase_z2_ai_fallback\\\\cache.py\u0027 | Select-Object -First 90`\n\nFiles checked:\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\phase_z2_pipeline.py`\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\tests\\\\test_imp47b_cache_save_gate.py`\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\phase_z2_ai_fallback\\\\cache.py`\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\phase_z2_ai_fallback\\\\step12.py`\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\phase_z2_ai_fallback\\\\router.py`\n- `D:\\\\ad-hoc\\\\kei\\\\design_agent\\\\src\\\\config.py`\n\nTest results:\n- `pytest -q tests/test_imp47b_cache_save_gate.py` -\u003e `6 passed in 0.06s`\n\nRemaining units: [u6, u7, u8, u9, u10]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO\r\n",
"assets": [
],
"created_at": "2026-05-21T21:53:18+09:00",
"updated_at": "2026-05-21T21:53:18+09:00"
}
codex
Verified IMP-47B u13, posted the Codex comment to Gitea, and mirrored the exact body to:
[76_stage_code-edit_codex_r3.md](D:/ad-hoc/kei/design_agent/.orchestrator/drafts/76_stage_code-edit_codex_r3.md)
Gitea comment: https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/76#issuecomment-23305
Focused test run: `pytest -q tests/test_imp47b_cache_save_gate.py` -> `6 passed in 0.06s`.
tokens used
83,989