This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
[Claude #2] Stage 3: 수정 계획 — round 2
|
||||
|
||||
Codex #1 round 1 의 4 blocking gap + rollback 정정 검증 + lock. 모든 line 번호 main HEAD 실측 (PR 미시작). 코드 미수정.
|
||||
|
||||
## 0. Codex #1 정정 검증
|
||||
|
||||
| Codex 정정 | 검증 결과 | 처분 |
|
||||
|---|---|---|
|
||||
| Gap 1 — Step 8 per-zone width/height 주입이 span zone 에서 깨짐 (`:3068-3070` zip 이 per-zone 과 per-row/col 혼동) | ✅ 확인. `debug_zones` = per-zone (top-1-bottom-2 → 3 entry: top/bottom-left/bottom-right). 2-D `heights_px` = per-row (2 entry). zip 직접 적용 시 top 이 row[0] 만, bottom-left 가 row[1], bottom-right 가 *idx-out-of-range or dropped*. 현재 코드의 zip 이 1-D 에서만 의도된 path. | 수용 — area-aware mapper 신설 (§2-A) |
|
||||
| Gap 2 — PR 2 override 가 T / 2x2 의 X,Y,W,H → grid track 변환 룰 미정의 | ✅ 확인. round 1 의 `_build_layout_css_from_override` 는 horizontal-2 (h) / vertical-2 (w) legacy 그대로. T / 2x2 의 span zone 이 grid row/col 양쪽에 영향 — 룰 부재. | 수용 — override 룰 5 조항 명시 (§2-B) |
|
||||
| Gap 3 — PR 3 V4 32-frame test 가 `select_layout_preset(unit_count)` 으로 signature 오기. assertion `dynamic_rows or dynamic_cols or single` 의 `single` field 부재 | ✅ 확인. `src/phase_z2_composition.py:709` signature = `select_layout_preset(units: list[CompositionUnit])`. 또한 `single` 분기는 `dynamic_rows=False, dynamic_cols=False` (Stage 2 §1 #3 lock) — `or single` 은 *field* 가 아니라 *preset 이름* 의도였으나 assertion 으로 부정확. | 수용 — assertion 재작성 (§2-C). 단 invariant 자체도 surface 변경 (8-preset coverage assertion 으로 확장) |
|
||||
| Gap 4 — `_parse_css_areas` / `compute_zone_layout_2d` 가 malformed 입력에 div-by-zero / silent 빈 출력 | ✅ 확인. round 1 snippet 의 `C = len(rows[0]) if R > 0 else 0` → `avail_w = total_width - gap * (C - 1)` 이 C=0 일 때 `total_width + gap` 으로 garbage. zone position 이 css_areas 에 없을 때도 silent 0 weight. | 수용 — strict validation (§2-D) |
|
||||
| Rollback 정정 a — pixel cols `"583px 583px"` 가 invalid CSS spec 이라 한 표현은 잘못. grid-template-columns 가 px 값 허용. | ✅ 확인. CSS Grid spec — `grid-template-columns: <length>` 허용. round 1 의 rollback trigger 표현 오류. | 수용 — rollback trigger 재작성 (§2-E) |
|
||||
| Rollback 정정 b — per-zone planned geometry 와 CSS grid span mismatch 가 PR 2 의 주 failure surface, rollback trigger 로 추가 필요 | ✅ 동의. Gap 1 의 area-aware mapper 가 misfire 시 fixture 가 catch 해야 함. | 수용 — rollback trigger 추가 (§2-E) |
|
||||
|
||||
6 정정 모두 수용. round 1 plan 의 미정의 surface 가 본 round 에서 lock.
|
||||
|
||||
## 1. 정정의 root cause
|
||||
|
||||
round 1 plan 의 근본 결함 = **2-D layout 에서 "zone" 과 "grid track" 의 분리 모델 부재**. 1-D 에서는 zone = row (또는 col) 라서 zip 으로 충분. 2-D 에서는 :
|
||||
- grid track (row / col) = 물리 분할 단위 (catalog `css_areas` 의 토큰 위치)
|
||||
- zone = 의미 단위 (debug_zones 의 unique position)
|
||||
- spanning zone = 1 zone 이 N track 차지
|
||||
|
||||
3 개념 분리 필요. round 2 가 area-aware mapper 로 분리 lock.
|
||||
|
||||
## 2. 정제된 contract (round 1 → round 2 변경)
|
||||
|
||||
### 2-A. Area-aware zone geometry mapper (Gap 1)
|
||||
|
||||
신규 helper `_compute_per_zone_geometry` :
|
||||
|
||||
```python
|
||||
def _compute_per_zone_geometry(
|
||||
layout_css: dict,
|
||||
debug_zones: list[dict],
|
||||
gap: int = GRID_GAP,
|
||||
) -> list[dict]:
|
||||
"""Map grid-track heights/widths to per-zone aggregated dimensions.
|
||||
|
||||
Handles spanning zones in 2-D layouts (T, inverted-T, side-T, 2x2):
|
||||
zone_height = sum(track_heights for rows occupied) + gap * (span_rows - 1)
|
||||
zone_width = sum(track_widths for cols occupied) + gap * (span_cols - 1)
|
||||
|
||||
For 1-D layouts (horizontal-2, vertical-2):
|
||||
degenerates to simple positional indexing (no spanning).
|
||||
|
||||
Returns list[{position, zone_height_px, zone_width_px,
|
||||
zone_height_ratio, zone_width_ratio}].
|
||||
"""
|
||||
css_areas = layout_css.get("areas")
|
||||
heights_px = layout_css.get("heights_px") or []
|
||||
widths_px = layout_css.get("widths_px") or []
|
||||
raw_zl = layout_css.get("raw_zone_layout") or {}
|
||||
rows_grid = raw_zl.get("rows_grid") # set by compute_zone_layout_2d only
|
||||
|
||||
if rows_grid is None:
|
||||
# 1-D path : positional zip (legacy horizontal-2 / new vertical-2)
|
||||
per_zone = []
|
||||
for i, dz in enumerate(debug_zones):
|
||||
per_zone.append({
|
||||
"position": dz["position"],
|
||||
"zone_height_px": heights_px[i] if i < len(heights_px) else None,
|
||||
"zone_width_px": widths_px[i] if i < len(widths_px) else None,
|
||||
"zone_height_ratio": (
|
||||
layout_css.get("ratios", [])[i]
|
||||
if i < len(layout_css.get("ratios", [])) else None
|
||||
),
|
||||
"zone_width_ratio": (
|
||||
layout_css.get("width_ratios", [])[i]
|
||||
if i < len(layout_css.get("width_ratios", [])) else None
|
||||
),
|
||||
})
|
||||
return per_zone
|
||||
|
||||
# 2-D path : aggregate via parsed grid
|
||||
per_zone = []
|
||||
for dz in debug_zones:
|
||||
pos = dz["position"]
|
||||
occupied_rows = sorted({r for r, row in enumerate(rows_grid)
|
||||
if pos in row})
|
||||
occupied_cols = sorted({c for r, row in enumerate(rows_grid)
|
||||
for c, tok in enumerate(row) if tok == pos})
|
||||
if not occupied_rows or not occupied_cols:
|
||||
# zone declared but not in css_areas — strict error (catalog corruption)
|
||||
raise ValueError(
|
||||
f"zone position '{pos}' not found in parsed css_areas {rows_grid}"
|
||||
)
|
||||
zh = sum(heights_px[r] for r in occupied_rows) \
|
||||
+ gap * (len(occupied_rows) - 1)
|
||||
zw = sum(widths_px[c] for c in occupied_cols) \
|
||||
+ gap * (len(occupied_cols) - 1)
|
||||
per_zone.append({
|
||||
"position": pos,
|
||||
"zone_height_px": zh,
|
||||
"zone_width_px": zw,
|
||||
"zone_height_ratio": round(zh / SLIDE_BODY_HEIGHT, 3),
|
||||
"zone_width_ratio": round(zw / SLIDE_BODY_WIDTH, 3),
|
||||
})
|
||||
return per_zone
|
||||
```
|
||||
|
||||
호출처 변경 :
|
||||
- `:3067-3073` (debug_zones 갱신) — zip 직접 사용 폐기. 대신 :
|
||||
```python
|
||||
per_zone_geo = _compute_per_zone_geometry(layout_css, debug_zones, GRID_GAP)
|
||||
for dz, geo in zip(debug_zones, per_zone_geo):
|
||||
dz["height_px"] = geo["zone_height_px"]
|
||||
dz["ratio"] = geo["zone_height_ratio"]
|
||||
dz["width_px"] = geo["zone_width_px"]
|
||||
dz["width_ratio"] = geo["zone_width_ratio"]
|
||||
```
|
||||
- `:3228-3251` (Step 8 per-zone) — `dz.get("height_px")` / `dz.get("width_px")` 이 mapper 결과 (이미 위에서 주입) 그대로.
|
||||
|
||||
이 패턴은 1-D / 2-D 양쪽 모두 동일 API. spanning 처리는 mapper 안에 격리.
|
||||
|
||||
검증 fixture (PR 2 추가) :
|
||||
- `top-1-bottom-2` : `top.zone_width_px = widths_px[0] + widths_px[1] + GRID_GAP`, `bottom-left.zone_width_px = widths_px[0]`.
|
||||
- `grid-2x2` : `top-left.zone_width_px = widths_px[0]`, `top-left.zone_height_px = heights_px[0]` (span 없음).
|
||||
|
||||
### 2-B. Override branch 의 T / 2x2 룰 (Gap 2)
|
||||
|
||||
신규 `_build_layout_css_from_override` 의 일반 룰 (5 조항) :
|
||||
|
||||
1. **Consistency** : 모든 zone position 이 `override_zone_geometries` 에 있어야 함. partial override (일부 zone 만) → strict error.
|
||||
2. **Spanning consistency** : zone 이 N track 을 span 할 때, 그 zone 의 `w` (또는 `h`) 는 *총 span* 비율이어야 함. 예: top-1-bottom-2 의 `top` = `w=1.0` (cols 0+1 전체) 필수. `w=0.5` 와 같이 부분 값이면 strict error.
|
||||
3. **Track contribution** : 각 grid track 의 ratio = 그 track 에 포함된 non-span zone 의 ratio (있을 때만). 모든 zone 이 그 track 에서 span 이면 → spanning zone 의 ratio 를 N 등분하여 적용 (Codex Gap 2 의 "aggregated zone rectangles" 옵션).
|
||||
4. **Track sum normalization** : 각 axis 의 track ratio 합 = 1.0 (오차 0.01 허용, 마지막 track 흡수).
|
||||
5. **Missing geometry** : zone 이 override 에 없으면 strict error. partial fallback (override + dynamic mix) 미지원 (별 axis).
|
||||
|
||||
구현 :
|
||||
```python
|
||||
def _override_to_grid_tracks(
|
||||
preset: dict, override_zone_geometries: dict,
|
||||
css_areas: str, axis: str, # "row" or "col"
|
||||
) -> list[float]:
|
||||
"""Convert override X,Y,W,H per zone -> grid track ratios.
|
||||
|
||||
axis="row" -> returns row ratios derived from H values.
|
||||
axis="col" -> returns col ratios derived from W values.
|
||||
|
||||
Spanning rule: a zone spanning N tracks on the axis MUST have
|
||||
its H (or W) value equal the total span ratio. The function
|
||||
distributes that span H/W equally across the spanned tracks if
|
||||
no non-span zone fixes them; otherwise non-span zones win.
|
||||
"""
|
||||
rows_grid, unique_zones = _parse_css_areas(css_areas)
|
||||
R, C = len(rows_grid), len(rows_grid[0]) if rows_grid else 0
|
||||
|
||||
# 5-#1 / #5 — consistency / missing
|
||||
for pos in unique_zones:
|
||||
if pos not in override_zone_geometries:
|
||||
raise ValueError(
|
||||
f"override missing zone '{pos}' (partial override not supported)"
|
||||
)
|
||||
|
||||
if axis == "row":
|
||||
track_count = R
|
||||
track_values: list[Optional[float]] = [None] * R
|
||||
for pos, geom in override_zone_geometries.items():
|
||||
occupied = sorted({r for r, row in enumerate(rows_grid) if pos in row})
|
||||
span = len(occupied)
|
||||
if span == 0:
|
||||
continue
|
||||
if span == 1:
|
||||
track_values[occupied[0]] = geom["h"] # non-span wins
|
||||
else:
|
||||
# 5-#3 — spanning zone, distribute equally if no non-span fixed
|
||||
per_track = geom["h"] / span
|
||||
for r in occupied:
|
||||
if track_values[r] is None:
|
||||
track_values[r] = per_track
|
||||
else: # axis == "col"
|
||||
track_count = C
|
||||
track_values = [None] * C
|
||||
for pos, geom in override_zone_geometries.items():
|
||||
occupied = sorted({c for r, row in enumerate(rows_grid)
|
||||
for c, tok in enumerate(row) if tok == pos})
|
||||
span = len(occupied)
|
||||
if span == 0:
|
||||
continue
|
||||
if span == 1:
|
||||
track_values[occupied[0]] = geom["w"]
|
||||
else:
|
||||
per_track = geom["w"] / span
|
||||
for c in occupied:
|
||||
if track_values[c] is None:
|
||||
track_values[c] = per_track
|
||||
|
||||
if any(v is None for v in track_values):
|
||||
raise ValueError(
|
||||
f"override produced unresolved {axis} tracks: {track_values} "
|
||||
f"(catalog/override mismatch)"
|
||||
)
|
||||
|
||||
total = sum(track_values)
|
||||
if not 0.99 <= total <= 1.01:
|
||||
raise ValueError(
|
||||
f"override {axis} ratios sum to {total} (expected ~1.0)"
|
||||
)
|
||||
# 5-#4 — normalize last track to absorb rounding
|
||||
track_values[-1] += 1.0 - total
|
||||
return track_values
|
||||
```
|
||||
|
||||
`build_layout_css` override branch (`:856-902`) 재구조화 :
|
||||
```python
|
||||
if override_zone_geometries:
|
||||
row_ratios = _override_to_grid_tracks(
|
||||
preset, override_zone_geometries, preset["css_areas"], "row"
|
||||
)
|
||||
col_ratios = _override_to_grid_tracks(
|
||||
preset, override_zone_geometries, preset["css_areas"], "col"
|
||||
)
|
||||
heights_px = [int(round(r * (SLIDE_BODY_HEIGHT - GRID_GAP * (len(row_ratios)-1))))
|
||||
for r in row_ratios]
|
||||
widths_px = [int(round(c * (SLIDE_BODY_WIDTH - GRID_GAP * (len(col_ratios)-1))))
|
||||
for c in col_ratios]
|
||||
return {
|
||||
"areas": preset["css_areas"],
|
||||
"cols": " ".join(f"{w}px" for w in widths_px),
|
||||
"rows": " ".join(f"{h}px" for h in heights_px),
|
||||
"heights_px": heights_px,
|
||||
"widths_px": widths_px,
|
||||
"ratios": row_ratios,
|
||||
"width_ratios": col_ratios,
|
||||
"computation": "override_zone_geometries",
|
||||
"dynamic_rows": True,
|
||||
"dynamic_cols": True,
|
||||
"raw_zone_layout": None,
|
||||
}
|
||||
```
|
||||
|
||||
horizontal-2 / vertical-2 의 *legacy override path* (h-only / w-only) 는 **삭제**. 새 일반 룰이 horizontal-2 (`R=2, C=1`) / vertical-2 (`R=1, C=2`) 도 자연스럽게 처리. 단 기존 fixture (Stage 2 §2-A retry_gate horizontal2 fixture) 의 input override 형식 (`h=0.6, h=0.4`) 은 보존되어야 함 — 호출 시 w 값이 없으면 catalog `css_cols=1fr` 에서 col=1 이므로 each zone w=1.0 implied. 기존 input dict 호환성 :
|
||||
|
||||
```python
|
||||
# Default missing axis value : full extent (1.0) — preserves horizontal-2 legacy behavior
|
||||
for pos, geom in override_zone_geometries.items():
|
||||
geom.setdefault("h", 1.0)
|
||||
geom.setdefault("w", 1.0)
|
||||
geom.setdefault("x", 0.0)
|
||||
geom.setdefault("y", 0.0)
|
||||
```
|
||||
|
||||
이 default = legacy h-only override 가 vertical-2 의 R=1 일 때 R 의 row_ratio 합 = 1.0 (zone 1 개) 으로 자연 일관.
|
||||
|
||||
### 2-C. PR 3 V4 invariant test 재작성 (Gap 3)
|
||||
|
||||
round 1 의 `select_layout_preset(unit_count)` 잘못된 signature 폐기. 또한 *V4 32-frame surface* 는 `select_layout_preset` 의 4 preset (single / horizontal-2 / top-1-bottom-2 / grid-2x2) 만 커버 — 8 preset 전수 dynamic 확인은 *별 axis* (direct build_layout_css 호출).
|
||||
|
||||
분리된 2 test :
|
||||
|
||||
```python
|
||||
# tests/phase_z2/test_all_8_presets_dynamic.py
|
||||
def test_all_8_presets_produce_dynamic_geometry():
|
||||
"""Every layout preset in catalog must produce dynamic geometry,
|
||||
not fr_default_from_preset sink. PR 3 closing invariant."""
|
||||
expected_computation_substring = {
|
||||
"single": "single_zone_full_body",
|
||||
"horizontal-2": "content_weight_distribution",
|
||||
"vertical-2": "content_weight_distribution_cols",
|
||||
"top-1-bottom-2": "content_weight_distribution_2d",
|
||||
"top-2-bottom-1": "content_weight_distribution_2d",
|
||||
"left-1-right-2": "content_weight_distribution_2d",
|
||||
"left-2-right-1": "content_weight_distribution_2d",
|
||||
"grid-2x2": "content_weight_distribution_2d",
|
||||
}
|
||||
for preset_id, expected in expected_computation_substring.items():
|
||||
zones_data = _synthesize_zones_for_preset(preset_id)
|
||||
result = build_layout_css(preset_id, zones_data)
|
||||
assert expected in result["computation"], (
|
||||
f"preset={preset_id} computation={result['computation']!r}, "
|
||||
f"expected substring {expected!r}"
|
||||
)
|
||||
assert result["computation"] != "fr_default_from_preset", (
|
||||
f"preset={preset_id} fell through to fr_default sink"
|
||||
)
|
||||
|
||||
# tests/phase_z2/test_v4_full32_layout_coverage.py
|
||||
def test_v4_full32_default_preset_paths():
|
||||
"""Passive cross-check: for the 4 presets reachable via
|
||||
select_layout_preset(units), V4 32-frame catalog never triggers
|
||||
fr_default sink. select_layout_preset accepts list[CompositionUnit]."""
|
||||
from src.phase_z2_composition import CompositionUnit, select_layout_preset
|
||||
# Build dummy units for 1..4 unit counts (preset coverage of v0 selector)
|
||||
for unit_count in range(1, 5):
|
||||
units = [_dummy_composition_unit() for _ in range(unit_count)]
|
||||
preset_id = select_layout_preset(units)
|
||||
assert preset_id is not None
|
||||
zones_data = _synthesize_zones_for_preset(preset_id)
|
||||
result = build_layout_css(preset_id, zones_data)
|
||||
assert result["computation"] != "fr_default_from_preset", (
|
||||
f"unit_count={unit_count} preset={preset_id} fell through"
|
||||
)
|
||||
```
|
||||
|
||||
`_synthesize_zones_for_preset` = helper. catalog 의 `positions` 를 읽고 각 zone 에 `content_weight={score: 1.0}` 부여. zero-weight 가드 (§2-A round 1) 와 별 axis (동등 분배 확인 따로).
|
||||
|
||||
### 2-D. Strict validation (Gap 4)
|
||||
|
||||
`_parse_css_areas` 보강 :
|
||||
```python
|
||||
def _parse_css_areas(css_areas: str) -> tuple[list[list[str]], list[str]]:
|
||||
"""Parse CSS grid-template-areas string into (row x col) cell grid.
|
||||
|
||||
Raises ValueError on:
|
||||
- empty input
|
||||
- non-rectangular (rows with different column counts)
|
||||
- no quoted row strings found
|
||||
"""
|
||||
rows = []
|
||||
seen = []
|
||||
quoted = re.findall(r'"([^"]+)"', css_areas)
|
||||
if not quoted:
|
||||
raise ValueError(f"css_areas has no quoted row strings: {css_areas!r}")
|
||||
for row_str in quoted:
|
||||
tokens = row_str.split()
|
||||
if not tokens:
|
||||
raise ValueError(f"empty row in css_areas: {css_areas!r}")
|
||||
rows.append(tokens)
|
||||
for t in tokens:
|
||||
if t not in seen:
|
||||
seen.append(t)
|
||||
col_counts = {len(r) for r in rows}
|
||||
if len(col_counts) > 1:
|
||||
raise ValueError(
|
||||
f"non-rectangular css_areas: row column counts = {col_counts}"
|
||||
)
|
||||
return rows, seen
|
||||
```
|
||||
|
||||
`compute_zone_layout_2d` 보강 :
|
||||
```python
|
||||
def compute_zone_layout_2d(zones_data, css_areas, ...):
|
||||
...
|
||||
rows, _seen = _parse_css_areas(css_areas)
|
||||
R, C = len(rows), len(rows[0])
|
||||
if R == 0 or C == 0:
|
||||
raise ValueError(
|
||||
f"compute_zone_layout_2d: degenerate grid R={R} C={C}"
|
||||
)
|
||||
# Validate every zone in zones_data has a position in css_areas
|
||||
zone_positions = {z["position"] for z in zones_data}
|
||||
grid_positions = set(_seen)
|
||||
extra = zone_positions - grid_positions
|
||||
if extra:
|
||||
raise ValueError(
|
||||
f"zones_data has positions {extra} not in css_areas {grid_positions}"
|
||||
)
|
||||
missing = grid_positions - zone_positions
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"css_areas has positions {missing} not in zones_data"
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
신규 negative test `tests/phase_z2/test_parse_css_areas_validation.py` :
|
||||
```python
|
||||
def test_parse_css_areas_empty(): ... # raises ValueError
|
||||
def test_parse_css_areas_no_quotes(): ... # raises
|
||||
def test_parse_css_areas_non_rectangular(): ... # raises
|
||||
def test_parse_css_areas_single_row(): ... # ([["top"]], ["top"])
|
||||
def test_parse_css_areas_t_shape(): ... # rectangular T (span tokens)
|
||||
|
||||
def test_compute_zone_layout_2d_missing_zone(): ... # raises
|
||||
def test_compute_zone_layout_2d_extra_zone(): ... # raises
|
||||
def test_compute_zone_layout_2d_zero_weight_total(): ... # equal split
|
||||
```
|
||||
|
||||
8 negative + edge case test. PR 2 land.
|
||||
|
||||
### 2-E. Rollback trigger 재작성
|
||||
|
||||
round 1 §6 정정 :
|
||||
|
||||
| PR | round 1 trigger | round 2 정정 |
|
||||
|---|---|---|
|
||||
| PR 1 | "vertical-2 가 `cols` 에 `\"NNpx NNpx\"` 출력 → grid-template-columns invalid" | **수정**: "vertical-2 final.html render 후 visual_check 단계 (`run_visual_check.py`) 가 grid layout broken (zone 박스 위치 mismatch) 감지" |
|
||||
| PR 2 | (없음) | **추가**: "per-zone fixture 의 `zone_width_px` / `zone_height_px` 가 parsed css_areas span 과 mathematical inconsistent (= `_compute_per_zone_geometry` 가 spanning zone 을 잘못 집계)" |
|
||||
| PR 2 | (없음) | **추가**: "override 호출 시 `_override_to_grid_tracks` 가 strict error 를 던지지 않고 silent garbage 반환 — partial override / span 불일치 case 모두 explicit raise 필요" |
|
||||
| 공통 | "기존 42 unit test 중 하나라도 fail" | 유지 |
|
||||
|
||||
## 3. PR scope 정합화 (round 1 → round 2)
|
||||
|
||||
§2-A `_compute_per_zone_geometry` 와 §2-B `_override_to_grid_tracks` 의 **PR 배치 결정** :
|
||||
|
||||
| 구성요소 | PR 1 | PR 2 | PR 3 | 이유 |
|
||||
|---|---|---|---|---|
|
||||
| `_compute_per_zone_geometry` (1-D path) | ✅ | | | vertical-2 의 per-zone width 주입 시 필요 (`debug_zones` 갱신) |
|
||||
| `_compute_per_zone_geometry` (2-D path, rows_grid 분기) | | ✅ | | `rows_grid` 는 `compute_zone_layout_2d` 출력에만 — 함수 자체는 PR 1 신설, 2-D 분기 활성화는 PR 2 |
|
||||
| `_parse_css_areas` + strict validation | | ✅ | | 2-D solver 전제. PR 1 의 vertical-2 는 css_areas parsing 불필요 |
|
||||
| `_override_to_grid_tracks` (일반 룰) | | ✅ | | T / 2x2 의 override 룰 = PR 2 land. PR 1 은 legacy h-only override (horizontal-2/vertical-2) preserve |
|
||||
| `_override_to_grid_tracks` (h/w default 1.0) | ✅ | | | PR 1 의 vertical-2 override 도 새 룰 적용 — 동시 land |
|
||||
|
||||
PR 1 의 override branch 처리 = vertical-2 만 (h-only horizontal-2 legacy preserve, w-only vertical-2 신설). 일반 룰 (`_override_to_grid_tracks`) 은 PR 2 에서 도입 + 4 preset (h-2/v-2/T/2x2) 전수 migrate. 즉 PR 1 의 vertical-2 override 는 round 1 의 임시 분기 유지 (PR 2 가 통합).
|
||||
|
||||
수정 PR 시퀀스 (Stage 2 §2-C 호환 + Gap 1/2 반영) :
|
||||
|
||||
| PR | 작업 (round 2 lock) |
|
||||
|---|---|
|
||||
| **PR 1** | (a) `compute_zone_layout_cols` 신설 (§3-A round 1) <br>(b) `_compute_per_zone_geometry` 신설 — 1-D path 만 활성화 (2-D rows_grid 분기는 PR 2 가 enable) <br>(c) `build_layout_css` override branch — vertical-2 legacy 분기 추가 (h-only horizontal-2 + w-only vertical-2 둘 다 preserve) <br>(d) layout_css 신규 key (`widths_px` / `width_ratios` / `dynamic_cols`) — 모든 return path 일관 <br>(e) Step 8 col-axis 4 신규 field (per-zone width via `_compute_per_zone_geometry` 1-D path) <br>(f) **Retry gate** (`_attempt_zone_ratio_retry:1322` 직후) <br>(g) fixtures : horizontal-2 ×3 + vertical-2 ×3 + retry_gate ×3 <br>(h) unit tests : `test_compute_zone_layout_cols.py`, `test_build_layout_css.py` (h-2/v-2), `test_retry_gate.py`, `test_compute_per_zone_geometry.py` (1-D path) |
|
||||
| **PR 2** | (a) `_parse_css_areas` + strict validation (§2-D) <br>(b) `compute_zone_layout_2d` (Q-r2-2 row sum, Q-r2-3 2x2 row/col sum) <br>(c) `_compute_per_zone_geometry` 2-D path enable (spanning zone aggregation) <br>(d) `_override_to_grid_tracks` 일반 룰 (5 조항 §2-B) — h-2/v-2/T/2x2 4 preset 전수 migrate (legacy 분기 폐기) <br>(e) `build_layout_css` dynamic branch — topology dispatcher (preset string fallback) <br>(f) fixtures : top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2 ×3 = 15 YAML <br>(g) unit tests : `test_compute_zone_layout_2d.py`, `test_build_layout_css.py` 8-preset extension, `test_parse_css_areas_validation.py` (8 negative case), `test_override_to_grid_tracks.py` (5 룰 case) |
|
||||
| **PR 3** | (a) `single` planned geometry 명시 (`heights_px=[SLIDE_BODY_HEIGHT]`, `widths_px=[SLIDE_BODY_WIDTH]`) <br>(b) Step 8 nested status (`zone_geometry_status="done"`, `region_geometry_status="partial"`) <br>(c) note 갱신 (`:3265-3273`) <br>(d) fixtures : single ×3 <br>(e) **PR 3 invariant tests** (§2-C) : `test_all_8_presets_dynamic.py` + `test_v4_full32_layout_coverage.py` |
|
||||
|
||||
## 4. 잔여 axis (본 round 처리 X)
|
||||
|
||||
| 항목 | 상태 | 근거 |
|
||||
|---|---|---|
|
||||
| `select_layout_preset` hardcoded → catalog-driven | 별 issue | Codex #3 Stage 1 정정 — IMP-09 out-of-scope |
|
||||
| `frame_contracts.yaml` 의 `min_width_px` 도입 | 별 issue | Stage 2 §3-C — col-axis weight-only invariant lock |
|
||||
| IMP-19 `_group_blocks_by_area` 직접 통합 | 별 issue | Stage 2 §3-B — reference only |
|
||||
| Step 8 `step_status="complete"` 승급 | 별 issue | Stage 2 §2-B / Q-r2-4 — region-level partial 별 axis |
|
||||
| Render template gap row/col 분리 | 미적용 | Stage 2 §4 #4 — shorthand 유지 lock |
|
||||
|
||||
## 5. Round 2 결론
|
||||
|
||||
- Codex #1 의 4 blocking gap (per-zone width injection, override 룰, V4 test signature, malformed validation) + 2 rollback 정정 모두 검증 + 수용.
|
||||
- 신규 helper 2 (`_compute_per_zone_geometry`, `_override_to_grid_tracks`) + strict validation + negative test 8 개 추가.
|
||||
- PR scope 재정합 — `_compute_per_zone_geometry` 1-D path PR 1, 2-D enable PR 2. `_override_to_grid_tracks` 일반 룰 PR 2.
|
||||
- 잔여 open 결정 0. Stage 2 의 7 결정 분기 + round 1 Q-r2-1~5 + round 2 Gap 1~4 모두 코드 변경 단위 lock.
|
||||
- 본 round 변경 = 0 (plan-only, code 미수정).
|
||||
|
||||
Codex 가 §2-A area-aware mapper / §2-B override 5 룰 / §2-C 재작성 test / §2-D strict validation / §2-E rollback trigger / §3 PR 재정합 6 axis 모두 ack 하면 Stage 3 종료 → Stage 4 implementation handoff.
|
||||
|
||||
FINAL_CONSENSUS: NO
|
||||
Reference in New Issue
Block a user