feat(#88): IMP-88 u1~u7 Step 17 retry chain — layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entry
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s

Step 17 salvage dispatcher previously only ran the 3 actions in
_SALVAGE_FAIL_BY_ACTION (cross_zone_redistribute / glue_compression /
font_step_compression). Any next_proposed_action outside that set hit
salvage_terminal_action and dropped through, so visual_check aborted on
layout_adjust / image_fit / frame_internal_fit_candidate cascades.

u1 — router data surface (src/phase_z2_router.py)
  - ACTION_BY_CATEGORY: image_aspect_mismatch -> image_fit (new row),
    frame_capacity_mismatch -> frame_internal_fit_candidate (was
    frame_reselect).
  - ACTION_IMPLEMENTATION_STATUS: layout_adjust / image_fit /
    frame_internal_fit_candidate flipped MISSING -> IMPLEMENTED with
    inline IMP-88 rationale.

u2 — failure_router cascade surface (src/phase_z2_failure_router.py)
  - FAILURE_TYPE_DESCRIPTIONS + SALVAGE_FAILURE_TYPE_BY_ACTION extended
    with layout_adjust_insufficient / image_fit_insufficient /
    frame_internal_fit_insufficient producers.
  - NEXT_ACTION_BY_FAILURE + NEXT_ACTION_RATIONALE +
    NEXT_ACTION_IMPLEMENTATION_STATUS rows added; cascade chain becomes
    font_step_compression -> layout_adjust -> frame_internal_fit_candidate
    -> frame_reselect -> details_popup_escalation (#64 terminal).

u3~u5 — planners + apply helpers (src/phase_z2_retry.py)
  - plan_layout_adjust / apply_layout_adjust_layout_css with
    _layout_swap_priority across 8-preset LAYOUT_PRESETS (preset switch,
    no shared-margin shrink per Phase Z spacing direction).
  - plan_image_fit / apply_image_fit_css scoped to frame slot using
    existing classifier image_event payload (object-fit + max-w/h
    derivation).
  - plan_frame_internal_fit_candidate / apply_frame_internal_fit_candidate_css
    stays inside declared frame contract envelope; emits infeasible path
    when envelope is absent.

u6~u7 — pipeline wiring (src/phase_z2_pipeline.py)
  - _SALVAGE_FAIL_BY_ACTION extended; _attempt_salvage_chain gains
    layout_adjust distinct-render branch + frame_internal_fit_candidate
    CSS-overlay branch + loop cap.
  - _attempt_step17_image_fit_single_pass added for image_fit entry.
  - §11.7.1 / §11.7.2 entry triggers wired; Step 17/18/19 artifact
    refresh + note logging closes the salvage_terminal_action fall-through
    for the 3 IMP-88 actions.

Tests
  - New: test_router_actions_imp88.py (12),
    test_failure_router_imp88_cascade.py (12),
    test_phase_z2_retry_layout_adjust.py (10),
    test_phase_z2_retry_image_fit.py (13),
    test_phase_z2_retry_frame_internal_fit.py (13),
    test_phase_z2_pipeline_salvage_imp88.py (8),
    test_phase_z2_pipeline_step17_entry_imp88.py.
  - Regression-aligned: test_phase_z2_failure_router_cascade.py,
    test_phase_z2_step17_salvage_chain.py — pre-existing cascade +
    salvage-chain assertions updated to the IMPLEMENTED surface.

Out of scope (separate axes / issues)
  - details_popup_escalation terminal body (#64).
  - frame_reselect MISSING flip (different axis).
  - Step 14/16 detection refinement.
  - Stage 0 mdx_normalizer integration (locked 2026-05-08).
  - AI fallback activation.

Guardrails respected
  - Phase Z spacing direction: layout_adjust switches preset; no shared
    margin shrink.
  - AI isolation contract: planners + dispatcher are deterministic; zero
    AI calls in u1~u7.
  - No hardcoding: routing + cascade live in router/failure_router data
    rows, not inline conditionals.
  - IMP-46 (#62) cache carve-out: untouched.
  - 1 commit = 1 decision unit: u1~u7 grouped as a single IMP-88 unit.

Stage 4 verification: 7 IMP-88 test files + 2 modified regression files
PASS (Claude #12 + Codex #12 consensus YES). Full-suite sweep deferred to
a separate step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:01:55 +09:00
parent e0c39f1bc1
commit 2e3747c5ab
13 changed files with 3007 additions and 15 deletions

View File

@@ -63,11 +63,17 @@ from phase_z2_retry import (
DEFAULT_SAFETY_MARGIN_PX,
apply_cross_zone_redistribute_css,
apply_font_step_compression_css,
apply_frame_internal_fit_candidate_css,
apply_glue_compression_css,
apply_image_fit_css,
apply_layout_adjust_layout_css,
apply_retry_to_layout_css,
plan_cross_zone_redistribute,
plan_font_step_compression,
plan_frame_internal_fit_candidate,
plan_glue_compression,
plan_image_fit,
plan_layout_adjust,
plan_zone_ratio_retry,
)
from phase_z2_failure_router import (
@@ -2571,10 +2577,16 @@ def _attempt_zone_ratio_retry(
# IMP-12 u8 — Step 17 salvage cascade orchestrator (deterministic, no normal-path AI).
# Plan/apply pairs: phase_z2_retry (u4/u5/u6). Routing: failure_router.route_retry_failure (u3).
# Pipeline wiring (cascade_inputs assembly + retry_trace merge) lands in u9.
# IMP-88 u6 — extended with layout_adjust + frame_internal_fit_candidate dispatch.
# Mirror of failure_router.SALVAGE_FAILURE_TYPE_BY_ACTION (single source-of-truth lives
# there; this local map gates which actions the salvage loop can execute and feeds the
# loop-cap range(len(...)) so cascade depth scales with implemented executors).
_SALVAGE_FAIL_BY_ACTION = {
"cross_zone_redistribute": "cross_zone_redistribute_insufficient",
"glue_compression": "glue_absorption_insufficient",
"font_step_compression": "font_step_insufficient",
"layout_adjust": "layout_adjust_insufficient",
"frame_internal_fit_candidate": "frame_internal_fit_candidate_insufficient",
}
@@ -2602,6 +2614,52 @@ def _attempt_salvage_chain(
trace["salvage_terminal_action"] = next_action
trace["salvage_terminal_rationale"] = routing.get("rationale")
return trace
# IMP-88 u6 — layout_adjust takes a distinct render path (fresh
# render_slide with the new preset + remapped zones_data + new
# layout_css), so it is dispatched BEFORE the shared CSS-overlay
# planner cluster below. No common margin / slide-body shrink
# ([[feedback_phase_z_spacing_direction]]) — topology swap only.
if next_action == "layout_adjust":
plan = plan_layout_adjust(
current_layout_preset=layout_preset, zones_data=zones_data)
new_layout_css = (
apply_layout_adjust_layout_css(plan, gap_px=gap_px)
if (plan and plan.get("feasible")) else None
)
candidate_path = run_dir / f"salvage_{next_action}_candidate.html"
candidate_html, candidate_overflow, passed = None, None, False
if new_layout_css:
candidate_html = render_slide(
slide_title, slide_footer,
plan["new_zones_data"], plan["new_layout_preset"],
new_layout_css, gap_px=gap_px,
)
candidate_path.write_text(candidate_html, encoding="utf-8")
candidate_overflow = run_overflow_check(candidate_path)
passed = bool(candidate_overflow.get("passed", False))
step = {
"action": next_action, "plan": plan, "passed": passed,
"new_layout_preset": (
plan.get("new_layout_preset") if isinstance(plan, dict) else None
),
"candidate_path": (
str(candidate_path.relative_to(PROJECT_ROOT))
if new_layout_css else None
),
}
if passed:
out_path.write_text(candidate_html, encoding="utf-8")
step["post_salvage_overflow"] = candidate_overflow
trace["salvage_steps"].append(step)
trace["salvage_passed"] = True
return trace
step["failure_reason"] = (
(plan.get("failure_reason") if isinstance(plan, dict) else None)
or (candidate_overflow.get("fail_reasons") if candidate_overflow else None)
or "infeasible or render emitted no candidate")
trace["salvage_steps"].append(step)
failure_type = _SALVAGE_FAIL_BY_ACTION[next_action]
continue
if next_action == "cross_zone_redistribute":
if ci.get("fit_analysis") is None:
plan = {"action": "cross_zone_redistribute", "feasible": False,
@@ -2617,6 +2675,21 @@ def _attempt_salvage_chain(
block_count=int(ci.get("block_count") or 0),
zone_position=str(ci.get("zone_position") or ""))
apply_fn = apply_glue_compression_css
elif next_action == "frame_internal_fit_candidate":
# IMP-88 u6 — per-zone frame-scoped envelope variant. Resolves
# frame_template_id from zones_data via cascade_inputs.zone_position
# so the planner stays within the frame contract envelope
# (no shared margin shrink per [[feedback_phase_z_spacing_direction]]).
_target_pos = str(ci.get("zone_position") or "")
_target_zone = next(
(z for z in zones_data if z.get("position") == _target_pos), {},
) or {}
_frame_tid = str(_target_zone.get("template_id") or "")
plan = plan_frame_internal_fit_candidate(
frame_template_id=_frame_tid,
overflow_zone={"excess_y": float(ci.get("excess_px") or 0.0)},
)
apply_fn = apply_frame_internal_fit_candidate_css
else:
plan = plan_font_step_compression(
current_font_px=float(ci.get("current_font_px") or 0.0),
@@ -2653,6 +2726,89 @@ def _attempt_salvage_chain(
return trace
# IMP-88 (#88) u7 — Step 17 image_fit single-pass entry executor.
# image_fit is NOT a salvage cascade stage (deliberately OUT of
# _SALVAGE_FAIL_BY_ACTION per u6 guard). Instead it is a Step 17 ENTRY
# single-pass: aggregate per-image plan/apply via plan_image_fit (u4) +
# apply_image_fit_css (u4), emit one CSS overlay, re-render once,
# run_overflow_check. PASS promotes final.html; FAIL records `image_fit`
# step with failure_reason so failure_router (u2)
# SALVAGE_FAILURE_TYPE_BY_ACTION classifies it as `image_fit_insufficient`
# and the cascade entry block routes onto layout_adjust.
# Honors [[feedback_phase_z_spacing_direction]] — frame-scoped img CSS only,
# no common margin / slide-body / zone gap shrink. AI isolation contract
# (PZ-1) — deterministic data-surface, no AI call.
def _attempt_step17_image_fit_single_pass(
*, run_dir: Path, out_path: Path, slide_title: str, slide_footer: Optional[str],
zones_data: list[dict], layout_preset: str, layout_css: dict,
image_events: list[dict], gap_px: int,
delta_tol: float = IMAGE_ASPECT_DELTA_TOL,
) -> dict:
"""IMP-88 u7 — Step 17 image_fit single-pass executor.
Returns a result dict with shape:
- triggered : bool — True iff any feasible image_fit plan emitted CSS.
- passed : bool — True iff post-rerender overflow check passed.
- step : dict|None — salvage_steps[] entry shape (action="image_fit",
image_fit_event_plans, candidate_path, post_salvage_overflow
on pass / failure_reason on fail).
- candidate_html : str|None — rendered HTML when triggered (None otherwise).
- candidate_overflow : dict|None — run_overflow_check result when triggered.
- event_plans : list[dict] — every plan_image_fit result (feasible + no-op),
surfaced for telemetry even when none emit CSS.
Side effect on PASS only: writes candidate_html to out_path.
"""
event_plans = []
css_chunks = []
for ev in (image_events or []):
plan = plan_image_fit(image_event=ev, delta_tol=delta_tol)
event_plans.append(plan)
if plan.get("feasible"):
css = apply_image_fit_css(plan)
if css:
css_chunks.append(css)
if not css_chunks:
return {
"triggered": False, "passed": False, "step": None,
"candidate_html": None, "candidate_overflow": None,
"event_plans": event_plans,
}
candidate_path = run_dir / "salvage_image_fit_candidate.html"
base = render_slide(
slide_title, slide_footer, zones_data, layout_preset, layout_css,
gap_px=gap_px,
)
style = "<style>\n" + "\n".join(css_chunks) + "\n</style>"
candidate_html = (
base.replace("</head>", f"{style}\n</head>", 1)
if "</head>" in base else style + base
)
candidate_path.write_text(candidate_html, encoding="utf-8")
candidate_overflow = run_overflow_check(candidate_path)
passed = bool(candidate_overflow.get("passed", False))
step = {
"action": "image_fit", "passed": passed,
"image_fit_event_plans": event_plans,
"candidate_path": (
str(candidate_path.relative_to(PROJECT_ROOT))
if candidate_path.is_absolute() else str(candidate_path)
),
}
if passed:
step["post_salvage_overflow"] = candidate_overflow
out_path.write_text(candidate_html, encoding="utf-8")
else:
step["failure_reason"] = (
candidate_overflow.get("fail_reasons")
or "image_fit single-pass: overflow persists"
)
return {
"triggered": True, "passed": passed, "step": step,
"candidate_html": candidate_html, "candidate_overflow": candidate_overflow,
"event_plans": event_plans,
}
def _remeasure_after_frame_reselect(
*, candidate_path: Path, plan: Optional[dict] = None,
) -> dict:
@@ -6370,6 +6526,144 @@ def run_phase_z2_mvp1(
# fields become None (no failure to classify, no escalation pending).
enrich_retry_trace_with_failure_classification(retry_trace)
# 11.7.1 IMP-88 (#88) u7 — Step 17 image_fit single-pass entry trigger.
# Activates when router proposes `image_fit` (image_aspect_mismatch
# classifications, ACTION_BY_CATEGORY row added in u1). Single-pass executor
# (`_attempt_step17_image_fit_single_pass`): per-image plan/apply via
# plan_image_fit + apply_image_fit_css → aggregated CSS overlay → single
# re-render → run_overflow_check. PASS promotes final.html and refreshes
# Step 17/18/19 state; FAIL records `image_fit` step with failure_reason
# so failure_router (u2) SALVAGE_FAILURE_TYPE_BY_ACTION classifies it as
# `image_fit_insufficient` and the §11.7.2 cascade entry block routes
# onto layout_adjust. image_fit stays OUT of _SALVAGE_FAIL_BY_ACTION
# (u6 guard) — Step 17 ENTRY single-pass, not a salvage cascade stage.
if (
not retry_trace.get("retry_passed")
and not retry_trace.get("salvage_passed")
and "image_fit" in set(router_decision.get("proposed_actions_summary") or [])
):
_img_result = _attempt_step17_image_fit_single_pass(
run_dir=run_dir, out_path=out_path,
slide_title=slide_title, slide_footer=slide_footer,
zones_data=zones_data, layout_preset=layout_preset,
layout_css=layout_css,
image_events=overflow.get("image_events") or [],
gap_px=GRID_GAP,
)
if _img_result["triggered"]:
retry_trace.setdefault("salvage_steps", []).append(_img_result["step"])
retry_trace["salvage_attempted"] = True
if _img_result["passed"]:
retry_trace["salvage_passed"] = True
overflow = _img_result["candidate_overflow"]
fit_classification = classify_visual_runtime_check(overflow, debug_zones)
router_decision = route_fit_classification(fit_classification)
router_decision["v4_fallback_summary"] = comp_debug.get("v4_fallback_summary")
router_decision["v4_fallback_selections"] = comp_debug.get("v4_fallback_selections", [])
router_decision["frame_reselect_fallback_status"] = (
"pre_render_rank_2_3_fallback_implemented; "
"post_render visual-fail rerender remains routed through existing action trace"
)
enrich_retry_trace_with_failure_classification(retry_trace)
# 11.7.2 IMP-88 (#88) u7 — direct entry triggers for layout_adjust /
# frame_internal_fit_candidate when the donor_slack pathway above did not
# engage but the router proposes one of these actions (e.g. moderate_overflow
# / layout_zone_mismatch → layout_adjust; frame_capacity_mismatch →
# frame_internal_fit_candidate per u1 ACTION_BY_CATEGORY). Re-uses
# _attempt_salvage_chain with a synthetic initial_failure_type that
# routes (via failure_router NEXT_ACTION_BY_FAILURE / u2) onto the proposed
# action, so the cascade tail (frame_reselect → details_popup_escalation)
# stays consistent with the donor_slack pathway. Also handles the
# image_fit single-pass FAIL → image_fit_insufficient escalation onto
# the cascade (u2 row routes image_fit_insufficient → layout_adjust).
if not retry_trace.get("retry_passed") and not retry_trace.get("salvage_passed"):
_u7_proposed = set(router_decision.get("proposed_actions_summary") or [])
_u7_last_step = (retry_trace.get("salvage_steps") or [])
_u7_image_fit_failed = bool(
_u7_last_step
and _u7_last_step[-1].get("action") == "image_fit"
and _u7_last_step[-1].get("passed") is False
)
_u7_initial = None
if _u7_image_fit_failed:
_u7_initial = "image_fit_insufficient"
elif "layout_adjust" in _u7_proposed:
_u7_initial = "font_step_insufficient"
elif "frame_internal_fit_candidate" in _u7_proposed:
_u7_initial = "layout_adjust_insufficient"
if _u7_initial:
_u7_cls_list = fit_classification.get("classifications") or []
_u7_entry_cls = next(
(c for c in _u7_cls_list
if c.get("proposed_action") in {
"layout_adjust", "frame_internal_fit_candidate", "image_fit",
}),
{},
)
_u7_tpos = _u7_entry_cls.get("zone_position") or ""
_u7_tdz = next(
(dz for dz in debug_zones if dz.get("position") == _u7_tpos), {},
) or {}
_u7_zof = {z.get("position"): z for z in (overflow.get("zones") or [])}
_u7_zm = _u7_zof.get(_u7_tpos) or {}
_u7_excess = max(
0.0,
float(_u7_zm.get("scrollHeight") or 0.0)
- float(_u7_zm.get("clientHeight") or 0.0),
)
from src.fit_verifier import FitAnalysis, RoleFit
_u7_fa_roles, _u7_fa_containers = {}, {}
for _u7_dz in debug_zones:
_u7_pos = _u7_dz.get("position")
if not _u7_pos:
continue
_u7_alloc = float(_u7_dz.get("height_px") or 0.0)
_u7_zmi = _u7_zof.get(_u7_pos) or {}
_u7_ch = float(_u7_zmi.get("clientHeight") or _u7_alloc)
_u7_sh = float(_u7_zmi.get("scrollHeight") or _u7_ch)
_u7_fa_roles[_u7_pos] = RoleFit(
role=_u7_pos, allocated_px=_u7_alloc,
shortfall_px=_u7_sh - _u7_ch,
)
_u7_fa_containers[_u7_pos] = {
"zone": "slide_body", "height_px": int(_u7_alloc),
}
_u7_salvage = _attempt_salvage_chain(
run_dir=run_dir, out_path=out_path,
slide_title=slide_title, slide_footer=slide_footer,
zones_data=zones_data, layout_preset=layout_preset, layout_css=layout_css,
cascade_inputs={
"fit_analysis": FitAnalysis(roles=_u7_fa_roles),
"containers": _u7_fa_containers,
"min_margin_px": None,
"excess_px": _u7_excess, "excess_after_glue_px": _u7_excess,
"block_count": len(
(_u7_tdz.get("placement_trace") or {}).get("internal_regions") or []
) or 1,
"zone_position": _u7_tpos,
"current_font_px": float(_u7_tdz.get("font_size_px") or 0.0),
"available_lines": int(_u7_tdz.get("available_lines") or 0),
"chars_per_line": int(_u7_tdz.get("chars_per_line") or 0),
},
initial_failure_type=_u7_initial, gap_px=GRID_GAP,
)
_u7_prior_steps = retry_trace.get("salvage_steps") or []
_u7_new_steps = _u7_salvage.get("salvage_steps") or []
retry_trace.update(_u7_salvage)
retry_trace["salvage_steps"] = _u7_prior_steps + _u7_new_steps
if _u7_salvage.get("salvage_passed"):
overflow = (_u7_new_steps[-1].get("post_salvage_overflow")) or overflow
fit_classification = classify_visual_runtime_check(overflow, debug_zones)
router_decision = route_fit_classification(fit_classification)
router_decision["v4_fallback_summary"] = comp_debug.get("v4_fallback_summary")
router_decision["v4_fallback_selections"] = comp_debug.get("v4_fallback_selections", [])
router_decision["frame_reselect_fallback_status"] = (
"pre_render_rank_2_3_fallback_implemented; "
"post_render visual-fail rerender remains routed through existing action trace"
)
enrich_retry_trace_with_failure_classification(retry_trace)
# 11.8 IMP-35 (#64) u5 — Step 17 deterministic POPUP gate executor.
# Runs after the salvage cascade exits at cascade-terminal action
# `details_popup_escalation` (router u3 IMPLEMENTED + failure_router u2
@@ -6433,7 +6727,9 @@ def run_phase_z2_mvp1(
note=(
"A3 — zone_ratio_retry + IMP-12 u8/u9 salvage cascade "
"(cross_zone_redistribute → glue_compression → font_step_compression). "
"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)."
),
)