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:
@@ -4434,15 +4434,87 @@ def run_phase_z2_mvp1(
|
||||
try:
|
||||
slot_payload = map_mdx_to_slots(synth_section, unit.frame_template_id)
|
||||
except FitError as e:
|
||||
_fit_error_str = str(e)
|
||||
_unit_provisional = bool(getattr(unit, "provisional", False))
|
||||
adapter_record = {
|
||||
"position": position,
|
||||
"source_section_ids": unit.source_section_ids,
|
||||
"merge_type": unit.merge_type,
|
||||
"template_id": unit.frame_template_id,
|
||||
"reason": "fit_error",
|
||||
"fit_error": str(e),
|
||||
"fit_error": _fit_error_str,
|
||||
}
|
||||
adapter_needed_units.append(adapter_record)
|
||||
# IMP-86 u1 — placeholder zones_data + debug_zone keep the failed
|
||||
# unit's preset position so downstream build_layout_css /
|
||||
# _compute_per_zone_geometry observe len(zones_data) == active
|
||||
# layout preset's css_areas rows (R). Without this both arrays
|
||||
# drift relative to R, raising
|
||||
# ValueError("heights_px length N != grid rows R=M") at
|
||||
# _compute_per_zone_geometry. Mirrors the IMP-30 empty-shell
|
||||
# pattern: zones_data uses template_id="__empty__" so render_slide
|
||||
# short-circuits to empty partial_html; debug_zone keeps the
|
||||
# original V4 evidence and adapter_needed_units stays the
|
||||
# authoritative adapter signal.
|
||||
# IMP-86 u5 — per-record telemetry. adapter_needed=True +
|
||||
# mapper_fit_error=<str(FitError)> + provisional mirror the
|
||||
# adapter signal directly on each placeholder record so
|
||||
# debug.json / final.html consumers can identify the adapter
|
||||
# contract surface from the zones array alone, without joining
|
||||
# against slide_status.adapter_needed_units. adapter_needed_units
|
||||
# itself is unchanged (still the authoritative per-slide list).
|
||||
_placeholder_popup = compose_zone_popup_payload(unit, 0)
|
||||
zones_data.append({
|
||||
"position": position,
|
||||
"template_id": "__empty__",
|
||||
"slot_payload": {},
|
||||
"content_weight": {"score": 0},
|
||||
"min_height_px": min_height_px,
|
||||
"assignment_source": "imp86_u1_adapter_needed",
|
||||
"section_assignment_override": False,
|
||||
"provisional": _unit_provisional,
|
||||
"adapter_needed": True,
|
||||
"mapper_fit_error": _fit_error_str,
|
||||
**_placeholder_popup,
|
||||
})
|
||||
debug_zones.append({
|
||||
"position": position,
|
||||
"source_section_ids": list(unit.source_section_ids),
|
||||
"merge_type": unit.merge_type,
|
||||
"title": unit.title,
|
||||
"v4_rank1_frame_id": unit.frame_id,
|
||||
"v4_rank1_frame_number": unit.frame_number,
|
||||
"v4_template_id": unit.frame_template_id,
|
||||
"v4_label": unit.label,
|
||||
"v4_confidence": float(unit.confidence or 0.0),
|
||||
"v4_selected_rank": unit.v4_rank,
|
||||
"selection_path": unit.selection_path,
|
||||
"fallback_reason": unit.fallback_reason,
|
||||
"fallback_used": bool(unit.selection_path and "fallback" in unit.selection_path),
|
||||
"phase_z_status": unit.phase_z_status,
|
||||
"composition_score": float(unit.score or 0.0),
|
||||
"composition_rationale": dict(unit.rationale or {}),
|
||||
"composition_notes": list(unit.notes),
|
||||
"mapper_type": "adapter_needed",
|
||||
"contract_id": unit.frame_template_id,
|
||||
"contract_frame_id": contract_frame_id,
|
||||
"builder": builder_name,
|
||||
"min_height_px": min_height_px,
|
||||
"slot_payload_keys": [],
|
||||
"content_truncated_count": None,
|
||||
"assets_dir": None,
|
||||
"content_weight": {"score": 0},
|
||||
"placement_trace": placement_trace,
|
||||
"assignment_source": "imp86_u1_adapter_needed",
|
||||
"section_assignment_override": False,
|
||||
"replaced_auto_unit": None,
|
||||
"skipped_collided_auto_units": [],
|
||||
"uncovered_section_ids": [],
|
||||
"skipped_reason": "imp86_u1_adapter_needed_mapper_fit_error",
|
||||
"provisional": _unit_provisional,
|
||||
"adapter_needed": True,
|
||||
"mapper_fit_error": _fit_error_str,
|
||||
})
|
||||
print(f" adapter : zone--{position} {unit.source_section_ids} → "
|
||||
f"{unit.frame_template_id} FitError → adapter_needed (skip render)")
|
||||
continue
|
||||
@@ -4935,6 +5007,38 @@ def run_phase_z2_mvp1(
|
||||
|
||||
# 6. Build layout CSS — horizontal-2 = dynamic heights (regression preserve), 그 외 = fr default.
|
||||
# Step D-ext : override_zone_geometries 가 들어오면 layout_css 강제.
|
||||
# IMP-86 u2 — pre-build layout invariant guard. zones_data / debug_zones
|
||||
# MUST be cardinality- and position-aligned with the active layout
|
||||
# preset's css_areas tokens before build_layout_css derives heights_px
|
||||
# / widths_px and _compute_per_zone_geometry validates them against R/C.
|
||||
# If a mapper FitError path (IMP-86 u1) — or any future zone-drop path —
|
||||
# forgets to append a placeholder, the resulting shape drift would
|
||||
# surface as a confusing `heights_px length N != grid rows R=M`
|
||||
# ValueError deep inside the geometry helper. This guard fails fast at
|
||||
# the pipeline boundary with preset / expected positions / actual
|
||||
# positions / count diagnostics so the root cause is obvious from the
|
||||
# log line (factual_verification: value + path + upstream).
|
||||
_active_preset = LAYOUT_PRESETS[layout_preset]
|
||||
_expected_positions = _parse_css_areas(_active_preset["css_areas"])[1]
|
||||
_actual_positions = [zd["position"] for zd in zones_data]
|
||||
_debug_positions = [dz["position"] for dz in debug_zones]
|
||||
if (
|
||||
len(zones_data) != len(_expected_positions)
|
||||
or len(debug_zones) != len(_expected_positions)
|
||||
or sorted(_actual_positions) != sorted(_expected_positions)
|
||||
or sorted(_debug_positions) != sorted(_expected_positions)
|
||||
):
|
||||
raise ValueError(
|
||||
"phase_z2_pipeline pre-build layout invariant violation: "
|
||||
f"layout_preset={layout_preset!r} "
|
||||
f"css_areas={_active_preset['css_areas']!r} "
|
||||
f"expected_positions={_expected_positions!r} "
|
||||
f"zones_data_positions={_actual_positions!r} "
|
||||
f"debug_zones_positions={_debug_positions!r} "
|
||||
f"zones_count={len(zones_data)} "
|
||||
f"debug_count={len(debug_zones)} "
|
||||
f"expected_count={len(_expected_positions)}"
|
||||
)
|
||||
layout_css = build_layout_css(
|
||||
layout_preset, zones_data, override_zone_geometries=override_zone_geometries
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user