feat(IMP-06): Stage 4 part 1 — replaced_auto_unit field + comment fix
Refs #6 Stage 4 split per Codex #10 acceptance: this commit lands the schema + trace refinements required before the render-path rewiring. The actual units/zones_data/Step 9/Step 20 plan-driven materialization remains in Stage 4 part 2 (follow-up commit) so each commit is reviewable on its own and regression-safe. - _build_position_assignment_plan: add replaced_auto_unit field. Populated only when the explicitly overridden position already held an auto unit AND that auto unit had different source_section_ids than the override. Documents a same-position override replacement as a distinct audit fact, separate from skipped_collided_auto_units which captures cross-position whole-skips per the locked collision policy. - Backfill replaced_auto_unit = None on the empty/collision/auto branches for schema-stable consumers. - Update the override-application comment near the helper invocation so it no longer claims the helper "reorders units"; Stage 4 part 2 will be the commit that wires the plan into the actual render path. - Helper unit tests: assert replaced_auto_unit shape in the collision scenario and add a dedicated case that distinguishes same-sections (template swap via --override-frame -> None) from different-sections same-position replacement (populated, reason="same_position_override_replacement"). No AI, no calculate_fit, no full planner rerun, no frontend, no sample hardcoding. plan_composition() signature preserved. helper remains pure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -954,6 +954,18 @@ def _build_position_assignment_plan(
|
|||||||
sid for sid in previous_source_section_ids if sid not in sids_set
|
sid for sid in previous_source_section_ids if sid not in sids_set
|
||||||
]
|
]
|
||||||
template_id, skipped_reason, selector_trace = _resolve_template_for_override(pos, sids)
|
template_id, skipped_reason, selector_trace = _resolve_template_for_override(pos, sids)
|
||||||
|
# IMP-06 Stage 4 (Codex #9 R1 + Claude #9 Catch L + Codex #10) — replaced_auto_unit
|
||||||
|
# populated only when the same position previously had an auto unit and that
|
||||||
|
# auto unit was different from the requested override. Documents "this auto
|
||||||
|
# unit was removed from this position to apply the override" as a distinct
|
||||||
|
# audit fact (vs skipped_collided_auto_units which is cross-position skip).
|
||||||
|
replaced_auto_unit = None
|
||||||
|
if auto_unit is not None and list(auto_unit.source_section_ids) != list(sids):
|
||||||
|
replaced_auto_unit = {
|
||||||
|
"unit_id": _unit_id(list(auto_unit.source_section_ids)),
|
||||||
|
"source_section_ids": list(auto_unit.source_section_ids),
|
||||||
|
"reason": "same_position_override_replacement",
|
||||||
|
}
|
||||||
plan.append({
|
plan.append({
|
||||||
"position": pos,
|
"position": pos,
|
||||||
"assignment_source": "cli_override",
|
"assignment_source": "cli_override",
|
||||||
@@ -967,6 +979,7 @@ def _build_position_assignment_plan(
|
|||||||
"zone_id": pos,
|
"zone_id": pos,
|
||||||
"requested_section_ids": list(sids),
|
"requested_section_ids": list(sids),
|
||||||
},
|
},
|
||||||
|
"replaced_auto_unit": replaced_auto_unit,
|
||||||
"skipped_collided_auto_units": [],
|
"skipped_collided_auto_units": [],
|
||||||
"uncovered_section_ids": uncovered_from_previous,
|
"uncovered_section_ids": uncovered_from_previous,
|
||||||
"skipped_reason": skipped_reason,
|
"skipped_reason": skipped_reason,
|
||||||
@@ -984,6 +997,7 @@ def _build_position_assignment_plan(
|
|||||||
"template_id": None,
|
"template_id": None,
|
||||||
"previous_source_section_ids": [],
|
"previous_source_section_ids": [],
|
||||||
"section_assignment_override": None,
|
"section_assignment_override": None,
|
||||||
|
"replaced_auto_unit": None,
|
||||||
"skipped_collided_auto_units": [],
|
"skipped_collided_auto_units": [],
|
||||||
"uncovered_section_ids": [],
|
"uncovered_section_ids": [],
|
||||||
"skipped_reason": "no_auto_unit_available",
|
"skipped_reason": "no_auto_unit_available",
|
||||||
@@ -1008,6 +1022,7 @@ def _build_position_assignment_plan(
|
|||||||
"template_id": None,
|
"template_id": None,
|
||||||
"previous_source_section_ids": list(auto_unit.source_section_ids),
|
"previous_source_section_ids": list(auto_unit.source_section_ids),
|
||||||
"section_assignment_override": None,
|
"section_assignment_override": None,
|
||||||
|
"replaced_auto_unit": None,
|
||||||
"skipped_collided_auto_units": [{
|
"skipped_collided_auto_units": [{
|
||||||
"unit_id": unit_id_str,
|
"unit_id": unit_id_str,
|
||||||
"source_section_ids": list(auto_unit.source_section_ids),
|
"source_section_ids": list(auto_unit.source_section_ids),
|
||||||
@@ -1029,6 +1044,7 @@ def _build_position_assignment_plan(
|
|||||||
),
|
),
|
||||||
"previous_source_section_ids": list(auto_unit.source_section_ids),
|
"previous_source_section_ids": list(auto_unit.source_section_ids),
|
||||||
"section_assignment_override": None,
|
"section_assignment_override": None,
|
||||||
|
"replaced_auto_unit": None,
|
||||||
"skipped_collided_auto_units": [],
|
"skipped_collided_auto_units": [],
|
||||||
"uncovered_section_ids": [],
|
"uncovered_section_ids": [],
|
||||||
"skipped_reason": None,
|
"skipped_reason": None,
|
||||||
@@ -2042,11 +2058,12 @@ 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 15-axis lock) — zone-section assignment override.
|
# IMP-06 (#6 / Codex #6/#7/#10 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 computes a position_assignment_plan; we then validate unknown
|
# The helper validates unknown zone ids / unknown section ids and builds a
|
||||||
# zone ids / unknown section ids and reorder units to match the plan so that
|
# `position_assignment_plan`. The plan drives downstream zones_data /
|
||||||
# downstream zones_data / debug_zones / Step 9 derive consistently.
|
# debug_zones / Step 9 application_plan / Step 20 coverage (see Stage 4
|
||||||
|
# block further below where `units_by_position` is materialized).
|
||||||
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:
|
||||||
|
|||||||
@@ -101,9 +101,15 @@ def test_override_collision_whole_skip_no_split_uncovered_traced():
|
|||||||
# re-extracted into the replaced position; instead it surfaces as uncovered.
|
# re-extracted into the replaced position; instead it surfaces as uncovered.
|
||||||
assert by_pos["top"]["previous_source_section_ids"] == ["MOCK_S1", "MOCK_S2"]
|
assert by_pos["top"]["previous_source_section_ids"] == ["MOCK_S1", "MOCK_S2"]
|
||||||
assert by_pos["top"]["uncovered_section_ids"] == ["MOCK_S2"]
|
assert by_pos["top"]["uncovered_section_ids"] == ["MOCK_S2"]
|
||||||
|
# IMP-06 Stage 4 (Codex #10): replaced_auto_unit populated for same-position
|
||||||
|
# override replacement (previous auto unit had different sections).
|
||||||
|
assert by_pos["top"]["replaced_auto_unit"] is not None
|
||||||
|
assert by_pos["top"]["replaced_auto_unit"]["unit_id"] == "MOCK_S1+MOCK_S2"
|
||||||
|
assert by_pos["top"]["replaced_auto_unit"]["reason"] == "same_position_override_replacement"
|
||||||
# bottom : non-overlapping auto_solo is retained (no collision).
|
# bottom : non-overlapping auto_solo is retained (no collision).
|
||||||
assert by_pos["bottom"]["assignment_source"] == "auto"
|
assert by_pos["bottom"]["assignment_source"] == "auto"
|
||||||
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S3"]
|
assert by_pos["bottom"]["source_section_ids"] == ["MOCK_S3"]
|
||||||
|
assert by_pos["bottom"]["replaced_auto_unit"] is None
|
||||||
# Summary aggregates MOCK_S2 as the global uncovered section.
|
# Summary aggregates MOCK_S2 as the global uncovered section.
|
||||||
assert summary["uncovered_section_ids"] == ["MOCK_S2"]
|
assert summary["uncovered_section_ids"] == ["MOCK_S2"]
|
||||||
|
|
||||||
@@ -255,6 +261,42 @@ def test_position_with_no_auto_unit_marked_empty():
|
|||||||
assert summary["applied_count"] == 0
|
assert summary["applied_count"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Case 9b : replaced_auto_unit distinguishes same-sections vs different ──
|
||||||
|
|
||||||
|
|
||||||
|
def test_replaced_auto_unit_only_when_previous_auto_differs_from_override():
|
||||||
|
"""Codex #10 R1 + Claude #10 Catch L : replaced_auto_unit populated only when
|
||||||
|
the same position previously had an auto unit AND that auto unit had
|
||||||
|
different sections than the override. Same sections (override just swaps
|
||||||
|
template via --override-frame) yields replaced_auto_unit = None.
|
||||||
|
"""
|
||||||
|
# Case A : same sections → replaced_auto_unit None (just template swap)
|
||||||
|
units_a = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto")]
|
||||||
|
plan_a, _ = _build_position_assignment_plan(
|
||||||
|
units=units_a,
|
||||||
|
positions=["top"],
|
||||||
|
override_section_assignments={"top": ["MOCK_S1"]},
|
||||||
|
sections_by_id={"MOCK_S1": _FakeSection("MOCK_S1")},
|
||||||
|
override_frames={"MOCK_S1": "MOCK_T_explicit_swap"},
|
||||||
|
)
|
||||||
|
assert plan_a[0]["replaced_auto_unit"] is None
|
||||||
|
assert plan_a[0]["template_id"] == "MOCK_T_explicit_swap"
|
||||||
|
|
||||||
|
# Case B : different sections → replaced_auto_unit populated
|
||||||
|
units_b = [_FakeUnit(source_section_ids=["MOCK_S1"], frame_template_id="MOCK_T_auto")]
|
||||||
|
plan_b, _ = _build_position_assignment_plan(
|
||||||
|
units=units_b,
|
||||||
|
positions=["top"],
|
||||||
|
override_section_assignments={"top": ["MOCK_S2"]},
|
||||||
|
sections_by_id={"MOCK_S2": _FakeSection("MOCK_S2")},
|
||||||
|
override_frames={"MOCK_S2": "MOCK_T_for_S2"},
|
||||||
|
)
|
||||||
|
assert plan_b[0]["replaced_auto_unit"] is not None
|
||||||
|
assert plan_b[0]["replaced_auto_unit"]["unit_id"] == "MOCK_S1"
|
||||||
|
assert plan_b[0]["replaced_auto_unit"]["source_section_ids"] == ["MOCK_S1"]
|
||||||
|
assert plan_b[0]["replaced_auto_unit"]["reason"] == "same_position_override_replacement"
|
||||||
|
|
||||||
|
|
||||||
# ─── Case 9 : summary aggregation invariants ───────────────────────────
|
# ─── Case 9 : summary aggregation invariants ───────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user