← 미리디 아카이브 / 병합 대치 정렬 문제

내용시각화 post-replace-relayout v0.6 · 아카이브 작성 2026-08-28

재배치 알고리즘 파이프라인 v0.6

아카이브 — 이 판은 더 갱신되지 않는다

작성 2026-08-28 · 브랜치 feat/post-replace-relayout-2 · 커밋 2aed14ada · 코퍼스 1,177건 시점의 글이다.

이 글이 §5 에서 지적한 중복이 그 뒤 실제로 정리됐다 — 「짝 밀기 두 벌을 method 7 한 벌로 합친다」(bad3810f2) · 「한 이름을 쓰던 두 뜻을 가른다」(9a4f52fb2). 현재판은 재배치 알고리즘 파이프라인 v0.7 이다.

내용 대치가 끝난 컴포넌트를 넘침·겹침·유휴 여백이 없도록 다시 배치한다. 연산자가 여덟이 됐고, 새로 붙은 둘은 페이지의 빈 공간을 이동 예산으로 쓴다. 이 판은 파이프라인을 흐름도로 펴고, 책임이 겹치는 자리·복잡도·선택 층을 코드로 따져 적었다.

miricanvas-iui · src/post-replace-relayout 브랜치 feat/post-replace-relayout-2 후보 16개 (단독 8 · 합본 8) 코퍼스 1,177
§0

30초 요약 — 네 가지 물음에 먼저 답한다

  • 7과 8은?양자택일이 아니라 분업이다. 둘은 penetrationOf 라는 같은 판별식으로 겹침 쌍을 서로 겹치지 않게 나눠 가진다 — 한 쌍은 반드시 한쪽에만 간다. 합본에서 둘 다 돌지만 같은 쌍을 두 번 건드리지 않는다. §4
  • 겹치는 책임있다. method 4 의 2단계(대칭 절반 밀기)와 method 7 의 2-b(비대칭 짝 밀기)는 같은 층·같은 축·같은 게이트다 — 후자가 전자의 진부분집합적 일반화다. 둘을 갈라 두는 유일한 이유는 τ 트리거뿐. §5
  • 단순화 여지penetrationOf두 파일에 글자 하나 다르지 않게 복제돼 있고 (그 동일성에 분업의 정확성이 걸려 있다), OVERLAP_EPS같은 이름으로 1 과 100 두 뜻을 쓴다. §5
  • 복잡도leaf 노드가 중앙값 12 · 최대 60 이라 O(L²) 는 사실상 상수다. 지배항은 method 6 — 배율 한 칸마다 앞의 다섯을 통째로 되부르고, 회귀 가드가 켜지면 그것이 두 번 돈다. §6.1
  • 공간doc 한 벌이 중앙값 39.1KB, 합본 끝에 대여섯 벌이 동시 생존한다. 절대량은 작고 진짜 부담은 할당 회전 — 한 케이스에 사본 수십 벌이 났다 사라진다. §6.2
  • 가장 복잡 ≠ 가장 비쌈1,177건 전건 실행 실측 — 최악 복잡도가 가장 나쁜 method 5 는 합본에서 16.0% 발동 · 8.9% 조치로 가장 드물게 일한다. method 1↔2 는 100% 발동, method 6 은 43.2%, betterOf 는 9.7%. §6.3
  • 선택기는?결정론적 후보 선택기는 아직 코드에 없다(설계만). 구현된 것은 betterOf 이고, 그건 스프레드 합본 3종에서 두 갈래만 고른다. 설계안 C 는 후보 전부가 대상이라 범위가 다르다. §7

이 글의 근거

브랜치 feat/post-replace-relayout-2 · 커밋 2aed14ada작업트리다(git status 비어 있음 — 줄 번호가 그대로 맞는다). 실측은 aee07d0b 에서 돌렸고, 그 뒤 HEAD 가 움직였지만 이 글이 인용한 후보 파일은 한 줄도 안 바뀌었다 — 바뀐 것은 metrics.ts(+59줄) · relayout-core/index.ts · 문서, 그리고 새 파일 separateOverlaps.ts 다(§5 에 반영). 인용한 15개 줄 번호는 현재 HEAD 에서 다시 확인했다.

이 커밋은 원격에 없다 — GitHub permalink 가 404가 되므로 코드 링크는 vscode://file/… 편집기 링크로 건다.

이전판은 v0.5 (2026-08-26 · 연산자 여섯 · 실측 111건) 이다.

§1

파이프라인 상세 흐름도

본판 합본 method 3 + 1 + 2 + 5 + 4 + 7 + 8 + 6 이 한 문서를 어떻게 통과시키는지, 분기·되돌림·되부름까지 폈다. 왼쪽 여백은 그 단계가 거는 게이트와 되돌림 단위, 오른쪽 여백은 반복 상한이다.

게이트 · 되돌림 단위 파이프라인 반복 상한 rendered.json  — 후보의 유일한 입력 freezeRelations — 위상 동결 유닛 · 띠 · 체인 · 프레임을 한 번 정하고 굳힌다 이후 재계산 없음 method 3 · 가로 넘침 해소 도형을 좌우로 · 막히면 텍스트를 안쪽 폭으로 좁힌다 ① 없던 쌍 되돌림 = 노드 method 1 세로 재분배 method 2 가로 확장 폭이 바뀌면 높이가 바뀐다 — 고정점까지 되풀이 ① 없던 쌍 되돌림 = 노드 MAX_ROUNDS = 3 method 5 · 유닛 의 겹침 가로로 열거나 도형 안에서 다시 쌓는다 — 크기를 바꾸는 마지막 후보 ①②③ 되돌림 = 유닛 method 4 · 유닛 사이의 겹침 1단계 띠 쪼개기 → 2단계 pushApart 최소 침투 깊이의 절반씩 서로 반대로 — 여유를 안 본다 ①②③ 되돌림 = 체인·쌍 MAX_PUSHES = 20 base 있음 ∧ 신규 겹침 ≥ 1쌍 ∧ idleMarginRatio ≥ τ (0.5) 넘침은 조건이 아니다 — v0.5 와의 핵심 차이 아니오 7·8 을 건너뛴다 (빽빽한 페이지) 쌍 분류  penetrationOf(a, b) iy · ix · columnContained 를 잰다 — 두 후보가 같은 함수를 본다 iy ≤ ix 또는 열 포함 iy > ix 이고 열 포함 아님 method 7 유휴 세로 스프레드 코리도 placeRow / 비대칭 짝 밀기 method 8 유휴 가로 탈출 4방향 slack 으로 빈 자리 찾기 ①②③ (둘 다) 되돌림 = 클러스터 / 쌍 MAX_NUDGES = 20 MAX_MOVES = 20 7 이 먼저 — 세로는 정렬 대가가 없다 7·8 이 하나라도 옮겼나 아니오 예 — 두 갈래를 만든다 갈래 A — 스프레드 있음 method 6 (폰트 축소) on 7·8 결과 배율 한 칸마다 3+1+2+5+4 재실행 갈래 B — 기존 경로 method 6 on 4 까지의 결과 7·8 을 안 탄 완성본 ③ 넘침 증가 되돌림 = 문서 전체 betterOf — 완성본끼리 사전순 비교 ⑤-b 쌍 ↓ → 면적 ↓ → ① 노드 ↓ → 면적 ↓ → ② ↑ → ③ ↑ 전부 같으면 기존 경로(B)를 남긴다 After 장 — 재배치된 sheetJson 채점은 여기서 시작한다
점선 두 갈래가 우회로다 — 유휴 여백이 τ 미만이면 7·8 을 건너뛰고, 7·8 이 아무것도 안 옮겼으면 betterOf 비교 자체를 건너뛴다. 회귀 가드는 공짜가 아니라 필요할 때만 치르는 비용이다(§6).
§2

공통 토대 — 여덟이 같은 부품을 순서만 바꿔 부른다

위상 네 층 — 좌표를 건드리기 전에 굳힌다

freezeRelations 가 한 번 정하고 굳힌다. 옮기는 도중에 다시 재면 소속이 바뀌어 결과가 처리 순서에 의존한다. 이 층이 겹침 후보 넷을 가른다 — 움직이는 단위가 무엇이냐가 곧 무엇을 풀 수 있냐다.

① 유닛 — 함께 움직이는 최소 단위 배경 도형 하나 + 그 도형이 담은 텍스트 전부 → 같은 유닛 안에서 겹치면 옮길 대상이 하나라 영영 안 풀린다 — method 5 의 자리 ② 띠(가로) · ③ 체인(세로) 띠 — 세로 구간이 겹치는 유닛들 = 「같은 줄」 띠는 통째로 움직인다. 체인은 세로 자리를 다투는 열이다 체인 — 가로 구간이 겹치는 유닛들 = 「같은 열」 ④ 프레임 — 그릇 안에 다른 유닛을 담은 도형(카드·패널). 자기는 안 움직이고 그릇이 된다
되먹임이 하나 있다 — 2D 로 포개진 둘은 세로 구간도 당연히 겹치므로 자동으로 한 띠가 된다. 겹침이 스스로를 지키는 셈이고, method 4 의 1단계가 그 고리를 끊는 일이다.

위상을 굳히면 좌표는 정확히 풀린다

위상까지 탐색 변수로 열면 배치는 조합 최적화가 되어 다항시간에 못 푼다. 굳히면 남는 것은 1차원 좌표 문제이고, 그건 닫힌 해가 있다. 음수 갭이 곧 겹침이라 「겹친 것을 떼어 놓기」는 「갭 하한을 0 으로 두고 다시 나누기」와 같은 말이다.

부품무엇비용파일
여백 대수갭을 델타 균등 + 하한 water-filling 으로 다시 나눈다O(g²) 최악gapAlgebra.ts
PlaceRow (Abacus)겹치지 않으면서 가장 덜 움직이는 배치 — 정렬 후 1패스 클러스터 스윕O(k log k)placeRow.ts
실현가능성못 담으면 「몇 px 모자란가」를 남기고 던진다O(1)feasibility.ts
유휴 여백 지도 v0.6 신규장애물 · 4방향 여유 · 세로 코리도 — 격자가 아니라 상자 스윕O(L)idleSpace.ts
겹침 지도같은 그룹 안 leaf 쌍을 전부 견준다 — 지표·게이트의 심장O(L²)metrics.ts

게이트 — 후보가 스스로에게 거는 제동

게이트무엇을 묻나누가 쓰나
① 없던 쌍자기 입력에 없던 겹침 쌍을 만들었나1·2·3·4·5·7·8
② 목적 달성원안 대비 신규 겹침(지표 ⑤-b)이 실제로 줄었나4·5·7·8
③ 넘침 증가넘침이 늘지 않았나4·5·6·7·8

②가 겹침 후보에만 있는 이유 — 자리를 다시 나누는 연산은 한 쌍을 떼어놓느라 원래 있던 쌍을 키울 수 있는데 ①은 그것을 못 잡는다(쌍 자체는 있던 것이라서).

판정 — 무엇이 SUCCESS 인가

라벨조건
FAIL① 넘침이 원안보다 나쁘다(노드 수 ↑ 또는 면적이 OVERFLOW_AREA_TOL = 10% 넘게 ↑) · ⑤ 신규 겹침이 있다 · ② 폰트가 EXCESSIVE_FONT_SCALE = 0.7 미만 — 셋 중 하나라도
SUCCESSFAIL 아님 + 폰트 배율이 원안 그대로(=1) + ④ 유휴 여백이 원안 이하
ACCEPTABLE그 사이

「폰트 배율 = 1」이 method 6 의 자리를 정한다

글자를 한 톨이라도 줄이면 SUCCESS 가 영영 불가능하다. 그래서 method 6 은 맨 뒤이고, 앞이 이미 판정을 넘긴 케이스에는 아예 발동하지 않는다.

§3

연산자 여덟 — 층과 축으로 갈린다

#손대는 것크기트리거예측
1세로 유휴 여백 재분배노드·체인세로세로만 키움넘침0
2가로 확장노드가로넓힘넘침·좁은 칼럼줄바꿈
3가로 넘침 해소노드가로키우거나 좁힘가로 넘침줄 수
4겹침 — 띠 쪼개기 + 대칭 절반 밀기유닛 사이세로고정신규 겹침0
5겹침 — 도형 안에서 열거나 재쌓기유닛 안가로·세로바꿈신규 겹침일부
6가독성 하한 폰트 축소노드글자판정 미달0
7 신규겹침 — 코리도 재배치 + 비대칭 짝 밀기유닛 사이세로고정신규 겹침 ∧ 유휴 ≥ τ0
8 신규겹침 — 빈 자리로 탈출유닛 사이가로고정신규 겹침 ∧ 유휴 ≥ τ0

겹침을 푸는 후보가 넷이고, 넷을 가르는 것은 두 축이다 — (유닛 안 / 유닛 사이)과 이동축(세로 / 가로). 4·7 이 같은 칸에 앉는 것이 §5 에서 따질 중복이다.

§4

method 7 과 8 은 겹치는가 — 분업이다

답 — 양자택일도, 중복도 아니다. 같은 판별식으로 나눠 가진다

두 후보는 penetrationOf(a, b) 로 겹친 쌍의 세로 침투 iy · 가로 침투 ix · 열 포함 여부를 재고, 그 결과로 서로 배타적인 조건을 건다. 한 쌍은 반드시 정확히 한쪽에만 간다 — 둘 다 처리하는 쌍도, 아무도 안 맡는 쌍도 없다.

원안에 없던 겹침 쌍 유닛 박스로 잰다 — 움직이는 단위가 유닛이라서 penetrationOf(a, b) iy = 세로 겹친 길이 · ix = 가로 겹친 길이 columnContained = ix ≥ min(두 폭) − 1px iy ≤ ix  또는  columnContained → method 7 (세로 스프레드) iy 얕음 세로로 조금만 겹쳤다 — 떼어 놓을 축이 세로다 ix = 상자의 전체 폭 한 열에 통째로 — 가로는 분리축이 아니다 iy > ix  그리고  NOT columnContained → method 8 (가로 탈출) ix 얕음 옆에서 살짝 파고들었다 — 세로로 밀어도 안 풀린다
두 조건은 서로의 부정이다 — P = (iy > ix ∧ ¬columnContained) 라 할 때 method 8 은 P, method 7 은 ¬P 를 맡는다. 그래서 합본에서 둘 다 돌아도 한 쌍을 두 번 건드리지 않는다. 각자 못 푼 것은 리포트로 세어 상대에게 넘긴다 — method 7 의 horizontalPairs, method 8 의 verticalPairs 가 그 계량기다.

다만 「같은 쌍」의 정의가 시점에 달렸다

method 8 은 method 7 이 이미 옮긴 뒤의 문서에서 쌍을 다시 분류한다. method 7 이 다른 유닛을 세로로 옮기면 그 주변 쌍의 iy·ix 가 바뀌므로, 7 이전에 「가로」였던 쌍이 8 시점에는 아예 안 겹칠 수도, 반대로 새 쌍이 생길 수도 있다. 분업은 매 시점 다시 계산되는 분업이지 한 번 정해 둔 명단이 아니다. (새로 태어난 쌍은 게이트 ①이 잡아 되돌린다.)

§5

겹치는 책임과 단순화 여지

① 진짜 중복 — method 4 의 2단계와 method 7 의 2-b

겹침 후보 넷 중 4 와 7 은 같은 칸에 앉아 있다(§3) — 같은 층(유닛 사이), 같은 축(세로), 같은 게이트 셋(①②③), 같은 MOVE_EPS·OVERLAP_EPS. 각각의 「짝 밀기」를 나란히 놓으면 관계가 분명해진다.

method 4 — 2단계 pushApartmethod 7 — 2-b 짝 나누기
배분최소 침투 깊이의 절반씩 서로 반대로비대칭 — 위 유닛은 위 여유만큼, 아래 유닛은 아래 여유만큼
여유를 보나안 본다 — 밀고 나서 게이트가 잡는다directionalSlack(box).up / .down 을 먼저 잰다
부분 이동없다 — 되면 하고 아니면 되돌린다있다 — 여유가 침투보다 작으면 되는 데까지
대상 수가장 깊은 한 쌍클러스터 안의 쌍을 깊은 것부터
상한MAX_PUSHES = 20MAX_NUDGES = 20
트리거신규 겹침만신규 겹침 ∧ 유휴 ≥ τ

7 의 짝 밀기는 4 의 짝 밀기를 포함한다

위·아래 여유가 같을 때 비대칭 배분은 정확히 절반씩으로 떨어진다 — 즉 method 4 의 2단계는 method 7 2-b 의 특수한 경우다. 둘을 갈라 두는 유일한 이유는 τ 트리거다: method 7 은 유휴 여백이 0.5 미만인 빽빽한 페이지에서 아예 안 돈다.

단순화 제안 — 짝 밀기를 한 벌로 합치고 τ 를 그 단계의 파라미터로 내리면 (빽빽한 페이지에서는 여유가 작아 자연히 부분 이동조차 못 하므로) method 4 는 1단계 띠 쪼개기만 남고 세로 분리는 전부 method 7 이 맡는다. 두 벌의 상한·엡실론·게이트 배선이 한 벌이 된다.

② 코드 중복 — 고쳐야 할 셋

penetrationOf 가 두 벌

method7.tsmethod8.ts글자 하나 다르지 않게 복제돼 있고, COLUMN_EPS = 1 도 각자 선언한다.

위험§4 의 분업은 두 판별식이 같다는 전제 위에 서 있다. 한쪽만 고치면 아무도 안 맡는 쌍이나 둘 다 손대는 쌍이 조용히 생긴다.

고칠 자리relayout-core 로 올린다. 쌍을 다루는 모듈(pairing.ts)이 이미 있다.

OVERLAP_EPS 가 두 뜻

metrics.ts 의 것은 export 된 100 — 「이보다 작은 접촉은 겹침으로 안 센다」. method 4·7·8 의 것은 로컬 1 — 「원안보다 늘었나」를 물을 때의 잡음 하한.

같은 이름 · 다른 뜻 · 100배 차이다. 한 파일에서 둘을 같이 읽을 일이 없어 지금은 안 터지지만, 부품을 코어로 올리는 순간 부딪힌다.

고칠 자리 — 뜻이 이름에 드러나게 OVERLAP_MIN_AREA / OVERLAP_GROWTH_EPS 처럼 가른다.

겹침 분리가 세 벌이다

후보 쪽 둘(method 4·7) 말고 정답 제작 경로용이 하나 더 있다 — relayout-core/separateOverlaps.ts(2026-08-28 신규). 「열을 아래로 흘리기」만 하는 최소판이고, 합치지 않은 이유를 스스로 적어 두었다: 후보는 자기 입력에 견주고 정답은 원안에 견주므로 기준이 다르다.

그 파일이 태어난 사연이 이 절의 논지를 그대로 뒷받침한다 — 원래 스크립트 안의 지역 함수였는데 사본을 뜨면 정책이 갈라진다는 이유로 코어에 올렸다. 주석이 든 전례가 「내접 판정이 세 군데로 갈려 원형 컨테이너 위반을 놓친 사고」다. penetrationOf 두 벌이 바로 그 다음 후보다.

작은 것들

MOVE_EPS = 0.01method 1·4·5·7 네 벌. 값이 같다는 사실 자체가 계약인데 그 계약을 적어 둔 자리가 없다.

unitComponents(연결 성분)는 topology.ts 에 같은 꼴이 있지만 module-private 이라 method 7 이 다시 짰다 — 주석이 그 사정을 적어 두고 있다. 내보내기만 하면 한 벌이 된다.

③ 흐름의 비대칭 — 축소 뒤에 스프레드가 다시 안 돈다

method 6 은 배율을 한 칸 줄일 때마다 앞의 다섯을 되부른다(options.relayout). 그런데 본판 합본이 넘기는 되부름 함수는 3+1+2+5+4 뿐이다 — 7·8 이 빠져 있다.

method3Method1Method2Method5Method4Method7Method8Method6.ts되부름의 범위
const relayout = (doc) => runMethod3Method1Method2Method5Method4(doc, base, outlines).doc; // ↑ 7·8 이 없다 — 축소 뒤 기하에서 스프레드를 다시 잡지 않는다 const shrunk = runMethod6(escaped.doc, { base, relayout, outlines });

의도된 것일 수 있다 — 되부름을 한 겹으로 묶어 「배율 위에 배율」을 막는 규율이 있고, 7·8 을 넣으면 되부름 비용이 다시 배가 된다. 다만 결과적으로 축소된 판에서는 스프레드가 최신이 아니다. betterOf 가 그 손해를 사후에 걸러내므로 안전하지만 최적은 아닌 구조다 — 적어 둘 값어치가 있는 트레이드다.

§6

복잡도 — 시간 · 공간 · 실제 빈도

기호 — N = 전체 노드 수, L = leaf 노드 수, U = 유닛 수, P = 겹친 쌍 수, k = 한 행/클러스터의 셀 수, g = 한 체인의 갭 수, M = method 6 의 배율 칸 수(≤ 10).

먼저 규모를 박아 둔다 — L 이 작다

코퍼스 1,177건을 직접 세면 leaf 노드 중앙값 12 · p90 26 · 최대 60이다. 그래서 O(L²)최대 3,600 쌍 비교, 중앙값 144 — 사실상 상수다. 비용을 만드는 것은 지수가 아니라 그 O(L²)를 몇 번 부르느냐다.

6.1 시간

단계한 번의 비용반복합계
freezeRelationsO(L²)1O(L²)
method 3O(L)1O(L)
method 1 ↔ 2O(L + g²)≤ 3 라운드O(L + g²)
method 5O(L²)유닛당O(U·L²) 최악
method 4O(L²)≤ 2020·O(L²)
method 7O(L²) + O(k log k)≤ 2020·O(L²)
method 8O(L²) + O(P·L)≤ 2020·O(L²)
method 6앞의 다섯 전체 + O(L²)≤ M ≈ 10M · (전체 체인)
betterOf2 × O(L²)1O(L²)

6.2 공간

지배 요인은 문서 복제다. 모든 후보가 structuredClone(input) 으로 시작하고 (run(doc): Doc 계약이 「입력을 건드리지 않는다」이므로), 되풀이마다 trial 을 한 벌 더 뜬다. 실측으로 doc 한 벌이 JSON 기준 중앙값 39.1KB · p90 83.2KB · 최대 174.1KB 다 (객체 그래프는 그보다 몇 배).

무엇공간실측 크기수명
문서 사본 structuredCloneO(N)39.1KB (중앙값)단계마다 1벌 + 시도마다 trial 1벌
absBoxMapO(N)노드당 상자 4수계산 1회분
overlapAreaMapO(P)쌍 중앙값 2 · 최대 28겹친 쌍만 담는다 — L² 가 아니다
freezeRelations 그래프O(N)유닛·띠·체인·프레임후보 1회 실행 내내
tight box mapO(텍스트 수)1.1KB (중앙값)케이스 적재 시부터
outline (도형 실루엣)O(도형 수)1.2KB · 최대 115KB모듈 적재 때 한 번 읽어 둔다
placeRow 정렬 사본O(k)셀 수십한 클러스터 처리 중
idleSpace 장애물 배열O(L)≤ 60질의 1회분
최대 동시 생존 — 합본의 끝
본판 합본은 laid.doc · spread.doc · escaped.doc · shrunk.doc · plain.doc동시에 들고 betterOf 로 들어간다 — 문서 대여섯 벌 + 지표 두 벌이 한 시점에 살아 있다. 중앙값이면 ~0.2MB, 최대 케이스면 ~1MB. 절대량은 작다.
진짜 부담은 할당 회전(churn)
최대 생존량이 아니라 만들고 버리는 횟수다. method 4·7·8 이 시도마다 trial 을 뜨고(각 ≤ 20), method 6 은 배율 칸마다 체인을 통째로 다시 돌린다 — 한 케이스에 doc 사본 수십 벌이 났다 사라진다. 뷰어가 케이스 수십 건을 한꺼번에 갈아 고를 때 화면이 잠깐 멈추는 것이 이 지점이다.

6.3 실제로 몇 번 타나 1,177건 전건 실행 · 2026-08-28

복잡도의 상한실제 비용은 다르다. 코퍼스 전건에 후보를 태워 각 후보의 리포트를 집계했다 — 발동(그 후보가 할 일을 찾음)과 실제 조치(문서를 실제로 바꿈)를 나눠 셌다.

후보단독 발동단독 조치합본 발동합본 조치최악 복잡도
method 321.2%17.6%19.8%17.6%O(L)
method 1 ↔ 266.7% / 73.7%53.3% / 46.4%100%51.6%O(L + g²)
method 520.4%13.2%16.0%8.9%O(U·L²)
method 473.9%39.2%44.5%32.5%20·O(L²)
method 764.3%34.2%21.7%7.7%20·O(L²)
method 864.3%2.8%18.3%2.3%20·O(L²)
method 696.9%38.1%43.2%22.3%M · 전체 체인
betterOf9.7%9.7%+1 × method 6

단독 = 후보 하나를 rendered.json 에 바로 태웠을 때. 합본 = 본판 3+1+2+5+4+7+8+6 안에서 그 패스가 한 일. 원안이 있는 1,177건 전건, 예외 0건. ⚠️ 단독 method 2 는 세 번째 트리거(collided)를 못 받는다 — 그 값은 합본이 만들어 넘기므로 단독 수치가 그만큼 낮게 잡힌다.

물음에 답하면 — 가장 복잡한 후보가 가장 비싼 후보가 아니다

method 5 는 최악 복잡도가 O(U·L²) 로 가장 나쁘지만, 합본에서 16.0%만 발동하고 8.9%만 조치한다 — 여덟 중 가장 드물게 일하는 축이다. 게다가 U ≤ L ≤ 60 이라 최악값도 21만 연산 남짓이다.

실제 비용을 지배하는 것은 method 6 이다 — 43.2%에서 발동하고, 발동하면 배율 칸마다 앞의 다섯을 통째로 되부른다. 여기에 betterOf 가 9.7%에서 그 method 6 을 한 벌 더 돌린다. 「복잡도 × 빈도」로 읽으면 순위가 뒤집힌다.

6.4 분업이 실측으로 확인된다

§4 에서 코드로 보인 배타적 분업이 숫자로도 나온다. 각 후보는 자기 몫이 아닌 쌍을 세어서 넘긴다 — 그 계량기를 집계한 값이다.

넘기는 쪽넘기는 것케이스비율
method 4유닛 겹침 → method 5 의 몫25321.5%
method 4가로 침투 → method 8 의 몫15713.3%
method 7가로 침투 → method 8 의 몫937.9%
method 7유닛 겹침 → method 5 의 몫17414.8%
method 8세로로 풀 쌍 → method 4·7 의 몫57248.6%

단독 실행에서 7·8 이 실제로 옮긴 케이스를 갈라 보면 — 7만 옮김 394건 · 8만 옮김 25건 · 둘 다 옮김 8건 · 아무도 안 옮김 750건. 「둘 다」 8건은 모순이 아니다 — 한 문서 안의 서로 다른 쌍을 각자 맡은 것이다. 분업은 케이스 단위가 아니라 쌍 단위다.

method 8 이 실제로 맡는 쌍은 중앙값 1쌍 · 최대 10쌍(75건에서만 대상이 생긴다). 가로 침투가 진짜인 겹침은 드물고, 그래서 method 8 의 조치율이 2.3%로 가장 낮다§5 에서 「7 이 4 를 포함한다」고 본 것과 달리, 8 은 대체 불가능하되 사정권이 좁은 특수 연산자다.

§7

선택 층 — betterOf 와 후보 선택기는 다른 것이다

답 — 결정론적 후보 선택기는 아직 코드에 없다

저장소를 훑어도 선택기 구현이 없다(candidateSelector·selectBest 류 0건). 설계안 C설계 문서(2026-08-27)로만 있고, 거기 적힌 채택 게이트가 「A·B 실측 이후」다.

구현된 것은 betterOf 뿐이고, 그것은 선택기가 아니라 회귀 가드다 — method 7·8 전용도 아니고, 전 후보 대상도 아니다.

betterOf — 구현됨결정론적 후보 선택기 — 설계만
무엇을 고르나두 갈래 — 스프레드 있음 / 없음후보 전부pool = METHODS (단독 8 + 합본 8)
어디서 도나스프레드 합본 3종에서만…+7+6 · …+8+6 · …+7+8+6후보 하나(key: 'select')로 등록해 드롭박스에 오르는 구조
비교 항목6개 — ⑤-b 쌍·면적 → ① 노드·면적 → ② 폰트 → ③ 정렬선7개 — 판정 라벨 랭크로 시작, ④ 유휴 차이와 총 변위·등록 순서로 종결
동률 처리기존 경로(스프레드 없는 쪽)를 남긴다총 변위 → 후보 등록 순서로 동률을 없앤다
비용method 6 을 한 번 더 (≈ 2배)후보 N배 실행
목적안 나빠지게 — 나비효과 방어더 좋아지게 — 케이스마다 최선 후보
그래서 지금 보장되는 것
스프레드 합본에서 케이스 단위로 절대 나빠지지 않는다. 단계 게이트는 자기 단계만 지키므로, 합본 끝에서 완성본끼리 견주는 것이 구멍 없는 방어다 (5629794 가 그 필요를 증명했다 — 7·8 자신은 게이트를 통과했는데 뒤의 method 6 이 다른 경로를 타면서 SUCCESS 가 FAIL 로 떨어졌다).
아직 안 되는 것
「케이스 X 에는 4→7 이, 케이스 Y 에는 7→4 가 낫다」는 고정 순서 합본으로 담을 수 없다. 그 조합 한계를 푸는 것이 선택기의 몫이고, 설계안은 구현 전에 베이스라인 JSON 으로 상한을 먼저 재는 공짜 분석을 채택 게이트로 걸어 두었다.

정리하면 — 선택 층은 지금 「두 갈래 · 스프레드 합본 한정」까지만 와 있다. 전 후보를 대상으로 하는 층은 설계로만 존재하고, 그 설계에서 method 7·8 은 선택 대상의 일부일 뿐이다.

§8

유휴 여백 지도 — 「어디가」 비었나

지표 ④ idleMarginRatio 는 「여백이 얼마나 남았나」만 답하는 페이지 전역 스칼라였고, 후보들이 쓰는 여유는 전부 국소적이었다 — neighbors.ts 는 좌·우 이웃의 한계선, gapAlgebra.ts 는 한 체인의 1차원 갭. 「어디에 남았나」를 답하는 자리가 없어서 겹침을 옮겨 풀 후보가 밀 자리를 찾지 못했다.

obstaclesFor — 이동을 막는 것 장애물 장식 — 제외 페이지 배경판 · 자기 유닛 — 제외 leaf 만 본다. 겹침 판정(⑤)이 안 세는 것을 장애물로 세면 풀 수 있는 것을 못 푼다 directionalSlack — 4방향 여유 left right up down 그 방향으로 이만큼 옮겨도 장애물·경계에 안 닿는다 — method 8 이 쓴다 verticalCorridor — 세로 빈 구간 코리도 = x 교집합 밴드의 빈 세로 구간 클러스터를 통째로 placeRow 에 앉힌다 — 겹치지 않으면서 가장 덜 움직이는 배치
왜 격자(비트맵)가 아니라 상자 스윕인가 — ④의 4px 격자를 재활용하는 안도 있었지만, 격자는 「전체 점유율」에는 맞아도 방향 질의에는 O(격자) 스캔과 셀 경계 오차가 붙는다. 문서의 장애물은 수십 개 수준이라 상자 스윕이 더 단순하고 정확하다.
relayout-core/idleSpace.tsIDLE_SPREAD_TRIGGER
base(원안) 존재 신규 겹침 ≥ 1쌍 idleMarginRatio ≥ τ // τ 기본 0.5

넘침은 조건이 아니다. 이것이 v0.5 와의 가장 큰 차이다 — 기존 후보들은 「텍스트가 넘쳤을 때」 발동했고, 그래서 넘침 없이 겹침만 남은 케이스에는 아무도 손대지 않았다. 발동에 「신규 겹침 존재」가 결합돼 있어 원안부터 여백이 큰 미니멀 디자인을 멀쩡한데 건드리는 일은 없다 — τ 는 예산의 하한이지 목표가 아니다.

§9

자 — 무엇으로 재느냐가 결과를 바꿨다

v0.6 에서 가장 값진 발견은 새 연산자가 아니라 「자(판정 박스)의 불일치」였다. 후보 안쪽에서는 엔진이 잰 값을 못 얻으므로 frameBoxesAsTight노드 박스를 그 자리에 끼워 쓴다. 그런데 성적표는 실측 tight 로 잰다 — 두 자가 다르면 후보가 유령 겹침을 쫓는다.

무엇누가 보나
frameBoxesAsTight노드 박스를 tight 자리에 끼운 프레임 대용. 오토핏 여백만큼 과대 보고하지만 모든 후보가 같은 기준을 본다후보 안쪽(기본)
carryTightBoxes케이스의 rendered.json + 그 tight 를 승계해 시행 문서에 옮긴다 — 채점과 같은 자7·8 (tight 를 받았을 때) · betterOf
visualBox.ts엔진이 잰 tight + 글머리 기호 몫 = visual bounding box성적표(채점)

배관 하나가 회귀를 막고 사정권을 넓혔다

replaceLayout 이 케이스의 실측 tight 와 원안 tight 를 후보까지 넘기도록 (RelayoutRunContext.tight / baseTight) 배관을 깔자, method 7·8 의 트리거·게이트가 성적표와 같은 자를 보게 됐다. 이 배관이 회귀만 막은 게 아니다 — 기준 쪽 겹침 부풀림에 가려 안 보이던 신규 쌍들이 드러나 새로 풀린 케이스가 생겼다.

아직 남은 불일치

method 6(·4·5)은 여전히 프레임 대용 자로 발동을 판정한다. §5 의 중복 정리와 함께 묶어 보면, 「자를 어디서 정할 것인가」가 아직 한 곳에 모여 있지 않다tight 를 받는 후보와 안 받는 후보가 섞여 있고, 그 차이가 §5 ③ 의 되부름 비대칭과 같은 뿌리다.

§10

실측 — 숫자에는 날짜가 붙는다

코퍼스는 살아 있다 — 절대값을 믿기 전에 날짜를 본다

아래 두 성적표는 각각 다른 시점·다른 건수이고, 지금 코퍼스는 그 어느 쪽보다도 크다 — 그대로 견주면 안 된다.

지금 코퍼스 2026-08-28 직접 셈

케이스 (rendered.json)
1,177
tight · outline 구운 케이스
1,177 전건
정답(expected) 있는 케이스
484
before 판정 (라벨 2026-08-27)
F 1,166 / A 11 · S 0

eval-out/cases/*/case.json 을 직접 세어 얻은 값이다. before(대치 직후)의 FAIL 사유는 신규 겹침 843 · 넘침 709 · 유휴 11 로 갈린다(한 케이스가 여러 사유를 가진다).

후보 성적표 111건 · 2026-08-26 · 낡음

넘침 노드넘침 면적게이트 PASS폰트 배율정렬선
original (기준)451,086,451기준1.0001.000
before1394,107,47034/1111.0000.932
합본 3+1+214534,45443/1111.0000.895
합본 3+1+2+414534,45471/1111.0000.885
합본 3+1+2+5+414534,45483/1111.0000.864
합본 3+1+2+5+4+611368,34597/1110.9820.861
expected (목표선)39699,72960/900.9930.849

읽는 법 — 넘침은 3+1+2 에서 87% 가 사라진다. 거기서 게이트가 43/111 밖에 안 되는 것은 넘침과 겹침이 다른 문제이기 때문이다. +4 가 유닛 사이를, +5+4 가 유닛 안까지 푼다 — 그 셋은 넘침이 한 자리도 안 다르다(「넘침이 늘면 되돌린다」가 게이트라서). 마지막 +6글자를 줄이므로 넘침까지 지우지만, 대가로 SUCCESS 는 한 건도 안 는다.

method 7·8 의 기여 96건 · 2026-08-27 · 낡음

후보SAF⑤-b 쌍정렬선
현행 최선 3+1+2+5+4+6502917590.8540
+ method 7 (A)542913460.8486
+ method 8 (B)512916550.8498
+ 7 + 8 (A+B)552912420.8444

FAIL 17 → 12(−29%) · 신규 겹침 59 → 42쌍(−29%) · 나빠진 케이스 0건. §4 의 분업이 실측으로 확인된다 — 5628689 는 B 만이, 5611182·5694141·5698092·6377108 은 A 가 풀었고, A+B 는 정확히 둘의 합집합이다. 겹치는 일을 했다면 합집합보다 작았을 것이다. 자세한 설계·평가는 유휴 여백 재배치 설계 에 있다.

§11

코드 지도

아래 링크는 vscode://file/… 편집기 링크다 — 이 커밋(aee07d0b)이 원격에 없어 GitHub permalink 가 서지 않는다.

무엇파일여기서 확인한 것
레지스트리 · 진입점relayout-methods/index.ts:69후보 16개 · replaceLayout 의 6개 인자
method 4relayout-methods/method4.ts:152MAX_PUSHES = 20 · 2단계 대칭 절반 밀기
method 5relayout-methods/method5.ts:150유닛 안 · MOVE_EPS 사본
method 6relayout-methods/method6.ts:317SHRINK_STEP = 0.05 · relayout(trial) 되부름
method 7relayout-methods/method7.ts:227penetrationOf 원본 · MAX_NUDGES = 20 · 비대칭 짝 밀기
method 8relayout-methods/method8.ts:202penetrationOf 사본 · MAX_MOVES = 20
회귀 가드relayout-methods/betterOf.ts:436항목 사전순 · 동률 → 기존 경로
본판 합본relayout-methods/method3Method1Method2Method5Method4Method7Method8Method6.ts:55되부름에 7·8 이 빠진 자리(§5 ③)
위상relayout-core/topology.ts:57유닛 · 띠 · 체인 · 프레임 · PIN_WEIGHT = 100
유휴 여백 지도relayout-core/idleSpace.ts:57IDLE_SPREAD_TRIGGER = 0.5 · 세 질의
겹침·지표relayout-core/metrics.ts:95OVERLAP_EPS = 100 — 후보 로컬의 1 과 같은 이름
정확해 부품relayout-core/placeRow.ts:112정렬 후 1패스 — O(k log k)
여백 대수relayout-core/gapAlgebra.ts:85water-filling guard ≤ orig.length
판정relayout-core/verdict.ts:108EXCESSIVE_FONT_SCALE = 0.7 · OVERFLOW_AREA_TOL = 0.1
쌍짓기relayout-core/pairing.ts:33penetrationOf 를 올릴 자리 후보(§5 ②)

다시 재는 법

miricanvas-iui 루트에서파일을 만들지 않는다
# 규칙 검사 전건 pnpm exec vitest run src/post-replace-relayout # 지표 표를 터미널에 — 후보 전부 + tidy pnpm exec tsx scripts/relayout-metrics.ts # 후보 하나만 전 케이스에 — 케이스별 판정과 게이트 pnpm exec tsx scripts/relayout-method-run.ts --method method3+method1+method2+method5+method4+method7+method8+method6 # 케이스를 새로 넣었을 때 굽는 둘 pnpm exec tsx scripts/tight-box-cases.ts --only <id> pnpm exec tsx scripts/outline-cases.ts --only <id>

절대값을 테스트에 못박지 않는다

코퍼스가 늘고 줄 때마다 깨질 뿐 후보에 대해 아무것도 말하지 않는다. 못박는 것은 앞 합본과의 차이(방향과 크기)와 남는 케이스의 목록이다.