scripts/generate_frame_previews.py iterates figma_to_html_agent/blocks/{frame_id}/index.html,
renders preview.png via Selenium headless (capture_slide_screenshot pattern reuse), and writes
_preview_manifest.json (schema v1) with idempotent stale-detect (mtime+sha256). Build-time only
— no runtime pipeline integration, no AI calls, no MDX/Jinja regen. Stage 2 baseline (commit
56619a0): total=33, renderable=20, missing_index_html=13, orphan=1 (1171281192).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 3 lock implementation: extend build_layout_css dispatch beyond
the horizontal-2 / vertical-2 1-D dynamic paths. T / inverted-T /
side-T-left / side-T-right / 2x2 now flow through a 2-D track solver
instead of the fr_default sink, with length-locked heights_px (R) +
widths_px (C) on every return path (default and override).
PR 2 scope (u1~u5):
- u1: _aggregate_zone_signals_per_track — per-row + per-col virtual
zones via max(weight) + max(min_height_px) of single-span zones,
falling back to all-span when a track has none.
- u2: _build_grid_dynamic_2d default builder — feeds virtual zones
into compute_zone_layout + compute_zone_layout_cols; emits
computation="2d_dynamic_aggregated", dynamic_rows=True,
dynamic_cols=True.
- u3: _override_to_grid_tracks override builder — single-span
aggregation (max h per row, max w per col), normalize, multiply
by avail_h/avail_w, last-element diff absorb; emits
computation="user_override_geometry"; falls back to u2 when
total_h or total_w == 0.
- u4: build_layout_css dispatcher wiring — topology in
{T, inverted-T, side-T-left, side-T-right, 2x2} routes to
_build_grid_dynamic_2d (default) or _override_to_grid_tracks
(override); legacy [override-warning] stderr removed for the
5 presets; step08 trace gains a 2-D-aware print line that fires
before the dynamic_rows / dynamic_cols branches.
- u5: PR 1 lock test test_top_1_bottom_2_fr_default_populates_geometry
renamed to test_top_1_bottom_2_dynamic_2d_populates_geometry and
flipped to PR 2 reality (computation="2d_dynamic_aggregated",
dynamic_rows=True, dynamic_cols=True).
Fixtures: 10 build_layout_css (5 presets × {default, override}) +
5 retry_gate *_dynamic_2d.yaml locking the retry gate skip reason
"dynamic_cols (2-D topology) ... IMP-09 lock" for the 5 presets.
Tests: python -m pytest -q tests = 104 passed (Stage 2 baseline
10 RED → GREEN, 0 regressions). Kei archive
(build_containers_type_b / page_structure) untouched —
rg "build_containers_type_b|page_structure" src/phase_z2_pipeline.py
returns 0 hits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>