fix(IMP-08): Stage 5 R2 — aligner force-drill on sub-id override targets

Codex #1 (Stage 5) reproduced a smoke regression on the actual checkout :
when V4 carries the parent exact key (e.g., `04-2`) AND the drag/drop
override targets a sub-id (`primary=04-2-sub-1`), the aligner kept the
parent at parent granularity and emit `['04-1', '04-2']`, so the override
flag failed with `unknown section_id(s) ['04-2-sub-1']`.

Fix : `align_sections_to_v4_granularity` gains an optional
`override_target_section_ids` keyword. From each canonical
`${parent}-sub-N` target it derives the parent id and adds it to a
`force_drill_parents` set. Sections in that set are drilled into
sub-sections regardless of whether V4 carries the parent exact key.
Top-level override targets (no derived parent) do not trigger
force-drill, so backward-compat is preserved for parent-granularity
overrides.

The call site in `run_phase_z2_mvp1` collects sub-ids from
`override_section_assignments` and forwards them to the aligner.

Generalization (RULE 0) :
- Trigger is the override schema (`X-sub-N`), not a specific MDX / section /
  frame id. Applies to all 32-frame MDX uniformly.
- Decision is deterministic on the override target shape, independent of
  V4 yaml content.
- Default (no override) path is unchanged byte-for-byte.

Side fixes (forward-only RULE 1 cleanup, no history rewrite) :
- `align_sections_to_v4_granularity` docstring rewritten in English
  (overwrites the Korean docstring committed in 5191aca).
- Step 9 diagnostic comment quoted-string rewritten in English
  (overwrites `"V4 entry 없음"` committed in a422d72).

Tests : 3 new cases in `test_phase_z2_subsection_schema.py` —
`test_align_parent_v4_exact_keeps_section_when_no_override_targets_sub`
(backward-compat axis), `test_align_force_drills_when_override_targets_sub_id_with_parent_in_v4`
(blocker regression), `test_align_top_level_override_target_does_not_force_drill_other_sections`
(force-drill scope guard). Pytest scope-qualified result :
`test_phase_z2_subsection_schema.py` + `_section_assignment_override.py` +
`_v4_fallback.py` = 40 / 40 PASS.

Smoke (axis = sub-id override -> aligner -> assignment plan, both V4 yaml
shapes) :
- HEAD V4 yaml (`04-1`, `04-2.1`, `04-2.2` only) :
  `--override-section-assignment primary=04-2-sub-1` ->
  `aligned_section_ids=['04-1', '04-2-sub-1', '04-2-sub-2']`,
  `plan[0].assignment_source='cli_override'`,
  `plan[0].source_section_ids=['04-2-sub-1']`.
- V4 yaml with `04-2` exact key (Codex's stress case) : identical
  aligned output and identical assignment plan.

Downstream `composition_planner` abort
(`phase_z_status_not_allowed:extract_matched_zone`) is IMP-05 territory,
unchanged in both shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 02:28:46 +09:00
parent ab2764c8d0
commit 8f6cffc2a7
2 changed files with 132 additions and 26 deletions

View File

@@ -111,7 +111,9 @@ def test_mdx_section_default_construction_preserves_4_positional_callers():
def test_align_passthrough_when_v4_key_exact_match():
# Section already aligned to V4 key — aligner keeps it untouched.
# Section already aligned to V4 key (no override target): aligner
# keeps it untouched. Parent-level V4 evidence flows via exact-match
# lookup.
sections = [_section("04-1", 1, "1. Top", "body")]
v4 = {"mdx_sections": {"04-1": {"judgments_full32": []}}}
out = align_sections_to_v4_granularity(sections, v4)
@@ -119,6 +121,63 @@ def test_align_passthrough_when_v4_key_exact_match():
assert out[0].section_id == "04-1"
def test_align_parent_v4_exact_keeps_section_when_no_override_targets_sub():
# Backward-compat axis: when V4 carries the parent exact key and no
# drag/drop override targets a sub-id of this section, the aligner
# MUST keep the parent (preserves V4 evidence at parent granularity).
raw = "### 2.1 First\nbody1\n### 2.2 Second\nbody2\n"
sections = [_section("03-2", 2, "2. Parent", raw)]
v4 = {"mdx_sections": {"03-2": {"judgments_full32": []}}}
out = align_sections_to_v4_granularity(sections, v4)
assert [s.section_id for s in out] == ["03-2"]
def test_align_force_drills_when_override_targets_sub_id_with_parent_in_v4():
# Stage 5 R2 blocker-fix regression: when V4 has the parent exact key
# AND an override targets a sub-id of that section, the aligner MUST
# drill regardless of V4 parent presence. This makes drag/drop
# addressing deterministic across all V4 yaml shapes.
raw = "### 2.1 First\nbody1\n### 2.2 Second\nbody2\n"
sections = [_section("04-2", 2, "2. Parent", raw)]
v4 = {
"mdx_sections": {
"04-2": {"judgments_full32": []}, # parent V4 entry present
"04-2.1": {"judgments_full32": []}, # plus decimal sub entries
"04-2.2": {"judgments_full32": []},
}
}
out = align_sections_to_v4_granularity(
sections, v4, override_target_section_ids=["04-2-sub-1"]
)
# Force-drill: parent id MUST be replaced by canonical sub-ids.
assert [s.section_id for s in out] == ["04-2-sub-1", "04-2-sub-2"]
# Decimal aliases preserved (N-R5: decimal heading_number).
assert out[0].v4_alias_keys == ["04-2.1"]
assert out[1].v4_alias_keys == ["04-2.2"]
def test_align_top_level_override_target_does_not_force_drill_other_sections():
# Top-level override target ("primary=03-1") has no derive_parent_id,
# so it MUST NOT force-drill any section. Only "X-sub-N" targets
# trigger force-drill on parent X.
raw = "### 2.1 First\nbody1\n"
sections = [
_section("03-1", 1, "1. Top", "body"),
_section("03-2", 2, "2. Parent", raw),
]
v4 = {
"mdx_sections": {
"03-1": {"judgments_full32": []},
"03-2": {"judgments_full32": []},
}
}
out = align_sections_to_v4_granularity(
sections, v4, override_target_section_ids=["03-1"]
)
# No sub-id target -> both sections kept at parent granularity.
assert [s.section_id for s in out] == ["03-1", "03-2"]
def test_align_drill_emits_canonical_ordinal_id_with_decimal_alias():
# Decimal H3 headings -> canonical ordinal id + decimal alias (legacy V4 key).
raw = "### 2.1 First\nbody1\n### 2.2 Second\nbody2\n"