feat(#86): IMP-86 u1~u5 placeholder zones_data + invariant guard

Mapper FitError handler now appends a __empty__ placeholder to zones_data
and a matching debug_zone so the surviving cardinality stays in sync with
the active layout preset's grid rows. A pre-build_layout_css invariant
guard fails fast with preset/positions/count diagnostics if drift recurs.
Per-record telemetry (adapter_needed, mapper_fit_error, provisional) is
exposed on both placeholder records; authoritative slide_status.adapter_
needed_units schema is unchanged.

Closes mdx03 reject override regression: Step 12 AI router now reachable
without heights_px ValueError; default-path behavior unaffected.

u1 — FitError placeholder zones_data + debug_zone (src/phase_z2_pipeline.py)
u2 — pre-build_layout_css invariant guard (src/phase_z2_pipeline.py)
u3 — horizontal-2 normal+placeholder helper unit (test_compute_per_zone_geometry.py)
u4 — mdx03 reject override → Step 12 integration + default regression
u5 — placeholder telemetry surface (adapter_needed/mapper_fit_error/provisional)

Tests:
- u3 helper: 7 passed (0.06s)
- u4+u5 integration: 2 passed (7.87s)
- Phase Z2 + AI fallback regression: 544 passed (66.28s)
- Broader sweep (excl. matching/pipeline heavy): 1066 passed (96.12s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 18:25:14 +09:00
parent cacc5b30db
commit c53722ad0b
3 changed files with 397 additions and 1 deletions

View File

@@ -99,3 +99,63 @@ def test_fr_default_single_returns_full_body():
per_zone = _compute_per_zone_geometry(layout_css, debug_zones, GRID_GAP)
assert per_zone[0]["zone_height_px"] == SLIDE_BODY_HEIGHT
assert per_zone[0]["zone_width_px"] == SLIDE_BODY_WIDTH
def _placeholder_zone(position: str) -> dict:
# IMP-86 u3 — mirror the u1 mapper-FitError placeholder zones_data
# record shape (`src/phase_z2_pipeline.py:4459-4469`) used to keep the
# failed unit's preset position in zones_data so build_layout_css /
# _compute_per_zone_geometry observe len(zones_data) == active preset's
# css_areas rows (R). content_weight.score == 0 ensures the placeholder
# does not steal weight from the surviving normal zone.
return {
"position": position,
"template_id": "__empty__",
"slot_payload": {},
"content_weight": {"score": 0},
"min_height_px": 100,
"assignment_source": "imp86_u1_adapter_needed",
"section_assignment_override": False,
"provisional": False,
}
def test_horizontal_2_normal_plus_placeholder_preserves_R2_cardinality():
"""IMP-86 u3 — horizontal-2 with one mapper-success zone + one
mapper-FitError placeholder (per IMP-86 u1) must keep heights_px /
debug_zones / per_zone cardinality locked at R=2.
Reproduces the bug scenario in the issue body (mdx03 reject override
where 03-2 hits adapter_needed) at the helper level: if the FitError
path forgets to append a placeholder, heights_px length 1 vs R=2
raises ValueError at `_compute_per_zone_geometry`. With the u1
placeholder, all three artifacts (heights_px, debug_zones, per_zone)
stay at length 2 and the geometry helper succeeds.
"""
zones = [_zone("top", 1.0), _placeholder_zone("bottom")]
layout_css = build_layout_css("horizontal-2", zones)
debug_zones = [{"position": "top"}, {"position": "bottom"}]
# heights_px length is locked to R=2 (parsed from preset css_areas).
assert layout_css["areas"] == '"top" "bottom"'
assert len(layout_css["heights_px"]) == 2
# widths_px length is locked to C=1.
assert len(layout_css["widths_px"]) == 1
# Placeholder zone (score=0) gets its min_height_px (100) and the
# surviving normal zone absorbs the remaining body height after gap.
assert layout_css["heights_px"][1] == 100
assert (
layout_css["heights_px"][0] + layout_css["heights_px"][1] + GRID_GAP
== SLIDE_BODY_HEIGHT
)
per_zone = _compute_per_zone_geometry(layout_css, debug_zones, GRID_GAP)
# per_zone cardinality matches debug_zones (which matches R=2).
assert len(per_zone) == 2
assert [pz["position"] for pz in per_zone] == ["top", "bottom"]
assert per_zone[0]["zone_height_px"] == layout_css["heights_px"][0]
assert per_zone[1]["zone_height_px"] == layout_css["heights_px"][1]
# Both zones share the single column => width == SLIDE_BODY_WIDTH.
assert per_zone[0]["zone_width_px"] == SLIDE_BODY_WIDTH
assert per_zone[1]["zone_width_px"] == SLIDE_BODY_WIDTH