align_sections_to_v4_granularity now emits canonical sub-section ids
of the form ${section_id}-sub-${ordinal} (e.g., "04-2-sub-1"), matching
the frontend drag/drop schema. Each drilled sub-section populates
heading_number (decimal "2.1" / integer "1" / None for undecorated)
and v4_alias_keys for legacy V4 keys.
N-R5 decimal-only alias guard : v4_alias_keys is populated only when
heading_number matches re.fullmatch(r"\d+\.\d+", ...). Integer-only
H3 headings (e.g., MDX 05's "### 1", "### 2") and bare H3 headings
produce no alias to avoid sibling-parent V4 collisions (RULE 0
generalization — applies to all 32-frame MDX, not MDX 05-specific).
The drill regex is broadened from r"^###\s+(\d+\.\d+)\s+..." to
r"^###\s+(?:(\d+(?:\.\d+)?)\s+)?(.+?)$" so integer-only and bare H3
headings are now recognised as sub-sections; they previously failed
the regex and were silently kept under the parent section.
Tests : 7 new cases (MdxSection default 4-positional callers, V4 exact
passthrough, decimal drill with alias, integer-only no-alias guard,
bare H3 no-alias, no-H3 passthrough, end-to-end aligner -> resolver
round-trip with legacy V4 alias). 15/15 in test_phase_z2_subsection_schema
+ 14 override + 8 fallback baseline = 37/37 PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds sub-section schema fields (heading_number / v4_alias_keys /
sub_sections) to MdxSection with defaults so existing 4-positional
constructions remain valid. Introduces _resolve_v4_section_key helper
that resolves a V4 mdx_sections key in exact > alias > None order with
no parent/sibling promotion (axis 7 hybrid lock).
Rewires four runtime V4 lookup sites (lookup_v4_match,
lookup_v4_match_with_fallback, lookup_v4_all_judgments,
lookup_v4_candidates) to accept an optional alias_keys kwarg and go
through the resolver. U1 callers pass empty alias lists so behaviour
is byte-identical to the previous exact-match path; U2 will populate
aliases from MDX heading_number metadata.
Closure callers in run_phase_z2 build section_alias_by_id from
MdxSection.v4_alias_keys and forward into lookup_fn /
candidates_lookup_fn / lookup_v4_all_judgments (Step 7-A trace) and
into _select_template_for_overrides single-section selector.
Step 9 candidate report (post-decision diagnostic) is marked with an
inline English exemption comment per N-R6 — runtime selection goes
through _resolve_v4_section_key, the report path stays a direct
dict-shape lookup to avoid debug_zones schema plumbing.
derive_parent_id now recognises canonical ordinal ids
("03-1-sub-2" -> "03-1") first and keeps the legacy decimal fallback
("04-2.1" -> "04-2") for V4 alias compatibility.
Tests : 8 synthetic cases in tests/test_phase_z2_subsection_schema.py
covering derive_parent_id ordinal/decimal/none and the resolver
exact/alias/no-promote/miss cases. 30/30 PASS combined with the 14
override + 8 fallback baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Refs #6
Backend / CLI / composition path only — frontend bridge remains #38.
- Add `--override-section-assignment ZONE_ID=section_id[,section_id]` to the
Phase Z entry parser. Parse-time hard errors for malformed payloads, empty
zone id, empty section list, duplicate zone id, and duplicate section across
zones (a section may belong to at most one zone).
- Add `_build_position_assignment_plan` helper (pure function, resolved
`positions` injected). Builds a per-position assignment plan with the
Codex-locked template_id ladder: (1) `--override-frame` exact unit_id wins,
(2) exact existing auto unit reuse, (3) single-section direct-executable V4
selector via `lookup_v4_match_with_fallback(..., raw_content=section.raw_content)`,
(4) ad-hoc multi-section override without exact auto + without explicit
override-frame yields `skipped_reason='ad_hoc_merged_no_template'`.
- Lock the collision policy: explicit override wins per position, sections
appear in at most one position, overlapping auto units are skipped whole
(no split, no cascade, no replan), uncovered sections from the previous
same-position auto unit are recorded in `uncovered_section_ids`.
- Additive trace fields on each plan entry: `previous_source_section_ids`,
`skipped_collided_auto_units`, `uncovered_section_ids`, `v4_selector_trace`,
`section_assignment_override`. Top-level `comp_debug["section_assignment_plan"]`
+ `comp_debug["section_assignment_summary"]` so Step 9 / debug artifacts can
derive from a single source of truth.
- Wire `run_phase_z2_mvp1(override_section_assignments=...)` after final layout
preset resolution: validate ZONE_IDs against active layout positions and
validate section_ids against aligned sections (fail-fast). The plan is
attached to `comp_debug` for downstream artifacts. Actual `zones_data` /
unit-list rewiring is deferred to a follow-up commit so this change stays
regression-safe; trace artifacts already surface override intent and
collision impact.
- Add 9 helper unit tests with fully synthetic MOCK_ ids (no real catalog
/ no v4_full32_result.yaml): non-conflicting auto retention, collision
whole-skip + uncovered tracing, template ladder steps 1/2/4, unit_id
naming convention, previous_source_section_ids position history,
empty-position case, summary aggregation invariants.
No AI, no `calculate_fit`, no full planner rerun, no frontend, no sample
hardcoding, no `restructure`/`reject` silent promotion. `plan_composition()`
signature is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refs #5
Replace the hand-built Case 7 payload assertion with a temporary
production-source guard. The test now fails if Step 9 stops emitting
candidate_evidence, breaks the fallback_chain compat alias, or removes
the alias intent comment.
This is intentionally temporary because Step 9 application-plan unit
assembly is inline. Follow-up IMP-32 should extract a helper and replace
this source-string guard with a direct helper test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refs #5
- Add runtime template_id dedup in lookup_v4_match_with_fallback with
first-occurrence reservation; duplicate ranks become audit evidence,
not new fallback candidates.
- Add Step 9 candidate_evidence as the primary per-unit evidence field
while keeping fallback_chain as a compat alias for legacy readers.
- Add Step 20 fallback_selection_count and selection_paths derived from
comp_debug.v4_fallback_summary with defensive defaults; top-level
overall enum unchanged.
- Tighten synthetic fallback tests for duplicate handling (rank-1 reject A
+ rank-2 use_as_is A + rank-3 distinct B → rank-3 wins) and add tests
for candidate_evidence + alias equality and Step 20 qualifier presence
with defensive defaults.
- Verify with pytest (10 passed) and smoke_frame_render --self-check
(11/11 partials, IMP-04 F17 calibration intact).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>