iui 5d807e2dwiki 013fff13Frame v3핵심 회귀 470개 · 재현 9개
§0
수업 지도: 세 종류의 fallback을 구분하자
comp-frame은 “내 글을 이미 있는 디자인의 어느 칸에 넣을까?”를 계산합니다. 예를 들어 세 단계의 설명을 담으려면, 세 장의 카드가 있고 각 카드에 제목과 본문을 넣을 수 있는 디자인이 필요합니다. 코드는 먼저 칸 배정을 연습하고, 사용할 수 있는 후보를 추린 뒤 순위를 매깁니다.
① 입력 읽기YAML/JSON → 규약 검사
② 표현 정하기중첩 N 단계 → 카테고리 Tier
③ 후보 찾기Query Frame → Stage 0·1·2
④ 대치 연습글 배정 → 탈락 검사 → 벌점
⑤ 결과 만들기순위·중복 제거 → binding
⑥ 화면 확인실제 디자인 로딩·대치·렌더
이름
무엇을 바꾸나
짧은 비유
Tier 1·2·3
컴포넌트의 카테고리. 각 Tier 안에 후보 종류가 있다.
계단 그림이 없으면 목록 디자인도 찾는다.
Stage 0·1·2
같은 카테고리 안의 슬롯 역할 차이.
제목+본문에 수치 칸까지 있는 디자인도 살핀다.
N1~N5
입력의 중첩 컴포넌트 구조와 각 노드의 카테고리.
큰 상자 안 작은 상자들을 유지할지, 꺼낼지, 글로 풀지 정한다.
이번 수업에서 도달할 목표
“반복이 이상하다”에서 끝내지 않고 ① 무엇을 반복하고 ② 어떤 값이 변하며 ③ 어디서 멈추고 ④ 어떤 후보를 놓치는지를 설명해 봅시다. 해결된 문제, 재현된 결함, 의도된 절충을 구분합니다.
§1
입력과 출력: 글을 칸에 넣는 문제
입력은 VisualSummary 문서입니다. title은 문서 제목, components는 디자인으로 표현할 내용 덩어리들의 목록입니다. 화면은 YAML과 JSON을 받으며 단일 Component도 문서로 감싸서 처리합니다. 전체 문서는 version과 올바른 결합 type 등 규약 검사를 통과해야 합니다.
여기에는 컴포넌트 1개, 항목 3개, 항목마다 제목 1개와 본문 1개가 있습니다. 컴포넌트 3개가 아니라, 하나의 컴포넌트 안에 카드 3개가 필요한 것입니다.
출력
뜻
예
FrameSearchResult[]
후보별 디자인 주소와 평가 결과
do_id, node_id, tier, cost
binding
어떤 시트 노드에 어떤 글을 넣는지
sheet_id: title-1 → “견적 제출”
blank / preserve
입력 글이 배정되지 않은 슬롯의 처리 분류
비울 대상으로 분류 / 원문 보존
projection / matched_category
어떤 카테고리·변환으로 이 결과를 얻었는지
Steps → List, P1·P2
meta
조회와 탈락을 추적할 수치
조회 행 수, 용량·내용 배제, 오류
검색 결과는 아직 완성된 그림이 아닙니다. 화면이 이 결과를 받아 원본 디자인을 로딩하고 binding의 글을 넣어 썸네일을 만듭니다. 그래서 “검색 통과”와 “실제 이미지에서 읽기 좋음”은 별도로 확인해야 합니다.
L1·L2·L3와 문서 중복 제거
L1은 단일 컴포넌트, L2는 최상위 컴포넌트 여럿의 병치, L3는 entity.components[] 안에 다른 컴포넌트가 있는 중첩입니다. 프리셋의 여러 레벨이 정규화 후 같은 문서이면 검색은 한 번만 하고 L2·L3처럼 배지를 합칩니다. 직접 입력은 별도 레벨 하나로 취급합니다.
최상위 컴포넌트끼리는 독립적으로 검색하며 기본 동시 실행 수는 2입니다. 현재 flattenComponents는 보기·주소 추적에도 쓰이지만, 실제 L3 검색은 먼저 searchNestedComponent를 거칩니다. 처음부터 모든 자식을 독립 검색한다는 옛 설명은 현재 실행 경로와 다릅니다.
Frame은 “칸을 기록한 표”라고 생각하면 됩니다. 입력 쪽은 각 칸에 넣을 글을 적고, 디자인 쪽은 그 글을 받을 실제 노드 주소와 용량을 적습니다. 같은 모양의 표를 쓰기 때문에 서로 비교할 수 있습니다.
단위
입력 쪽
디자인 쪽
Frame
컴포넌트 전체의 요구
디자인 전체 또는 선택된 경계 하나
Unit
항목 하나: 견적 제출
반복 카드 하나
Slot
제목·수치·본문에 들어갈 글
실제 Text 노드, 용량, 별칭
NodeRef
보통 없음
rlscId: 구조 주소, sheetId: 실제 시트 주소
Cap
글 길이로 수요 계산
charsPerLine, lines, chars
satellites
페이지 제목
항목 바깥 제목·부제·설명 슬롯
dataset
표를 채울 문자열 등
표·차트의 grid 노드
nested / isForest
중첩 경계와 묶음 요구
대응하는 중첩 경계의 Frame
params / relations / decoration
방향·관계·장식 선호
후보가 가진 속성·관계·장식
입력 Frame은 그때 만들고, 디자인 Frame은 미리 만들어 둡니다
buildQueryFrames는 검색할 때 입력을 복제하고 관계 ID를 해석한 뒤 카테고리별 Frame을 만듭니다. 관계가 존재하지 않는 항목을 가리키면 실패를 돌려줍니다. buildIisFrame은 I.I.S.가 인식한 항목·필드를 실제 RLSC 노드에 연결하고, 적절한 I.I.S. 구조가 없으면 로더가 buildRuleFrame으로 반복 모양을 추정합니다.
디자인 Frame을 만들 때의 보정
I.I.S.가 반복 항목을 빠뜨렸고 규칙 검출과 모순되지 않으면 누락 항목을 보완합니다. 서로 충돌하면 억지로 합치지 않고 unitMismatch로 기록합니다. 위성 제목은 반복 항목 밖의 헤더에서 찾습니다. 그림 같은 비텍스트 말단은 텍스트 슬롯에서 제외하고, 컨테이너를 가리키면 하위 Text로 펼칩니다. 표 셀의 글은 표 전체 대치와 중복해서 배정하지 않습니다.
최신 기준은 Frame 버전 3
버전 2에서 중첩·장식 정보가 추가됐고, 현재 FRAME_VERSION = 3에는 슬롯의 aliases가 있습니다. 예를 들어 같은 물리적 칸을 한 카테고리는 value, 다른 카테고리는 emphasis라고 부를 수 있습니다. 한 노드에 두 번 대치하지 않도록 대표 슬롯 하나와 별칭을 저장합니다. 별칭은 단수 필드끼리만 허용하고 items/values에는 붙이지 않습니다.
DB 테이블 이름 comp_frames와 Frame 직렬화 버전 3은 서로 다른 이름입니다. 코드가 3이라고 기존 DB 행까지 모두 3으로 갱신됐다고 단정할 수는 없습니다.
단계 설명을 Steps로 표현하고 싶어도, 공급된 디자인 중 적당한 Steps가 없을 수 있습니다. 이때 규약이 허용하는 다른 카테고리를 차례로 찾습니다. 카테고리가 내용을 받아들일 수 있는지는 establish, 실제 디자인에 글이 들어가는지는 나중의 배정·용량 검사가 판단합니다.
Tier
카테고리 선택
주의
1
요청한 type. 성립 조건을 통과한 것.
화면 규약은 결합 type을 요구한다. alternatives는 검색에서 사용하지 않는다.
2
원래 요청 카테고리의 fallback 행을 왼쪽부터. 각각 투영 후 성립 검사.
A→B→C라고 표시해도 C는 원본 A에서 변환한다. B의 fallback 행으로 갈아타지 않는다.
3
항목 수·문장 수로 정한 종단 하나. 앞 Tier와 중복이면 생략.
List / LongFormTextLayout / HighlightedMessage를 전부 조회하는 것이 아니다.
예를 들어 Steps의 행은 Timeline → Staircase → List입니다. 입력이 17항목이면 상한 15인 Timeline과 상한 8인 Staircase는 성립하지 않아 건너뜁니다. List가 이미 Tier 2에 들어갔다면 Tier 3에서 또 넣지 않습니다.
Tier 3는 어떻게 하나를 고르나
terminalCategory는 measure를 텍스트 항목으로 구체화한 뒤 개수를 셉니다. 항목이 1개가 아니면 List, 1개이고 본문이 여러 문장이면 LongFormTextLayout, 그 외에는 HighlightedMessage를 고릅니다. 그 다음 buildTiersFor가 성립 여부를 별도로 확인합니다. 빈 입력에 결과를 만들어 준다는 뜻은 아닙니다. 문장 구분에는 줄바꿈·문장부호를 사용하며 이 구체적 구분법은 구현 선택입니다.
투영 P1~P7은 “다른 종류에서도 읽히게 내용의 자리를 바꾸기”입니다
규칙
쉬운 설명
P1
도착 카테고리가 읽는 공통 내용을 유지한다.
P2
도착지가 사용하지 않는 표현 옵션 params는 제거한다.
P3
도착지가 못 읽는 내용 필드는 items로 옮긴다. 지원하지 않는 자식 항목도 글로 푼다.
P4
표의 각 행을 항목으로 바꾼다. 첫 열 값은 label, 나머지 열은 “열 제목: 값·단위”로 옮긴다.
P5
measure를 못 읽는 곳에서는 숫자·단위를 텍스트 항목으로 옮긴다.
P6
항목을 표의 행으로 만든다. 앞서 옮긴 본문도 표 안에 포함한다.
P7
도착지가 표현하지 못하는 관계의 desc를 첫 참가자 본문으로 옮긴다.
P8 폐기와 “접기”를 혼동하지 않기
최상위 항목을 상한에 맞게 합치는 P8은 폐기됐습니다. 17항목을 15항목으로 합쳐 Timeline을 성립시키지 않습니다. 디자인의 최상위 Unit 수도 입력 이상이어야 합니다. 반면 한 항목의 문장을 마지막 본문 칸에 합치거나, 자식 내용을 본문으로 푸는 처리는 남아 있습니다. 이 중 배정 단계의 자식 접기에 남은 손실은 문제 F05에서 다룹니다.
현재 보정은 summary-projection.ts와 summary-content.ts에서 하며 벤더 eregion 코드는 그대로 둡니다. 현재 관계 변환은 desc만 남깁니다. “관계 종류·참가자도 함께 남긴다”는 일부 과거 문서와 주석은 최신 relationLines 본문과 다릅니다. 표 첫 열의 제목도 P4 텍스트 변환에는 남지 않습니다. 따라서 투영은 모든 의미를 무손실 보장하는 변환이 아닙니다.
기존 규약 문구의 충돌도 남겨 둡니다
고정 wiki의 schema-v0.5.md 678행에는 Gauge→HighlightedMessage에서 P8로 접는다는 옛 예시가 남아 있습니다. 그러나 명시적인 P8 폐기 결정과 현재 구현은 상한 초과 후보를 접어 성립시키지 않습니다. 입력의 카테고리 성립 상한과, 이미 존재하는 디자인을 인식하는 범위도 구분해야 합니다.
searchFrames 안에는 Tier → 카테고리 → Stage → 페이지 순서로 반복이 겹쳐 있습니다. “반복문이 있다”는 사실보다, 각 반복이 어떤 일을 하고 무엇을 세는지가 중요합니다.
Tier 1 → 2 → 3
그 Tier의 카테고리를 왼쪽부터
Stage 0 → 1 → 2
현재 조건의 다음 페이지
가져오기 → Query Frame 연결 → 배정·검사 → 통과한 행의 주소를 Set에 추가
값
기본/정의
어디에 쓰나
min_candidates
500
검색 전체의 통과 행 목표. 보장된 반환 개수가 아니다.
tier_targets
[50, 50]
Tier 처리가 끝난 뒤 누적 통과 수가 문턱 이상이면 하위 Tier를 생략한다.
top_n
50
최종 순위·Frame 중복 제거 후 반환 상한.
admittedCount
통과한 do_id|node_id의 수
반복의 중단 판단. Frame fingerprint 중복은 아직 포함한다.
scannedForCategory
현재 카테고리에서 읽은 행 수
실패가 계속돼도 조회량이 끝없이 늘지 않게 제한한다.
CATEGORY_SCAN_MULTIPLIER
4
카테고리의 배정량 ×4까지만 훑는다. 실제 공급 수보다 많이 읽지는 않는다.
FETCH_PAGE_SIZE
1,000
한 번에 요청할 최대 행 수.
한 페이지를 가져올 때 실제로 하는 일
각 카테고리에서 Stage 0~2의 합집합을 count()로 센다. 이것은 정밀 검사 전 공급 수다.
카테고리를 순서대로 처리한다. 현재 코드는 균등 배분하지 않는다. Tier 시작 때의 남은 목표로 카테고리 배정량을 정한다.
페이지 요청량은 min(남은 스캔 예산, 남은 통과 목표, 1000)이다. 가져온 만큼 offset과 스캔 수를 증가시킨다.
행 주소로 중복을 제거하고 그 행을 통과시킨 QueryFrame을 matchedQueries에 기록한다. 한 행이 여러 카테고리에 있어도, 실제로 통과한 질의로 채점한다.
그 페이지의 아직 승인되지 않은 행을 rankCandidates(..., dedup:false)로 검사한다. 배정·내용·용량·속성 조건을 통과한 행 주소만 admittedKeys에 넣는다.
전부 탈락했다면 다음 페이지 또는 Stage·카테고리·Tier로 계속 간다. 마지막에는 모은 전체 행을 다시 평가하고 정렬·fingerprint 중복 제거·top_n 절단을 한다.
조건
어디서 나가나
의미
누적 통과 수 ≥ min_candidates
다음 Stage·카테고리·Tier를 멈춤
목표만큼 승인했다.
카테고리 스캔 수 ≥ scanBudget
그 카테고리의 Stage 반복 종료
이 카테고리에 쓸 조회량을 다 썼다.
페이지가 요청보다 적게 옴
현재 Stage의 페이지 반복만 종료
그 조건의 공급이 끝났다. 다음 Stage는 볼 수 있다.
Tier 처리 후 누적 통과 수 ≥ 해당 문턱
Tier 반복 전체 종료
최대 500 목표를 못 채워도 충분하다고 판단한다.
모든 Tier·카테고리·Stage 소진
최종 평가로 이동
0개도 정상 결과일 수 있다.
옛 “미보충” 문제는 해결됐습니다
과거에는 Tier 1에서 50행을 읽었다는 이유로 중단하고, 나중에 전부 탈락해도 Tier 2를 보지 않았습니다. 현재는 그 50행이 모두 탈락하면 통과 수가 0이므로 계속 찾습니다. 다만 스캔 상한, fingerprint 중복 제거, 화면 사후 필터 때문에 결과가 부족해지는 다른 경로는 남아 있습니다.
작은 사례를 한 단계씩 따라가기
분기 이해용 고정 시나리오입니다. F01~F03은 현재 검색 함수를 직접 호출한 재현과 대응합니다. 실제 DB 요청을 실행하지 않습니다.
큰 Steps 상자 안에 작은 BigNumber 카드가 있다고 합시다. 처음부터 두 개를 따로 찾으면 “수치가 어느 단계에 속하는지”라는 배치 정보가 사라질 수 있습니다. 그래서 현재는 전체 중첩을 지킬 수 있는지부터 확인합니다.
단계
실제 동작
다음으로 넘어가는 조건
N1 구조·종류 그대로
전체 트리 또는 형제 forest를 그대로 검색. exactType:true로 카테고리 Tier 1만 사용.
반환 후보가 0개일 때 N2.
N2 구조 고정, 종류 변경
노드마다 원래 fallback 행의 변형을 만든다. 성립하고 중첩 topology를 유지하는 조합만 시도.
첫 성공 조합에서 반환. 전부 실패한 뒤에만 부모 벗기기.
N3 부모 한 층 벗기기
부모의 내용을 첫 승격 자식에 옮기고, 연속 형제를 큰 묶음부터 검색. 묶음마다 N1/N2를 다시 시도.
선택한 분할의 모든 조각에 후보가 있어야 성공.
N4 다시 벗기기
단일 조각이 아직 중첩이면 같은 절차를 재귀로 반복.
성공한 조각 반환 또는 그 조각의 N5.
N5 내용 평탄화
항목·하위 내용을 들여쓴 글로 만들고 종단 카테고리로 검색.
검색 결과를 그대로 반환. 0개도 가능.
여기서 exactType은 “Stage도 0만”이라는 뜻이 아닙니다. 카테고리 Tier만 1로 제한하며, 같은 카테고리 안의 Stage 0·1·2와 용량·내용 검사는 여전히 실행됩니다. 또한 N 단계는 트리 전체가 한 번에 N1→N2→N3→N4→N5로 한 줄 이동하는 모습만 있는 것이 아닙니다. 벗겨진 한 조각이 다시 N1/N2로 성공하거나, 다른 조각보다 먼저 N5에 도달할 수 있습니다.
N2의 조합 순서를 손으로 써 보기
부모와 자식에 원형 0, 첫 대안 1, 둘째 대안 2가 있다고 합시다. N1은 (0,0)입니다. N2는 강등 칸수의 합이 작은 순서이며, 동점에서는 더 깊은 노드를 먼저 내립니다.
(부모 0, 자식 1)(1, 0)(0, 2)(1, 1)(2, 0)…
중간 카테고리가 성립하지 않아 빠져도 원래 행에서의 칸 거리는 유지합니다. 각 조합의 투영은 원본 노드에서 출발합니다. 바뀐 카테고리의 fallback을 다시 따라가며 무한히 돌아가는 방식이 아닙니다.
왜 유한한 반복도 오래 걸릴까?
624
모든 노드의 선택지 수가 같다고 가정한 설명용 상한: mⁿ−1. 실제로는 노드별 선택지 수의 곱−1이며 성립·topology 검사와 조기 성공으로 줄어듭니다. N3/N4에서 다시 시도하는 묶음은 이 수에 포함하지 않았습니다.
부모를 벗길 때도 규칙이 있습니다
부모 내용 이전(N-R1): 부모의 label·items·관계 desc 등 텍스트화한 내용을 첫 자식의 첫 entity.items 뒤에 붙입니다. 부모를 제거하는 것과 그 글을 지우는 것은 다릅니다.
연속 형제 묶음(N-R2): 4형제는 [4] → [3,1] → [2,2] → [2,1,1] → [1,1,1,1] 순서입니다. [1,3] 같은 모든 순서 있는 분할을 열거하는 알고리즘이 아닙니다. 이 비대칭은 규약에 정해진 선택입니다.
Hierarchy 예외(N-R5): 현재 부모의 대분류가 Hierarchy면 N1/N2 실패 후 N3/N4를 건너뛰어 N5로 갑니다. 소속 계층이 사라지는 일을 피하려는 규칙입니다.
뿌리 없는 묶음: Unclassified를 검색 type으로 만들지 않습니다. forest의 바깥 Frame은 카테고리 없는 경계이며 내부 카테고리로 후보를 찾습니다.
종료·캐시·취소를 정확히 설명하기
semanticVariants는 모든 조합을 메모리에 쌓지 않고 하나씩 생성합니다. 총 강등 합은 유한한 suffixMax까지 증가하고, 조합 재귀의 position도 증가하므로 유한한 입력에서 끝납니다. N3/N4는 부모를 벗겨 중첩 깊이가 줄고, 형제 분할 재귀는 남은 형제 수가 줄어듭니다. N5는 검색 결과를 돌려주고 끝납니다.
이는 각 검색 요청이 결국 응답하거나 취소된다는 전제의 종료 설명입니다. 응답하지 않는 외부 요청까지 이 함수가 해결하지는 않습니다. N2에는 현재 216회·전체 600회 같은 횟수 제한이 없습니다. 16ms마다 이벤트 루프에 제어권을 돌려 취소 클릭을 처리할 수 있게 하지만, 탐색량을 줄이는 장치는 아닙니다.
attempt는 동일 forest와 exactType의 일반 시도를 캐시합니다. N2의 개별 변형은 조합 수만큼 메모리를 먹지 않도록 캐시하지 않습니다. 대신 preservedCache가 원래 형제 묶음의 N1/N2 전체 검색 결과를 재사용합니다. 캐시는 최상위 컴포넌트 검색 한 번의 범위입니다.
“N5는 성립한다”와 “DB에 후보가 있다”는 별개
평탄화한 내용을 표현할 카테고리를 정할 수 있어도, 필요한 칸·용량을 가진 실제 디자인이 없으면 결과는 비어 있습니다. 최신 코드의 조건은 results.length > 0입니다. 규약의 “빈 결과가 나오지 않는다”는 문장을 실제 공급 보장으로 읽으면 디버깅을 잘못하게 됩니다.
align은 첫 항목을 첫 카드에, 둘째 항목을 둘째 카드에 배정합니다. 모든 배치 조합을 탐색해 최적의 자리를 찾는 함수는 아닙니다. 같은 이름 → 별칭 → 다른 텍스트 칸으로 이동 순서로 글을 넣고, 그 결과를 costOf가 검사합니다.
상황
현재 동작
같은 역할이 있음
직접 배정한다. one은 단일 칸, many는 문서 순서의 여러 칸이다.
같은 이름은 없고 별칭이 있음
이미 채워지지 않은 단수 슬롯의 aliases를 찾는다. 성공한 필드를 기록해 뒤의 이동 단계가 또 넣지 않게 한다. marker는 별칭 받는 칸에서 제외한다.
전용 필드가 없음
본문 items로 옮기거나 label, 그 밖의 단수 텍스트 슬롯을 사용한다. 역할이 바뀐 이동을 기록한다.
본문·값 목록이 슬롯보다 많음
초과 내용을 마지막 칸에 줄바꿈 등으로 이어 붙인다. 합친 뒤 길이로 용량을 검사한다.
디자인에 최상위 카드가 더 많음
초과 카드를 blank로 분류한다. 입력보다 카드가 적으면 앞 필터·rank에서 배제한다.
Unit.children 칸이 부족함
초과 자식을 부모 본문으로 접는다. 이 경로는 P3/N5와 다른 함수이며, 필드 누락이 남아 있다.
옮길 텍스트 슬롯이 전혀 없음
droppedTexts와 contentLoss에 기록한다. 현재 rank는 contentLoss>0 후보를 배제한다.
번호 칸이 있음
순서형 카테고리의 marker에 01, 02…를 넣는다.
입력 없는 칸이 남음
원문 지도와 짧은 토큰 보존 규칙에 해당하면 preserve, 나머지는 blank. 위성 제목·부제·설명은 별도 규칙이다.
제목 슬롯이 있으면 페이지 제목을 넣습니다. 제목 입력이 없으면 원래 제목을 보존하고, 부제·설명 위성은 보존합니다. 제목을 받을 슬롯이 없으면 titleDropped 벌점 1이며 contentLoss와는 다른 판정입니다. 화면의 경계 밖 제목 구제는 다음 절에서 따로 설명합니다.
용량은 “합친 글이 몇 줄인가?”로 검사합니다
한 줄 20자 · 최대 3줄 · 총 60자
·
줄바꿈 없는 73자는 한 줄 20자 기준으로 4줄입니다. 1.0배이면 4>3 또는 73>60이므로 탈락합니다. 1.5배에서는 4≤4.5이고 73≤90이므로 통과하며 용량 벌점 약 5.01점을 받습니다. 허용 배수는 검사 기준이며 실제 폰트 크기를 바꾸지 않습니다.
수식과 검사의 한계
예상 줄 수 = 명시적 각 줄에 대해 max(1, ceil(UTF-16 길이 / 한 줄 용량))의 합
탈락 = 예상 줄 수 > cap.lines × m 또는 글자 수 > cap.chars × m
줄 벌점 = 4 × Σ sqrt(max(0, 예상 줄 수 − cap.lines))
글자 벌점 = 2 × Σ clamp((글자 수 / cap.chars − 1) / 0.43, 0, 1)
기본 1.0배에서 정상 용량을 가진 통과 후보는 넘침 벌점이 0입니다. 완화했을 때 넘치는 후보끼리 순위를 나누는 데 이 벌점이 쓰입니다. 길이는 UTF-16 단위이므로 이모지·실제 글꼴 폭·실제 줄바꿈을 완벽히 재현하지 않습니다.
용량이 없거나 charsPerLine·lines·chars 중 하나라도 0 이하이면 하드 용량 검사를 건너뜁니다. capUnknown 벌점은 완전히 같은 조건이 아니라 cap 자체가 없거나 charsPerLine≤0인 경우를 셉니다. 표·차트 grid는 텍스트 용량 모델에서 제외합니다. 실제 겹침·잘림·표 셀 가독성은 이 검사에 포함되지 않습니다.
벌점은 발생량 × 가중치를 더한 값이며 낮을수록 좋습니다. 빈 카드 1개가 10점, 다른 역할로 옮긴 필드 2건이 6점이면 두 항목의 합은 16점입니다. 그렇다고 최종 순위가 항상 총점 하나로 결정되지는 않습니다.
현재 항목
기본 가중치
무엇을 세나
빈 항목 unitBlank
15
비운 최상위·자식 Unit 수.
자리 이동 fieldMove
3
다른 역할로 옮긴 필드 건수. 별칭으로 직접 배정된 것은 이동으로 세지 않는다.
본문·값 초과 itemOverflow
2
many 목록의 준비된 칸 수를 넘은 항목 수.
빈 본문·값 칸 itemBlank
15
many 목록에 남은 칸 수.
줄 넘침 capOverflow
4
각 칸의 초과 줄 수 제곱근을 합산. 1줄 초과 4점, 4줄 초과 8점.
글자 넘침 capChars
2
용량 100%까지 0, 100~143% 비례 증가, 143% 이상 칸당 2점.
용량 미상 capUnknown
2
cap이 없거나 charsPerLine≤0인 배정 텍스트 칸 수. grid 제외.
관계 누락 relationGap
3
후보에 없는 관계 유형을 요구한 관계 수. 연결 대상·전체 연결 모양의 동일성을 증명하지 않는다.
옵션 충돌 paramConflict
5
정책 처리 후 남은 속성 충돌 수. 후보에 속성 자체가 없으면 비교 불가로 충돌을 세지 않을 수 있다.
구조 출처 provenance
0
규칙 Frame 또는 unitMismatch인 I.I.S. Frame이면 원시값 1. 기본 가중치에서는 영향 없음.
제목 탈락 titleDropped
1
페이지 제목을 위성 슬롯에 넣지 못함. 화면이 나중에 구제해도 검색 비용은 그대로.
추가 표·차트 extraGrids
10
대표 grid 외 추가 grid 수. 앞의 하드 비트 필터에서 먼저 제외될 수도 있다.
내용 손실 contentLoss
10
비용 정의는 남아 있지만 일반 rank 결과에서는 contentLoss>0을 배제하므로 가중치가 순위를 바꾸지 않는다. costOf 직접 호출용으로는 유효.
경계 밖 글 outsideText
10
저장된 경계 밖 Text 노드 수. 실제 대치 후 남은 글의 중요도·면적까지 평가하지 않는다.
경계 안 미배정 글 insideResidue
10
경계 안에 있으나 Frame 슬롯에 연결되지 않은 Text 노드 수.
Tier 2 tier2Penalty
10
tier_penalty 모드에서만 추가.
Tier 3 tier3Penalty
20
tier_penalty 모드에서만 추가.
글꼴 위계 fontHierarchy
0
최대 글꼴 / 나머지 평균의 비를 이용. 비가 1에 가까울수록 벌점이 크다. 데이터가 없으면 0.
빈 필드 칸 fieldBlank
5
슬롯은 있는데 채울 글이 없어 비운 필드 수. 121,851 후보 중 3.3%에서만 발화한다.
경계 밖 제목 구제 titleOutsideBonus
3
유일하게 값이 음수인 항목이다 — 경계 밖에 제목을 넣으면 raw가 −1이라 비용을 깎는다. 그래서 총 비용이 음수가 될 수 있다.
글꼴 축소 예측 fontShrink
5
렌더 전에 글꼴이 줄어들지를 예측한다. capChars·capOverflow와 별개 항목이고, 재적재 전 옛 Frame은 0이라 비용이 안 바뀐다.
새 글꼴 위계 항목과 기본값 해석
fontHierarchy의 원시값은 π/2 − atan(max(1, 최대/나머지평균) − 1)입니다. 비가 1이면 약 1.57점, 2이면 약 0.79점입니다. 글자 수·내용 보존의 검사가 아니라 원래 디자인의 제목/본문 크기 구분 신호입니다. 주석에서는 tie-breaker라고 부르지만 실제로는 총비용에 더해져 작은 비용 차이를 뒤집을 수도 있습니다.
18개 값은 현재 코드의 기본값입니다. 이 작업은 가중치를 학습하거나 사람의 선호와 비교하지 않았습니다. 예전 문서의 “16개/17개 모두 동일한 튜닝 전 상태”라는 표현 대신, 현재 항목·기본값과 검증 범위를 분리했습니다. comp-sign보다 품질이 좋다는 결론도 이 분석만으로는 낼 수 없습니다.
후보 하나의 순위는 사전식으로 비교합니다
모드
먼저 비교하는 키 → 다음 키
예
tier_pool 기본
Tier → 같은 Tier의 fallback 순서 → 장식 선호 → 비용 → 동점 처리
Tier 1이 30점, Tier 2가 0점이어도 Tier 1이 먼저.
tier_penalty
장식 선호 → Tier 벌점이 포함된 비용 → 동점 처리
장식 선호가 같다면 Tier 1 30점보다 Tier 2 0+10점이 먼저.
장식은 점수를 더하지 않고 preferred → neutral → avoided 순서로 비교합니다. 기본 모드에서는 같은 fallback 위치 안에서 적용합니다. 코드상 tier_penalty에도 장식 비교가 비용보다 먼저 있으므로 “총점만으로 정렬”이라고 설명하면 틀립니다. 장식 축의 순서는 ornament, background이며 중첩도 비교 대상입니다.
동점은 template_idx 내림차순(NULL 마지막) → 디자인 ID 내림차순 → 노드 ID 오름차순입니다. 정렬 후 frame_fingerprint가 같은 결과를 합치고 top_n까지만 반환합니다. fingerprint가 NULL인 행은 이 중복 제거에서 합치지 않습니다.
한 행이 여러 카테고리에 걸렸을 때
현재 rankCandidates는 실제 조회 필터를 통과한 Query Frame들을 Tier 순으로 보고 처음 성공한 해석을 채택합니다. 같은 행의 모든 해석 중 최저 비용을 찾지는 않습니다. 배제되거나 계산 오류가 나면 다음 해석을 계속 보므로, 한 후보의 오류가 검색 전체를 중단시키지는 않습니다.
계산 결과와 그림 사이에는 화면의 처리가 한 번 더 있습니다. 이 층을 빼면 “blank인데 왜 글이 보이지?” 또는 “제목이 보이는데 왜 제목 탈락 벌점이 있지?”라는 질문에 답할 수 없습니다.
검색 결과 분류
실제 화면의 대치
binding
sheet_id를 키로 실제 입력 글을 넣는다. sheet_id가 null이면 이 지도에서는 건너뛴다.
blank
현재 검토 화면은 완전한 공백 대신 임시 문구로 채운다.
inside_unbound_text
역할·길이에 맞는 임시 문구로 채운다.
preserve
지도에 쓰지 않아 원래 글을 유지한다. 유닛의 짧은 토큰 보존은 검색 호출의 원문 지도 제공 여부에 영향을 받는다.
outside_text
기본적으로 원문 그대로 둔다. 제목 구제 노드 하나만 예외다.
페이지 제목 구제
title binding이 없으면 경계 밖 Title 역할을 우선하고, 위쪽·왼쪽·큰 글꼴을 가중 비교해 한 노드를 고른다.
화면의 제목 구제는 같은 썸네일에 제목을 두 번 쓰지 않도록 이미 title binding이 있으면 생략합니다. 구제된 제목을 포함해 검색 점수를 다시 계산하거나 새 용량 게이트를 실행하지는 않습니다. forest Query Frame은 현재 위성 제목을 만들지 않는 경로도 있으므로, 제목이 각 단계에 실제로 남는지는 pageTitle → satellites.title → binding → 화면 지도를 따로 추적해야 합니다.
여러 조각이 나오면 한 페이지 후보 카드로 묶습니다
Steps 12점+BigNumber 3점+List 7.5점=그룹 22.5점
합산 방법을 보여 주는 계산 예시입니다.
조각이 하나면 결과마다 단일 카드를, 둘 이상이면 현재 선택한 후보들의 비용 합을 가진 그룹 카드 하나를 만듭니다. 그룹 Tier는 선택 결과 중 가장 큰 값입니다. 대안 선택을 바꾸면 비용·Tier를 다시 합쳐 목록을 정렬합니다. 가능한 모든 조합의 페이지를 자동 생성하는 것은 아닙니다.
색상·크기·간격의 조화는 이 비용 합에 포함되지 않습니다. 색상 대치와 선택적 재배치는 렌더 쪽 작업이며 이후 검색 점수를 다시 매기지 않습니다. N3/N4 조각을 새로운 한 장의 레이아웃으로 배치해 주는 엔진과, 화면에서 결과들을 그룹 카드로 보여 주는 것은 구분해야 합니다.
페이지 비교와 빈 칸
기본 모드는 그룹/단일 결과의 Tier를 먼저 비교하고, 같은 컴포넌트의 단일 대안 사이에는 fallback 순서·장식을 비교한 다음 비용을 봅니다. 나머지는 비용 → 레벨 → 자체 순위입니다. 이 조건부 비교에 F06의 순환이 남아 있습니다.
빈 슬롯의 비용은 0이며 그룹 Tier 계산에서도 빠집니다. 전부 비면 Tier가 Infinity지만, 일부만 비면 낮은 점수의 불완전 그룹이 위로 올 수 있습니다. 특히 tier_penalty 모드에서는 Tier 자체를 정렬 키로 쓰지 않습니다. 빈 그룹 처리 한계는 F09를 보세요.
아래의 “재현됨”은 현재 로컬 코드의 함수를 직접 호출한 작은 사례에서 확인했다는 뜻입니다. 운영 DB에서 얼마나 자주 발생하는지까지 측정한 것은 아닙니다. 각 항목에서 “왜?”와 “반론”을 함께 읽으면, 이상해 보이는 모든 분기를 무조건 삭제하는 실수를 피할 수 있습니다.
Tier 1이 없고 Tier 2·3만 있으며, tier_targets=[2,5], min_candidates=10이다. Tier 2에서 2개 통과하고 Tier 3에는 추가 후보가 있다.
현재 결과
tierTargetAt(..., tierIdx)가 압축된 배열의 첫 자리 0을 사용한다. 실제 Tier 2에 Tier 1 문턱 2가 붙어 Tier 3를 조회하지 않는다. 재현 R02. Timeline 17항목처럼 성립 실패로 Tier 1이 빠지는 실제 생성 경로도 확인했다.
왜 이상한가
Tier 번호는 2인데 배열 위치는 0이다. 식별 번호와 현재 배열 위치를 같은 값으로 취급했다. 옵션 계약은 Tier 1/2별 문턱인데 실행은 첫째/둘째 남은 Tier별 문턱이다.
반론까지 생각하면
현재 화면은 두 값을 동일하게 전달하므로 기본 [50,50]에서는 차이가 숨는다. 서로 다른 값을 주는 API/직접 호출에서 드러난다. 모든 사용자에게 기본 설정으로 발생한다고 말하면 과장이다.
개선 방향
실제 tier 번호로 문턱을 찾거나 {1: 목표, 2: 목표}로 표현한다. [1,2,3], [2,3], [3]과 서로 다른 문턱을 함께 검증한다.
ResumeCV 입력의 Unit.children에 label=자식, period=2099년, items=설명이 있고, 후보에는 자식 슬롯 없이 부모 label·items 슬롯만 있다.
현재 결과
초과 자식을 unitLikeForFold → foldText로 접는다. 이 보조 함수는 label·value·items만 꺼내 period가 최종 binding에서 사라지는데 contentLoss=0이며 searchFrames도 후보를 통과시킨다. 재현 R05.
왜 이상한가
contentLoss는 “넣으려다가 자리를 못 찾은 문자열”만 센다. 변환이 애초에 꺼내지 않은 필드는 검사 대상에 들어오지 않는다. 검사할 자료에서 먼저 지워진 손실은 검사기가 찾을 수 없다. 코드상 values·다른 단수 필드와 더 깊은 자식도 이 helper의 시야 밖이다.
반론까지 생각하면
P3/N5의 entityLines는 필드를 더 넓게 보존하도록 고쳐졌지만, 이곳은 별도의 alignment 내부 접기다. “projection을 고쳤으니 모든 접기가 안전하다”는 주장은 성립하지 않는다. 새로운 생성기가 안 쓰는 필드라도 기존 입력·직접 호출 경로는 남는다.
개선 방향
원본 필드별 기대값을 최종 binding과 대조하고, 자식 접기에도 공통 재귀 텍스트 변환 또는 동등한 전 필드 순회를 적용한다. contentLoss 가중치를 높이는 것만으로는 0인 손실을 잡을 수 없다.
좋은 수정은 반복문 수를 줄이는 작업이 아닙니다. 같은 조건을 두 번 확인하는 이유가 있는지, 끝낼 근거가 맞는지, 생략한 후보의 의미가 무엇인지를 먼저 따져야 합니다.
현재 선택
왜 필요한가
판단
원래 카테고리의 자기 fallback 행만 사용
도착지의 새 fallback을 계속 따라가며 우선순위가 변하는 것을 막는다.
유지할 규칙
Tier·Stage를 별도로 둠
표현 종류의 차이와 슬롯 역할의 차이를 구분한다.
하나의 숫자로 합치지 않는다.
Stage 2에서 1비트 패턴 제외
같은 카테고리에서 같은 행을 다시 조회하는 것을 막는다.
필요한 중복 방지
검사 후 승인 수로 중단
읽기만 한 후보가 탈락해도 다음 후보를 찾을 수 있다.
해결된 동작 유지
matchedQueries 기록
복수 태그 행을 통과하지도 않은 원래 카테고리로 잘못 승격시키지 않는다.
필터와 채점 연결에 필요
별칭 배정 후 aliasPlacedFields 기록
이미 넣은 필드를 이동 단계가 또 넣는 중복 방지. marker 제외도 덮어쓰기 방지.
겉보기 중복 분기가 아니라 역할이 있음
N2 후 N3, Hierarchy는 N5
공간 구조·소속 관계를 가급적 보존한다.
규약 순서 유지
N2 지연 생성 + 묶음 캐시
조합 전체 저장으로 메모리가 폭증하는 것을 줄인다.
조회량·실행 시간까지 줄여 주지는 않음
불필요해 보이는 계산은 어디에 남았나
정확 카테고리만 검색하는데 하위 Tier도 미리 만든다. 화면 searchOne은 항상 buildTiersFor를 전부 계산한 뒤 exactType이면 Tier 1만 남깁니다. N2가 많으면 결국 버릴 fallback 투영·성립 검사도 반복됩니다. exactType용 경로 또는 재사용 가능한 결과로 줄일 수 있습니다.
투영을 성립 검사와 Query Frame 생성 때 다시 한다.buildTiersFor가 투영해 검사한 구조를 버리고, buildQueryFrames가 같은 원본→도착 투영을 다시 만듭니다. 두 층의 책임을 유지하면서 변환 결과를 전달하는 방법을 검토할 수 있습니다.
페이지에서 검사한 행을 마지막에 다시 채점한다. 승인 수 확인용 평가와 최종 반환용 평가가 별도입니다. 현재는 예전 페이지 전체를 매 페이지 다시 채점하는 방식은 아닙니다. 최종 중복 평가를 줄이려면 새로운 matchedQueries가 추가돼 더 우선인 해석이 생길 수 있는 점을 함께 고려해야 합니다.
contentLoss 가중치는 일반 검색 순위에서 효과가 없다. 해당 후보가 이미 하드 배제되기 때문입니다. 비용 API와 규약 정의를 위해 남긴 것은 이해할 수 있지만, 화면에서 값을 바꾸면 손실 허용이 달라질 것처럼 읽히면 혼란스럽습니다.
코드 주석도 현재 동작과 대조해야 합니다
documentSearch.ts 머리말의 “L3는 바로 평탄화”, search.ts 주변 문서의 “allocateSlots로 배분”, rank.ts의 “alternatives 우선순위”, summary-projection.ts의 “관계 종류·참가자까지 보존”은 현재 실행문과 맞지 않는 설명이 남은 사례입니다. 이 문서는 현재 실행문과 재현 결과로 설명하고, 규약 의도는 별도 근거로 표시합니다.
개선 순서 제안
내용 누락·오류 전파·정렬 순환처럼 결과의 신뢰성을 깨는 F05·F07·F06을 먼저 다룬다.
후보 발견과 종료의 의미를 맞춘다. F01·F02·F04와 스캔 소진 사유(F03)를 정리한다. 화면 사후 태그 필터가 결과를 비우고 코어를 보충시키지 않는 경로도 같이 본다.
완성도와 품질를 나눠 본다. 누락된 조각(F09), 핵심 숫자·단위·조건, 카드별 분량 균형, 관계·순서, 남은 원문의 시각적 영향, 실제 겹침·잘림을 평가한다.
합치기 때문에 길어진 문장이 itemOverflow·capOverflow·capChars에 이미 중복 반영되는지 보고 새 과밀도 벌점을 조정한다. 효과는 총점 하락보다 사용 가능한 결과 비율·원본 내용 보존·사람의 선택과 일치·검색 및 렌더 시간으로 검증한다.
meta.candidate_count는 승인 수가 아니라 수집된 고유 행 수입니다. per_tier.count도 해당 Tier에서 새로 수집한 행 수입니다. fetched_rows는 반복 조회까지 포함합니다. 현재 응답에는 admittedCount나 종료 사유가 따로 없으므로, “후보 50”만 보고 성공 50이라고 판단하면 안 됩니다.
문서 searchCount는 N2 등을 포함한 실제 searchOne 호출 수이며, 최종 카드 수가 아닙니다. tookMs는 여러 요청 시간의 합이므로 동시 실행 시 사용자가 기다린 벽시계 시간과 다릅니다. capacity·param·content 배제 수는 마지막 채점에서 남은 한 사유 기준이며, 구조 불일치 등 모든 탈락을 전부 설명하는 계수는 아닙니다.
semantic_component_version · frame_version · stale
3. 식별과 중복 제거
frame_fingerprint · rlsc_fingerprint · unit_signature · design_object_uuid · template_idx · source. §9에서 본 같은 지문끼리 묶어 온전한 쪽을 대표로 세우는 처리가 frame_fingerprint를 씁니다.
4. 벌점 계산에 쓰는 부가 데이터
컬럼
내용
outside_text · outside_text_count
이 노드 경계 밖의 텍스트. outsideText 벌점과 제목 구제에 씁니다
inside_unbound_text · inside_unbound_count
경계 안이지만 슬롯에 안 묶인 잔여. insideResidue 벌점에 씁니다
preserve_text_nodes
원문을 그대로 둘 노드(번호·vs 등)
font_size_max · font_size_rest_avg
fontHierarchy 재료. 지금 가중치가 0이라 순위에 닿지 않습니다
threading_check
스레딩 검사 결과
5. 버전은 다섯 축으로 따로 돕니다
「버전」이 하나가 아닙니다. 무엇이 낡았는지 말하려면 어느 축의 버전인지를 같이 말해야 합니다.
축
컬럼
지금 값과 뜻
데이터 발행분
semantic_component_version
2.4 / 2.5 — 어느 발행분의 IIS 결과로 만들었나
Frame 스키마
frame_version
전부 5 — jsonb 구조 자체의 판
용량 측정법
cap_source
text_capacity:v2. 226,952행 중 225,382행(99.3%)이 v2이고, v2 슬롯 cap에는 hardLimit이 붙습니다
stale은 「이 행이 낡았다」 표시이고, built_at은 적재 시각입니다. provenance는 iis인지 rlsc-rule인지를 적습니다 — 분류는 IIS에서 받아도 Frame 자체는 규칙으로 만든 행이 있습니다.
6. 함정 둘
함정 1categories_from으로는 아무것도 구분하지 못합니다.
이 컬럼은 categories가 어디서 왔는지를 적는 메타이고 값은 iis·tags·none 셋입니다. 그런데 226,952행 전건이 iis입니다 — 적재기가 하드코딩하기 때문입니다. provenance가 rlsc-rule인 행(25,373행)도 분류는 IIS에서 받습니다. 지금은 정보가 없는 상수 컬럼입니다.
한 컴포넌트에 표·차트와 다른 유형이 섞였는지 보려면 그 컴포넌트의 행들을 모아 행마다 판정합니다. 기준은 둘이고, frame.dataset.kind === 'matrix' 쪽이 더 확실합니다 — 적재기가 시트에서 실제 grid를 감지했다는 뜻이기 때문입니다. categories의 이름(MagnitudeRanking_BarChart·Enumeration_ListingTable 등)은 보조로 봅니다.
함정 2 도형 속 글자는 이 표에 아예 없습니다.
이 표의 텍스트는 슬롯이든 outside_text든 inside_unbound_text든 전부 RLSC Text 노드에서만 옵니다. 그래서 도형에 들어 있는 글자(ExtendableShapeItem)는 2.5 기준 3,357개 컴포넌트에 8,418개가 있는데도 Frame 어디에도 안 잡힙니다.
즉 검색도 대치도 그 글자들의 존재를 모릅니다. 벌점에도 안 세고, 용량 계산에도 안 들어가고, 대치 대상도 아닙니다. 시트 JSON은 이 표가 아니라 public.design_objects.design_object_sheet_json에 따로 있습니다 — 그쪽을 읽어야 보입니다.
이 노드들을 대치 대상에 넣을지는 아직 정해지지 않았습니다(2026-10-02 기준). 「대치가 안 된다」가 아니라 「대상으로 보지 않는다」가 현재 상태입니다.
근거 규모·컬럼 구성·categories_from 전건 iis·ExtendableShapeItem 8,418개는 사용자가 2026-10-02에 2.5 전수를 직접 조회해 얻은 값이며 이 문서에서 DB로 재확인하지는 않았습니다. 전체 226,952행과 text_capacity:v2 225,382행(99.3%)·hardLimit은 저장소 문서(src/comp-frame/AGENTS.md)에서 교차 확인했습니다. categories_from·cap_source 타입은 packages/comp-frame-shared/src/types.ts에서 확인했습니다.
§14
출처·검증·기존 내용 정리 기록
2026-09-13 로컬 체크아웃 기준입니다. 구현 저장소는 /Users/miridih/Documents/miricanvas-iui, 기준 커밋은 5d807e2dda8ff678fde227a436d7eea762e356bc입니다. 이 작업에서 인용한 packages/comp-frame와 src/comp-frame는 HEAD 대비 변경이 없음을 확인했습니다. 저장소의 다른 변경(Cargo.lock)은 건드리지 않았습니다. 원격을 새로 받아 “원격 최신”을 확인한 것은 아닙니다.
규약 저장소는 로컬 mordor-wiki 013fff131c99bc737ec4fd93ed38b3545675ba19입니다. comp-frame의 정본 fixture MANIFEST도 같은 커밋을 가리킵니다. 구현과 규약이 다르면 현재 실행 결과와 규약의 의도를 나눠 적었습니다. 코드 링크는 모두 이 고정 커밋을 가리킵니다.