feat(IMP-06): Stage 4 Part 2 — render-path integration (units rebuild + empty zone + Catch K fix)
Refs #6 After `position_assignment_plan` is built, rebuild the `units` list to be plan-aligned so downstream `zones_data` / `debug_zones` / mapper / render all see the post-override sequence. This resolves the long-standing trace-only gap and closes Codex Catch K naturally because the helper now actually drives downstream materialization. - run_phase_z2_mvp1: after `_build_position_assignment_plan`, rebuild `units` ordered by `position_assignment_plan`. `cli_override` entries synthesize a CompositionUnit (resolved template_id + concatenated section raw_content + contract frame_id + selection_path="cli_override" + override audit in rationale). `auto` entries reuse the original planner unit. empty/collision-skipped entries become None placeholders so the downstream zone loop can emit an explicit empty zone record without distorting layout allocation. - zones_data / debug_zones loop: handle `unit is None` by appending an explicit empty record with template_id="__empty__", content_weight=0, min_height_px=0, plus the plan's skipped_reason / replaced_auto_unit / skipped_collided_auto_units / uncovered_section_ids audit fields. - partial render loop: `template_id == "__empty__"` short-circuits to `partial_html = ""` so the slide_base zones loop preserves grid identity without raising TemplateNotFound. - Update the helper-invocation comment so it now describes the actual Part 2 behavior (units rebuild + empty zone handling). Catch K is no longer a future-tense placeholder. Stage 4 Part 3 (follow-up commit) will add: Step 9 application_plan plan-aware additive fields, Step 20 list-shaped filtered_section_reasons entries for override-uncovered sections (Codex #10 Catch O schema), and integration tests proving zones_data["top"] actually contains the overridden section ids when --override-section-assignment is supplied. Regression: 20/20 unit tests pass (9 IMP-06 helper + 8 IMP-05 fallback + 2 catalog invariant + 1 dedicated replaced_auto_unit test), smoke self-check 11/11 (IMP-04 F17 calibration intact). No AI, no calculate_fit, no full planner rerun, no frontend, no sample hardcoding. plan_composition() signature preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1299,6 +1299,12 @@ def render_slide(slide_title: str, slide_footer: Optional[str],
|
|||||||
autoescape=select_autoescape(["html"]),
|
autoescape=select_autoescape(["html"]),
|
||||||
)
|
)
|
||||||
for zone in zones_data:
|
for zone in zones_data:
|
||||||
|
# Stage 4 Part 2 (Codex #10 Catch N) — empty zone produced by section
|
||||||
|
# assignment override has no partial template; render an empty string so
|
||||||
|
# the slide_base zones loop preserves grid identity without TemplateNotFound.
|
||||||
|
if zone.get("template_id") == "__empty__":
|
||||||
|
zone["partial_html"] = ""
|
||||||
|
continue
|
||||||
partial = env.get_template(f"families/{zone['template_id']}.html")
|
partial = env.get_template(f"families/{zone['template_id']}.html")
|
||||||
zone["partial_html"] = partial.render(slot_payload=zone["slot_payload"])
|
zone["partial_html"] = partial.render(slot_payload=zone["slot_payload"])
|
||||||
|
|
||||||
@@ -2058,12 +2064,16 @@ def run_phase_z2_mvp1(
|
|||||||
layout_preset = override_layout
|
layout_preset = override_layout
|
||||||
layout_override_applied = True
|
layout_override_applied = True
|
||||||
|
|
||||||
# IMP-06 (#6 / Codex #6/#7/#10 lock) — zone-section assignment override.
|
# IMP-06 (#6 / Codex #6/#7/#10/#11/#12 lock) — zone-section assignment override.
|
||||||
# Applied AFTER final layout_preset resolution. ZONE_ID = layout positions.
|
# Applied AFTER final layout_preset resolution. ZONE_ID = layout positions.
|
||||||
# The helper validates unknown zone ids / unknown section ids and builds a
|
# The helper validates unknown zone ids / unknown section ids and builds a
|
||||||
# `position_assignment_plan`. The plan drives downstream zones_data /
|
# `position_assignment_plan`. Immediately below, Stage 4 Part 2 rebuilds the
|
||||||
# debug_zones / Step 9 application_plan / Step 20 coverage (see Stage 4
|
# `units` list aligned with that plan: cli_override entries synthesize a
|
||||||
# block further below where `units_by_position` is materialized).
|
# CompositionUnit, auto entries reuse the original planner unit, and empty/
|
||||||
|
# collision-skipped entries become None placeholders. The downstream
|
||||||
|
# zones_data / debug_zones loop then handles None entries by emitting an
|
||||||
|
# explicit empty zone record (template_id="__empty__") so the slide grid
|
||||||
|
# preserves position identity without distorting layout allocation.
|
||||||
section_assignment_plan: Optional[list[dict]] = None
|
section_assignment_plan: Optional[list[dict]] = None
|
||||||
section_assignment_summary: Optional[dict] = None
|
section_assignment_summary: Optional[dict] = None
|
||||||
if override_section_assignments and layout_preset is not None:
|
if override_section_assignments and layout_preset is not None:
|
||||||
@@ -2106,6 +2116,61 @@ def run_phase_z2_mvp1(
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Stage 4 Part 2 — rebuild units list aligned with position_assignment_plan so
|
||||||
|
# downstream zones_data / debug_zones / Step 9 / Step 20 derive from the plan
|
||||||
|
# rather than the original auto-selected sequence. None placeholders preserve
|
||||||
|
# position identity for empty/collision-skipped zones (Codex #10 Catch N).
|
||||||
|
from src.phase_z2_composition import CompositionUnit
|
||||||
|
plan_units: list = []
|
||||||
|
for entry in section_assignment_plan:
|
||||||
|
assignment_source = entry["assignment_source"]
|
||||||
|
if assignment_source == "cli_override" and entry["template_id"] is not None:
|
||||||
|
sids = entry["source_section_ids"]
|
||||||
|
raw_content_parts = []
|
||||||
|
title_parts = []
|
||||||
|
for sid in sids:
|
||||||
|
sect = sections_by_id.get(sid)
|
||||||
|
if sect is None:
|
||||||
|
continue
|
||||||
|
raw_content_parts.append(sect.raw_content or "")
|
||||||
|
if sect.title:
|
||||||
|
title_parts.append(sect.title)
|
||||||
|
contract = get_contract(entry["template_id"])
|
||||||
|
contract_frame_id = (contract or {}).get("frame_id") or ""
|
||||||
|
override_unit = CompositionUnit(
|
||||||
|
source_section_ids=list(sids),
|
||||||
|
merge_type="cli_override",
|
||||||
|
frame_template_id=entry["template_id"],
|
||||||
|
frame_id=str(contract_frame_id),
|
||||||
|
frame_number=0,
|
||||||
|
confidence=0.0,
|
||||||
|
label="use_as_is",
|
||||||
|
phase_z_status="matched_zone",
|
||||||
|
raw_content="\n\n".join(raw_content_parts),
|
||||||
|
title=" / ".join(title_parts) if title_parts else "+".join(sids),
|
||||||
|
v4_rank=None,
|
||||||
|
selection_path="cli_override",
|
||||||
|
fallback_reason=None,
|
||||||
|
score=0.0,
|
||||||
|
rationale={
|
||||||
|
"section_assignment_override": entry["section_assignment_override"],
|
||||||
|
"replaced_auto_unit": entry["replaced_auto_unit"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
plan_units.append(override_unit)
|
||||||
|
elif assignment_source == "auto":
|
||||||
|
# Find original auto unit by source_section_ids
|
||||||
|
matched = None
|
||||||
|
for u in units:
|
||||||
|
if list(u.source_section_ids) == entry["source_section_ids"]:
|
||||||
|
matched = u
|
||||||
|
break
|
||||||
|
plan_units.append(matched) # may be None if not found (unexpected)
|
||||||
|
else:
|
||||||
|
# empty / collision-skipped
|
||||||
|
plan_units.append(None)
|
||||||
|
units = plan_units
|
||||||
|
|
||||||
if not units or layout_preset is None:
|
if not units or layout_preset is None:
|
||||||
# composition planner 결과 = 0 units. Sections 가 모두 V4 lookup 실패 또는
|
# composition planner 결과 = 0 units. Sections 가 모두 V4 lookup 실패 또는
|
||||||
# status filter 통과 못 함. error.json 기록 후 abort.
|
# status filter 통과 못 함. error.json 기록 후 abort.
|
||||||
@@ -2254,6 +2319,53 @@ def run_phase_z2_mvp1(
|
|||||||
|
|
||||||
for i, unit in enumerate(units):
|
for i, unit in enumerate(units):
|
||||||
position = positions[i] if i < len(positions) else f"zone_{i}"
|
position = positions[i] if i < len(positions) else f"zone_{i}"
|
||||||
|
# Stage 4 Part 2 — empty position (override plan produced no renderable unit).
|
||||||
|
# Preserve zone identity with an explicit empty record so layout/grid stay
|
||||||
|
# structurally consistent without distorting allocation (Codex #10 Catch N).
|
||||||
|
if unit is None:
|
||||||
|
plan_entry = None
|
||||||
|
if section_assignment_plan is not None and i < len(section_assignment_plan):
|
||||||
|
plan_entry = section_assignment_plan[i]
|
||||||
|
zones_data.append({
|
||||||
|
"position": position,
|
||||||
|
"template_id": "__empty__",
|
||||||
|
"slot_payload": {},
|
||||||
|
"content_weight": {"score": 0},
|
||||||
|
"min_height_px": 0,
|
||||||
|
"assignment_source": "empty",
|
||||||
|
"skipped_reason": (
|
||||||
|
(plan_entry or {}).get("skipped_reason")
|
||||||
|
or "section_assignment_override_empty_or_unrenderable"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
debug_zones.append({
|
||||||
|
"position": position,
|
||||||
|
"source_section_ids": [],
|
||||||
|
"merge_type": "empty",
|
||||||
|
"title": "",
|
||||||
|
"v4_template_id": "__empty__",
|
||||||
|
"v4_label": None,
|
||||||
|
"v4_confidence": 0.0,
|
||||||
|
"selection_path": "section_assignment_empty",
|
||||||
|
"fallback_reason": None,
|
||||||
|
"fallback_used": False,
|
||||||
|
"phase_z_status": None,
|
||||||
|
"composition_score": 0.0,
|
||||||
|
"composition_rationale": {},
|
||||||
|
"composition_notes": [],
|
||||||
|
"mapper_type": "empty",
|
||||||
|
"contract_id": "__empty__",
|
||||||
|
"contract_frame_id": None,
|
||||||
|
"assignment_source": "empty",
|
||||||
|
"skipped_reason": (
|
||||||
|
(plan_entry or {}).get("skipped_reason")
|
||||||
|
or "section_assignment_override_empty_or_unrenderable"
|
||||||
|
),
|
||||||
|
"replaced_auto_unit": (plan_entry or {}).get("replaced_auto_unit"),
|
||||||
|
"skipped_collided_auto_units": (plan_entry or {}).get("skipped_collided_auto_units", []),
|
||||||
|
"uncovered_section_ids": (plan_entry or {}).get("uncovered_section_ids", []),
|
||||||
|
})
|
||||||
|
continue
|
||||||
synth_section = MdxSection(
|
synth_section = MdxSection(
|
||||||
section_id="+".join(unit.source_section_ids),
|
section_id="+".join(unit.source_section_ids),
|
||||||
section_num=0,
|
section_num=0,
|
||||||
|
|||||||
Reference in New Issue
Block a user