feat(IMP-09): PR 1 — col-axis solver + per-zone geometry mapper + retry gate

Stage 3 round 4 lock implementation: extend build_layout_css beyond
the horizontal-2-only dynamic path. Every layout_css return now
carries length-locked col-axis keys (widths_px, width_ratios,
dynamic_cols) matching the parsed css_areas grid (R rows, C cols),
so 2-D layouts (T / 2x2 in PR 2) and the unified
_compute_per_zone_geometry mapper can plug in without further
contract churn.

PR 1 scope:
  - _parse_css_areas + _parse_fr_string + _compute_per_zone_geometry
    (unified — 1-D and 2-D from the same code path)
  - compute_zone_layout_cols (vertical-2 weight-only solver)
  - _build_fr_default / _build_rows_dynamic / _build_cols_dynamic
    (populate widths_px/heights_px on every return path)
  - build_layout_css override branch keeps the warn-and-fallthrough
    legacy for unsupported presets (PR 2 promotes to strict raise)
  - retry gate in _attempt_zone_ratio_retry skips when dynamic_cols=True
    or dynamic_rows=False, with explicit retry_skipped_reason
  - Step 8 artifact gains zone_widths_px_planned /
    zone_col_ratios_planned (top-level) + zone_width_px_planned /
    zone_col_ratio_planned (per-zone)
  - debug_zones width injection via _compute_per_zone_geometry
    (replaces the legacy row-only zip)

Tests: tests/phase_z2/ — 47 new cases (parse / fr-string / cols solver /
per-zone geometry / build_layout_css contract / retry gate +
6 build_layout_css YAML fixtures + 3 retry_gate fixtures).

Verification: python -m pytest -q tests = 89 passed (was 42).
horizontal-2 grid CSS strings (areas/cols/rows) byte-identical to
legacy; only additive col-axis keys are introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 12:03:23 +09:00
parent 8f6cffc2a7
commit 201099e53b
18 changed files with 1318 additions and 35 deletions

View File

@@ -0,0 +1,31 @@
input:
layout_preset: horizontal-2
zones_data:
- position: top
template_id: MOCK_top
content_weight:
score: 0.5
min_height_px: 200
- position: bottom
template_id: MOCK_bottom
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"top" "bottom"'
cols: 1fr
rows: 286px 285px
heights_px:
- 286
- 285
widths_px:
- 1180
ratios:
- 0.489
- 0.487
width_ratios:
- 1.0
computation: min_height_first + content_weight_distribution
dynamic_rows: true
dynamic_cols: false

View File

@@ -0,0 +1,41 @@
input:
layout_preset: horizontal-2
zones_data:
- position: top
template_id: MOCK_top
content_weight:
score: 0.5
min_height_px: 200
- position: bottom
template_id: MOCK_bottom
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries:
top:
x: 0
y: 0
w: 1.0
h: 0.3
bottom:
x: 0
y: 0.3
w: 1.0
h: 0.7
expected_layout_css:
areas: '"top" "bottom"'
cols: 1fr
rows: 176px 410px
heights_px:
- 176
- 410
widths_px:
- 1180
ratios:
- 0.3
- 0.7
width_ratios:
- 1.0
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: false

View File

@@ -0,0 +1,31 @@
input:
layout_preset: horizontal-2
zones_data:
- position: top
template_id: MOCK_top
content_weight:
score: 0.8
min_height_px: 200
- position: bottom
template_id: MOCK_bottom
content_weight:
score: 0.2
min_height_px: 150
override_zone_geometries: null
expected_layout_css:
areas: '"top" "bottom"'
cols: 1fr
rows: 377px 194px
heights_px:
- 377
- 194
widths_px:
- 1180
ratios:
- 0.644
- 0.332
width_ratios:
- 1.0
computation: min_height_first + content_weight_distribution
dynamic_rows: true
dynamic_cols: false

View File

@@ -0,0 +1,31 @@
input:
layout_preset: vertical-2
zones_data:
- position: left
template_id: MOCK_left
content_weight:
score: 0.5
min_height_px: 200
- position: right
template_id: MOCK_right
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"left right"'
cols: 583px 583px
rows: 1fr
heights_px:
- 585
widths_px:
- 583
- 583
ratios:
- 1.0
width_ratios:
- 0.494
- 0.494
computation: content_weight_distribution_cols
dynamic_rows: false
dynamic_cols: true

View File

@@ -0,0 +1,41 @@
input:
layout_preset: vertical-2
zones_data:
- position: left
template_id: MOCK_left
content_weight:
score: 0.5
min_height_px: 200
- position: right
template_id: MOCK_right
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries:
left:
x: 0
y: 0
w: 0.4
h: 1.0
right:
x: 0.4
y: 0
w: 0.6
h: 1.0
expected_layout_css:
areas: '"left right"'
cols: 40.0fr 60.0fr
rows: 1fr
heights_px:
- 585
widths_px:
- 466
- 700
ratios:
- 1.0
width_ratios:
- 0.4
- 0.6
computation: user_override_geometry
dynamic_rows: false
dynamic_cols: true

View File

@@ -0,0 +1,31 @@
input:
layout_preset: vertical-2
zones_data:
- position: left
template_id: MOCK_left
content_weight:
score: 0.7
min_height_px: 200
- position: right
template_id: MOCK_right
content_weight:
score: 0.3
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"left right"'
cols: 816px 350px
rows: 1fr
heights_px:
- 585
widths_px:
- 816
- 350
ratios:
- 1.0
width_ratios:
- 0.692
- 0.297
computation: content_weight_distribution_cols
dynamic_rows: false
dynamic_cols: true