feat(#77): IMP-48 composition planner re-split on all-reject (u1~u9)

Add resplit_all_reject_merges() helper in phase_z2_composition.py that
detects parent_merged / parent_merged_inferred units with label=reject
and rebuilds them as per-section single units using each section's own
rank-1 V4 evidence (no frame swap, MDX raw_content preserved).

Pipeline hook fires once after Step 6 settling chain (u12/u4/empty-shell)
and section_assignment_plan resolution, before Step 6 artifact write.
Guards: beneficial-split rule (>=1 non-reject), coverage equality, layout
cap (>4 abort), max_retry=1, section_assignment_override short-circuit.

Audit: comp_debug["imp48_resplit"] additive payload (applied, split_units,
skipped_units, post_split_unit_count, post_split_layout_preset);
selection_path="resplit_from_merge" telemetry on rebuilt singles;
layout_preset re-derived via select_layout_preset(new_units).

Tests: 39/39 PASS (composition u1~u6: 14 cases; pipeline u7~u9: 25 cases).
Scoped regression 720/6 with 6 failures isolated as pre-existing on
baseline 79f9ea5 (independent of IMP-48). mdx03 golden lock preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 05:00:07 +09:00
parent 79f9ea5c92
commit ee97f4fc78
4 changed files with 2554 additions and 0 deletions

View File

@@ -43,6 +43,7 @@ from phase_z2_composition import (
CompositionUnit,
derive_parent_id,
plan_composition,
resplit_all_reject_merges,
select_display_strategy_candidates,
select_layout_candidates,
select_region_layout_candidates,
@@ -3966,6 +3967,52 @@ def run_phase_z2_mvp1(
file=sys.stderr,
)
# IMP-48 (#77) — re-split merged-reject units into per-section singles.
# One-shot, deterministic (AI=0) post-pass. Fires AFTER all Step 6 settling
# chains (initial plan_composition / u12 mixed admission / u4 provisional
# retry / empty-shell) and AFTER section_assignment_plan is known, but
# BEFORE the Step 6 artifact write below — so the artifact reflects the
# post-resplit unit list. SKIPS when --override-section-assignments is
# active (IMP-06 / #6 is the ground truth). Helper guardrails (coverage
# equality / beneficial split / layout cap ≤ 4) keep mdx03 byte-identical
# (no-op on use_as_is / light_edit slides). u5 re-derives layout_preset
# below using the audit payload.
units, _imp48_audit = resplit_all_reject_merges(
units,
sections,
lookup_fn,
V4_LABEL_TO_PHASE_Z_STATUS,
MVP1_ALLOWED_STATUSES,
capacity_fit_fn=compute_capacity_fit,
v4_candidates_lookup_fn=candidates_lookup_fn,
section_assignment_override=section_assignment_plan is not None,
)
comp_debug["imp48_resplit"] = _imp48_audit
# u5 — re-derive layout_preset from helper audit (post-split count via
# select_layout_preset(out_units)). Helper guarantees post_split_unit_count
# ≤ 4 (layout cap abort), so the derived preset is always renderable by
# LAYOUT_PRESETS. Respect --override-layout when present (user's explicit
# choice wins over auto-redrive; mirrors the override gate above at L3697).
if _imp48_audit.get("applied"):
_imp48_post_preset = _imp48_audit.get("post_split_layout_preset")
if _imp48_post_preset and not layout_override_applied:
if _imp48_post_preset != layout_preset:
print(
f" [IMP-48] layout_preset re-derived: {layout_preset}"
f"{_imp48_post_preset} (post-split unit count="
f"{_imp48_audit.get('post_split_unit_count')})",
file=sys.stderr,
)
layout_preset = _imp48_post_preset
print(
f" [IMP-48] re-split applied — "
f"split={len(_imp48_audit.get('split_units', []))} "
f"skipped={len(_imp48_audit.get('skipped_units', []))} "
f"post_count={_imp48_audit.get('post_split_unit_count')} "
f"post_preset={_imp48_audit.get('post_split_layout_preset')!r}",
file=sys.stderr,
)
print(f" preset : {layout_preset} ({len(units)} units, composition v0 count-based)")
for u in units:
print(f" unit : {u.source_section_ids} merge={u.merge_type}"
@@ -4011,6 +4058,15 @@ def run_phase_z2_mvp1(
}
for u in units
],
# IMP-48 (#77) — re-split audit. Additive field. AI=0 deterministic
# one-shot post-pass on Step 6 settling result. applied=True means
# ≥1 parent_merged / parent_merged_inferred reject unit was split
# into per-section singles; selected_units already reflects the
# post-split list. Skipped reasons (incomplete_rebuild /
# no_beneficial_split / layout_cap_exceeded) keep the merged unit
# for IMP-47B (#76) AI handoff. section_assignment_override skip
# honors IMP-06 (#6) zoneSections ground truth.
"imp48_resplit": _imp48_audit,
},
step_status="done",
pipeline_path_connected=True,
@@ -4020,6 +4076,11 @@ def run_phase_z2_mvp1(
"composition v0 count-based — sections → candidates → score → greedy select. "
"Step 6-A (사용자 lock 2026-05-08): selected_units[i].v4_candidates 추가 "
"(non-reject max-6 후보 list, candidates[0] = 단일 frame_* 와 일관). "
"IMP-48 (#77, 2026-05-22): merged-reject 자동 분리 post-pass — "
"parent_merged / parent_merged_inferred + label=reject + ≥2 sections "
"→ per-section singles (each own rank-1 V4 evidence + raw_content 보존). "
"guardrails: coverage equality / beneficial split (≥1 non-reject) / "
"layout cap (≤4 units). imp48_resplit audit additive. "
"logic 무변 — runtime 결과 동일. Step 9 application_plan input."
),
)