feat(IMP-09): PR 2 — 2-D dynamic dispatch for 5 preset families

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>
This commit is contained in:
2026-05-17 18:51:23 +09:00
parent 201099e53b
commit 1fb973297f
17 changed files with 825 additions and 14 deletions

View File

@@ -953,6 +953,56 @@ def compute_zone_layout_cols(zones_data: list[dict],
} }
def _aggregate_zone_signals_per_track(
preset: dict,
zones_data: list[dict],
) -> tuple[list[dict], list[dict]]:
"""Build per-row + per-col virtual zones for 2-D dynamic dispatch.
Each virtual zone aggregates content_weight.score (max) and
min_height_px (max) across single-span zones on that track
(occupied_rows == {r} for rows, occupied_cols == {c} for cols).
Falls back to all-span zones (touching every track on the axis)
when a track has no single-span zone.
"""
rows_grid, _ = _parse_css_areas(preset["css_areas"])
R = len(rows_grid)
C = len(rows_grid[0])
occupancy: list[tuple[dict, set[int], set[int]]] = []
for z in zones_data:
pos = z["position"]
occ_rows = {r for r, row in enumerate(rows_grid) if pos in row}
occ_cols = {
c for row in rows_grid for c, tok in enumerate(row) if tok == pos
}
occupancy.append((z, occ_rows, occ_cols))
def _track_virtual(idx: int, axis: str) -> dict:
if axis == "row":
single = [z for z, rr, _cc in occupancy if rr == {idx}]
allspan = [z for z, rr, _cc in occupancy if rr == set(range(R))]
else:
single = [z for z, _rr, cc in occupancy if cc == {idx}]
allspan = [z for z, _rr, cc in occupancy if cc == set(range(C))]
candidates = single or allspan
return {
"position": f"_virtual_{axis}_{idx}",
"template_id": f"_virtual_{axis}_{idx}",
"content_weight": {
"score": max(c["content_weight"]["score"] for c in candidates)
},
"min_height_px": max(
c.get("min_height_px", DEFAULT_ZONE_MIN_HEIGHT_PX)
for c in candidates
),
}
row_virtuals = [_track_virtual(r, "row") for r in range(R)]
col_virtuals = [_track_virtual(c, "col") for c in range(C)]
return row_virtuals, col_virtuals
def _compute_per_zone_geometry( def _compute_per_zone_geometry(
layout_css: dict, layout_css: dict,
debug_zones: list[dict], debug_zones: list[dict],
@@ -1083,6 +1133,61 @@ def _build_rows_dynamic(preset: dict, zones_data: list[dict],
} }
def _build_grid_dynamic_2d(preset: dict, zones_data: list[dict],
gap: int = GRID_GAP) -> dict:
"""2-D dynamic path — dynamic row heights + dynamic column widths.
IMP-09 PR 2 (B-4) handler for the five preset families whose topology
is neither pure 'rows' nor pure 'cols':
- T (top-1-bottom-2)
- inverted-T (top-2-bottom-1)
- side-T-left (left-1-right-2)
- side-T-right (left-2-right-1)
- 2x2 (grid-2x2)
Strategy:
1) _aggregate_zone_signals_per_track builds R per-row + C per-col
virtual zones (max content_weight.score + max min_height_px of
single-span zones, falling back to all-span zones).
2) Row virtuals → compute_zone_layout → heights_px (R).
3) Col virtuals → compute_zone_layout_cols → widths_px (C).
4) Assemble layout_css dict with computation='2d_dynamic_aggregated'
and dynamic_rows=True, dynamic_cols=True.
raw_zone_layout carries both solver outputs + the virtual zone lists
so step08 trace can explain the per-track aggregation.
"""
row_virtuals, col_virtuals = _aggregate_zone_signals_per_track(
preset, zones_data
)
zl_row = compute_zone_layout(row_virtuals, gap=gap)
zl_col = compute_zone_layout_cols(col_virtuals, gap=gap)
heights_px = zl_row["heights_px"]
widths_px = zl_col["widths_px"]
rows_str = " ".join(f"{h}px" for h in heights_px)
cols_str = " ".join(f"{w}px" for w in widths_px)
return {
"areas": preset["css_areas"],
"cols": cols_str,
"rows": rows_str,
"heights_px": heights_px,
"widths_px": widths_px,
"ratios": [round(h / SLIDE_BODY_HEIGHT, 3) for h in heights_px],
"width_ratios": [round(w / SLIDE_BODY_WIDTH, 3) for w in widths_px],
"computation": "2d_dynamic_aggregated",
"dynamic_rows": True,
"dynamic_cols": True,
"raw_zone_layout": {
"row_layout": zl_row,
"col_layout": zl_col,
"row_virtuals": row_virtuals,
"col_virtuals": col_virtuals,
},
}
def _build_cols_dynamic(preset: dict, zones_data: list[dict], def _build_cols_dynamic(preset: dict, zones_data: list[dict],
gap: int = GRID_GAP) -> dict: gap: int = GRID_GAP) -> dict:
"""vertical-2 path — dynamic column widths, static fr row heights. """vertical-2 path — dynamic column widths, static fr row heights.
@@ -1113,6 +1218,105 @@ def _build_cols_dynamic(preset: dict, zones_data: list[dict],
} }
def _override_to_grid_tracks(
preset: dict,
zones_data: list[dict],
override_zone_geometries: dict[str, dict],
gap: int = GRID_GAP,
) -> dict:
"""2-D override path — derive heights_px (R) + widths_px (C) from
user-supplied zone_id -> {x, y, w, h} (0~1 within slide-body).
IMP-09 PR 2 (B-4) override handler for the five preset families
whose topology is neither pure 'rows' nor pure 'cols':
- T (top-1-bottom-2)
- inverted-T (top-2-bottom-1)
- side-T-left (left-1-right-2)
- side-T-right (left-2-right-1)
- 2x2 (grid-2x2)
Strategy:
1) Parse css_areas into R x C grid.
2) For each row r: aggregate h via max over single-row zones
(occupied_rows == {r}); fallback to all-span zones; else 0.0.
3) For each col c: same with w.
4) Normalize per-axis (divide by total) and multiply by avail_*,
absorbing rounding diff into the last element.
5) If total_h or total_w == 0 (degenerate / empty override),
fall back to _build_grid_dynamic_2d default path.
"""
rows_grid, _ = _parse_css_areas(preset["css_areas"])
R = len(rows_grid)
C = len(rows_grid[0])
occupancy: list[tuple[dict, set[int], set[int]]] = []
for z in zones_data:
pos = z["position"]
occ_rows = {r for r, row in enumerate(rows_grid) if pos in row}
occ_cols = {
c for row in rows_grid for c, tok in enumerate(row) if tok == pos
}
occupancy.append((z, occ_rows, occ_cols))
def _track_value(idx: int, axis: str) -> float:
if axis == "row":
single = [z for z, rr, _cc in occupancy if rr == {idx}]
allspan = [z for z, rr, _cc in occupancy if rr == set(range(R))]
key = "h"
else:
single = [z for z, _rr, cc in occupancy if cc == {idx}]
allspan = [z for z, _rr, cc in occupancy if cc == set(range(C))]
key = "w"
candidates = single or allspan
vals = [
float(override_zone_geometries[z["position"]][key])
for z in candidates
if z["position"] in override_zone_geometries
]
return max(vals) if vals else 0.0
row_values = [_track_value(r, "row") for r in range(R)]
col_values = [_track_value(c, "col") for c in range(C)]
total_h = sum(row_values)
total_w = sum(col_values)
if total_h == 0 or total_w == 0:
return _build_grid_dynamic_2d(preset, zones_data, gap=gap)
row_ratios = [v / total_h for v in row_values]
col_ratios = [v / total_w for v in col_values]
avail_h = SLIDE_BODY_HEIGHT - gap * (R - 1)
avail_w = SLIDE_BODY_WIDTH - gap * (C - 1)
heights_px = [int(round(r * avail_h)) for r in row_ratios]
widths_px = [int(round(r * avail_w)) for r in col_ratios]
diff_h = avail_h - sum(heights_px)
if diff_h != 0 and heights_px:
heights_px[-1] += diff_h
diff_w = avail_w - sum(widths_px)
if diff_w != 0 and widths_px:
widths_px[-1] += diff_w
rows_str = " ".join(f"{h}px" for h in heights_px)
cols_str = " ".join(f"{w}px" for w in widths_px)
return {
"areas": preset["css_areas"],
"cols": cols_str,
"rows": rows_str,
"heights_px": heights_px,
"widths_px": widths_px,
"ratios": [round(rr, 3) for rr in row_ratios],
"width_ratios": [round(rr, 3) for rr in col_ratios],
"computation": "user_override_geometry",
"dynamic_rows": True,
"dynamic_cols": True,
"raw_zone_layout": {
"override_applied": True,
"source": override_zone_geometries,
},
}
# Layout preset → zone position 순서 = LAYOUT_PRESETS[preset]["positions"] 직접 사용. # Layout preset → zone position 순서 = LAYOUT_PRESETS[preset]["positions"] 직접 사용.
# 이전 ZONE_POSITIONS_BY_PRESET (type-b 등 legacy 명) 는 dead code 로 제거 (2026-04-29). # 이전 ZONE_POSITIONS_BY_PRESET (type-b 등 legacy 명) 는 dead code 로 제거 (2026-04-29).
@@ -1202,21 +1406,29 @@ def build_layout_css(layout_preset: str, zones_data: list[dict],
"dynamic_cols": True, "dynamic_cols": True,
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries}, "raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
} }
elif topology in ("T", "inverted-T", "side-T-left", "side-T-right", "2x2"):
# IMP-09 PR 2 — 2-D override path (T / inverted-T / side-T / 2x2).
# Degenerate inputs (total_h == 0 or total_w == 0) fall back to
# _build_grid_dynamic_2d inside the helper.
return _override_to_grid_tracks(
preset, zones_data, override_zone_geometries, gap=gap
)
else: else:
# PR 1 lock — warn-and-fallthrough preserved. # warn-and-fallthrough preserved for remaining presets (single).
# PR 2 promotes this to strict ValueError via _override_to_grid_tracks. # PR 3 territory.
print( print(
f" [override-warning] zone-geometry override 는 layout '{layout_preset}' 미지원 " f" [override-warning] zone-geometry override 는 layout '{layout_preset}' 미지원 "
f"(현재 horizontal-2 / vertical-2 만). default layout_css 사용.", f"(현재 horizontal-2 / vertical-2 / 2-D presets 만). default layout_css 사용.",
file=sys.stderr, file=sys.stderr,
) )
# ── Dynamic branch — topology dispatch (PR 1: rows / cols only) ── # ── Dynamic branch — topology dispatch ──
if topology == "rows": if topology == "rows":
return _build_rows_dynamic(preset, zones_data, gap) return _build_rows_dynamic(preset, zones_data, gap)
if topology == "cols": if topology == "cols":
return _build_cols_dynamic(preset, zones_data, gap) return _build_cols_dynamic(preset, zones_data, gap)
# PR 2 will dispatch T / inverted-T / side-T-{left,right} / 2x2 here. if topology in ("T", "inverted-T", "side-T-left", "side-T-right", "2x2"):
return _build_grid_dynamic_2d(preset, zones_data, gap)
# PR 3 will dispatch single here. # PR 3 will dispatch single here.
return _build_fr_default(preset) return _build_fr_default(preset)
@@ -3383,7 +3595,14 @@ def run_phase_z2_mvp1(
dz["ratio"] = geo["zone_height_ratio"] dz["ratio"] = geo["zone_height_ratio"]
dz["width_px"] = geo["zone_width_px"] dz["width_px"] = geo["zone_width_px"]
dz["width_ratio"] = geo["zone_width_ratio"] dz["width_ratio"] = geo["zone_width_ratio"]
if layout_css["dynamic_rows"]: if layout_css["dynamic_rows"] and layout_css.get("dynamic_cols"):
print(
f" zones : 2-D heights {layout_css['heights_px']} px, "
f"widths {layout_css['widths_px']} px, "
f"ratios {layout_css['ratios']}, "
f"width_ratios {layout_css['width_ratios']}"
)
elif layout_css["dynamic_rows"]:
print(f" zones : heights {layout_css['heights_px']} px, ratios {layout_css['ratios']}") print(f" zones : heights {layout_css['heights_px']} px, ratios {layout_css['ratios']}")
elif layout_css.get("dynamic_cols"): elif layout_css.get("dynamic_cols"):
print(f" zones : widths {layout_css['widths_px']} px, width_ratios {layout_css['width_ratios']}") print(f" zones : widths {layout_css['widths_px']} px, width_ratios {layout_css['width_ratios']}")

View File

@@ -0,0 +1,43 @@
input:
layout_preset: grid-2x2
zones_data:
- position: top-left
template_id: MOCK_top-left
content_weight:
score: 0.25
min_height_px: 200
- position: top-right
template_id: MOCK_top-right
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-left
template_id: MOCK_bottom-left
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-right
template_id: MOCK_bottom-right
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"top-left top-right" "bottom-left bottom-right"'
cols: 583px 583px
rows: 286px 285px
heights_px:
- 286
- 285
widths_px:
- 583
- 583
ratios:
- 0.489
- 0.487
width_ratios:
- 0.494
- 0.494
computation: 2d_dynamic_aggregated
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,63 @@
input:
layout_preset: grid-2x2
zones_data:
- position: top-left
template_id: MOCK_top-left
content_weight:
score: 0.25
min_height_px: 200
- position: top-right
template_id: MOCK_top-right
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-left
template_id: MOCK_bottom-left
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-right
template_id: MOCK_bottom-right
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries:
top-left:
x: 0
y: 0
w: 0.55
h: 0.4
top-right:
x: 0.55
y: 0
w: 0.45
h: 0.4
bottom-left:
x: 0
y: 0.4
w: 0.55
h: 0.6
bottom-right:
x: 0.55
y: 0.4
w: 0.45
h: 0.6
expected_layout_css:
areas: '"top-left top-right" "bottom-left bottom-right"'
cols: 641px 525px
rows: 228px 343px
heights_px:
- 228
- 343
widths_px:
- 641
- 525
ratios:
- 0.4
- 0.6
width_ratios:
- 0.55
- 0.45
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,38 @@
input:
layout_preset: left-1-right-2
zones_data:
- position: left
template_id: MOCK_left
content_weight:
score: 0.5
min_height_px: 200
- position: right-top
template_id: MOCK_right-top
content_weight:
score: 0.25
min_height_px: 200
- position: right-bottom
template_id: MOCK_right-bottom
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"left right-top" "left right-bottom"'
cols: 777px 389px
rows: 286px 285px
heights_px:
- 286
- 285
widths_px:
- 777
- 389
ratios:
- 0.489
- 0.487
width_ratios:
- 0.658
- 0.33
computation: 2d_dynamic_aggregated
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,53 @@
input:
layout_preset: left-1-right-2
zones_data:
- position: left
template_id: MOCK_left
content_weight:
score: 0.5
min_height_px: 200
- position: right-top
template_id: MOCK_right-top
content_weight:
score: 0.25
min_height_px: 200
- position: right-bottom
template_id: MOCK_right-bottom
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries:
left:
x: 0
y: 0
w: 0.4
h: 1.0
right-top:
x: 0.4
y: 0
w: 0.6
h: 0.5
right-bottom:
x: 0.4
y: 0.5
w: 0.6
h: 0.5
expected_layout_css:
areas: '"left right-top" "left right-bottom"'
cols: 466px 700px
rows: 286px 285px
heights_px:
- 286
- 285
widths_px:
- 466
- 700
ratios:
- 0.5
- 0.5
width_ratios:
- 0.4
- 0.6
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,38 @@
input:
layout_preset: left-2-right-1
zones_data:
- position: left-top
template_id: MOCK_left-top
content_weight:
score: 0.25
min_height_px: 200
- position: left-bottom
template_id: MOCK_left-bottom
content_weight:
score: 0.25
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-top right" "left-bottom right"'
cols: 389px 777px
rows: 286px 285px
heights_px:
- 286
- 285
widths_px:
- 389
- 777
ratios:
- 0.489
- 0.487
width_ratios:
- 0.33
- 0.658
computation: 2d_dynamic_aggregated
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,53 @@
input:
layout_preset: left-2-right-1
zones_data:
- position: left-top
template_id: MOCK_left-top
content_weight:
score: 0.25
min_height_px: 200
- position: left-bottom
template_id: MOCK_left-bottom
content_weight:
score: 0.25
min_height_px: 200
- position: right
template_id: MOCK_right
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries:
left-top:
x: 0
y: 0
w: 0.35
h: 0.6
left-bottom:
x: 0
y: 0.6
w: 0.35
h: 0.4
right:
x: 0.35
y: 0
w: 0.65
h: 1.0
expected_layout_css:
areas: '"left-top right" "left-bottom right"'
cols: 408px 758px
rows: 343px 228px
heights_px:
- 343
- 228
widths_px:
- 408
- 758
ratios:
- 0.6
- 0.4
width_ratios:
- 0.35
- 0.65
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,38 @@
input:
layout_preset: top-1-bottom-2
zones_data:
- position: top
template_id: MOCK_top
content_weight:
score: 0.5
min_height_px: 200
- position: bottom-left
template_id: MOCK_bottom-left
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-right
template_id: MOCK_bottom-right
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries: null
expected_layout_css:
areas: '"top top" "bottom-left bottom-right"'
cols: 583px 583px
rows: 314px 257px
heights_px:
- 314
- 257
widths_px:
- 583
- 583
ratios:
- 0.537
- 0.439
width_ratios:
- 0.494
- 0.494
computation: 2d_dynamic_aggregated
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,53 @@
input:
layout_preset: top-1-bottom-2
zones_data:
- position: top
template_id: MOCK_top
content_weight:
score: 0.5
min_height_px: 200
- position: bottom-left
template_id: MOCK_bottom-left
content_weight:
score: 0.25
min_height_px: 200
- position: bottom-right
template_id: MOCK_bottom-right
content_weight:
score: 0.25
min_height_px: 200
override_zone_geometries:
top:
x: 0
y: 0
w: 1.0
h: 0.3
bottom-left:
x: 0
y: 0.3
w: 0.5
h: 0.7
bottom-right:
x: 0.5
y: 0.3
w: 0.5
h: 0.7
expected_layout_css:
areas: '"top top" "bottom-left bottom-right"'
cols: 583px 583px
rows: 171px 400px
heights_px:
- 171
- 400
widths_px:
- 583
- 583
ratios:
- 0.3
- 0.7
width_ratios:
- 0.5
- 0.5
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,38 @@
input:
layout_preset: top-2-bottom-1
zones_data:
- position: top-left
template_id: MOCK_top-left
content_weight:
score: 0.25
min_height_px: 200
- position: top-right
template_id: MOCK_top-right
content_weight:
score: 0.25
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-left top-right" "bottom bottom"'
cols: 583px 583px
rows: 257px 314px
heights_px:
- 257
- 314
widths_px:
- 583
- 583
ratios:
- 0.439
- 0.537
width_ratios:
- 0.494
- 0.494
computation: 2d_dynamic_aggregated
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,53 @@
input:
layout_preset: top-2-bottom-1
zones_data:
- position: top-left
template_id: MOCK_top-left
content_weight:
score: 0.25
min_height_px: 200
- position: top-right
template_id: MOCK_top-right
content_weight:
score: 0.25
min_height_px: 200
- position: bottom
template_id: MOCK_bottom
content_weight:
score: 0.5
min_height_px: 200
override_zone_geometries:
top-left:
x: 0
y: 0
w: 0.6
h: 0.4
top-right:
x: 0.6
y: 0
w: 0.4
h: 0.4
bottom:
x: 0
y: 0.4
w: 1.0
h: 0.6
expected_layout_css:
areas: '"top-left top-right" "bottom bottom"'
cols: 700px 466px
rows: 228px 343px
heights_px:
- 228
- 343
widths_px:
- 700
- 466
ratios:
- 0.4
- 0.6
width_ratios:
- 0.6
- 0.4
computation: user_override_geometry
dynamic_rows: true
dynamic_cols: true

View File

@@ -0,0 +1,24 @@
case_id: grid-2x2_dynamic_2d
description: |
grid-2x2 (2x2 topology) is promoted to 2-D dynamic in IMP-09 PR 2.
Row-axis retry MUST be skipped by the gate with the
"dynamic_cols (2-D topology)" reason.
input_layout_css:
areas: '"top-left top-right" "bottom-left bottom-right"'
cols: 583px 583px
rows: 286px 285px
heights_px: [286, 285]
widths_px: [583, 583]
ratios: [0.489, 0.487]
width_ratios: [0.494, 0.494]
dynamic_rows: true
dynamic_cols: true
router_decision:
router_active: true
proposed_actions_summary: [zone_ratio_retry]
expected_gate:
retry_attempted: false
retry_skipped_reason_contains:
- "dynamic_cols"
- "2-D"
- "IMP-09"

View File

@@ -0,0 +1,24 @@
case_id: left-1-right-2_dynamic_2d
description: |
left-1-right-2 (side-T-left topology) is promoted to 2-D dynamic in
IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the
"dynamic_cols (2-D topology)" reason.
input_layout_css:
areas: '"left right-top" "left right-bottom"'
cols: 777px 389px
rows: 286px 285px
heights_px: [286, 285]
widths_px: [777, 389]
ratios: [0.489, 0.487]
width_ratios: [0.658, 0.33]
dynamic_rows: true
dynamic_cols: true
router_decision:
router_active: true
proposed_actions_summary: [zone_ratio_retry]
expected_gate:
retry_attempted: false
retry_skipped_reason_contains:
- "dynamic_cols"
- "2-D"
- "IMP-09"

View File

@@ -0,0 +1,24 @@
case_id: left-2-right-1_dynamic_2d
description: |
left-2-right-1 (side-T-right topology) is promoted to 2-D dynamic in
IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the
"dynamic_cols (2-D topology)" reason.
input_layout_css:
areas: '"left-top right" "left-bottom right"'
cols: 389px 777px
rows: 286px 285px
heights_px: [286, 285]
widths_px: [389, 777]
ratios: [0.489, 0.487]
width_ratios: [0.33, 0.658]
dynamic_rows: true
dynamic_cols: true
router_decision:
router_active: true
proposed_actions_summary: [zone_ratio_retry]
expected_gate:
retry_attempted: false
retry_skipped_reason_contains:
- "dynamic_cols"
- "2-D"
- "IMP-09"

View File

@@ -0,0 +1,26 @@
case_id: top-1-bottom-2_dynamic_2d
description: |
top-1-bottom-2 (T topology) is promoted to 2-D dynamic in IMP-09
PR 2 (dynamic_rows=True, dynamic_cols=True). Row-axis retry MUST be
skipped by the IMP-09 gate with the "dynamic_cols (2-D topology)"
skip reason, because row-only redistribution cannot reconcile both
axes simultaneously.
input_layout_css:
areas: '"top top" "bottom-left bottom-right"'
cols: 583px 583px
rows: 314px 257px
heights_px: [314, 257]
widths_px: [583, 583]
ratios: [0.537, 0.439]
width_ratios: [0.494, 0.494]
dynamic_rows: true
dynamic_cols: true
router_decision:
router_active: true
proposed_actions_summary: [zone_ratio_retry]
expected_gate:
retry_attempted: false
retry_skipped_reason_contains:
- "dynamic_cols"
- "2-D"
- "IMP-09"

View File

@@ -0,0 +1,24 @@
case_id: top-2-bottom-1_dynamic_2d
description: |
top-2-bottom-1 (inverted-T topology) is promoted to 2-D dynamic in
IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the
"dynamic_cols (2-D topology)" reason.
input_layout_css:
areas: '"top-left top-right" "bottom bottom"'
cols: 583px 583px
rows: 257px 314px
heights_px: [257, 314]
widths_px: [583, 583]
ratios: [0.439, 0.537]
width_ratios: [0.494, 0.494]
dynamic_rows: true
dynamic_cols: true
router_decision:
router_active: true
proposed_actions_summary: [zone_ratio_retry]
expected_gate:
retry_attempted: false
retry_skipped_reason_contains:
- "dynamic_cols"
- "2-D"
- "IMP-09"

View File

@@ -138,21 +138,21 @@ def test_vertical_2_override_keeps_fr_cols_legacy():
assert result["width_ratios"] == [0.4, 0.6] assert result["width_ratios"] == [0.4, 0.6]
# ────────────────────── fr_default sink (PR 1) ────────────────────── # ────────────────────── 2-D dynamic dispatch (PR 2) ──────────────────────
def test_top_1_bottom_2_fr_default_populates_geometry(): def test_top_1_bottom_2_dynamic_2d_populates_geometry():
"""T-shape (top-1-bottom-2) falls through to fr_default in PR 1 """T-shape (top-1-bottom-2) is dispatched through the 2-D dynamic
but heights_px / widths_px must be populated (length-locked to builder in PR 2: heights_px / widths_px length-locked to grid
grid R=2, C=2).""" R=2, C=2 with both dynamic flags True."""
zones = [ zones = [
_zone("top", 0.5), _zone("top", 0.5),
_zone("bottom-left", 0.25), _zone("bottom-left", 0.25),
_zone("bottom-right", 0.25), _zone("bottom-right", 0.25),
] ]
result = build_layout_css("top-1-bottom-2", zones) result = build_layout_css("top-1-bottom-2", zones)
assert result["computation"] == "fr_default_from_preset" assert result["computation"] == "2d_dynamic_aggregated"
assert result["dynamic_rows"] is False assert result["dynamic_rows"] is True
assert result["dynamic_cols"] is False assert result["dynamic_cols"] is True
assert len(result["heights_px"]) == 2 # R rows assert len(result["heights_px"]) == 2 # R rows
assert len(result["widths_px"]) == 2 # C cols assert len(result["widths_px"]) == 2 # C cols