유휴 여백 재배치 설계
대치 후 재배치가 남기는 FAIL을 줄이기 위해, 「텍스트 오버플로우가 있을 때만」이던 발동 조건을 유휴 여백 기준으로 일반화하고, 페이지의 빈 공간을 이동 예산으로 쓰는 후보 설계.
코퍼스는 살아 있다(오늘 2건 제외돼 96건으로 이동 중) — 절대값은 낡으니, 구현 후 평가는 같은 시점의 코퍼스에서 베이스라인과 함께 다시 잰다.
§1무엇을 고치려는 판인가
현행 최선 합본은 넘침의 87%를 지우고 판정을 S51/A29/F18까지 끌어올린다. 남은 것을 갈라 보면 판이 세 덩어리다.
| 덩어리 | 규모 | 실체 | 이 설계의 목표 |
|---|---|---|---|
| 이길 수 있는 FAIL | 4~5건 | 정답(expected)이 SUCCESS·ACCEPTABLE에 도달한 케이스 — 5933332(카드 넘침), 6690122, 5647291, 6780937 | 라벨 뒤집기 — A·B의 1차 표적 |
| 정답도 못 푼 FAIL | 13건 | expected 제작 과정도 신규 겹침 게이트를 못 넘겨 UNSOLVABLE로 남긴 케이스 | 라벨 대신 ⑤-b 쌍·면적 감소 — 라벨이 그대로여도 눈에 보이는 겹침이 줄면 서비스 품질은 좋아진다 |
| ACCEPTABLE 29건 | 29건 | 전원 단일 사유 「유휴 여백 > 원안」 — 대치로 글이 짧아져 글리프 면적이 준 것 | 이번 라운드 목표가 아니다 — 평행이동은 점유율을 못 바꾸고(§3), 사람이 만든 정답도 18건이 같은 사유로 ACCEPTABLE에 머문다 |
§2남는 원인 — 실제 케이스 세 장
잔존 FAIL의 공통 구조: 결함(겹침)은 있는데 그 주변에 빈 공간이 있고, 알고리즘은 그 빈 공간을 모른다. 잔존 18건 중 15건이 유휴 여백 ≥ 0.5인 페이지다.
구조적 원인 네 가지
| # | 원인 | 코드의 자리 |
|---|---|---|
| 1 | 발동이 결함(넘침) 중심 — 넘침이 없으면 method 1·3은 no-op. 겹침 후보(4·5)는 돌지만 공간 지식이 없다 | method1.ts 조기 반환 · method3.ts site 조건 |
| 2 | 여백 지식이 전역 스칼라 하나 — 「얼마나 비었나」(④)만 있고 「어디가 비었나」가 없다 | metrics.ts idleMarginRatio · architecture.md §9.5 |
| 3 | 짝 밀기가 대칭·단발 — 절반씩만 밀고, 첫 게이트 실패에서 문서 전체를 포기 | method4.ts pushApart |
| 4 | 가로가 전면 금지 — 「이웃을 민다」와 「빈 공간으로 간다」를 구분하지 못한다 | §4.1 규칙 |
§3데이터가 설계를 바꾼 세 가지
① 「폰트 복원」 후보는 기각했다. 초기 후보 1순위는 「오토핏으로 줄어든 폰트를 유휴 여백으로 복원」이었다. 실측 결과 현 코퍼스의 rendered.json은 98건 전부 폰트가 원안과 동일하다(렌더본을 오토핏 끄고 굽는 현행 방식). 복원할 것이 없다. case.json의 problems: shrink 79건은 옛 굽기 기준의 낡은 라벨이다 — scripts/label-test-cases.ts 재실행을 별도 정리 작업으로 권고한다.
② FAIL 18건 중 13건은 정답도 못 넘겼다(UNSOLVABLE). 라벨 개선의 천장은 4~5건이다. 그래서 A·B의 주 지표를 라벨이 아니라 ⑤-b 신규 겹침 쌍·면적 감소로 잡는다 — 못박는 것은 앞 합본과의 차이와 잔존 케이스 목록의 변화다(relayout_logics.md §10의 원칙 그대로).
③ ACCEPTABLE 29건(유휴 > 원안)은 이동으로 물리적으로 안 바뀐다. 점유율은 leaf 박스들의 union인데 평행이동은 union 면적을 바꾸지 못한다. 잃어버린 글리프 면적을 채우려면 무언가를 키워야 하고(컨테이너 확대 = 사실상 리디자인), 사람이 만든 정답조차 18건이 같은 사유로 ACCEPTABLE에 머문다. 이 덩어리는 명시적으로 이번 범위 밖에 둔다.
§4후보 세 개 — A·B는 구현, C는 설계
세로 스프레드
겹침 클러스터를 세로 빈 구간(코리도) 안에서 placeRow 정확해로 다시 쌓는다. pushApart의 일반화 — 비대칭·N유닛·클러스터 단위 되돌림.
- 발동: 신규 겹침 존재 ∧ 유휴 ≥ τ(0.5) — 오버플로우 불필요
- 장: 예측 0 · 크기 불변 · 정렬 대가 없음 · 기존 부품 재사용
- 단: 세로 한정 — 코리도보다 큰 클러스터는 실현불가로 남김
유휴 여백 지도 · 가로 탈출
「어디가 비었나」를 답하는 공용 모듈(4방향 slack, A와 공용) + 가로로 파고든 겹침을 빈 공간으로만 옮겨 푸는 후보.
- 발동: 가로 침투가 진짜인 쌍(ⓒ2) ∧ 유휴 ≥ τ
- 장: §4.1을 고치지 않고 지킴(이웃 아닌 빈 자리로) · 이중 방어(지도+게이트)
- 단: 좌변 정렬(③) 대가 가능 — 최소 변위 우선으로 상쇄
결정론적 후보 선택기
후보 전부를 돌리고 사전순 규칙(라벨 → ⑤-b → ① → ② → ④ → ③ → 변위)으로 케이스별 최선을 고른다. 학습·가중치 없음.
- 장: 고정 순서 합본의 조합 한계 해소 · 구현 0에 가까움(기존 함수 조합)
- 단: 실행 N배 · 생성력이 아니라 선택력
- 채택 게이트: 베이스라인 JSON으로 상한 선계산 후 결정
합본에서의 자리는 셋이 같은 그림을 본다:
method 3 → 1 ↔ 2 → 5 → 4 (현행: 크기 → 유닛 안 → 유닛 사이)
→ 7 세로 스프레드 ← A. 4가 남긴 겹침을 코리도로
→ 8 가로 탈출 ← B. 7이 넘긴 가로 침투 쌍을 빈 공간으로
→ 6 폰트 축소 (마지막 수단, 그대로)
§5트리거 베이스라인 τ — 기본 0.5, 조정 전제
- 정의:
idleMarginRatio ≥ τ(= 콘텐츠 점유율 ≤ 1−τ)일 때 A·B가 발동한다. τ 기본값은 0.5. - 실측 커버리지: 잔존 FAIL 18건 기준 τ=0.5 → 15건, τ=0.4 → 17건, τ=0.65(라벨러의
IDLE_MARGIN_AT_LEAST) → 9건이 발동권. 평가에서 {0.4, 0.5, 0.65}를 스윕해 게이트 되돌림 비율과 함께 정한다. - 단독 절대값 트리거의 위험은 A·B에는 없다 — 발동에 「신규 겹침 존재」가 결합돼 있어, 원안부터 여백이 큰 미니멀 디자인(원안 유휴 0.795 같은)을 멀쩡한데 건드리는 일이 없다.
- 고정점 규율: 발동시킨 결함(겹침)이 실제로 줄 때만 확정 — method 2의 전례를 따른다. τ 상수는 후보가 읽어야 하므로
batch/가 아닌relayout-core/에 둔다(저장소 관례).
§6측정 계획과 성공 기준
- 등록:
method7·method8단독 + 합본 3종(…+7+6,…+8+6,…+7+8+6) — A·B 각각의 기여와 시너지를 가른다. - 재는 것: 판정 분포(S/A/F), ⑤-b 쌍·면적, 넘침, 정렬선 보존율(대가), 잔존 FAIL 케이스 목록의 변화, 실현불가·slack부족 리포트.
- 성공 기준: ① 신규 겹침 쌍 합계가 현행 최선 대비 유의미하게 감소(1차 지표) ② FAIL 최소 2건 전환 ③ 정렬선 평균 하락 ≤ 0.01 ④ 어떤 케이스도 라벨이 나빠지지 않음(게이트 구조상 보장되어야 하고, 실측으로 재확인).
- 규칙 검사:
pnpm exec vitest run src/post-replace-relayout전건 통과 + 신규 모듈 단위 테스트(idleSpace 스윕 규칙, 게이트 되돌림).
구현과 평가 결과는 아래 §7이다(설계 당시 계획 그대로 덧붙였다).
§7구현 결과 (2026-08-27 · 96건)
결론 — A·B를 구현해 붙인 합본이 FAIL 17 → 12(−29%), 신규 겹침 59 → 42쌍(−29%)을 만들었고, 나빠진 케이스는 0건이다. 라벨을 딴 5건 중 5건 모두가 「정답 제작 과정도 게이트를 못 넘겨 UNSOLVABLE로 남았던」 케이스다 — §1에서 4~5건으로 잡았던 천장이 실제로는 더 물렀다.
코퍼스가 평가 중 98 → 96건으로 움직여(케이스 2건 제외) 모든 수치를 96건에서 다시 쟀다 — 베이스라인(현행 최선 합본)도 같은 판에서 S50/A29/F17이다.
| 후보 | S | A | F | ⑤-b 쌍 | ⑤-b 면적 | 정렬선 평균 |
|---|---|---|---|---|---|---|
| before (대치 직후) | 0 | 11 | 85 | 165 | 1,509,716 | 0.9276 |
현행 최선 3+1+2+5+4+6 | 50 | 29 | 17 | 59 | 665,521 | 0.8540 |
| + method 7 (A) | 54 | 29 | 13 | 46 | 605,879 | 0.8486 |
| + method 8 (B) | 51 | 29 | 16 | 55 | 652,928 | 0.8498 |
| + 7 + 8 (A+B) | 55 | 29 | 12 | 42 | 593,286 | 0.8444 |
| expected (목표선) | 22 | 18 | 5U 51 | 6 | 52,906 | 0.8228 |
- 라벨 이동(최선 → A+B): FAIL→SUCCESS 5건 — 5611182 · 5628689 · 5694141 · 5698092 · 6377108. 나빠진 케이스 0건(회귀 가드가 지켰다).
- 역할 분담이 실측으로 확인됐다 — 5628689는 B(가로 탈출)만이, 5611182·5694141·5698092·6377108은 A(세로 스프레드)가 풀었고, A+B는 정확히 둘의 합집합 + 상호 보완이다.
- 대가: 정렬선 평균 0.8540 → 0.8444(−0.0096). 크게 깎인 4건(5628689 −0.400, 6377108 −0.357, 5694141 −0.177, 5698092 −0.014)은 전부 FAIL→SUCCESS를 딴 케이스다 — 겹침 해소와 정렬 유지의 명시적 트레이드.
- τ 스윕(단독 method 7): τ=0.4 → 발동 56건·개선 30건, τ=0.5 → 51건·29건, τ=0.65 → 36건·19건. 디폴트 0.5 유지가 합리적이고, 0.4로 내리면 +1건(6780937, 유휴 0.46)이 사정권에 든다.
구현이 설계에서 배운 것 — 회귀 한 건이 가르친 두 가지
첫 평가에서 5629794가 SUCCESS→FAIL로 떨어졌다. 원인은 자(판정 박스)의 불일치였다: 후보 안의 게이트는 프레임 대용 상자로, 성적표는 실측 tight(carryTightBoxes 승계)로 재서, 유령 겹침을 쫓은 이동이 바깥 채점에서 신규 겹침으로 태어났다. 고친 것 둘 —
- 실측 tight 배관:
replaceLayout이 케이스의 rendered·원안 tight를 후보까지 넘겨(RelayoutRunContext.tight/baseTight), method 7·8의 트리거·게이트가 성적표와 같은 자를 본다. 이 배관이 회귀만 막은 게 아니라 기준 쪽 겹침 부풀림에 가려 안 보이던 신규 쌍들을 드러내 FAIL→SUCCESS 5건 중 3건(5611182·5698092·6377108)을 새로 사정권에 넣었다. betterOf회귀 가드: 스프레드 합본은 method 6의 나비효과까지 포함한 두 갈래 완성본(스프레드 유/무)을 같은 자로 견줘 나은 쪽만 남긴다(같으면 기존 경로) — 케이스 단위로 「절대 나빠지지 않음」이 구조적으로 보장된다.
잔존 FAIL 12건 — 왜 남았고 무엇이 다음인가
| 원인 | 케이스 | 다음 수 |
|---|---|---|
| 세로 여유 자체가 없다 — 코리도 실현불가 + 짝 나누기 여유 0 | 5546858(13쌍, 855px 텍스트 열) · 5638155 · 5647291(스택 764px vs 코리도 590px) · 6672017 · 6688247 · 6805703 | 폭 열기(method 2·3)와의 결합 — 좁은 칼럼을 넓혀 스택 높이를 줄인 뒤 스프레드. 5647291이 교과서 케이스 |
| 넘침만 남았다 — 겹침이 없어 7·8의 발동 조건 밖 | 5933332 · 6690122 · 5108314(+1쌍) | expected는 SUCCESS·ACCEPTABLE에 도달 — method 1의 hug 계획/게이트를 케이스 단위로 파야 한다(이동이 아니라 크기의 문제) |
| 빽빽한 페이지 — 유휴 0.36으로 τ 미만 + 가로 slack 0 | 6577997(가로 8쌍 · 그릇 4쌍) | 유휴 예산이 없는 판 — 스프레드 계열의 사정권 밖이 맞다(정답도 UNSOLVABLE) |
| τ 경계 — 유휴 0.46으로 0.5에 걸림 | 6780937(세로 1쌍) | τ=0.4 검토 대상(스윕상 부작용 없음) |
| 가로 slack 부족 | 6478618(가로 3쌍) | 비대칭·연쇄 가로 이동(placeRow의 가로판) — B의 2단계 확장 |
| 같은 유닛 안(method 5의 몫) · 그릇이 낀 쌍 | 6805703(같은 유닛 2쌍) · 6577997(그릇 4쌍) | method 5 강화 / 그릇 이동은 위상 전제를 바꾸는 별건 |
딸려 나온 정리 거리
case.json의problems: shrink라벨은 옛 굽기 기준의 유물 —scripts/label-test-cases.ts재실행 권고(§3①).- method 6도 프레임 대용 자로 발동을 판정한다 — 이번에 깐 tight 배관을 method 6(·4·5)의 게이트에도 물리면 같은 급의 정밀도 개선 여지가 있다.
docs/relayout_logics.md·AGENTS.md에 method 7·8 절 추가는 리뷰 후 반영 예정.