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:
2026-05-14 06:10:43 +09:00
parent d596fabde0
commit b81e564f65
2 changed files with 63 additions and 4 deletions

View File

@@ -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.
assert by_pos["top"]["previous_source_section_ids"] == ["MOCK_S1", "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).
assert by_pos["bottom"]["assignment_source"] == "auto"
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.
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
# ─── 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 ───────────────────────────