# Template-Fit v1 — MDX ↔ Figma 매칭 스펙 **목적**: MDX 꼭지를 Figma 디자인 32개 중 가장 적합한 1개에 매칭하고, 신뢰도에 따라 `use_as_is / light_edit / restructure / reject` 로 라우팅한다. **설계 원칙**: - **매칭 단계에는 LLM 없음**. 코드로만. LLM은 restructure 단계에서만 호출. - "구조 DB"가 아니라 "템플릿 적합성 DB". anchor / slot / affordance / adaptation_cost. - 계산 먼저, AI 판단은 재구성 단계에서만. --- ## 1. 파일 / 스키마 버전 관리 - 파일: `tests/matching/structure_ontology.yaml` (기존 파일 유지) - `meta.schema_version: template-fit-v1` 선언 - 기존 `frames:` 블록 → **legacy 보존** (Phase 21b 회귀 비교용) - 신규 `templates_v1:` 블록 → template-fit-v1 스키마 - 전환 스크립트: `tests/matching/template_fit.py` (phase 넘버 쓰지 않음) --- ## 2. Frame 스키마 (templates_v1) ```yaml templates_v1: : short_id: "13" template_id: three_parallel_requirements schema_version: template-fit-v1 source: title: 필수조건 original_layout: 3col-parallel # Stage 1 벡터 검색용 자연어 설명. suits 맥락도 이 안에 녹여 씀. description: | DX 시행을 위한 3대 필수조건을 3열 병렬 구조로 설명하는 디자인. 기술, 사람, 자연이라는 3개 동등 요소를 각 카드로 나누어 설명한다. 3개 병렬 조건·3대 요소·3개 축의 균형 설명에 적합. visual_pattern: family: list # list / cards / table / compare / diagram 등 layout: 3-column # 표시용. scoring 미사용 axis: horizontal # horizontal / vertical relation_type: parallel # parallel / sequence / compare / hierarchy / definition / emphasis cardinality: ideal: 3 min: 3 max: 3 slots: - {id: title, type: heading, required: true, max_chars: 30} - {id: pillar_1_label, type: label, required: true, max_chars: 10} - {id: pillar_1_body, type: body, required: true, max_chars: 100} # ... 반복 # OR 관계: 여러 세트 중 최고 매칭률 하나만 채택 (max) anchor_sets: - id: three_pillars terms: [기술, 사람, 자연] - id: prerequisites terms: [필수조건, 필수요건, 요건, 조건, 3요소] # 검수용 + 점수용 분리 fit_notes: suits: # 검수용 bullet (description과 중복 허용). 점수 미사용 - 3개 병렬 조건 - 3대 요소 not_suits: # 점수용. hit 당 -0.20 감점, 상한 0.30 - 시간 순서 - 원인-결과 흐름 - 2개 비교 - 4개 이상 항목 adaptation_allowed: split: true # MDX 한 꼭지를 슬롯 여러 개로 분할 허용? merge: false # 여러 꼭지를 슬롯 하나로 합침 허용? infer_missing_slot: false # 빠진 슬롯을 날조? 기본 false rewrite_label: true # 라벨 재작성? rewrite_body: true # 본문 재작성? # Phase 21b 회귀 비교용 legacy: family: list surface: bullets-3 semantic_role: prerequisites layout_original: 3col-parallel ``` --- ## 3. MDX Analysis 스키마 (코드로 생성) ```yaml mdx_analysis: title: "DX 시행을 위한 필수요건" summary: "제목 + 첫 문단 + 상위 bullet 결합 (코드)" detected_terms: [DX, 기술, 사람, 자연, 필수요건, ...] # Kiwi + synonyms item_count: detected: 3 # ###, 볼드 블릿, 표 행 수 중 최대 source: bullets # subsections / table_rows / bullets relation_type: value: parallel # 규칙: 볼드 블릿 N개 → parallel, ### X.Y 순서 → sequence 등 confidence: high content_shape: has_table: false has_subsections: false has_bullets: true bullet_count: 3 slot_candidates: - {label: "기술", body: "디지털기술·기반지식"} - {label: "사람", body: "역량·창의성"} - {label: "자연", body: "여건·투자 기반"} ``` **생성 규칙 (모두 코드)**: - summary: `title + 첫 문단 + top-3 bullet` 결합 - relation_type: - `###` 서브섹션 2+ 있고 "과정/결과" 키워드 → `compare` - `###` 서브섹션 3+ 시간 순서 표시 → `sequence` - 볼드 블릿 N개, 시간어 없음 → `parallel` - 표 헤더 에 2개 주체 + 다수 행 → `compare` - slot_candidates: heading, table row, bold bullet, numbered list에서 추출 LLM 호출은 이 단계에 **없다**. 정확도 부족 시 별도 논의. --- ## 4. 점수 공식 (0 ~ 1 범위) ```python base_score = ( 0.25 * anchor_match + # 0~1 0.20 * cardinality_match + # 0~1 0.20 * relation_match + # 0~1 0.15 * slot_coverage + # 0~1 0.20 * content_embedding # 0~1 (ko-sroberta cosine) ) # 개별 캡 adaptation_penalty = min(0.30, sum(adaptation_costs)) not_suits_penalty = min(0.30, not_suits_hits * 0.20) # 전체 캡 total_penalty = min(0.50, adaptation_penalty + not_suits_penalty) confidence = base_score - total_penalty # 0~1 ``` ### 4.1 anchor_match **기본 계산**: ```python ratio[set] = (mdx.detected_terms ∩ set.terms) / len(set.terms) anchor_match = max over sets of ratio ``` **Per-set 옵션 (anchor_sets[i] 에 선언)**: | 필드 | 기본 | 용도 | |------|------|------| | `min_hits` | 1 | 이 수치 미만 매치면 해당 set 무시 | | `confidence_cap` | 1.0 | raw_ratio 상한 (1.0 미만이면 cap) | | `cap_exempt_if_corroborated_by` | null | 같은 템플릿의 다른 set 최고 ratio가 이 값 이상이면 cap 면제 | **조건부 cap 규칙 (Short Generic Anchor 보호)**: 짧고 일반적인 anchor set (예: `[BIM, DX]`, 2 term) 은 단독 증거로는 약하다 (우연 언급 가능). 하지만 같은 템플릿의 다른 anchor set 도 같이 매칭되면 진짜 주제로 인정. ```python # set 별 effective_ratio 계산 if set.cap < 1.0 and raw_ratio > set.cap: others_max = max(raw_ratio[other] for other in same template's anchor_sets) if set.exempt is not None and others_max >= set.exempt: effective = raw_ratio # cap 면제 else: effective = set.cap # cap 적용 else: effective = raw_ratio anchor_match = max(effective) across sets ``` **현재 cap 적용 대상 (v1)**: - `bim_dx [BIM, DX]`: cap 0.70, 방증 임계 0.50 - 이유: 2-term generic pair. 둘 다 등장해도 비교 주제라고 단정 불가. - 검증: MDX01-2 (진짜 BIM/DX 비교) 는 `comparison_dimensions` 0.71 방증으로 **면제** → 정답 0.922 유지. MDX03-2 (BIM/DX 배경 언급) 는 `comparison_dimensions` 0.29 < 0.50 → **cap 적용** → 0.896 → 0.821 (light_edit, 정답 process_product 0.919와 격차 0.098). **토큰 매칭 규칙**: - synonyms.yaml 정규화 → HTML/JSX 제거 → Kiwi 명사 추출 → anchor 매칭 - 토큰 exact match 우선 - anchor term 길이 ≥ 2자: anchor_vocab substring 으로 보강 (Kiwi 가 복합어를 분해하는 경우 커버) ### 4.2 cardinality_match ```python if n == ideal: 1.0 elif min <= n <= max: 0.8 elif outside AND (split or merge) allowed: 0.5 else: 0.0 ``` - infer_missing_slot=false면 없는 슬롯 날조 불가 → split만으로 가능한 케이스에 한함 ### 4.3 relation_match | MDX \ Frame | parallel | sequence | compare | hierarchy | |-------------|----------|----------|---------|-----------| | parallel | 1.0 | 0.2 | 0.4 | 0.3 | | sequence | 0.2 | 1.0 | 0.3 | 0.4 | | compare | 0.4 | 0.3 | 1.0 | 0.3 | | hierarchy | 0.3 | 0.4 | 0.3 | 1.0 | ### 4.4 slot_coverage (rough v1) ```python slot_coverage = 0.5 if mdx.item_count ∈ [min, max] # 항목 수 적합 + 0.3 if every item has label-like anchor # 라벨 후보 있음 + 0.2 if every item has body-like content # 본문 후보 있음 ``` - 세부 max_chars · 폰트 적합은 v1 에서 측정 안 함 ### 4.5 content_embedding ```python content_embedding = cosine(embed(mdx.summary), embed(frame.description)) ``` - 모델: `jhgan/ko-sroberta-multitask` (huggingface) - 32 frame description 임베딩은 **캐시** (변경 시 재계산) - Stage 1 후보 필터에서 이미 계산한 값 재사용 ### 4.6 adaptation_cost | 조작 | 감점 | |-----|-----| | rewrite_label 1회 | 0.05 | | rewrite_body 1회 | 0.05 | | split (1→N) | 0.10 | | merge (N→1) | 0.15 | | infer_missing_slot | 0.25 | | not_suits hit | 0.20 (별도 not_suits_penalty로 집계) | | forbidden op 시도 | reject (즉시 0점) | 개별 캡: `min(0.30, 누적)` 전체 캡: `min(0.50, adaptation + not_suits)` --- ## 5. 라우팅 | confidence | 처리 | 재구성 AI | 설명 | |-----------|------|---------|------| | ≥ 0.90 | **use_as_is** | 0회 | 코드로 slot fill만. 문장 그대로. | | 0.75 ~ 0.90 | **light_edit** | 0~1회 | max_chars 초과 시에만 AI. 구조 변경 없음 | | 0.60 ~ 0.75 | **restructure** | 1회 | adaptation_allowed 조작 실행. AI로 슬롯 재채움 | | < 0.60 | **reject** | 0회 | 다음 후보. top-5 모두 <0.60 이면 "적합 디자인 없음" | --- ## 6. 전체 파이프라인 ``` [사전 준비 1회] Figma 32 frame → templates_v1 스키마 작성 frame.description 임베딩 캐시 → numpy npy [MDX 입력] 1. MDX analysis 생성 (코드, LLM 0회) → title, summary, detected_terms, item_count, relation_type, content_shape, slot_candidates 2. Stage 1 필터 (벡터) cosine(mdx.summary, frame.description) top-5 추출 3. Stage 2 fit scoring 각 후보에 대해 base_score - total_penalty 계산 4. 라우팅 최고 confidence 기준으로 use_as_is / light_edit / restructure / reject [결과] - use_as_is / light_edit: 해당 frame 확정 - restructure: frame 확정 + 재구성 AI 1회 호출 (나중 단계) - reject: 다음 후보 또는 실패 ``` --- ## 7. 대표 패턴 5개 (샘플 대상) | # | template_id | frame_id | 핵심 | |---|-------------|---------|------| | 1 | three_parallel_requirements | 1171281190 | 3열 병렬 조건형 (기술·사람·자연) | | 2 | three_persona_benefits | 1171281191 | 3주체 카드형 (발주자·시공자·설계자) | | 3 | bim_dx_comparison_table | 1171281195 | BIM vs DX 행별 비교표 | | 4 | process_product_two_way | 1171281210 | 과정/결과 2분할 (배너 + 2col) | | 5 | sw_reality_three_emphasis | 1171281209 | 현실/문제 3강조 (상용 S/W 현실) | 이 5개 스키마로 매칭 로직 검증 → 32개 확장. --- ## 8. 실행 순서 1. ✅ template-fit-v1 스펙 확정 (이 문서) 2. ✅ 대표 패턴 5개 샘플 작성 (`structure_ontology.yaml` → `templates_v1`) 3. ✅ 사용자 사인오프 4. ✅ `template_fit.py` 구현 (5개 샘플 대상) 5. ✅ MDX analysis 코드 구현 (`detect_mdx.py`, LLM 없음) 6. ✅ ko-sroberta content embedding (`embeddings.py`, numpy cosine) 7. ✅ 조건부 cap (short generic anchor 보호) — 4/4 정답, 오답 use_as_is 없음, 격차 ≥ 0.05 8. **32개 확장** ← 다음 단계 9. Phase 21b 대비 최종 비교 --- ## 9. 운영 규칙 (검수 결과 반영) ### 9.1 description ↔ fit_notes.suits 동기화 - description(산문)과 fit_notes.suits(bullet)는 같은 정보의 두 표현. - **수정 시 반드시 함께 갱신**. 한쪽만 고치면 embedding과 검수표가 어긋남. - not_suits는 description에 녹이지 않음 — 점수용 별도 필드. ### 9.2 relation_type 확장 여지 - 현재 vocab: `parallel / sequence / compare / hierarchy / definition / emphasis` - **Frame 14 persona 케이스는 일반 parallel로 유지**. 나중에 혼동이 커지면 `persona_parallel` 같은 하위 타입을 추가할 수 있으나 v1에서는 보류. - 하위 타입 추가 기준: 동일 relation_type 안에서 매칭 충돌이 반복 발생할 때만. ### 9.3 slot 추상화 수준 - Frame 18 (bim_dx_comparison_table)의 `rows: table` 슬롯은 **v1 추상화 수준**. - restructure 단계에서 행별 재구성이 필요해지면 `row_slots[]`로 쪼개야 함 (nested slots). - v1에서는 rows 전체를 한 덩어리로 취급 — slot_coverage 계산 시 항목수는 MDX 항목수로 체크. ### 9.4 merge 비용 - `merge`는 `split`보다 정보 손실 위험이 큼 → adaptation_cost 설정이 이미 그것을 반영: - split: 0.10 - merge: 0.15 - Frame 28처럼 `split/merge` 둘 다 true인 프레임도 비용 차이로 자연스럽게 merge를 덜 선호하게 됨. - 특정 프레임이 merge에 더 취약하면 `adaptation_cost_overrides` 로 프레임별 상향 가능 (v1에서는 미구현). ### 9.5 adaptation_allowed 의미 재확인 | 조작 | 허용 = true 의 의미 | |-----|------------------| | split | MDX 한 꼭지를 여러 슬롯으로 쪼개도 의미 손실 없음 | | merge | 여러 MDX 꼭지를 한 슬롯으로 합쳐도 의미 손실 적음 | | infer_missing_slot | 빠진 슬롯을 AI가 새로 만들어도 날조가 아님 (**기본 false 유지 권장**) | | rewrite_label | 라벨 문구 재작성 허용 | | rewrite_body | 본문 문구 재작성 허용 (max_chars 준수) | - infer_missing_slot은 프레임 특성상 "빈 슬롯 허용 디자인"에만 true. - 5개 샘플 전부 infer_missing_slot: false — 날조 방지. --- ## 9. Phase 21b 회귀 비교 | 구분 | 접근 | 가중치 | |------|-----|------| | Phase 21b (legacy) | family/semantic_role 구조 매칭 | 0.5/0.3/0.1 | | Template-fit (신규) | anchor + slot + cardinality + relation + embedding | 0.25/0.20/0.20/0.15/0.20 (+ penalties) | 4 TARGET_UNITS 기준 hit 수 비교 → 개선 확인. 4 TARGET: - MDX01-2-details → Frame 18 (1171281195) - MDX02-2.2-table → Frame 14 (1171281191) - MDX03-1 → Frame 13 (1171281190) - MDX03-2 → Frame 29 (1171281210)