# 🔍 Cross Review — Job 5e43d80f: Layout Engine 2×K 구현

- **Reviewer**: `planner-reviewer-claude-01`
- **대상**: W1~W5 (`layout.py`, `lib.sh:435`, `.mam.env.example`, 테스트 3파일)
- **검증**: BSP 시뮬레이터 실행 + 헤드리스 궤적 실행 + **pytest 전체 393건 실행**

> **이해충돌 고지**: 본 구현의 사양(`layout_engine_improvement_plan.md` Rev.2)은 제가 Planner 로 작성했습니다. 아래는 **타인이 작성한 코드가 그 사양을 충족하는가**에 대한 검증이며, 사양 자체의 타당성에 대한 독립 검증이 아닙니다. §4 의 F-1 은 실제로 **제 사양의 결함**이며 그렇게 명시합니다.

---

## 1. 핵심 목표 달성 — 실측 확인

BSP 세분할 시뮬레이터로 N=1→5 궤적을 실행했습니다.

```
N=1  [p1 277x78]
N=2  split right on p1  (new_column_right)
     p1 138x78 / p2 139x78                      widths=[138,139] heights=[78]  balanced=True
N=3  split down  on p1  (fill_column)
     p1 138x39 / p3 138x39 / p2 139x78          (전이 상태 — 정상)
N=4  split down  on p2  (fill_column)
     p1 138x39 / p3 138x39 / p2 139x39 / p4 139x39
     widths=[138,139] heights=[39]              balanced=True   ← 🎯 깨끗한 2×2
N=5  OVERFLOW (grid_capacity_reached) → 새 워크스페이스
```

**4 에이전트 2×2 목표 달성.** Rev.1 이 진단한 R-1 왜곡(N=4 에서 widths `{138,139,277}`, heights `{19,20,39}`)이 완전히 해소되었습니다.

### 1.1 GUI ↔ 헤드리스 패리티 (C-1 처방 검증)
```
headless n=1: right    target=p1  reason=new_column_right
headless n=2: down     target=p1  reason=fill_column
headless n=3: down     target=p2  reason=fill_column
headless n=4: overflow target=p4  reason=grid_capacity_reached
```
GUI 와 **방향·순서가 완전히 일치**하며, 타깃도 `p1 → p2` 로 **열을 번갈아** 갑니다(§4.2 `panes[n - max_columns]` 명세대로). 홀짝(`n % 2`) 로직은 완전히 제거되었습니다. `creator-grok-01` 이 제기했던 "n=3 갈라짐"이 원천 차단되었습니다.

---

## 2. W1~W5 항목별 검증

| W | 항목 | 상태 |
|---|---|---|
| **W1** | 단일 결정표 공유 | ✅ `_decide` / `_decide_headless` 분리, `compute_2xk_layout` 은 관측→위임 2단 |
| **W2** | N=1 → `right` | ✅ `_full_height_pane` 경유 `new_column_right` |
| **W3** | `_full_height_pane` | ✅ `len(col)!=1 → None`, `area_h<=0` 폴백, `tol=2` |
| **W4** | 캡 3중 배선 | ✅ **전부** — 아래 §2.1 |
| **W5** | 테스트 | ✅ 사양의 8개 테스트 전건 구현 |

### 2.1 W4 — Rev.2 가 지적한 이중 누락이 모두 해소됨
```python
# layout.py:235-236  ← _env_int 에 default 부여 (이전엔 None 반환)
--max-cols  default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS", default=2)
--max-rows  default=_env_int("MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS", default=2)
```
```bash
# lib.sh:435  ← 이전엔 --max-cols 를 아예 넘기지 않았음
--max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}"
```
```
# .mam.env.example:147-153  ← C-5 문서 자체 불일치("unset -> no cap" vs "=3")도 정정
#default: 2  /  MAM_MAX_PANE_COLS=2  +  MAM_MAX_PANE_ROWS=2 신규 블록
```
추가로 `compute_2xk_layout` 진입부에 `max_columns is None → 2` 방어가 들어가 **네 번째 경로**까지 막았습니다. 사양보다 견고합니다.

`test_max_cols_default_reaches_cli_path` 와 `test_lib_sh_passes_max_cols_and_rows` 가 이 배선을 계약으로 고정합니다 — Rev.2 가 "이게 없으면 전체가 프로덕션에서 무효"라고 경고한 지점이라 특히 중요합니다.

### 2.2 기존 테스트 갱신 처리
| 테스트 | 처리 |
|---|---|
| `test_1_pane_split_down` → `test_1_pane_split_right` | ✅ 개명 + 계약 갱신 |
| `test_2_panes_to_3_panes_new_column_right` → `test_2_panes_fill_left_column_down` | ✅ |
| `test_4_panes_to_5_panes_new_column` → `..._overflows_at_capacity` | ✅ |
| `test_headless_max_columns_growth_guard` | ✅ 옛 홀짝 단언(`n=2→right`, `headless_odd_down`) 전면 재작성 |
| `test_b19_headless_layout_does_not_overflow` | ✅ tall 케이스 재해석 + **wide-tall 케이스 신설로 커버리지 보존** |

`test_b19` 처리가 특히 좋습니다 — 단언만 뒤집지 않고 비오버플로 경로를 검증하는 새 픽스처를 추가해 커버리지를 유지했습니다.

### 2.3 테스트 실행
```
pytest tests/ -q  →  393 passed in 511.60s
```
**실패 0건.** (직전 리뷰에서 관측된 `test_d23` nats 태그 불일치도 해소되었습니다.)

---

## 3. 🟡 F-2 — `MAX_*=0` 이 "무제한"이 아니라 "전면 차단"입니다

`_env_int` 는 J-1 계약에 따라 명시적 `0` 을 보존합니다. 그 결과:

| 설정 | 실측 결과 |
|---|---|
| `max_columns=0, max_rows=2` | `down / fill_column` |
| `max_columns=2, max_rows=0` | `right` → 이후 `overflow` |
| `max_columns=0, max_rows=0` | **`overflow / grid_capacity_reached` (즉시)** |
| 헤드리스, 둘 중 하나라도 0 | `n >= 0` 이 항상 참 → **영구 overflow** |

문제는 **같은 설정 파일 안의 의미 충돌**입니다:
```
# .mam.env.example
# MAM_MIN_PANE_ROWS=0     ← "Set to 0 to disable vertical row constraints"
# MAM_MAX_PANE_ROWS=2     ← 0 을 넣으면 "용량 0" = 모든 세션이 새 워크스페이스
```
`MIN_*=0` 이 "제약 해제"를 뜻하므로, 운영자가 `MAX_*=0` 을 "상한 없음"으로 읽는 것은 자연스럽습니다. 그러나 실제로는 **에이전트마다 워크스페이스가 무한 생성**됩니다.

**개선 방향**:
```python
if max_columns is None or max_columns <= 0:
    max_columns = 2          # 또는 '무제한' 의도라면 sys.maxsize
if max_rows is None or max_rows <= 0:
    max_rows = 2
```
`.mam.env.example` 에도 `0 은 허용되지 않습니다(최솟값 1)` 한 줄을 덧붙이십시오. 비차단이나 오설정 시 피해가 크고 되돌리기 어렵습니다(생성된 워크스페이스가 남음).

---

## 4. 🟠 F-1 — 세로 전용 적층 능력이 사라졌습니다 (**제 사양의 결함**)

### 현상 (실측)
`min_cols=60` 에서 단일 페인 폭별 결정:

| 폭 × 높이 | 결과 |
|---|---|
| 80×60 | `overflow / column_width_overflow` |
| 100×60 | `overflow` |
| 119×60 | `overflow` |
| 120×60 | `right` |

**폭 120 미만이면 N=1 에서 즉시 오버플로**합니다. 그러나 80×60 을 세로로 쌓으면 `80×30` 페인 2개가 되고, **두 페인 모두 폭 80 ≥ min_cols 60 을 만족**합니다. 즉 **사용 가능한 배치를 거부하고 새 워크스페이스를 만듭니다.**

### 원인 — 폭 게이트가 폴스루하지 않음
```python
if len(cols) < max_columns:
    fh = _full_height_pane(cols[-1], area_h)
    if fh is not None:
        if fh.width > 0 and fh.width // 2 < min_cols:
            return _overflow("column_width_overflow", fh)   # ← 즉시 반환
        return _right("new_column_right", fh)
# ② 세로 채우기에 도달하지 못함
```
새 엔진은 **1열 × K행 배치를 구조적으로 만들 수 없습니다.** 구 엔진의 `single_pane_split_down` 이 담당하던 경로가 사라졌습니다.

### 책임 소재
**이것은 구현 결함이 아니라 제 사양의 결함입니다.** Rev.2 §4.1 의사코드가 정확히 `return OVERFLOW("column_width_overflow", fh)` 로 적혀 있었고, 구현은 그대로 따랐습니다. 폭 부족 시 세로 폴백을 명시하지 않은 것은 제 누락입니다.

### 실무 영향
기본값 `min_cols=15` 에서는 폭 30 미만이어야 발동하므로 **사실상 도달 불가**합니다. 다만 `MAM_MIN_PANE_COLS` 기본값은 60 → 40 → 15 로 변해 왔고, `.mam.env` 는 **gitignore 대상이라 자동 마이그레이션되지 않습니다.** 구 설정(`=60`)을 지닌 기존 설치는 120칸 미만 터미널에서 **에이전트마다 워크스페이스가 하나씩** 생기게 됩니다.

### 개선 방향 (구체)
폭 게이트를 **폴스루**로 바꿉니다:
```python
if len(cols) < max_columns:
    fh = _full_height_pane(cols[-1], area_h)
    if fh is not None and not (fh.width > 0 and fh.width // 2 < min_cols):
        return _right("new_column_right", fh)
    # 폭이 새 열을 감당하지 못하면 ②(세로 채우기)로 내려간다
```
이렇게 하면 80×60/min_cols=60 은 `down / fill_column` → `80×30` 2개가 되고, 폭·행이 모두 소진된 뒤에야 ③에서 오버플로합니다. 회귀 가드:
```python
def test_narrow_terminal_falls_back_to_vertical_stacking():
    d = compute_2xk_layout(_one(80, 60), min_cols=60)
    assert d.direction == "down" and not d.is_overflow
```
**주의**: 이 변경은 `test_b19` 의 tall 케이스 단언을 다시 뒤집습니다(현재 `overflow` → `down`). 원래 그 테스트가 지키던 계약이 바로 이 세로 폴백이었으므로, 사실상 **원복**입니다.

---

## 5. 판정

| 항목 | 판정 |
|---|---|
| W1~W5 사양 충족 | ✅ 전건. W4 는 사양보다 견고 |
| 4 에이전트 2×2 | ✅ 시뮬레이터 실측 |
| GUI/헤드리스 패리티 | ✅ 방향·타깃 모두 일치 |
| 전체 테스트 | ✅ **393 passed, 0 failed** |
| F-1 세로 폴백 상실 | 🟠 **제 사양 누락** — 후속 수정 |
| F-2 `MAX_*=0` 함정 | 🟡 후속 수정 |

구현은 승인된 사양을 **정확히, 그리고 일부는 더 견고하게** 충족했으며 핵심 목표가 실측으로 증명되었습니다. F-1·F-2 는 이번 변경이 만든 새 결함이 아니라 **사양의 미비**로, 각각 3~5줄 수정으로 해소됩니다. 설계 재작업 사유가 아니므로 `[ESCALATE: PLANNER]` 는 부여하지 않습니다.

**후속 잡 권고**: F-1(세로 폴백) + F-2(0 값 클램프) 를 묶어 한 커밋으로. F-1 은 구 `.mam.env` 를 지닌 기존 설치에 실제 영향이 있으므로 우선순위가 높습니다.

[VERDICT: PASS]
