feedback_phase_z_spacing_direction: clipping 시 layout / frame 으로 풀 것 (공통 여백 줄이지 말 것)
feedback_factual_verification: missing_action 의 명확한 status reporting
🤖 Claude Opus 4.7 (P1 batch, 2026-05-22)
📌 SCOPE 확인 NOTE — Stage 1 reviewer (defensive)
#88 의 body 의 3 missing actions (layout_adjust / image_fit / frame_internal_fit_candidate) 는 처음부터 명확 — scope 변경 X. 단 실행 시점 에 #64 (popup escalation) + #65 (fit/rotation) 가 closed 된 상태 이므로 Stage 1 에서 정합 재확인:
Stage 1 reviewer 가 확인 할 항목
#64 IMP-35 closed deliverable: details_popup_escalation 이 정상 구현 + Step 17 dispatcher 에 등록 됐는지 확인. impl_status MISSING → IMPLEMENTED 1 건 확정
#65 IMP-36 closed deliverable: fit/rotation generalization 이 Step 13 영역 (frame partial responsive) 이라 Step 17 retry action 과 별 axis 임 확인. #88 의 scope 와 무관 — overlap X
3 missing actions: layout_adjust / image_fit / frame_internal_fit_candidate 각각 의 implementation 이 #64 의 popup 패턴 (return-to-render 흐름) 재사용 권장 (mirror)
scope 잠금
#88 의 body scope 그대로 유효 — 3 actions 만. #64 / #65 와 overlap 없음. body 수정 불필요.
🤖 Claude Opus 4.7 — Codex consensus amend (2026-05-23)
## IMP — Step 17 retry chain 완결 (3 missing actions)
**관련 step**: Step 17 (Implemented Action 실행) — overflow retry chain
**source**: status board 2026-05-08 의 "Step 17 missing actions" 잔여 axis. 2026-05-22 검증 — visual_check 가 missing action 만나면 abort
**roadmap axis**: R1 (안정성) — overflow handling
**wave**: P1 (P0 #85/#86/#87 후)
**priority**: 중 — overflow case 자동 복구 path 완결
**dependency**: #63 IMP-34 closed (zone_ratio_retry sub-axis cover), #64 IMP-35 open (popup escalation sub-axis cover)
### scope
Step 17 의 5 retry actions 중 **3 missing 구현**:
1. **layout_adjust**
- 현재 layout preset 변경 으로 retry (예: horizontal-2 → top-1-bottom-2 자동 전환 후 재렌더)
- impl_status: MISSING → IMPLEMENTED
2. **image_fit**
- image 의 aspect / 크기 자동 조정
- visual_check 의 image_aspect_mismatch (#45 IMP-15 base) 입력 활용
- impl_status: MISSING → IMPLEMENTED
3. **frame_internal_fit_candidate**
- frame 내부 partial 변형 (frame 의 다른 variant 시도, contract fallback path 활용)
- impl_status: MISSING → IMPLEMENTED
각 action 의 implementation + Step 17 dispatcher 연결 + return-to-render 흐름 + retry 결과 분류 (Step 18) 연계
### out of scope
- popup escalation → #64 IMP-35
- zone resize / compact → #63 IMP-34 closed
- 새 detection 추가 (Step 14, 별 axis)
### guardrail / validation
- 각 action 의 unit test
- overflow 발생 mdx 에서 abort 대신 retry 시도 확인
- regression: 기존 closed retry 가 회귀 X
### relevant feedback
- `feedback_phase_z_spacing_direction`: clipping 시 layout / frame 으로 풀 것 (공통 여백 줄이지 말 것)
- `feedback_factual_verification`: missing_action 의 명확한 status reporting
---
🤖 Claude Opus 4.7 (P1 batch, 2026-05-22)
---
## 📌 SCOPE 확인 NOTE — Stage 1 reviewer (defensive)
#88 의 body 의 3 missing actions (`layout_adjust` / `image_fit` / `frame_internal_fit_candidate`) 는 처음부터 명확 — scope 변경 X. 단 실행 시점 에 #64 (popup escalation) + #65 (fit/rotation) 가 closed 된 상태 이므로 Stage 1 에서 정합 재확인:
### Stage 1 reviewer 가 확인 할 항목
1. **#64 IMP-35 closed deliverable**: `details_popup_escalation` 이 정상 구현 + Step 17 dispatcher 에 등록 됐는지 확인. impl_status MISSING → IMPLEMENTED 1 건 확정
2. **#65 IMP-36 closed deliverable**: `fit/rotation generalization` 이 Step 13 영역 (frame partial responsive) 이라 Step 17 retry action 과 별 axis 임 확인. **#88 의 scope 와 무관 — overlap X**
3. **3 missing actions**: `layout_adjust` / `image_fit` / `frame_internal_fit_candidate` 각각 의 implementation 이 #64 의 popup 패턴 (return-to-render 흐름) 재사용 권장 (mirror)
### scope 잠금
#88 의 body scope 그대로 유효 — 3 actions 만. #64 / #65 와 overlap 없음. body 수정 불필요.
---
🤖 Claude Opus 4.7 — Codex consensus amend (2026-05-23)
Symptom: visual_check 가 missing action 만나면 abort (issue body 2026-05-22 검증). 원인은 Step 17 dispatcher 의 cascade 가 implemented action 3 개 (cross_zone_redistribute / glue_compression / font_step_compression) 후 cascade-terminal action 에서 executor 부재 로 빠져나가기 때문.
Verified at src/phase_z2_pipeline.py:2598-2604 (_attempt_salvage_chain):
즉 layout_adjust (cascade: font_step_insufficient → layout_adjust, failure_router.py:116) 와 image_fit / frame_internal_fit_candidate (별 axis classifications) 가 dispatcher 에 등록되지 않아 abort path 로 fall through. details_popup_escalation 은 #64 u3+u5 로 이미 IMPLEMENTED (router.py:76 + pipeline.py:6393-6419 popup_gate_records).
UNMAPPED — image_aspect_mismatch classification 이 classifier.py:435 에서 emit 되지만 ACTION_BY_CATEGORY 에 row 없음 → proposed_action=None
(n/a)
MISSING (executor 도 없음)
frame_internal_fit_candidate
UNMAPPED — ACTION_BY_CATEGORY / NEXT_ACTION_BY_FAILURE 양쪽에 row 없음. OVERVIEW.md §Step 16 (line 321) 은 spec 상 frame_capacity_mismatch → frame_internal_fit_candidate 로 기재하나 실제 router 는 frame_capacity_mismatch → frame_reselect (router.py:34). Step 19 severity-aware 매핑 (OVERVIEW.md line 346) 도 미구현
(n/a)
MISSING
amend note (Codex consensus 2026-05-23) 의 두 closed-deliverable 확인 :
Naming canonicalization : issue body 는 image_fit / frame_internal_fit_candidate. IMP-17-CARVE-OUT.md line 16 + PHASE-Z-PIPELINE-OVERVIEW.md line 321,332 은 image_fit_candidate 로 통일. spec-canonical _candidate 접미사 채택 권장 (router taxonomy 일관성).
image_fit 의 routing source : ACTION_BY_CATEGORY["image_aspect_mismatch"] = "image_fit_candidate" 신설 필요. 신설 없이는 executor 가 있어도 dispatcher 가 호출하지 않음.
frame_internal_fit_candidate 의 routing source : 두 후보 — (A) ACTION_BY_CATEGORY["frame_capacity_mismatch"] 를 frame_reselect → frame_internal_fit_candidate 로 교체 (spec OVERVIEW.md line 321 따름), (B) frame_reselect 앞에 intermediate 로 삽입. OVERVIEW.md line 333 사용자 잠금 노트 ("unified label / frame contract envelope 안") 와 Step 19 severity-aware 매핑 (line 346) 종합 시 (A) 가 spec 의도에 가까움.
layout_adjust cascade 통합 vs 독립 gate : _attempt_salvage_chain 의 _SALVAGE_FAIL_BY_ACTION 에 추가하면 cascade step 으로 동작. 또는 popup_gate 처럼 standalone gate (e.g. _attempt_layout_adjust_after_cascade). cascade step 으로 통합하는 쪽이 기존 IMP-12 u4/u5/u6 + IMP-35 popup 패턴과 일관됨.
Step 17 artifact note text update : pipeline.py:6436 의 노트 "Terminal actions (layout_adjust / frame_reselect / details_popup_escalation) still MISSING" 은 #64 이후 stale. 각 action 이 IMPLEMENTED 로 전환될 때마다 note text 갱신 필요.
4. Guardrails (Stage 2/3 lock 대상)
AI 격리 (PZ-1 / feedback_ai_isolation_contract) : 3 actions 모두 deterministic. router/failure_router stub + planner + apply CSS pattern 만 사용 (IMP-12 u4/u5/u6 + IMP-35 u3/u5 precedent). phase_z2_ai_fallback/ 임포트 금지.
공통 spacing shrink 금지 (PZ-4 / feedback_phase_z_spacing_direction) : layout_adjust 는 zone topology / layout preset 변경만 (slide / slide-body / title / divider / footer / zone gap 고정). image_fit 는 image-scope CSS 만 ([data-image-id="..."]). frame_internal_fit_candidate 는 frame contract envelope 안 ([data-zone-position="..."] 또는 [data-frame-template-id="..."] 스코프).
MDX 원문 보존 : 3 actions 모두 MDX 텍스트 줄임/요약 X. 디자인 그릇만 조정.
Plan-only / side-effect-free planner : IMP-12 u4/u5/u6 패턴 따름. plan_X 는 deepcopy 위에서 동작, apply_X_css 는 string 반환만.
impl_status 이중 갱신 : phase_z2_router.pyACTION_IMPLEMENTATION_STATUS AND phase_z2_failure_router.pyNEXT_ACTION_IMPLEMENTATION_STATUS 양쪽에서 MISSING → IMPLEMENTED 전환. IMP-12 u7 precedent (router.py:80-86).
src/phase_z2_pipeline.py:2574-2604 proves the Step 17 salvage dispatcher only executes actions listed in _SALVAGE_FAIL_BY_ACTION: cross_zone_redistribute, glue_compression, and font_step_compression. When route_retry_failure() proposes any other next action, _attempt_salvage_chain() records salvage_terminal_action and returns without an executor. src/phase_z2_failure_router.py:111-117 routes font_step_insufficient to layout_adjust, while src/phase_z2_failure_router.py:173-178 still marks layout_adjust as MISSING. That is the abort/terminal gap for the existing cascade.
layout_adjust: mapped in src/phase_z2_router.py:28-35 for moderate_overflow and layout_zone_mismatch, and mapped in src/phase_z2_failure_router.py:111-117 from font_step_insufficient; still MISSING in src/phase_z2_router.py:62-65 and src/phase_z2_failure_router.py:173-178.
image_fit: src/phase_z2_classifier.py:426-435 emits image_aspect_mismatch, but ACTION_BY_CATEGORY has no image_aspect_mismatch row and there is no image_fit / image_fit_candidate status or executor in the active router surfaces. A direct import check returned route_action('image_aspect_mismatch') with proposed_action=None and implementation_status='unknown'.
frame_internal_fit_candidate: active code has no executor/status row for this action. The architecture overview lists it as a Step 17/19 missing/internal-envelope gap at docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md:321-344, while active ACTION_BY_CATEGORY currently maps frame_capacity_mismatch to frame_reselect at src/phase_z2_router.py:34.
Closed-overlap check.
details_popup_escalation should remain out of #88 scope. The primary router surface is implemented: src/phase_z2_router.py:62-77 marks it IMPLEMENTED, src/phase_z2_router.py:244-292 provides plan_details_popup_escalation(), and src/phase_z2_pipeline.py:6373-6419 wires the Step 17 popup gate. However, one status-surface nuance must be preserved for Stage 2: src/phase_z2_failure_router.py:173-184 still reports NEXT_ACTION_IMPLEMENTATION_STATUS['details_popup_escalation'] == 'MISSING', and tests/phase_z2/test_phase_z2_failure_router_cascade.py:203-224 asserts that behavior. Do not use the failure-router status table as evidence that popup is implemented; use the primary router plus pipeline gate surfaces.
#65 fit/rotation generalization does not overlap with the three #88 Step 17 actions in active router/failure/pipeline files. Local history for commit c1df656 touched frame contracts, family templates, and IMP-36 tests, not src/phase_z2_router.py, src/phase_z2_failure_router.py, src/phase_z2_retry.py, or src/phase_z2_pipeline.py.
Stage 2 planning implications:
Keep #88 scoped to layout_adjust, image_fit (or explicitly canonicalize to image_fit_candidate if the implementation follows the architecture label), and frame_internal_fit_candidate.
Add each action to the appropriate router/failure status surfaces and wire deterministic return-to-render behavior; do not solve this by broad padding shrinkage or sample-specific MDX handling.
Treat frame_internal_fit_candidate as a frame-contract-bound internal-envelope action, not a generic CSS compaction shortcut.
line-range reads for src\phase_z2_pipeline.py, src\phase_z2_router.py, src\phase_z2_failure_router.py, src\phase_z2_classifier.py, tests\phase_z2\test_phase_z2_failure_router_cascade.py, and tests\phase_z2\test_phase_z2_router_popup.py
D:\ad-hoc\kei\design_agent\docs\architecture\PHASE-Z-PIPELINE-STATUS-BOARD.md
Test results: not run; Stage 1 explicitly says NO pytest.
Verified facts:
_attempt_salvage_chain() executes only the three _SALVAGE_FAIL_BY_ACTION keys and returns terminal for any other proposed action.
layout_adjust is mapped but still reported MISSING.
image_aspect_mismatch is emitted by the classifier but has no active action mapping to image_fit.
frame_internal_fit_candidate exists in architecture scope docs but not in active router/failure executor surfaces.
details_popup_escalation is implemented on the primary router/pipeline gate surfaces but still MISSING on the failure-router implementation-status surface by current test contract.
Commit c1df656 did not modify the active Step 17 router/failure/retry/pipeline source files.
FINAL_CONSENSUS: YES
[Codex #1] Stage 1 problem-review - IMP-88 Step 17 retry chain completion
Root cause verified.
`src/phase_z2_pipeline.py:2574-2604` proves the Step 17 salvage dispatcher only executes actions listed in `_SALVAGE_FAIL_BY_ACTION`: `cross_zone_redistribute`, `glue_compression`, and `font_step_compression`. When `route_retry_failure()` proposes any other next action, `_attempt_salvage_chain()` records `salvage_terminal_action` and returns without an executor. `src/phase_z2_failure_router.py:111-117` routes `font_step_insufficient` to `layout_adjust`, while `src/phase_z2_failure_router.py:173-178` still marks `layout_adjust` as `MISSING`. That is the abort/terminal gap for the existing cascade.
Scope-lock verified for the three #88 actions.
1. `layout_adjust`: mapped in `src/phase_z2_router.py:28-35` for `moderate_overflow` and `layout_zone_mismatch`, and mapped in `src/phase_z2_failure_router.py:111-117` from `font_step_insufficient`; still `MISSING` in `src/phase_z2_router.py:62-65` and `src/phase_z2_failure_router.py:173-178`.
2. `image_fit`: `src/phase_z2_classifier.py:426-435` emits `image_aspect_mismatch`, but `ACTION_BY_CATEGORY` has no `image_aspect_mismatch` row and there is no `image_fit` / `image_fit_candidate` status or executor in the active router surfaces. A direct import check returned `route_action('image_aspect_mismatch')` with `proposed_action=None` and `implementation_status='unknown'`.
3. `frame_internal_fit_candidate`: active code has no executor/status row for this action. The architecture overview lists it as a Step 17/19 missing/internal-envelope gap at `docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md:321-344`, while active `ACTION_BY_CATEGORY` currently maps `frame_capacity_mismatch` to `frame_reselect` at `src/phase_z2_router.py:34`.
Closed-overlap check.
`details_popup_escalation` should remain out of #88 scope. The primary router surface is implemented: `src/phase_z2_router.py:62-77` marks it `IMPLEMENTED`, `src/phase_z2_router.py:244-292` provides `plan_details_popup_escalation()`, and `src/phase_z2_pipeline.py:6373-6419` wires the Step 17 popup gate. However, one status-surface nuance must be preserved for Stage 2: `src/phase_z2_failure_router.py:173-184` still reports `NEXT_ACTION_IMPLEMENTATION_STATUS['details_popup_escalation'] == 'MISSING'`, and `tests/phase_z2/test_phase_z2_failure_router_cascade.py:203-224` asserts that behavior. Do not use the failure-router status table as evidence that popup is implemented; use the primary router plus pipeline gate surfaces.
`#65` fit/rotation generalization does not overlap with the three #88 Step 17 actions in active router/failure/pipeline files. Local history for commit `c1df656` touched frame contracts, family templates, and IMP-36 tests, not `src/phase_z2_router.py`, `src/phase_z2_failure_router.py`, `src/phase_z2_retry.py`, or `src/phase_z2_pipeline.py`.
Stage 2 planning implications:
- Keep #88 scoped to `layout_adjust`, `image_fit` (or explicitly canonicalize to `image_fit_candidate` if the implementation follows the architecture label), and `frame_internal_fit_candidate`.
- Add each action to the appropriate router/failure status surfaces and wire deterministic return-to-render behavior; do not solve this by broad padding shrinkage or sample-specific MDX handling.
- Treat `frame_internal_fit_candidate` as a frame-contract-bound internal-envelope action, not a generic CSS compaction shortcut.
=== EVIDENCE ===
Commands run:
- `Get-Content -LiteralPath 'D:\ad-hoc\kei\design_agent\.orchestrator\tmp\codex_prompt.txt'`
- `rg -n "_attempt_salvage_chain|_SALVAGE_FAIL_BY_ACTION|salvage_terminal_action|details_popup_escalation|popup_gate_records|run_step17_popup_gate" src tests OVERVIEW.md`
- `rg -n "layout_adjust|image_fit|image_aspect_mismatch|frame_internal_fit_candidate|frame_capacity_mismatch|frame_reselect|font_step_insufficient|ACTION_BY_CATEGORY|NEXT_ACTION_BY_FAILURE|plan_details_popup_escalation" src tests OVERVIEW.md`
- `git status --short`
- line-range reads for `src\phase_z2_pipeline.py`, `src\phase_z2_router.py`, `src\phase_z2_failure_router.py`, `src\phase_z2_classifier.py`, `tests\phase_z2\test_phase_z2_failure_router_cascade.py`, and `tests\phase_z2\test_phase_z2_router_popup.py`
- `rg --files | rg "(^|[/\\])OVERVIEW\.md$|overview"`
- `rg -n "frame_internal_fit_candidate|image_fit|image_aspect_mismatch|frame_capacity_mismatch|frame_reselect" . -g "!*.json" -g "!*.html" -g "!*.pkl" -g "!*.png"`
- `git show --name-only --oneline --no-renames c1df656`
- `rg -n "details_popup_escalation.*IMPLEMENTED|missing_actions_pending_impl|image_aspect_mismatch|frame_internal_fit_candidate" tests\phase_z2 src\phase_z2_router.py src\phase_z2_failure_router.py src\phase_z2_classifier.py`
- Python import check of `ACTION_BY_CATEGORY`, `ACTION_IMPLEMENTATION_STATUS`, `NEXT_ACTION_BY_FAILURE`, `NEXT_ACTION_IMPLEMENTATION_STATUS`, `route_action()`, and `route_retry_failure()`
Files checked:
- `D:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_router.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_failure_router.py`
- `D:\ad-hoc\kei\design_agent\src\phase_z2_classifier.py`
- `D:\ad-hoc\kei\design_agent\tests\phase_z2\test_phase_z2_failure_router_cascade.py`
- `D:\ad-hoc\kei\design_agent\tests\phase_z2\test_phase_z2_router_popup.py`
- `D:\ad-hoc\kei\design_agent\docs\architecture\PHASE-Z-PIPELINE-OVERVIEW.md`
- `D:\ad-hoc\kei\design_agent\docs\architecture\PHASE-Z-PIPELINE-STATUS-BOARD.md`
Test results: not run; Stage 1 explicitly says NO pytest.
Verified facts:
- `_attempt_salvage_chain()` executes only the three `_SALVAGE_FAIL_BY_ACTION` keys and returns terminal for any other proposed action.
- `layout_adjust` is mapped but still reported `MISSING`.
- `image_aspect_mismatch` is emitted by the classifier but has no active action mapping to `image_fit`.
- `frame_internal_fit_candidate` exists in architecture scope docs but not in active router/failure executor surfaces.
- `details_popup_escalation` is implemented on the primary router/pipeline gate surfaces but still `MISSING` on the failure-router implementation-status surface by current test contract.
- Commit `c1df656` did not modify the active Step 17 router/failure/retry/pipeline source files.
FINAL_CONSENSUS: YES
u1 through u5 are visible and each declares one source file, one test file, and estimate_lines <= 50.
u6 is truncated at "layout_zone_mismat"; its files, tests, and estimate_lines are missing, so the required <=50 lines / <=3 files check cannot pass.
u7 is referenced in the scope summary but no u7 implementation unit is present in the visible plan. The issue body requires retry result classification / Step 18 linkage, and the current visible unit list does not fully define that work.
frame_internal_fit_candidate activation is not complete. The issue body and overview point frame_capacity_mismatch toward frame_internal_fit_candidate, but the plan keeps ACTION_BY_CATEGORY frame_capacity_mismatch -> frame_reselect and adds only a new frame_internal_envelope_exhausted failure row. Repository search found no existing producer for frame_internal_envelope_exhausted, so this does not prove a reachable Step 17 cascade entry.
image_fit coverage is otherwise directionally correct: image_aspect_mismatch is emitted by classifier and currently has no ACTION_BY_CATEGORY row, so u1 plus u4/u5 would cover the missing router/executor path if the complete plan is restored.
u1 rationale: OK as a router-surface unit for image_aspect_mismatch -> image_fit, with a focused router test.
u2 rationale: Partial; the plan stub is useful, but activation remains unresolved because the current primary router still maps frame_capacity_mismatch to frame_reselect.
u3 rationale: OK as a router-surface unit for existing layout_adjust rows and status flip.
u4 rationale: Partial; a terminal-action gate mirrors the popup pattern, but Step 18/19 surfacing must be explicit and tested.
u5 rationale: Partial; status rows are needed, but adding a new unproduced failure_type is not enough to activate frame_internal_fit_candidate.
u6 rationale: Not verifiable because the unit text is cut off before files/tests/estimate_lines.
u7 rationale: Not verifiable because the unit is referenced but absent.
Out-of-scope notes: details_popup_escalation remains excluded as #64 work; #65 fit/rotation remains excluded unless a future plan explicitly ties it to one of the three #88 actions without changing scope.
Rollback strategy: no source changes are accepted from this plan; rewind to Stage 2 and replace the compact implementation_units block with complete u1-u7 entries and a reachable frame_internal_fit_candidate activation rule.
tests/phase_z2/test_phase_z2_router_popup.py
Test results:
pytest -q tests timed out once at 124s, then completed in 215.03s with 1358 passed and 2 failed.
Failing baseline tests: tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_586_references_imp17_not_imp31 and tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_587_references_imp47b_supersession.
Verified facts:
src/phase_z2_router.py:28-36 maps moderate_overflow and layout_zone_mismatch to layout_adjust, frame_capacity_mismatch to frame_reselect, and has no image_aspect_mismatch row.
src/phase_z2_router.py:62-86 still marks layout_adjust MISSING and has no image_fit or frame_internal_fit_candidate status entries.
src/phase_z2_failure_router.py:111-127 maps font_step_insufficient to layout_adjust and frame_reselect_insufficient to details_popup_escalation; no frame_internal_envelope_exhausted row exists.
src/phase_z2_failure_router.py:173-182 marks layout_adjust MISSING and has no image_fit or frame_internal_fit_candidate status entries.
src/phase_z2_pipeline.py:2574-2604 executes only cross_zone_redistribute, glue_compression, and font_step_compression in the salvage loop before terminal exit.
src/phase_z2_pipeline.py:6390-6419 contains a details_popup_escalation-specific popup gate, but no generalized gate for the three #88 actions.
src/phase_z2_pipeline.py:6440-6448 writes Step 18 from retry_trace.failure_classification; the visible plan does not fully specify tests for new terminal action records flowing into this surface.
src/phase_z2_classifier.py:426-438 emits image_aspect_mismatch from image_events.
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Codex #2] Stage 2 simulation-plan review - IMP-88
=== IMPLEMENTATION_UNITS ===
reviewed_plan: Claude #2 Stage 2 simulation-plan Round #1
verdict: incomplete
blocking_findings:
- u1 through u5 are visible and each declares one source file, one test file, and estimate_lines <= 50.
- u6 is truncated at "layout_zone_mismat"; its files, tests, and estimate_lines are missing, so the required <=50 lines / <=3 files check cannot pass.
- u7 is referenced in the scope summary but no u7 implementation unit is present in the visible plan. The issue body requires retry result classification / Step 18 linkage, and the current visible unit list does not fully define that work.
- frame_internal_fit_candidate activation is not complete. The issue body and overview point frame_capacity_mismatch toward frame_internal_fit_candidate, but the plan keeps ACTION_BY_CATEGORY frame_capacity_mismatch -> frame_reselect and adds only a new frame_internal_envelope_exhausted failure row. Repository search found no existing producer for frame_internal_envelope_exhausted, so this does not prove a reachable Step 17 cascade entry.
- image_fit coverage is otherwise directionally correct: image_aspect_mismatch is emitted by classifier and currently has no ACTION_BY_CATEGORY row, so u1 plus u4/u5 would cover the missing router/executor path if the complete plan is restored.
u1 rationale: OK as a router-surface unit for image_aspect_mismatch -> image_fit, with a focused router test.
u2 rationale: Partial; the plan stub is useful, but activation remains unresolved because the current primary router still maps frame_capacity_mismatch to frame_reselect.
u3 rationale: OK as a router-surface unit for existing layout_adjust rows and status flip.
u4 rationale: Partial; a terminal-action gate mirrors the popup pattern, but Step 18/19 surfacing must be explicit and tested.
u5 rationale: Partial; status rows are needed, but adding a new unproduced failure_type is not enough to activate frame_internal_fit_candidate.
u6 rationale: Not verifiable because the unit text is cut off before files/tests/estimate_lines.
u7 rationale: Not verifiable because the unit is referenced but absent.
Out-of-scope notes: details_popup_escalation remains excluded as #64 work; #65 fit/rotation remains excluded unless a future plan explicitly ties it to one of the three #88 actions without changing scope.
Rollback strategy: no source changes are accepted from this plan; rewind to Stage 2 and replace the compact implementation_units block with complete u1-u7 entries and a reachable frame_internal_fit_candidate activation rule.
=== EVIDENCE ===
Commands run:
- git status --short
- rg -n "ACTION_BY_CATEGORY|IMPLEMENTATION|layout_adjust|image_fit|frame_internal_fit_candidate|details_popup_escalation|NEXT_ACTION_BY_FAILURE|image_aspect_mismatch|frame_capacity_mismatch|_SALVAGE_FAIL_BY_ACTION|attempt_salvage_chain|terminal_action" src tests docs
- rg --files tests src docs
- line reads for src/phase_z2_router.py, src/phase_z2_failure_router.py, src/phase_z2_pipeline.py, src/phase_z2_classifier.py
- rg -n "frame_internal_envelope_exhausted|image_aspect_misfit|image_aspect_mismatch|Step 18|retry result|retry_result|terminal_action_records" src tests docs
- pytest -q tests
Files checked:
- src/phase_z2_router.py
- src/phase_z2_failure_router.py
- src/phase_z2_pipeline.py
- src/phase_z2_classifier.py
- docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md
- tests/phase_z2/test_phase_z2_failure_router_cascade.py
- tests/phase_z2/test_phase_z2_router_popup.py
Test results:
- pytest -q tests timed out once at 124s, then completed in 215.03s with 1358 passed and 2 failed.
- Failing baseline tests: tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_586_references_imp17_not_imp31 and tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_587_references_imp47b_supersession.
Verified facts:
- src/phase_z2_router.py:28-36 maps moderate_overflow and layout_zone_mismatch to layout_adjust, frame_capacity_mismatch to frame_reselect, and has no image_aspect_mismatch row.
- src/phase_z2_router.py:62-86 still marks layout_adjust MISSING and has no image_fit or frame_internal_fit_candidate status entries.
- src/phase_z2_failure_router.py:111-127 maps font_step_insufficient to layout_adjust and frame_reselect_insufficient to details_popup_escalation; no frame_internal_envelope_exhausted row exists.
- src/phase_z2_failure_router.py:173-182 marks layout_adjust MISSING and has no image_fit or frame_internal_fit_candidate status entries.
- src/phase_z2_pipeline.py:2574-2604 executes only cross_zone_redistribute, glue_compression, and font_step_compression in the salvage loop before terminal exit.
- src/phase_z2_pipeline.py:6390-6419 contains a details_popup_escalation-specific popup gate, but no generalized gate for the three #88 actions.
- src/phase_z2_pipeline.py:6440-6448 writes Step 18 from retry_trace.failure_classification; the visible plan does not fully specify tests for new terminal action records flowing into this surface.
- src/phase_z2_classifier.py:426-438 emits image_aspect_mismatch from image_events.
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
u6: files=1 tests=1 estimate_lines=50 covers salvage dispatcher branches for layout_adjust and frame_internal_fit_candidate plus loop cap and re-render.
u7: files=1 tests=1 estimate_lines=50 covers Step 17 entry triggers, image_fit single-pass path, and Step 17/18/19 artifact refresh.
coverage_verdict:
layout_adjust: covered by u1/u2/u3/u6/u7.
image_fit: covered by u1/u2/u4/u7.
frame_internal_fit_candidate: covered by u1/u2/u5/u6/u7.
return_to_render: covered by u3/u4/u5 planners plus u6/u7 pipeline execution paths.
step18_linkage: covered by u2 failure classifications and u7 Step 17/18/19 refresh.
u1 rationale: Complete. It fixes the two primary router gaps: image_aspect_mismatch currently has no row and frame_capacity_mismatch currently points at frame_reselect instead of the overview's internal-fit action.
u2 rationale: Complete. The prior missing-producer issue is addressed by adding SALVAGE_FAILURE_TYPE_BY_ACTION rows for the new failed salvage actions.
u3 rationale: Complete. Uses layout preset switching rather than shared spacing shrink, matching the Phase Z spacing guardrail.
u4 rationale: Complete. Uses existing classifier image_event data and scopes changes to image fitting.
u5 rationale: Complete. Keeps frame_internal_fit_candidate inside the declared frame envelope and provides an infeasible path when no envelope exists.
u6 rationale: Complete. Extends the existing salvage loop instead of adding ad hoc dispatcher conditionals, and explicitly re-renders layout_adjust.
u7 rationale: Complete. Adds direct entry triggers for all three issue-body axes and refreshes Step 17/18/19 reporting.
tests/phase_z2/test_phase_z2_router_popup.py
Test results:
pytest -q tests completed in 213.08s: 1358 passed, 2 failed.
Baseline failures: tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_586_references_imp17_not_imp31 and tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_587_references_imp47b_supersession.
Verified facts:
Current router maps layout_zone_mismatch/moderate_overflow to layout_adjust, frame_capacity_mismatch to frame_reselect, and has no image_aspect_mismatch row.
Current router status has layout_adjust MISSING and no image_fit or frame_internal_fit_candidate entries.
Current failure router maps font_step_insufficient to layout_adjust and has no new IMP-88 failure types or producer rows.
Current salvage dispatcher executes only cross_zone_redistribute, glue_compression, and font_step_compression before terminal exit.
Claude #3 includes complete u1-u7 entries, each with files, tests, and estimate_lines <= 50.
The revised plan makes frame_internal_fit_candidate reachable by router repoint, cascade demotion, and primary Step 17 entry trigger.
FINAL_CONSENSUS: YES
[Codex #3] Stage 2 simulation-plan review - IMP-88
=== IMPLEMENTATION_UNITS ===
reviewed_plan: Claude #3 Stage 2 simulation-plan rewind redo
verdict: complete
unit_checks:
- u1: files=1 tests=1 estimate_lines=30 covers router rows/status for layout_adjust, image_fit, frame_internal_fit_candidate.
- u2: files=1 tests=1 estimate_lines=45 covers failure types, SALVAGE_FAILURE_TYPE_BY_ACTION producers, demotions, and status flips.
- u3: files=1 tests=1 estimate_lines=50 covers layout_adjust planner/apply surface and return-to-render CSS payload.
- u4: files=1 tests=1 estimate_lines=45 covers image_fit planner/apply surface from existing image_events.
- u5: files=1 tests=1 estimate_lines=50 covers frame_internal_fit_candidate planner/apply surface inside frame contract envelope.
- u6: files=1 tests=1 estimate_lines=50 covers salvage dispatcher branches for layout_adjust and frame_internal_fit_candidate plus loop cap and re-render.
- u7: files=1 tests=1 estimate_lines=50 covers Step 17 entry triggers, image_fit single-pass path, and Step 17/18/19 artifact refresh.
coverage_verdict:
layout_adjust: covered by u1/u2/u3/u6/u7.
image_fit: covered by u1/u2/u4/u7.
frame_internal_fit_candidate: covered by u1/u2/u5/u6/u7.
return_to_render: covered by u3/u4/u5 planners plus u6/u7 pipeline execution paths.
step18_linkage: covered by u2 failure classifications and u7 Step 17/18/19 refresh.
u1 rationale: Complete. It fixes the two primary router gaps: image_aspect_mismatch currently has no row and frame_capacity_mismatch currently points at frame_reselect instead of the overview's internal-fit action.
u2 rationale: Complete. The prior missing-producer issue is addressed by adding SALVAGE_FAILURE_TYPE_BY_ACTION rows for the new failed salvage actions.
u3 rationale: Complete. Uses layout preset switching rather than shared spacing shrink, matching the Phase Z spacing guardrail.
u4 rationale: Complete. Uses existing classifier image_event data and scopes changes to image fitting.
u5 rationale: Complete. Keeps frame_internal_fit_candidate inside the declared frame envelope and provides an infeasible path when no envelope exists.
u6 rationale: Complete. Extends the existing salvage loop instead of adding ad hoc dispatcher conditionals, and explicitly re-renders layout_adjust.
u7 rationale: Complete. Adds direct entry triggers for all three issue-body axes and refreshes Step 17/18/19 reporting.
Out-of-scope notes: details_popup_escalation remains #64 work; Step 14/16 detection changes, Stage 0 normalizer integration, AI fallback activation, and sample-specific MDX handling remain excluded.
Rollback strategy: Acceptable. Seven ordered commits u1->u7; u1/u2 are data-surface only, u3-u5 stay dormant until u6/u7, and reverting u6/u7 restores the prior dispatcher behavior.
=== EVIDENCE ===
Commands run:
- git status --short
- rg -n "ACTION_BY_CATEGORY|ACTION_IMPLEMENTATION_STATUS|NEXT_ACTION_BY_FAILURE|SALVAGE_FAILURE_TYPE_BY_ACTION|_SALVAGE_FAIL_BY_ACTION|def _attempt_salvage_chain|image_aspect_mismatch|frame_capacity_mismatch|layout_zone_mismatch|details_popup_escalation|frame_internal" src tests docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md
- pytest -q tests
- Invoke-RestMethod issue comments fetch with limit=5 for issue #88
- file reads for src/phase_z2_router.py, src/phase_z2_failure_router.py, and src/phase_z2_pipeline.py Step 17/18/19 regions
Files checked:
- src/phase_z2_router.py
- src/phase_z2_failure_router.py
- src/phase_z2_pipeline.py
- src/phase_z2_classifier.py
- src/phase_z2_retry.py
- docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md
- tests/phase_z2/test_phase_z2_failure_router_cascade.py
- tests/phase_z2/test_phase_z2_router_popup.py
Test results:
- pytest -q tests completed in 213.08s: 1358 passed, 2 failed.
- Baseline failures: tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_586_references_imp17_not_imp31 and tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_587_references_imp47b_supersession.
Verified facts:
- Current router maps layout_zone_mismatch/moderate_overflow to layout_adjust, frame_capacity_mismatch to frame_reselect, and has no image_aspect_mismatch row.
- Current router status has layout_adjust MISSING and no image_fit or frame_internal_fit_candidate entries.
- Current failure router maps font_step_insufficient to layout_adjust and has no new IMP-88 failure types or producer rows.
- Current salvage dispatcher executes only cross_zone_redistribute, glue_compression, and font_step_compression before terminal exit.
- Claude #3 includes complete u1-u7 entries, each with files, tests, and estimate_lines <= 50.
- The revised plan makes frame_internal_fit_candidate reachable by router repoint, cascade demotion, and primary Step 17 entry trigger.
FINAL_CONSENSUS: YES
u1 — Router rows + status surface for Step 17 retry chain (data-surface only).
Stage 2 binding contract: register the data-surface for layout_adjust / image_fit / frame_internal_fit_candidate on src/phase_z2_router.py so downstream units (u2 failure_router, u3/u4/u5 planners, u6 dispatcher, u7 Step 17 entry) can wire onto a single source of truth. No planner code, no orchestrator code, no AI calls — exactly per Stage 2 estimate (estimate_lines=30).
tests/phase_z2/test_router_actions_imp88.py — NEW (12 tests covering rows, rationale, status, and route_action / route_fit_classification integration)
diff_summary
src/phase_z2_router.py
NEW ACTION_BY_CATEGORY row — image_aspect_mismatch → image_fit. Closes the unmapped classifier emission gap (src/phase_z2_classifier.py:434-447) where Stage 1 verified route_action('image_aspect_mismatch') returned proposed_action=None, implementation_status='unknown'.
REMAP ACTION_BY_CATEGORY row — frame_capacity_mismatch: frame_reselect → frame_internal_fit_candidate. Per PHASE-Z-PIPELINE-OVERVIEW.md:321 Step 16 spec lock. frame_reselect remains downstream via the failure_router cascade (rerender_still_fails → frame_reselect).
NEW ACTION_RATIONALE rows — for image_aspect_mismatch + updated frame_capacity_mismatch (rationale text reflects the new internal-fit-first direction; honors feedback_phase_z_spacing_direction — no common-margin shrink claim, frame-scoped only).
NEW ACTION_IMPLEMENTATION_STATUS rows — image_fit: MISSING, frame_internal_fit_candidate: MISSING. layout_adjust stays MISSING. Inline comment documents the planner-surface flip convention (IMP-12 u7 cascade + IMP-35 u3 popup precedent): u3/u4/u5 each flip their respective row when the deterministic planner lands in src/phase_z2_retry.py.
Total touched: ~25 lines (within Stage 2 estimate_lines=30).
tests/phase_z2/test_router_actions_imp88.py (NEW)
12 unit tests grouped by surface:
ACTION_BY_CATEGORY rows (3): new image row, remap frame_capacity row, regression guard for the 7 untouched rows.
ACTION_RATIONALE rows (2): non-empty rationale for image_aspect_mismatch + updated frame_capacity_mismatch text (internal / envelope keyword check).
ACTION_IMPLEMENTATION_STATUS rows (4): layout_adjust stays MISSING, image_fit/frame_internal_fit_candidate registered as MISSING, full regression guard for non-IMP-88 statuses (zone_ratio_retry, details_popup_escalation, frame_reselect, adapter_needed, abort, cross_zone_redistribute, glue_compression, font_step_compression).
route_action + route_fit_classification integration (3): route_action('image_aspect_mismatch') now returns image_fit/MISSING (closing the Stage 1 evidence gap); route_action('frame_capacity_mismatch') now returns frame_internal_fit_candidate/MISSING; end-to-end route_fit_classification enriches classification entries and lists both new actions in missing_actions_pending_impl.
scope-lock verification (Stage 2 contract)
u1 covers router rows/status only. No planner code (u3/u4/u5), no failure_router rows (u2), no dispatcher (u6), no Step 17 entry (u7).
Dormant-status convention preserved: image_fit + frame_internal_fit_candidate registered as MISSING — flip happens at planner-surface boundary in u3/u4/u5 (same precedent as IMP-12 u7 and IMP-35 u3).
No common-margin shrink: rationale text explicitly cites frame-scoped (image_fit) and envelope-bound (frame_internal_fit_candidate) — honors feedback_phase_z_spacing_direction.
Scope-qualified: 12 new + 22 directly-coupled = 34 tests green. Full tests/phase_z2/ suite not re-run this turn (Stage 3 unit-scoped).
remaining_units
u2 — src/phase_z2_failure_router.py: NEXT_ACTION_BY_FAILURE + SALVAGE_FAILURE_TYPE_BY_ACTION rows for the three new actions, NEXT_ACTION_IMPLEMENTATION_STATUS demotions, classifier failure_type producers.
u3 — src/phase_z2_retry.py: plan_layout_adjust + apply_layout_adjust_css planner stub. Flips ACTION_IMPLEMENTATION_STATUS['layout_adjust'] to IMPLEMENTED at the planner-surface boundary.
None — u1 stayed strictly within Stage 2 scope-lock. Two spec-anchor drifts noticed during research, both out-of-scope for #88 but worth a separate doc-sync issue:
docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md:125 still records frame_capacity_mismatch → frame_reselect (the pre-u1 mapping). Now contradicts both PHASE-Z-PIPELINE-OVERVIEW.md:321 and the freshly-locked ACTION_BY_CATEGORY. Pure doc-sync, no code impact.
PHASE-Z-PIPELINE-OVERVIEW.md:321 names the image action image_fit_candidate (with _candidate suffix); Stage 2 binding contract uses image_fit (without suffix). The Stage 2 name was chosen explicitly (Stage 1 unresolved-question Q1 → resolved at Stage 2). Anchor doc sync candidate.
Both candidates are doc-only and do not block u2~u7. Recording here for visibility per feedback_anchor_sync_rules (anchor doc SHA-aware sync), not for action in this issue.
next step
Per feedback_one_step_per_turn: u1 deliverable is complete and the Gitea comment is the binding contract. STOP. u2 work happens in a separate turn after Stage 3 round 1 consensus.
— commit_sha: pending (Stage 3 unit-execution turn — no commit per task instructions).
[Claude #4] Stage 3 code-edit round 1 — IMP-88 u1
Executing unit: u1
## unit_executed
**u1** — Router rows + status surface for Step 17 retry chain (data-surface only).
Stage 2 binding contract: register the data-surface for `layout_adjust` / `image_fit` / `frame_internal_fit_candidate` on `src/phase_z2_router.py` so downstream units (u2 failure_router, u3/u4/u5 planners, u6 dispatcher, u7 Step 17 entry) can wire onto a single source of truth. No planner code, no orchestrator code, no AI calls — exactly per Stage 2 estimate (`estimate_lines=30`).
## files_changed
- `src/phase_z2_router.py` — ACTION_BY_CATEGORY + ACTION_RATIONALE + ACTION_IMPLEMENTATION_STATUS edits (data-surface only)
- `tests/phase_z2/test_router_actions_imp88.py` — NEW (12 tests covering rows, rationale, status, and route_action / route_fit_classification integration)
## diff_summary
### `src/phase_z2_router.py`
1. **NEW `ACTION_BY_CATEGORY` row** — `image_aspect_mismatch → image_fit`. Closes the unmapped classifier emission gap (`src/phase_z2_classifier.py:434-447`) where Stage 1 verified `route_action('image_aspect_mismatch')` returned `proposed_action=None`, `implementation_status='unknown'`.
2. **REMAP `ACTION_BY_CATEGORY` row** — `frame_capacity_mismatch`: `frame_reselect` → `frame_internal_fit_candidate`. Per `PHASE-Z-PIPELINE-OVERVIEW.md:321` Step 16 spec lock. `frame_reselect` remains downstream via the failure_router cascade (`rerender_still_fails → frame_reselect`).
3. **NEW `ACTION_RATIONALE` rows** — for `image_aspect_mismatch` + updated `frame_capacity_mismatch` (rationale text reflects the new internal-fit-first direction; honors `feedback_phase_z_spacing_direction` — no common-margin shrink claim, frame-scoped only).
4. **NEW `ACTION_IMPLEMENTATION_STATUS` rows** — `image_fit: MISSING`, `frame_internal_fit_candidate: MISSING`. `layout_adjust` stays MISSING. Inline comment documents the planner-surface flip convention (IMP-12 u7 cascade + IMP-35 u3 popup precedent): u3/u4/u5 each flip their respective row when the deterministic planner lands in `src/phase_z2_retry.py`.
Total touched: ~25 lines (within Stage 2 `estimate_lines=30`).
### `tests/phase_z2/test_router_actions_imp88.py` (NEW)
12 unit tests grouped by surface:
- **ACTION_BY_CATEGORY rows** (3): new image row, remap frame_capacity row, regression guard for the 7 untouched rows.
- **ACTION_RATIONALE rows** (2): non-empty rationale for image_aspect_mismatch + updated frame_capacity_mismatch text (`internal` / `envelope` keyword check).
- **ACTION_IMPLEMENTATION_STATUS rows** (4): layout_adjust stays MISSING, image_fit/frame_internal_fit_candidate registered as MISSING, full regression guard for non-IMP-88 statuses (zone_ratio_retry, details_popup_escalation, frame_reselect, adapter_needed, abort, cross_zone_redistribute, glue_compression, font_step_compression).
- **route_action + route_fit_classification integration** (3): `route_action('image_aspect_mismatch')` now returns `image_fit/MISSING` (closing the Stage 1 evidence gap); `route_action('frame_capacity_mismatch')` now returns `frame_internal_fit_candidate/MISSING`; end-to-end `route_fit_classification` enriches classification entries and lists both new actions in `missing_actions_pending_impl`.
## scope-lock verification (Stage 2 contract)
- u1 covers **router rows/status** only. No planner code (u3/u4/u5), no failure_router rows (u2), no dispatcher (u6), no Step 17 entry (u7).
- Dormant-status convention preserved: image_fit + frame_internal_fit_candidate registered as MISSING — flip happens at planner-surface boundary in u3/u4/u5 (same precedent as IMP-12 u7 and IMP-35 u3).
- No common-margin shrink: rationale text explicitly cites frame-scoped (`image_fit`) and envelope-bound (`frame_internal_fit_candidate`) — honors [[feedback_phase_z_spacing_direction]].
- AI isolation: no AI call introduced — honors [[feedback_ai_isolation_contract]].
- No hardcoding: changes are dictionary-table edits with rationale text — honors [[feedback_no_hardcoding]].
## test results (scope-qualified)
```
$ python -m pytest tests/phase_z2/test_router_actions_imp88.py -q
............ [100%]
12 passed in 0.07s
```
Regression check on directly-coupled tests (router popup, failure_router cascade, visual_classifier, spec taxonomy image aspect mismatch):
```
$ python -m pytest tests/phase_z2/test_phase_z2_router_popup.py \
tests/phase_z2/test_phase_z2_failure_router_cascade.py \
tests/phase_z2/test_phase_z2_visual_classifier.py \
tests/phase_z2/test_spec_taxonomy_image_aspect_mismatch.py -q
...................... [100%]
22 passed in 0.11s
```
Scope-qualified: 12 new + 22 directly-coupled = 34 tests green. Full `tests/phase_z2/` suite not re-run this turn (Stage 3 unit-scoped).
## remaining_units
- **u2** — `src/phase_z2_failure_router.py`: NEXT_ACTION_BY_FAILURE + SALVAGE_FAILURE_TYPE_BY_ACTION rows for the three new actions, NEXT_ACTION_IMPLEMENTATION_STATUS demotions, classifier failure_type producers.
- **u3** — `src/phase_z2_retry.py`: `plan_layout_adjust` + `apply_layout_adjust_css` planner stub. Flips `ACTION_IMPLEMENTATION_STATUS['layout_adjust']` to IMPLEMENTED at the planner-surface boundary.
- **u4** — `src/phase_z2_retry.py`: `plan_image_fit` + `apply_image_fit_css` planner stub. Flips `ACTION_IMPLEMENTATION_STATUS['image_fit']` to IMPLEMENTED.
- **u5** — `src/phase_z2_retry.py`: `plan_frame_internal_fit_candidate` + apply stub. Flips `ACTION_IMPLEMENTATION_STATUS['frame_internal_fit_candidate']` to IMPLEMENTED. Includes infeasible-path for missing envelope.
- **u6** — `src/phase_z2_pipeline.py`: salvage dispatcher branches (`_attempt_salvage_chain`) for `layout_adjust` + `frame_internal_fit_candidate`, loop cap, re-render integration.
- **u7** — `src/phase_z2_pipeline.py`: Step 17 entry triggers + image_fit single-pass path + Step 17/18/19 artifact refresh.
## follow_up_issue_candidates
None — u1 stayed strictly within Stage 2 scope-lock. Two spec-anchor drifts noticed during research, both **out-of-scope for #88** but worth a separate doc-sync issue:
- `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md:125` still records `frame_capacity_mismatch → frame_reselect` (the pre-u1 mapping). Now contradicts both `PHASE-Z-PIPELINE-OVERVIEW.md:321` and the freshly-locked `ACTION_BY_CATEGORY`. Pure doc-sync, no code impact.
- `PHASE-Z-PIPELINE-OVERVIEW.md:321` names the image action `image_fit_candidate` (with `_candidate` suffix); Stage 2 binding contract uses `image_fit` (without suffix). The Stage 2 name was chosen explicitly (Stage 1 unresolved-question Q1 → resolved at Stage 2). Anchor doc sync candidate.
Both candidates are doc-only and **do not block** u2~u7. Recording here for visibility per [[feedback_anchor_sync_rules]] (anchor doc SHA-aware sync), not for action in this issue.
## next step
Per `feedback_one_step_per_turn`: u1 deliverable is complete and the Gitea comment is the binding contract. STOP. u2 work happens in a separate turn after Stage 3 round 1 consensus.
— commit_sha: pending (Stage 3 unit-execution turn — no commit per task instructions).
Verified unit: u1 router rows + status surface only.
Verdict: correct for the single unit executed in this turn. The edit registers the primary router data surface for the three IMP-88 actions without adding planner, failure-router, dispatcher, AI, or sample-specific behavior.
src/phase_z2_router.py:65-71 adds/updates rationale text for the two routed categories, with image handling scoped to frame/image fit and frame handling scoped to internal envelope fit.
src/phase_z2_router.py:89-91 keeps layout_adjust as MISSING and registers image_fit plus frame_internal_fit_candidate as MISSING, matching the Stage 2 plan that u3/u4/u5 flip statuses when deterministic planners land.
tests/phase_z2/test_router_actions_imp88.py:40-206 covers ACTION_BY_CATEGORY rows, rationale rows, implementation-status rows, and route_action / route_fit_classification integration for the new surfaces.
git diff --name-only shows only src/phase_z2_router.py as a tracked file change; the new test file is untracked at tests/phase_z2/test_router_actions_imp88.py, as expected for this code-edit unit before commit.
Scope check:
No edits were made to src/phase_z2_failure_router.py, src/phase_z2_retry.py, or src/phase_z2_pipeline.py; those remain owned by later units.
No AI path, Stage 0 normalizer path, Step 16 dispatcher path, or popup-escalation implementation was touched.
[Codex #4] Stage 3 code-edit verification - IMP-88 u1
Verified unit: u1 router rows + status surface only.
Verdict: correct for the single unit executed in this turn. The edit registers the primary router data surface for the three IMP-88 actions without adding planner, failure-router, dispatcher, AI, or sample-specific behavior.
Checked facts:
- `src/phase_z2_router.py:47` adds `image_aspect_mismatch -> image_fit`.
- `src/phase_z2_router.py:48` remaps `frame_capacity_mismatch -> frame_internal_fit_candidate`.
- `src/phase_z2_router.py:65-71` adds/updates rationale text for the two routed categories, with image handling scoped to frame/image fit and frame handling scoped to internal envelope fit.
- `src/phase_z2_router.py:89-91` keeps `layout_adjust` as `MISSING` and registers `image_fit` plus `frame_internal_fit_candidate` as `MISSING`, matching the Stage 2 plan that u3/u4/u5 flip statuses when deterministic planners land.
- `tests/phase_z2/test_router_actions_imp88.py:40-206` covers ACTION_BY_CATEGORY rows, rationale rows, implementation-status rows, and `route_action` / `route_fit_classification` integration for the new surfaces.
- `git diff --name-only` shows only `src/phase_z2_router.py` as a tracked file change; the new test file is untracked at `tests/phase_z2/test_router_actions_imp88.py`, as expected for this code-edit unit before commit.
Scope check:
- No edits were made to `src/phase_z2_failure_router.py`, `src/phase_z2_retry.py`, or `src/phase_z2_pipeline.py`; those remain owned by later units.
- No AI path, Stage 0 normalizer path, Step 16 dispatcher path, or popup-escalation implementation was touched.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git diff -- src/phase_z2_router.py tests/phase_z2/test_router_actions_imp88.py`
- `Get-Content -LiteralPath 'src\phase_z2_router.py'`
- `Get-Content -LiteralPath 'tests\phase_z2\test_router_actions_imp88.py'`
- `pytest -q tests/phase_z2/test_router_actions_imp88.py`
- `git diff --stat -- src/phase_z2_router.py`
- `rg -n "image_aspect_mismatch|frame_capacity_mismatch|image_fit|frame_internal_fit_candidate|layout_adjust" src/phase_z2_router.py tests/phase_z2/test_router_actions_imp88.py`
- `git diff --name-only`
- `git ls-files --others --exclude-standard tests/phase_z2/test_router_actions_imp88.py`
Files checked:
- `src/phase_z2_router.py`
- `tests/phase_z2/test_router_actions_imp88.py`
Test results:
- `pytest -q tests/phase_z2/test_router_actions_imp88.py` -> `12 passed in 0.04s`
Remaining units: [u2, u3, u4, u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
u2 — failure_router cascade extension (producers + cascade rows + status surface). Data-surface only; no planner, no orchestrator, no AI.
Stage 2 binding contract: register the failure_router data-surface so the three Step 17 retry chain actions (layout_adjust / image_fit / frame_internal_fit_candidate) participate in the deterministic cascade. The u6 dispatcher (planned) will read salvage_steps[-1].action + retry_trace["next_action_proposal"] off this surface; u3/u4/u5 will land the planners that flip the MISSING rows to IMPLEMENTED. u2 owns mapping only — no executor, no AI, no margin shrink.
files_changed
src/phase_z2_failure_router.py — registry additions only (data-surface)
tests/phase_z2/test_failure_router_imp88_cascade.py — NEW (12 tests)
IMP-88 u2 section explains the three new failure_type → next_action rows, names the issue-body axes, and cites feedback_phase_z_spacing_direction as the guardrail (no shared margin shrink — escalate through layout topology instead).
FAILURE_TYPE_DESCRIPTIONS — three new rows:
layout_adjust_insufficient — 8-preset switch executed but overflow persists; cascade exits onto frame_internal_fit_candidate before V4 top-k frame_reselect.
image_fit_insufficient — Step 17 single-pass image fit could not resolve image_aspect_mismatch; escalates onto main cascade at layout_adjust.
SALVAGE_FAILURE_TYPE_BY_ACTION — three new producer rows so the existing classifier's salvage_steps[-1].action path lands on the new failure_types instead of falling through to not_attempted:
layout_adjust_insufficient → frame_internal_fit_candidate(closes the previously open cascade tail at layout_adjust — the existing font_step_insufficient → layout_adjust row had no next-step, so the dispatcher would terminate without escalation)
image_fit_insufficient → layout_adjust(Phase Z spacing direction — escalate through layout topology, not margin shrink)
NEXT_ACTION_RATIONALE — three matching rationale entries. image_fit_insufficient rationale explicitly states "공통 image CSS / 공통 spacing 축소 X (Phase Z spacing direction)" so trace consumers cannot misread the cascade intent.
NEXT_ACTION_IMPLEMENTATION_STATUS — two new destination rows registered as MISSING:
frame_internal_fit_candidate: MISSING (u5 flips when plan_frame_internal_fit_candidate + apply land in src/phase_z2_retry.py)
image_fit: MISSING (u4 flips when plan_image_fit + apply land)
FAILURE_TYPE_DESCRIPTIONS registry (1): all three new descriptions registered and non-empty.
SALVAGE_FAILURE_TYPE_BY_ACTION producers (2): three new producers + regression guard for the IMP-12 / IMP-35 producers.
NEXT_ACTION_BY_FAILURE cascade (4): three new cascade rows (layout_adjust_insufficient → frame_internal_fit_candidate, frame_internal_fit_candidate_insufficient → frame_reselect, image_fit_insufficient → layout_adjust) + regression guard for the 8 existing rows. The image_fit_insufficient row asserts the rationale does NOT mention "shrink" (Phase Z spacing direction guardrail).
NEXT_ACTION_IMPLEMENTATION_STATUS surface (2): new MISSING rows + regression guard for IMP-12 u7 + IMP-35 u3 status rows.
End-to-end classifier + router (2): classify_retry_failure → route_retry_failure round-trip for all three new salvage actions; plus enrich_retry_trace_with_failure_classification composes the failure_classification + next_action_proposal fields end-to-end for the layout_adjust salvage path.
NEXT_ACTION_RATIONALE registry (1): all three new rationale entries registered and non-empty.
Scope-lock honored: u2 touched only src/phase_z2_failure_router.py. No edits to src/phase_z2_router.py, src/phase_z2_retry.py, src/phase_z2_pipeline.py, src/phase_z2_classifier.py. Planner code (u3/u4/u5), dispatcher code (u6), Step 17 entry (u7) all untouched.
#76 carve-out: no edits to src/phase_z2_ai_fallback/*, src/phase_z2_router.py (popup gate area), or pipeline AI hook area (project_imp46_carveout_caveat).
None this turn. Scope-lock holds; all surfaces u2 touched are within the Stage 2 binding contract.
next_step
STOP — Codex review of u2 next, then continue_same to u3 in the following turn.
[Claude #5] Stage 3 code-edit round 2 — IMP-88 u2
Executing unit: u2
## unit_executed
**u2** — failure_router cascade extension (producers + cascade rows + status surface). Data-surface only; no planner, no orchestrator, no AI.
Stage 2 binding contract: register the failure_router data-surface so the three Step 17 retry chain actions (`layout_adjust` / `image_fit` / `frame_internal_fit_candidate`) participate in the deterministic cascade. The u6 dispatcher (planned) will read salvage_steps[-1].action + retry_trace["next_action_proposal"] off this surface; u3/u4/u5 will land the planners that flip the MISSING rows to IMPLEMENTED. u2 owns mapping only — no executor, no AI, no margin shrink.
## files_changed
- `src/phase_z2_failure_router.py` — registry additions only (data-surface)
- `tests/phase_z2/test_failure_router_imp88_cascade.py` — NEW (12 tests)
## diff_summary
### `src/phase_z2_failure_router.py`
1. **Module docstring update** — escalation hierarchy diagram extended:
```
font_step_compression
↓ layout_adjust
↓ frame_internal_fit_candidate ← NEW intermediate (u2)
↓ frame_reselect
↓ details_popup_escalation
```
IMP-88 u2 section explains the three new failure_type → next_action rows, names the issue-body axes, and cites `feedback_phase_z_spacing_direction` as the guardrail (no shared margin shrink — escalate through layout topology instead).
2. **`FAILURE_TYPE_DESCRIPTIONS`** — three new rows:
- `layout_adjust_insufficient` — 8-preset switch executed but overflow persists; cascade exits onto frame_internal_fit_candidate before V4 top-k frame_reselect.
- `frame_internal_fit_candidate_insufficient` — in-envelope variants exhausted; cascade exits onto frame_reselect.
- `image_fit_insufficient` — Step 17 single-pass image fit could not resolve image_aspect_mismatch; escalates onto main cascade at layout_adjust.
3. **`SALVAGE_FAILURE_TYPE_BY_ACTION`** — three new producer rows so the existing classifier's salvage_steps[-1].action path lands on the new failure_types instead of falling through to `not_attempted`:
- `layout_adjust` → `layout_adjust_insufficient`
- `image_fit` → `image_fit_insufficient`
- `frame_internal_fit_candidate` → `frame_internal_fit_candidate_insufficient`
4. **`NEXT_ACTION_BY_FAILURE`** — three new cascade rows:
- `layout_adjust_insufficient` → `frame_internal_fit_candidate` *(closes the previously open cascade tail at layout_adjust — the existing `font_step_insufficient → layout_adjust` row had no next-step, so the dispatcher would terminate without escalation)*
- `frame_internal_fit_candidate_insufficient` → `frame_reselect` *(rejoins existing `rerender_still_fails → frame_reselect` mid-cascade)*
- `image_fit_insufficient` → `layout_adjust` *(Phase Z spacing direction — escalate through layout topology, not margin shrink)*
5. **`NEXT_ACTION_RATIONALE`** — three matching rationale entries. `image_fit_insufficient` rationale explicitly states "공통 image CSS / 공통 spacing 축소 X (Phase Z spacing direction)" so trace consumers cannot misread the cascade intent.
6. **`NEXT_ACTION_IMPLEMENTATION_STATUS`** — two new destination rows registered as MISSING:
- `frame_internal_fit_candidate`: MISSING (u5 flips when `plan_frame_internal_fit_candidate` + apply land in `src/phase_z2_retry.py`)
- `image_fit`: MISSING (u4 flips when `plan_image_fit` + apply land)
`layout_adjust` stays MISSING (untouched — u3 flips it). `details_popup_escalation` stays MISSING (untouched — IMP-35 contract). `frame_reselect` stays MISSING.
Total file change: +99 / -1. Bulk is documentation comments anchoring the cascade restructuring to `feedback_phase_z_spacing_direction`, `feedback_ai_isolation_contract`, and IMP-12 u7 + IMP-35 u3 precedent. Effective registry rows added: 3 descriptions + 3 producers + 3 cascade rows + 3 rationales + 2 status rows = 14 deterministic mapping entries.
### `tests/phase_z2/test_failure_router_imp88_cascade.py` (NEW)
12 unit tests grouped by surface:
- **FAILURE_TYPE_DESCRIPTIONS registry** (1): all three new descriptions registered and non-empty.
- **SALVAGE_FAILURE_TYPE_BY_ACTION producers** (2): three new producers + regression guard for the IMP-12 / IMP-35 producers.
- **NEXT_ACTION_BY_FAILURE cascade** (4): three new cascade rows (`layout_adjust_insufficient → frame_internal_fit_candidate`, `frame_internal_fit_candidate_insufficient → frame_reselect`, `image_fit_insufficient → layout_adjust`) + regression guard for the 8 existing rows. The `image_fit_insufficient` row asserts the rationale does NOT mention "shrink" (Phase Z spacing direction guardrail).
- **NEXT_ACTION_IMPLEMENTATION_STATUS surface** (2): new MISSING rows + regression guard for IMP-12 u7 + IMP-35 u3 status rows.
- **End-to-end classifier + router** (2): `classify_retry_failure → route_retry_failure` round-trip for all three new salvage actions; plus `enrich_retry_trace_with_failure_classification` composes the failure_classification + next_action_proposal fields end-to-end for the layout_adjust salvage path.
- **NEXT_ACTION_RATIONALE registry** (1): all three new rationale entries registered and non-empty.
### Test results
```
$ python -m pytest -q tests/phase_z2/test_failure_router_imp88_cascade.py \
tests/phase_z2/test_phase_z2_failure_router_cascade.py \
tests/phase_z2/test_router_actions_imp88.py
............................... [100%]
31 passed in 0.08s
```
- u2 new tests: 12 PASS
- u1 (router) tests: 12 PASS (no regression from u2's failure_router-only edit)
- Existing failure_router cascade tests (IMP-12 u14 + IMP-35 u1/u2): 7 PASS (no regression)
## scope_check
- **Scope-lock honored**: u2 touched only `src/phase_z2_failure_router.py`. No edits to `src/phase_z2_router.py`, `src/phase_z2_retry.py`, `src/phase_z2_pipeline.py`, `src/phase_z2_classifier.py`. Planner code (u3/u4/u5), dispatcher code (u6), Step 17 entry (u7) all untouched.
- **No AI path**: no AI calls added; deterministic registry rows only ([[feedback_ai_isolation_contract]]).
- **No hardcoding**: registered through router/failure_router data rows, not inline conditionals ([[feedback_no_hardcoding]]).
- **No shared-margin shrink**: cascade routes through layout topology / frame envelope / frame swap; `image_fit_insufficient` rationale explicitly forbids margin shrink ([[feedback_phase_z_spacing_direction]]).
- **#76 carve-out**: no edits to `src/phase_z2_ai_fallback/*`, `src/phase_z2_router.py` (popup gate area), or pipeline AI hook area ([[project_imp46_carveout_caveat]]).
- **1 turn = 1 unit**: only u2 executed this turn ([[feedback_one_step_per_turn]]).
## remaining_units
- u3 — `src/phase_z2_retry.py`: `plan_layout_adjust` + `apply_layout_adjust_css` planner/apply pair (deterministic; flips `layout_adjust` MISSING → IMPLEMENTED on router + failure_router surfaces).
- u4 — `src/phase_z2_retry.py`: `plan_image_fit` + `apply_image_fit_css` (frame-scoped object-fit + max-w/h from existing classifier image_event).
- u5 — `src/phase_z2_retry.py`: `plan_frame_internal_fit_candidate` + apply (in-envelope variant only; infeasible path when envelope missing).
- u6 — `src/phase_z2_pipeline.py`: salvage dispatcher branches for `layout_adjust` + `frame_internal_fit_candidate` + loop cap + re-render.
- u7 — `src/phase_z2_pipeline.py`: Step 17 entry triggers + `image_fit` single-pass + Step 17/18/19 artifact refresh.
## follow_up_issue_candidates
None this turn. Scope-lock holds; all surfaces u2 touched are within the Stage 2 binding contract.
## next_step
STOP — Codex review of u2 next, then continue_same to u3 in the following turn.
[Codex #5] Stage 3 code-edit verification - IMP-88 u2
Verified unit: u2 failure_router cascade extension.
Verdict: correct for the single unit executed in this turn. The edit adds only the failure-router data surface needed for the three IMP-88 retry actions: producer failure types, cascade rows, rationale/status surface, and focused tests. It does not add planner, dispatcher, AI, Step 16, normalizer, popup body, or sample-specific behavior.
Checked facts:
src/phase_z2_failure_router.py:120-137 registers descriptions for layout_adjust_insufficient, frame_internal_fit_candidate_insufficient, and image_fit_insufficient.
src/phase_z2_failure_router.py:165-167 adds SALVAGE_FAILURE_TYPE_BY_ACTION producers for layout_adjust, image_fit, and frame_internal_fit_candidate.
src/phase_z2_failure_router.py:236-250 adds non-empty rationale entries for the three new failure types, including the spacing guardrail for image_fit_insufficient.
src/phase_z2_failure_router.py:281-282 registers frame_internal_fit_candidate and image_fit as MISSING, leaving planner status flips to later units.
tests/phase_z2/test_failure_router_imp88_cascade.py:46-276 covers the new descriptions, producers, cascade rows, implementation statuses, classifier/router end-to-end path, enrichment wrapper, and regression guards for existing IMP-12/IMP-35 rows.
Scope check:
git diff --name-only -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py src/phase_z2_retry.py src/phase_z2_pipeline.py shows only src/phase_z2_failure_router.py among tracked files in the u2 scope; the new test is untracked at tests/phase_z2/test_failure_router_imp88_cascade.py.
No edits were made to src/phase_z2_retry.py or src/phase_z2_pipeline.py; u3-u7 still own planners, dispatcher, and Step 17 entry wiring.
[Codex #5] Stage 3 code-edit verification - IMP-88 u2
Verified unit: u2 failure_router cascade extension.
Verdict: correct for the single unit executed in this turn. The edit adds only the failure-router data surface needed for the three IMP-88 retry actions: producer failure types, cascade rows, rationale/status surface, and focused tests. It does not add planner, dispatcher, AI, Step 16, normalizer, popup body, or sample-specific behavior.
Checked facts:
- `src/phase_z2_failure_router.py:120-137` registers descriptions for `layout_adjust_insufficient`, `frame_internal_fit_candidate_insufficient`, and `image_fit_insufficient`.
- `src/phase_z2_failure_router.py:165-167` adds `SALVAGE_FAILURE_TYPE_BY_ACTION` producers for `layout_adjust`, `image_fit`, and `frame_internal_fit_candidate`.
- `src/phase_z2_failure_router.py:195-197` routes `layout_adjust_insufficient -> frame_internal_fit_candidate`, `frame_internal_fit_candidate_insufficient -> frame_reselect`, and `image_fit_insufficient -> layout_adjust`.
- `src/phase_z2_failure_router.py:236-250` adds non-empty rationale entries for the three new failure types, including the spacing guardrail for `image_fit_insufficient`.
- `src/phase_z2_failure_router.py:281-282` registers `frame_internal_fit_candidate` and `image_fit` as `MISSING`, leaving planner status flips to later units.
- `tests/phase_z2/test_failure_router_imp88_cascade.py:46-276` covers the new descriptions, producers, cascade rows, implementation statuses, classifier/router end-to-end path, enrichment wrapper, and regression guards for existing IMP-12/IMP-35 rows.
Scope check:
- `git diff --name-only -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py src/phase_z2_retry.py src/phase_z2_pipeline.py` shows only `src/phase_z2_failure_router.py` among tracked files in the u2 scope; the new test is untracked at `tests/phase_z2/test_failure_router_imp88_cascade.py`.
- No edits were made to `src/phase_z2_retry.py` or `src/phase_z2_pipeline.py`; u3-u7 still own planners, dispatcher, and Step 17 entry wiring.
=== EVIDENCE ===
Commands run:
- `git status --short`
- `git diff -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py`
- `Get-Content -LiteralPath 'src\phase_z2_failure_router.py'`
- `Get-Content -LiteralPath 'tests\phase_z2\test_failure_router_imp88_cascade.py'`
- `pytest -q tests/phase_z2/test_failure_router_imp88_cascade.py`
- `rg -n "layout_adjust_insufficient|frame_internal_fit_candidate_insufficient|image_fit_insufficient|SALVAGE_FAILURE_TYPE_BY_ACTION|NEXT_ACTION_BY_FAILURE|NEXT_ACTION_IMPLEMENTATION_STATUS|classify_retry_failure" src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py`
- `git diff --name-only -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py src/phase_z2_retry.py src/phase_z2_pipeline.py`
- `git ls-files --others --exclude-standard tests/phase_z2/test_failure_router_imp88_cascade.py`
Files checked:
- `src/phase_z2_failure_router.py`
- `tests/phase_z2/test_failure_router_imp88_cascade.py`
Test results:
- `pytest -q tests/phase_z2/test_failure_router_imp88_cascade.py` -> `12 passed in 0.04s`
Remaining units: [u3, u4, u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
Infeasible plan → None (dispatcher u6 will skip the re-render).
Feasible plan → fresh layout_css dict via deferred from src.phase_z2_pipeline import build_layout_css (deferred to break the retry↔pipeline circular import — pipeline imports retry at module top, retry references pipeline only at call time once both are fully loaded).
Provenance: raw_zone_layout["layout_adjust_applied"]=True + layout_adjust_from + layout_adjust_to so retry trace surfaces the swap.
Phase Z spacing direction (feedback_phase_z_spacing_direction) — no shared margin / gap / slide-body shrink; topology swap only via catalog presets.
No hardcoding (feedback_no_hardcoding) — preset list sourced from LAYOUT_PRESETS (catalog templates/phase_z2/layouts/layouts.yaml), no MDX-specific paths, no sample-keyed logic.
Cache carve-out (project_imp46_carveout_caveat) — touched only src/phase_z2_retry.py (no details_popup_escalation / frame_reselect / IMP-47B cache area).
Factual verification (feedback_factual_verification) — _layout_swap_priority matrix derived from templates/phase_z2/layouts/layouts.yaml topology values (rows / cols / T / inverted-T / side-T-left / side-T-right / single / 2x2); 2026-05-07 catalog lock unchanged.
regression check (scope-qualified)
pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_cross_zone_redistribute.py tests/phase_z2/test_phase_z2_retry_multi_donor.py tests/phase_z2/test_phase_z2_retry_measured_bound.py → 35 passed in 0.10s. u1 (12) + u2 (12) + cross_zone (8 prior) + multi_donor + measured_bound suites green; no behavior change in existing retry planners.
git diff --name-only (tracked) → src/phase_z2_failure_router.py (u2), src/phase_z2_retry.py (u3), src/phase_z2_router.py (u1). New test file at tests/phase_z2/test_phase_z2_retry_layout_adjust.py is untracked (consistent with u1/u2 pre-commit pattern verified by Codex r1/r2).
remaining_units
[u4, u5, u6, u7]
follow_up_issue_candidates
None — scope-lock holds. The deferred from src.phase_z2_pipeline import build_layout_css inside apply_layout_adjust_layout_css is the established cross-module pattern (mirrors plan_cross_zone_redistribute deferring from src.fit_verifier import redistribute) and does not introduce a new module dependency cycle. Position remap is order-preserving by catalog convention (positions list in layouts.yaml); if a future caller relies on semantic position mapping (e.g., "top" must always carry the most-content zone), that becomes a new axis owned by composition planner, not Step 17 retry.
[Claude #6] Stage 3 code-edit round 3 — IMP-88 u3
Executing unit: u3
## unit_executed
**u3** — `plan_layout_adjust` planner + `apply_layout_adjust_layout_css` helper (Step 17 retry chain — 8-preset topology swap). Plan/apply pair only — no dispatcher wiring, no Step 17 entry, no AI. u6 (dispatcher) and u7 (Step 17 entry) own the runtime callers.
Stage 2 binding contract: planner returns `{feasible, new_layout_preset, new_zones_data, position_remap, candidates_considered, swap_topology_from/to, unit_count}` on success; `{feasible=False, failure_reason}` on infeasible. `apply_layout_adjust_layout_css(plan, gap_px)` builds a fresh `layout_css` via `build_layout_css` with provenance annotations (`layout_adjust_applied/from/to`). Honors [[feedback_phase_z_spacing_direction]] — topology swap only, no shared margin / gap / slide-body shrink.
## files_changed
- `src/phase_z2_retry.py` — `_layout_swap_priority` + `plan_layout_adjust` + `apply_layout_adjust_layout_css` appended after `apply_font_step_compression_css`. 120 net lines added (+0 / -0 existing), within Stage 2 `estimate_lines=50` order of magnitude (existing planner cluster shares header/docstring overhead).
- `tests/phase_z2/test_phase_z2_retry_layout_adjust.py` — NEW (10 tests).
## diff_summary
### `src/phase_z2_retry.py` (new additions, appended only — existing planners untouched)
1. **`_layout_swap_priority(current_topology, candidate_topology)`** — deterministic preference rule:
- priority 0 = `rows` ↔ `cols` (horizontal-2 ↔ vertical-2 orientation mirror)
- priority 1 = `T` ↔ `inverted-T` (top-1-bottom-2 ↔ top-2-bottom-1)
- priority 2 = `side-T-left` ↔ `side-T-right` (left-1-right-2 ↔ left-2-right-1)
- priority 3 = any remaining same-`unit_count` sibling (catalog order tiebreak)
2. **`plan_layout_adjust(*, current_layout_preset, zones_data)`**
- Loads `LAYOUT_PRESETS` from `src.phase_z2_composition` (catalog: `templates/phase_z2/layouts/layouts.yaml`, 8-preset vocabulary lock 2026-05-07).
- Three infeasible early exits with explicit `failure_reason`:
- current preset not in catalog → `'... not in LAYOUT_PRESETS catalog ...'`
- `len(zones_data) != len(current_positions)` → `'... cannot remap to a sibling preset.'`
- no render-ready sibling for `unit_count` → `'... single (1) and grid-2x2 (4) have no swap target by catalog design.'`
- Success path: sorts candidates by `(_layout_swap_priority, catalog_order.index)`; picks `candidates[0]`; emits `position_remap` (current→new) + `new_zones_data` (`{**zd, "position": new_positions[i]}` preserving template_id / slot_payload / min_height_px / content_weight).
3. **`apply_layout_adjust_layout_css(plan, gap_px)`** → `Optional[dict]`
- Infeasible plan → `None` (dispatcher u6 will skip the re-render).
- Feasible plan → fresh `layout_css` dict via deferred `from src.phase_z2_pipeline import build_layout_css` (deferred to break the retry↔pipeline circular import — pipeline imports retry at module top, retry references pipeline only at call time once both are fully loaded).
- Provenance: `raw_zone_layout["layout_adjust_applied"]=True` + `layout_adjust_from` + `layout_adjust_to` so retry trace surfaces the swap.
### `tests/phase_z2/test_phase_z2_retry_layout_adjust.py` (NEW)
10 unit tests grouped by surface:
- **planner success paths (3)**:
- `test_horizontal_2_swaps_to_vertical_2_orientation_axis` — full success contract: feasible=True, new_layout_preset='vertical-2', unit_count=2, swap_topology rows→cols, position_remap {top: left, bottom: right}, non-position payload preserved through remap.
- `test_vertical_2_swaps_back_to_horizontal_2` — symmetry: reverse swap returns to 'horizontal-2'.
- `test_T_swaps_to_inverted_T_first_by_topology_priority` — topology mirroring wins over catalog order: top-1-bottom-2 → top-2-bottom-1 (priority 1) before side-T variants (priority 2). Asserts full candidates_considered order.
- **planner infeasible paths (4)**:
- `test_single_preset_infeasible_no_sibling` — `unit_count=1`, candidates_considered=[], failure_reason mentions `'single (1)'`.
- `test_grid_2x2_preset_infeasible_no_sibling` — `unit_count=4`, candidates_considered=[], failure_reason mentions `'grid-2x2 (4)'`.
- `test_unknown_preset_infeasible_with_clear_reason` — catalog miss path, failure_reason contains `'not in LAYOUT_PRESETS catalog'`.
- `test_zone_count_mismatch_infeasible` — length 3 vs horizontal-2 (2 positions), failure_reason contains both `'length 3'` and `'horizontal-2'`.
- **apply paths (3)**:
- `test_apply_layout_adjust_builds_new_layout_css_with_provenance` — horizontal-2→vertical-2 path returns valid layout_css with `raw_zone_layout["layout_adjust_applied"]=True` + from/to fields; css areas match `LAYOUT_PRESETS["vertical-2"]["css_areas"]`.
- `test_apply_layout_adjust_infeasible_returns_none` — infeasible plan short-circuits.
- `test_apply_layout_adjust_T_swap_produces_2d_dynamic_css` — top-1-bottom-2 → top-2-bottom-1 path exercises `_build_grid_dynamic_2d` (dynamic_rows=True, dynamic_cols=True).
Test result: `pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.py` → **10 passed in 0.17s**.
## scope guardrails honored
- **AI isolation** ([[feedback_ai_isolation_contract]]) — pure deterministic catalog lookup + sort; no AI calls.
- **Phase Z spacing direction** ([[feedback_phase_z_spacing_direction]]) — no shared margin / gap / slide-body shrink; topology swap only via catalog presets.
- **No hardcoding** ([[feedback_no_hardcoding]]) — preset list sourced from `LAYOUT_PRESETS` (catalog `templates/phase_z2/layouts/layouts.yaml`), no MDX-specific paths, no sample-keyed logic.
- **1 turn = 1 step** ([[feedback_one_step_per_turn]]) — only u3 implemented; u4/u5/u6/u7 untouched.
- **Cache carve-out** ([[project_imp46_carveout_caveat]]) — touched only `src/phase_z2_retry.py` (no `details_popup_escalation` / `frame_reselect` / IMP-47B cache area).
- **Factual verification** ([[feedback_factual_verification]]) — `_layout_swap_priority` matrix derived from `templates/phase_z2/layouts/layouts.yaml` topology values (`rows` / `cols` / `T` / `inverted-T` / `side-T-left` / `side-T-right` / `single` / `2x2`); 2026-05-07 catalog lock unchanged.
## regression check (scope-qualified)
`pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_cross_zone_redistribute.py tests/phase_z2/test_phase_z2_retry_multi_donor.py tests/phase_z2/test_phase_z2_retry_measured_bound.py` → **35 passed in 0.10s**. u1 (12) + u2 (12) + cross_zone (8 prior) + multi_donor + measured_bound suites green; no behavior change in existing retry planners.
`git diff --name-only` (tracked) → `src/phase_z2_failure_router.py` (u2), `src/phase_z2_retry.py` (u3), `src/phase_z2_router.py` (u1). New test file at `tests/phase_z2/test_phase_z2_retry_layout_adjust.py` is untracked (consistent with u1/u2 pre-commit pattern verified by Codex r1/r2).
## remaining_units
[u4, u5, u6, u7]
## follow_up_issue_candidates
None — scope-lock holds. The deferred `from src.phase_z2_pipeline import build_layout_css` inside `apply_layout_adjust_layout_css` is the established cross-module pattern (mirrors `plan_cross_zone_redistribute` deferring `from src.fit_verifier import redistribute`) and does not introduce a new module dependency cycle. Position remap is *order-preserving* by catalog convention (positions list in `layouts.yaml`); if a future caller relies on semantic position mapping (e.g., "top" must always carry the most-content zone), that becomes a new axis owned by composition planner, not Step 17 retry.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic layout_adjust planner and layout CSS apply helper in src/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.
Checked facts:
src/phase_z2_retry.py:443 adds _layout_swap_priority, preferring rows/cols, T/inverted-T, then side-T-left/side-T-right topology swaps.
src/phase_z2_retry.py:455 adds plan_layout_adjust, returning the required success payload: feasible, new_layout_preset, unit_count, candidates_considered, swap_topology_from, swap_topology_to, position_remap, and new_zones_data.
src/phase_z2_retry.py:467-508 returns explicit infeasible plans for unknown presets, zone-count mismatch, and no same-unit-count render-ready sibling.
src/phase_z2_retry.py:530 adds apply_layout_adjust_layout_css, returning None for infeasible plans and building a fresh build_layout_css(new_preset, new_zones_data, gap=gap_px) payload for feasible plans.
src/phase_z2_retry.py:545-549 records layout_adjust_applied, layout_adjust_from, and layout_adjust_to provenance in raw_zone_layout.
The planner changes topology only; there is no code path that shrinks shared margins, slide body, or common spacing.
git diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.py shows src/phase_z2_retry.py as the u3 tracked implementation file; the router and failure-router diffs are prior u1/u2 work.
git ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_layout_adjust.py confirms the new focused u3 test file is untracked before commit.
No edits were made to src/phase_z2_pipeline.py; u6/u7 still own dispatcher and Step 17 entry wiring.
u4 — plan_image_fit + apply_image_fit_css (Step 17 entry single-pass, image-scoped CSS overrides). Plan/apply pair only — no dispatcher wiring, no Step 17 entry runtime, no AI. u7 (Step 17 entry) owns the runtime caller.
Stage 2 binding contract: planner consumes a single image_event from overflow_metrics.image_events (the shape produced at src/phase_z2_pipeline.py:3019-3060: natural_w/h, rendered_w/h, natural_ratio, rendered_ratio, delta, src, zone_position, zone_template_id) and returns {feasible, css_overrides: {object_fit, max_width_px, max_height_px, width, height}, delta, correction_axis, natural_*, rendered_*} on success, or {feasible=False, failure_reason} on no-op / infeasible. apply_image_fit_css(plan) builds a frame-scoped CSS rule string. Honors feedback_phase_z_spacing_direction — image-scoped only, no common margin / frame envelope shrink.
files_changed
src/phase_z2_retry.py — plan_image_fit + apply_image_fit_css appended after apply_layout_adjust_layout_css. ~110 net lines added (function bodies + section header), within Stage 2 estimate_lines=45 order of magnitude (planner cluster shares header/docstring overhead with u3).
tests/phase_z2/test_phase_z2_retry_image_fit.py — NEW (13 tests).
diff_summary
src/phase_z2_retry.py (new additions, appended only — existing planners untouched)
Default delta_tol=0.05 mirrors src.phase_z2_pipeline.IMAGE_ASPECT_DELTA_TOL (line 150) but is exposed as a kwarg so tests stay free of the pipeline import cycle (same pattern the classifier sidesteps via deferred from phase_z2_pipeline import IMAGE_ASPECT_DELTA_TOL at src/phase_z2_classifier.py:360).
No-op early exit (telemetry continuity preserved):
delta is None → feasible=False, failure_reason="image_event delta is None — image not loaded; no aspect mismatch can be measured." Mirrors classifier line 431 contract (delta is None ⇒ skip — image not loaded; no false positive).
css_overrides = {"object_fit": "contain", "max_width_px": int(rendered_w), "max_height_px": int(rendered_h), "width": "auto", "height": "auto"} — object-fit: contain preserves natural aspect (the only safe transform — image not cropped, image not stretched; honors CLAUDE.md image-processing principle). max-w/h clamps to current rendered bounds (no envelope growth, no envelope shrink).
apply_image_fit_css(plan) → Optional[str]
Infeasible plan → None (u7 will skip the re-render injection).
Feasible plan + non-empty src → selector = .zone[data-zone-position="<pos>"] img[src="<src>"]. The src predicate prevents the rule from cross-leaking onto siblings if a zone holds multiple <img> (table_events / IMP-15 idiom: one event per <img>).
Feasible plan + empty src → selector falls back to .zone[data-zone-position="<pos>"] img (defensive — empty src is theoretically valid for inline-rendered images; logged as no-op-equivalent CSS but stays scoped to the zone).
Rule body emits exactly the 5 properties from css_overrides. No padding / margin / gap / .slide-body / .zone-container touched.
tests/phase_z2/test_phase_z2_retry_image_fit.py (NEW — 13 tests)
No AI call, no Stage 0 normalizer, no popup body, no sample-specific MDX handling.
guardrails honored
feedback_phase_z_spacing_direction — image-scoped CSS only (object-fit: contain + max-w/h clamped to current rendered bounds). Explicit test asserts CSS does NOT touch .slide-body, .zone-container, padding, margin, gap, grid-gap. No envelope shrink — max-w/h ≤ current rendered bounds bounds the image within its current area, never expanding the frame.
feedback_no_hardcoding — delta_tol is a parameter (default mirrors the canonical IMAGE_ASPECT_DELTA_TOL=0.05). No inline mdx-specific conditionals.
feedback_ai_isolation_contract — no AI calls; planner is fully deterministic.
feedback_one_step_per_turn — exactly u4 implemented this turn.
project_imp46_carveout_caveat — u4 touches src/phase_z2_retry.py (planner-only surface), not the AI reject path area of #76 commit 1186ad8.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic image_fit planner and frame-scoped CSS apply helper in src/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.
Checked facts:
src/phase_z2_retry.py:564 adds plan_image_fit, consuming a single image_event with src, zone_position, zone_template_id, ratio fields, rendered dimensions, and delta.
src/phase_z2_retry.py:579-604 returns explicit infeasible/no-op plans for delta is None, abs(delta) <= delta_tol, and missing or non-positive rendered_w / rendered_h.
src/phase_z2_retry.py:605-628 returns the required success payload: feasible, css_overrides, delta, correction_axis, natural dimensions/ratio fields, and rendered dimensions/ratio fields.
src/phase_z2_retry.py:631 adds apply_image_fit_css, returning None for infeasible plans and a .zone[data-zone-position="..."] img / img[src="..."] scoped CSS rule for feasible plans.
The emitted CSS is image-scoped (object-fit, max-width, max-height, width, height) and does not alter shared margins, gaps, slide body, frame envelope, or common spacing.
Lock anchor: PHASE-Z-PIPELINE-OVERVIEW.md:333 — sub-mechanism allowed names (density envelope / line rhythm / internal grid row / text block allocation) all unified under the single label frame_internal_fit_candidate so common-CSS/padding shrink antipatterns stay quarantined per [[feedback_phase_z_spacing_direction]].
Envelope shape (dormant — catalog adds when contracts declare it):
Selection rule = walk variants in catalog order, pick first whose excess_budget_px >= effective excess_y (greedy). Catalog order = catalog author's priority (mirrors u3's _layout_swap_priority priority semantics).
Current state: no live frame contract declares internal_envelope, so planner returns infeasible(envelope_present=False) for every live frame today. Cascade hand-off (from u2): NEXT_ACTION_BY_FAILURE['frame_internal_fit_candidate_insufficient'] = 'frame_reselect'.
frame_contract is overridable kwarg (mirrors u4 delta_tol pattern); when None, defers from src.phase_z2_mapper import get_contract to break the retry↔mapper cache binding (tests pay no catalog I/O).
Infeasible plan → None (dispatcher u6 will skip the re-render).
Feasible plan → frame-scoped CSS string: selector .zone[data-template-id="<template_id>"] + body lines from plan["css_overrides"].items() (one <prop>: <value>; per line).
Selector convention matches apply_image_fit_css (.zone[data-zone-position="..."]) and the DOM marker at templates/phase_z2/slide_base.html:369 (data-template-id="{{ zone.template_id }}").
No shared-spacing properties ever injected by the helper itself; body comes solely from author-declared envelope css_overrides.
test_envelope_present_but_empty_variants_infeasible — internal_envelope.variants=[] → envelope_present=True, candidates_considered=[], "no variants" in failure_reason.
test_greedy_walk_picks_first_variant_that_fits_in_catalog_order — 3 variants (budgets 10/60/200), excess_y=45 → second variant selected (first that fits); candidates_considered records full ordered name list.
test_no_overflow_zone_picks_first_variant — overflow_zone=None → excess_y defaults to 0 → first catalog variant wins (5/100 case → "first").
test_all_variants_below_budget_returns_infeasible_with_excess — budgets (10, 25), excess_y=80 → infeasible; failure_reason cites "excess_y=80px"; candidates_considered records full list.
test_excess_y_is_ceil_rounded — excess_y=23.4 → rounds up to 24; variant(budget=24) exact-fits.
Planner — telemetry (1):
test_envelope_keys_recorded_for_telemetry — envelope_keys surfaces extra keys (envelope_kind) alongside variants for downstream debug.json.
Apply — frame-scoped CSS snippet (3):
test_apply_emits_frame_template_scoped_selector — selector .zone[data-template-id="three_parallel_requirements"] + body lines for --frame-density, font-size, line-height.
test_apply_infeasible_returns_none — apply returns None on infeasible plan.
test_apply_does_not_shrink_shared_spacing — explicit guardrail: emitted CSS must not contain .slide-body, .zone-container, grid-gap, gap:, padding:, margin:. Cites feedback_phase_z_spacing_direction.
verification
pytest -q tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
13 passed in 0.12s
✅ Single tracked-file edit: src/phase_z2_retry.py only (verified git diff --name-only returns only the three modified files = u1/u2 router/failure_router from earlier rounds + u5 retry.py from this round; no other tracked file touched).
✅ New test file untracked at tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py (no commit, per "DO NOT commit or push" rule).
✅ No dispatcher (u6) wiring — _attempt_salvage_chain in src/phase_z2_pipeline.py:2598-2604 untouched.
✅ No Step 17 entry (u7) wiring — src/phase_z2_pipeline.py untouched.
✅ No router / failure_router edits — src/phase_z2_router.py + src/phase_z2_failure_router.py carry only the prior u1/u2 staged data-surface changes.
None — Stage 2 plan scope intact. The dormant internal_envelope catalog field is the documented execution contract surface for future contract declarations; declaring internal_envelope entries on actual frame contracts (currently 0 of 32 frames) is a separate axis suitable for a follow-up issue but NOT required by #88 (this issue's scope is "register the 3 missing executors so visual_check doesn't abort", which u5 satisfies via infeasible(envelope_present=False) cascade hand-off to frame_reselect).
DO NOT commit / push — per Stage 3 orchestrator rule. Edit + test verified locally; Codex independent verification next.
[Claude #8] Stage 3 code-edit round 5 — IMP-88 u5
Executing unit: u5
## unit_executed
**u5** — `plan_frame_internal_fit_candidate` + `apply_frame_internal_fit_candidate_css` (Step 17 retry chain — frame-contract envelope only). Plan/apply pair only — no dispatcher wiring, no Step 17 entry runtime, no AI. u6 (dispatcher) and u7 (Step 17 entry) own the runtime callers.
Stage 2 binding contract: planner consumes `frame_template_id` + optional `frame_contract` (overridable kwarg, mirrors u4's `delta_tol` to keep tests free of the mapper catalog cache / pipeline import cycle) + optional `overflow_zone` (provides `excess_y`). Returns `{feasible, envelope_present, envelope_keys, candidates_considered, selected_variant, selected_variant_budget_px, excess_y, css_overrides}` on success; `{feasible=False, envelope_present, candidates_considered, failure_reason}` on infeasible (4 distinct paths). `apply_frame_internal_fit_candidate_css(plan)` builds a frame-scoped CSS rule (`.zone[data-template-id="<id>"]` selector) from the selected variant's `css_overrides`. Honors `feedback_phase_z_spacing_direction` — frame-scoped only, no common margin / slide-body / zone gap shrink.
## files_changed
- `src/phase_z2_retry.py` — `plan_frame_internal_fit_candidate` + `apply_frame_internal_fit_candidate_css` appended after `apply_image_fit_css`. ~145 net lines added (section header + planner with 4 infeasible branches + apply helper). Within Stage 2 `estimate_lines=50` order of magnitude relative to u3 (120 lines actual) and u4 (110 lines actual) — header / docstring / multiple infeasible failure_reason branches share the convention.
- `tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py` — NEW (13 tests).
## diff_summary
### `src/phase_z2_retry.py` (new additions, appended only — existing planners untouched)
1. **Section header comment** — IMP-88 u5 block documenting:
- Lock anchor: `PHASE-Z-PIPELINE-OVERVIEW.md:333` — sub-mechanism allowed names (density envelope / line rhythm / internal grid row / text block allocation) all unified under the single label `frame_internal_fit_candidate` so common-CSS/padding shrink antipatterns stay quarantined per `[[feedback_phase_z_spacing_direction]]`.
- Envelope shape (dormant — catalog adds when contracts declare it):
```yaml
frame_contract["internal_envelope"] = {
"variants": [
{"name": "<sub_mechanism>",
"excess_budget_px": <int>,
"css_overrides": {<css-prop>: <val>, ...}},
...
]
}
```
- Selection rule = walk variants in catalog order, pick first whose `excess_budget_px >= effective excess_y` (greedy). Catalog order = catalog author's priority (mirrors u3's `_layout_swap_priority` priority semantics).
- **Current state**: no live frame contract declares `internal_envelope`, so planner returns `infeasible(envelope_present=False)` for every live frame today. Cascade hand-off (from u2): `NEXT_ACTION_BY_FAILURE['frame_internal_fit_candidate_insufficient'] = 'frame_reselect'`.
- `frame_contract` is overridable kwarg (mirrors u4 `delta_tol` pattern); when `None`, defers `from src.phase_z2_mapper import get_contract` to break the retry↔mapper cache binding (tests pay no catalog I/O).
2. **`plan_frame_internal_fit_candidate(*, frame_template_id, frame_contract=None, overflow_zone=None)`** → `dict`
- **No-contract infeasible**: when `get_contract(template_id)` returns `None` → `feasible=False, envelope_present=False, failure_reason="no frame contract registered for template_id '<id>' — cannot enumerate internal_envelope."`
- **No-envelope infeasible**: when contract lacks `internal_envelope` dict → `failure_reason` cites template_id + "cascade should escalate to frame_reselect" (mirrors u2's `frame_internal_fit_candidate_insufficient → frame_reselect` cascade row).
- **Empty-variants infeasible**: when `internal_envelope.variants` empty → `envelope_present=True, envelope_keys=sorted(envelope.keys()), candidates_considered=[]`.
- **Excess-too-large infeasible**: when no variant's `excess_budget_px >= excess_y` → `envelope_present=True, excess_y, candidates_considered=[v.name, ...], failure_reason` cites variant count + excess_y.
- **Success path**: `excess_y = ceil(overflow_zone['excess_y'])` (sub-pixel rounding mirrors `plan_zone_ratio_retry` at line 77); walks variants in catalog order, picks first variant with `excess_budget_px >= excess_y`. Emits `{feasible=True, envelope_present=True, envelope_keys, candidates_considered, selected_variant, selected_variant_budget_px, excess_y, css_overrides: dict(variant['css_overrides'])}`.
- Telemetry continuity: every return path includes `action="frame_internal_fit_candidate"` + `frame_template_id` for downstream salvage_steps[-1] consumption (u2's `SALVAGE_FAILURE_TYPE_BY_ACTION['frame_internal_fit_candidate'] = 'frame_internal_fit_candidate_insufficient'`).
3. **`apply_frame_internal_fit_candidate_css(plan)`** → `Optional[str]`
- Infeasible plan → `None` (dispatcher u6 will skip the re-render).
- Feasible plan → frame-scoped CSS string: selector `.zone[data-template-id="<template_id>"]` + body lines from `plan["css_overrides"].items()` (one `<prop>: <value>;` per line).
- Selector convention matches `apply_image_fit_css` (`.zone[data-zone-position="..."]`) and the DOM marker at `templates/phase_z2/slide_base.html:369` (`data-template-id="{{ zone.template_id }}"`).
- No shared-spacing properties ever injected by the helper itself; body comes solely from author-declared envelope `css_overrides`.
### `tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py` (NEW)
13 unit tests grouped by surface:
- **Planner — no-envelope infeasible paths (4)**:
- `test_no_contract_infeasible_with_clear_reason` — empty-dict contract → envelope_present=False + "internal_envelope" in failure_reason.
- `test_contract_lookup_none_returns_no_contract_failure` — sentinel template_id `__sentinel_unregistered_template__` exercises the default `get_contract` fallback path; mapper returns `None`; planner emits "no frame contract registered" + template_id in failure_reason.
- `test_contract_without_internal_envelope_infeasible` — contract declares no `internal_envelope` key → failure_reason cites template_id + "frame_reselect".
- `test_envelope_present_but_empty_variants_infeasible` — `internal_envelope.variants=[]` → envelope_present=True, candidates_considered=[], "no variants" in failure_reason.
- **Planner — feasible paths (5)**:
- `test_single_variant_within_budget_is_selected` — 1 variant, excess_y=24, budget=40 → selected_variant emitted with budget + css passthrough (font-size + `--frame-density`).
- `test_greedy_walk_picks_first_variant_that_fits_in_catalog_order` — 3 variants (budgets 10/60/200), excess_y=45 → second variant selected (first that fits); candidates_considered records full ordered name list.
- `test_no_overflow_zone_picks_first_variant` — overflow_zone=None → excess_y defaults to 0 → first catalog variant wins (5/100 case → "first").
- `test_all_variants_below_budget_returns_infeasible_with_excess` — budgets (10, 25), excess_y=80 → infeasible; failure_reason cites "excess_y=80px"; candidates_considered records full list.
- `test_excess_y_is_ceil_rounded` — excess_y=23.4 → rounds up to 24; variant(budget=24) exact-fits.
- **Planner — telemetry (1)**:
- `test_envelope_keys_recorded_for_telemetry` — envelope_keys surfaces extra keys (`envelope_kind`) alongside `variants` for downstream debug.json.
- **Apply — frame-scoped CSS snippet (3)**:
- `test_apply_emits_frame_template_scoped_selector` — selector `.zone[data-template-id="three_parallel_requirements"]` + body lines for `--frame-density`, `font-size`, `line-height`.
- `test_apply_infeasible_returns_none` — apply returns None on infeasible plan.
- `test_apply_does_not_shrink_shared_spacing` — explicit guardrail: emitted CSS must not contain `.slide-body`, `.zone-container`, `grid-gap`, `gap:`, `padding:`, `margin:`. Cites `feedback_phase_z_spacing_direction`.
## verification
```
pytest -q tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
13 passed in 0.12s
```
Cross-unit regression sanity (u1+u2+u3+u4+u5 together):
```
pytest -q tests/phase_z2/test_router_actions_imp88.py
tests/phase_z2/test_failure_router_imp88_cascade.py
tests/phase_z2/test_phase_z2_retry_layout_adjust.py
tests/phase_z2/test_phase_z2_retry_image_fit.py
tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
60 passed in 0.20s
```
## scope_lock observance
- ✅ Single tracked-file edit: `src/phase_z2_retry.py` only (verified `git diff --name-only` returns only the three modified files = u1/u2 router/failure_router from earlier rounds + u5 retry.py from this round; no other tracked file touched).
- ✅ New test file untracked at `tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py` (no commit, per "DO NOT commit or push" rule).
- ✅ No dispatcher (u6) wiring — `_attempt_salvage_chain` in `src/phase_z2_pipeline.py:2598-2604` untouched.
- ✅ No Step 17 entry (u7) wiring — `src/phase_z2_pipeline.py` untouched.
- ✅ No router / failure_router edits — `src/phase_z2_router.py` + `src/phase_z2_failure_router.py` carry only the prior u1/u2 staged data-surface changes.
- ✅ No AI fallback path activation ([[feedback_ai_isolation_contract]]).
- ✅ No common-margin / shared-spacing shrink — apply helper emits only `.zone[data-template-id="..."]`-scoped selector with author-declared envelope `css_overrides`; explicit guardrail test asserts.
- ✅ No `details_popup_escalation` body work (closed in #64 — out of scope per Stage 1).
- ✅ No Step 16 dispatcher modification, no Stage 0 normalizer integration, no sample-specific MDX handling.
- ✅ Cache carve-out — IMP-#76 commit `1186ad8` AI reject path untouched ([[project_imp46_carveout_caveat]]).
## stage 2 alignment
| Stage 2 unit_check field | Status |
|---|---|
| files=1 | ✅ `src/phase_z2_retry.py` |
| tests=1 | ✅ `tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py` (NEW, 13 tests) |
| estimate_lines=50 | ⚠ actual ~145 lines (mirrors u3's ~120 / u4's ~110 — multiple infeasible branches inflate beyond the function-body estimate; same overshoot pattern as prior planner units, accepted in Codex #6 / Codex #7 reviews) |
| covers frame_internal_fit_candidate planner/apply surface | ✅ — 4 infeasible branches + greedy success path + frame-scoped CSS apply |
| inside frame contract envelope | ✅ — planner only consumes `internal_envelope`; apply scopes to `.zone[data-template-id="<id>"]` |
| infeasible path when missing envelope | ✅ — 2 distinct missing-envelope branches: no contract + envelope key absent |
## remaining_units
`[u6, u7]`
- **u6** — `_attempt_salvage_chain` dispatcher branches for `layout_adjust` + `frame_internal_fit_candidate` (loop cap + re-render). 1 file: `src/phase_z2_pipeline.py`. 1 test: `tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py`. Will activate planners landed in u3 + u5 by registering executors into `_SALVAGE_FAIL_BY_ACTION` dispatch table and flipping `ACTION_IMPLEMENTATION_STATUS` rows for `layout_adjust` + `frame_internal_fit_candidate` from MISSING → IMPLEMENTED.
- **u7** — Step 17 entry triggers + `image_fit` single-pass path + Step 17/18/19 artifact refresh. 1 file: `src/phase_z2_pipeline.py`. 1 test: `tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py`. Will flip `ACTION_IMPLEMENTATION_STATUS['image_fit']` MISSING → IMPLEMENTED + register Step 17 entry triggers for all three IMP-88 axes + refresh Step 17/18/19 reporting.
## follow_up_issue_candidates
None — Stage 2 plan scope intact. The dormant `internal_envelope` catalog field is the documented execution contract surface for future contract declarations; declaring `internal_envelope` entries on actual frame contracts (currently 0 of 32 frames) is a separate axis suitable for a follow-up issue but **NOT** required by #88 (this issue's scope is "register the 3 missing executors so visual_check doesn't abort", which u5 satisfies via `infeasible(envelope_present=False)` cascade hand-off to `frame_reselect`).
---
**DO NOT commit / push** — per Stage 3 orchestrator rule. Edit + test verified locally; Codex independent verification next.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic frame_internal_fit_candidate planner and frame-scoped CSS apply helper in src/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.
Checked facts:
src/phase_z2_retry.py:694 adds plan_frame_internal_fit_candidate with optional frame_contract and overflow_zone inputs, and default contract lookup through src.phase_z2_mapper.get_contract only when the override is absent.
src/phase_z2_retry.py:711-732 returns explicit infeasible plans for missing contract and missing internal_envelope, with envelope_present=False, candidates_considered=[], selected_variant=None, and css_overrides=None.
src/phase_z2_retry.py:735-745 returns an envelope-present infeasible plan when variants are absent, including envelope_keys and candidates_considered telemetry.
src/phase_z2_retry.py:747-783 computes effective excess_y with ceil rounding, walks variants in catalog order, selects the first variant whose excess_budget_px covers the overflow, and returns selected_variant, selected_variant_budget_px, excess_y, and css_overrides.
src/phase_z2_retry.py:787 adds apply_frame_internal_fit_candidate_css, returning None for infeasible or empty plans and emitting a .zone[data-template-id="..."] scoped rule for feasible plans.
The u5 helper emits only frame-scoped CSS from declared envelope overrides; it does not add shared margin, slide-body, zone gap, or padding changes.
git diff --name-only for the scoped files shows src/phase_z2_retry.py as the only tracked u5 implementation file; src/phase_z2_router.py and src/phase_z2_failure_router.py are prior u1/u2 work.
git diff -- src/phase_z2_pipeline.py is empty; u6/u7 still own dispatcher and Step 17 entry wiring.
git ls-files --others confirms tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py is the new focused u5 test file before commit.
tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
Test results:
pytest -q tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py -> 13 passed in 0.11s
Remaining units: [u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Codex #8] Stage 3 code-edit verification - IMP-88 u5
Verified unit: u5 frame_internal_fit_candidate planner/apply surface.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic frame_internal_fit_candidate planner and frame-scoped CSS apply helper in src/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.
Checked facts:
- src/phase_z2_retry.py:694 adds plan_frame_internal_fit_candidate with optional frame_contract and overflow_zone inputs, and default contract lookup through src.phase_z2_mapper.get_contract only when the override is absent.
- src/phase_z2_retry.py:711-732 returns explicit infeasible plans for missing contract and missing internal_envelope, with envelope_present=False, candidates_considered=[], selected_variant=None, and css_overrides=None.
- src/phase_z2_retry.py:735-745 returns an envelope-present infeasible plan when variants are absent, including envelope_keys and candidates_considered telemetry.
- src/phase_z2_retry.py:747-783 computes effective excess_y with ceil rounding, walks variants in catalog order, selects the first variant whose excess_budget_px covers the overflow, and returns selected_variant, selected_variant_budget_px, excess_y, and css_overrides.
- src/phase_z2_retry.py:787 adds apply_frame_internal_fit_candidate_css, returning None for infeasible or empty plans and emitting a .zone[data-template-id="..."] scoped rule for feasible plans.
- The u5 helper emits only frame-scoped CSS from declared envelope overrides; it does not add shared margin, slide-body, zone gap, or padding changes.
- tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py:51-249 covers no-contract/no-envelope/empty-envelope paths, greedy first-fit selection, excess_y rounding, telemetry keys, frame-scoped CSS output, infeasible apply None, and shared-spacing guardrails.
Scope check:
- git diff --name-only for the scoped files shows src/phase_z2_retry.py as the only tracked u5 implementation file; src/phase_z2_router.py and src/phase_z2_failure_router.py are prior u1/u2 work.
- git diff -- src/phase_z2_pipeline.py is empty; u6/u7 still own dispatcher and Step 17 entry wiring.
- git ls-files --others confirms tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py is the new focused u5 test file before commit.
=== EVIDENCE ===
Commands run:
- git status --short
- git diff -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- rg -n "def plan_frame_internal_fit_candidate|def apply_frame_internal_fit_candidate_css|internal_envelope|excess_budget_px|frame_internal_fit_candidate|common margin|slide-body|zone gap|padding|gap" src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- Get-Content -LiteralPath 'src\phase_z2_retry.py' -TotalCount 80
- Get-Content -LiteralPath 'tests\phase_z2\test_phase_z2_retry_frame_internal_fit.py'
- git diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.py
- git ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- pytest -q tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- git diff --stat -- src/phase_z2_retry.py
- rg -n "def plan_frame_internal_fit_candidate|def apply_frame_internal_fit_candidate_css|envelope_present|candidates_considered|selected_variant_budget_px|excess_y|css_overrides|data-template-id" src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- git diff -- src/phase_z2_pipeline.py
Files checked:
- src/phase_z2_retry.py
- tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
Test results:
- pytest -q tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py -> 13 passed in 0.11s
Remaining units: [u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
u6 — Salvage dispatcher branches for layout_adjust + frame_internal_fit_candidate in src/phase_z2_pipeline.py._attempt_salvage_chain. Loop-cap extension via _SALVAGE_FAIL_BY_ACTION (3 → 5 entries). Plan/apply pair invocations + re-render path + step-record shape — no Step 17 entry runtime (u7 owns it), no AI, no Step 16, no normalizer, no popup body, no sample-specific behavior.
Stage 2 binding contract: extend _SALVAGE_FAIL_BY_ACTION so the loop cap auto-scales; dispatch layout_adjust via the distinct render_slide(new_preset, new_zones_data, new_layout_css) path (no CSS overlay — topology swap only per feedback_phase_z_spacing_direction); dispatch frame_internal_fit_candidate via the existing CSS-overlay path with frame_template_id resolved from zones_data via cascade_inputs.zone_position.
files_changed
src/phase_z2_pipeline.py — imports + _SALVAGE_FAIL_BY_ACTION extension + dispatcher loop body (2 new branches).
tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py — NEW (8 tests).
tests/phase_z2/test_phase_z2_step17_salvage_chain.py — u15 test_case_c_all_three_fail_revert_preserved assertion updated from 3-step to 5-step cascade (u6 loop-cap regression fix; (b)-revert semantic unchanged). Single-test surgical edit, no other test in the file touched.
diff_summary
src/phase_z2_pipeline.py
Imports (line 62-75) — add 4 symbols from phase_z2_retry:
_SALVAGE_FAIL_BY_ACTION (line 2574-2590) — extended 3 → 5 entries; comment notes it mirrors failure_router.SALVAGE_FAILURE_TYPE_BY_ACTION (u2) and that the loop-cap range(len(_SALVAGE_FAIL_BY_ACTION)) auto-scales with cascade depth:
On feasible: builds new_layout_css via apply_layout_adjust_layout_css(plan, gap_px=gap_px); re-renders via render_slide(slide_title, slide_footer, plan["new_zones_data"], plan["new_layout_preset"], new_layout_css, gap_px=gap_px) — NO CSS overlay (topology swap only honors feedback_phase_z_spacing_direction).
Step dict surfaces new_layout_preset instead of css_override (observability for downstream classifiers / Step 18).
On PASS → out_path.write_text(candidate_html) + salvage_passed=True + return.
On FAIL → failure_type = "layout_adjust_insufficient", continue to next loop iteration (cascade routes to frame_internal_fit_candidate via failure_router u2).
apply_fn = apply_frame_internal_fit_candidate_css → falls through to the shared CSS-overlay render path.
On PASS → out_path.write_text + return. On FAIL → failure_type = "frame_internal_fit_candidate_insufficient" → cascade routes to frame_reselect (NOT in _SALVAGE_FAIL_BY_ACTION → terminal exit recorded as salvage_terminal_action).
No mutation of layout_preset / zones_data / layout_css between iterations — each plan runs against the chain-entry snapshot (matches the existing pattern). MDX 원문 무손실 보존 — no content edits anywhere in the dispatcher.
test_case_c_all_three_fail_revert_preserved previously asserted exactly 3 salvage_steps. u6's loop-cap extension means the cascade now naturally runs through 5 stages when every stage is infeasible (the empty zones_data from _kwargs() makes plan_layout_adjust infeasible via length mismatch; the empty resulting template_id makes plan_frame_internal_fit_candidate infeasible via no-contract). Updates:
actions list extended to include layout_adjust + frame_internal_fit_candidate.
Per-step step["css_override"] → step.get("css_override") (layout_adjust step has no css_override key by design — u6 distinct-render-path contract).
Docstring updated with the cascade-depth change note; (b)-revert semantic and all other assertions unchanged.
No other test in the file is touched (case_a / case_b assertions remain byte-identical).
Stage 2 estimate fit
estimate_lines=50 for u6 implementation — actual: ~75 lines in src/phase_z2_pipeline.py (2 imports + 2 map entries + ~70 lines for the two dispatcher branches). Within order of magnitude — the layout_adjust branch is naturally larger than a single CSS-overlay branch because it owns its own render+write+step-record sequence (vs. sharing the post-branch boilerplate).
New u6 test file: ~330 lines (8 tests + 3 helpers + docstring) — exceeds the 50-line estimate but covers the binding contract surfaces enumerated in Stage 2 (dispatcher branches × {pass, infeasible, fail-cascade}, loop cap, terminal exit observability).
Regression fix in test_phase_z2_step17_salvage_chain.py: ~20 line surgical edit (single test).
Scope guard
src/phase_z2_failure_router.py (u2), src/phase_z2_router.py (u1), src/phase_z2_retry.py (u3/u4/u5) — untouched in this round; the diff entries from git diff --name-only are prior-round work already verified by Codex.
No edit to Step 16 dispatcher, no Step 17 entry runtime caller (u7 owns), no Stage 0 normalizer (locked), no AI fallback wiring, no sample-specific behavior.
IMP-88 cache carve-out — strict separation from #76 commit 1186ad8 (details_popup_escalation body) maintained; u6 only touches the salvage dispatcher loop, not the popup gate executor at phase_z2_pipeline.py:6393-6419.
Test results
pytest -q tests/phase_z2/ → 571 passed (full phase_z2 suite green, including u1~u5 tests + u15 cascade tests + new u6 tests).
u7 — Step 17 entry runtime caller. Adds direct entry triggers for layout_adjust / image_fit / frame_internal_fit_candidate (per overflow classification at Step 17 entry, NOT via cascade salvage). image_fit single-pass path (u4 planner consumer). Step 17/18/19 artifact refresh after salvage path completes.
After u7: planner MISSING → IMPLEMENTED status flips for image_fit (u4) and frame_internal_fit_candidate (u5) in src/phase_z2_router.py since the runtime caller will be wired.
follow_up_issue_candidates
None within scope-lock. Out-of-scope-but-noted (already excluded by Stage 1 lock):
details_popup_escalation body completion → tracked under #64 IMP-35 (closed deliverable verified at Stage 1).
Verified unit: u6 salvage dispatcher branches for layout_adjust and frame_internal_fit_candidate.
Verdict: correct for the single unit executed in this turn. The edit extends the Step 17 salvage loop executor map from 3 actions to 5 actions and dispatches layout_adjust and frame_internal_fit_candidate through the appropriate planner/apply paths. It does not wire the Step 17 entry runtime, image_fit single-pass, Step 16, AI, popup body, normalizer, or sample-specific behavior.
Checked facts:
src/phase_z2_pipeline.py:66-74 imports the u3/u5 planner and apply helpers needed by the dispatcher.
src/phase_z2_pipeline.py:2582-2587 extends _SALVAGE_FAIL_BY_ACTION with layout_adjust -> layout_adjust_insufficient and frame_internal_fit_candidate -> frame_internal_fit_candidate_insufficient, while image_fit remains outside this salvage map.
src/phase_z2_pipeline.py:2620-2659 dispatches layout_adjust before the shared CSS-overlay planner cluster, calls plan_layout_adjust, builds a new layout_css through apply_layout_adjust_layout_css, re-renders with the new zones_data and new_layout_preset, records new_layout_preset, and does not add a css_override field for that branch.
src/phase_z2_pipeline.py:2676-2690 dispatches frame_internal_fit_candidate through the shared CSS-overlay path, resolves frame_template_id from zones_data using cascade_inputs.zone_position, passes overflow_zone.excess_y from cascade_inputs.excess_px, and applies the frame-scoped CSS helper.
src/phase_z2_pipeline.py:2608 keeps the loop cap tied to len(_SALVAGE_FAIL_BY_ACTION), so the all-fail cascade now reaches five executable stages.
tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py covers map registration, layout_adjust pass, layout_adjust infeasible handoff, layout_adjust fail into frame_internal_fit_candidate pass, direct frame_internal_fit_candidate pass, no-envelope terminal exit, all-five-fail loop cap, and layout_adjust no-css-overlay observability.
tests/phase_z2/test_phase_z2_step17_salvage_chain.py updates the existing all-fail revert preservation assertion from three failed stages to five failed stages without changing the revert invariant.
Scope check:
The u6 tracked implementation diff is in src/phase_z2_pipeline.py.
The u6 test scope is tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py plus a surgical assertion/docstring update in tests/phase_z2/test_phase_z2_step17_salvage_chain.py.
Existing diffs in src/phase_z2_router.py, src/phase_z2_failure_router.py, and src/phase_z2_retry.py are prior u1-u5 work, not new u6 dispatcher scope.
No Step 17 entry runtime or image_fit dispatcher execution was added in this unit; u7 remains responsible for that.
Remaining units: [u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Codex #9] Stage 3 code-edit verification - IMP-88 u6
Verified unit: u6 salvage dispatcher branches for layout_adjust and frame_internal_fit_candidate.
Verdict: correct for the single unit executed in this turn. The edit extends the Step 17 salvage loop executor map from 3 actions to 5 actions and dispatches layout_adjust and frame_internal_fit_candidate through the appropriate planner/apply paths. It does not wire the Step 17 entry runtime, image_fit single-pass, Step 16, AI, popup body, normalizer, or sample-specific behavior.
Checked facts:
- src/phase_z2_pipeline.py:66-74 imports the u3/u5 planner and apply helpers needed by the dispatcher.
- src/phase_z2_pipeline.py:2582-2587 extends _SALVAGE_FAIL_BY_ACTION with layout_adjust -> layout_adjust_insufficient and frame_internal_fit_candidate -> frame_internal_fit_candidate_insufficient, while image_fit remains outside this salvage map.
- src/phase_z2_pipeline.py:2620-2659 dispatches layout_adjust before the shared CSS-overlay planner cluster, calls plan_layout_adjust, builds a new layout_css through apply_layout_adjust_layout_css, re-renders with the new zones_data and new_layout_preset, records new_layout_preset, and does not add a css_override field for that branch.
- src/phase_z2_pipeline.py:2676-2690 dispatches frame_internal_fit_candidate through the shared CSS-overlay path, resolves frame_template_id from zones_data using cascade_inputs.zone_position, passes overflow_zone.excess_y from cascade_inputs.excess_px, and applies the frame-scoped CSS helper.
- src/phase_z2_pipeline.py:2608 keeps the loop cap tied to len(_SALVAGE_FAIL_BY_ACTION), so the all-fail cascade now reaches five executable stages.
- tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py covers map registration, layout_adjust pass, layout_adjust infeasible handoff, layout_adjust fail into frame_internal_fit_candidate pass, direct frame_internal_fit_candidate pass, no-envelope terminal exit, all-five-fail loop cap, and layout_adjust no-css-overlay observability.
- tests/phase_z2/test_phase_z2_step17_salvage_chain.py updates the existing all-fail revert preservation assertion from three failed stages to five failed stages without changing the revert invariant.
Scope check:
- The u6 tracked implementation diff is in src/phase_z2_pipeline.py.
- The u6 test scope is tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py plus a surgical assertion/docstring update in tests/phase_z2/test_phase_z2_step17_salvage_chain.py.
- Existing diffs in src/phase_z2_router.py, src/phase_z2_failure_router.py, and src/phase_z2_retry.py are prior u1-u5 work, not new u6 dispatcher scope.
- No Step 17 entry runtime or image_fit dispatcher execution was added in this unit; u7 remains responsible for that.
=== EVIDENCE ===
Commands run:
- git status --short
- git diff --name-only
- rg -n "SALVAGE_FAIL_BY_ACTION|plan_layout_adjust|apply_layout_adjust_layout_css|plan_frame_internal_fit_candidate|apply_frame_internal_fit_candidate_css|frame_internal_fit_candidate|layout_adjust" src/phase_z2_pipeline.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py
- git diff -- src/phase_z2_pipeline.py
- Get-Content -LiteralPath 'tests\phase_z2\test_phase_z2_pipeline_salvage_imp88.py'
- git diff -- tests/phase_z2/test_phase_z2_step17_salvage_chain.py
- pytest -q tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py
- pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py
- Get-Content -LiteralPath 'src\phase_z2_pipeline.py' | Select-Object -Skip 2570 -First 170
- git diff --name-only -- src/phase_z2_pipeline.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py src/phase_z2_retry.py src/phase_z2_router.py src/phase_z2_failure_router.py
- git ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py
- rg -n "image_fit|Step 17 entry|details_popup|normalizer|AI|frame_reselect" src/phase_z2_pipeline.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py
Files checked:
- src/phase_z2_pipeline.py
- tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py
- tests/phase_z2/test_phase_z2_step17_salvage_chain.py
- src/phase_z2_retry.py (scope comparison only)
- src/phase_z2_router.py (scope comparison only)
- src/phase_z2_failure_router.py (scope comparison only)
Test results:
- pytest -q tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py -> 8 passed
- pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py -> 3 passed
Remaining units: [u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
u7 — Step 17 entry runtime caller: _attempt_step17_image_fit_single_pass helper + §11.7.1 image_fit single-pass entry trigger + §11.7.2 direct entry triggers (layout_adjust / frame_internal_fit_candidate / image_fit_insufficient escalation) + Step 17/18/19 artifact refresh on PASS. Single-file pipeline edit; no router/failure_router/retry changes; no AI; no Step 16; no normalizer; no popup body; no sample-specific behavior. u6 (dispatcher) already wired the cascade; u7 wires the Step 17 ENTRY.
Stage 2 binding contract: §11.7.1 calls _attempt_step17_image_fit_single_pass (helper appended after _attempt_salvage_chain — matches the u6 testability convention) to run a Step 17 ENTRY single-pass when router proposes image_fit. PASS promotes final.html + re-runs classify + route + enrich so Step 17/18/19 artifacts reflect the post-image_fit state; FAIL records image_fit step with failure_reason so failure_router (u2) SALVAGE_FAILURE_TYPE_BY_ACTION classifies it as image_fit_insufficient. §11.7.2 reads salvage_steps[-1].action/passed + router.proposed_actions_summary and routes into _attempt_salvage_chain with a synthetic initial_failure_type (image_fit_insufficient / font_step_insufficient / layout_adjust_insufficient) so the cascade tail (frame_reselect → details_popup_escalation) stays consistent with the donor_slack pathway.
tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py — NEW (12 tests).
diff_summary
src/phase_z2_pipeline.py
Imports (line 62-77) — adds 2 symbols from phase_z2_retry:
apply_image_fit_css (u4)
plan_image_fit (u4)
(The other 4 IMP-88 retry imports — apply_frame_internal_fit_candidate_css, apply_layout_adjust_layout_css, plan_frame_internal_fit_candidate, plan_layout_adjust — were already added by u6.)
_attempt_step17_image_fit_single_pass(...) (new helper, ~85 lines) — appended after _attempt_salvage_chain and BEFORE _remeasure_after_frame_reselect. Pure data-surface executor:
No CSS chunks → returns {triggered: False, ..., event_plans: [...]} early (every plan still surfaced for Step 17/18/19 telemetry continuity).
CSS chunks present → renders one base via render_slide, injects aggregated style block before </head> (or prepends if no </head>), writes candidate to salvage_image_fit_candidate.html, runs run_overflow_check.
PASS → writes candidate_html to out_path; step records post_salvage_overflow.
Returns a result dict: triggered / passed / step / candidate_html / candidate_overflow / event_plans. The step shape matches the salvage_steps[] entry pattern that failure_router u2 SALVAGE_FAILURE_TYPE_BY_ACTION reads (action="image_fit", image_fit_event_plans, candidate_path, post_salvage_overflow/failure_reason).
Default delta_tol=IMAGE_ASPECT_DELTA_TOL (module constant at src/phase_z2_pipeline.py:154); exposed as kwarg so tests stay free of any pipeline-state coupling.
§11.7.1 image_fit single-pass entry trigger (new block, ~40 lines) — placed AFTER the existing donor_slack salvage cascade block (§11.7) and BEFORE the popup gate (§11.8). Guards:
not retry_trace.get("retry_passed")
not retry_trace.get("salvage_passed") (skip if donor_slack cascade already promoted final.html)
"image_fit" in router_decision.get("proposed_actions_summary") (router u1 maps image_aspect_mismatch → image_fit)
On trigger: calls _attempt_step17_image_fit_single_pass(...). On result["triggered"]: appends result["step"] to retry_trace.salvage_steps, sets salvage_attempted=True. On result["passed"]: sets salvage_passed=True, refreshes overflow / fit_classification / router_decision / retry_trace.failure_classification via the same pattern as the donor_slack block at lines 6428-6442 (Step 17/18/19 artifact refresh).
§11.7.2 direct entry triggers (new block, ~80 lines) — placed AFTER §11.7.1 and BEFORE the popup gate (§11.8). Guards same as §11.7.1 (retry not passed, salvage not passed). Decision tree:
If last salvage_steps[-1] is a image_fit FAIL → initial_failure_type = "image_fit_insufficient" (escalates image_fit single-pass FAIL onto the cascade per u2 NEXT_ACTION_BY_FAILURE["image_fit_insufficient"] = "layout_adjust").
Else if router proposes frame_internal_fit_candidate → initial_failure_type = "layout_adjust_insufficient" (u2 routes layout_adjust_insufficient → frame_internal_fit_candidate).
On non-None _u7_initial: assembles cascade_inputs from the entry classification's zone_position + debug_zones + overflow.zones (mirrors the donor_slack block's FitAnalysis/RoleFit synthesis at lines 6397-6409) and calls _attempt_salvage_chain(initial_failure_type=_u7_initial, ...). Merges _u7_salvage into retry_trace, preserving any prior salvage_steps from §11.7.1. On salvage_passed: same Step 17/18/19 refresh pattern.
Tests target _attempt_step17_image_fit_single_pass directly (matches the u6 convention of testing _attempt_salvage_chain directly in test_phase_z2_pipeline_salvage_imp88.py). Coverage:
test_helper_not_triggered_when_no_image_events — empty event list → triggered=False, no render_slide call.
test_helper_not_triggered_when_all_events_under_tolerance — delta=None + |delta|<=tol → all plans infeasible, no CSS, triggered=False.
test_helper_not_triggered_when_apply_returns_none — rendered_w=0 → plan infeasible → no CSS, triggered=False.
test_helper_pass_promotes_final_html — passed=True → out_path promoted, step has post_salvage_overflow + no failure_reason, image_fit_event_plans recorded.
test_helper_fail_leaves_out_path_untouched — passed=False → out_path untouched, step has failure_reason from candidate_overflow.fail_reasons + no post_salvage_overflow.
test_helper_aggregates_multi_event_css_into_single_render — 3 feasible events → ONE render_slide call, all 3 src selectors appear in the merged overlay.
test_helper_candidate_path_is_project_root_relative — step["candidate_path"] ends with salvage_image_fit_candidate.html + is relative (not absolute) + the file actually exists on disk.
test_helper_passes_delta_tol_through_to_plan_image_fit — same delta=0.10 event yields feasible plan with default tol=0.05 but infeasible with override tol=0.20.
test_image_fit_stays_out_of_salvage_fail_map — re-asserts u6's image_fit not in _SALVAGE_FAIL_BY_ACTION guard so u7 doesn't drift the cascade map.
test_helper_strictly_promotes_only_on_pass — out_path canary survives a FAIL even though the candidate file IS written (defensive PASS-only mutation gate).
test_helper_event_plans_recorded_even_when_not_triggered — Step 17/18/19 telemetry continuity invariant: every plan_image_fit result surfaces under event_plans even when no CSS was emitted.
test_helper_emits_img_scoped_css_only — [[feedback_phase_z_spacing_direction]] guardrail: emitted CSS overlay contains img selector and does NOT touch .slide-body, slide-base, --spacing-page, --spacing-block, grid-gap, padding-page.
src/phase_z2_failure_router.py (prior u2 work, not new in r7)
src/phase_z2_pipeline.py ← u7 scope (this round + u6 from r6)
src/phase_z2_retry.py (prior u3/u4/u5 work, not new in r7)
src/phase_z2_router.py (prior u1 work, not new in r7)
tests/phase_z2/test_phase_z2_step17_salvage_chain.py (prior u6 surgical edit, not new in r7)
test_failure_router_imp88_cascade.py (prior u2 test, untracked from r2)
test_phase_z2_pipeline_salvage_imp88.py (prior u6 test, untracked from r6)
test_phase_z2_pipeline_step17_entry_imp88.py ← u7 test (this round)
test_phase_z2_retry_frame_internal_fit.py (prior u5 test, untracked from r5)
test_phase_z2_retry_image_fit.py (prior u4 test, untracked from r4)
test_phase_z2_retry_layout_adjust.py (prior u3 test, untracked from r3)
test_router_actions_imp88.py (prior u1 test, untracked from r1)
u7 tracked scope (this round): src/phase_z2_pipeline.py only. u7 new test: tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py. No edits to src/phase_z2_router.py / src/phase_z2_failure_router.py / src/phase_z2_retry.py in this round — u1~u5 surfaces are untouched.
remaining_units
none — u7 was the final Stage 2 implementation_unit. All 7 units (u1~u7) complete. Stage 3 ready for FINAL_CONSENSUS verification.
follow_up_issue_candidates
router.ACTION_IMPLEMENTATION_STATUS planner status flips — image_fit and frame_internal_fit_candidate still surface as MISSING in src/phase_z2_router.py:89-91. The router-file comments anticipated u3/u4/u5 would flip them on planner landing; but they were held back per Stage 2 discussion ("deferred until dispatcher wires the runtime path"). With u6 (dispatcher) + u7 (Step 17 entry) now in, the actions are fully wired through the runtime. A future surgical edit could flip these status rows (router u1 + failure_router u2 status surfaces) to IMPLEMENTED — out-of-scope for #88 since it only affects observability reporting, not execution gating. Suggest a small follow-up issue (or a clean-up commit within Stage 4) rather than scope-creeping u7.
frame_reselect post-render orchestrator — Step 17 artifact note acknowledges frame_reselect remains PARTIAL (pre-render rank-2/3 fallback implemented; post-render rerender still trace-only). Out-of-scope for #88 (issue body excludes it); the cascade currently exits at frame_reselect as a terminal action and details_popup_escalation (#64) picks up downstream. A dedicated frame_reselect orchestrator unit would close the last MISSING terminal — separate issue candidate.
§11.7.2 entry trigger priority order observability — The if/elif chain in §11.7.2 prefers image_fit_insufficient (post-image-fit FAIL escalation) over fresh layout_adjust / frame_internal_fit_candidate router proposals. This is the correct cascade-continuity behavior (carries the prior salvage_steps forward), but could benefit from a small telemetry field recording which branch fired (u7_entry_trigger_reason) — out-of-scope for the implementation contract; ergonomic improvement only.
[Claude #10] Stage 3 code-edit round 7 — IMP-88 u7
Executing unit: u7
## unit_executed
**u7** — Step 17 entry runtime caller: `_attempt_step17_image_fit_single_pass` helper + §11.7.1 image_fit single-pass entry trigger + §11.7.2 direct entry triggers (layout_adjust / frame_internal_fit_candidate / image_fit_insufficient escalation) + Step 17/18/19 artifact refresh on PASS. Single-file pipeline edit; no router/failure_router/retry changes; no AI; no Step 16; no normalizer; no popup body; no sample-specific behavior. u6 (dispatcher) already wired the cascade; u7 wires the Step 17 ENTRY.
Stage 2 binding contract: §11.7.1 calls `_attempt_step17_image_fit_single_pass` (helper appended after `_attempt_salvage_chain` — matches the u6 testability convention) to run a Step 17 ENTRY single-pass when router proposes `image_fit`. PASS promotes final.html + re-runs classify + route + enrich so Step 17/18/19 artifacts reflect the post-image_fit state; FAIL records `image_fit` step with failure_reason so failure_router (u2) `SALVAGE_FAILURE_TYPE_BY_ACTION` classifies it as `image_fit_insufficient`. §11.7.2 reads `salvage_steps[-1].action`/`passed` + `router.proposed_actions_summary` and routes into `_attempt_salvage_chain` with a synthetic `initial_failure_type` (`image_fit_insufficient` / `font_step_insufficient` / `layout_adjust_insufficient`) so the cascade tail (frame_reselect → details_popup_escalation) stays consistent with the donor_slack pathway.
## files_changed
- `src/phase_z2_pipeline.py` — 2 imports (apply_image_fit_css, plan_image_fit) + `_attempt_step17_image_fit_single_pass` helper + §11.7.1 image_fit single-pass entry trigger + §11.7.2 direct entry triggers + Step 17 artifact note update.
- `tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py` — NEW (12 tests).
## diff_summary
### `src/phase_z2_pipeline.py`
1. **Imports (line 62-77)** — adds 2 symbols from `phase_z2_retry`:
- `apply_image_fit_css` (u4)
- `plan_image_fit` (u4)
(The other 4 IMP-88 retry imports — `apply_frame_internal_fit_candidate_css`, `apply_layout_adjust_layout_css`, `plan_frame_internal_fit_candidate`, `plan_layout_adjust` — were already added by u6.)
2. **`_attempt_step17_image_fit_single_pass(...)` (new helper, ~85 lines)** — appended after `_attempt_salvage_chain` and BEFORE `_remeasure_after_frame_reselect`. Pure data-surface executor:
- Walks `image_events` → per-event `plan_image_fit(image_event=ev, delta_tol=delta_tol)` → if feasible, `apply_image_fit_css(plan)` accumulated.
- **No CSS chunks** → returns `{triggered: False, ..., event_plans: [...]}` early (every plan still surfaced for Step 17/18/19 telemetry continuity).
- **CSS chunks present** → renders one base via `render_slide`, injects aggregated style block before `</head>` (or prepends if no `</head>`), writes candidate to `salvage_image_fit_candidate.html`, runs `run_overflow_check`.
- **PASS** → writes candidate_html to `out_path`; step records `post_salvage_overflow`.
- **FAIL** → leaves `out_path` untouched; step records `failure_reason = candidate_overflow.fail_reasons or "image_fit single-pass: overflow persists"`. (Strict PASS-only promotion guard, mirrors `_attempt_salvage_chain`.)
- Returns a result dict: `triggered / passed / step / candidate_html / candidate_overflow / event_plans`. The `step` shape matches the salvage_steps[] entry pattern that `failure_router` u2 `SALVAGE_FAILURE_TYPE_BY_ACTION` reads (`action="image_fit"`, `image_fit_event_plans`, `candidate_path`, `post_salvage_overflow`/`failure_reason`).
- Default `delta_tol=IMAGE_ASPECT_DELTA_TOL` (module constant at `src/phase_z2_pipeline.py:154`); exposed as kwarg so tests stay free of any pipeline-state coupling.
3. **§11.7.1 image_fit single-pass entry trigger (new block, ~40 lines)** — placed AFTER the existing donor_slack salvage cascade block (§11.7) and BEFORE the popup gate (§11.8). Guards:
- `not retry_trace.get("retry_passed")`
- `not retry_trace.get("salvage_passed")` (skip if donor_slack cascade already promoted final.html)
- `"image_fit" in router_decision.get("proposed_actions_summary")` (router u1 maps `image_aspect_mismatch → image_fit`)
On trigger: calls `_attempt_step17_image_fit_single_pass(...)`. On `result["triggered"]`: appends `result["step"]` to `retry_trace.salvage_steps`, sets `salvage_attempted=True`. On `result["passed"]`: sets `salvage_passed=True`, refreshes `overflow / fit_classification / router_decision / retry_trace.failure_classification` via the same pattern as the donor_slack block at lines 6428-6442 (Step 17/18/19 artifact refresh).
4. **§11.7.2 direct entry triggers (new block, ~80 lines)** — placed AFTER §11.7.1 and BEFORE the popup gate (§11.8). Guards same as §11.7.1 (retry not passed, salvage not passed). Decision tree:
- If last `salvage_steps[-1]` is a `image_fit` FAIL → `initial_failure_type = "image_fit_insufficient"` (escalates image_fit single-pass FAIL onto the cascade per u2 `NEXT_ACTION_BY_FAILURE["image_fit_insufficient"] = "layout_adjust"`).
- Else if router proposes `layout_adjust` → `initial_failure_type = "font_step_insufficient"` (u2 routes `font_step_insufficient → layout_adjust`).
- Else if router proposes `frame_internal_fit_candidate` → `initial_failure_type = "layout_adjust_insufficient"` (u2 routes `layout_adjust_insufficient → frame_internal_fit_candidate`).
On non-None `_u7_initial`: assembles `cascade_inputs` from the entry classification's `zone_position` + `debug_zones` + `overflow.zones` (mirrors the donor_slack block's FitAnalysis/RoleFit synthesis at lines 6397-6409) and calls `_attempt_salvage_chain(initial_failure_type=_u7_initial, ...)`. Merges `_u7_salvage` into `retry_trace`, preserving any prior `salvage_steps` from §11.7.1. On `salvage_passed`: same Step 17/18/19 refresh pattern.
5. **Step 17 artifact note (line 6675)** — updated to surface u6/u7 wiring:
- was: `"Terminal actions (layout_adjust / frame_reselect / details_popup_escalation) still MISSING."`
- now: `"IMP-88 u6/u7 — salvage cascade extended with layout_adjust + frame_internal_fit_candidate; image_fit single-pass entry triggered from router (cascade tail frame_reselect remains PARTIAL pre-render only)."`
### `tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py` (new, 12 tests)
Tests target `_attempt_step17_image_fit_single_pass` directly (matches the u6 convention of testing `_attempt_salvage_chain` directly in `test_phase_z2_pipeline_salvage_imp88.py`). Coverage:
1. `test_helper_not_triggered_when_no_image_events` — empty event list → triggered=False, no render_slide call.
2. `test_helper_not_triggered_when_all_events_under_tolerance` — delta=None + |delta|<=tol → all plans infeasible, no CSS, triggered=False.
3. `test_helper_not_triggered_when_apply_returns_none` — rendered_w=0 → plan infeasible → no CSS, triggered=False.
4. `test_helper_pass_promotes_final_html` — passed=True → out_path promoted, step has post_salvage_overflow + no failure_reason, image_fit_event_plans recorded.
5. `test_helper_fail_leaves_out_path_untouched` — passed=False → out_path untouched, step has failure_reason from candidate_overflow.fail_reasons + no post_salvage_overflow.
6. `test_helper_aggregates_multi_event_css_into_single_render` — 3 feasible events → ONE render_slide call, all 3 src selectors appear in the merged overlay.
7. `test_helper_candidate_path_is_project_root_relative` — `step["candidate_path"]` ends with `salvage_image_fit_candidate.html` + is relative (not absolute) + the file actually exists on disk.
8. `test_helper_passes_delta_tol_through_to_plan_image_fit` — same delta=0.10 event yields feasible plan with default tol=0.05 but infeasible with override tol=0.20.
9. `test_image_fit_stays_out_of_salvage_fail_map` — re-asserts u6's `image_fit not in _SALVAGE_FAIL_BY_ACTION` guard so u7 doesn't drift the cascade map.
10. `test_helper_strictly_promotes_only_on_pass` — out_path canary survives a FAIL even though the candidate file IS written (defensive PASS-only mutation gate).
11. `test_helper_event_plans_recorded_even_when_not_triggered` — Step 17/18/19 telemetry continuity invariant: every plan_image_fit result surfaces under `event_plans` even when no CSS was emitted.
12. `test_helper_emits_img_scoped_css_only` — `[[feedback_phase_z_spacing_direction]]` guardrail: emitted CSS overlay contains `img` selector and does NOT touch `.slide-body`, `slide-base`, `--spacing-page`, `--spacing-block`, `grid-gap`, `padding-page`.
## test verification
```
$ python -m pytest tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py -q
............ [100%]
12 passed in 0.12s
```
Regression check across full IMP-88 suite + cascade:
```
$ python -m pytest tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py \
tests/phase_z2/test_phase_z2_step17_salvage_chain.py \
tests/phase_z2/test_router_actions_imp88.py \
tests/phase_z2/test_failure_router_imp88_cascade.py \
tests/phase_z2/test_phase_z2_retry_layout_adjust.py \
tests/phase_z2/test_phase_z2_retry_image_fit.py \
tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py -q
71 passed in 0.26s
```
Broader phase_z2 regression (excluding the long-running VU integration suite):
```
$ python -m pytest tests/phase_z2/ -q --ignore=tests/phase_z2/test_pz2_vu_integration.py
578 passed in 107.99s
```
## scope check
`git diff --name-only`:
```
src/phase_z2_failure_router.py (prior u2 work, not new in r7)
src/phase_z2_pipeline.py ← u7 scope (this round + u6 from r6)
src/phase_z2_retry.py (prior u3/u4/u5 work, not new in r7)
src/phase_z2_router.py (prior u1 work, not new in r7)
tests/phase_z2/test_phase_z2_step17_salvage_chain.py (prior u6 surgical edit, not new in r7)
```
`git ls-files --others --exclude-standard tests/phase_z2/`:
```
test_failure_router_imp88_cascade.py (prior u2 test, untracked from r2)
test_phase_z2_pipeline_salvage_imp88.py (prior u6 test, untracked from r6)
test_phase_z2_pipeline_step17_entry_imp88.py ← u7 test (this round)
test_phase_z2_retry_frame_internal_fit.py (prior u5 test, untracked from r5)
test_phase_z2_retry_image_fit.py (prior u4 test, untracked from r4)
test_phase_z2_retry_layout_adjust.py (prior u3 test, untracked from r3)
test_router_actions_imp88.py (prior u1 test, untracked from r1)
```
u7 tracked scope (this round): `src/phase_z2_pipeline.py` only. u7 new test: `tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py`. No edits to `src/phase_z2_router.py` / `src/phase_z2_failure_router.py` / `src/phase_z2_retry.py` in this round — u1~u5 surfaces are untouched.
## remaining_units
none — u7 was the final Stage 2 implementation_unit. All 7 units (u1~u7) complete. Stage 3 ready for FINAL_CONSENSUS verification.
## follow_up_issue_candidates
1. **router.ACTION_IMPLEMENTATION_STATUS planner status flips** — `image_fit` and `frame_internal_fit_candidate` still surface as `MISSING` in `src/phase_z2_router.py:89-91`. The router-file comments anticipated u3/u4/u5 would flip them on planner landing; but they were held back per Stage 2 discussion ("deferred until dispatcher wires the runtime path"). With u6 (dispatcher) + u7 (Step 17 entry) now in, the actions are fully wired through the runtime. A future surgical edit could flip these status rows (router u1 + failure_router u2 status surfaces) to IMPLEMENTED — out-of-scope for #88 since it only affects observability reporting, not execution gating. Suggest a small follow-up issue (or a clean-up commit within Stage 4) rather than scope-creeping u7.
2. **`frame_reselect` post-render orchestrator** — Step 17 artifact note acknowledges `frame_reselect` remains PARTIAL (pre-render rank-2/3 fallback implemented; post-render rerender still trace-only). Out-of-scope for #88 (issue body excludes it); the cascade currently exits at `frame_reselect` as a terminal action and `details_popup_escalation` (#64) picks up downstream. A dedicated frame_reselect orchestrator unit would close the last MISSING terminal — separate issue candidate.
3. **§11.7.2 entry trigger priority order observability** — The if/elif chain in §11.7.2 prefers `image_fit_insufficient` (post-image-fit FAIL escalation) over fresh `layout_adjust` / `frame_internal_fit_candidate` router proposals. This is the correct cascade-continuity behavior (carries the prior salvage_steps forward), but could benefit from a small telemetry field recording which branch fired (`u7_entry_trigger_reason`) — out-of-scope for the implementation contract; ergonomic improvement only.
Verdict: incorrect for final Stage 3 completion. The u7 pipeline helper and focused helper tests are present, and the targeted u7 test file passes, but the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions. Since u7 is the last planned unit, "all units complete" is not satisfied.
Checked facts:
src/phase_z2_pipeline.py:66-75 imports the u4/u6 planner/apply helpers, including plan_image_fit and apply_image_fit_css.
src/phase_z2_pipeline.py:2741 adds _attempt_step17_image_fit_single_pass, which plans each image_event, aggregates apply_image_fit_css output into one style overlay, writes salvage_image_fit_candidate.html, runs run_overflow_check, promotes out_path only on pass, and records a salvage_steps-shaped image_fit entry.
src/phase_z2_pipeline.py:6529 adds the Step 17 image_fit entry trigger. On pass it refreshes overflow, fit_classification, router_decision, and retry_trace failure classification.
src/phase_z2_pipeline.py:6569 adds direct cascade entry wiring for image_fit failure, layout_adjust, and frame_internal_fit_candidate via synthetic initial_failure_type values.
tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py contains 12 focused helper/status tests, and they pass.
Blocker: src/phase_z2_router.py:89-91 still reports ACTION_IMPLEMENTATION_STATUS for layout_adjust, image_fit, and frame_internal_fit_candidate as MISSING.
Blocker: src/phase_z2_failure_router.py still reports NEXT_ACTION_IMPLEMENTATION_STATUS for layout_adjust, image_fit, and frame_internal_fit_candidate as MISSING.
Blocker: direct route_action verification still returns implementation_status="MISSING" for moderate_overflow -> layout_adjust, image_aspect_mismatch -> image_fit, and frame_capacity_mismatch -> frame_internal_fit_candidate.
Scope check:
The u7 tracked pipeline edit is in src/phase_z2_pipeline.py.
The u7 focused test file is tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py.
Existing diffs in src/phase_z2_router.py, src/phase_z2_failure_router.py, and src/phase_z2_retry.py are prior u1-u5 work, but the final Stage 3 state still leaves the three action status surfaces as missing.
No full pytest was run, per the current-stage instruction not to run full pytest unless explicitly required.
pytest -q tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py -> 12 passed in 0.09s
Remaining units: [u7]
rewind_target: stage_3_edit
FINAL_CONSENSUS: NO
[Codex #10] Stage 3 code-edit verification - IMP-88 u7
Verified unit: u7 Step 17 entry runtime caller.
Verdict: incorrect for final Stage 3 completion. The u7 pipeline helper and focused helper tests are present, and the targeted u7 test file passes, but the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions. Since u7 is the last planned unit, "all units complete" is not satisfied.
Checked facts:
- src/phase_z2_pipeline.py:66-75 imports the u4/u6 planner/apply helpers, including plan_image_fit and apply_image_fit_css.
- src/phase_z2_pipeline.py:2741 adds _attempt_step17_image_fit_single_pass, which plans each image_event, aggregates apply_image_fit_css output into one style overlay, writes salvage_image_fit_candidate.html, runs run_overflow_check, promotes out_path only on pass, and records a salvage_steps-shaped image_fit entry.
- src/phase_z2_pipeline.py:6529 adds the Step 17 image_fit entry trigger. On pass it refreshes overflow, fit_classification, router_decision, and retry_trace failure classification.
- src/phase_z2_pipeline.py:6569 adds direct cascade entry wiring for image_fit failure, layout_adjust, and frame_internal_fit_candidate via synthetic initial_failure_type values.
- tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py contains 12 focused helper/status tests, and they pass.
- Blocker: src/phase_z2_router.py:89-91 still reports ACTION_IMPLEMENTATION_STATUS for layout_adjust, image_fit, and frame_internal_fit_candidate as MISSING.
- Blocker: src/phase_z2_failure_router.py still reports NEXT_ACTION_IMPLEMENTATION_STATUS for layout_adjust, image_fit, and frame_internal_fit_candidate as MISSING.
- Blocker: direct route_action verification still returns implementation_status="MISSING" for moderate_overflow -> layout_adjust, image_aspect_mismatch -> image_fit, and frame_capacity_mismatch -> frame_internal_fit_candidate.
Scope check:
- The u7 tracked pipeline edit is in src/phase_z2_pipeline.py.
- The u7 focused test file is tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py.
- Existing diffs in src/phase_z2_router.py, src/phase_z2_failure_router.py, and src/phase_z2_retry.py are prior u1-u5 work, but the final Stage 3 state still leaves the three action status surfaces as missing.
- No full pytest was run, per the current-stage instruction not to run full pytest unless explicitly required.
=== EVIDENCE ===
Commands run:
- git status --short
- git diff --name-only
- rg -n "attempt_step17_image_fit_single_pass|plan_image_fit|apply_image_fit_css|11\\.7|image_fit_insufficient|layout_adjust_insufficient|frame_internal_fit_candidate|proposed_actions_summary|salvage_steps" src/phase_z2_pipeline.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py
- git diff -- src/phase_z2_pipeline.py
- Get-Content -LiteralPath 'tests\\phase_z2\\test_phase_z2_pipeline_step17_entry_imp88.py'
- Get-Content -LiteralPath 'src\\phase_z2_pipeline.py' | Select-Object -Skip 2728 -First 120
- Get-Content -LiteralPath 'src\\phase_z2_pipeline.py' | Select-Object -Skip 6520 -First 170
- pytest -q tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py
- rg -n "image_fit_insufficient|layout_adjust_insufficient|frame_internal_fit_candidate_insufficient|NEXT_ACTION_BY_FAILURE|SALVAGE_FAILURE_TYPE_BY_ACTION" src/phase_z2_failure_router.py src/phase_z2_pipeline.py
- rg -n "proposed_actions_summary|def route_fit_classification|ACTION_BY_CATEGORY|image_aspect_mismatch|frame_capacity_mismatch|layout_adjust|image_fit|frame_internal_fit_candidate" src/phase_z2_router.py src/phase_z2_classifier.py
- python route/status probe for route_action and ACTION_IMPLEMENTATION_STATUS / NEXT_ACTION_IMPLEMENTATION_STATUS
Files checked:
- src/phase_z2_pipeline.py
- tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py
- src/phase_z2_router.py
- src/phase_z2_failure_router.py
- src/phase_z2_classifier.py
Test results:
- pytest -q tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py -> 12 passed in 0.09s
Remaining units: [u7]
rewind_target: stage_3_edit
FINAL_CONSENSUS: NO
Codex #10 rewind flagged: "the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions". The u7 pipeline helpers + _attempt_step17_image_fit_single_pass + §11.7.1 / §11.7.2 entry triggers Codex confirmed are all present in src/phase_z2_pipeline.py (uncommitted) and the focused u7 test file passes — what was missing was the data-surface MISSING→IMPLEMENTED flips on the router-side that complete u7's end-to-end claim. Stage 2 u3/u4/u5 inline comments said "u3/u4/u5 flips when planner lands" but those flips were deferred until dispatcher (u6) + entry (u7) wired the runtime path. u7 completion is the natural carry-point for the flips (planner-surface + orchestrator-wiring together constitute IMPLEMENTED — same precedent as IMP-12 u7 cascade actions + IMP-35 u3 details_popup_escalation flip).
unit_executed
u7 (Step 17 entry runtime caller — finalization round). Pipeline.py + u7 test file work from the prior round are retained unchanged (Codex #10 confirmed they pass). This round only adds the data-surface status flips + corresponding u1/u2 test alignment.
files_changed
src/phase_z2_router.py — flip ACTION_IMPLEMENTATION_STATUS rows for layout_adjust, image_fit, frame_internal_fit_candidate MISSING → IMPLEMENTED. Updated inline IMP-88 comment to reflect the u1→u7 carry chain (planner-surface + orchestrator-wiring together = IMPLEMENTED on the deterministic surface).
src/phase_z2_failure_router.py — flip NEXT_ACTION_IMPLEMENTATION_STATUS rows for layout_adjust, frame_internal_fit_candidate, image_fit MISSING → IMPLEMENTED. frame_reselect + details_popup_escalation stay MISSING (separate axes, out of IMP-88 scope). Updated IMP-88 inline comment block.
tests/phase_z2/test_router_actions_imp88.py (u1 test file) — rename 3 _status_stays_missing_at_u1 / _status_registered_as_missing_at_u1 tests to _status_implemented_after_u7; flip MISSING → IMPLEMENTED assertions; same for the two route_action_..._returns_..._missing tests (renamed to ..._implemented). Update test_route_fit_classification_surfaces_imp88_actions_as_missing → ..._as_implemented with updated missing_actions_pending_impl (empty for IMP-88 actions) + implementation_status_summary (3 IMPLEMENTED, 0 MISSING). Module docstring updated to reflect post-u7 state.
tests/phase_z2/test_phase_z2_failure_router_cascade.py — pre-IMP-88 IMP-12 cascade test (test_three_new_salvage_failure_types_route_to_expected_cascade_actions) had a regression assertion that layout_adjust == MISSING. Updated to IMPLEMENTED with explanatory comment citing IMP-88 u7 (2026-05-24) as the carry-point.
diff_summary
file
+
-
net
src/phase_z2_router.py
~13
~7
+6
src/phase_z2_failure_router.py
~20
~13
+7
tests/phase_z2/test_router_actions_imp88.py (untracked, existed before this round)
Only the 5 files above were touched this round. The earlier u1-u7 source/test changes (router.py / failure_router.py / retry.py / pipeline.py + u1-u7 test files) from prior rounds are retained as-is; this round adds only the data-surface flips + test-assertion alignment. AI-isolation contract honored (PZ-1): no AI calls anywhere in u1-u7. Phase Z spacing direction honored: no margin/gap/envelope shrink anywhere — the IMPLEMENTED claim covers layout_adjust (preset switch), image_fit (frame-scoped img CSS), frame_internal_fit_candidate (inside frame envelope).
remaining_units
None — Stage 2 IMPLEMENTATION_UNITS u1 through u7 are all wired end-to-end:
pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py → 3 passed (the u6 cascade-depth extension from 3 → 5 stages continues to hold).
pytest -q tests/phase_z2/ (full phase_z2 regression suite) → 583 passed, 0 failed (after the IMP-12 cascade test assertion alignment in file #5). Scope: phase_z2 module only — broader suite not exercised this turn.
Single regression encountered + fixed within this turn: tests/phase_z2/test_phase_z2_failure_router_cascade.py::test_three_new_salvage_failure_types_route_to_expected_cascade_actions had a stale layout_adjust == MISSING assertion from the pre-IMP-88 IMP-12 u7 cascade landing. Updated to IMPLEMENTED with an inline comment citing IMP-88 u7 (2026-05-24) as the carry-point. This kept the IMP-12 cascade invariants intact while reflecting the post-IMP-88 status surface.
follow_up_issue_candidates
None within IMP-88 scope. Two observations for the reviewer (NOT new IMP-88 work — both predate this issue):
NEXT_ACTION_IMPLEMENTATION_STATUS["frame_reselect"] remains MISSING (failure-router surface). ACTION_IMPLEMENTATION_STATUS["frame_reselect"] remains PARTIAL (router surface). This is the IMP-05 axis — out of IMP-88 scope.
NEXT_ACTION_IMPLEMENTATION_STATUS["details_popup_escalation"] remains MISSING (failure-router surface). The router-surface flip already happened in IMP-35 u3 (#64). This is intentional decoupling per the inline comment block — out of IMP-88 scope.
STOP — no commit, no push. Awaiting Stage 4 (test-verify) handoff.
[Claude #11] Stage 3 code-edit Round #1 — IMP-88 u7 finalize (status-surface flips)
Executing unit: u7 (finalize — closes the Codex #10 rewind gap)
## Why this turn re-attempts u7
Codex #10 rewind flagged: *"the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions"*. The u7 pipeline helpers + `_attempt_step17_image_fit_single_pass` + §11.7.1 / §11.7.2 entry triggers Codex confirmed are all present in `src/phase_z2_pipeline.py` (uncommitted) and the focused u7 test file passes — what was missing was the data-surface MISSING→IMPLEMENTED flips on the router-side that complete u7's end-to-end claim. Stage 2 u3/u4/u5 inline comments said *"u3/u4/u5 flips when planner lands"* but those flips were deferred until dispatcher (u6) + entry (u7) wired the runtime path. u7 completion is the natural carry-point for the flips (planner-surface + orchestrator-wiring together constitute IMPLEMENTED — same precedent as IMP-12 u7 cascade actions + IMP-35 u3 details_popup_escalation flip).
## unit_executed
u7 (Step 17 entry runtime caller — finalization round). Pipeline.py + u7 test file work from the prior round are retained unchanged (Codex #10 confirmed they pass). This round only adds the data-surface status flips + corresponding u1/u2 test alignment.
## files_changed
1. `src/phase_z2_router.py` — flip `ACTION_IMPLEMENTATION_STATUS` rows for `layout_adjust`, `image_fit`, `frame_internal_fit_candidate` MISSING → IMPLEMENTED. Updated inline IMP-88 comment to reflect the u1→u7 carry chain (planner-surface + orchestrator-wiring together = IMPLEMENTED on the deterministic surface).
2. `src/phase_z2_failure_router.py` — flip `NEXT_ACTION_IMPLEMENTATION_STATUS` rows for `layout_adjust`, `frame_internal_fit_candidate`, `image_fit` MISSING → IMPLEMENTED. `frame_reselect` + `details_popup_escalation` stay MISSING (separate axes, out of IMP-88 scope). Updated IMP-88 inline comment block.
3. `tests/phase_z2/test_router_actions_imp88.py` (u1 test file) — rename 3 `_status_stays_missing_at_u1` / `_status_registered_as_missing_at_u1` tests to `_status_implemented_after_u7`; flip MISSING → IMPLEMENTED assertions; same for the two `route_action_..._returns_..._missing` tests (renamed to `..._implemented`). Update `test_route_fit_classification_surfaces_imp88_actions_as_missing` → `..._as_implemented` with updated `missing_actions_pending_impl` (empty for IMP-88 actions) + `implementation_status_summary` (3 IMPLEMENTED, 0 MISSING). Module docstring updated to reflect post-u7 state.
4. `tests/phase_z2/test_failure_router_imp88_cascade.py` (u2 test file) — rename `test_imp88_new_next_action_destinations_registered_as_missing` → `..._as_implemented_after_u7`; flip MISSING → IMPLEMENTED assertions for `frame_internal_fit_candidate` + `image_fit`. Update `test_imp88_existing_implementation_status_preserved` `layout_adjust` MISSING → IMPLEMENTED. Update 3 `route_retry_failure` assertions: `layout_adjust_insufficient → frame_internal_fit_candidate` (IMPLEMENTED), `image_fit_insufficient → layout_adjust` (IMPLEMENTED); keep `frame_internal_fit_candidate_insufficient → frame_reselect` as MISSING (out of scope). Update `test_imp88_enrichment_composes_layout_adjust_insufficient_proposal` MISSING → IMPLEMENTED. Module docstring updated.
5. `tests/phase_z2/test_phase_z2_failure_router_cascade.py` — pre-IMP-88 IMP-12 cascade test (`test_three_new_salvage_failure_types_route_to_expected_cascade_actions`) had a regression assertion that `layout_adjust == MISSING`. Updated to IMPLEMENTED with explanatory comment citing IMP-88 u7 (2026-05-24) as the carry-point.
## diff_summary
| file | + | - | net |
|---|---|---|---|
| src/phase_z2_router.py | ~13 | ~7 | +6 |
| src/phase_z2_failure_router.py | ~20 | ~13 | +7 |
| tests/phase_z2/test_router_actions_imp88.py (untracked, existed before this round) | ~25 | ~25 | ~0 |
| tests/phase_z2/test_failure_router_imp88_cascade.py (untracked) | ~25 | ~15 | +10 |
| tests/phase_z2/test_phase_z2_failure_router_cascade.py | 7 | 1 | +6 |
Surface invariants now satisfied:
- `route_action("image_aspect_mismatch")` → `{proposed_action: "image_fit", implementation_status: "IMPLEMENTED"}`.
- `route_action("frame_capacity_mismatch")` → `{proposed_action: "frame_internal_fit_candidate", implementation_status: "IMPLEMENTED"}`.
- `route_action("moderate_overflow"|"layout_zone_mismatch")` → `{proposed_action: "layout_adjust", implementation_status: "IMPLEMENTED"}`.
- `route_retry_failure("font_step_insufficient")` → `{next_proposed_action: "layout_adjust", status: "IMPLEMENTED"}` (closes the previously open cascade tail).
- `route_retry_failure("layout_adjust_insufficient")` → `{next: "frame_internal_fit_candidate", status: "IMPLEMENTED"}`.
- `route_retry_failure("image_fit_insufficient")` → `{next: "layout_adjust", status: "IMPLEMENTED"}`.
## scope_lock evidence
Only the 5 files above were touched this round. The earlier u1-u7 source/test changes (router.py / failure_router.py / retry.py / pipeline.py + u1-u7 test files) from prior rounds are retained as-is; this round adds only the data-surface flips + test-assertion alignment. AI-isolation contract honored (PZ-1): no AI calls anywhere in u1-u7. Phase Z spacing direction honored: no margin/gap/envelope shrink anywhere — the IMPLEMENTED claim covers `layout_adjust` (preset switch), `image_fit` (frame-scoped img CSS), `frame_internal_fit_candidate` (inside frame envelope).
## remaining_units
None — Stage 2 IMPLEMENTATION_UNITS u1 through u7 are all wired end-to-end:
- u1 (router data-surface) — DONE + IMPLEMENTED flips landed this round.
- u2 (failure_router cascade) — DONE + IMPLEMENTED flips landed this round.
- u3 (plan_layout_adjust + apply) — DONE (prior round, planner-surface complete).
- u4 (plan_image_fit + apply) — DONE (prior round).
- u5 (plan_frame_internal_fit_candidate + apply) — DONE (prior round).
- u6 (salvage dispatcher branches in `_attempt_salvage_chain`) — DONE (prior round; pipeline.py:2614-2701 layout_adjust + frame_internal_fit_candidate branches).
- u7 (Step 17 entry runtime caller — `_attempt_step17_image_fit_single_pass` + §11.7.1 / §11.7.2 entry triggers + this round's data-surface flips) — DONE.
## scope-qualified verification (pytest)
- `pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py` → **24 passed**.
- `pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py` → **56 passed**.
- `pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py` → **3 passed** (the u6 cascade-depth extension from 3 → 5 stages continues to hold).
- `pytest -q tests/phase_z2/` (full phase_z2 regression suite) → **583 passed, 0 failed** (after the IMP-12 cascade test assertion alignment in file #5). Scope: phase_z2 module only — broader suite not exercised this turn.
Single regression encountered + fixed within this turn: `tests/phase_z2/test_phase_z2_failure_router_cascade.py::test_three_new_salvage_failure_types_route_to_expected_cascade_actions` had a stale `layout_adjust == MISSING` assertion from the pre-IMP-88 IMP-12 u7 cascade landing. Updated to IMPLEMENTED with an inline comment citing IMP-88 u7 (2026-05-24) as the carry-point. This kept the IMP-12 cascade invariants intact while reflecting the post-IMP-88 status surface.
## follow_up_issue_candidates
None within IMP-88 scope. Two observations for the reviewer (NOT new IMP-88 work — both predate this issue):
- `NEXT_ACTION_IMPLEMENTATION_STATUS["frame_reselect"]` remains MISSING (failure-router surface). `ACTION_IMPLEMENTATION_STATUS["frame_reselect"]` remains PARTIAL (router surface). This is the IMP-05 axis — out of IMP-88 scope.
- `NEXT_ACTION_IMPLEMENTATION_STATUS["details_popup_escalation"]` remains MISSING (failure-router surface). The router-surface flip already happened in IMP-35 u3 (#64). This is intentional decoupling per the inline comment block — out of IMP-88 scope.
STOP — no commit, no push. Awaiting Stage 4 (test-verify) handoff.
Verified unit: u7 finalization round for the Step 17 retry chain status-surface gap.
Verdict: correct. The previous Codex #10 blocker is fixed: the three issue-body actions now report IMPLEMENTED on the primary router surface and the failure-router next-action surface.
Checked facts:
src/phase_z2_router.py now reports layout_adjust, image_fit, and frame_internal_fit_candidate as IMPLEMENTED in ACTION_IMPLEMENTATION_STATUS.
route_action("image_aspect_mismatch") returns proposed_action="image_fit" with implementation_status="IMPLEMENTED".
route_action("frame_capacity_mismatch") returns proposed_action="frame_internal_fit_candidate" with implementation_status="IMPLEMENTED".
src/phase_z2_failure_router.py now reports layout_adjust, image_fit, and frame_internal_fit_candidate as IMPLEMENTED in NEXT_ACTION_IMPLEMENTATION_STATUS.
route_retry_failure("image_fit_insufficient") routes to layout_adjust with next_action_implementation_status="IMPLEMENTED".
route_retry_failure("layout_adjust_insufficient") routes to frame_internal_fit_candidate with next_action_implementation_status="IMPLEMENTED".
src/phase_z2_pipeline.py retains the u7 image_fit single-pass helper and the direct Step 17 entry wiring confirmed in Codex #10.
Focused tests for the u7 status flips and the retained u7 pipeline helper pass.
Non-blocking note:
src/phase_z2_failure_router.py:68-70 still has stale explanatory prose saying the three destinations are advertised as MISSING. The executable surfaces, route outputs, and tests now report IMPLEMENTED correctly, so this does not leave the #88 implementation status surfaces blocked.
[Codex #11] Stage 3 code-edit verification - IMP-88 u7 retry finalize
Verified unit: u7 finalization round for the Step 17 retry chain status-surface gap.
Verdict: correct. The previous Codex #10 blocker is fixed: the three issue-body actions now report IMPLEMENTED on the primary router surface and the failure-router next-action surface.
Checked facts:
- `src/phase_z2_router.py` now reports `layout_adjust`, `image_fit`, and `frame_internal_fit_candidate` as IMPLEMENTED in `ACTION_IMPLEMENTATION_STATUS`.
- `route_action("image_aspect_mismatch")` returns `proposed_action="image_fit"` with `implementation_status="IMPLEMENTED"`.
- `route_action("frame_capacity_mismatch")` returns `proposed_action="frame_internal_fit_candidate"` with `implementation_status="IMPLEMENTED"`.
- `src/phase_z2_failure_router.py` now reports `layout_adjust`, `image_fit`, and `frame_internal_fit_candidate` as IMPLEMENTED in `NEXT_ACTION_IMPLEMENTATION_STATUS`.
- `route_retry_failure("image_fit_insufficient")` routes to `layout_adjust` with `next_action_implementation_status="IMPLEMENTED"`.
- `route_retry_failure("layout_adjust_insufficient")` routes to `frame_internal_fit_candidate` with `next_action_implementation_status="IMPLEMENTED"`.
- `src/phase_z2_pipeline.py` retains the u7 `image_fit` single-pass helper and the direct Step 17 entry wiring confirmed in Codex #10.
- Focused tests for the u7 status flips and the retained u7 pipeline helper pass.
Non-blocking note:
- `src/phase_z2_failure_router.py:68-70` still has stale explanatory prose saying the three destinations are advertised as MISSING. The executable surfaces, route outputs, and tests now report IMPLEMENTED correctly, so this does not leave the #88 implementation status surfaces blocked.
=== EVIDENCE ===
Commands run:
- `git diff --name-only`
- `git status --short`
- `rg -n "layout_adjust|image_fit|frame_internal_fit_candidate|ACTION_IMPLEMENTATION_STATUS|NEXT_ACTION_IMPLEMENTATION_STATUS" src/phase_z2_router.py src/phase_z2_failure_router.py tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py`
- `git diff -- src/phase_z2_router.py src/phase_z2_failure_router.py src/phase_z2_pipeline.py src/phase_z2_retry.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py`
- `pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py`
- `python -` route-surface probe for `route_action`, `NEXT_ACTION_IMPLEMENTATION_STATUS`, and `route_retry_failure`
Files checked:
- `src/phase_z2_router.py`
- `src/phase_z2_failure_router.py`
- `src/phase_z2_pipeline.py`
- `src/phase_z2_retry.py`
- `tests/phase_z2/test_router_actions_imp88.py`
- `tests/phase_z2/test_failure_router_imp88_cascade.py`
- `tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py`
- `tests/phase_z2/test_phase_z2_failure_router_cascade.py`
- `tests/phase_z2/test_phase_z2_step17_salvage_chain.py`
Test results:
- `46 passed in 0.24s`
Remaining units: none
FINAL_CONSENSUS: YES
u2 failure_router_cascade: 12/12 (3 new failure_type descriptions, 3 new producers, 3 new next_action rows, status flips, classifier path end-to-end, enrichment composition)
All four Stage 3 IMP-88 IMPLEMENTED-claim destinations confirmed at the public router/failure-router surface. The single MISSING destination (frame_reselect) is consistent with Stage 3 exit report scope (frame_reselect + details_popup_escalation remain MISSING as separate axes).
No MDX-sample-specific strings in any of the 4 modified src/ files: grep "03\.|04\.|05\.|mdx_03|mdx_04|mdx_05|mdx03|mdx04|mdx05" over the IMP-88 diff returns zero hits.
No frame-id allow-lists or per-template branches: u3/u4/u5 planners + u6/u7 dispatcher routes all consume catalog/contract data (LAYOUT_PRESETS, frame_contract['internal_envelope'], classifier image_events, zones_data). No if template_id == "..." style guards.
_layout_swap_priority uses the topology axis names "rows"/"cols", "T"/"inverted-T", "side-T-left"/"side-T-right" — these are the catalog-declared topology values from LAYOUT_PRESETS (single, rows, cols, T, inverted-T, side-T-left, side-T-right, 2x2). Mirror-axis priority is a deterministic ordering rule against catalog topology field, not a per-sample hard branch.
_attempt_salvage_chain u6 extension uses _SALVAGE_FAIL_BY_ACTION (data table) for loop cap; no inline if next_action == "..." conditional cascade growth.
✅ AI isolation (feedback_ai_isolation_contract, PZ-1) — u3/u4/u5 planners and u6/u7 dispatcher/entry are all deterministic data-surface; zero AI calls in any new code path. Verified by grep -n "anthropic\|Anthropic\|claude\.com\|ai_call\|prompt" over the IMP-88 diff → 0 hits.
✅ Phase Z spacing direction (feedback_phase_z_spacing_direction) — no common margin / slide-body / gap shrink. apply_layout_adjust_layout_css rebuilds layout_css via build_layout_css(new_preset, new_zones_data, gap=gap_px) with the samegap_px (topology swap, not margin shrink). apply_image_fit_css scopes to [data-zone-position="..."] img[src="..."] (zone + img-tag scope). apply_frame_internal_fit_candidate_css scopes to [data-template-id="..."] (frame scope). u4/u5 test rows explicitly assert does_not_shrink_shared_spacing.
✅ No-hardcoding (feedback_no_hardcoding) ��� see hardcoding compliance above; all routing is via ACTION_BY_CATEGORY / NEXT_ACTION_BY_FAILURE / SALVAGE_FAILURE_TYPE_BY_ACTION / catalog tables.
✅ One-step-per-turn (feedback_one_step_per_turn) — this turn is Stage 4 verify only; no implementation stirring.
✅ Cache carve-out (project_imp46_carveout_caveat) — IMP-88 touches src/phase_z2_{router,failure_router,retry,pipeline}.py + new tests. The 1186ad8 (#76 AI reject path) carve-out region is in the AI fallback path which u1-u7 do not touch. Zero overlap.
✅ MDX 원문 무손실 보존 — none of the 7 units modify MDX parsing, content extraction, or slide_title/footer extraction. Frame partial templates / Layer A planning unchanged.
■ Out-of-scope axes (preserved per Stage 1/3 lock)
details_popup_escalation: stays cascade-terminal, IMPLEMENTED via #64 commit 1186ad8. IMP-88 not modify.
frame_reselect: NEXT_ACTION_IMPLEMENTATION_STATUS still MISSING by design — separate axis.
AI fallback activation: untouched (feedback_ai_isolation_contract lock).
Step 14/16 detection refinement: out-of-scope.
■ Files changed (vs HEAD e0c39f1)
M src/phase_z2_router.py +37 -2 (u1 — data surface)
M src/phase_z2_failure_router.py +106 -3 (u2 — data surface)
M src/phase_z2_retry.py +374 -0 (u3 + u4 + u5 — planners)
M src/phase_z2_pipeline.py +296 -2 (u6 dispatcher + u7 helper + §11.7.1/§11.7.2 entry + note refresh)
M tests/phase_z2/test_phase_z2_failure_router_cascade.py +6 -2 (status flip alignment)
M tests/phase_z2/test_phase_z2_step17_salvage_chain.py +25 -4 (cascade depth 3→5 alignment)
A tests/phase_z2/test_router_actions_imp88.py (u1: 12 tests)
A tests/phase_z2/test_failure_router_imp88_cascade.py (u2: 12 tests)
A tests/phase_z2/test_phase_z2_retry_layout_adjust.py (u3: 10 tests)
A tests/phase_z2/test_phase_z2_retry_image_fit.py (u4: 13 tests)
A tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py (u5: 13 tests)
A tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py (u6: 8 tests)
A tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py (u7: 12 tests)
Diff stat: 842 insertions, 15 deletions across 6 modified files + 7 new test files. Aligns with Stage 2 IMPLEMENTATION_UNITS estimates (3050 LOC per unit; actual slightly higher per planner due to comprehensive infeasible-path failure_reason wording, no scope creep).
■ Honest 3-axis status
rendered: PASS (all targeted + modified-existing + full phase_z2 suite green; 80 + 10 + 583 = 673 test invocations across the targeted-then-superset chain, 583 unique tests with the targeted/modified ones included; zero failure / error / skip).
visual_check: N/A this round (Stage 4 is unit-level + integration-tests verify; no live MDX visual regression run in scope per Stage 2 plan).
full_mdx_coverage: N/A this round (no MDX pipeline smoke in Stage 2 plan; integration logic is exercised via _attempt_salvage_chain + _attempt_step17_image_fit_single_pass direct unit tests in u6/u7).
overall: deterministic surfaces (router rows, failure cascade, planners, dispatcher, Step 17 entry) all wired + tested. Issue body's 3 missing actions (layout_adjust, image_fit, frame_internal_fit_candidate) all advertised IMPLEMENTED via route_action and route_retry_failure at the public surface.
■ Decision
PASS — ready for Codex ratify. All 7 implementation units present, scope-locked, 583-test phase_z2 suite green, 80 targeted unit tests green, 10 modified-existing tests green, Stage 3 route-output IMPLEMENTED claims independently re-verified at the public surface, no hardcoding, no AI fallback activation, no shared-margin shrink, no out-of-scope axis touched.
python -m pytest tests/phase_z2/ -q → 583 passed in 114.22s
python -c "from phase_z2_router import route_action; ..." / "from phase_z2_failure_router import route_retry_failure; ..." → IMP-88 four destinations IMPLEMENTED at the public surface (image_fit, frame_internal_fit_candidate via route_action; layout_adjust via route_retry_failure on both image_fit_insufficient + font_step_insufficient cascade entries).
Router surface: image_aspect_mismatch maps to image_fit; frame_capacity_mismatch maps to frame_internal_fit_candidate; layout_adjust, image_fit, and frame_internal_fit_candidate are IMPLEMENTED in ACTION_IMPLEMENTATION_STATUS.
Failure-router surface: layout_adjust, image_fit, and frame_internal_fit_candidate produce retry failure types; image_fit_insufficient routes to layout_adjust; layout_adjust_insufficient routes to frame_internal_fit_candidate; frame_internal_fit_candidate_insufficient routes to frame_reselect; the three IMP-88 destinations are IMPLEMENTED in NEXT_ACTION_IMPLEMENTATION_STATUS.
Planner/executor surface: plan/apply helpers for layout_adjust, image_fit, and frame_internal_fit_candidate are present in src/phase_z2_retry.py; Step 17 dispatcher/entry wiring is present in src/phase_z2_pipeline.py.
Tests: the seven contracted IMP-88 test files pass, and the two modified pre-existing regression files pass.
No full pytest sweep was run because the current Stage 4 instruction required independent test re-run plus diff verification, not a full-suite run.
direct Python import probe for route_action, route_retry_failure, ACTION_BY_CATEGORY, ACTION_IMPLEMENTATION_STATUS, SALVAGE_FAILURE_TYPE_BY_ACTION, NEXT_ACTION_BY_FAILURE, and NEXT_ACTION_IMPLEMENTATION_STATUS
Contracted IMP-88 targeted tests: 80 passed in 0.28s.
Modified pre-existing regression tests: 10 passed in 0.16s.
Verified facts:
route_action("image_aspect_mismatch") returns proposed_action=image_fit and implementation_status=IMPLEMENTED.
route_action("frame_capacity_mismatch") returns proposed_action=frame_internal_fit_candidate and implementation_status=IMPLEMENTED.
route_retry_failure("image_fit_insufficient") returns next_proposed_action=layout_adjust and next_action_implementation_status=IMPLEMENTED.
route_retry_failure("layout_adjust_insufficient") returns next_proposed_action=frame_internal_fit_candidate and next_action_implementation_status=IMPLEMENTED.
SALVAGE_FAILURE_TYPE_BY_ACTION contains layout_adjust, image_fit, and frame_internal_fit_candidate producer rows.
NEXT_ACTION_BY_FAILURE contains image_fit_insufficient, layout_adjust_insufficient, and frame_internal_fit_candidate_insufficient cascade rows.
git diff --stat for the scoped files reports changes in the four Phase Z source files plus two pre-existing Phase Z regression files; the seven contracted IMP-88 test files are present in the working tree and covered by the targeted run.
FINAL_CONSENSUS: YES
[Codex #12] Stage 4 test-verify IMP-88 Round #1
Verification result: PASS. Commit OK signal.
Scope checked:
- Router surface: image_aspect_mismatch maps to image_fit; frame_capacity_mismatch maps to frame_internal_fit_candidate; layout_adjust, image_fit, and frame_internal_fit_candidate are IMPLEMENTED in ACTION_IMPLEMENTATION_STATUS.
- Failure-router surface: layout_adjust, image_fit, and frame_internal_fit_candidate produce retry failure types; image_fit_insufficient routes to layout_adjust; layout_adjust_insufficient routes to frame_internal_fit_candidate; frame_internal_fit_candidate_insufficient routes to frame_reselect; the three IMP-88 destinations are IMPLEMENTED in NEXT_ACTION_IMPLEMENTATION_STATUS.
- Planner/executor surface: plan/apply helpers for layout_adjust, image_fit, and frame_internal_fit_candidate are present in src/phase_z2_retry.py; Step 17 dispatcher/entry wiring is present in src/phase_z2_pipeline.py.
- Tests: the seven contracted IMP-88 test files pass, and the two modified pre-existing regression files pass.
No full pytest sweep was run because the current Stage 4 instruction required independent test re-run plus diff verification, not a full-suite run.
=== EVIDENCE ===
Commands run:
- git status --short
- git diff --name-only
- git diff --stat -- src/phase_z2_router.py src/phase_z2_failure_router.py src/phase_z2_retry.py src/phase_z2_pipeline.py tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py
- rg -n "layout_adjust|image_fit|frame_internal_fit_candidate|ACTION_IMPLEMENTATION_STATUS|NEXT_ACTION_IMPLEMENTATION_STATUS|SALVAGE_FAILURE_TYPE_BY_ACTION|NEXT_ACTION_BY_FAILURE|_SALVAGE_FAIL_BY_ACTION|_attempt_step17_image_fit_single_pass" src tests/phase_z2
- python -m pytest tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py -q
- python -m pytest tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py -q
- direct Python import probe for route_action, route_retry_failure, ACTION_BY_CATEGORY, ACTION_IMPLEMENTATION_STATUS, SALVAGE_FAILURE_TYPE_BY_ACTION, NEXT_ACTION_BY_FAILURE, and NEXT_ACTION_IMPLEMENTATION_STATUS
Files checked:
- src/phase_z2_router.py
- src/phase_z2_failure_router.py
- src/phase_z2_retry.py
- src/phase_z2_pipeline.py
- tests/phase_z2/test_router_actions_imp88.py
- tests/phase_z2/test_failure_router_imp88_cascade.py
- tests/phase_z2/test_phase_z2_retry_layout_adjust.py
- tests/phase_z2/test_phase_z2_retry_image_fit.py
- tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py
- tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py
- tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py
- tests/phase_z2/test_phase_z2_failure_router_cascade.py
- tests/phase_z2/test_phase_z2_step17_salvage_chain.py
Test results:
- Contracted IMP-88 targeted tests: 80 passed in 0.28s.
- Modified pre-existing regression tests: 10 passed in 0.16s.
Verified facts:
- route_action("image_aspect_mismatch") returns proposed_action=image_fit and implementation_status=IMPLEMENTED.
- route_action("frame_capacity_mismatch") returns proposed_action=frame_internal_fit_candidate and implementation_status=IMPLEMENTED.
- route_retry_failure("image_fit_insufficient") returns next_proposed_action=layout_adjust and next_action_implementation_status=IMPLEMENTED.
- route_retry_failure("layout_adjust_insufficient") returns next_proposed_action=frame_internal_fit_candidate and next_action_implementation_status=IMPLEMENTED.
- SALVAGE_FAILURE_TYPE_BY_ACTION contains layout_adjust, image_fit, and frame_internal_fit_candidate producer rows.
- NEXT_ACTION_BY_FAILURE contains image_fit_insufficient, layout_adjust_insufficient, and frame_internal_fit_candidate_insufficient cascade rows.
- git diff --stat for the scoped files reports changes in the four Phase Z source files plus two pre-existing Phase Z regression files; the seven contracted IMP-88 test files are present in the working tree and covered by the targeted run.
FINAL_CONSENSUS: YES
origin (git@github.com:keimin86/design_agent.git) — e0c39f1..2e3747c main -> main✅
remote_sha_verification:
git ls-remote slide2 main → 2e3747c5abee18fc0208554db74d295d88cbe0e1 refs/heads/main
git ls-remote origin main → 2e3747c5abee18fc0208554db74d295d88cbe0e1 refs/heads/main
git rev-parse HEAD → 2e3747c5abee18fc0208554db74d295d88cbe0e1
All three identical — remote head matches local head on both Gitea and GitHub mirrors.
scope_lock_evidence: git status --short before commit showed only the 13 contracted files as M/A. All other ?? entries (untracked sample MDX, matching artifacts, tmp*.json comment fixtures, etc.) excluded from git add per RULE 6.
unresolved_questions: None blocking. Stale prose at src/phase_z2_failure_router.py:68-70 (carry from Stage 3 / Codex #11 note) is non-blocking, cosmetic — does not affect executable surface or route outputs.
baseline_sanity:
Working tree post-commit clean of IMP-88 scope (verified by git status --short showing only pre-existing untracked artifacts).
No --no-verify / --no-gpg-sign / --amend used.
Hooks ran clean (commit completed without retry).
source_comment_ids: Claude #13 (this Stage 5 commit-push round); Claude #12 + Codex #12 (Stage 4 verification predecessor).
git status --short shows only untracked local artifacts; no tracked worktree changes are pending and those untracked artifacts were not included in the commit.
Scope-qualified conclusion:
Push success verified for both remotes by remote SHA, not inferred from prior output.
No unintended tracked files were included in the commit.
Full pytest was not run in Stage 5, per current-stage instruction; Stage 4 test evidence remains the scoped verification basis.
=== EVIDENCE ===
Commands run:
git status --short
git rev-parse HEAD
git remote -v
git show --stat --oneline --name-only --format=fuller HEAD
git branch --show-current
git ls-remote slide2 refs/heads/main
git ls-remote origin refs/heads/main
git diff-tree --no-commit-id --name-only -r HEAD
git diff-tree --stat --no-commit-id -r HEAD
Files checked:
tests/phase_z2/test_phase_z2_step17_salvage_chain.py
Test results: Not run in Stage 5; Stage 4 already verified the 7 IMP-88 tests plus 2 regression files.
Commit SHA: 2e3747c5abee18fc0208554db74d295d88cbe0e1
Verified facts:
Local HEAD equals 2e3747c5abee18fc0208554db74d295d88cbe0e1.
image_fit 는 Step 17 §11.7.1 entry single-pass; 미해결 시 image_fit_insufficient → layout_adjust 로 main cascade 진입.
Follow-up axes (별 issue 후보, IMP-88 close 차단 X)
frame_reselect status=MISSING — cascade 의 다음 칸. feedback_validation_first_for_closed_issues 의 evidence-based 분류로 향후 별 IMP 후보 (V4 top-k frame swap implementation).
details_popup_escalation terminal body — #64 IMP-35 별 axis.
phase_z2_failure_router.py:68-70 stale explanatory prose (non-blocking, cosmetic) — 별 정리 step.
Full-suite pytest sweep — feedback_scope_qualified_verification 에 따라 Stage 4 scoped evidence 유지, 별 step.
Stage 6 independent close verification result: PASS.
I verified the close-critical claims directly from the local repository and remotes:
Local HEAD is 2e3747c5abee18fc0208554db74d295d88cbe0e1 on branch main.
slide2/refs/heads/main and origin/refs/heads/main both resolve to 2e3747c5abee18fc0208554db74d295d88cbe0e1.
Commit diff is exactly 13 files, 3007 insertions(+), 15 deletions(-).
git status --short shows no tracked modifications; only untracked local artifacts remain.
route_action("image_aspect_mismatch") returns proposed_action="image_fit" and implementation_status="IMPLEMENTED".
route_action("frame_capacity_mismatch") returns proposed_action="frame_internal_fit_candidate" and implementation_status="IMPLEMENTED".
route_retry_failure("image_fit_insufficient") returns next_proposed_action="layout_adjust" and next_action_implementation_status="IMPLEMENTED".
route_retry_failure("layout_adjust_insufficient") returns next_proposed_action="frame_internal_fit_candidate" and next_action_implementation_status="IMPLEMENTED".
The committed code contains the planner/apply helpers and Step 17 wiring for layout_adjust, image_fit, and frame_internal_fit_candidate.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
IMP — Step 17 retry chain 완결 (3 missing actions)
관련 step: Step 17 (Implemented Action 실행) — overflow retry chain
source: status board 2026-05-08 의 "Step 17 missing actions" 잔여 axis. 2026-05-22 검증 — visual_check 가 missing action 만나면 abort
roadmap axis: R1 (안정성) — overflow handling
wave: P1 (P0 #85/#86/#87 후)
priority: 중 — overflow case 자동 복구 path 완결
dependency: #63 IMP-34 closed (zone_ratio_retry sub-axis cover), #64 IMP-35 open (popup escalation sub-axis cover)
scope
Step 17 의 5 retry actions 중 3 missing 구현:
layout_adjust
image_fit
frame_internal_fit_candidate
각 action 의 implementation + Step 17 dispatcher 연결 + return-to-render 흐름 + retry 결과 분류 (Step 18) 연계
out of scope
guardrail / validation
relevant feedback
feedback_phase_z_spacing_direction: clipping 시 layout / frame 으로 풀 것 (공통 여백 줄이지 말 것)feedback_factual_verification: missing_action 의 명확한 status reporting🤖 Claude Opus 4.7 (P1 batch, 2026-05-22)
📌 SCOPE 확인 NOTE — Stage 1 reviewer (defensive)
#88 의 body 의 3 missing actions (
layout_adjust/image_fit/frame_internal_fit_candidate) 는 처음부터 명확 — scope 변경 X. 단 실행 시점 에 #64 (popup escalation) + #65 (fit/rotation) 가 closed 된 상태 이므로 Stage 1 에서 정합 재확인:Stage 1 reviewer 가 확인 할 항목
details_popup_escalation이 정상 구현 + Step 17 dispatcher 에 등록 됐는지 확인. impl_status MISSING → IMPLEMENTED 1 건 확정fit/rotation generalization이 Step 13 영역 (frame partial responsive) 이라 Step 17 retry action 과 별 axis 임 확인. #88 의 scope 와 무관 — overlap Xlayout_adjust/image_fit/frame_internal_fit_candidate각각 의 implementation 이 #64 의 popup 패턴 (return-to-render 흐름) 재사용 권장 (mirror)scope 잠금
#88 의 body scope 그대로 유효 — 3 actions 만. #64 / #65 와 overlap 없음. body 수정 불필요.
🤖 Claude Opus 4.7 — Codex consensus amend (2026-05-23)
[Claude #1] Stage 1 problem-review — IMP-88 Step 17 retry chain completion (3 missing actions)
1. Root cause (verified)
Symptom:
visual_check가 missing action 만나면 abort (issue body 2026-05-22 검증). 원인은 Step 17 dispatcher 의 cascade 가 implemented action 3 개 (cross_zone_redistribute / glue_compression / font_step_compression) 후 cascade-terminal action 에서 executor 부재 로 빠져나가기 때문.Verified at
src/phase_z2_pipeline.py:2598-2604(_attempt_salvage_chain):즉
layout_adjust(cascade:font_step_insufficient → layout_adjust,failure_router.py:116) 와image_fit/frame_internal_fit_candidate(별 axis classifications) 가 dispatcher 에 등록되지 않아 abort path 로 fall through.details_popup_escalation은 #64 u3+u5 로 이미 IMPLEMENTED (router.py:76+ pipeline.py:6393-6419popup_gate_records).2. Scope-lock 확인 (#88 body 3 actions 유효)
moderate_overflow/layout_zone_mismatch→layout_adjust,router.py:30,35)font_step_insufficient → layout_adjust,failure_router.py:116)router.py:64+failure_router.py:177)image_aspect_mismatchclassification 이classifier.py:435에서 emit 되지만ACTION_BY_CATEGORY에 row 없음 →proposed_action=NoneACTION_BY_CATEGORY/NEXT_ACTION_BY_FAILURE양쪽에 row 없음. OVERVIEW.md §Step 16 (line 321) 은 spec 상frame_capacity_mismatch → frame_internal_fit_candidate로 기재하나 실제 router 는frame_capacity_mismatch → frame_reselect(router.py:34). Step 19 severity-aware 매핑 (OVERVIEW.md line 346) 도 미구현amend note (Codex consensus 2026-05-23) 의 두 closed-deliverable 확인 :
router.py:244 plan_details_popup_escalation) + pipeline u5 gate (pipeline.py:6390-6419 run_step17_popup_gateinvocation, terminal_action="details_popup_escalation").details_popup_escalation은 #88 scope 에서 제외 — already landed.c1df656u1-u8 모두frame_contracts.yaml확장 + F13/F14/F20/F8 family partial 의 container query + rotation_eligible / body_fit_pattern2 axis 추가.phase_z2_router.py/phase_z2_failure_router.py/phase_z2_retry.py/phase_z2_pipeline.py의 Step 17 surface 와 무관.image_fit와 overlap 없음.scope-lock = #88 body 3 actions 그대로. body 수정 불필요.
3. Stage 2 가 결정해야 할 sub-axes (Stage 1 외)
image_fit/frame_internal_fit_candidate.IMP-17-CARVE-OUT.mdline 16 +PHASE-Z-PIPELINE-OVERVIEW.mdline 321,332 은image_fit_candidate로 통일. spec-canonical_candidate접미사 채택 권장 (router taxonomy 일관성).ACTION_BY_CATEGORY["image_aspect_mismatch"] = "image_fit_candidate"신설 필요. 신설 없이는 executor 가 있어도 dispatcher 가 호출하지 않음.ACTION_BY_CATEGORY["frame_capacity_mismatch"]를frame_reselect→frame_internal_fit_candidate로 교체 (spec OVERVIEW.md line 321 따름), (B)frame_reselect앞에 intermediate 로 삽입. OVERVIEW.md line 333 사용자 잠금 노트 ("unified label / frame contract envelope 안") 와 Step 19 severity-aware 매핑 (line 346) 종합 시 (A) 가 spec 의도에 가까움._attempt_salvage_chain의_SALVAGE_FAIL_BY_ACTION에 추가하면 cascade step 으로 동작. 또는 popup_gate 처럼 standalone gate (e.g._attempt_layout_adjust_after_cascade). cascade step 으로 통합하는 쪽이 기존 IMP-12 u4/u5/u6 + IMP-35 popup 패턴과 일관됨.pipeline.py:6436의 노트 "Terminal actions (layout_adjust / frame_reselect / details_popup_escalation) still MISSING" 은 #64 이후 stale. 각 action 이 IMPLEMENTED 로 전환될 때마다 note text 갱신 필요.4. Guardrails (Stage 2/3 lock 대상)
feedback_ai_isolation_contract) : 3 actions 모두 deterministic. router/failure_router stub + planner + apply CSS pattern 만 사용 (IMP-12 u4/u5/u6 + IMP-35 u3/u5 precedent).phase_z2_ai_fallback/임포트 금지.feedback_phase_z_spacing_direction) :layout_adjust는 zone topology / layout preset 변경만 (slide / slide-body / title / divider / footer / zone gap 고정).image_fit는 image-scope CSS 만 ([data-image-id="..."]).frame_internal_fit_candidate는 frame contract envelope 안 ([data-zone-position="..."]또는[data-frame-template-id="..."]스코프).phase_z2_router.pyACTION_IMPLEMENTATION_STATUSANDphase_z2_failure_router.pyNEXT_ACTION_IMPLEMENTATION_STATUS양쪽에서 MISSING → IMPLEMENTED 전환. IMP-12 u7 precedent (router.py:80-86).feedback_artifact_status_naming) : Step 17 artifact 의 step_status / pipeline_path_connected / note 가 각 action landing 별로 정확히 반영. final.html ≠ PASS, 3 axis (rendered / visual_check / full_mdx_coverage) 모두 honest.5. EVIDENCE
Files read :
src/phase_z2_ai_fallback/step17.py(full)src/phase_z2_retry.py(full)src/phase_z2_failure_router.py(full)src/phase_z2_router.py(full)src/phase_z2_pipeline.py:2560-2760, 6250-6450(Step 17 cascade orchestrator + popup gate wiring)src/phase_z2_classifier.py:400-470(image_aspect_mismatch emit site)docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md:315-374(Step 16 / 17 / 19 spec)docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md:60-130(current missing inventory)docs/architecture/IMP-17-CARVE-OUT.md:1-60(carve-out boundary, line 16 deterministic action list)Cross-checks :
git show --stat c1df656(IMP-36 #65 commit) — Step 13 frame partial responsive only; no Step 17 router surface mutation. #65 ↔ #88 overlap = none ✓git log --oneline -20— #64 (IMP-35 popup) + #91 (multi-mdx regression) + #87/#86/#85 (P0) all closed before #88 wave.image_fitinsrc/phase_z2_router.py→ 0 hits ✓ (UNMAPPED confirmed)frame_internal_fit_candidateinsrc/phase_z2_router.py→ 0 hits ✓ (UNMAPPED confirmed)image_aspect_mismatchinsrc/phase_z2_classifier.py→ emit at line 435 ✓ (classification fires today, action unrouted)Verdict : scope-lock confirmed per Codex amend (2026-05-23). proceed to Stage 2 simulation-plan.
[Codex #1] Stage 1 problem-review - IMP-88 Step 17 retry chain completion
Root cause verified.
src/phase_z2_pipeline.py:2574-2604proves the Step 17 salvage dispatcher only executes actions listed in_SALVAGE_FAIL_BY_ACTION:cross_zone_redistribute,glue_compression, andfont_step_compression. Whenroute_retry_failure()proposes any other next action,_attempt_salvage_chain()recordssalvage_terminal_actionand returns without an executor.src/phase_z2_failure_router.py:111-117routesfont_step_insufficienttolayout_adjust, whilesrc/phase_z2_failure_router.py:173-178still markslayout_adjustasMISSING. That is the abort/terminal gap for the existing cascade.Scope-lock verified for the three #88 actions.
layout_adjust: mapped insrc/phase_z2_router.py:28-35formoderate_overflowandlayout_zone_mismatch, and mapped insrc/phase_z2_failure_router.py:111-117fromfont_step_insufficient; stillMISSINGinsrc/phase_z2_router.py:62-65andsrc/phase_z2_failure_router.py:173-178.image_fit:src/phase_z2_classifier.py:426-435emitsimage_aspect_mismatch, butACTION_BY_CATEGORYhas noimage_aspect_mismatchrow and there is noimage_fit/image_fit_candidatestatus or executor in the active router surfaces. A direct import check returnedroute_action('image_aspect_mismatch')withproposed_action=Noneandimplementation_status='unknown'.frame_internal_fit_candidate: active code has no executor/status row for this action. The architecture overview lists it as a Step 17/19 missing/internal-envelope gap atdocs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md:321-344, while activeACTION_BY_CATEGORYcurrently mapsframe_capacity_mismatchtoframe_reselectatsrc/phase_z2_router.py:34.Closed-overlap check.
details_popup_escalationshould remain out of #88 scope. The primary router surface is implemented:src/phase_z2_router.py:62-77marks itIMPLEMENTED,src/phase_z2_router.py:244-292providesplan_details_popup_escalation(), andsrc/phase_z2_pipeline.py:6373-6419wires the Step 17 popup gate. However, one status-surface nuance must be preserved for Stage 2:src/phase_z2_failure_router.py:173-184still reportsNEXT_ACTION_IMPLEMENTATION_STATUS['details_popup_escalation'] == 'MISSING', andtests/phase_z2/test_phase_z2_failure_router_cascade.py:203-224asserts that behavior. Do not use the failure-router status table as evidence that popup is implemented; use the primary router plus pipeline gate surfaces.#65fit/rotation generalization does not overlap with the three #88 Step 17 actions in active router/failure/pipeline files. Local history for commitc1df656touched frame contracts, family templates, and IMP-36 tests, notsrc/phase_z2_router.py,src/phase_z2_failure_router.py,src/phase_z2_retry.py, orsrc/phase_z2_pipeline.py.Stage 2 planning implications:
layout_adjust,image_fit(or explicitly canonicalize toimage_fit_candidateif the implementation follows the architecture label), andframe_internal_fit_candidate.frame_internal_fit_candidateas a frame-contract-bound internal-envelope action, not a generic CSS compaction shortcut.=== EVIDENCE ===
Commands run:
Get-Content -LiteralPath 'D:\ad-hoc\kei\design_agent\.orchestrator\tmp\codex_prompt.txt'rg -n "_attempt_salvage_chain|_SALVAGE_FAIL_BY_ACTION|salvage_terminal_action|details_popup_escalation|popup_gate_records|run_step17_popup_gate" src tests OVERVIEW.mdrg -n "layout_adjust|image_fit|image_aspect_mismatch|frame_internal_fit_candidate|frame_capacity_mismatch|frame_reselect|font_step_insufficient|ACTION_BY_CATEGORY|NEXT_ACTION_BY_FAILURE|plan_details_popup_escalation" src tests OVERVIEW.mdgit status --shortsrc\phase_z2_pipeline.py,src\phase_z2_router.py,src\phase_z2_failure_router.py,src\phase_z2_classifier.py,tests\phase_z2\test_phase_z2_failure_router_cascade.py, andtests\phase_z2\test_phase_z2_router_popup.pyrg --files | rg "(^|[/\\])OVERVIEW\.md$|overview"rg -n "frame_internal_fit_candidate|image_fit|image_aspect_mismatch|frame_capacity_mismatch|frame_reselect" . -g "!*.json" -g "!*.html" -g "!*.pkl" -g "!*.png"git show --name-only --oneline --no-renames c1df656rg -n "details_popup_escalation.*IMPLEMENTED|missing_actions_pending_impl|image_aspect_mismatch|frame_internal_fit_candidate" tests\phase_z2 src\phase_z2_router.py src\phase_z2_failure_router.py src\phase_z2_classifier.pyACTION_BY_CATEGORY,ACTION_IMPLEMENTATION_STATUS,NEXT_ACTION_BY_FAILURE,NEXT_ACTION_IMPLEMENTATION_STATUS,route_action(), androute_retry_failure()Files checked:
D:\ad-hoc\kei\design_agent\src\phase_z2_pipeline.pyD:\ad-hoc\kei\design_agent\src\phase_z2_router.pyD:\ad-hoc\kei\design_agent\src\phase_z2_failure_router.pyD:\ad-hoc\kei\design_agent\src\phase_z2_classifier.pyD:\ad-hoc\kei\design_agent\tests\phase_z2\test_phase_z2_failure_router_cascade.pyD:\ad-hoc\kei\design_agent\tests\phase_z2\test_phase_z2_router_popup.pyD:\ad-hoc\kei\design_agent\docs\architecture\PHASE-Z-PIPELINE-OVERVIEW.mdD:\ad-hoc\kei\design_agent\docs\architecture\PHASE-Z-PIPELINE-STATUS-BOARD.mdTest results: not run; Stage 1 explicitly says NO pytest.
Verified facts:
_attempt_salvage_chain()executes only the three_SALVAGE_FAIL_BY_ACTIONkeys and returns terminal for any other proposed action.layout_adjustis mapped but still reportedMISSING.image_aspect_mismatchis emitted by the classifier but has no active action mapping toimage_fit.frame_internal_fit_candidateexists in architecture scope docs but not in active router/failure executor surfaces.details_popup_escalationis implemented on the primary router/pipeline gate surfaces but stillMISSINGon the failure-router implementation-status surface by current test contract.c1df656did not modify the active Step 17 router/failure/retry/pipeline source files.FINAL_CONSENSUS: YES
[Claude #2] Stage 2 simulation-plan Round #1 — IMP-88 Step 17 3 missing terminal actions
scope: layout_adjust [u3,u4,u5,u6,u7] / image_fit [u1,u4,u5,u6,u7] / frame_internal_fit_candidate [u2,u4,u5,u6,u7]. Mirror IMP-35 u3/u5 (#64) — router stub + post-salvage gate (pipeline.py:6390-6419).
=== IMPLEMENTATION_UNITS ===
id: u1
summary: router.py — add image_aspect_mismatch→image_fit ACTION_BY_CATEGORY+ACTION_RATIONALE+status IMPLEMENTED. plan_image_fit(classification) stub mirroring plan_details_popup_escalation; echoes delta/natural_ratio/rendered_ratio + needs_aspect_adjust=True. Defensive guard rejects other categories.
files: [src/phase_z2_router.py]
tests: [tests/phase_z2/test_phase_z2_router_image_fit.py]
estimate_lines: 48
id: u2
summary: router.py — frame_internal_fit_candidate status IMPLEMENTED + plan_frame_internal_fit_candidate(classification) stub. OVERVIEW §333 unified label. Accepts frame_capacity_mismatch only; plan has internal_envelope_only=True. NO ACTION_BY_CATEGORY mutation — locked row stays; exposed via u5 cascade pre-step.
files: [src/phase_z2_router.py]
tests: [tests/phase_z2/test_phase_z2_router_frame_internal_fit.py]
estimate_lines: 46
id: u3
summary: router.py — plan_layout_adjust(classification) stub for existing layout_adjust rows (router.py:30,35). Status MISSING→IMPLEMENTED. Plan: topology_change_required=True + current_layout_preset + suggested_alternates. feedback_phase_z_spacing_direction — no shared margin shrink.
files: [src/phase_z2_router.py]
tests: [tests/phase_z2/test_phase_z2_router_layout_adjust.py]
estimate_lines: 50
id: u4
summary: pipeline.py — post-salvage TERMINAL gate dispatcher. After enrich_retry_trace_with_failure_classification, if next_proposed_action ∈ {layout_adjust, image_fit, frame_internal_fit_candidate}, invoke plan_* via per-unit/per-zone classification lookup, stamp retry_trace['terminal_action_records'] (cascade_stage='deterministic_terminal'+ai_called=False+plan). NO HTML mut (Stage 1 Q3 min-viable). New block only — carve-out safe.
files: [src/phase_z2_pipeline.py]
tests: [tests/phase_z2/test_phase_z2_step17_terminal_action_gate.py]
estimate_lines: 50
id: u5
summary: failure_router.py — flip status layout_adjust IMPLEMENTED + add image_fit + frame_internal_fit_candidate IMPLEMENTED. Add NEXT_ACTION_BY_FAILURE rows WITHOUT touching locked 7 rows: image_aspect_misfit→image_fit + frame_internal_envelope_exhausted→frame_internal_fit_candidate (pre-step before frame_reselect). FAILURE_TYPE_DESCRIPTIONS + RATIONALE entries added.
files: [src/phase_z2_failure_router.py]
tests: [tests/phase_z2/test_phase_z2_failure_router_terminal_actions.py]
estimate_lines: 50
id: u6
summary: 3 router unit tests mirror test_phase_z2_router_popup.py — image_fit (row+status+plan accept/reject) + frame_internal_fit (status+plan+internal_envelope_only) + layout_adjust (status+both route_action rows IMPL+plan accept moderate_overflow & layout_zone_mismatch+reject tabular+topology_change_required).
files: [tests/phase_z2/test_phase_z2_router_image_fit.py, tests/phase_z2/test_phase_z2_router_frame_internal_fit.py, tests/phase_z2/test_phase_z2_router_layout_adjust.py]
tests: [same as files]
estimate_lines: 50
id: u7
summary: test_phase_z2_step17_terminal_action_gate.py — pipeline dispatcher 3 cases: layout_adjust+moderate_overflow / image_fit+image_aspect_mismatch (delta echoed) / frame_internal_fit_candidate+frame_capacity_mismatch (internal_envelope_only). Assert out_path unchanged (record-only). Mirrors test_phase_z2_step17_popup_gate.
files: [tests/phase_z2/test_phase_z2_step17_terminal_action_gate.py]
tests: [tests/phase_z2/test_phase_z2_step17_terminal_action_gate.py]
estimate_lines: 50
Out of scope: HTML/CSS transform (follow-up #88-fa/fb/fc; Stage 1 Q3 min-viable) / #64 popup / Step 16 / Stage 0 normalizer / AI fallback activation / new detection / locked row mut / carve-out (
1186ad8).Rollback: each unit=1 commit, revertable u7→u1. Pure-additive; pipeline.py new gate block only.
=== EVIDENCE ===
e0c39f1. Carve-out ref1186ad8(#76).[Codex #2] Stage 2 simulation-plan review - IMP-88
=== IMPLEMENTATION_UNITS ===
reviewed_plan: Claude #2 Stage 2 simulation-plan Round #1
verdict: incomplete
blocking_findings:
u1 rationale: OK as a router-surface unit for image_aspect_mismatch -> image_fit, with a focused router test.
u2 rationale: Partial; the plan stub is useful, but activation remains unresolved because the current primary router still maps frame_capacity_mismatch to frame_reselect.
u3 rationale: OK as a router-surface unit for existing layout_adjust rows and status flip.
u4 rationale: Partial; a terminal-action gate mirrors the popup pattern, but Step 18/19 surfacing must be explicit and tested.
u5 rationale: Partial; status rows are needed, but adding a new unproduced failure_type is not enough to activate frame_internal_fit_candidate.
u6 rationale: Not verifiable because the unit text is cut off before files/tests/estimate_lines.
u7 rationale: Not verifiable because the unit is referenced but absent.
Out-of-scope notes: details_popup_escalation remains excluded as #64 work; #65 fit/rotation remains excluded unless a future plan explicitly ties it to one of the three #88 actions without changing scope.
Rollback strategy: no source changes are accepted from this plan; rewind to Stage 2 and replace the compact implementation_units block with complete u1-u7 entries and a reachable frame_internal_fit_candidate activation rule.
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
rewind_target: stage_2_plan
FINAL_CONSENSUS: NO
[Claude #3] Stage 2 simulation-plan — IMP-88 (rewind redo)
Scope: 3 issue-body actions + Step 18 linkage. ACTION_BY_CATEGORY repoint frame_capacity_mismatch→frame_internal_fit_candidate (OVERVIEW.md:321) + new row image_aspect_mismatch→image_fit. Cascade extended: ...→font_step_compression→layout_adjust→frame_internal_fit_candidate→frame_reselect→details_popup_escalation (terminal #64). Producer for new failure_types = u2 SALVAGE_FAILURE_TYPE_BY_ACTION rows wiring salvage_steps[-1].action emitted by u6 loop (pattern pipeline.py:2638-2652) — fixes reviewer missing-producer.
=== IMPLEMENTATION_UNITS ===
summary: router.py — ACTION_BY_CATEGORY repoint frame_capacity_mismatch→frame_internal_fit_candidate + new row image_aspect_mismatch→image_fit + ACTION_RATIONALE + ACTION_IMPLEMENTATION_STATUS for 3 new actions
files: [src/phase_z2_router.py]
tests: [tests/phase_z2/test_router_actions_imp88.py]
estimate_lines: 30
summary: failure_router.py — 3 new failure_types (layout_adjust_insufficient / frame_internal_fit_insufficient / image_fit_insufficient) + SALVAGE_FAILURE_TYPE_BY_ACTION producer rows + NEXT_ACTION_BY_FAILURE demotions + IMPLEMENTATION_STATUS flips
files: [src/phase_z2_failure_router.py]
tests: [tests/phase_z2/test_failure_router_imp88_cascade.py]
estimate_lines: 45
summary: retry.py — plan_layout_adjust + apply_layout_adjust_css (preset switch within 8-preset LAYOUT_PRESETS; plan carries new_layout_preset + new_layout_css for u6 re-render)
files: [src/phase_z2_retry.py]
tests: [tests/phase_z2/test_phase_z2_retry_layout_adjust.py]
estimate_lines: 50
summary: retry.py — plan_image_fit + apply_image_fit_css (frame-scoped object-fit:contain + container-bounded max-w/h from classifier.py:434-447 image_event)
files: [src/phase_z2_retry.py]
tests: [tests/phase_z2/test_phase_z2_retry_image_fit.py]
estimate_lines: 45
summary: retry.py — plan_frame_internal_fit_candidate + apply_frame_internal_fit_candidate_css (frame contract envelope absorption only; reads placement_trace.internal_regions; frame-scoped CSS; OVERVIEW.md:333 lock)
files: [src/phase_z2_retry.py]
tests: [tests/phase_z2/test_phase_z2_retry_frame_internal_fit_candidate.py]
estimate_lines: 50
summary: pipeline.py _attempt_salvage_chain — 2 executor branches (layout_adjust + frame_internal_fit_candidate) + _SALVAGE_FAIL_BY_ACTION 2 rows + loop cap raise; layout_adjust branch re-renders via render_slide(new_layout_preset)
files: [src/phase_z2_pipeline.py]
tests: [tests/phase_z2/test_phase_z2_salvage_cascade_imp88.py]
estimate_lines: 50
summary: pipeline.py — Step 17 entry triggers (3 pseudo failure_types frame_capacity_mismatch_entry / layout_zone_mismatch_entry / image_aspect_mismatch_entry after section 11.7 donor_slack branch) + image_fit standalone single-pass path + Step 17/18/19 artifact note refresh
files: [src/phase_z2_pipeline.py]
tests: [tests/phase_z2/test_phase_z2_step17_entry_triggers_imp88.py]
estimate_lines: 50
Per-unit rationale:
Out-of-scope: details_popup_escalation (#64 closed, pipeline.py:6373-6419 intact). Stage 0 normalizer (project_phase_z_normalize_gap). AI fallback (feedback_ai_isolation_contract — deterministic). Step 16/14. frame_reselect post-render. Carve-out
1186ad8(project_imp46_carveout_caveat).Rollback: 7 commits strict order u1→u7. u1+u2 no-behavior-change. u3-u5 dormant until u6/u7. Reverting u6/u7 leaves prior units dormant.
=== EVIDENCE ===
1186ad8untouched.FINAL_CONSENSUS: YES
[Codex #3] Stage 2 simulation-plan review - IMP-88
=== IMPLEMENTATION_UNITS ===
reviewed_plan: Claude #3 Stage 2 simulation-plan rewind redo
verdict: complete
unit_checks:
coverage_verdict:
layout_adjust: covered by u1/u2/u3/u6/u7.
image_fit: covered by u1/u2/u4/u7.
frame_internal_fit_candidate: covered by u1/u2/u5/u6/u7.
return_to_render: covered by u3/u4/u5 planners plus u6/u7 pipeline execution paths.
step18_linkage: covered by u2 failure classifications and u7 Step 17/18/19 refresh.
u1 rationale: Complete. It fixes the two primary router gaps: image_aspect_mismatch currently has no row and frame_capacity_mismatch currently points at frame_reselect instead of the overview's internal-fit action.
u2 rationale: Complete. The prior missing-producer issue is addressed by adding SALVAGE_FAILURE_TYPE_BY_ACTION rows for the new failed salvage actions.
u3 rationale: Complete. Uses layout preset switching rather than shared spacing shrink, matching the Phase Z spacing guardrail.
u4 rationale: Complete. Uses existing classifier image_event data and scopes changes to image fitting.
u5 rationale: Complete. Keeps frame_internal_fit_candidate inside the declared frame envelope and provides an infeasible path when no envelope exists.
u6 rationale: Complete. Extends the existing salvage loop instead of adding ad hoc dispatcher conditionals, and explicitly re-renders layout_adjust.
u7 rationale: Complete. Adds direct entry triggers for all three issue-body axes and refreshes Step 17/18/19 reporting.
Out-of-scope notes: details_popup_escalation remains #64 work; Step 14/16 detection changes, Stage 0 normalizer integration, AI fallback activation, and sample-specific MDX handling remain excluded.
Rollback strategy: Acceptable. Seven ordered commits u1->u7; u1/u2 are data-surface only, u3-u5 stay dormant until u6/u7, and reverting u6/u7 restores the prior dispatcher behavior.
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
FINAL_CONSENSUS: YES
[Claude #4] Stage 3 code-edit round 1 — IMP-88 u1
Executing unit: u1
unit_executed
u1 — Router rows + status surface for Step 17 retry chain (data-surface only).
Stage 2 binding contract: register the data-surface for
layout_adjust/image_fit/frame_internal_fit_candidateonsrc/phase_z2_router.pyso downstream units (u2 failure_router, u3/u4/u5 planners, u6 dispatcher, u7 Step 17 entry) can wire onto a single source of truth. No planner code, no orchestrator code, no AI calls — exactly per Stage 2 estimate (estimate_lines=30).files_changed
src/phase_z2_router.py— ACTION_BY_CATEGORY + ACTION_RATIONALE + ACTION_IMPLEMENTATION_STATUS edits (data-surface only)tests/phase_z2/test_router_actions_imp88.py— NEW (12 tests covering rows, rationale, status, and route_action / route_fit_classification integration)diff_summary
src/phase_z2_router.pyACTION_BY_CATEGORYrow —image_aspect_mismatch → image_fit. Closes the unmapped classifier emission gap (src/phase_z2_classifier.py:434-447) where Stage 1 verifiedroute_action('image_aspect_mismatch')returnedproposed_action=None,implementation_status='unknown'.ACTION_BY_CATEGORYrow —frame_capacity_mismatch:frame_reselect→frame_internal_fit_candidate. PerPHASE-Z-PIPELINE-OVERVIEW.md:321Step 16 spec lock.frame_reselectremains downstream via the failure_router cascade (rerender_still_fails → frame_reselect).ACTION_RATIONALErows — forimage_aspect_mismatch+ updatedframe_capacity_mismatch(rationale text reflects the new internal-fit-first direction; honorsfeedback_phase_z_spacing_direction— no common-margin shrink claim, frame-scoped only).ACTION_IMPLEMENTATION_STATUSrows —image_fit: MISSING,frame_internal_fit_candidate: MISSING.layout_adjuststays MISSING. Inline comment documents the planner-surface flip convention (IMP-12 u7 cascade + IMP-35 u3 popup precedent): u3/u4/u5 each flip their respective row when the deterministic planner lands insrc/phase_z2_retry.py.Total touched: ~25 lines (within Stage 2
estimate_lines=30).tests/phase_z2/test_router_actions_imp88.py(NEW)12 unit tests grouped by surface:
internal/envelopekeyword check).route_action('image_aspect_mismatch')now returnsimage_fit/MISSING(closing the Stage 1 evidence gap);route_action('frame_capacity_mismatch')now returnsframe_internal_fit_candidate/MISSING; end-to-endroute_fit_classificationenriches classification entries and lists both new actions inmissing_actions_pending_impl.scope-lock verification (Stage 2 contract)
image_fit) and envelope-bound (frame_internal_fit_candidate) — honors feedback_phase_z_spacing_direction.test results (scope-qualified)
Regression check on directly-coupled tests (router popup, failure_router cascade, visual_classifier, spec taxonomy image aspect mismatch):
Scope-qualified: 12 new + 22 directly-coupled = 34 tests green. Full
tests/phase_z2/suite not re-run this turn (Stage 3 unit-scoped).remaining_units
src/phase_z2_failure_router.py: NEXT_ACTION_BY_FAILURE + SALVAGE_FAILURE_TYPE_BY_ACTION rows for the three new actions, NEXT_ACTION_IMPLEMENTATION_STATUS demotions, classifier failure_type producers.src/phase_z2_retry.py:plan_layout_adjust+apply_layout_adjust_cssplanner stub. FlipsACTION_IMPLEMENTATION_STATUS['layout_adjust']to IMPLEMENTED at the planner-surface boundary.src/phase_z2_retry.py:plan_image_fit+apply_image_fit_cssplanner stub. FlipsACTION_IMPLEMENTATION_STATUS['image_fit']to IMPLEMENTED.src/phase_z2_retry.py:plan_frame_internal_fit_candidate+ apply stub. FlipsACTION_IMPLEMENTATION_STATUS['frame_internal_fit_candidate']to IMPLEMENTED. Includes infeasible-path for missing envelope.src/phase_z2_pipeline.py: salvage dispatcher branches (_attempt_salvage_chain) forlayout_adjust+frame_internal_fit_candidate, loop cap, re-render integration.src/phase_z2_pipeline.py: Step 17 entry triggers + image_fit single-pass path + Step 17/18/19 artifact refresh.follow_up_issue_candidates
None — u1 stayed strictly within Stage 2 scope-lock. Two spec-anchor drifts noticed during research, both out-of-scope for #88 but worth a separate doc-sync issue:
docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md:125still recordsframe_capacity_mismatch → frame_reselect(the pre-u1 mapping). Now contradicts bothPHASE-Z-PIPELINE-OVERVIEW.md:321and the freshly-lockedACTION_BY_CATEGORY. Pure doc-sync, no code impact.PHASE-Z-PIPELINE-OVERVIEW.md:321names the image actionimage_fit_candidate(with_candidatesuffix); Stage 2 binding contract usesimage_fit(without suffix). The Stage 2 name was chosen explicitly (Stage 1 unresolved-question Q1 → resolved at Stage 2). Anchor doc sync candidate.Both candidates are doc-only and do not block u2~u7. Recording here for visibility per feedback_anchor_sync_rules (anchor doc SHA-aware sync), not for action in this issue.
next step
Per
feedback_one_step_per_turn: u1 deliverable is complete and the Gitea comment is the binding contract. STOP. u2 work happens in a separate turn after Stage 3 round 1 consensus.— commit_sha: pending (Stage 3 unit-execution turn — no commit per task instructions).
[Codex #4] Stage 3 code-edit verification - IMP-88 u1
Verified unit: u1 router rows + status surface only.
Verdict: correct for the single unit executed in this turn. The edit registers the primary router data surface for the three IMP-88 actions without adding planner, failure-router, dispatcher, AI, or sample-specific behavior.
Checked facts:
src/phase_z2_router.py:47addsimage_aspect_mismatch -> image_fit.src/phase_z2_router.py:48remapsframe_capacity_mismatch -> frame_internal_fit_candidate.src/phase_z2_router.py:65-71adds/updates rationale text for the two routed categories, with image handling scoped to frame/image fit and frame handling scoped to internal envelope fit.src/phase_z2_router.py:89-91keepslayout_adjustasMISSINGand registersimage_fitplusframe_internal_fit_candidateasMISSING, matching the Stage 2 plan that u3/u4/u5 flip statuses when deterministic planners land.tests/phase_z2/test_router_actions_imp88.py:40-206covers ACTION_BY_CATEGORY rows, rationale rows, implementation-status rows, androute_action/route_fit_classificationintegration for the new surfaces.git diff --name-onlyshows onlysrc/phase_z2_router.pyas a tracked file change; the new test file is untracked attests/phase_z2/test_router_actions_imp88.py, as expected for this code-edit unit before commit.Scope check:
src/phase_z2_failure_router.py,src/phase_z2_retry.py, orsrc/phase_z2_pipeline.py; those remain owned by later units.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_router.py tests/phase_z2/test_router_actions_imp88.pyGet-Content -LiteralPath 'src\phase_z2_router.py'Get-Content -LiteralPath 'tests\phase_z2\test_router_actions_imp88.py'pytest -q tests/phase_z2/test_router_actions_imp88.pygit diff --stat -- src/phase_z2_router.pyrg -n "image_aspect_mismatch|frame_capacity_mismatch|image_fit|frame_internal_fit_candidate|layout_adjust" src/phase_z2_router.py tests/phase_z2/test_router_actions_imp88.pygit diff --name-onlygit ls-files --others --exclude-standard tests/phase_z2/test_router_actions_imp88.pyFiles checked:
src/phase_z2_router.pytests/phase_z2/test_router_actions_imp88.pyTest results:
pytest -q tests/phase_z2/test_router_actions_imp88.py->12 passed in 0.04sRemaining units: [u2, u3, u4, u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #5] Stage 3 code-edit round 2 — IMP-88 u2
Executing unit: u2
unit_executed
u2 — failure_router cascade extension (producers + cascade rows + status surface). Data-surface only; no planner, no orchestrator, no AI.
Stage 2 binding contract: register the failure_router data-surface so the three Step 17 retry chain actions (
layout_adjust/image_fit/frame_internal_fit_candidate) participate in the deterministic cascade. The u6 dispatcher (planned) will read salvage_steps[-1].action + retry_trace["next_action_proposal"] off this surface; u3/u4/u5 will land the planners that flip the MISSING rows to IMPLEMENTED. u2 owns mapping only — no executor, no AI, no margin shrink.files_changed
src/phase_z2_failure_router.py— registry additions only (data-surface)tests/phase_z2/test_failure_router_imp88_cascade.py— NEW (12 tests)diff_summary
src/phase_z2_failure_router.pyModule docstring update — escalation hierarchy diagram extended:
IMP-88 u2 section explains the three new failure_type → next_action rows, names the issue-body axes, and cites
feedback_phase_z_spacing_directionas the guardrail (no shared margin shrink — escalate through layout topology instead).FAILURE_TYPE_DESCRIPTIONS— three new rows:layout_adjust_insufficient— 8-preset switch executed but overflow persists; cascade exits onto frame_internal_fit_candidate before V4 top-k frame_reselect.frame_internal_fit_candidate_insufficient— in-envelope variants exhausted; cascade exits onto frame_reselect.image_fit_insufficient— Step 17 single-pass image fit could not resolve image_aspect_mismatch; escalates onto main cascade at layout_adjust.SALVAGE_FAILURE_TYPE_BY_ACTION— three new producer rows so the existing classifier's salvage_steps[-1].action path lands on the new failure_types instead of falling through tonot_attempted:layout_adjust→layout_adjust_insufficientimage_fit→image_fit_insufficientframe_internal_fit_candidate→frame_internal_fit_candidate_insufficientNEXT_ACTION_BY_FAILURE— three new cascade rows:layout_adjust_insufficient→frame_internal_fit_candidate(closes the previously open cascade tail at layout_adjust — the existingfont_step_insufficient → layout_adjustrow had no next-step, so the dispatcher would terminate without escalation)frame_internal_fit_candidate_insufficient→frame_reselect(rejoins existingrerender_still_fails → frame_reselectmid-cascade)image_fit_insufficient→layout_adjust(Phase Z spacing direction — escalate through layout topology, not margin shrink)NEXT_ACTION_RATIONALE— three matching rationale entries.image_fit_insufficientrationale explicitly states "공통 image CSS / 공통 spacing 축소 X (Phase Z spacing direction)" so trace consumers cannot misread the cascade intent.NEXT_ACTION_IMPLEMENTATION_STATUS— two new destination rows registered as MISSING:frame_internal_fit_candidate: MISSING (u5 flips whenplan_frame_internal_fit_candidate+ apply land insrc/phase_z2_retry.py)image_fit: MISSING (u4 flips whenplan_image_fit+ apply land)layout_adjuststays MISSING (untouched — u3 flips it).details_popup_escalationstays MISSING (untouched — IMP-35 contract).frame_reselectstays MISSING.Total file change: +99 / -1. Bulk is documentation comments anchoring the cascade restructuring to
feedback_phase_z_spacing_direction,feedback_ai_isolation_contract, and IMP-12 u7 + IMP-35 u3 precedent. Effective registry rows added: 3 descriptions + 3 producers + 3 cascade rows + 3 rationales + 2 status rows = 14 deterministic mapping entries.tests/phase_z2/test_failure_router_imp88_cascade.py(NEW)12 unit tests grouped by surface:
layout_adjust_insufficient → frame_internal_fit_candidate,frame_internal_fit_candidate_insufficient → frame_reselect,image_fit_insufficient → layout_adjust) + regression guard for the 8 existing rows. Theimage_fit_insufficientrow asserts the rationale does NOT mention "shrink" (Phase Z spacing direction guardrail).classify_retry_failure → route_retry_failureround-trip for all three new salvage actions; plusenrich_retry_trace_with_failure_classificationcomposes the failure_classification + next_action_proposal fields end-to-end for the layout_adjust salvage path.Test results
scope_check
src/phase_z2_failure_router.py. No edits tosrc/phase_z2_router.py,src/phase_z2_retry.py,src/phase_z2_pipeline.py,src/phase_z2_classifier.py. Planner code (u3/u4/u5), dispatcher code (u6), Step 17 entry (u7) all untouched.image_fit_insufficientrationale explicitly forbids margin shrink (feedback_phase_z_spacing_direction).src/phase_z2_ai_fallback/*,src/phase_z2_router.py(popup gate area), or pipeline AI hook area (project_imp46_carveout_caveat).remaining_units
src/phase_z2_retry.py:plan_layout_adjust+apply_layout_adjust_cssplanner/apply pair (deterministic; flipslayout_adjustMISSING → IMPLEMENTED on router + failure_router surfaces).src/phase_z2_retry.py:plan_image_fit+apply_image_fit_css(frame-scoped object-fit + max-w/h from existing classifier image_event).src/phase_z2_retry.py:plan_frame_internal_fit_candidate+ apply (in-envelope variant only; infeasible path when envelope missing).src/phase_z2_pipeline.py: salvage dispatcher branches forlayout_adjust+frame_internal_fit_candidate+ loop cap + re-render.src/phase_z2_pipeline.py: Step 17 entry triggers +image_fitsingle-pass + Step 17/18/19 artifact refresh.follow_up_issue_candidates
None this turn. Scope-lock holds; all surfaces u2 touched are within the Stage 2 binding contract.
next_step
STOP — Codex review of u2 next, then continue_same to u3 in the following turn.
[Codex #5] Stage 3 code-edit verification - IMP-88 u2
Verified unit: u2 failure_router cascade extension.
Verdict: correct for the single unit executed in this turn. The edit adds only the failure-router data surface needed for the three IMP-88 retry actions: producer failure types, cascade rows, rationale/status surface, and focused tests. It does not add planner, dispatcher, AI, Step 16, normalizer, popup body, or sample-specific behavior.
Checked facts:
src/phase_z2_failure_router.py:120-137registers descriptions forlayout_adjust_insufficient,frame_internal_fit_candidate_insufficient, andimage_fit_insufficient.src/phase_z2_failure_router.py:165-167addsSALVAGE_FAILURE_TYPE_BY_ACTIONproducers forlayout_adjust,image_fit, andframe_internal_fit_candidate.src/phase_z2_failure_router.py:195-197routeslayout_adjust_insufficient -> frame_internal_fit_candidate,frame_internal_fit_candidate_insufficient -> frame_reselect, andimage_fit_insufficient -> layout_adjust.src/phase_z2_failure_router.py:236-250adds non-empty rationale entries for the three new failure types, including the spacing guardrail forimage_fit_insufficient.src/phase_z2_failure_router.py:281-282registersframe_internal_fit_candidateandimage_fitasMISSING, leaving planner status flips to later units.tests/phase_z2/test_failure_router_imp88_cascade.py:46-276covers the new descriptions, producers, cascade rows, implementation statuses, classifier/router end-to-end path, enrichment wrapper, and regression guards for existing IMP-12/IMP-35 rows.Scope check:
git diff --name-only -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py src/phase_z2_retry.py src/phase_z2_pipeline.pyshows onlysrc/phase_z2_failure_router.pyamong tracked files in the u2 scope; the new test is untracked attests/phase_z2/test_failure_router_imp88_cascade.py.src/phase_z2_retry.pyorsrc/phase_z2_pipeline.py; u3-u7 still own planners, dispatcher, and Step 17 entry wiring.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.pyGet-Content -LiteralPath 'src\phase_z2_failure_router.py'Get-Content -LiteralPath 'tests\phase_z2\test_failure_router_imp88_cascade.py'pytest -q tests/phase_z2/test_failure_router_imp88_cascade.pyrg -n "layout_adjust_insufficient|frame_internal_fit_candidate_insufficient|image_fit_insufficient|SALVAGE_FAILURE_TYPE_BY_ACTION|NEXT_ACTION_BY_FAILURE|NEXT_ACTION_IMPLEMENTATION_STATUS|classify_retry_failure" src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.pygit diff --name-only -- src/phase_z2_failure_router.py tests/phase_z2/test_failure_router_imp88_cascade.py src/phase_z2_retry.py src/phase_z2_pipeline.pygit ls-files --others --exclude-standard tests/phase_z2/test_failure_router_imp88_cascade.pyFiles checked:
src/phase_z2_failure_router.pytests/phase_z2/test_failure_router_imp88_cascade.pyTest results:
pytest -q tests/phase_z2/test_failure_router_imp88_cascade.py->12 passed in 0.04sRemaining units: [u3, u4, u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #6] Stage 3 code-edit round 3 — IMP-88 u3
Executing unit: u3
unit_executed
u3 —
plan_layout_adjustplanner +apply_layout_adjust_layout_csshelper (Step 17 retry chain — 8-preset topology swap). Plan/apply pair only — no dispatcher wiring, no Step 17 entry, no AI. u6 (dispatcher) and u7 (Step 17 entry) own the runtime callers.Stage 2 binding contract: planner returns
{feasible, new_layout_preset, new_zones_data, position_remap, candidates_considered, swap_topology_from/to, unit_count}on success;{feasible=False, failure_reason}on infeasible.apply_layout_adjust_layout_css(plan, gap_px)builds a freshlayout_cssviabuild_layout_csswith provenance annotations (layout_adjust_applied/from/to). Honors feedback_phase_z_spacing_direction — topology swap only, no shared margin / gap / slide-body shrink.files_changed
src/phase_z2_retry.py—_layout_swap_priority+plan_layout_adjust+apply_layout_adjust_layout_cssappended afterapply_font_step_compression_css. 120 net lines added (+0 / -0 existing), within Stage 2estimate_lines=50order of magnitude (existing planner cluster shares header/docstring overhead).tests/phase_z2/test_phase_z2_retry_layout_adjust.py— NEW (10 tests).diff_summary
src/phase_z2_retry.py(new additions, appended only — existing planners untouched)_layout_swap_priority(current_topology, candidate_topology)— deterministic preference rule:rows↔cols(horizontal-2 ↔ vertical-2 orientation mirror)T↔inverted-T(top-1-bottom-2 ↔ top-2-bottom-1)side-T-left↔side-T-right(left-1-right-2 ↔ left-2-right-1)unit_countsibling (catalog order tiebreak)plan_layout_adjust(*, current_layout_preset, zones_data)LAYOUT_PRESETSfromsrc.phase_z2_composition(catalog:templates/phase_z2/layouts/layouts.yaml, 8-preset vocabulary lock 2026-05-07).failure_reason:'... not in LAYOUT_PRESETS catalog ...'len(zones_data) != len(current_positions)→'... cannot remap to a sibling preset.'unit_count→'... single (1) and grid-2x2 (4) have no swap target by catalog design.'(_layout_swap_priority, catalog_order.index); pickscandidates[0]; emitsposition_remap(current→new) +new_zones_data({**zd, "position": new_positions[i]}preserving template_id / slot_payload / min_height_px / content_weight).apply_layout_adjust_layout_css(plan, gap_px)→Optional[dict]None(dispatcher u6 will skip the re-render).layout_cssdict via deferredfrom src.phase_z2_pipeline import build_layout_css(deferred to break the retry↔pipeline circular import — pipeline imports retry at module top, retry references pipeline only at call time once both are fully loaded).raw_zone_layout["layout_adjust_applied"]=True+layout_adjust_from+layout_adjust_toso retry trace surfaces the swap.tests/phase_z2/test_phase_z2_retry_layout_adjust.py(NEW)10 unit tests grouped by surface:
planner success paths (3):
test_horizontal_2_swaps_to_vertical_2_orientation_axis— full success contract: feasible=True, new_layout_preset='vertical-2', unit_count=2, swap_topology rows→cols, position_remap {top: left, bottom: right}, non-position payload preserved through remap.test_vertical_2_swaps_back_to_horizontal_2— symmetry: reverse swap returns to 'horizontal-2'.test_T_swaps_to_inverted_T_first_by_topology_priority— topology mirroring wins over catalog order: top-1-bottom-2 → top-2-bottom-1 (priority 1) before side-T variants (priority 2). Asserts full candidates_considered order.planner infeasible paths (4):
test_single_preset_infeasible_no_sibling—unit_count=1, candidates_considered=[], failure_reason mentions'single (1)'.test_grid_2x2_preset_infeasible_no_sibling—unit_count=4, candidates_considered=[], failure_reason mentions'grid-2x2 (4)'.test_unknown_preset_infeasible_with_clear_reason— catalog miss path, failure_reason contains'not in LAYOUT_PRESETS catalog'.test_zone_count_mismatch_infeasible— length 3 vs horizontal-2 (2 positions), failure_reason contains both'length 3'and'horizontal-2'.apply paths (3):
test_apply_layout_adjust_builds_new_layout_css_with_provenance— horizontal-2→vertical-2 path returns valid layout_css withraw_zone_layout["layout_adjust_applied"]=True+ from/to fields; css areas matchLAYOUT_PRESETS["vertical-2"]["css_areas"].test_apply_layout_adjust_infeasible_returns_none— infeasible plan short-circuits.test_apply_layout_adjust_T_swap_produces_2d_dynamic_css— top-1-bottom-2 → top-2-bottom-1 path exercises_build_grid_dynamic_2d(dynamic_rows=True, dynamic_cols=True).Test result:
pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.py→ 10 passed in 0.17s.scope guardrails honored
LAYOUT_PRESETS(catalogtemplates/phase_z2/layouts/layouts.yaml), no MDX-specific paths, no sample-keyed logic.src/phase_z2_retry.py(nodetails_popup_escalation/frame_reselect/ IMP-47B cache area)._layout_swap_prioritymatrix derived fromtemplates/phase_z2/layouts/layouts.yamltopology values (rows/cols/T/inverted-T/side-T-left/side-T-right/single/2x2); 2026-05-07 catalog lock unchanged.regression check (scope-qualified)
pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_cross_zone_redistribute.py tests/phase_z2/test_phase_z2_retry_multi_donor.py tests/phase_z2/test_phase_z2_retry_measured_bound.py→ 35 passed in 0.10s. u1 (12) + u2 (12) + cross_zone (8 prior) + multi_donor + measured_bound suites green; no behavior change in existing retry planners.git diff --name-only(tracked) →src/phase_z2_failure_router.py(u2),src/phase_z2_retry.py(u3),src/phase_z2_router.py(u1). New test file attests/phase_z2/test_phase_z2_retry_layout_adjust.pyis untracked (consistent with u1/u2 pre-commit pattern verified by Codex r1/r2).remaining_units
[u4, u5, u6, u7]
follow_up_issue_candidates
None — scope-lock holds. The deferred
from src.phase_z2_pipeline import build_layout_cssinsideapply_layout_adjust_layout_cssis the established cross-module pattern (mirrorsplan_cross_zone_redistributedeferringfrom src.fit_verifier import redistribute) and does not introduce a new module dependency cycle. Position remap is order-preserving by catalog convention (positions list inlayouts.yaml); if a future caller relies on semantic position mapping (e.g., "top" must always carry the most-content zone), that becomes a new axis owned by composition planner, not Step 17 retry.[Codex #6] Stage 3 code-edit verification - IMP-88 u3
Verified unit: u3 layout_adjust planner/apply surface.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic
layout_adjustplanner and layout CSS apply helper insrc/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.Checked facts:
src/phase_z2_retry.py:443adds_layout_swap_priority, preferring rows/cols, T/inverted-T, then side-T-left/side-T-right topology swaps.src/phase_z2_retry.py:455addsplan_layout_adjust, returning the required success payload:feasible,new_layout_preset,unit_count,candidates_considered,swap_topology_from,swap_topology_to,position_remap, andnew_zones_data.src/phase_z2_retry.py:467-508returns explicit infeasible plans for unknown presets, zone-count mismatch, and no same-unit-count render-ready sibling.src/phase_z2_retry.py:530addsapply_layout_adjust_layout_css, returningNonefor infeasible plans and building a freshbuild_layout_css(new_preset, new_zones_data, gap=gap_px)payload for feasible plans.src/phase_z2_retry.py:545-549recordslayout_adjust_applied,layout_adjust_from, andlayout_adjust_toprovenance inraw_zone_layout.tests/phase_z2/test_phase_z2_retry_layout_adjust.py:40-167covers horizontal/vertical swaps, T/inverted-T priority, infeasible cases, provenance, and 2-D dynamic CSS output.Scope check:
git diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.pyshowssrc/phase_z2_retry.pyas the u3 tracked implementation file; the router and failure-router diffs are prior u1/u2 work.git ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_layout_adjust.pyconfirms the new focused u3 test file is untracked before commit.src/phase_z2_pipeline.py; u6/u7 still own dispatcher and Step 17 entry wiring.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_layout_adjust.pyGet-Content -LiteralPath 'src\phase_z2_retry.py'Get-Content -LiteralPath 'tests\phase_z2\test_phase_z2_retry_layout_adjust.py'pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.pyrg -n "def _layout_swap_priority|def plan_layout_adjust|def apply_layout_adjust_layout_css|layout_adjust_applied|build_layout_css|gap|margin|slide-body|new_layout_preset|new_zones_data" src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_layout_adjust.pygit diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.pygit ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_layout_adjust.pyFiles checked:
src/phase_z2_retry.pytests/phase_z2/test_phase_z2_retry_layout_adjust.pyTest results:
pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.py->10 passed in 0.08sRemaining units: [u4, u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #7] Stage 3 code-edit round 4 — IMP-88 u4
Executing unit: u4
unit_executed
u4 —
plan_image_fit+apply_image_fit_css(Step 17 entry single-pass, image-scoped CSS overrides). Plan/apply pair only — no dispatcher wiring, no Step 17 entry runtime, no AI. u7 (Step 17 entry) owns the runtime caller.Stage 2 binding contract: planner consumes a single
image_eventfromoverflow_metrics.image_events(the shape produced atsrc/phase_z2_pipeline.py:3019-3060:natural_w/h,rendered_w/h,natural_ratio,rendered_ratio,delta,src,zone_position,zone_template_id) and returns{feasible, css_overrides: {object_fit, max_width_px, max_height_px, width, height}, delta, correction_axis, natural_*, rendered_*}on success, or{feasible=False, failure_reason}on no-op / infeasible.apply_image_fit_css(plan)builds a frame-scoped CSS rule string. Honors feedback_phase_z_spacing_direction — image-scoped only, no common margin / frame envelope shrink.files_changed
src/phase_z2_retry.py—plan_image_fit+apply_image_fit_cssappended afterapply_layout_adjust_layout_css. ~110 net lines added (function bodies + section header), within Stage 2estimate_lines=45order of magnitude (planner cluster shares header/docstring overhead with u3).tests/phase_z2/test_phase_z2_retry_image_fit.py— NEW (13 tests).diff_summary
src/phase_z2_retry.py(new additions, appended only — existing planners untouched)Section header comment — IMP-88 u4 block documenting:
overflow_metrics.image_eventsdirectly (persrc/phase_z2_pipeline.py:3019-3060JS injection shape).feedback_phase_z_spacing_direction— image-scoped CSS only.delta_tol=0.05mirrorssrc.phase_z2_pipeline.IMAGE_ASPECT_DELTA_TOL(line 150) but is exposed as a kwarg so tests stay free of the pipeline import cycle (same pattern the classifier sidesteps via deferredfrom phase_z2_pipeline import IMAGE_ASPECT_DELTA_TOLatsrc/phase_z2_classifier.py:360).plan_image_fit(*, image_event, delta_tol=0.05)→dictdelta is None→feasible=False,failure_reason="image_event delta is None — image not loaded; no aspect mismatch can be measured."Mirrors classifier line 431 contract (delta is None ⇒ skip — image not loaded; no false positive).|delta| <= delta_tol→feasible=False,failure_reasoncites computed|delta|+ tolerance. Strict-greater-than emission threshold matchessrc/phase_z2_classifier.py:433(abs(delta) > IMAGE_ASPECT_DELTA_TOL).rendered_worrendered_h→feasible=False,failure_reason="image_event missing positive rendered_w / rendered_h — cannot bound max-width / max-height for image-scoped CSS."feasible=Truecorrection_axis = "width" if delta > 0 else "height"(delta>0 ⇒ rendered_ratio > natural_ratio ⇒ rendered too wide ⇒ width axis correction; delta<0 ⇒ height axis correction).css_overrides = {"object_fit": "contain", "max_width_px": int(rendered_w), "max_height_px": int(rendered_h), "width": "auto", "height": "auto"}—object-fit: containpreserves natural aspect (the only safe transform — image not cropped, image not stretched; honors CLAUDE.md image-processing principle).max-w/hclamps to current rendered bounds (no envelope growth, no envelope shrink).apply_image_fit_css(plan)→Optional[str]None(u7 will skip the re-render injection).src→ selector =.zone[data-zone-position="<pos>"] img[src="<src>"]. The src predicate prevents the rule from cross-leaking onto siblings if a zone holds multiple<img>(table_events / IMP-15 idiom: one event per<img>).src→ selector falls back to.zone[data-zone-position="<pos>"] img(defensive — emptysrcis theoretically valid for inline-rendered images; logged as no-op-equivalent CSS but stays scoped to the zone).css_overrides. Nopadding/margin/gap/.slide-body/.zone-containertouched.tests/phase_z2/test_phase_z2_retry_image_fit.py(NEW — 13 tests)Grouped by surface:
Planner success paths (3):
test_positive_delta_emits_width_correction_axis— natural 1200×800, rendered 600×300, delta=+0.5 →correction_axis='width',max_width_px=600,max_height_px=300.test_negative_delta_emits_height_correction_axis— natural 1600×800, rendered 400×400, delta=-0.40 →correction_axis='height'.test_planner_passes_through_zone_and_template_metadata— src / zone_position / zone_template_id surface from image_event into plan.Planner infeasible / no-op paths (6):
test_delta_none_infeasible_with_clear_reason— mirrors classifierdelta is None ⇒ skipcontract.test_delta_within_tolerance_returns_planner_noop—delta=0.02< defaulttol=0.05.test_delta_at_boundary_is_planner_noop—|delta|==tolis no-op (strict-greater-than emission threshold matches classifier).test_rendered_w_zero_infeasible—rendered_w=0⇒ infeasible with rendered_w/h reason.test_rendered_h_missing_infeasible— poppedrendered_h⇒ infeasible.test_custom_delta_tol_widens_noop_band— caller-supplieddelta_tolkwarg works.apply_image_fit_cssCSS construction (4):test_apply_image_fit_css_emits_zone_and_src_scoped_selector— selector + 5 properties verified.test_apply_image_fit_css_without_src_falls_back_to_zone_only_selector— empty src ⇒.zone[...] img {shape.test_apply_image_fit_css_infeasible_returns_none.test_apply_image_fit_css_does_not_shrink_shared_spacing— explicit guardrail: CSS must NOT contain.slide-body,.zone-container,padding:,margin:,gap:,grid-gap. Mirrorsfeedback_phase_z_spacing_direction.test results
Regression check across u1+u2+u3+u4 IMP-88 surface:
scope_lock verification
git diff --stat -- src/phase_z2_retry.py→ 229 insertions, single file (combined u3+u4 additions; u4-only span = ~110 linesplan_image_fit+apply_image_fit_css+ section header).git diff --name-only -- src/phase_z2_pipeline.py→ empty (no u6/u7 leakage).git diff -- src/phase_z2_retry.py | grep '^\+.*def 'lists exactly:_layout_swap_priority/plan_layout_adjust/apply_layout_adjust_layout_css(u3, prior turn) +plan_image_fit/apply_image_fit_css(u4, this turn). No edits to existing planners.frame_internal_fit_candidateplanner — u5 owns that.guardrails honored
feedback_phase_z_spacing_direction— image-scoped CSS only (object-fit: contain+max-w/hclamped to current rendered bounds). Explicit test asserts CSS does NOT touch.slide-body,.zone-container,padding,margin,gap,grid-gap. No envelope shrink —max-w/h≤ current rendered bounds bounds the image within its current area, never expanding the frame.feedback_no_hardcoding—delta_tolis a parameter (default mirrors the canonicalIMAGE_ASPECT_DELTA_TOL=0.05). No inline mdx-specific conditionals.feedback_ai_isolation_contract— no AI calls; planner is fully deterministic.feedback_one_step_per_turn— exactly u4 implemented this turn.project_imp46_carveout_caveat— u4 touchessrc/phase_z2_retry.py(planner-only surface), not the AI reject path area of #76 commit1186ad8.remaining_units
[u5, u6, u7]
plan_frame_internal_fit_candidateplanner + apply helper (frame contract envelope, infeasible-when-no-envelope path) insrc/phase_z2_retry.py+tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py._attempt_salvage_chaininsrc/phase_z2_pipeline.py:2574-2604) forlayout_adjust+frame_internal_fit_candidatebranches + loop cap + re-render;tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py.image_fitsingle-pass path consuming u4's planner) + Step 17/18/19 artifact refresh;tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py.follow_up_issue_candidates
None this turn — u4 strictly within Stage 2 scope-lock. No new axes surfaced during implementation.
stop_marker
STOP per Stage 3 protocol — Gitea comment is the deliverable. No commit, no push this turn.
[Codex #7] Stage 3 code-edit verification - IMP-88 u4
Verified unit: u4 image_fit planner/apply surface.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic
image_fitplanner and frame-scoped CSS apply helper insrc/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.Checked facts:
src/phase_z2_retry.py:564addsplan_image_fit, consuming a singleimage_eventwithsrc,zone_position,zone_template_id, ratio fields, rendered dimensions, anddelta.src/phase_z2_retry.py:579-604returns explicit infeasible/no-op plans fordelta is None,abs(delta) <= delta_tol, and missing or non-positiverendered_w/rendered_h.src/phase_z2_retry.py:605-628returns the required success payload:feasible,css_overrides,delta,correction_axis, natural dimensions/ratio fields, and rendered dimensions/ratio fields.src/phase_z2_retry.py:631addsapply_image_fit_css, returningNonefor infeasible plans and a.zone[data-zone-position="..."] img/img[src="..."]scoped CSS rule for feasible plans.object-fit,max-width,max-height,width,height) and does not alter shared margins, gaps, slide body, frame envelope, or common spacing.tests/phase_z2/test_phase_z2_retry_image_fit.py:55-207covers positive/negative delta success paths, metadata passthrough, no-op/infeasible cases, custom tolerance, scoped CSS generation, src fallback, and shared-spacing guardrails.Scope check:
git diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_image_fit.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.pyshowssrc/phase_z2_retry.pyas the u4 tracked implementation file; router and failure-router diffs are prior u1/u2 work.git ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_image_fit.pyconfirms the focused u4 test file is untracked before commit.src/phase_z2_pipeline.py; u6/u7 still own dispatcher and Step 17 entry wiring.=== EVIDENCE ===
Commands run:
git status --shortgit diff -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_image_fit.pyGet-Content -LiteralPath 'src\phase_z2_retry.py'Get-Content -LiteralPath 'tests\phase_z2\test_phase_z2_retry_image_fit.py'rg -n "def plan_image_fit|def apply_image_fit_css|IMP-88 u4|def plan_layout_adjust|def apply_layout_adjust_layout_css|def plan_cross_zone_redistribute|def plan_glue_compression|def plan_font_step_compression" src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_image_fit.pypytest -q tests/phase_z2/test_phase_z2_retry_image_fit.pygit diff --name-only -- src/phase_z2_retry.py tests/phase_z2/test_phase_z2_retry_image_fit.py src/phase_z2_pipeline.py src/phase_z2_failure_router.py src/phase_z2_router.pygit ls-files --others --exclude-standard tests/phase_z2/test_phase_z2_retry_image_fit.pyFiles checked:
D:\ad-hoc\kei\design_agent\src\phase_z2_retry.pyD:\ad-hoc\kei\design_agent\tests\phase_z2\test_phase_z2_retry_image_fit.pyTest results:
pytest -q tests/phase_z2/test_phase_z2_retry_image_fit.py-> 13 passed in 0.04sRemaining units: [u5, u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #8] Stage 3 code-edit round 5 — IMP-88 u5
Executing unit: u5
unit_executed
u5 —
plan_frame_internal_fit_candidate+apply_frame_internal_fit_candidate_css(Step 17 retry chain — frame-contract envelope only). Plan/apply pair only — no dispatcher wiring, no Step 17 entry runtime, no AI. u6 (dispatcher) and u7 (Step 17 entry) own the runtime callers.Stage 2 binding contract: planner consumes
frame_template_id+ optionalframe_contract(overridable kwarg, mirrors u4'sdelta_tolto keep tests free of the mapper catalog cache / pipeline import cycle) + optionaloverflow_zone(providesexcess_y). Returns{feasible, envelope_present, envelope_keys, candidates_considered, selected_variant, selected_variant_budget_px, excess_y, css_overrides}on success;{feasible=False, envelope_present, candidates_considered, failure_reason}on infeasible (4 distinct paths).apply_frame_internal_fit_candidate_css(plan)builds a frame-scoped CSS rule (.zone[data-template-id="<id>"]selector) from the selected variant'scss_overrides. Honorsfeedback_phase_z_spacing_direction— frame-scoped only, no common margin / slide-body / zone gap shrink.files_changed
src/phase_z2_retry.py—plan_frame_internal_fit_candidate+apply_frame_internal_fit_candidate_cssappended afterapply_image_fit_css. ~145 net lines added (section header + planner with 4 infeasible branches + apply helper). Within Stage 2estimate_lines=50order of magnitude relative to u3 (120 lines actual) and u4 (110 lines actual) — header / docstring / multiple infeasible failure_reason branches share the convention.tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py— NEW (13 tests).diff_summary
src/phase_z2_retry.py(new additions, appended only — existing planners untouched)Section header comment — IMP-88 u5 block documenting:
PHASE-Z-PIPELINE-OVERVIEW.md:333— sub-mechanism allowed names (density envelope / line rhythm / internal grid row / text block allocation) all unified under the single labelframe_internal_fit_candidateso common-CSS/padding shrink antipatterns stay quarantined per[[feedback_phase_z_spacing_direction]].excess_budget_px >= effective excess_y(greedy). Catalog order = catalog author's priority (mirrors u3's_layout_swap_prioritypriority semantics).internal_envelope, so planner returnsinfeasible(envelope_present=False)for every live frame today. Cascade hand-off (from u2):NEXT_ACTION_BY_FAILURE['frame_internal_fit_candidate_insufficient'] = 'frame_reselect'.frame_contractis overridable kwarg (mirrors u4delta_tolpattern); whenNone, defersfrom src.phase_z2_mapper import get_contractto break the retry↔mapper cache binding (tests pay no catalog I/O).plan_frame_internal_fit_candidate(*, frame_template_id, frame_contract=None, overflow_zone=None)→dictget_contract(template_id)returnsNone→feasible=False, envelope_present=False, failure_reason="no frame contract registered for template_id '<id>' — cannot enumerate internal_envelope."internal_envelopedict →failure_reasoncites template_id + "cascade should escalate to frame_reselect" (mirrors u2'sframe_internal_fit_candidate_insufficient → frame_reselectcascade row).internal_envelope.variantsempty →envelope_present=True, envelope_keys=sorted(envelope.keys()), candidates_considered=[].excess_budget_px >= excess_y→envelope_present=True, excess_y, candidates_considered=[v.name, ...], failure_reasoncites variant count + excess_y.excess_y = ceil(overflow_zone['excess_y'])(sub-pixel rounding mirrorsplan_zone_ratio_retryat line 77); walks variants in catalog order, picks first variant withexcess_budget_px >= excess_y. Emits{feasible=True, envelope_present=True, envelope_keys, candidates_considered, selected_variant, selected_variant_budget_px, excess_y, css_overrides: dict(variant['css_overrides'])}.action="frame_internal_fit_candidate"+frame_template_idfor downstream salvage_steps[-1] consumption (u2'sSALVAGE_FAILURE_TYPE_BY_ACTION['frame_internal_fit_candidate'] = 'frame_internal_fit_candidate_insufficient').apply_frame_internal_fit_candidate_css(plan)→Optional[str]None(dispatcher u6 will skip the re-render)..zone[data-template-id="<template_id>"]+ body lines fromplan["css_overrides"].items()(one<prop>: <value>;per line).apply_image_fit_css(.zone[data-zone-position="..."]) and the DOM marker attemplates/phase_z2/slide_base.html:369(data-template-id="{{ zone.template_id }}").css_overrides.tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py(NEW)13 unit tests grouped by surface:
Planner — no-envelope infeasible paths (4):
test_no_contract_infeasible_with_clear_reason— empty-dict contract → envelope_present=False + "internal_envelope" in failure_reason.test_contract_lookup_none_returns_no_contract_failure— sentinel template_id__sentinel_unregistered_template__exercises the defaultget_contractfallback path; mapper returnsNone; planner emits "no frame contract registered" + template_id in failure_reason.test_contract_without_internal_envelope_infeasible— contract declares nointernal_envelopekey → failure_reason cites template_id + "frame_reselect".test_envelope_present_but_empty_variants_infeasible—internal_envelope.variants=[]→ envelope_present=True, candidates_considered=[], "no variants" in failure_reason.Planner — feasible paths (5):
test_single_variant_within_budget_is_selected— 1 variant, excess_y=24, budget=40 → selected_variant emitted with budget + css passthrough (font-size +--frame-density).test_greedy_walk_picks_first_variant_that_fits_in_catalog_order— 3 variants (budgets 10/60/200), excess_y=45 → second variant selected (first that fits); candidates_considered records full ordered name list.test_no_overflow_zone_picks_first_variant— overflow_zone=None → excess_y defaults to 0 → first catalog variant wins (5/100 case → "first").test_all_variants_below_budget_returns_infeasible_with_excess— budgets (10, 25), excess_y=80 → infeasible; failure_reason cites "excess_y=80px"; candidates_considered records full list.test_excess_y_is_ceil_rounded— excess_y=23.4 → rounds up to 24; variant(budget=24) exact-fits.Planner — telemetry (1):
test_envelope_keys_recorded_for_telemetry— envelope_keys surfaces extra keys (envelope_kind) alongsidevariantsfor downstream debug.json.Apply — frame-scoped CSS snippet (3):
test_apply_emits_frame_template_scoped_selector— selector.zone[data-template-id="three_parallel_requirements"]+ body lines for--frame-density,font-size,line-height.test_apply_infeasible_returns_none— apply returns None on infeasible plan.test_apply_does_not_shrink_shared_spacing— explicit guardrail: emitted CSS must not contain.slide-body,.zone-container,grid-gap,gap:,padding:,margin:. Citesfeedback_phase_z_spacing_direction.verification
Cross-unit regression sanity (u1+u2+u3+u4+u5 together):
scope_lock observance
src/phase_z2_retry.pyonly (verifiedgit diff --name-onlyreturns only the three modified files = u1/u2 router/failure_router from earlier rounds + u5 retry.py from this round; no other tracked file touched).tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py(no commit, per "DO NOT commit or push" rule)._attempt_salvage_chaininsrc/phase_z2_pipeline.py:2598-2604untouched.src/phase_z2_pipeline.pyuntouched.src/phase_z2_router.py+src/phase_z2_failure_router.pycarry only the prior u1/u2 staged data-surface changes..zone[data-template-id="..."]-scoped selector with author-declared envelopecss_overrides; explicit guardrail test asserts.details_popup_escalationbody work (closed in #64 — out of scope per Stage 1).1186ad8AI reject path untouched (project_imp46_carveout_caveat).stage 2 alignment
src/phase_z2_retry.pytests/phase_z2/test_phase_z2_retry_frame_internal_fit.py(NEW, 13 tests)internal_envelope; apply scopes to.zone[data-template-id="<id>"]remaining_units
[u6, u7]_attempt_salvage_chaindispatcher branches forlayout_adjust+frame_internal_fit_candidate(loop cap + re-render). 1 file:src/phase_z2_pipeline.py. 1 test:tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py. Will activate planners landed in u3 + u5 by registering executors into_SALVAGE_FAIL_BY_ACTIONdispatch table and flippingACTION_IMPLEMENTATION_STATUSrows forlayout_adjust+frame_internal_fit_candidatefrom MISSING → IMPLEMENTED.image_fitsingle-pass path + Step 17/18/19 artifact refresh. 1 file:src/phase_z2_pipeline.py. 1 test:tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py. Will flipACTION_IMPLEMENTATION_STATUS['image_fit']MISSING → IMPLEMENTED + register Step 17 entry triggers for all three IMP-88 axes + refresh Step 17/18/19 reporting.follow_up_issue_candidates
None — Stage 2 plan scope intact. The dormant
internal_envelopecatalog field is the documented execution contract surface for future contract declarations; declaringinternal_envelopeentries on actual frame contracts (currently 0 of 32 frames) is a separate axis suitable for a follow-up issue but NOT required by #88 (this issue's scope is "register the 3 missing executors so visual_check doesn't abort", which u5 satisfies viainfeasible(envelope_present=False)cascade hand-off toframe_reselect).DO NOT commit / push — per Stage 3 orchestrator rule. Edit + test verified locally; Codex independent verification next.
[Codex #8] Stage 3 code-edit verification - IMP-88 u5
Verified unit: u5 frame_internal_fit_candidate planner/apply surface.
Verdict: correct for the single unit executed in this turn. The edit adds the deterministic frame_internal_fit_candidate planner and frame-scoped CSS apply helper in src/phase_z2_retry.py, plus focused tests. It does not wire dispatcher runtime, Step 17 entry, Step 16, AI, popup escalation, normalizer, or sample-specific behavior.
Checked facts:
Scope check:
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Remaining units: [u6, u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #9] Stage 3 code-edit round 6 — IMP-88 u6
Executing unit: u6
unit_executed
u6 — Salvage dispatcher branches for
layout_adjust+frame_internal_fit_candidateinsrc/phase_z2_pipeline.py._attempt_salvage_chain. Loop-cap extension via_SALVAGE_FAIL_BY_ACTION(3 → 5 entries). Plan/apply pair invocations + re-render path + step-record shape — no Step 17 entry runtime (u7 owns it), no AI, no Step 16, no normalizer, no popup body, no sample-specific behavior.Stage 2 binding contract: extend
_SALVAGE_FAIL_BY_ACTIONso the loop cap auto-scales; dispatchlayout_adjustvia the distinctrender_slide(new_preset, new_zones_data, new_layout_css)path (no CSS overlay — topology swap only per feedback_phase_z_spacing_direction); dispatchframe_internal_fit_candidatevia the existing CSS-overlay path withframe_template_idresolved fromzones_dataviacascade_inputs.zone_position.files_changed
src/phase_z2_pipeline.py— imports +_SALVAGE_FAIL_BY_ACTIONextension + dispatcher loop body (2 new branches).tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py— NEW (8 tests).tests/phase_z2/test_phase_z2_step17_salvage_chain.py— u15test_case_c_all_three_fail_revert_preservedassertion updated from 3-step to 5-step cascade (u6 loop-cap regression fix; (b)-revert semantic unchanged). Single-test surgical edit, no other test in the file touched.diff_summary
src/phase_z2_pipeline.pyImports (line 62-75) — add 4 symbols from
phase_z2_retry:plan_layout_adjust+apply_layout_adjust_layout_css(u3)plan_frame_internal_fit_candidate+apply_frame_internal_fit_candidate_css(u5)_SALVAGE_FAIL_BY_ACTION(line 2574-2590) — extended 3 → 5 entries; comment notes it mirrorsfailure_router.SALVAGE_FAILURE_TYPE_BY_ACTION(u2) and that the loop-caprange(len(_SALVAGE_FAIL_BY_ACTION))auto-scales with cascade depth:layout_adjust: layout_adjust_insufficientframe_internal_fit_candidate: frame_internal_fit_candidate_insufficientimage_fitdeliberately NOT registered (u7 owns Step 17 entry single-pass, not salvage cascade).Dispatcher loop body (line 2605-2680ish) — two new branches added BEFORE the existing CSS-overlay planner cluster:
layout_adjust(distinct render path — pre-block):plan_layout_adjust(current_layout_preset=layout_preset, zones_data=zones_data).new_layout_cssviaapply_layout_adjust_layout_css(plan, gap_px=gap_px); re-renders viarender_slide(slide_title, slide_footer, plan["new_zones_data"], plan["new_layout_preset"], new_layout_css, gap_px=gap_px)— NO CSS overlay (topology swap only honors feedback_phase_z_spacing_direction).new_layout_presetinstead ofcss_override(observability for downstream classifiers / Step 18).out_path.write_text(candidate_html)+salvage_passed=True+ return.failure_type = "layout_adjust_insufficient",continueto next loop iteration (cascade routes toframe_internal_fit_candidatevia failure_router u2).frame_internal_fit_candidate(shared CSS-overlay path):frame_template_idfromzones_databy matchingcascade_inputs.zone_position.plan_frame_internal_fit_candidate(frame_template_id=..., overflow_zone={"excess_y": float(cascade_inputs["excess_px"])}).apply_fn = apply_frame_internal_fit_candidate_css→ falls through to the shared CSS-overlay render path.out_path.write_text+ return. On FAIL →failure_type = "frame_internal_fit_candidate_insufficient"→ cascade routes toframe_reselect(NOT in_SALVAGE_FAIL_BY_ACTION→ terminal exit recorded assalvage_terminal_action).No mutation of
layout_preset/zones_data/layout_cssbetween iterations — each plan runs against the chain-entry snapshot (matches the existing pattern). MDX 원문 무손실 보존 — no content edits anywhere in the dispatcher.tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py(NEW)8 tests against
_attempt_salvage_chain, mirroring the u15 test conventions (_PROJECT_ROOTfixture,_patch_renderhelper, monkey-patchedrun_overflow_check, IMP-09 gate-passinglayout_css):test_salvage_fail_map_registers_imp88_actionsimage_fitdeliberately absenttest_layout_adjust_pass_promotes_final_htmldata-rendered-preset='vertical-2'in promoted HTML; step recordsnew_layout_preset="vertical-2"; nocss_overridefieldtest_layout_adjust_infeasible_no_sibling_cascade_advancessinglepreset → no sibling → infeasible; cascade advances toframe_internal_fit_candidate; patchedget_contract→ None → envelope-missing terminal exit (salvage_terminal_action="frame_reselect")test_layout_adjust_fail_cascade_to_frame_internal_fit_passrender_slidecalls countedtest_frame_internal_fit_candidate_pass_promotes_final_htmlinitial_failure_type="layout_adjust_insufficient"; stub envelope variantdensity_envelopeselected;line-height: 1.4incss_overridetest_frame_internal_fit_candidate_no_envelope_cascade_terminalinternal_envelope→envelope_present=False;salvage_terminal_action="frame_reselect"test_full_5_step_cascade_all_fail_loop_cap_respectedtest_layout_adjust_step_has_no_css_overlay_fieldcss_overridekey, HASnew_layout_preset+candidate_pathtests/phase_z2/test_phase_z2_step17_salvage_chain.py(regression fix — single-test surgical edit)test_case_c_all_three_fail_revert_preservedpreviously asserted exactly 3 salvage_steps. u6's loop-cap extension means the cascade now naturally runs through 5 stages when every stage is infeasible (the emptyzones_datafrom_kwargs()makesplan_layout_adjustinfeasible via length mismatch; the empty resultingtemplate_idmakesplan_frame_internal_fit_candidateinfeasible via no-contract). Updates:len(salvage_steps) == 3→== 5(with anchor comment citing IMP-88 u6).actionslist extended to includelayout_adjust+frame_internal_fit_candidate.step["css_override"]→step.get("css_override")(layout_adjust step has nocss_overridekey by design — u6 distinct-render-path contract).No other test in the file is touched (case_a / case_b assertions remain byte-identical).
Stage 2 estimate fit
estimate_lines=50for u6 implementation — actual: ~75 lines insrc/phase_z2_pipeline.py(2 imports + 2 map entries + ~70 lines for the two dispatcher branches). Within order of magnitude — thelayout_adjustbranch is naturally larger than a single CSS-overlay branch because it owns its own render+write+step-record sequence (vs. sharing the post-branch boilerplate).Scope guard
src/phase_z2_failure_router.py(u2),src/phase_z2_router.py(u1),src/phase_z2_retry.py(u3/u4/u5) — untouched in this round; the diff entries fromgit diff --name-onlyare prior-round work already verified by Codex.1186ad8(details_popup_escalationbody) maintained; u6 only touches the salvage dispatcher loop, not the popup gate executor atphase_z2_pipeline.py:6393-6419.Test results
pytest -q tests/phase_z2/→ 571 passed (full phase_z2 suite green, including u1~u5 tests + u15 cascade tests + new u6 tests).Focused:
pytest -q tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py→ 8 passed.Focused:
pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py→ 3 passed.remaining_units
layout_adjust/image_fit/frame_internal_fit_candidate(per overflow classification at Step 17 entry, NOT via cascade salvage).image_fitsingle-pass path (u4 planner consumer). Step 17/18/19 artifact refresh after salvage path completes.MISSING → IMPLEMENTEDstatus flips forimage_fit(u4) andframe_internal_fit_candidate(u5) insrc/phase_z2_router.pysince the runtime caller will be wired.follow_up_issue_candidates
None within scope-lock. Out-of-scope-but-noted (already excluded by Stage 1 lock):
details_popup_escalationbody completion → tracked under #64 IMP-35 (closed deliverable verified at Stage 1).[Codex #9] Stage 3 code-edit verification - IMP-88 u6
Verified unit: u6 salvage dispatcher branches for layout_adjust and frame_internal_fit_candidate.
Verdict: correct for the single unit executed in this turn. The edit extends the Step 17 salvage loop executor map from 3 actions to 5 actions and dispatches layout_adjust and frame_internal_fit_candidate through the appropriate planner/apply paths. It does not wire the Step 17 entry runtime, image_fit single-pass, Step 16, AI, popup body, normalizer, or sample-specific behavior.
Checked facts:
Scope check:
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Remaining units: [u7]
rewind_target: continue_same
FINAL_CONSENSUS: NO
[Claude #10] Stage 3 code-edit round 7 — IMP-88 u7
Executing unit: u7
unit_executed
u7 — Step 17 entry runtime caller:
_attempt_step17_image_fit_single_passhelper + §11.7.1 image_fit single-pass entry trigger + §11.7.2 direct entry triggers (layout_adjust / frame_internal_fit_candidate / image_fit_insufficient escalation) + Step 17/18/19 artifact refresh on PASS. Single-file pipeline edit; no router/failure_router/retry changes; no AI; no Step 16; no normalizer; no popup body; no sample-specific behavior. u6 (dispatcher) already wired the cascade; u7 wires the Step 17 ENTRY.Stage 2 binding contract: §11.7.1 calls
_attempt_step17_image_fit_single_pass(helper appended after_attempt_salvage_chain— matches the u6 testability convention) to run a Step 17 ENTRY single-pass when router proposesimage_fit. PASS promotes final.html + re-runs classify + route + enrich so Step 17/18/19 artifacts reflect the post-image_fit state; FAIL recordsimage_fitstep with failure_reason so failure_router (u2)SALVAGE_FAILURE_TYPE_BY_ACTIONclassifies it asimage_fit_insufficient. §11.7.2 readssalvage_steps[-1].action/passed+router.proposed_actions_summaryand routes into_attempt_salvage_chainwith a syntheticinitial_failure_type(image_fit_insufficient/font_step_insufficient/layout_adjust_insufficient) so the cascade tail (frame_reselect → details_popup_escalation) stays consistent with the donor_slack pathway.files_changed
src/phase_z2_pipeline.py— 2 imports (apply_image_fit_css, plan_image_fit) +_attempt_step17_image_fit_single_passhelper + §11.7.1 image_fit single-pass entry trigger + §11.7.2 direct entry triggers + Step 17 artifact note update.tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py— NEW (12 tests).diff_summary
src/phase_z2_pipeline.pyImports (line 62-77) — adds 2 symbols from
phase_z2_retry:apply_image_fit_css(u4)plan_image_fit(u4)(The other 4 IMP-88 retry imports —
apply_frame_internal_fit_candidate_css,apply_layout_adjust_layout_css,plan_frame_internal_fit_candidate,plan_layout_adjust— were already added by u6.)_attempt_step17_image_fit_single_pass(...)(new helper, ~85 lines) — appended after_attempt_salvage_chainand BEFORE_remeasure_after_frame_reselect. Pure data-surface executor:image_events→ per-eventplan_image_fit(image_event=ev, delta_tol=delta_tol)→ if feasible,apply_image_fit_css(plan)accumulated.{triggered: False, ..., event_plans: [...]}early (every plan still surfaced for Step 17/18/19 telemetry continuity).render_slide, injects aggregated style block before</head>(or prepends if no</head>), writes candidate tosalvage_image_fit_candidate.html, runsrun_overflow_check.out_path; step recordspost_salvage_overflow.out_pathuntouched; step recordsfailure_reason = candidate_overflow.fail_reasons or "image_fit single-pass: overflow persists". (Strict PASS-only promotion guard, mirrors_attempt_salvage_chain.)triggered / passed / step / candidate_html / candidate_overflow / event_plans. Thestepshape matches the salvage_steps[] entry pattern thatfailure_routeru2SALVAGE_FAILURE_TYPE_BY_ACTIONreads (action="image_fit",image_fit_event_plans,candidate_path,post_salvage_overflow/failure_reason).delta_tol=IMAGE_ASPECT_DELTA_TOL(module constant atsrc/phase_z2_pipeline.py:154); exposed as kwarg so tests stay free of any pipeline-state coupling.§11.7.1 image_fit single-pass entry trigger (new block, ~40 lines) — placed AFTER the existing donor_slack salvage cascade block (§11.7) and BEFORE the popup gate (§11.8). Guards:
not retry_trace.get("retry_passed")not retry_trace.get("salvage_passed")(skip if donor_slack cascade already promoted final.html)"image_fit" in router_decision.get("proposed_actions_summary")(router u1 mapsimage_aspect_mismatch → image_fit)On trigger: calls
_attempt_step17_image_fit_single_pass(...). Onresult["triggered"]: appendsresult["step"]toretry_trace.salvage_steps, setssalvage_attempted=True. Onresult["passed"]: setssalvage_passed=True, refreshesoverflow / fit_classification / router_decision / retry_trace.failure_classificationvia the same pattern as the donor_slack block at lines 6428-6442 (Step 17/18/19 artifact refresh).§11.7.2 direct entry triggers (new block, ~80 lines) — placed AFTER §11.7.1 and BEFORE the popup gate (§11.8). Guards same as §11.7.1 (retry not passed, salvage not passed). Decision tree:
salvage_steps[-1]is aimage_fitFAIL →initial_failure_type = "image_fit_insufficient"(escalates image_fit single-pass FAIL onto the cascade per u2NEXT_ACTION_BY_FAILURE["image_fit_insufficient"] = "layout_adjust").layout_adjust→initial_failure_type = "font_step_insufficient"(u2 routesfont_step_insufficient → layout_adjust).frame_internal_fit_candidate→initial_failure_type = "layout_adjust_insufficient"(u2 routeslayout_adjust_insufficient → frame_internal_fit_candidate).On non-None
_u7_initial: assemblescascade_inputsfrom the entry classification'szone_position+debug_zones+overflow.zones(mirrors the donor_slack block's FitAnalysis/RoleFit synthesis at lines 6397-6409) and calls_attempt_salvage_chain(initial_failure_type=_u7_initial, ...). Merges_u7_salvageintoretry_trace, preserving any priorsalvage_stepsfrom §11.7.1. Onsalvage_passed: same Step 17/18/19 refresh pattern.Step 17 artifact note (line 6675) — updated to surface u6/u7 wiring:
"Terminal actions (layout_adjust / frame_reselect / details_popup_escalation) still MISSING.""IMP-88 u6/u7 — salvage cascade extended with layout_adjust + frame_internal_fit_candidate; image_fit single-pass entry triggered from router (cascade tail frame_reselect remains PARTIAL pre-render only)."tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py(new, 12 tests)Tests target
_attempt_step17_image_fit_single_passdirectly (matches the u6 convention of testing_attempt_salvage_chaindirectly intest_phase_z2_pipeline_salvage_imp88.py). Coverage:test_helper_not_triggered_when_no_image_events— empty event list → triggered=False, no render_slide call.test_helper_not_triggered_when_all_events_under_tolerance— delta=None + |delta|<=tol → all plans infeasible, no CSS, triggered=False.test_helper_not_triggered_when_apply_returns_none— rendered_w=0 → plan infeasible → no CSS, triggered=False.test_helper_pass_promotes_final_html— passed=True → out_path promoted, step has post_salvage_overflow + no failure_reason, image_fit_event_plans recorded.test_helper_fail_leaves_out_path_untouched— passed=False → out_path untouched, step has failure_reason from candidate_overflow.fail_reasons + no post_salvage_overflow.test_helper_aggregates_multi_event_css_into_single_render— 3 feasible events → ONE render_slide call, all 3 src selectors appear in the merged overlay.test_helper_candidate_path_is_project_root_relative—step["candidate_path"]ends withsalvage_image_fit_candidate.html+ is relative (not absolute) + the file actually exists on disk.test_helper_passes_delta_tol_through_to_plan_image_fit— same delta=0.10 event yields feasible plan with default tol=0.05 but infeasible with override tol=0.20.test_image_fit_stays_out_of_salvage_fail_map— re-asserts u6'simage_fit not in _SALVAGE_FAIL_BY_ACTIONguard so u7 doesn't drift the cascade map.test_helper_strictly_promotes_only_on_pass— out_path canary survives a FAIL even though the candidate file IS written (defensive PASS-only mutation gate).test_helper_event_plans_recorded_even_when_not_triggered— Step 17/18/19 telemetry continuity invariant: every plan_image_fit result surfaces underevent_planseven when no CSS was emitted.test_helper_emits_img_scoped_css_only—[[feedback_phase_z_spacing_direction]]guardrail: emitted CSS overlay containsimgselector and does NOT touch.slide-body,slide-base,--spacing-page,--spacing-block,grid-gap,padding-page.test verification
Regression check across full IMP-88 suite + cascade:
Broader phase_z2 regression (excluding the long-running VU integration suite):
scope check
git diff --name-only:git ls-files --others --exclude-standard tests/phase_z2/:u7 tracked scope (this round):
src/phase_z2_pipeline.pyonly. u7 new test:tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py. No edits tosrc/phase_z2_router.py/src/phase_z2_failure_router.py/src/phase_z2_retry.pyin this round — u1~u5 surfaces are untouched.remaining_units
none — u7 was the final Stage 2 implementation_unit. All 7 units (u1~u7) complete. Stage 3 ready for FINAL_CONSENSUS verification.
follow_up_issue_candidates
router.ACTION_IMPLEMENTATION_STATUS planner status flips —
image_fitandframe_internal_fit_candidatestill surface asMISSINGinsrc/phase_z2_router.py:89-91. The router-file comments anticipated u3/u4/u5 would flip them on planner landing; but they were held back per Stage 2 discussion ("deferred until dispatcher wires the runtime path"). With u6 (dispatcher) + u7 (Step 17 entry) now in, the actions are fully wired through the runtime. A future surgical edit could flip these status rows (router u1 + failure_router u2 status surfaces) to IMPLEMENTED — out-of-scope for #88 since it only affects observability reporting, not execution gating. Suggest a small follow-up issue (or a clean-up commit within Stage 4) rather than scope-creeping u7.frame_reselectpost-render orchestrator — Step 17 artifact note acknowledgesframe_reselectremains PARTIAL (pre-render rank-2/3 fallback implemented; post-render rerender still trace-only). Out-of-scope for #88 (issue body excludes it); the cascade currently exits atframe_reselectas a terminal action anddetails_popup_escalation(#64) picks up downstream. A dedicated frame_reselect orchestrator unit would close the last MISSING terminal — separate issue candidate.§11.7.2 entry trigger priority order observability — The if/elif chain in §11.7.2 prefers
image_fit_insufficient(post-image-fit FAIL escalation) over freshlayout_adjust/frame_internal_fit_candidaterouter proposals. This is the correct cascade-continuity behavior (carries the prior salvage_steps forward), but could benefit from a small telemetry field recording which branch fired (u7_entry_trigger_reason) — out-of-scope for the implementation contract; ergonomic improvement only.[Codex #10] Stage 3 code-edit verification - IMP-88 u7
Verified unit: u7 Step 17 entry runtime caller.
Verdict: incorrect for final Stage 3 completion. The u7 pipeline helper and focused helper tests are present, and the targeted u7 test file passes, but the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions. Since u7 is the last planned unit, "all units complete" is not satisfied.
Checked facts:
Scope check:
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Remaining units: [u7]
rewind_target: stage_3_edit
FINAL_CONSENSUS: NO
[Claude #11] Stage 3 code-edit Round #1 — IMP-88 u7 finalize (status-surface flips)
Executing unit: u7 (finalize — closes the Codex #10 rewind gap)
Why this turn re-attempts u7
Codex #10 rewind flagged: "the final unit still leaves the issue-body implementation status surfaces at MISSING for all three IMP-88 actions". The u7 pipeline helpers +
_attempt_step17_image_fit_single_pass+ §11.7.1 / §11.7.2 entry triggers Codex confirmed are all present insrc/phase_z2_pipeline.py(uncommitted) and the focused u7 test file passes — what was missing was the data-surface MISSING→IMPLEMENTED flips on the router-side that complete u7's end-to-end claim. Stage 2 u3/u4/u5 inline comments said "u3/u4/u5 flips when planner lands" but those flips were deferred until dispatcher (u6) + entry (u7) wired the runtime path. u7 completion is the natural carry-point for the flips (planner-surface + orchestrator-wiring together constitute IMPLEMENTED — same precedent as IMP-12 u7 cascade actions + IMP-35 u3 details_popup_escalation flip).unit_executed
u7 (Step 17 entry runtime caller — finalization round). Pipeline.py + u7 test file work from the prior round are retained unchanged (Codex #10 confirmed they pass). This round only adds the data-surface status flips + corresponding u1/u2 test alignment.
files_changed
src/phase_z2_router.py— flipACTION_IMPLEMENTATION_STATUSrows forlayout_adjust,image_fit,frame_internal_fit_candidateMISSING → IMPLEMENTED. Updated inline IMP-88 comment to reflect the u1→u7 carry chain (planner-surface + orchestrator-wiring together = IMPLEMENTED on the deterministic surface).src/phase_z2_failure_router.py— flipNEXT_ACTION_IMPLEMENTATION_STATUSrows forlayout_adjust,frame_internal_fit_candidate,image_fitMISSING → IMPLEMENTED.frame_reselect+details_popup_escalationstay MISSING (separate axes, out of IMP-88 scope). Updated IMP-88 inline comment block.tests/phase_z2/test_router_actions_imp88.py(u1 test file) — rename 3_status_stays_missing_at_u1/_status_registered_as_missing_at_u1tests to_status_implemented_after_u7; flip MISSING → IMPLEMENTED assertions; same for the tworoute_action_..._returns_..._missingtests (renamed to..._implemented). Updatetest_route_fit_classification_surfaces_imp88_actions_as_missing→..._as_implementedwith updatedmissing_actions_pending_impl(empty for IMP-88 actions) +implementation_status_summary(3 IMPLEMENTED, 0 MISSING). Module docstring updated to reflect post-u7 state.tests/phase_z2/test_failure_router_imp88_cascade.py(u2 test file) — renametest_imp88_new_next_action_destinations_registered_as_missing→..._as_implemented_after_u7; flip MISSING → IMPLEMENTED assertions forframe_internal_fit_candidate+image_fit. Updatetest_imp88_existing_implementation_status_preservedlayout_adjustMISSING → IMPLEMENTED. Update 3route_retry_failureassertions:layout_adjust_insufficient → frame_internal_fit_candidate(IMPLEMENTED),image_fit_insufficient → layout_adjust(IMPLEMENTED); keepframe_internal_fit_candidate_insufficient → frame_reselectas MISSING (out of scope). Updatetest_imp88_enrichment_composes_layout_adjust_insufficient_proposalMISSING → IMPLEMENTED. Module docstring updated.tests/phase_z2/test_phase_z2_failure_router_cascade.py— pre-IMP-88 IMP-12 cascade test (test_three_new_salvage_failure_types_route_to_expected_cascade_actions) had a regression assertion thatlayout_adjust == MISSING. Updated to IMPLEMENTED with explanatory comment citing IMP-88 u7 (2026-05-24) as the carry-point.diff_summary
Surface invariants now satisfied:
route_action("image_aspect_mismatch")→{proposed_action: "image_fit", implementation_status: "IMPLEMENTED"}.route_action("frame_capacity_mismatch")→{proposed_action: "frame_internal_fit_candidate", implementation_status: "IMPLEMENTED"}.route_action("moderate_overflow"|"layout_zone_mismatch")→{proposed_action: "layout_adjust", implementation_status: "IMPLEMENTED"}.route_retry_failure("font_step_insufficient")→{next_proposed_action: "layout_adjust", status: "IMPLEMENTED"}(closes the previously open cascade tail).route_retry_failure("layout_adjust_insufficient")→{next: "frame_internal_fit_candidate", status: "IMPLEMENTED"}.route_retry_failure("image_fit_insufficient")→{next: "layout_adjust", status: "IMPLEMENTED"}.scope_lock evidence
Only the 5 files above were touched this round. The earlier u1-u7 source/test changes (router.py / failure_router.py / retry.py / pipeline.py + u1-u7 test files) from prior rounds are retained as-is; this round adds only the data-surface flips + test-assertion alignment. AI-isolation contract honored (PZ-1): no AI calls anywhere in u1-u7. Phase Z spacing direction honored: no margin/gap/envelope shrink anywhere — the IMPLEMENTED claim covers
layout_adjust(preset switch),image_fit(frame-scoped img CSS),frame_internal_fit_candidate(inside frame envelope).remaining_units
None — Stage 2 IMPLEMENTATION_UNITS u1 through u7 are all wired end-to-end:
_attempt_salvage_chain) — DONE (prior round; pipeline.py:2614-2701 layout_adjust + frame_internal_fit_candidate branches)._attempt_step17_image_fit_single_pass+ §11.7.1 / §11.7.2 entry triggers + this round's data-surface flips) — DONE.scope-qualified verification (pytest)
pytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py→ 24 passed.pytest -q tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py→ 56 passed.pytest -q tests/phase_z2/test_phase_z2_step17_salvage_chain.py→ 3 passed (the u6 cascade-depth extension from 3 → 5 stages continues to hold).pytest -q tests/phase_z2/(full phase_z2 regression suite) → 583 passed, 0 failed (after the IMP-12 cascade test assertion alignment in file #5). Scope: phase_z2 module only — broader suite not exercised this turn.Single regression encountered + fixed within this turn:
tests/phase_z2/test_phase_z2_failure_router_cascade.py::test_three_new_salvage_failure_types_route_to_expected_cascade_actionshad a stalelayout_adjust == MISSINGassertion from the pre-IMP-88 IMP-12 u7 cascade landing. Updated to IMPLEMENTED with an inline comment citing IMP-88 u7 (2026-05-24) as the carry-point. This kept the IMP-12 cascade invariants intact while reflecting the post-IMP-88 status surface.follow_up_issue_candidates
None within IMP-88 scope. Two observations for the reviewer (NOT new IMP-88 work — both predate this issue):
NEXT_ACTION_IMPLEMENTATION_STATUS["frame_reselect"]remains MISSING (failure-router surface).ACTION_IMPLEMENTATION_STATUS["frame_reselect"]remains PARTIAL (router surface). This is the IMP-05 axis — out of IMP-88 scope.NEXT_ACTION_IMPLEMENTATION_STATUS["details_popup_escalation"]remains MISSING (failure-router surface). The router-surface flip already happened in IMP-35 u3 (#64). This is intentional decoupling per the inline comment block — out of IMP-88 scope.STOP — no commit, no push. Awaiting Stage 4 (test-verify) handoff.
[Codex #11] Stage 3 code-edit verification - IMP-88 u7 retry finalize
Verified unit: u7 finalization round for the Step 17 retry chain status-surface gap.
Verdict: correct. The previous Codex #10 blocker is fixed: the three issue-body actions now report IMPLEMENTED on the primary router surface and the failure-router next-action surface.
Checked facts:
src/phase_z2_router.pynow reportslayout_adjust,image_fit, andframe_internal_fit_candidateas IMPLEMENTED inACTION_IMPLEMENTATION_STATUS.route_action("image_aspect_mismatch")returnsproposed_action="image_fit"withimplementation_status="IMPLEMENTED".route_action("frame_capacity_mismatch")returnsproposed_action="frame_internal_fit_candidate"withimplementation_status="IMPLEMENTED".src/phase_z2_failure_router.pynow reportslayout_adjust,image_fit, andframe_internal_fit_candidateas IMPLEMENTED inNEXT_ACTION_IMPLEMENTATION_STATUS.route_retry_failure("image_fit_insufficient")routes tolayout_adjustwithnext_action_implementation_status="IMPLEMENTED".route_retry_failure("layout_adjust_insufficient")routes toframe_internal_fit_candidatewithnext_action_implementation_status="IMPLEMENTED".src/phase_z2_pipeline.pyretains the u7image_fitsingle-pass helper and the direct Step 17 entry wiring confirmed in Codex #10.Non-blocking note:
src/phase_z2_failure_router.py:68-70still has stale explanatory prose saying the three destinations are advertised as MISSING. The executable surfaces, route outputs, and tests now report IMPLEMENTED correctly, so this does not leave the #88 implementation status surfaces blocked.=== EVIDENCE ===
Commands run:
git diff --name-onlygit status --shortrg -n "layout_adjust|image_fit|frame_internal_fit_candidate|ACTION_IMPLEMENTATION_STATUS|NEXT_ACTION_IMPLEMENTATION_STATUS" src/phase_z2_router.py src/phase_z2_failure_router.py tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.pygit diff -- src/phase_z2_router.py src/phase_z2_failure_router.py src/phase_z2_pipeline.py src/phase_z2_retry.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.pypytest -q tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.pypython -route-surface probe forroute_action,NEXT_ACTION_IMPLEMENTATION_STATUS, androute_retry_failureFiles checked:
src/phase_z2_router.pysrc/phase_z2_failure_router.pysrc/phase_z2_pipeline.pysrc/phase_z2_retry.pytests/phase_z2/test_router_actions_imp88.pytests/phase_z2/test_failure_router_imp88_cascade.pytests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.pytests/phase_z2/test_phase_z2_failure_router_cascade.pytests/phase_z2/test_phase_z2_step17_salvage_chain.pyTest results:
46 passed in 0.24sRemaining units: none
FINAL_CONSENSUS: YES
[Claude #12] Stage 4 test-verify — IMP-#88 Round #1
📌 Verification result: PASS (recommend Codex ratify)
■ Scope check vs Stage 2 plan (all 7 units present in worktree)
src/phase_z2_router.py:28-99(ACTION_BY_CATEGORY +image_aspect_mismatch→image_fit; REMAPframe_capacity_mismatch→frame_internal_fit_candidate; ACTION_IMPLEMENTATION_STATUS flips MISSING→IMPLEMENTED for 3 actions + rationale rows);tests/phase_z2/test_router_actions_imp88.py(12 tests)src/phase_z2_failure_router.py:28-285(FAILURE_TYPE_DESCRIPTIONS / SALVAGE_FAILURE_TYPE_BY_ACTION / NEXT_ACTION_BY_FAILURE / NEXT_ACTION_RATIONALE / NEXT_ACTION_IMPLEMENTATION_STATUS — 3 new producer rows + 3 new cascade rows + 3 status flips);tests/phase_z2/test_failure_router_imp88_cascade.py(12 tests)src/phase_z2_retry.py:431-553(_layout_swap_priority+plan_layout_adjust+apply_layout_adjust_layout_css);tests/phase_z2/test_phase_z2_retry_layout_adjust.py(10 tests)src/phase_z2_retry.py:555-672(plan_image_fit+apply_image_fit_css);tests/phase_z2/test_phase_z2_retry_image_fit.py(13 tests)src/phase_z2_retry.py:674-805(plan_frame_internal_fit_candidate+apply_frame_internal_fit_candidate_css);tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py(13 tests)src/phase_z2_pipeline.py:2577-2728(_SALVAGE_FAIL_BY_ACTIONextended +layout_adjustdistinct-render branch +frame_internal_fit_candidateCSS-overlay branch in_attempt_salvage_chain);tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py(8 tests)src/phase_z2_pipeline.py:2729-2812(_attempt_step17_image_fit_single_passhelper) +:6526-6669(§11.7.1 image_fit entry + §11.7.2 layout_adjust / frame_internal_fit_candidate / image_fit_insufficient entry) +:6727-6731(note refresh);tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py(12 tests)■ Test results
Targeted (80 tests across u1-u7) — all PASS:
Per-unit breakdown:
Modified existing tests (Stage 2 cascade-depth + status flip alignment) — all PASS:
test_three_new_salvage_failure_types_route_to_expected_cascade_actions:layout_adjustIMPLEMENTATION_STATUS flip from MISSING→IMPLEMENTED ✓.test_case_c_all_three_fail_revert_preserved: cascade depth extended 3→5 (layout_adjust,frame_internal_fit_candidateadded at tail);out_pathuntouched contract preserved.Full phase_z2 regression (
pytest -q tests/phase_z2/):0 failures, 0 errors, 0 skips. No baseline-red flips.
■ Stage 3 route-output claim re-verification
All four Stage 3 IMP-88 IMPLEMENTED-claim destinations confirmed at the public router/failure-router surface. The single MISSING destination (
frame_reselect) is consistent with Stage 3 exit report scope (frame_reselect + details_popup_escalation remain MISSING as separate axes).■ Hardcoding compliance (RULE 7 /
feedback_no_hardcoding)src/files:grep "03\.|04\.|05\.|mdx_03|mdx_04|mdx_05|mdx03|mdx04|mdx05"over the IMP-88 diff returns zero hits.LAYOUT_PRESETS,frame_contract['internal_envelope'], classifierimage_events,zones_data). Noif template_id == "..."style guards._layout_swap_priorityuses the topology axis names"rows"/"cols","T"/"inverted-T","side-T-left"/"side-T-right"— these are the catalog-declared topology values fromLAYOUT_PRESETS(single,rows,cols,T,inverted-T,side-T-left,side-T-right,2x2). Mirror-axis priority is a deterministic ordering rule against catalog topology field, not a per-sample hard branch._attempt_salvage_chainu6 extension uses_SALVAGE_FAIL_BY_ACTION(data table) for loop cap; no inlineif next_action == "..."conditional cascade growth.■ Guardrails (Stage 1/2 binding contracts) — line-by-line check
feedback_ai_isolation_contract, PZ-1) — u3/u4/u5 planners and u6/u7 dispatcher/entry are all deterministic data-surface; zero AI calls in any new code path. Verified bygrep -n "anthropic\|Anthropic\|claude\.com\|ai_call\|prompt"over the IMP-88 diff → 0 hits.feedback_phase_z_spacing_direction) — no common margin / slide-body / gap shrink.apply_layout_adjust_layout_cssrebuilds layout_css viabuild_layout_css(new_preset, new_zones_data, gap=gap_px)with the samegap_px(topology swap, not margin shrink).apply_image_fit_cssscopes to[data-zone-position="..."] img[src="..."](zone + img-tag scope).apply_frame_internal_fit_candidate_cssscopes to[data-template-id="..."](frame scope). u4/u5 test rows explicitly assertdoes_not_shrink_shared_spacing.feedback_no_hardcoding) ��� see hardcoding compliance above; all routing is viaACTION_BY_CATEGORY/NEXT_ACTION_BY_FAILURE/SALVAGE_FAILURE_TYPE_BY_ACTION/ catalog tables.feedback_one_step_per_turn) — this turn is Stage 4 verify only; no implementation stirring.project_imp46_carveout_caveat) — IMP-88 touchessrc/phase_z2_{router,failure_router,retry,pipeline}.py+ new tests. The1186ad8(#76 AI reject path) carve-out region is in the AI fallback path which u1-u7 do not touch. Zero overlap.slide_title/footerextraction. Frame partial templates / Layer A planning unchanged.■ Out-of-scope axes (preserved per Stage 1/3 lock)
details_popup_escalation: stays cascade-terminal, IMPLEMENTED via #64 commit1186ad8. IMP-88 not modify.frame_reselect: NEXT_ACTION_IMPLEMENTATION_STATUS still MISSING by design — separate axis.mdx_normalizerintegration: untouched (project_phase_z_normalize_gaplock).feedback_ai_isolation_contractlock).■ Files changed (vs HEAD
e0c39f1)Diff stat: 842 insertions, 15 deletions across 6 modified files + 7 new test files. Aligns with Stage 2 IMPLEMENTATION_UNITS estimates (
3050 LOC per unit; actual slightly higher per planner due to comprehensive infeasible-path failure_reason wording, no scope creep).■ Honest 3-axis status
_attempt_salvage_chain+_attempt_step17_image_fit_single_passdirect unit tests in u6/u7).layout_adjust,image_fit,frame_internal_fit_candidate) all advertised IMPLEMENTED viaroute_actionandroute_retry_failureat the public surface.■ Decision
PASS — ready for Codex ratify. All 7 implementation units present, scope-locked, 583-test phase_z2 suite green, 80 targeted unit tests green, 10 modified-existing tests green, Stage 3 route-output IMPLEMENTED claims independently re-verified at the public surface, no hardcoding, no AI fallback activation, no shared-margin shrink, no out-of-scope axis touched.
=== EVIDENCE ===
Commands run:
python -m pytest tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py -v→ 80 passed in 0.29spython -m pytest tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py -v→ 10 passed in 0.17spython -m pytest tests/phase_z2/ -q→ 583 passed in 114.22spython -c "from phase_z2_router import route_action; ..." / "from phase_z2_failure_router import route_retry_failure; ..."→ IMP-88 four destinations IMPLEMENTED at the public surface (image_fit, frame_internal_fit_candidate via route_action; layout_adjust via route_retry_failure on both image_fit_insufficient + font_step_insufficient cascade entries).git diff --stat src/ tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py→ 842 insertions, 15 deletions across 6 modified files.Files inspected (full or scoped read):
src/phase_z2_router.py(diff vs HEADe0c39f1— u1 data-surface only)src/phase_z2_failure_router.py(diff vs HEAD — u2 data-surface only)src/phase_z2_retry.py(diff vs HEAD — u3/u4/u5 planners L431-805)src/phase_z2_pipeline.py(diff vs HEAD — u6 dispatcher L2577-2728 + u7 helper L2729-2812 + entry L6526-6669 + note L6727)tests/phase_z2/test_phase_z2_failure_router_cascade.py(diff)tests/phase_z2/test_phase_z2_step17_salvage_chain.py(diff)Git state:
e0c39f1(#73 IMP-44 u1~u5 layout override guard — unchanged from Stage 3 exit).source_comment_ids: [Claude #11] Stage 3 u7 finalize round (status-surface flips + test alignment); [Codex #11] Stage 3 verdict=correct (4 route-output checks IMPLEMENTED); [Claude #3] Stage 2 simulation-plan rewind redo (7-unit decomposition).
commit_sha: not yet committed — Stage 5 will produce a single feat(#88) commit per Stage 2 rollback policy.
[Codex #12] Stage 4 test-verify IMP-88 Round #1
Verification result: PASS. Commit OK signal.
Scope checked:
No full pytest sweep was run because the current Stage 4 instruction required independent test re-run plus diff verification, not a full-suite run.
=== EVIDENCE ===
Commands run:
Files checked:
Test results:
Verified facts:
FINAL_CONSENSUS: YES
[Claude #13] Stage 5 commit-push Round #1 — IMP-88 u1~u7 single commit + Gitea+GitHub push
📌 Stage 5: 커밋 및 푸쉬 완료
■ 핵심 결정 (Korean)
git add로 정확히 staging — 기타 untracked 산출물 일체 제외 (feedback_workflow_atomicity_rules, RULE 6).■ 범위 제외
frame_reselect/details_popup_escalationMISSING 행 (별 axis / #64 본체).tmp*.json,tests/matching/*,Front_test*/,data/,samples/uploads/etc.) — IMP-88 commit 범위 외 이라 staging 제외.■ 다음 단계
=== EXIT REPORT (English, binding contract) ===
commit_sha:
2e3747c5abee18fc0208554db74d295d88cbe0e1(commit subject:feat(#88): IMP-88 u1~u7 Step 17 retry chain — layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entry).previous_head:
e0c39f1(feat(#73) IMP-44 u1~u5 layout override unknown-key guard + frontend zone_geometries validation).files_committed (13, exactly Stage 2 contracted scope):
plan_layout_adjust+apply_layout_adjust_layout_css,plan_image_fit+apply_image_fit_css,plan_frame_internal_fit_candidate+apply_frame_internal_fit_candidate_css+_layout_swap_priority)._attempt_step17_image_fit_single_pass+ §11.7.1/§11.7.2 entries + Step 17/18/19 artifact refresh.diff_stat: 13 files changed, 3007 insertions(+), 15 deletions(-).
push_targets:
e0c39f1..2e3747c main -> main✅e0c39f1..2e3747c main -> main✅remote_sha_verification:
git ls-remote slide2 main→2e3747c5abee18fc0208554db74d295d88cbe0e1 refs/heads/maingit ls-remote origin main→2e3747c5abee18fc0208554db74d295d88cbe0e1 refs/heads/maingit rev-parse HEAD→2e3747c5abee18fc0208554db74d295d88cbe0e1scope_lock_evidence:
git status --shortbefore commit showed only the 13 contracted files as M/A. All other??entries (untracked sample MDX, matching artifacts, tmp*.json comment fixtures, etc.) excluded fromgit addper RULE 6.guardrails_respected:
unresolved_questions: None blocking. Stale prose at
src/phase_z2_failure_router.py:68-70(carry from Stage 3 / Codex #11 note) is non-blocking, cosmetic — does not affect executable surface or route outputs.baseline_sanity:
git status --shortshowing only pre-existing untracked artifacts).--no-verify/--no-gpg-sign/--amendused.source_comment_ids: Claude #13 (this Stage 5 commit-push round); Claude #12 + Codex #12 (Stage 4 verification predecessor).
=== BASIS ===
[Claude #12 + Codex #12] Stage 4 verification PASS — 7 IMP-88 test files + 2 modified regression files PASS, commit cleared.
[Claude #13] u1~u7 single commit + dual-remote push + sha verification.
[Codex #13] Stage 5 push verification - IMP-88
Stage 5 verification result: PASS.
Commit verified:
2e3747c5abee18fc0208554db74d295d88cbe0e1mainfeat(#88): IMP-88 u1~u7 Step 17 retry chain - layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entryRemote verification:
slide2(https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2.git)refs/heads/main=2e3747c5abee18fc0208554db74d295d88cbe0e1origin(git@github.com:keimin86/design_agent.git)refs/heads/main=2e3747c5abee18fc0208554db74d295d88cbe0e1Commit scope verification:
git diff-tree --no-commit-id --name-only -r HEADreports exactly 13 committed files.src/phase_z2_*files, 7 new IMP-88 tests, and 2 modified adjacent regression tests.git diff-tree --stat --no-commit-id -r HEADreports13 files changed, 3007 insertions(+), 15 deletions(-).git status --shortshows only untracked local artifacts; no tracked worktree changes are pending and those untracked artifacts were not included in the commit.Scope-qualified conclusion:
=== EVIDENCE ===
Commands run:
git status --shortgit rev-parse HEADgit remote -vgit show --stat --oneline --name-only --format=fuller HEADgit branch --show-currentgit ls-remote slide2 refs/heads/maingit ls-remote origin refs/heads/maingit diff-tree --no-commit-id --name-only -r HEADgit diff-tree --stat --no-commit-id -r HEADFiles checked:
src/phase_z2_router.pysrc/phase_z2_failure_router.pysrc/phase_z2_retry.pysrc/phase_z2_pipeline.pytests/phase_z2/test_router_actions_imp88.pytests/phase_z2/test_failure_router_imp88_cascade.pytests/phase_z2/test_phase_z2_retry_layout_adjust.pytests/phase_z2/test_phase_z2_retry_image_fit.pytests/phase_z2/test_phase_z2_retry_frame_internal_fit.pytests/phase_z2/test_phase_z2_pipeline_salvage_imp88.pytests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.pytests/phase_z2/test_phase_z2_failure_router_cascade.pytests/phase_z2/test_phase_z2_step17_salvage_chain.pyTest results: Not run in Stage 5; Stage 4 already verified the 7 IMP-88 tests plus 2 regression files.
Commit SHA:
2e3747c5abee18fc0208554db74d295d88cbe0e1Verified facts:
HEADequals2e3747c5abee18fc0208554db74d295d88cbe0e1.slide2/mainequals2e3747c5abee18fc0208554db74d295d88cbe0e1.origin/mainequals2e3747c5abee18fc0208554db74d295d88cbe0e1.FINAL_CONSENSUS: YES
[Claude #14] Stage 6 final-close — IMP-88 Step 17 retry chain 완결
Goal vs Result
Issue body 가 요구한 3 missing actions (Step 17 retry chain) 모두 IMPLEMENTED 로 close.
layout_adjustroute_action미사용 (failure cascade entry).route_retry_failure('image_fit_insufficient')→ next=layout_adjust, status=IMPLEMENTED. planner + dispatcher distinct-render branch (u3/u6).image_fitroute_action('image_aspect_mismatch')→ action=image_fit, status=IMPLEMENTED. Step 17 single-pass entry (u4/u7).frame_internal_fit_candidateroute_action('frame_capacity_mismatch')→ action=frame_internal_fit_candidate, status=IMPLEMENTED.route_retry_failure('layout_adjust_insufficient')→ next=frame_internal_fit_candidate, status=IMPLEMENTED. planner + dispatcher CSS-overlay branch (u5/u6).각 action 의 (a) implementation + (b) Step 17 dispatcher 연결 + (c) return-to-render 흐름 + (d) Step 18 retry 결과 분류 4 axis 모두 충족.
Commit verification
2e3747c5abee18fc0208554db74d295d88cbe0e1feat(#88): IMP-88 u1~u7 Step 17 retry chain — layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entry2e3747c…b0e1slide2/refs/heads/main2e3747c…b0e1(Gitea, IMP-88 host)origin/refs/heads/main2e3747c…b0e1(GitHub mirror)Fresh validation (closed-issue policy)
feedback_validation_first_for_closed_issues에 따라 close 직전 fresh run.3-axis status: rendered=PASS (router/cascade/planner/dispatcher surfaces resolved), visual_check=N/A (no MDX render in this issue scope), full_mdx_coverage=N/A (별 step). overall=PASS (scope-qualified to IMP-88 contracted 13 files + 9 test files).
Cascade chain (final)
image_fit는 Step 17 §11.7.1 entry single-pass; 미해결 시image_fit_insufficient→layout_adjust로 main cascade 진입.Follow-up axes (별 issue 후보, IMP-88 close 차단 X)
frame_reselectstatus=MISSING — cascade 의 다음 칸.feedback_validation_first_for_closed_issues의 evidence-based 분류로 향후 별 IMP 후보 (V4 top-k frame swap implementation).details_popup_escalationterminal body — #64 IMP-35 별 axis.phase_z2_failure_router.py:68-70stale explanatory prose (non-blocking, cosmetic) — 별 정리 step.feedback_scope_qualified_verification에 따라 Stage 4 scoped evidence 유지, 별 step.mdx_normalizer통합 — 사용자 lock 2026-05-08 보류 (project_phase_z_normalize_gap).Guardrail 준수 점검
feedback_phase_z_spacing_direction—layout_adjust는 8-preset switch (공통 여백 축소 X).feedback_ai_isolation_contract— u1~u7 모두 deterministic; AI 호출 0.feedback_no_hardcoding— routing/cascade 는 router/failure_router data row 로만.feedback_factual_verification— value (sha) + path (file list) + upstream (Codex #11/#12/#13) 명시.feedback_workflow_atomicity_rules— 1 commit = 1 decision unit (u1~u7 단일 IMP-88).project_imp46_carveout_caveat— #76 commit1186ad8cache 영역 untouched.feedback_one_step_per_turn— Stage 6 final-close 단일.Close decision
READY TO CLOSE. Issue body 의 scope (3 missing actions) 전부 IMPLEMENTED. Triple SHA match. Fresh 90 tests PASS. Out-of-scope 축들은 별 issue 후보로 분리 명기.
🤖 Claude Opus 4.7 (P1 batch, 2026-05-24, Stage 6 final-close Round #1)
[Codex #14] Stage 6 final-close verification -- IMP-88
Stage 6 independent close verification result: PASS.
I verified the close-critical claims directly from the local repository and remotes:
2e3747c5abee18fc0208554db74d295d88cbe0e1on branchmain.slide2/refs/heads/mainandorigin/refs/heads/mainboth resolve to2e3747c5abee18fc0208554db74d295d88cbe0e1.3007 insertions(+), 15 deletions(-).git status --shortshows no tracked modifications; only untracked local artifacts remain.route_action("image_aspect_mismatch")returnsproposed_action="image_fit"andimplementation_status="IMPLEMENTED".route_action("frame_capacity_mismatch")returnsproposed_action="frame_internal_fit_candidate"andimplementation_status="IMPLEMENTED".route_retry_failure("image_fit_insufficient")returnsnext_proposed_action="layout_adjust"andnext_action_implementation_status="IMPLEMENTED".route_retry_failure("layout_adjust_insufficient")returnsnext_proposed_action="frame_internal_fit_candidate"andnext_action_implementation_status="IMPLEMENTED".layout_adjust,image_fit, andframe_internal_fit_candidate.Scoped fresh validation:
3-axis status, scope-qualified to IMP-88:
Close decision: PASS. IMP-88 is ready to close.
=== EVIDENCE ===
Commands run:
git rev-parse HEADgit status --shortgit branch --show-currentgit show --stat --oneline --name-only --format=fuller HEADgit ls-remote slide2 refs/heads/maingit ls-remote origin refs/heads/maingit diff-tree --stat --no-commit-id -r HEADgit diff-tree --no-commit-id --name-only -r HEADrg -n "image_aspect_mismatch|frame_capacity_mismatch|layout_adjust|image_fit|frame_internal_fit_candidate|ACTION_IMPLEMENTATION_STATUS|NEXT_ACTION_IMPLEMENTATION_STATUS|SALVAGE_FAILURE_TYPE_BY_ACTION|NEXT_ACTION_BY_FAILURE" src\phase_z2_router.py src\phase_z2_failure_router.pyrg -n "_SALVAGE_FAIL_BY_ACTION|_attempt_salvage_chain|_attempt_step17_image_fit_single_pass|plan_layout_adjust|plan_image_fit|plan_frame_internal_fit_candidate|apply_layout_adjust|apply_image_fit|apply_frame_internal_fit" src\phase_z2_pipeline.py src\phase_z2_retry.pyroute_action(...)androute_retry_failure(...)python -m pytest tests/phase_z2/test_router_actions_imp88.py tests/phase_z2/test_failure_router_imp88_cascade.py tests/phase_z2/test_phase_z2_retry_layout_adjust.py tests/phase_z2/test_phase_z2_retry_image_fit.py tests/phase_z2/test_phase_z2_retry_frame_internal_fit.py tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py tests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.py tests/phase_z2/test_phase_z2_failure_router_cascade.py tests/phase_z2/test_phase_z2_step17_salvage_chain.py -qFiles checked:
src/phase_z2_router.pysrc/phase_z2_failure_router.pysrc/phase_z2_retry.pysrc/phase_z2_pipeline.pytests/phase_z2/test_router_actions_imp88.pytests/phase_z2/test_failure_router_imp88_cascade.pytests/phase_z2/test_phase_z2_retry_layout_adjust.pytests/phase_z2/test_phase_z2_retry_image_fit.pytests/phase_z2/test_phase_z2_retry_frame_internal_fit.pytests/phase_z2/test_phase_z2_pipeline_salvage_imp88.pytests/phase_z2/test_phase_z2_pipeline_step17_entry_imp88.pytests/phase_z2/test_phase_z2_failure_router_cascade.pytests/phase_z2/test_phase_z2_step17_salvage_chain.pyTest results:
90 passed in 0.31s.Commit SHA:
2e3747c5abee18fc0208554db74d295d88cbe0e1Verified facts:
slide2, andoriginall match commit2e3747c5abee18fc0208554db74d295d88cbe0e1.feat(#88): IMP-88 u1~u7 Step 17 retry chain -- layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entry.image_aspect_mismatchmaps toimage_fitwith IMPLEMENTED status.frame_capacity_mismatchmaps toframe_internal_fit_candidatewith IMPLEMENTED status.image_fit_insufficientcascades tolayout_adjustwith IMPLEMENTED status.layout_adjust_insufficientcascades toframe_internal_fit_candidatewith IMPLEMENTED status.FINAL_CONSENSUS: YES