3255 lines
180 KiB
Plaintext
3255 lines
180 KiB
Plaintext
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
|
||
I’ll read the orchestrator prompt first, then I’ll 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
|
||
I’ve got the binding prompt. The current task is Codex verification for Stage 3 code-edit on unit u13, so I’m 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. I’m now checking the cache gate contract directly and running the focused u13 test, because this unit’s 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. I’m 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. I’m 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. I’m 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
|