feat(#88): IMP-88 u1~u7 Step 17 retry chain — layout_adjust + image_fit + frame_internal_fit_candidate executors + dispatcher + entry
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 23s
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:
1 parent
e0c39f1bc1
commit
2e3747c5ab
13 files changed
+3007
-15
No files matched your search
@@ -428,3 +428,377 @@ def apply_font_step_compression_css(plan: dict) -> str:
|
||||
return ""
|
||||
return (f'[data-zone-position="{zone_position}"] {{\n'
|
||||
f" font-size: {float(target_font_px):.1f}px;\n}}")
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# IMP-88 u3 : layout_adjust — Step 17 retry chain (8-preset topology swap).
|
||||
# Honors feedback_phase_z_spacing_direction: no shared margin / gap / slide-body
|
||||
# shrink. Cascade entry per u2: image_fit_insufficient → layout_adjust;
|
||||
# downstream per u2: layout_adjust_insufficient → frame_internal_fit_candidate.
|
||||
# Plan-only — dispatcher (u6) consumes new_layout_preset + new_zones_data and
|
||||
# rebuilds layout_css via apply_layout_adjust_layout_css(plan, gap_px).
|
||||
# ──────────────────────────────────────
|
||||
|
||||
|
||||
def _layout_swap_priority(current_topology: str, candidate_topology: str) -> int:
|
||||
"""Lower = preferred swap target. Honors topology-axis mirroring first."""
|
||||
pair = frozenset({current_topology, candidate_topology})
|
||||
if pair == frozenset({"rows", "cols"}):
|
||||
return 0
|
||||
if pair == frozenset({"T", "inverted-T"}):
|
||||
return 1
|
||||
if pair == frozenset({"side-T-left", "side-T-right"}):
|
||||
return 2
|
||||
return 3
|
||||
|
||||
|
||||
def plan_layout_adjust(
|
||||
*, current_layout_preset: str, zones_data: list[dict],
|
||||
) -> dict:
|
||||
"""Layout-preset switch plan (Step 17 retry chain — IMP-88 u3).
|
||||
|
||||
Finds a render-ready sibling preset (same candidate_when.unit_count) and
|
||||
remaps zone positions in catalog order. No common spacing shrink —
|
||||
feedback_phase_z_spacing_direction lock: escalate via layout topology only.
|
||||
"""
|
||||
from src.phase_z2_composition import LAYOUT_PRESETS
|
||||
base = {
|
||||
"action": "layout_adjust",
|
||||
"current_layout_preset": current_layout_preset,
|
||||
}
|
||||
current_spec = LAYOUT_PRESETS.get(current_layout_preset)
|
||||
if current_spec is None:
|
||||
return {
|
||||
**base, "feasible": False, "new_layout_preset": None,
|
||||
"candidates_considered": [],
|
||||
"failure_reason": (
|
||||
f"current_layout_preset '{current_layout_preset}' not in "
|
||||
f"LAYOUT_PRESETS catalog — cannot enumerate same-unit-count siblings."
|
||||
),
|
||||
}
|
||||
current_positions = list(current_spec.get("positions") or [])
|
||||
if len(zones_data) != len(current_positions):
|
||||
return {
|
||||
**base, "feasible": False, "new_layout_preset": None,
|
||||
"candidates_considered": [],
|
||||
"failure_reason": (
|
||||
f"zones_data length {len(zones_data)} != current preset "
|
||||
f"'{current_layout_preset}' positions {current_positions} — "
|
||||
f"cannot remap to a sibling preset."
|
||||
),
|
||||
}
|
||||
unit_count = (current_spec.get("candidate_when") or {}).get("unit_count")
|
||||
candidates = [
|
||||
pid for pid, spec in LAYOUT_PRESETS.items()
|
||||
if pid != current_layout_preset
|
||||
and spec.get("render_ready", False)
|
||||
and ((spec.get("candidate_when") or {}).get("unit_count") == unit_count)
|
||||
]
|
||||
base = {**base, "unit_count": unit_count,
|
||||
"candidates_considered": list(candidates)}
|
||||
if not candidates:
|
||||
return {
|
||||
**base, "feasible": False, "new_layout_preset": None,
|
||||
"failure_reason": (
|
||||
f"no render-ready 8-preset sibling for unit_count {unit_count} "
|
||||
f"(current='{current_layout_preset}'). single (1) and grid-2x2 (4) "
|
||||
f"have no swap target by catalog design."
|
||||
),
|
||||
}
|
||||
catalog_order = list(LAYOUT_PRESETS.keys())
|
||||
current_topo = current_spec.get("topology")
|
||||
candidates.sort(key=lambda pid: (
|
||||
_layout_swap_priority(current_topo, LAYOUT_PRESETS[pid].get("topology")),
|
||||
catalog_order.index(pid),
|
||||
))
|
||||
new_preset = candidates[0]
|
||||
new_positions = list(LAYOUT_PRESETS[new_preset].get("positions") or [])
|
||||
new_zones_data = [
|
||||
{**zd, "position": new_positions[i]} for i, zd in enumerate(zones_data)
|
||||
]
|
||||
return {
|
||||
**base,
|
||||
"feasible": True,
|
||||
"new_layout_preset": new_preset,
|
||||
"swap_topology_from": current_topo,
|
||||
"swap_topology_to": LAYOUT_PRESETS[new_preset].get("topology"),
|
||||
"position_remap": dict(zip(current_positions, new_positions)),
|
||||
"new_zones_data": new_zones_data,
|
||||
}
|
||||
|
||||
|
||||
def apply_layout_adjust_layout_css(plan: dict, gap_px: int) -> Optional[dict]:
|
||||
"""Build a fresh layout_css dict for the swapped preset.
|
||||
|
||||
Returns None when plan is infeasible. Dispatcher (u6) re-renders with
|
||||
render_slide(zones_data=plan['new_zones_data'], layout_preset=plan
|
||||
['new_layout_preset'], layout_css=<this return>, gap_px=gap_px).
|
||||
"""
|
||||
if not plan.get("feasible"):
|
||||
return None
|
||||
new_preset = plan.get("new_layout_preset")
|
||||
new_zones_data = plan.get("new_zones_data") or []
|
||||
if not new_preset or not new_zones_data:
|
||||
return None
|
||||
from src.phase_z2_pipeline import build_layout_css
|
||||
new_layout_css = dict(build_layout_css(new_preset, new_zones_data, gap=gap_px))
|
||||
raw = dict(new_layout_css.get("raw_zone_layout") or {})
|
||||
raw["layout_adjust_applied"] = True
|
||||
raw["layout_adjust_from"] = plan.get("current_layout_preset")
|
||||
raw["layout_adjust_to"] = new_preset
|
||||
new_layout_css["raw_zone_layout"] = raw
|
||||
return new_layout_css
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# IMP-88 u4 : image_fit — Step 17 single-pass image-scoped CSS override.
|
||||
# Consumes overflow_metrics.image_events directly (natural_w/h, rendered_w/h,
|
||||
# natural_ratio, rendered_ratio, delta). Honors feedback_phase_z_spacing
|
||||
# _direction — image-scoped CSS only, no shared margin / frame envelope shrink.
|
||||
# Plan-only — Step 17 entry (u7) is the runtime caller.
|
||||
# Default delta_tol mirrors src.phase_z2_pipeline.IMAGE_ASPECT_DELTA_TOL = 0.05;
|
||||
# overridable arg keeps tests free of the pipeline import cycle.
|
||||
# ──────────────────────────────────────
|
||||
|
||||
|
||||
def plan_image_fit(
|
||||
*, image_event: dict, delta_tol: float = 0.05,
|
||||
) -> dict:
|
||||
"""Image_fit planner (Step 17 retry chain — IMP-88 u4).
|
||||
|
||||
Emits frame-scoped object-fit + max-width/height from a single
|
||||
image_event (overflow_metrics.image_events). Image-scoped only.
|
||||
"""
|
||||
base = {
|
||||
"action": "image_fit",
|
||||
"src": image_event.get("src"),
|
||||
"zone_position": image_event.get("zone_position"),
|
||||
"zone_template_id": image_event.get("zone_template_id"),
|
||||
}
|
||||
delta = image_event.get("delta")
|
||||
if delta is None:
|
||||
return {
|
||||
**base, "feasible": False, "css_overrides": None,
|
||||
"failure_reason": (
|
||||
"image_event delta is None — image not loaded; no aspect "
|
||||
"mismatch can be measured."
|
||||
),
|
||||
}
|
||||
if abs(float(delta)) <= float(delta_tol):
|
||||
return {
|
||||
**base, "feasible": False, "css_overrides": None,
|
||||
"delta": float(delta),
|
||||
"failure_reason": (
|
||||
f"|delta|={abs(float(delta)):.4f} <= delta_tol={delta_tol} — "
|
||||
f"no image_aspect_mismatch to correct (planner no-op)."
|
||||
),
|
||||
}
|
||||
rendered_w = image_event.get("rendered_w")
|
||||
rendered_h = image_event.get("rendered_h")
|
||||
if not (isinstance(rendered_w, (int, float)) and rendered_w > 0
|
||||
and isinstance(rendered_h, (int, float)) and rendered_h > 0):
|
||||
return {
|
||||
**base, "feasible": False, "css_overrides": None,
|
||||
"delta": float(delta),
|
||||
"failure_reason": (
|
||||
"image_event missing positive rendered_w / rendered_h — "
|
||||
"cannot bound max-width / max-height for image-scoped CSS."
|
||||
),
|
||||
}
|
||||
return {
|
||||
**base,
|
||||
"feasible": True,
|
||||
"delta": float(delta),
|
||||
"natural_ratio": image_event.get("natural_ratio"),
|
||||
"rendered_ratio": image_event.get("rendered_ratio"),
|
||||
"natural_w": image_event.get("natural_w"),
|
||||
"natural_h": image_event.get("natural_h"),
|
||||
"rendered_w": int(rendered_w),
|
||||
"rendered_h": int(rendered_h),
|
||||
# delta > 0 ⇒ rendered_ratio > natural_ratio ⇒ rendered too wide ⇒
|
||||
# width axis correction; delta < 0 ⇒ height axis correction.
|
||||
"correction_axis": "width" if float(delta) > 0 else "height",
|
||||
"css_overrides": {
|
||||
"object_fit": "contain",
|
||||
"max_width_px": int(rendered_w),
|
||||
"max_height_px": int(rendered_h),
|
||||
"width": "auto",
|
||||
"height": "auto",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def apply_image_fit_css(plan: dict) -> Optional[str]:
|
||||
"""Build a frame-scoped CSS snippet from a feasible image_fit plan.
|
||||
|
||||
Returns None when plan is infeasible. u7 (Step 17 entry) injects the
|
||||
snippet into the per-slide style override and re-renders.
|
||||
"""
|
||||
if not plan.get("feasible"):
|
||||
return None
|
||||
overrides = plan.get("css_overrides") or {}
|
||||
if not overrides:
|
||||
return None
|
||||
src = plan.get("src") or ""
|
||||
zone_position = plan.get("zone_position") or ""
|
||||
if src:
|
||||
selector = (
|
||||
f".zone[data-zone-position=\"{zone_position}\"] "
|
||||
f"img[src=\"{src}\"]"
|
||||
)
|
||||
else:
|
||||
selector = f".zone[data-zone-position=\"{zone_position}\"] img"
|
||||
return (
|
||||
f"{selector} {{\n"
|
||||
f" object-fit: {overrides.get('object_fit', 'contain')};\n"
|
||||
f" max-width: {int(overrides.get('max_width_px') or 0)}px;\n"
|
||||
f" max-height: {int(overrides.get('max_height_px') or 0)}px;\n"
|
||||
f" width: {overrides.get('width', 'auto')};\n"
|
||||
f" height: {overrides.get('height', 'auto')};\n"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# IMP-88 u5 : frame_internal_fit_candidate — Step 17 retry chain.
|
||||
# Operates ONLY inside the frame contract's declared `internal_envelope`
|
||||
# (PHASE-Z-PIPELINE-OVERVIEW.md:333 lock). Sub-mechanism names allowed by
|
||||
# the OVERVIEW: density envelope / line rhythm / internal grid row / text
|
||||
# block allocation — all unified under the single action label
|
||||
# `frame_internal_fit_candidate` so common-CSS/padding shrink antipatterns
|
||||
# stay quarantined ([[feedback_phase_z_spacing_direction]]).
|
||||
#
|
||||
# Envelope shape (dormant — catalog adds when contracts declare it):
|
||||
# frame_contract["internal_envelope"] = {
|
||||
# "variants": [
|
||||
# {"name": "<sub_mechanism_name>",
|
||||
# "excess_budget_px": <int — px of vertical excess this variant can absorb>,
|
||||
# "css_overrides": {<css-property>: <value>, ...}},
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
# Selection = walk variants in catalog order, pick first whose excess_budget_px
|
||||
# >= effective excess_y (greedy). Catalog order = catalog author's priority.
|
||||
#
|
||||
# No frame contract currently declares `internal_envelope`, so the planner
|
||||
# returns infeasible(envelope_present=False) for every live frame today.
|
||||
# Cascade hand-off: NEXT_ACTION_BY_FAILURE['frame_internal_fit_candidate_
|
||||
# insufficient'] = 'frame_reselect' (set in u2). Plan-only — u6 (salvage
|
||||
# dispatcher) and u7 (Step 17 entry) own the runtime call site.
|
||||
#
|
||||
# frame_contract is an overridable kwarg so tests don't pay the mapper
|
||||
# catalog cache cost / pipeline import cycle (mirrors u4's delta_tol).
|
||||
# ──────────────────────────────────────
|
||||
|
||||
|
||||
def plan_frame_internal_fit_candidate(
|
||||
*, frame_template_id: str,
|
||||
frame_contract: Optional[dict] = None,
|
||||
overflow_zone: Optional[dict] = None,
|
||||
) -> dict:
|
||||
"""frame_internal_fit_candidate planner (Step 17 retry chain — IMP-88 u5).
|
||||
|
||||
Walks `frame_contract['internal_envelope']['variants']` in catalog order
|
||||
and picks the first variant whose excess_budget_px covers `overflow_zone
|
||||
['excess_y']`. Returns infeasible when no contract / no envelope / no
|
||||
variant fits. No common-margin shrink — sub-mechanism CSS is frame-scoped.
|
||||
"""
|
||||
base = {
|
||||
"action": "frame_internal_fit_candidate",
|
||||
"frame_template_id": frame_template_id,
|
||||
}
|
||||
if frame_contract is None:
|
||||
from src.phase_z2_mapper import get_contract
|
||||
frame_contract = get_contract(frame_template_id)
|
||||
if frame_contract is None:
|
||||
return {
|
||||
**base, "feasible": False, "envelope_present": False,
|
||||
"candidates_considered": [], "selected_variant": None,
|
||||
"css_overrides": None,
|
||||
"failure_reason": (
|
||||
f"no frame contract registered for template_id "
|
||||
f"'{frame_template_id}' — cannot enumerate internal_envelope."
|
||||
),
|
||||
}
|
||||
envelope = frame_contract.get("internal_envelope")
|
||||
if not isinstance(envelope, dict):
|
||||
return {
|
||||
**base, "feasible": False, "envelope_present": False,
|
||||
"candidates_considered": [], "selected_variant": None,
|
||||
"css_overrides": None,
|
||||
"failure_reason": (
|
||||
f"frame contract '{frame_template_id}' does not declare "
|
||||
f"internal_envelope — cascade should escalate to frame_reselect."
|
||||
),
|
||||
}
|
||||
variants = list(envelope.get("variants") or [])
|
||||
candidates_considered = [v.get("name") for v in variants if isinstance(v, dict)]
|
||||
if not variants:
|
||||
return {
|
||||
**base, "feasible": False, "envelope_present": True,
|
||||
"envelope_keys": sorted(envelope.keys()),
|
||||
"candidates_considered": candidates_considered,
|
||||
"selected_variant": None, "css_overrides": None,
|
||||
"failure_reason": (
|
||||
f"frame contract '{frame_template_id}' internal_envelope "
|
||||
f"declares no variants — no sub-mechanism available."
|
||||
),
|
||||
}
|
||||
excess_y = 0
|
||||
if isinstance(overflow_zone, dict):
|
||||
ey = overflow_zone.get("excess_y")
|
||||
if isinstance(ey, (int, float)) and ey > 0:
|
||||
excess_y = int(math.ceil(float(ey)))
|
||||
selected: Optional[dict] = None
|
||||
for variant in variants:
|
||||
if not isinstance(variant, dict):
|
||||
continue
|
||||
budget = variant.get("excess_budget_px")
|
||||
if not isinstance(budget, (int, float)):
|
||||
continue
|
||||
if int(budget) >= excess_y:
|
||||
selected = variant
|
||||
break
|
||||
if selected is None:
|
||||
return {
|
||||
**base, "feasible": False, "envelope_present": True,
|
||||
"envelope_keys": sorted(envelope.keys()),
|
||||
"candidates_considered": candidates_considered,
|
||||
"selected_variant": None, "css_overrides": None,
|
||||
"excess_y": excess_y,
|
||||
"failure_reason": (
|
||||
f"all {len(variants)} internal_envelope variant(s) for "
|
||||
f"'{frame_template_id}' have excess_budget_px below excess_y="
|
||||
f"{excess_y}px — internal fit cannot absorb overflow."
|
||||
),
|
||||
}
|
||||
overrides = selected.get("css_overrides") or {}
|
||||
return {
|
||||
**base, "feasible": True, "envelope_present": True,
|
||||
"envelope_keys": sorted(envelope.keys()),
|
||||
"candidates_considered": candidates_considered,
|
||||
"selected_variant": selected.get("name"),
|
||||
"selected_variant_budget_px": int(selected.get("excess_budget_px") or 0),
|
||||
"excess_y": excess_y,
|
||||
"css_overrides": dict(overrides),
|
||||
}
|
||||
|
||||
|
||||
def apply_frame_internal_fit_candidate_css(plan: dict) -> Optional[str]:
|
||||
"""Build a frame-scoped CSS snippet from a feasible frame_internal_fit
|
||||
plan. Returns None when plan is infeasible. u6 / u7 inject the snippet
|
||||
into the per-slide style override and re-render.
|
||||
"""
|
||||
if not plan.get("feasible"):
|
||||
return None
|
||||
overrides = plan.get("css_overrides") or {}
|
||||
if not overrides:
|
||||
return None
|
||||
template_id = plan.get("frame_template_id") or ""
|
||||
if not template_id:
|
||||
return None
|
||||
selector = f".zone[data-template-id=\"{template_id}\"]"
|
||||
body_lines = [
|
||||
f" {prop}: {value};" for prop, value in overrides.items()
|
||||
]
|
||||
return f"{selector} {{\n" + "\n".join(body_lines) + "\n}"
|
||||
Reference in new issue
Block a user