← 미리디 아카이브

miricanvas-iui comp-sign 2026-08-26 코드 실측

Component Signature (comp-sign)

컴포넌트의 지문이다. 디자인 문서(sheetJson)와 RLSC 하나에서 구조 해시 · 텍스트 맵 · 채점 feature · visual tree 를 뽑아 두면, 사용자가 쓴 글도 같은 방식으로 해시해 등호 하나로 담을 컴포넌트를 찾을 수 있다. 검색·중복 판정·내용 대치가 모두 이 지문 위에 서 있다. 지금 3세대(sig3) 가 검증 세트에서 돌고 있고, 기본 세트는 아직 2세대다.

패키지 @miri-unicorn/…-comp-sign 4.3.8 캐노니컬 SIGNATURE_VERSION 40 후보 CANDIDATE 50 (sig3) 기본 세트 v3.4 (아직 sig2)

이 문서의 출처 두 갈래

코드·설계 사실miricanvas-iui 리포지터리를 직접 읽어 적었다 — 커밋 d5e7490f 기준이고 인용한 파일은 워킹트리와 같음을 확인했다. 조직·일정·의사결정 맥락은 Slack 논의 요약이 출처이고, 그런 항목은 Slack 으로 표시했다.

첨부된 두 링크는 이 문서 작성 시점에 열지 못했다 — Confluence 보고서(/wiki/x/vIjMp)는 로그인 리다이렉트, PoC (iui.miricanvas.com/comp-match)는 스크립트로 그려지는 앱이라 본문이 비어 온다. 그 둘의 내용은 리포지터리 안의 정본 문서 (docs/comp-sign-exact-match-plan.md)로 대체해 적었다.

목차

  1. §1개념 — 지문 하나와 네 필드
  2. §2배경 — 무엇이 안 됐나
  3. §3설계 — 네 단계 한 코어
  4. §43세대(sig3) 의 네 결정
  5. §5격자 · 서브셋 — 표 · 차트 · 스마트블록
  6. §6채점과 랭킹 — 후보를 어떻게 줄이나
  7. §7진행 상황 — 버전 연표
  8. §8이슈 · 열린 문제
  9. §9코드 색인
§1

개념 — 지문 하나와 네 필드

패키지 설명 한 줄이 정의다 — «Build component signatures (structure hash, text map, score features, visual tree) from a design document + RLSC. DB-free.» 컴포넌트 하나(디자인 오브젝트)에서 내용·색과 무관한 골격만 남긴 트리를 만들고, 그 트리를 문자열로 직렬화해 해시한다. 그 해시가 검색 인덱스 키다.

핵심은 양쪽이 같은 형식을 쓴다는 것이다. 컴포넌트는 RLSC 에서, 사용자 글은 markdown 요약에서 각각 트리를 만드는데 직렬화 함수는 하나(제네릭)라, 매칭이 «비슷한가» 가 아니라 «해시가 같은가» 가 된다.

적재 측 (INGESTION) — 미리 뽑아 둔다 sheetJson RLSC 시그니처 트리 role + 포함 관계 buildCompSignatures — 컴포넌트당 최대 3건 배치로 iui.comp_signs* 에 적재 · DB 는 호출자 몫 쿼리 측 (QUERY) — 그때그때 만든다 markdown 요약 트리 SummaryNode[] 변형 N ≤100 buildCompSearchData — 파싱 1회로 트리와 해시를 함께 낸다 제목 제외 · 키:밸류 · 자식 확장 · 병합 · 격자 위치… 같은 직렬화 — buildStructureString / buildMarkedStructureString (제네릭) → xxhash h64 → signed int64 10진 문자열 = structure_hash where structure_hash in (…) — 등호 매칭 + 용량 하한(cap_chars · cap_lines) prefilter
«비슷한 컴포넌트 찾기» 가 아니라 «같은 골격 찾기»다. 그래서 쿼리 쪽이 한 가지 트리만 던지지 않고 변형 여러 개를 던진다 — 원본으로 안 걸리면 제목을 빼거나 자식을 병합한 트리로도 두드린다. 변형마다 랭킹 감점이 다르다(§6).

네 필드 — MORDOR 가 적재하는 compSignature

Slack 에 정리된 「compSignature 는 4개 필드」가 바로 이것이다. 실제 산출물 (CompSignature)에는 이 넷 외에 행 레벨 메타가 몇 개 더 붙는다 — MORDOR 배치가 채우는 public.comp_signatures.signature jsonb 가 이 타입 그대로다.

필드무엇쓰이는 곳
structureHash시그니처 트리의 구조 해시. xxhash h64 → signed int64 10진 문자열검색 인덱스 키. PK 구성 요소 (design_object_id, structure_hash)
textMap시그니처 트리의 path → 텍스트 노드 맵. {id, chars, lines, charsPerLine, box, styleKey, fontSize, 정렬, role}내용 대치 시 텍스트를 넣을 자리 · 채점 입력 전부
scoreFeatures내용과 무관해 미리 계산 가능한 채점 feature(면적비 · 형제 구분 · 그림 패턴 · 페이지 크기)검색 때 doc 없이 채점
visualTree디자인 문서를 화면상 포함 관계로 구조화한 트리(node-labeler 저장본)용량 계산 · 형제 구분 감점 · 텍스트 가로 확장
— 같은 행에 함께 저장되는 메타 —
capLines · capCharstextMap 의 줄/글자 용량 DB WHERE 용량 prefilter
textNodeRoles모든 RLSC Text 노드의 {role, chars}(v36+)rlsc 없이 내용 대치 재현 · placeholder 감점
fingerprint대치 텍스트 원문 합 해시(doc 비의존)후보 중복 제거(dedup)
hasHeaderOrFooter머리/바닥글 role 포함 여부검색 결과 필터

왜 해시가 number 가 아니고 string 인가

64-bit 해시는 IEEE-754 double 의 안전 정수(253)를 넘어 정밀도를 잃는다. 10진 문자열로 두면 PostgreSQL int8 컬럼에 그대로 INSERT 되며 자동 cast 되고, 큰 정수 정밀도 손실도 없다.

직렬화 규칙 — 자식 수를 괄호로 적는다

노드 하나는 <자식수>(<자식1>)(<자식2>)…, leaf 는 0. forest 는 가상 루트의 자식으로 본다. 여기에 3세대가 둘을 더한다 — 머리글 노드 앞의 H 마크, 그리고 표·차트 자리의 [table]/[chart] 토큰.

시그니처 트리 (한 카드) Title Hl Desc Desc 파랑 = 머리글 클래스 H {Title, Subtitle, Hl} → H Desc · 컨테이너 → 무마크 구조 문자열 2세대 (캐노니컬 · 무마크) 1(1(2(0)(0))) 3세대 sig3 (H 마킹) 1(H1(H2(0)(0))) 제목 + 표 (격자 토큰) 1(H1([table])) → xxhash h64 → signed int64 -7389510952193149572
마크가 없던 2세대에서는 «Title 카드 N개 그리드» 와 «Desc N줄 리스트» 가 같은 해시였다. H 마킹이 그 둘을 가른다. 오른쪽 해시는 실물 do_id 1717971 의 v49 라이브 해시로, 이 카드가 6벌 + 카테고리 Title 3개인 forest 전체의 값이다. 격자 토큰Table[table], Chart[chart] 로 갈라 문자 데이터 표 쿼리가 차트 컴포넌트로 흘러가지 않게 한다(§5).
§2

배경 — 무엇이 안 됐나

시작 — Content Signature 와 다른 축이 필요했다

계기 Slack

C.D. 파이프라인의 검색 쿼리

콘텐츠 문서 생성 파이프라인에서 검색 쿼리를 기존 Content Signature 와 다른 구조로 바꿔야 할 필요가 생겼다.

거기서 「Component Search Signature」 개념이 처음 제안됐다.

조직 Slack

RLSC Advancement Pod · 파트2

MORDOR 스쿼드 안의 Search Signature 파트가 담당한다. Blockifier Pod 과 밀접하게 붙어 있다.

목적

내용시각화의 재현율

「Search Signature v2 만 구현해도 내용시각화에서 제기한 문제 중 중요한 것 대부분이 해결됨」이 실제로 확인됐다. Slack

덱 쪽 근거는 Visual Summary 규약 문서에 있다 — BAD 200건 분류에서 85% 가 매칭 문제였다.

v2 의 설계 결정 — 후보를 넉넉히, 랭킹은 앞단에서 Slack

「검색 후보를 넉넉히 가져온 뒤 FE 에서 랭킹한다」로 정리됐고, 그 모양이 지금 코드에 그대로 있다 — 서버는 해시·용량으로만 거칠게 거르고, 전수 채점·중복 제거·정렬은 클라이언트가 한다 (rankCompCandidates). 무거운 doc 은 상위 50개에만 지연 fetch 한다(§6 Phase A/C).

3세대를 시작한 이유 — 정확 매치가 안 되는 구조적 원인 셋

docs/comp-sign-exact-match-plan.md 의 「배경」이 정본이다. 요약하면, 해시가 담는 정보와 쿼리가 아는 정보가 어긋나 있었다.

1

해시는 자식 수 문자열만 담는다

role 도 스타일도 해시에 직접 안 들어간다. 스타일은 트리 변형(승급 tie-break · demoteStyleBreaks)을 통해 간접 반영될 뿐이다.

2

쿼리 측에는 스타일 정보가 없다

markdown 요약 트리에 폰트·크기가 있을 수 없다. 그러니 스타일이 만든 구조는 쿼리가 원리적으로 예측할 수 없다 — miss 의 원천이다.

거기에 스타일 tie-break 는 fontSize 반올림 ±1 노이즈에 뒤집히고 순서 의존적이었다.

3

용량 prefilter 가 합산치뿐

cap_chars/cap_lines>= 하나라, 한 노드가 크게 부족해도 총합만 맞으면 통과했다.

그리고 8/7 — 「시그니처만 고쳐서는 별 소득이 없네요」 Slack

v3 첫 공유의 결론이 «개선이 거의 안 되었어요» 였다. 진단은 「다각형 배경」과 「tightBBox」로 개선한 visualTree 로 컴포넌트를 더 걸러내고 텍스트 영역을 확보해야 효과가 난다는 것.

그 진단이 코드에 실제로 반영됐다 — VT2(사각형 대신 실제 외곽 다각형으로 포함 판정하는 visual tree)가 후보 v46 · 캐노니컬 v40 으로 들어갔고, 텍스트 가로 확장도 VT2 를 쓴다(§7 연표).

§3

설계 — 네 단계 한 코어

comp-sign 패키지는 이름만 「시그니처」지만, 사실 컴포넌트 매칭 파이프라인 전체다. 네 단계가 같은 계산 코어(요약 트리 · 변형 · 용량 모델 · 채점)를 공유하므로 적재/검색/랭킹/대치의 결과가 서로 어긋나지 않는다. 그리고 DB 를 모른다 — fetch·upsert 와 design_object_id·version·source 부여는 호출자 몫이다.

공개 API 네 단계
① 추출
컴포넌트 → 시그니처
buildCompSignatures
② 검색
글 → 트리 + 조회 해시
buildCompSearchData
③ 랭킹
후보 채점 · 중복 제거 · 상위 N
rankCompCandidates
④ 대치
렌더 메시지(텍스트·박스·형제군)
buildContentReplacement

① 은 배치
오프라인으로 iui.comp_signs* 에 적재. xxhash-wasm init 을 1회만 하는 빌더를 재사용한다.
②는 비동기, ③④는 동기
③④ 는 해시 계산이 없다 — 채점 경로(paths)를 요약 트리 + 변형 플래그로 복원할 뿐이다.
④는 rlsc 불요
role·글자 용량을 text_node_roles(v36+)가 담아서, 대치 때 RLSC 를 다시 안 본다.
PoC UI comp-match 도 이 API 들을 그대로 소비한다 — 화면과 파이프라인이 다르게 계산하는 일을 구조적으로 막는다. 패키지명은 comp-sign 이지만 매칭 점수·node-labeler·text-metrics 의 공유 계산 코어까지 함께 담는다(시그니처 빌드가 그것들에 의존하기 때문).

런타임 흐름 — 누가 무엇을 하나

#주체동작이 라이브러리
1클라이언트사용자 글을 서버로 전송— (앱)
2서버글을 markdown 요약으로 변환— (요약 서비스/LLM)
3서버markdown 을 클라이언트로— (전송)
4클라이언트markdown → text_treebuildCompSearchDatatextTree
5클라이언트변형별 해시를 만들어 서버로buildCompSearchDatavariants
6서버해시로 comp_signs 조회— (where … in)
7서버후보 선정 후 클라이언트로 (변형별 demand 로 용량 prefilter, 각 후보에 variant 부착)— (fetch)
8클라이언트후보 중 상위 n 선정rankCompCandidates
9클라이언트상위 n 내용 대치 · 렌더buildContentReplacement

4·5 가 한 호출인 이유

buildCompSearchData(markdown) 한 번이 textTree(4)와 조회 variants(5)를 함께 낸다. markdown 을 한 번만 파싱하므로 8·9 에서 복원한 paths 가 5 에서 만든 해시와 정확히 맞물린다. 파싱이 두 번이면 그 사이에 미묘하게 어긋난다.

서버는 매칭된 각 행에 그 해시의 variant 를 실어 돌려줘야 한다 — 변형 정보 없이는 클라이언트가 후보를 채점하거나 대치할 문자열을 고를 수 없다. 그래서 «해시만 반환하는 축약형» 을 두지 않았다.

변형(variation) — 원본 하나로는 잘 안 걸린다

변형무엇을 바꾸나랭킹
기본원본 트리 그대로
키:밸류 분리 transformed"key: value" leaf 를 key/value 부모-자식으로 분해가점(고정)
제목 제외 unwrapped단일 root 트리의 최상위 제목을 벗긴다감점(고정)
자식 수 확장 expandedChildCount형제 그룹의 자식 수 불균형을 빈 노드로 채운다감점 × 확장량
자식 병합 mergeStep자식을 단계적으로 묶는다(제목+본문 3줄 → 제목+본문 1덩어리)감점 × step(step+1)/2
격자 위치 gridIndex표·차트를 형제 내 다른 자리로 옮긴 트리(sig3 전용)후순위 stage
표 승급 gridPromoted격자의 부모·형제를 버리고 격자만 승급감점 10(기본값)

변형은 정책 순서로 정렬돼 나온다 — 기본 → 키:밸류 → 제목 제외 → 자식 수 확장 → 병합(step 오름차순). 앞쪽이 원본에 가까우니 앞에서부터 채우면 검색 UI 의 stage 순서와 같아진다. 개수 상한은 DEFAULT_MAX_VARIANTS = 100 인데, 예제 입력 73개로 측정해 정한 값이다 — 중앙값 6개, 100 초과는 3개(346·162·162)뿐이라 거의 모든 입력은 온전히 통과하고 자식 확장·병합이 폭증하는 소수만 잘린다(잘리는 건 항상 저우선 tail).

§4

3세대(sig3) 의 네 결정

2026-08 논의의 정본이 docs/comp-sign-exact-match-plan.md 다. 목표는 한 문장 — «후보가 줄어도 정확히 매치». 결정 넷이 그 방향을 정한다.

결정 1 — 스타일로 트리를 변형하지 않는다 (role-only)
스타일 개입 지점은 비교자 하나로 수렴해 있었으므로 항상-같음 비교자를 주입하면 role-only 파이프라인이 된다. 사라지는 것: 승급 tie-break 2종 · demoteStyleBreaks 의 스타일 브레이크 · 형제 Text 스타일 비교. 남는 것: «최고 role 이 유일하면 보스» · role 브레이크 · 컨테이너 승급·강급·평탄화. 결과로 시그니처가 «role + 포함 관계» 의 순수 함수가 된다.
결정 2 — 머리글 클래스 H 를 전 노드에 마킹
{Title, Subtitle, Hl}H, Desc·컨테이너는 무마크. literal 'Title' 마킹은 하지 않는다 — 실측에서 depth 1 내부 노드 role 이 Title 41% / Hl 22% / Subtitle 14% / Desc 22% 로 갈려, Title 등호 매칭은 코퍼스 절반 이상을 조용히 잃는다. H 정규화가 그중 78% 를 수렴시킨다. 루트만 마킹하는 안은 기각 — 루트의 98.9% 가 Title 이라 변별력이 없다.
결정 3 — chars/lines 는 해시에 넣지 않는다
매칭의 본질이 부등호(용량 ≥ 수요)라 등호 매칭인 해시와 안 맞고, 경계 miss 를 막으려면 노드당 버킷 열거로 k노드수 폭발한다(노드 7개 × ±1버킷 = ×128). 게다가 용량은 계속 보정되는 모델 산출값이라(v24→v37 이력) 해시에 넣으면 모든 보정이 파괴적 변경이 된다. 대신 per-node 용량 벡터 컬럼cap_lines_arr/cap_chars_arr(int2[])을 RPC 에서 NOT EXISTS element-wise 로 early-exit 비교한다.
결정 4 — 스타일은 트리 입력이 아니라 자격 게이트
최종 트리의 같은 role leaf 형제 그룹이 스타일 이질이면 그 시그니처를 버린다. 이질 형제에 markdown 의 균일한 리스트 항목이 들어가면 이상해질 게 자명하니, 구조로 억지 설명(중첩)하지 않고 풀에서 뺀다. 규칙 둘 — ① 형제 이질(대표 스타일 다름, fontSize 비율 0.77 허용) ② 부모-자식 동일(role 이 다른데 폰트·장식 같고 크기 비율 ≥ 0.95 → role 위계가 스타일로 안 갈림).

결정 1·4 가 실제로 무엇을 고치나 — do_id 2054129

원본 (RLSC) Group Title (33pt) Desc (f66 / 28) Desc (f66 / 25.3) Desc (fd9 / 22.7) ← 폰트가 다름 2세대 v22 — 스타일이 구조를 만든다 Title[ D28[ D25[ D23 ] ] ] 스타일 tie-break 가 D28 을 보스로 뽑고 demoteStyleBreaks 체인이 겹쳐 4중 사슬 markdown 이 예측할 수 없는 구조 = 영구 miss fontSize ±1 반올림에 뒤집히는 순서 의존 규칙 3세대 sig3 — role 만으로 멈춘다 Title[ D, D, D ] 보스 선정은 원래 role 유일성이었으므로 같은 결과까지 가고 거기서 멈춘다 markdown 리스트와 형태가 같다 게이트: Desc 형제 그룹 스타일 이질 → 탈락 이 컴포넌트를 풀에서 버린다 — 구조로 설명하지 않는다 게이트 비교자는 관대하게, 판정은 좁게 D28 vs D25.3(같은 폰트, 비율 0.90) = autofit 축소 산물이라 같은 스타일로 본다 색·alpha 는 계속 무시한다 — 교차 색상 리스트는 의도된 디자인이고, 균일 텍스트를 채워도 이상하지 않다.
게이트는 변형별로 적용된다 — Text:Subtitle/Text:Hl strip 변형이 이질 노드를 제거해 살아남을 수 있고, 전 변형이 탈락하면 그때 컴포넌트가 탈락한다. 실측 게이트 탈락률은 10.4%(사유: Desc 51 · Hl 2 · Title 1)로 fallback 임계 30% 를 크게 하회해 「버림」 정책을 유지했다.

실측 근거 — v3.4 세트 3,250행 샘플 (2026-08-06)

지표
루트(depth 0)가 Title 인 행98.9%
Title 이 depth≥1 에도 있는 행11.1%
Title 여러 개인 행(카드류)38.5%
depth 1 내부 노드 roleTitle 41 / Hl 22 / Subtitle 14 / Desc 22 %
depth≥1 leaf 노드 roleDesc ~97%
시그니처 내 텍스트 role 어휘Title · Subtitle · Hl · Desc 4종뿐
후보 v38 샘플 실측 (520 컴포넌트)
(sanity) 현재 코드 캐노니컬 ∈ DB 적재본100.0%
구조 변화(후보 ∉ 적재본)23.7%
게이트 탈락10.4%
탈락 건 중 구조도 변한 것54/54 = 100%
최대 해시 점유율1(1(0)) 37.5% → 1(H1(0)) 36.7%
distinct 구조 수94 → 90

마지막 줄이 중요하다 — 최대 해시(Title[Desc])는 H 마킹으로 갈라지지 않는다. 그건 본질적 쏠림이고, 결정 3(per-node 용량 벡터)이 담당할 몫임을 실측으로 재확인한 것이다.

트리를 만드는 순서 (sig3)

  1. filterTextOnly — 보이는 TextItemLayoutContainer 만 남긴다. 텍스트 role 은 화이트리스트 {Title, Subtitle, Desc, Hl} 만 통과(v39). 자식이 하나뿐인 컨테이너는 해제한다 — markdown 에 «래퍼» 개념이 없으니 대칭에 유리하다.
  2. 유일 Title 형제 선강급 demoteToSoleTitleForest — 보스 선출보다 먼저, 형제 중 role 이 Title 인 노드가 정확히 하나면 나머지를 그 Title 밑으로 내린다. 이게 없으면 섹션 안 카드들이 먼저 승급돼 Title role 이 된 뒤 섹션 레벨에서 Title 후보가 여럿으로 보여 평탄화된다(실물 do_id 1717971 — 3개 섹션의 카드 6장이 전부 평탄화됐다).
  3. role-only 승급 findBossRoleOnly — 최고 role 이 유일하면 그 노드가 컨테이너를 대체하고 나머지 형제는 그 자식이 된다. 동률이면 평탄화. 단 Subtitle·Hl 이 보스일 때는 형제 중 맨 앞일 때만 승급한다(v49) — 뒤쪽 보스가 자기보다 앞선 무관한 형제까지 거슬러 감싸는 reading-order 역행 승급을 막는다. Title 은 위치 무관하게 항상 보스다(Hl 뱃지가 Title 앞에 오는 카드가 흔하다).
  4. rank 그룹핑 groupSiblingsByRank — markdown 헤딩 스택과 같은 규칙. 직전 head 보다 낮은 rank 만 그 아래로, 같은 rank 는 형제 유지, 높은 rank 는 새 head. 비-leaf 경계마다 스택을 리셋한다. 구 demoteStyleBreaks체인 중첩과 형제 재정렬을 대체한 단계다.
  5. 게이트 checkSignatureStyleGate — 변형별로 통과/탈락. sig3 빌더는 탈락 변형을 아예 만들지 않는다(전 변형 탈락 시 컴포넌트 미적재).
§5

격자 · 서브셋 — 표 · 차트 · 스마트블록

표와 차트는 일반 트리 시그니처가 맞지 않는다 — 표는 셀 스타일 혼재로 게이트에서 97% 전멸하고, 차트는 텍스트 노드가 아예 없다(Chart 노드 + chartProps). 그런데 둘 다 «표 형태 데이터» 를 표현한다. 그래서 격자 차원을 시그니처에 넣었다.

쿼리 (markdown) # 분기별 매출 | 분기 | 매출 | 성장률 | |---|---|---| | Q1 | 120 | 12% | | Q2 | 138 | 15% | 본문 한 줄 항상 — 표 스트림 1(H2([table])(0)) 데이터가 차트로 쓸 만하면 — 차트 스트림 1(H2([chart])(0)) 숫자 열 과반 + 데이터 행 ≥2 TABLE 컴포넌트 source 3 · 6 · 8 CHART 컴포넌트 source 2 · 5 · 7 토큰을 왜 갈랐나 (v40) 공용 [grid] 하나였을 때, 「제목+표」 해시 542건 중 차트가 219 + 풀 312 로 표 11건을 묻어 버렸다. 차트는 숫자 계열 데이터여야 의미가 있으니, 문자 데이터 표 쿼리가 차트로 흘러가는 경로를 끊었다.
차트 스트림은 모든 변형 분기에 붙는다(2026-08-11). 처음엔 기본+격자순열에만 붙였는데, 차트 컴포넌트가 「제목+차트」(495건)·「제목+차트+본문1」(298건) 모양뿐이고 자식 3개 이상은 0건이라, 본문이 2줄 이상인 글은 표 해시만 던지고 0건으로 끝났다 — 실측 「제목+표+본문2」: 표 11 · 차트 0 → 개선 후 차트 206건.

격자 관련 규칙 다섯

규칙내용
트리에 남긴다 Table/Chart 노드를 grid leaf 로 남기고 내부 셀 텍스트 서브트리는 제거 「제목+표」 쿼리가 제목+표 구조에 정확 매치되고 제목까지 대치된다. 구 flat 축은 표만 대치하고 제목을 버렸다
doc 검증 type='Chart'/'Table' 이어도 doc 에 chartProps·cellProps·dataVizProps 가 없으면 leaf 가 아니라 컨테이너로 풀린다 진짜 차트를 감싸는 RLSC 래퍼를 leaf 로 자르면 내부 텍스트·차트가 시그니처에서 사라지고 대치가 렌더러 모르는 id 에 글을 붙였다
격자 위치 순열 단일 grid leaf 를 형제 내 다른 자리로 옮긴 트리를 후순위 변형으로 낸다(GDD → DGD · DDG) 형제 순서가 해시에 들어가는데 컴포넌트마다 표 위치가 제각각. 실측: 원 어순 0건 → 표-마지막 순열로 11건
표 승급 격자를 감싼 부모 노드를 지우고 격자만 그 자리로 올린다. 다른 변형과 조합 불가 「제목+본문 사이 표 하나」 컴포넌트를 표만 남은 컴포넌트와 매치시키려고. 대신 고정 감점 10
다중 격자 제외 격자가 2개 이상인 컴포넌트는 통째로 버린다 표 1개짜리 쿼리에 채우면 나머지 표가 placeholder 로 남는다. 반대로 다중 표 쿼리는 0건이 된다(§8)

세트와 source — 어떤 풀에서 온 시그니처인가

두 축이 직교한다. 세트는 «어떤 design_object 풀» 인지, SIGNATURE_VERSION 은 «어떻게 계산했는지» 를 가른다. 그리고 세트 안에서 source 가 «어떤 선정 기준으로 들어왔는지» 를 라벨한다.

세트테이블비고
v3.4 기본iui.comp_signs_v3_4신규 재추출 풀. 현재 DEFAULT_SIGNATURE_SET — 아직 2세대 시그니처
v3.4-sig3 검증iui.comp_signs_v3_4_sig3풀은 v3.4 와 같고 시그니처만 3세대. H 마킹 해시 공간이 무마크와 겹칠 수 있어 같은 테이블 공존 안 함
v3.2iui.comp_signs기존 풀 보존. 다이어그램 서브셋(source=1)은 여기에만 있다
source무엇
0기본 풀(bundle 기준 선정)
1다이어그램 — v3.2 전용
2 · 3CHART · TABLE dedicated — 사람이 확정한 uuid CSV
4스마트블록 — smart_component_batch_results active
5 · 6CHART · TABLE general — archive 태그 키매칭
7 · 8CHART · TABLE common — 위 둘의 후보가 겹치는 do_id

common(7·8) 이 왜 생겼나 — PK 가 source 를 안 걸어서

dedicated 와 general 의 후보가 상당히 겹친다(실측: CHART 163개 중 111개, TABLE 158개 중 3개). comp_signs_v3_4_sig3 의 PK 는 (design_object_id, structure_hash) 뿐이라 source 가 키에 없다 — 두 스크립트가 같은 do_id 에 각자 다른 source 로 upsert 하면 나중 실행이 앞을 조용히 덮어써 「표/차트 Pool 선택」이 무의미해졌다. 그래서 겹치는 do_id 는 새로 넣지 않고 공통 pool 로 source 만 옮긴다 (그래서 CSV 스크립트를 먼저 돌려야 한다).

스마트블록 서브셋 (source=4)

원본이 다르다
rlsc_jsonsb_gap·sb_padding·sb_direction 같은 자동 레이아웃 주석이 붙어 있다. 일반 structure_json 과 다르다.
복제하지 않는다
시그니처 테이블에 rlsc_json 을 복사하지 않고, 내용 대치 때 원본 테이블을 다시 조회해 postMessage 에 실어 보낸다.
감점을 완화한다
고정 치수로 계산하는 줄 수 초과는 항상 0 으로 면제하고, 면적 비율 감점은 0.1 배로 줄인다. 랭킹에서는 점수와 무관하게 항상 일반 컴포넌트보다 먼저 놓을 수 있다(토글).
§6

채점과 랭킹 — 후보를 어떻게 줄이나

해시로 걸린 후보는 수천 건이다. 여기서 doc 을 안 읽고 전수 채점한 뒤, 상위 50개에만 무거운 doc 을 가져와 내용 대치한다. 이 두 겹이 v2 의 «후보를 넉넉히, 랭킹은 앞단에서» 결정이 코드로 굳은 모습이다.

Phase A → Phase C
해시 조회
변형 해시 + 변형별 용량 하한을 배열로 넘겨 군 하나를 왕복 1회
comp_match_candidates RPC
Phase A — 전수 채점
text_map + score_features 으로 채점 · 중복 제거 · 정렬
rankCompCandidates
Phase C — 상위 50
doc·rlsc·썸네일 지연 fetch → 내용 대치 → iframe 렌더
buildContentReplacement

가져오지 않는 것
doc · visual_tree · design_objects 를 Phase A 에서 안 가져온다. 스타일·박스·정렬·role·페이지 크기 같은 doc 파생값은 빌드 시점에 미리 구워 text_map·score_features 에 넣어 뒀다.
그래서 생기는 규칙
구버전(미재빌드) 행은 그 필드가 비어 중립값으로 빠진다 — SIGNATURE_VERSION 재적재가 끝난 뒤에 새 채점을 배포해야 정확하다.
단계적 실행 — 기본 → 키:밸류 → 제목 제외 → 자식 확장 → 병합(step별) 순으로 fetch 하며 누적하고, 후보 수 한도에 닿으면 이후 단계를 건너뛴다. 병합·표 승급은 두 겹으로 뒤에 있다: ① 비병합만으로 목표를 채울 수 있으면 아예 빼고, ② rank ≥ 100 이라 정렬 맨 뒤.

점수 = 항목별 기여도의 합 (높을수록 좋고 0 이 최고)

항목부호무엇을 보나
스타일 불일치같은 depth 텍스트끼리 스타일이 다르면 쌍마다 weight ÷ 공통조상까지 거리
부모-자식 동일 스타일제목·본문은 달라야 자연스럽다
글꼴 크기 보너스+제목이 본문보다 크면 가점
줄 수 초과노드 용량의 charsPerLine 폭으로 줄바꿈한 줄 수가 lines넘을 때만 초과분에 감점. 합산 prefilter 가 못 잡는 노드별 부족을 여기서 본다
자식 확장 · 제목 제외 · 키:밸류 · 자식 병합 · 표 승급− / +변형별 고정·비례 가감점(§3 표)
면적 비율컴포넌트 면적 ÷ 페이지 면적. 임계는 트리 «면적»에 따라 동적
형제 구분 불가시그니처상 비형제인데 화면(visual tree)상 형제인 노드 쌍
항목별 그림+같은 depth·다른 부모·비슷한 크기 leaf 그림 ≥2 패턴
제목 글꼴최상위 제목 글꼴이 임계 미만일수록
대치 후 축소글을 넣었을 때 예측 bbox 가 줄어든 폭·높이
임시 문구입력에 매핑 안 된 텍스트 노드 수 × weight

가중치는 UI 에서 조정하면 재검색 없이 다시 합산된다. 정렬 위에 절대 우선순위가 두 겹 있다 — 의미 태그 군(tier)스마트블록 최상위 배치. 둘 다 «점수보다 위» 라서, 1군은 아무리 점수가 높아도 0군을 앞서지 못한다. (의미 태그 UI 는 현재 꺼져 있다 — §8)

§7

진행 상황 — 버전 연표

인프라 — 완료
CompSignature 배치 파이프라인(MOR-2120 / MOR-2158)이 coordinator 큐 소진 방식으로 전환돼 production 배포(8/24 · v1.37.14). 10만 건 상한 없이 전량 자동 처리, 조기 완료 버그도 수정. 처리 속도 약 20배. Slack
알고리즘 — 진행 중
후보 로직이 v38 → v50 까지 왔다(아래 연표). 검증 세트 comp_signs_v3_4_sig3 에만 적재하고 production 두 테이블은 무영향. 기본 세트 상수는 아직 'v3.4' — 주석에 «sig3 검증 완료 후 교체 예정».
품질 판정 — 남은 과제
judge 기준 정립이 남았다 — 「차주 iui VLM judge 기준(고정 여부, 시그니처 v2/v3 점수 비교)」 논의 중이고 「시그니처 v3 Judge 팔로업」 마감 8/31. 표/차트 시그니처 v3(UNICORN-78106)는 별도 트랙. Slack

후보 로직(sig3) 연표 — SIGNATURE_VERSION_CANDIDATE

ver무엇이 바뀌었나해시
38role-only 파이프라인 + H 마킹 + 스타일 자격 게이트 결정 1·2·4 구현. sig3 세트·용량 벡터 컬럼·per-hash RPC 신설(08-06)신규
39노드 필터 3건 — ① 격자 leaf 를 [grid] 토큰으로 트리에 남김 ② 텍스트 role 화이트리스트 ③ 보존 텍스트 제외 v47 에서 철회변경
40격자 토큰 분리 — [table] / [chart]변경
41격자 leaf 를 doc 검증으로 판정(래퍼는 leaf 아님)변경
42v41 마무리 — 검증 실패 래퍼의 type 을 Group 으로 중화(해시는 [chart] 인데 트리엔 래퍼가 남는 불일치 수정)변경
43text_node_roles 에서 표 텍스트 노드 제외 — 표 하나당 −100 고정 감점이 붙던 문제불변
44유일 Title 형제 선강급(캐노니컬 v39 와 동일 규칙)변경
46visualTree 형제-only + VT2(외곽선 기반) 지원. 45 는 건너뜀 — 병렬 브랜치가 이미 45 로 191,049행을 적재해 뒀다불변
visual_tree 변경
47v39 ③ 철회 — 보존 텍스트(한 글자·vs·TAM/SAM/SOM·숫자+단위) 노드를 시그니처에서 빼던 것을 되돌림. 매칭 자체를 막아 버렸다변경
48승급·H 마킹을 Title 하나로 축소(원래 이 브랜치에선 v44 였다)변경
49v48 되돌림 + 승급에 「보스는 맨 앞, Title 은 예외」 제약 추가변경
50dataVizProps(차트 preset v2.0.0)를 격자 실체로 인식 — 실 데이터가 있는데 «래퍼» 로 오판돼 트리에서 사라졌다변경

연표를 읽는 두 가지 주의

① 번호와 시간 순서가 어긋난다. 계획 문서의 절 제목은 «v44 — Title 하나로 축소(08-18)» · «v45 — 되돌림(08-19)» 인데, 병합 시점에 다른 브랜치가 44·46·47 을 먼저 차지해 코드 상수는 48 · 49 로 밀렸다. 내용·순서는 그대로다.

② 번호 재사용은 사고가 된다. 이미 적재된 행이 같은 번호를 갖고 있으면 버전 비교에서 «이미 최신» 으로 오판되어 그 변경이 반영 안 된 채 스킵된다 — 실제로 캐노니컬 v39/v44 재적재 때 겪은 문제다.

캐노니컬 연표 — SIGNATURE_VERSION 은 40 에서 멈춰 있다

구간무엇
v21 · v22쓰레기 tightBoundingBox 무시 → 승급 로직 확정(2단계 승급 + 형제-보스 중첩 + 최상위 강급·평탄화). 구조 변환은 v22 이후 동결 — 그래서 「현재 코드 캐노니컬 ∈ DB 적재본」이 100%다
v24 → v37전부 용량 모델 보정 이력이다 — 고정 박스 → 세로 확장 박스 → 코퍼스 평균 글자폭 → RLSC 대비 보정 계수 → charsPerLine clamp → styleKey 압축 → text_node_roles 확장 → 추출 전용 여백 상수 분리. 구조 해시는 불변, text_map 만 계속 바뀌었다
v38컴포넌트 그룹화 5건 — 배치(위/아래 vs 좌/우) 일치 · 구분선은 멤버가 아니다 · 텍스트만으로 된 컴포넌트 허용 · 배경은 겹침으로 세지 않는다 · 구분선을 끊는 건 나중에 그려진 형제뿐
v39유일 Title 형제 선강급 — 구조 해시 변경, 재적재
v40 현재visualTree 형제-only + VT2 지원. VT2 는 DB 를 못 보는 패키지 특성상 호출자가 런타임에 외곽선·tight box 를 계산해 넘겨야 한다(헤드리스 크롬 실측 — 문서당 평균 119ms, 178,095건 직렬 약 5.9시간)

8/12 ~ 8/21 개선 스레드 Slack

날짜내용코드 대응
08-12「시그니처 v3 작업 중이라 거기에 통합해서 개선 가능」
08-13개선 요구사항 정리(Confluence)
08-20표/차트 컴포넌트를 프로덕션과 동일하게 검색풀에 반영 + 위계(상하 위치) 안 맞는 컴포넌트는 매치 제외. 「표 승급」 옵션 신설(형제·부모 모두 버리고 승급, 10점 감점 조절 가능)promoteGridLeaf 변형 + gridPromotedPenalty: 10 · 표/차트 Pool 라디오 토글
08-21위계 불일치 컴포넌트 22건(CHART 9 · TABLE 13) 검색 대상 탈락 처리이 22건을 집계하는 코드 지점은 특정하지 못했다 — 적재 스크립트가 찍는 「다중 격자 제외 · 중복 제외 · 게이트 전탈락」 집계가 그 계열이다
08-21「시그니처를 설명 → 차트 순으로 만들면 좋지만, 지금은 RLSC 에 의존해 그렇게 만들 수 없다」 — 근본 한계 언급§8 이슈 ①

Signature 를 소비하는 쪽

① 컴포넌트 검색과 대치
Dedupfingerprint 로 중복 판정. 검색structureHash 등호 + 용량 하한. 랭킹·대치textMap·scoreFeatures·visualTree. 텍스트 노드 수용량을 합산만이 아니라 개별 비교로 정교화한 것(줄 수 초과 항목)이 이 축의 최근 개선이다.
② Visual Summary / IIS
「IIS / Visual Summary」 production 합류 작업이 8/26 당일 업무로 진행됐고, 「Visual Summary 생성기」 PR 이 별도로 진행 중이다. Slack Visual Summary 가 Signature 의 어느 필드를 소비하는지는 조사 범위에서 확인되지 않았다 — 규약 쪽 정리는 Visual Summary 규약, 추출 레이어는 IIS 문서에 있다.
§8

이슈 · 열린 문제

① 근본

시그니처 품질이 RLSC 정확도에 종속된다

열림 Slack 08-21

결정 1 의 귀결이 그대로 한계다 — 시그니처가 «role + 포함 관계» 의 순수 함수가 되면서, 그 role 을 주는 RLSC 가 틀리면 시그니처도 틀린다. 「시그니처를 설명 → 차트 순서로 만들면 이상적이지만 지금은 RLSC 에 의존해 그렇게 만들 수 없다」가 이 얘기다.

스타일을 지렛대로 쓰던 2세대는 RLSC 가 틀려도 스타일이 일부를 보정했다. 3세대는 그 보정을 의도적으로 버렸다(예측 가능성과 맞바꿨다). RLSC 생성 여정 · IIS 가 같은 문제의 상류다.

뚱뚱한 해시 — 한 해시에 수만 행

열림

최대 해시 Title[Desc] 하나가 풀의 37.5% 를 먹는다. H 마킹으로도 안 갈라지는 본질적 쏠림이라 결정 3(용량 벡터)이 담당해야 하는데, 뚱뚱한 해시(약 8.6만 행) 최악 0.5~2초가 예상된다 (LIMIT 도달 시 조기 종료). 계획 문서의 표현으로는 기존 타임아웃 문제(뚱뚱한 해시 40% · Postgres 57014)를 악화시킨다.

  • 싼 필터(합산 cap 복합 인덱스)를 앞단에 두고 벡터 비교는 생존자에게만
  • count 쿼리에는 벡터 필터를 걸지 않는다LIMIT early-exit 이 없어 전체 스캔이 된다(슬롯 과대 추정은 무해)
  • 물러설 자리: 함수 PARALLEL SAFE 병렬 스캔, 또는 뚱뚱한 해시만 클라이언트 필터로 남기는 하이브리드

벤치 실측이 아직 남아 있다 — 계획 문서의 「열린 문제」 첫 항목.

게이트 「버림」 정책 — 탈락률 재측정 필요

측정 대기

샘플 520 컴포넌트에서 10.4%. fallback 임계 30% 를 하회해 「버림」 을 유지했지만, 적재된 트리로는 정확한 측정이 불가능하다 — 구 demoteStyleBreaks 가 이질성을 구조 중첩으로 숨겨 놓았기 때문이다. 탈락률이 과하면 「버림」 대신 감점/후순위(tier)로 낮추는 안이 대기 중.

다중 격자 컴포넌트를 통째로 버린다 → 다중 표 쿼리는 0건

의도된 절충

격자가 2개 이상인 컴포넌트는 제외한다(표 1개 쿼리에 채우면 나머지가 placeholder 로 남으니까). 그래서 쿼리 측이 표 2개짜리 트리(…([table])([table]))를 그대로 내도 풀에 대상이 없어 0건이 된다. 한때 컴포넌트 측도 결합 시그니처로 적재했으나 제외로 선회했다.

스마트블록 rlscdoc 불일치 17.6%

방침 보류

스냅샷 4,358건 중 82.4% 는 노드 id 구성이 design_objects.structure_json 과 완전히 같지만 (sb_* 주석만 추가), 17.6%(769건)는 노드 집합 자체가 다르다(그중 256건은 공유 노드의 type 도 다름). 시그니처는 rlsc_json 을 신뢰하고 doc 만 design_objects 에서 읽는데, 이 17.6% 는 대치 시 rlsc 가 가리키는 노드가 현재 doc 에 없을 수 있다는 뜻이다. 처리 방침 결정 보류 중.

의미 태그 — 기능은 꺼져 있다

보류 08-20

군(tier) 3단계 검색 정책 · fillByTagQuota · GIN 인덱스 컬럼까지 다 있는데, base 풀 백필이 막혀(bundle 기반 타겟 선정과 --tags-version 이 상호 배타) 컬럼이 전 풀에서 비어 있다. 그래서 UI 전체를 플래그 하나로 가렸다 (SEMANTIC_TAG_UI_ENABLED = false) — 기능을 뜯어내지 않고 남겨 뒀다.

배경에 사고가 하나 있다 — --tags-version 2.2 로 base 풀을 «태그 붙은 아무 design_object» 로 채운 적이 있는데, 그러면 전용 서브셋에 못 들어간 컴포넌트가 source=0 에 섞여 들어와 「표/차트 Pool 선택」이 무의미해진다(실물 do_id 6149525). 그래서 base 풀은 항상 --bundle-id 로 잡는다.

운영 함정 — --fresh 는 서브셋까지 지운다

런북화됨

--fresh 는 세트 테이블을 통째로 TRUNCATE 하는데, 그 명령이 다시 채우는 건 base 풀(source=0)뿐이다. 표/차트·스마트블록 행은 별도 스크립트가 채우므로 직후 이어 돌리지 않으면 그 source 들이 0건으로 빈 채 남는다 — 게다가 재적재 자체는 에러 없이 «성공» 으로 끝나 눈치채기 어렵다. 실제로 한 스크립트가 런북에서 빠져 있어 general pool(5·6)이 며칠간 0건이었다.

필수 실행 순서 — CSV 가 먼저여야 common 분리가 된다
tsx scripts/build-comp-signs.ts --set v3.4-sig3 --bundle-id <uuid> --fresh --vt2 db
tsx scripts/add-tablechart-comps-from-csv.ts
tsx scripts/add-keymatch-tablechart-comps.ts
tsx scripts/import-smart-blocks.ts
⑧ 교훈

«더 엄격하게» 가 회귀였던 v48 → v49

해결

승급·H 마킹을 Title 하나로 좁히면 정밀도가 오를 것 같았다. 표 컴포넌트 하나 (do_id 7511480)에서 Hl[Table] 연관이 사라지는 걸 «트레이드오프» 로 인지하고 진행했는데, 실제 검색 쿼리가 전혀 안 걸리는 걸 사용자가 발견했다.

원인은 쿼리 측과의 비대칭이다 — 쿼리는 지금도 markdown 헤딩 레벨을 전부 H 로 마킹해 Title(H)[Hl(H)[Desc, Desc]](중첩)를 내는데, 컴포넌트 측은 Hl 이 랭크에서 빠져 Title[Hl, Desc, Desc](flat)이 됐다. 해시가 어긋난다. 실측상 depth-1 노드의 Hl 22% + Subtitle 14% ≈ 36% 를 깨는 회귀였다.

되돌린 뒤 목적만 남겼다 — 「보스는 형제 중 맨 앞일 때만, Title 은 예외」. Title 예외는 실측에서 나왔다(do_id 1717971 의 카드 6개 전부가 [Hl 뱃지, Title, Desc, Desc] 순서였다).

남는 규칙 — 한쪽만 엄격하게 만들면 대칭이 깨진다. 시그니처 변경은 쿼리 측 마킹 규칙과 함께 봐야 한다.

계획 문서에 열려 있는 항목

항목상태
게이트 탈락률 실측(샘플 재빌드 후)대기
뚱뚱한 해시 벤치 → RPC / 하이브리드 결정대기
H 마킹 하의 파생 변형별 head 전파 규칙 확정대기
rank 그룹핑 세부 — 같은 rank head 가 연속될 때([Hl, Hl]) 경계 처리대기
보스-위치 제약이 실 코퍼스에서 얼마나 걸리는지(v43 대비 해시 변경률)미측정
스마트블록 rlsc/doc 불일치 17.6% 처리 방침보류
iui VLM judge 기준 · 시그니처 v2/v3 점수 비교 Slack08-31 마감

운영에서 늘 걸리는 규칙 하나

SIGNATURE_VERSION 이 바뀌는 릴리스는 소비 측이 comp_signs 를 재적재해야 반영된다 — 패키지를 게시만 해서는 저장된 text_map 이 바뀌지 않는다. 그리고 채점은 재적재가 끝난 뒤 배포해야 한다(미재빌드 행은 중립값으로 빠진다).

§9

코드 색인

줄 번호는 커밋 d5e7490f 기준이고, 인용 전 그 파일들의 git diff --stat HEAD 가 비어 있음을 확인했다.

무엇어디
패키지 정본 문서(4단계 API 전문)packages/comp-sign/README.md · AGENTS.md
3세대 설계 정본(결정 1~4 · 실측 · 마이그레이션 · 열린 문제)docs/comp-sign-exact-match-plan.md
버전 연표(캐노니컬 · 후보 changelog 주석)signatureTree.ts#L105 · #L240
트리 변환 본체 — 승급 · rank 그룹핑 · 선강급findBossRoleOnly#L1144 · groupSiblingsByRank#L1208 · demoteToSoleTitleForest#L731
스타일 자격 게이트checkSignatureStyleGate#L1362
구조 문자열 직렬화(무마크 · H 마킹)buildStructureString#L1465 · buildMarkedStructureString#L1430
공개 API 네 개buildCompSignatures · buildCompSearchData · rankCompCandidates · buildContentReplacement
검색 변형 · 표 승급 · 격자 위치buildSearchVariations#L1123 · promoteGridLeaf#L125
차트 적합성 판별isChartlikeTableCells#L198
채점 · 가중치(표 승급 감점 10)computeScoreBreakdown#L443 · scoringWeights.ts#L65
세트 매핑 · 기본 세트 상수signatureSet.ts#L24
적재 배치 · 서브셋 스크립트build-comp-signs.ts · tablechart-archive-import.md
검색 UI(Phase A/C · 채점 항목 전문)src/comp-match/README.md
컴포넌트 그룹화(반복 단위 찾기) 해설docs/component-grouping-lattice.md

한 줄 결론

2세대는 스타일로 구조를 설명했다. 쿼리는 스타일을 모르니 그 구조를 예측할 수 없었고, 그게 miss 의 원천이었다. 3세대는 그 지렛대를 버리고 시그니처를 «role + 포함 관계» 의 순수 함수로 만든 뒤, 스타일을 «이건 채우면 이상해진다» 는 자격 게이트로 옮겼다. 대가는 RLSC 정확도에 그대로 종속된다는 것 — 그래서 다음 개선은 시그니처 안이 아니라 상류(RLSC·IIS)와 하류(용량 벡터·judge)에 있다.