# ๐Ÿงญ Layout Engine ๊ฐœ์„  ๊ณ„ํš (Rev.2) โ€” ๊ฒฐ์ •๋ก ์  2ร—K ๊ทธ๋ฆฌ๋“œ - **Planner**: `planner-reviewer-claude-01` - **Job**: `8722045f` (Rev.1 = `29924fd4`) - **๊ฐœ์ • ์‚ฌ์œ **: `creator-grok-01` ์ด์˜์ œ๊ธฐ(`b907f997`) ์ˆ˜์šฉ - **๊ฒ€์ฆ**: BSP ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ + ๊ฒฉ๋ฆฌ ์›Œํฌ์ŠคํŽ˜์ด์Šค ์‹ค์ธก(`w16`/`w17`, ์ •๋ฆฌ ์™„๋ฃŒ) + ์ฝ”๋“œ ์‹ค์ธก --- ## 0. Rev.1 ๋Œ€๋น„ ๋ณ€๊ฒฝ ์š”์•ฝ | # | ํ•ญ๋ชฉ | Rev.1 | **Rev.2** | |---|---|---|---| | **C-1** | ํ—ค๋“œ๋ฆฌ์Šค ๊ฒฐ์ • ๊ทœ์น™ | "ํ™€์ง ๋ฐ˜์ „" (ยง6.1) | **ํ™€์ง ํ๊ธฐ.** GUI ์™€ **๋™์ผ ๊ฒฐ์ •ํ‘œ** ๊ณต์œ  (ยง4.2) | | **C-2** | `max_columns` ๋ฐฐ์„  | ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ธฐ๋ณธ๊ฐ’ 2 | **`_env_int(default=2)` + `lib.sh --max-cols` ๋™์‹œ ์ ์šฉ** (ยง4.5) | | **C-3** | GUI ์ฑ„์šฐ๊ธฐ ๊ทœ์น™ | singleton ํœด๋ฆฌ์Šคํ‹ฑ | **์—ด ํŽ˜์ธ ์ˆ˜ ๊ธฐ๋ฐ˜**(`max_rows` ์ผ๋ฐ˜ํ™”) (ยง4.1) | | **C-4** | ๊ถค์  ํ‘œ | `(3,2)โ†’(3,3)` (3ํ–‰) | `max_rows=2` ์™€ **๋ชจ์ˆœ ํ•ด์†Œ** โ€” 4ํŽ˜์ธ์—์„œ ์ •์ง€ (ยง4.1) | | **C-5** | `.mam.env.example` | ๋ฏธ์–ธ๊ธ‰ | `MAM_MAX_PANE_COLS` ๋ฌธ์„œ ๊ธฐ๋ณธ๊ฐ’ ๊ฐฑ์‹  ๋Œ€์ƒ์— ์ถ”๊ฐ€ (ยง4.5) | **์ด์˜์ œ๊ธฐ 3๊ฑด ๋ชจ๋‘ ์ธ์šฉ(SUSTAINED)ํ•ฉ๋‹ˆ๋‹ค.** C-3ยทC-4ยทC-5 ๋Š” ์ด์˜์ œ๊ธฐ๊ฐ€ ๋“œ๋Ÿฌ๋‚ธ ์ œ ๊ณ„ํš์„œ์˜ ์ถ”๊ฐ€ ๊ฒฐํ•จ์œผ๋กœ, ์ž์ง„ ์ •์ •ํ•ฉ๋‹ˆ๋‹ค. ยง1~ยง3(๊ทผ๋ณธ ์›์ธ ๋ถ„์„ยท์‹ค์ธก ์ฆ๊ฑฐ)์€ Rev.1 ๊ทธ๋Œ€๋กœ ์œ ํšจํ•˜๋ฏ€๋กœ ยง11 ์— ์š”์•ฝ๋งŒ ๋‚จ๊น๋‹ˆ๋‹ค. --- ## 1. C-1 ์ธ์šฉ โ€” ํ™€์ง ๋ฐ˜์ „์€ n=3 ์—์„œ GUI ์™€ ๊ฐˆ๋ผ์ง„๋‹ค ### ์ด์˜์ œ๊ธฐ ๊ฒ€์ฆ ํ˜„ํ–‰ ํ—ค๋“œ๋ฆฌ์Šค(`layout.py:113-125`)๋Š” `n % 2` ๋กœ ๊ธฐํ•˜๋ฅผ ํ‰๋‚ด ๋ƒ…๋‹ˆ๋‹ค. Rev.1 ยง6.1 ์ด "๋ฐ˜์ „"์ด๋ผ๊ณ ๋งŒ ์ ์—ˆ์œผ๋ฏ€๋กœ, ๊ตฌํ˜„์ž๊ฐ€ ๋ฌธ์ž ๊ทธ๋Œ€๋กœ ๋’ค์ง‘์œผ๋ฉด: | n | ํ™€์ง ๋ฐ˜์ „ ๊ฒฐ๊ณผ | GUI(Rev.2 ยง4.1) | ํŒ์ • | |---|---|---|---| | 1 | `right` | `right` | โœ… | | 2 | `down` | `down` | โœ… | | 3 | **`right`** | **`down`** | ๐Ÿ”ด **๊ฐˆ๋ผ์ง โ€” 3์—ด์„ ์—ผ** | | 4 | `down` | `overflow` | ๐Ÿ”ด | 0ร—0 ์—์„œ๋Š” ์—ด ๊ทธ๋ฃนํ•‘์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ์ž˜๋ชป๋œ `right` ๊ฐ€ **GUI ๋ณด๋‹ค ๋จผ์ €, ๋” ์กฐ์šฉํžˆ** 3์—ด์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด์˜์ œ๊ธฐ๊ฐ€ ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค. ### ๊ทผ๋ณธ ์›์ธ **์—ด ์ฑ„์šฐ๊ธฐ๋Š” `n % 2` ๋กœ ํ‘œํ˜„๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ํŒจ๋ฆฌํ‹ฐ๋Š” "์ง์ „์— ๋ฌด์—‡์„ ํ–ˆ๋Š”๊ฐ€"๋ฅผ ์ธ์ฝ”๋”ฉํ•  ๋ฟ, "๊ฐ ์—ด์ด ์–ผ๋งˆ๋‚˜ ์ฐผ๋Š”๊ฐ€"๋ฅผ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. Rev.1 ์€ ์ด๋ฅผ "๋ฐ˜์ „"์ด๋ผ๋Š” ํ•œ ๋‹จ์–ด๋กœ ๋„˜๊ฒจ ๊ตฌํ˜„์ž์—๊ฒŒ ์ž˜๋ชป๋œ ์ž์œ ๋„๋ฅผ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค. ### ๊ธฐ์กด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ณ ์ • ์ค‘์ธ ์˜› ๊ณ„์•ฝ `tests/test_layout.py:380-414` `test_headless_max_columns_growth_guard` ๋Š” ํ˜„ํ–‰ ํ™€์ง์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค: ```python d2 = compute_2xk_layout(headless(2), max_columns=2) # right d3 = compute_2xk_layout(headless(3), max_columns=2) # down d5 = compute_2xk_layout(headless(5), max_columns=2) # down, despite cap assert compute_2xk_layout(headless(4)).direction == "right" # ๋ฌด์ œํ•œ์ผ ๋•Œ ``` `n=2 โ†’ right` ์™€ `n=4 โ†’ right` ๋‹จ์–ธ์€ Rev.2 ๊ณ„์•ฝ๊ณผ **์ •๋ฉด ์ถฉ๋Œ**ํ•˜๋ฏ€๋กœ W2 ์ปค๋ฐ‹์—์„œ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(ยง7.2). --- ## 2. ํ•ต์‹ฌ ์„ค๊ณ„ โ€” ๋‹จ์ผ ๊ฒฐ์ •ํ‘œ (Single Decision Table) **GUI ๋ถ„๊ธฐ์™€ ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ๋Š” ๊ฐ™์€ ๊ฒฐ์ •ํ‘œ๋ฅผ ์“ด๋‹ค.** ์ฐจ์ด๋Š” *์ƒํƒœ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€์ธกํ•˜๋Š”๊ฐ€*๋ฟ์ด๋ฉฐ, *๋ฌด์—‡์„ ๊ฒฐ์ •ํ•˜๋Š”๊ฐ€*๋Š” ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ``` ์ƒํƒœ: cols = ์—ด๋ณ„ ํŽ˜์ธ ์ˆ˜ ๋ฆฌ์ŠคํŠธ, capacity = max_columns ร— max_rows ๊ด€์ธก: GUI โ†’ x ์ขŒํ‘œ ๊ทธ๋ฃนํ•‘์œผ๋กœ cols ์‚ฐ์ถœ ํ—ค๋“œ๋ฆฌ์Šค โ†’ ์ƒ์„ฑ ์ˆœ์„œ๋กœ cols ์ถ”๋ก  (ยง4.2) ๊ฒฐ์ •ํ‘œ (๊ณตํ†ต): โ‘  len(cols) < max_columns ๊ทธ๋ฆฌ๊ณ  ์ „๊ณ  ํŽ˜์ธ ์กด์žฌ โ†’ RIGHT (์ƒˆ ์—ด) โ‘ก ๊ฐ€์žฅ ์ ์€ ์—ด์˜ ํŽ˜์ธ ์ˆ˜ < max_rows โ†’ DOWN (๊ทธ ์—ด ์ฑ„์šฐ๊ธฐ) โ‘ข ๊ทธ ์™ธ โ†’ OVERFLOW ``` ์ด ํ‘œ๊ฐ€ ์œ ์ผํ•œ ์ง„์‹ค ์›์ฒœ์ด๋ฉฐ, ๋‘ ๋ถ„๊ธฐ๋Š” ์ด๋ฅผ **ํ˜ธ์ถœ๋งŒ** ํ•ฉ๋‹ˆ๋‹ค. --- ## 3. ๊ถค์  (C-4 ์ •์ •) `max_columns=2`, `max_rows=2` (capacity 4) ๊ธฐ์ค€. n = **ํ˜„์žฌ ํŽ˜์ธ ์ˆ˜**, ๊ฒฐ์ •์€ *๋‹ค์Œ* ํŽ˜์ธ์šฉ์ž…๋‹ˆ๋‹ค. | n | ์ƒํƒœ `(a,b)` | ๊ทœ์น™ | ๊ฒฐ์ • | ๊ฒฐ๊ณผ | |---|---|---|---|---| | 1 | `(1,-)` | โ‘  | **`right`** on p1 | `(1,1)` | | 2 | `(1,1)` | โ‘ก | **`down`** on ์—ด1 ์ตœํ•˜๋‹จ | `(2,1)` | | 3 | `(2,1)` | โ‘ก | **`down`** on ์—ด2 ์ตœํ•˜๋‹จ | `(2,2)` โ† **2ร—2 ์™„์„ฑ** | | 4 | `(2,2)` | โ‘ข | **`overflow`** | ์ƒˆ ์›Œํฌ์ŠคํŽ˜์ด์Šค | > **Rev.1 ์ •์ •**: Rev.1 ยง4.1 ์€ ๊ถค์ ์„ `(3,2) โ†’ (3,3)` ๊นŒ์ง€ ์ ์—ˆ์œผ๋‚˜, ๊ฐ™์€ ๋ฌธ์„œ ยง4.4 ๊ฐ€ `max_rows=2` ๋ฅผ ์ œ์•ˆํ•˜์—ฌ **์ž๊ธฐ๋ชจ์ˆœ**์ด์—ˆ์Šต๋‹ˆ๋‹ค. Rev.2 ๋Š” `max_rows=2` ๊ธฐ์ค€์œผ๋กœ 4ํŽ˜์ธ์—์„œ ์ •์ง€ํ•ฉ๋‹ˆ๋‹ค. `max_rows=3` ์„ ์—ด๋ฉด ๊ถค์ ์ด `(3,2) โ†’ (3,3)` ์œผ๋กœ ์ž์—ฐํžˆ ์—ฐ์žฅ๋˜๋ฉฐ, ๊ทธ๋•Œ๋Š” ยง5 ํ–‰ ๊ท ๋“ฑํ™”๊ฐ€ ์„ ํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. --- ## 4. ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ช…์„ธ ### 4.1 GUI ๋ถ„๊ธฐ (C-3 โ€” singleton ํœด๋ฆฌ์Šคํ‹ฑ ํ๊ธฐ) ํ˜„ํ–‰ `fill_singleton_column`(`layout.py:147-159`)์€ `len(col) == 1` ๋งŒ ๋ด…๋‹ˆ๋‹ค. ์ด๋Š” **`max_rows=2` ์—์„œ๋งŒ ์šฐ์—ฐํžˆ ๋งž๊ณ **, `max_rows=3` ์—์„œ๋Š” `(2,2)` ์ƒํƒœ์— singleton ์ด ์—†์–ด ์ƒˆ ์—ด์„ ์‹œ๋„ํ•˜๋‹ค ์บก์— ๊ฑธ๋ ค **์กฐ๊ธฐ overflow** ํ•ฉ๋‹ˆ๋‹ค. ์—ด ํŽ˜์ธ ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๋ฐ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค. ```python def _decide(cols, area_h, max_columns, max_rows, min_cols, min_rows): # โ‘  ์ƒˆ ์—ด: ์ „๊ณ  ํŽ˜์ธ์ด ์žˆ์„ ๋•Œ๋งŒ (R-2) 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) # ์ „๊ณ  ํŽ˜์ธ์ด ์—†์œผ๋ฉด ์ƒˆ ์—ด์„ ์—ด ์ˆ˜ ์—†๋‹ค โ†’ โ‘ก๋กœ ํด๋ฐฑ # โ‘ก ๊ฐ€์žฅ ์ ์€ ์—ด์„ ์ฑ„์šด๋‹ค (๋™๋ฅ ์ด๋ฉด ์ขŒ์ธก ์šฐ์„  โ€” ๊ฒฐ์ •๋ก ) shortest = min(cols, key=lambda c: (len(c), c[0].x)) if len(shortest) < max_rows: bottom = shortest[-1] # y ์ •๋ ฌ ํ›„ ์ตœํ•˜๋‹จ if min_rows > 0 and bottom.height > 0 and bottom.height // 2 < min_rows: return OVERFLOW("row_height_overflow", bottom) return DOWN("fill_column", bottom) # โ‘ข return OVERFLOW("grid_capacity_reached", cols[-1][0]) ``` `_full_height_pane` (R-2 ์ฒ˜๋ฐฉ): ```python def _full_height_pane(col, area_h, tol=2): """์—ด ์ „์ฒด ๋†’์ด๋ฅผ ์ ์œ ํ•˜๋Š” ๋‹จ์ผ ํŽ˜์ธ. ์—†์œผ๋ฉด None.""" if len(col) != 1: return None return col[0] if (area_h <= 0 or abs(col[0].height - area_h) <= tol) else None ``` **๋™๋ฅ  ์‹œ ์ขŒ์ธก ์šฐ์„ **(`(len(c), c[0].x)`)์€ ๊ฒฐ์ •๋ก  ๋ณด์žฅ์„ ์œ„ํ•œ ํ•„์ˆ˜ ํƒ€์ด๋ธŒ๋ ˆ์ด์ปค์ž…๋‹ˆ๋‹ค. `min()` ์€ ์ฒซ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ `cols` ์ •๋ ฌ์ด ๋ฐ”๋€Œ๋ฉด ๊ฒฐ๊ณผ๊ฐ€ ํ”๋“ค๋ฆฌ๋ฏ€๋กœ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. ### 4.2 ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ (C-1 ์ฒ˜๋ฐฉ โ€” ์ƒ์„ฑ ์ˆœ์„œ๋กœ ์—ด ์ถ”๋ก ) 0ร—0 ์—์„œ๋„ **์ƒ์„ฑ ์ˆœ์„œ๊ฐ€ ์—ด์„ ๊ฒฐ์ •**ํ•ฉ๋‹ˆ๋‹ค. `right` ํ›„ `p1`=์—ด1, `p2`=์—ด2 ์ด๊ณ , ์ดํ›„ `down` ์ฑ„์šฐ๊ธฐ๋Š” ์—ด์„ ๋ฒˆ๊ฐˆ์•„ ๊ฐ‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ธ๋ฑ์Šค `i`(0-based)์˜ ํŽ˜์ธ์€ ์—ด `i % max_columns` ์— ์†ํ•ฉ๋‹ˆ๋‹ค. ```python def _decide_headless(panes, max_columns, max_rows): n = len(panes) if n >= max_columns * max_rows: return OVERFLOW("grid_capacity_reached", panes[-1]) if n < max_columns: return RIGHT("new_column_right", panes[n - 1]) return DOWN("fill_column", panes[n - max_columns]) ``` **ํƒ€๊นƒ ์„ ํƒ ๊ทผ๊ฑฐ**: `panes[n - max_columns]` ๋Š” ๋‹ค์Œ์— ์ฑ„์šธ ์—ด์˜ **์ตœํ•˜๋‹จ ํŽ˜์ธ**์ž…๋‹ˆ๋‹ค. | n | `n - max_columns` | ํƒ€๊นƒ | ๋“ค์–ด๊ฐ€๋Š” ์—ด | |---|---|---|---| | 2 | 0 | `p1` | ์—ด1 โ†’ `(2,1)` โœ… | | 3 | 1 | `p2` | ์—ด2 โ†’ `(2,2)` โœ… | | 4 | 2 | `p3` | ์—ด1 (max_rows=3 ์ผ ๋•Œ) โœ… | | 5 | 3 | `p4` | ์—ด2 โœ… | **์ฃผ์˜**: ํ—ค๋“œ๋ฆฌ์Šค์—์„œ `default_anchor_id`(lib.sh ์˜ `--sample-pane`)๋ฅผ ํƒ€๊นƒ์œผ๋กœ ์“ฐ๋ฉด **์•ˆ ๋ฉ๋‹ˆ๋‹ค.** lib.sh ๋Š” ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ *์ฒซ* ํŽ˜์ธ์„ ๋„˜๊ธฐ๋ฏ€๋กœ, ๊ทธ๊ฒƒ์„ ๊ณ„์† ํƒ€๊นƒํ•˜๋ฉด ํ•œ ์—ด๋งŒ ๊นŠ์–ด์ง‘๋‹ˆ๋‹ค. ํ˜„ํ–‰ ์ฝ”๋“œ์˜ `anchor = default_anchor_id or panes[-1]` ๋Š” ์ด ๊ฒฝ๋กœ์—์„œ **์ œ๊ฑฐ**ํ•ด์•ผ ํ•˜๋ฉฐ, `default_anchor_id` ๋Š” ํŽ˜์ธ์ด 0๊ฐœ์ผ ๋•Œ์˜ ํด๋ฐฑ์œผ๋กœ๋งŒ ๋‚จ๊น๋‹ˆ๋‹ค. ### 4.3 ๊ฒฐ์ •ํ‘œ ๊ณต์œ  ๊ฐ•์ œ (๊ตฌ์กฐ์  ๋ณด์ฆ) ๋‘ ๋ถ„๊ธฐ๊ฐ€ ๊ฐˆ๋ผ์ง€์ง€ ์•Š๋„๋ก `compute_2xk_layout` ์€ **๊ด€์ธก โ†’ ๊ณตํ†ต ๊ฒฐ์ •** 2๋‹จ์œผ๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค: ```python def compute_2xk_layout(data, min_cols=15, min_rows=0, max_columns=2, max_rows=2, default_anchor_id=None): panes = extract_panes(data) if not panes: return RIGHT("no_panes_default", default_anchor_id or "") if all(p.width <= 0 or p.height <= 0 for p in panes): return _decide_headless(panes, max_columns, max_rows) # โ† ๊ฐ™์€ ํ‘œ cols = _group_columns(panes) return _decide(cols, _area_height(data, panes), max_columns, max_rows, min_cols, min_rows) ``` ยง7.2 ์˜ parity ํ…Œ์ŠคํŠธ๊ฐ€ ์ด ๊ณต์œ ๋ฅผ **๊ณ„์•ฝ์œผ๋กœ ๊ณ ์ •**ํ•ฉ๋‹ˆ๋‹ค. ### 4.4 ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ | ๋ณ€์ˆ˜ | ํ˜„์žฌ | Rev.2 | ๊ทผ๊ฑฐ | |---|---|---|---| | `MAM_MAX_PANE_COLS` | unset(๋ฌด์ œํ•œ) | **2** | "2ร—K" ์˜ 2๋ฅผ ์‹ค์ œ๋กœ ๊ฐ•์ œ | | `MAM_MAX_PANE_ROWS` | ์—†์Œ | **2** (์‹ ๊ทœ) | ยง5 ๊ท ๋“ฑํ™” ์ „๊นŒ์ง€ ๋ณด์žฅ ๊ตฌ๊ฐ„ | | `MAM_MIN_PANE_COLS` | 15 | ์œ ์ง€ | 2์—ด ์ƒํ•œ ํ•˜์—์„œ ์—ญํ•  ์ถ•์†Œ | | `MAM_MIN_PANE_ROWS` | 0 | ์œ ์ง€ | ์ƒ๋™ | ### 4.5 C-2 ์ธ์šฉ โ€” `max_columns` ๋ฐฐ์„  (์‹ค์ธก ํ™•์ธ) ์ด์˜์ œ๊ธฐ์˜ ๋ถ€์ˆ˜ ์ง€์ ์„ ์ฝ”๋“œ๋กœ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ```python # layout.py:203 โ† default= ์—†์Œ โ†’ env ๋ฏธ์„ค์ • ์‹œ None parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS")) # layout.py:217-223 โ† ํ•ญ์ƒ ์ „๋‹ฌ decision = compute_2xk_layout(..., max_columns=args.max_cols, ...) ``` `None` ์ด **๋ฌด์กฐ๊ฑด ์ „๋‹ฌ**๋˜๋ฏ€๋กœ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ธฐ๋ณธ๊ฐ’ `2` ๋Š” CLI ๊ฒฝ๋กœ์—์„œ **์ ˆ๋Œ€ ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ๊ทธ๋ฆฌ๊ณ  `lib.sh:432` ๋Š” `--max-cols` ๋ฅผ **์•„์˜ˆ ๋„˜๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค**(grep ๊ฒฐ๊ณผ `lib.sh` ๋‚ด 0๊ฑด). ์ฆ‰ **lib.sh โ†’ layout.py ๊ฒฝ๋กœ๊ฐ€ ์œ ์ผํ•œ ์ƒ์‚ฐ ๊ฒฝ๋กœ์ธ๋ฐ, ๊ฑฐ๊ธฐ์„œ ์บก์ด ์˜์›ํžˆ `None`** ์ž…๋‹ˆ๋‹ค. Rev.1 ์˜ W4 ๋Š” ํ”„๋กœ๋•์…˜์— ๋ฌดํšจ์˜€์Šต๋‹ˆ๋‹ค. **ํ•„์ˆ˜ 3์ค‘ ์กฐ์น˜ (๊ฐ™์€ ์ปค๋ฐ‹)**: ```python # 1) layout.py:203 parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS", default=2)) parser.add_argument("--max-rows", type=int, default=_env_int("MAM_MAX_ROWS", "MAM_MAX_PANE_ROWS", default=2)) ``` ```bash # 2) lib.sh:432 python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-15}" --min-rows "${MAM_MIN_PANE_ROWS:-0}" \ --max-cols "${MAM_MAX_PANE_COLS:-2}" --max-rows "${MAM_MAX_PANE_ROWS:-2}" --sample-pane "$sample_pane" ``` ``` # 3) .mam.env.example:146-148 (C-5) #default: 2 โ† ํ˜„์žฌ "(unset -> no column cap)" ์ด๊ณ  ์˜ˆ์‹œ๊ฐ€ =3 ์ด๋ผ ์ด์ค‘์œผ๋กœ ์–ด๊ธ‹๋‚จ # MAM_MAX_PANE_COLS=2 # (์‹ ๊ทœ ๋ธ”๋ก) MAM_MAX_PANE_ROWS=2 ``` > **C-5 ์ถ”๊ฐ€ ๋ฐœ๊ฒฌ**: `.mam.env.example:147` ์€ ๊ธฐ๋ณธ๊ฐ’์„ "(unset โ†’ no column cap)" ๋กœ, `:148` ์˜ˆ์‹œ๋Š” `=3` ์œผ๋กœ ์ ์–ด **๋ฌธ์„œ ์ž์ฒด๊ฐ€ ์ด๋ฏธ ๋ถˆ์ผ์น˜**ํ•ฉ๋‹ˆ๋‹ค. Rev.2 ๊ฐ’์œผ๋กœ ์–‘์ชฝ์„ ํ•จ๊ป˜ ์ •์ •ํ•˜์‹ญ์‹œ์˜ค. **W4 ํšŒ๊ท€ ๊ฐ€๋“œ** (ยง7.2์— ํฌํ•จ): ```python def test_max_cols_default_reaches_cli_path(): ... # env ์—†์ด --json ์‹คํ–‰ ์‹œ 4ํŽ˜์ธ์—์„œ overflow def test_lib_sh_passes_max_cols_and_rows(): ... # lib.sh ์†Œ์Šค ๋ฌธ์ž์—ด ๊ฐ€๋“œ ``` --- ## 5. ํ–‰ ๊ท ๋“ฑํ™” โ€” `max_rows โ‰ฅ 3` ์˜ ์ „์ œ์กฐ๊ฑด BSP ๋‹จ์ผ ๋ถ„ํ• ์€ **ํ•œ ํŽ˜์ธ๋งŒ** ์ด๋“ฑ๋ถ„ํ•˜๋ฏ€๋กœ ํ˜•์ œ ๋†’์ด๊ฐ€ ์•ˆ ๋ฐ”๋€๋‹ˆ๋‹ค. ๋†’์ด 78, 2ํŽ˜์ธ(๊ฐ 39)์ธ ์—ด์— 1๊ฐœ๋ฅผ ๋”ํ•˜๋ฉด `{39, 19, 20}` ์ด ๋˜๊ณ  `--ratio` ๋ฅผ ์จ๋„ ํ˜•์ œ๋Š” ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค. **3ํ–‰ ๊ท ๋“ฑ์€ ๋ถ„ํ• ๋งŒ์œผ๋กœ ๋ถˆ๊ฐ€๋Šฅ**ํ•˜๋ฉฐ `herdr pane resize --pane --direction up|down --amount ` ์ •๊ทœํ™” ํŒจ์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ **`max_rows` ๊ธฐ๋ณธ๊ฐ’ 2 ๋Š” ์ž„์˜ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ "๊ท ๋“ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€์น˜"** ์ž…๋‹ˆ๋‹ค. ยง6 W6 ์™„๋ฃŒ ์ „์—๋Š” 3ํ–‰์„ ์—ด์ง€ ๋งˆ์‹ญ์‹œ์˜ค(D2). --- ## 6. WBS | ๋‹จ๊ณ„ | ์ž‘์—… | ํŒŒ์ผ | ๋น„๊ณ  | |---|---|---|---| | **W1** | `_full_height_pane`, `_group_columns` ์ถ”์ถœ | `layout.py` | | | **W2** | **๊ณตํ†ต ๊ฒฐ์ •ํ‘œ + GUI/ํ—ค๋“œ๋ฆฌ์Šค ์–‘ ๋ถ„๊ธฐ ๋™์‹œ ์ „ํ™˜** | `layout.py` | **C-1. "1์ค„" ์•„๋‹˜** | | **W3** | ํ—ค๋“œ๋ฆฌ์Šค ํƒ€๊นƒ `panes[n - max_columns]`, anchor ์˜ค์šฉ ์ œ๊ฑฐ | `layout.py` | C-1 | | **W4** | `max_cols`/`max_rows` **3์ค‘ ๋ฐฐ์„ ** | `layout.py`, `lib.sh:432`, `.mam.env.example` | **C-2ยทC-5** | | **W5** | `grid_health()` + `--health` | `layout.py` | ์ง„๋‹จ | | **W6** | ratio ์ •๊ทœํ™” ๋ฆฌ์ปจ์‚ฌ์ผ๋Ÿฌ | ์‹ ๊ทœ `lib_py/layout_repair.py` | ยง5 | | **W7** | ๋ฆฌ์ปจ์‚ฌ์ผ๋Ÿฌ ๋ฐฐ์„  | `create/resume/reconcile` | | | **W8** | ํ…Œ์ŠคํŠธ (ยง7) | `tests/test_layout.py` | | ### 6.1 W2 ๋ฒ”์œ„ ์ •์ • (C-1) Rev.1 ์€ W2 ๋ฅผ **"ํ•ต์‹ฌ 1์ค„"** ์ด๋ผ ์ ์—ˆ์Šต๋‹ˆ๋‹ค. **์ด ํ‘œํ˜„์„ ์ฒ ํšŒํ•ฉ๋‹ˆ๋‹ค.** W2 ๋Š” ์ตœ์†Œํ•œ ๋‹ค์Œ์„ **ํ•˜๋‚˜์˜ ์ปค๋ฐ‹**์— ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: 1. GUI ๋ถ„๊ธฐ๋ฅผ ยง4.1 ๊ฒฐ์ •ํ‘œ๋กœ ๊ต์ฒด 2. ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๊ธฐ๋ฅผ ยง4.2 ๊ฒฐ์ •ํ‘œ๋กœ ๊ต์ฒด (**ํ™€์ง ๋กœ์ง ์‚ญ์ œ**) 3. ๋‘ ๋ถ„๊ธฐ๊ฐ€ ๊ฐ™์€ `_decide*` ๊ณ„์ธต์„ ํ˜ธ์ถœํ•˜๋„๋ก ๊ตฌ์กฐ ์ •๋ฆฌ (ยง4.3) 4. `test_headless_max_columns_growth_guard` ๋“ฑ ์˜› ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ ๊ฐฑ์‹  **๋ถ„๋ฆฌ ์ปค๋ฐ‹ ๊ธˆ์ง€**: GUI ๋งŒ ๋ฐ”๊พธ๊ณ  ํ—ค๋“œ๋ฆฌ์Šค๋ฅผ ๋‚จ๊ธฐ๋ฉด ยง1 ํ‘œ์˜ n=3 ๊ฐˆ๋ผ์ง์ด ๊ทธ๋Œ€๋กœ ์ƒ์‚ฐ์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. --- ## 7. ํ…Œ์ŠคํŠธ ๊ณ„ํš ### 7.1 ๋ˆ„์  ์‹œํ€€์Šค ๊ฐ€๋“œ (Rev.1 ์œ ์ง€ โ€” ์ตœ์ค‘์š”) ๋‹จ์ผ ๊ฒฐ์ •๋งŒ ๋‹จ์–ธํ•˜๋Š” ํ˜„ํ–‰ ๋ฐฉ์‹์€ R-1 ์„ ํ†ต๊ณผ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. **BSP ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋ฅผ ํ…Œ์ŠคํŠธ ํ—ฌํผ๋กœ ์Šน๊ฒฉ**ํ•ฉ๋‹ˆ๋‹ค. ```python def test_four_panes_form_clean_2x2(): panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] for _ in range(3): d = compute_2xk_layout(_payload(panes)) assert not d.is_overflow panes = _bsp_split(panes, d.target_pane_id, d.direction) assert len(panes) == 4 assert max(p["w"] for p in panes) - min(p["w"] for p in panes) <= 2 assert max(p["h"] for p in panes) - min(p["h"] for p in panes) <= 2 def test_fifth_pane_overflows(): ... # capacity 4 โ†’ 4ํŽ˜์ธ ์ƒํƒœ์—์„œ overflow ``` ### 7.2 GUI โ†” ํ—ค๋“œ๋ฆฌ์Šค parity (C-1 ์ฒ˜๋ฐฉ โ€” ํ™•์žฅ) Rev.1 ์€ `n=1` ๋งŒ ๋‹จ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์˜์ œ๊ธฐ๋Œ€๋กœ **์ „ ๊ตฌ๊ฐ„**์„ ๋‹จ์–ธํ•ฉ๋‹ˆ๋‹ค: ```python @pytest.mark.parametrize("n,expected", [(1,"right"), (2,"down"), (3,"down"), (4,"overflow")]) def test_headless_matches_gui_decision(n, expected): hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) assert hl.direction == expected, f"headless n={n}" def test_headless_gui_direction_parity_full_sequence(): """๊ฐ™์€ n ์—์„œ ๋‘ ๋ถ„๊ธฐ์˜ direction ์ด ํ•ญ์ƒ ์ผ์น˜ํ•œ๋‹ค.""" panes = [{"id": "p1", "x": 0, "y": 0, "w": 277, "h": 78}] for n in range(1, 5): gui = compute_2xk_layout(_payload(panes), max_columns=2, max_rows=2) hl = compute_2xk_layout(_headless(n), max_columns=2, max_rows=2) assert gui.direction == hl.direction, f"divergence at n={n}" if gui.is_overflow: break panes = _bsp_split(panes, gui.target_pane_id, gui.direction) def test_headless_fills_alternating_columns(): """C-1: n=2 ๋Š” p1, n=3 ์€ p2 ๋ฅผ ํƒ€๊นƒํ•ด์•ผ ํ•œ ์—ด๋งŒ ๊นŠ์–ด์ง€์ง€ ์•Š๋Š”๋‹ค.""" assert compute_2xk_layout(_headless(2), max_columns=2, max_rows=2).target_pane_id == "p1" assert compute_2xk_layout(_headless(3), max_columns=2, max_rows=2).target_pane_id == "p2" def test_headless_ignores_sample_pane_anchor(): """ยง4.2: --sample-pane ์ด ์ฑ„์šฐ๊ธฐ ํƒ€๊นƒ์„ ์˜ค์—ผ์‹œํ‚ค์ง€ ์•Š๋Š”๋‹ค.""" d = compute_2xk_layout(_headless(3), max_columns=2, max_rows=2, default_anchor_id="p1") assert d.target_pane_id == "p2" ``` ### 7.3 ๊ฐฑ์‹  ๋Œ€์ƒ ๊ธฐ์กด ํ…Œ์ŠคํŠธ | ํ…Œ์ŠคํŠธ | ์ถฉ๋Œ ๋‹จ์–ธ | ์กฐ์น˜ | |---|---|---| | `test_headless_max_columns_growth_guard:399` | `n=2 โ†’ right` | โ†’ `down` | | ใ€ƒ `:409` | `n=5 โ†’ down` (์บก ๋ฌด์‹œ) | capacity ๊ทœ์น™์œผ๋กœ ์žฌ์ž‘์„ฑ | | ใ€ƒ `:414` | `n=4 ๋ฌด์ œํ•œ โ†’ right` | ๊ธฐ๋ณธ ์บก 2 ํ•˜์—์„œ ์žฌ์ •์˜ | | `test_headless_0x0_transitions:142` | ํ™€์ง ์ „์ œ | ์ „๋ฉด ์žฌ์ž‘์„ฑ | | `test_j1*` (min_cols/rows) | ์˜ํ–ฅ ์—†์Œ(๋ช…์‹œ ์ „๋‹ฌ) | ์œ ์ง€ | ### 7.4 W4 ๋ฐฐ์„  ๊ฐ€๋“œ ยง4.5 ์˜ ๋‘ ํ…Œ์ŠคํŠธ. **env ๋ฏธ์„ค์ • ์ƒํƒœ**์—์„œ CLI ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์บก์„ ์ ์šฉํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. --- ## 8. ๋ฆฌ์Šคํฌ | ๋ฆฌ์Šคํฌ | ์˜ํ–ฅ | ์™„ํ™” | |---|---|---| | **GUI/ํ—ค๋“œ๋ฆฌ์Šค ๋ถ„๋ฆฌ ์ปค๋ฐ‹** | n=3 ๊ฐˆ๋ผ์ง์ด ์กฐ์šฉํžˆ ์ƒ์‚ฐ ์ง„์ž… | ยง6.1 ๋‹จ์ผ ์ปค๋ฐ‹ ๊ฐ•์ œ + ยง7.2 parity ํ…Œ์ŠคํŠธ | | **W4 ๋ฐฐ์„  ๋ˆ„๋ฝ** | ์บก์ด `None` ์œผ๋กœ ๋‚จ์•„ Rev.2 ์ „์ฒด๊ฐ€ ๋ฌดํšจ | ยง4.5 3์ค‘ ์กฐ์น˜ + ยง7.4 ๊ฐ€๋“œ | | ํ—ค๋“œ๋ฆฌ์Šค anchor ์˜ค์šฉ | ํ•œ ์—ด๋งŒ ๊นŠ์–ด์ง | ยง4.2 ์ฃผ์˜ + `test_headless_ignores_sample_pane_anchor` | | ๊ธฐ์กด ํ…Œ์ŠคํŠธ ๋Œ€๋Ÿ‰ ์‹คํŒจ | ๊ณ„์•ฝ ๋ณ€๊ฒฝ์ด๋ผ ๋ถˆ๊ฐ€ํ”ผ | ยง7.3 ๋ชฉ๋ก๋Œ€๋กœ ๊ฐฑ์‹  | | capacity 4 ๋กœ ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ฆ๊ฐ€ | 5+ ์—์ด์ „ํŠธ์—์„œ ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ˆ˜โ†‘ | ์˜๋„๋œ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„(D1) | --- ## 9. ๊ฒฐ์ • ํ•„์š” ์‚ฌํ•ญ | ID | ํ•ญ๋ชฉ | ๊ถŒ์žฅ | |---|---|---| | **D1** | `max_columns=2`, `max_rows=2` (capacity 4) ์ˆ˜์šฉ | **์ˆ˜์šฉ** โ€” 4 ์—์ด์ „ํŠธ 2ร—2 ๋ชฉํ‘œ์™€ ์ผ์น˜ | | **D2** | 3ํ–‰ ๊ฐœ๋ฐฉ ์‹œ์  | W6 ์ •๊ทœํ™” ์™„๋ฃŒ ํ›„ | | **D3** | ๋ฆฌ์ปจ์‚ฌ์ผ๋Ÿฌ ์ž๋™ ์žฌ๋ฐฐ์น˜ ๋ฒ”์œ„ | ratio ์ •๊ทœํ™”๊นŒ์ง€๋งŒ ์ž๋™ | | **D4** | ๊ธฐ์กด ์™œ๊ณก ์›Œํฌ์ŠคํŽ˜์ด์Šค | ์ง„๋‹จ๋งŒ, ๋ณต๊ตฌ ์ˆ˜๋™ | --- ## 10. ์ด์˜์ œ๊ธฐ ๋Œ€์‘ ์ •๋ฆฌ | ์ด์˜ | ํŒ์ • | ๋ฐ˜์˜ | |---|---|---| | ํ™€์ง ๋ฐ˜์ „ ์‹œ n=3 ๊ฐˆ๋ผ์ง | โœ… **์ธ์šฉ** | ยง1, ยง4.2, ยง6.1, ยง7.2 | | `max_columns` ๋ฐฐ์„  ๋ˆ„๋ฝ | โœ… **์ธ์šฉ** (์‹ค์ธก ํ™•์ธ) | ยง4.5, ยง7.4 | | parity ํ…Œ์ŠคํŠธ๊ฐ€ n=1 ๋งŒ ๋‹จ์–ธ | โœ… **์ธ์šฉ** | ยง7.2 ์ „ ๊ตฌ๊ฐ„ ํŒŒ๋ผ๋ฏธํ„ฐํ™” | | *(์ž์ง„ ์ •์ •)* singleton ํœด๋ฆฌ์Šคํ‹ฑ ๋น„์ผ๋ฐ˜์„ฑ | โ€” | ยง4.1 | | *(์ž์ง„ ์ •์ •)* ๊ถค์  ํ‘œ โ†” `max_rows=2` ๋ชจ์ˆœ | โ€” | ยง3 | | *(์ž์ง„ ๋ฐœ๊ฒฌ)* `.mam.env.example` ์ž์ฒด ๋ถˆ์ผ์น˜ | โ€” | ยง4.5 C-5 | --- ## 11. Rev.1 ๊ทผ๊ฑฐ ์š”์•ฝ (๋ณ€๊ฒฝ ์—†์Œ) - **herdr = ์—„๊ฒฉ BSP**: `split` ์€ ๋Œ€์ƒ ํŽ˜์ธ rect ๋งŒ ์ด๋“ฑ๋ถ„(๊ฒฉ๋ฆฌ `w16` ์‹ค์ธก). - **R-1**: `down` ์šฐ์„  ์‹œ ํ•˜๋‹จ ํŽ˜์ธ์ด ์ „ํญ์œผ๋กœ ๋‚จ์•„ **2ร—2 ๋„๋‹ฌ ๋ถˆ๊ฐ€**. ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ƒ N=4 ์—์„œ widths `{138,139,277}`, heights `{19,20,39}`. - **R-2**: ์ „๊ณ  ํŽ˜์ธ์ด ์—†์œผ๋ฉด `right` ๋Š” ๋ฐ˜์ชฝ ์—ด๋งŒ ์ƒ์„ฑ. - **ํ•ด๋ฒ• ์‹ค์ฆ**: `right` ์šฐ์„  โ†’ `down` ร—2 โ†’ widths `[138,139]`, heights `[39]` ์™„์ „ ๊ท ๋“ฑ(๊ฒฉ๋ฆฌ `w17` ์‹ค์ธก). ํŠธ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์‚ด์•„์žˆ๋Š” `w15` ์™€ ๋™์ผ. --- ## 12. ๊ฒฐ๋ก  ์ด์˜์ œ๊ธฐ 3๊ฑด์„ ๋ชจ๋‘ ์ธ์šฉํ•˜๋ฉฐ, ๊ทธ ๊ณผ์ •์—์„œ ์ œ ๊ณ„ํš์„œ์˜ ์ถ”๊ฐ€ ๊ฒฐํ•จ 3๊ฑด(C-3ยทC-4ยทC-5)์„ ์ž์ง„ ์ •์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. Rev.1 ์˜ ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ํ‘œํ˜„์€ **"ํ•ต์‹ฌ 1์ค„"** ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทผ๋ณธ ์›์ธ ์ง„๋‹จ์€ ์˜ณ์•˜์œผ๋‚˜, ์ฒ˜๋ฐฉ์˜ ๋ฒ”์œ„๋ฅผ ๊ณผ์†Œ ํ‘œ๊ธฐํ•˜์—ฌ ๊ตฌํ˜„์ž๊ฐ€ GUI ๋งŒ ๊ณ ์น˜๊ณ  ํ—ค๋“œ๋ฆฌ์Šค๋ฅผ ํ™€์ง ๋ฐ˜์ „์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์—ฌ์ง€๋ฅผ ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค. Rev.2 ๋Š” ์ด๋ฅผ **๋‹จ์ผ ๊ฒฐ์ •ํ‘œ ๊ณต์œ **๋กœ ๊ตฌ์กฐ์ ์œผ๋กœ ์ฐจ๋‹จํ•˜๊ณ (ยง4.3), parity ํ…Œ์ŠคํŠธ๋กœ ๊ณ„์•ฝ์„ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค(ยง7.2). `max_columns` ๋ฐฐ์„  ์ง€์ ์€ ํŠนํžˆ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค โ€” ์ด๊ฒƒ์ด ์—†์œผ๋ฉด **Rev.2 ์ „์ฒด๊ฐ€ ํ”„๋กœ๋•์…˜์—์„œ ๋ฌดํšจ**์ž…๋‹ˆ๋‹ค. lib.sh ๊ฐ€ `--max-cols` ๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š๊ณ  `_env_int` ๊ฐ€ `None` ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ด์ค‘ ๋ˆ„๋ฝ์ด๋ผ, ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ธฐ๋ณธ๊ฐ’๋งŒ ๋ฐ”๊พธ๋Š” ์ˆ˜์ •์€ ํ…Œ์ŠคํŠธ๋งŒ ํ†ต๊ณผํ•˜๊ณ  ์‹ค์‚ฌ์šฉ์—์„œ๋Š” ์•„๋ฌด ํšจ๊ณผ๊ฐ€ ์—†์—ˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.