"""Pipeline Step 11 — templates_v1 + _COMPAT 구조 기준 갭 감사. 목적: V3 의 "대안 탐색" 역할이 왜 현재 세팅에서 약한지 수치/사례로 규명. 다음 단계 (taxonomy v2 설계) 의 근거 자료. 감사 축: 1. _COMPAT 커버리지 — MDX × Figma layout 쌍 중 실제 정의 비율 2. layout 관계 그래프 — _COMPAT 을 그래프로 보고, 고립된 layout / 대체 가능 layout 분석 3. structure_intent 분포 — 얼마나 세분화되어 있나, free-form / controlled 어느 쪽인가 4. slot 체계 — slot id 어휘, 재사용성 5. V3 실패 사례 분해 — Holdout 평가에서 비합리 판정 받은 V3 프레임의 구조 지표 6. layout 간 "대체 가능" 관계 발굴 — 현재 압묵 정보 확인 입력: - structure_ontology.yaml (templates_v1) - phase_common._COMPAT - v3_structure_rerank_result.yaml (V3 실제 출력) - holdout_labels.yaml (라벨 결과) - phase_common.detect_mdx_layout_v2 (MDX 측 layout 생성) 출력: - TEMPLATES_V1_AUDIT.md - TEMPLATES_V1_AUDIT.html """ from collections import Counter, defaultdict from pathlib import Path import sys import yaml import markdown HERE = Path(__file__).parent sys.path.insert(0, str(HERE)) from phase_common import _COMPAT, detect_mdx_layout_v2, structural_match_v2, load_frame_index from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR MD_PATH = HERE / 'TEMPLATES_V1_AUDIT.md' HTML_PATH = HERE / 'TEMPLATES_V1_AUDIT.html' def extract_mdx_raw(sid): cfg = MDX_SECTIONS[sid] p = MDX_DIR / cfg['file'] lines = p.read_text(encoding='utf-8').split('\n') start = None for i, ln in enumerate(lines): if ln.strip() == cfg['start'].strip(): start = i break end = len(lines) if cfg.get('end_prefix'): for i in range(start + 1, len(lines)): if lines[i].strip().startswith(cfg['end_prefix']): end = i break return '\n'.join(lines[start:end]) def main(): ontology = yaml.safe_load((HERE / 'structure_ontology.yaml').read_text(encoding='utf-8')) templates = ontology['templates_v1'] v3 = yaml.safe_load((HERE / 'v3_structure_rerank_result.yaml').read_text(encoding='utf-8')) labels = yaml.safe_load((HERE / 'holdout_labels.yaml').read_text(encoding='utf-8')) idx_data, frame_to_short = load_frame_index() frame_num_map = {fid: int(frame_to_short[fid]) for fid in templates.keys()} num_to_fid = {v: k for k, v in frame_num_map.items()} # ============================================================ # 1. _COMPAT 커버리지 # ============================================================ # 실제 등장하는 fig_layout 집합 (source.original_layout 기준) fig_layouts_used = Counter() for fid, tpl in templates.items(): fig_layouts_used[tpl['source']['original_layout']] += 1 fig_layouts_set = set(fig_layouts_used.keys()) # MDX 쪽은 _COMPAT 키 + detect_mdx_layout_v2 결과의 union mdx_layouts_from_compat = set(_COMPAT.keys()) mdx_layouts_from_sections = set() section_mdx_layout = {} for sid in MDX_SECTIONS: raw = extract_mdx_raw(sid) mdx_layout = detect_mdx_layout_v2(raw) section_mdx_layout[sid] = mdx_layout mdx_layouts_from_sections.add(mdx_layout) mdx_all_layouts = mdx_layouts_from_compat | mdx_layouts_from_sections # 각 MDX layout 에 대해 _COMPAT 매핑 수 / 실제 32 프레임 커버리지 compat_coverage = {} for mdx_layout in sorted(mdx_all_layouts): row = _COMPAT.get(mdx_layout, {}) defined_pairs = len(row) covered_frames = sum(1 for fig in fig_layouts_used if fig in row) total_frames_with_any_layout = len(fig_layouts_used) # fallback 비율 compat_coverage[mdx_layout] = { 'defined_pairs': defined_pairs, 'covered_frame_types': covered_frames, 'total_frame_types': total_frames_with_any_layout, 'fallback_frame_types': total_frames_with_any_layout - covered_frames, 'coverage_pct': round(100 * covered_frames / total_frames_with_any_layout, 1), } # 전체 쌍 집계 total_possible_pairs = len(mdx_all_layouts) * len(fig_layouts_set) total_defined_pairs = sum( 1 for mdx_l in mdx_all_layouts for fig_l in fig_layouts_set if fig_l in _COMPAT.get(mdx_l, {}) ) # ============================================================ # 2. structure_intent 분포 # ============================================================ intents = Counter() intent_to_frames = defaultdict(list) for fid, tpl in templates.items(): for it in tpl['visual_pattern'].get('structure_intent', []): intents[it] += 1 intent_to_frames[it].append(frame_num_map[fid]) intent_stats = { 'total_unique': len(intents), 'total_tags': sum(intents.values()), 'orphan_count': sum(1 for _, c in intents.items() if c == 1), 'top_shared': intents.most_common(), } # ============================================================ # 3. slot 체계 # ============================================================ slot_ids = Counter() frame_slot_counts = [] for fid, tpl in templates.items(): for s in tpl.get('slots', []): slot_ids[s['id']] += 1 frame_slot_counts.append(len(tpl.get('slots', []))) slot_stats = { 'unique_ids': len(slot_ids), 'most_common': slot_ids.most_common(10), 'frame_slot_count_min': min(frame_slot_counts) if frame_slot_counts else 0, 'frame_slot_count_max': max(frame_slot_counts) if frame_slot_counts else 0, 'frame_slot_count_mean': round(sum(frame_slot_counts) / len(frame_slot_counts), 1) if frame_slot_counts else 0, } # ============================================================ # 4. V3 실패 사례 분해 (Holdout 3 섹션 기반) # ============================================================ failure_cases = [] for sid, sec in labels['sections'].items(): v3_pick = sec['picks']['V3'] v3_rating = v3_pick['rating'] if v3_rating == 'rational': continue # 성공 케이스 skip # V3 의 전체 결과에서 해당 섹션 Top-5 v3_full = v3['mdx_sections'][sid]['v3_rerank'] v3_sorted = sorted(v3_full, key=lambda x: x['v3_rank']) v3_top = v3_sorted[0] # V1 이 잡은 1위 (비교용) # v3 yaml 에는 v1_rank 도 있으므로 그걸 이용 v1_top_row = [r for r in v3_full if r['v1_rank'] == 1] v1_top = v1_top_row[0] if v1_top_row else None mdx_layout = section_mdx_layout.get(sid) case = { 'sid': sid, 'mdx_title': sec['mdx_title'], 'mdx_layout': mdx_layout, 'v3_rating': v3_rating, 'v3_score': v3_pick.get('score', 0), 'v3_top': { 'frame_number': v3_top['frame_number'], 'fig_layout': v3_top.get('fig_layout'), 'structure_compat': v3_top.get('structure_compat'), 'template_intent': templates[v3_top['frame_id']]['visual_pattern'].get('structure_intent', []), }, } if v1_top: case['v1_top'] = { 'frame_number': v1_top['frame_number'], 'fig_layout': v1_top.get('fig_layout'), 'structure_compat': v1_top.get('structure_compat'), 'template_intent': templates[v1_top['frame_id']]['visual_pattern'].get('structure_intent', []), } # V3 가 잘못 올린 이유 분석 case['diagnosis'] = { 'v3_pick_compat': v3_top['structure_compat'], 'v1_pick_compat': v1_top['structure_compat'], 'intent_overlap_v3': len(set(case['v3_top']['template_intent'])), 'intent_overlap_v1': len(set(case['v1_top']['template_intent'])), } failure_cases.append(case) # ============================================================ # 5. layout 대체 가능 관계 발굴 # ============================================================ # 각 MDX layout 에 대해 compat ≥ 0.7 인 fig_layout 이 몇 개 있나 — "대안이 있는가" alternative_richness = {} for mdx_layout in sorted(mdx_all_layouts): row = _COMPAT.get(mdx_layout, {}) high = sum(1 for v in row.values() if v >= 0.7) mid = sum(1 for v in row.values() if 0.4 <= v < 0.7) low = sum(1 for v in row.values() if v < 0.4) alternative_richness[mdx_layout] = { 'high_compat_count': high, 'mid_compat_count': mid, 'low_compat_count': low, } # ============================================================ # Markdown 리포트 # ============================================================ md = [] md.append('# templates_v1 + _COMPAT 구조 기준 갭 감사') md.append('') md.append( '_V3 의 "대안 탐색" 역할이 현재 세팅에서 왜 약한지 수치/사례로 규명. ' 'C.2 taxonomy v2 설계의 근거 자료._' ) md.append('') # 1. _COMPAT 커버리지 md.append('## 1. _COMPAT 커버리지 — V3 구분력의 근본 한계') md.append('') md.append( f"- 전체 가능 쌍: **{len(mdx_all_layouts)} MDX layouts × {len(fig_layouts_set)} Figma layouts = " f"{total_possible_pairs} 쌍**" ) md.append( f"- _COMPAT 에 실제 정의된 쌍: **{total_defined_pairs} / {total_possible_pairs} " f"({round(100 * total_defined_pairs / total_possible_pairs, 1)}%)**" ) md.append( f"- 정의 안 된 쌍 (fallback 0.15): **{total_possible_pairs - total_defined_pairs} 쌍 " f"({round(100 * (total_possible_pairs - total_defined_pairs) / total_possible_pairs, 1)}%)**" ) md.append('') md.append('### MDX layout 별 커버리지') md.append('') md.append('| MDX layout | _COMPAT 정의 쌍 | 커버 Figma 유형 | 총 유형 | 커버리지 | fallback 유형 수 |') md.append('|---|---:|---:|---:|---:|---:|') for mdx_l in sorted(mdx_all_layouts): c = compat_coverage[mdx_l] used_in_sections = ', '.join(sid for sid, ml in section_mdx_layout.items() if ml == mdx_l) mark = f' **[섹션 {used_in_sections}]**' if used_in_sections else '' md.append( f"| `{mdx_l}`{mark} | {c['defined_pairs']} | {c['covered_frame_types']} | " f"{c['total_frame_types']} | {c['coverage_pct']}% | {c['fallback_frame_types']} |" ) md.append('') # 2. alternative richness md.append('## 2. MDX layout 별 "대안 탐색" 풍부도') md.append('') md.append('각 MDX layout 에 대해 _COMPAT 에서 compat ≥ 0.7 인 Figma layout 이 몇 개 있나.') md.append( '값이 작으면 그 MDX 구조에 "대안이 없음" — V3 가 대안을 제시할 재료가 부족한 상태.' ) md.append('') md.append('| MDX layout | 고호환 (≥0.7) | 중 (0.4~0.7) | 저 (<0.4) |') md.append('|---|---:|---:|---:|') for mdx_l in sorted(mdx_all_layouts): a = alternative_richness[mdx_l] md.append( f"| `{mdx_l}` | {a['high_compat_count']} | {a['mid_compat_count']} | {a['low_compat_count']} |" ) md.append('') # 3. structure_intent md.append('## 3. structure_intent 분포') md.append('') md.append(f"- 고유 intent 수: **{intent_stats['total_unique']}**") md.append(f"- 전체 태그 수: {intent_stats['total_tags']}") md.append( f"- 1 프레임만 쓰는 고아 intent: {intent_stats['orphan_count']} — " f"{'많음 (구조 재사용 약함)' if intent_stats['orphan_count'] > 5 else '적음'}" ) md.append('') md.append('| intent | 빈도 | 프레임 |') md.append('|---|---:|---|') for it, c in intent_stats['top_shared']: frames = ', '.join(str(n) for n in intent_to_frames[it]) md.append(f"| `{it}` | {c} | {frames} |") md.append('') md.append( '**판정**: 10개 intent, 32 프레임 — controlled vocabulary 느낌은 있으나 ' '어휘 자체가 풍부하지 않음. 새 MDX 유형이 오면 기존 intent 에 억지로 매핑해야 할 가능성.' ) md.append('') # 4. slot md.append('## 4. slot 체계') md.append('') md.append( f"- 고유 slot id: **{slot_stats['unique_ids']}**" ) md.append( f"- 프레임당 slot 수: 평균 {slot_stats['frame_slot_count_mean']} · " f"최소 {slot_stats['frame_slot_count_min']} / 최대 {slot_stats['frame_slot_count_max']}" ) md.append('') md.append('### 가장 자주 쓰이는 slot id (Top 10)') md.append('') md.append('| slot id | 프레임 수 |') md.append('|---|---:|') for sid, c in slot_stats['most_common']: md.append(f"| `{sid}` | {c} |") md.append('') # 5. V3 실패 사례 분해 md.append('## 5. V3 실패 사례 분해 (Holdout 평가 기준)') md.append('') if not failure_cases: md.append('V3 실패 사례 없음.') else: md.append(f'V3 가 비합리/애매 판정 받은 섹션: {len(failure_cases)} 건.') md.append('') for case in failure_cases: md.append(f"### {case['sid']} — {case['mdx_title']}") md.append('') md.append(f"- MDX layout (코드 추출): `{case['mdx_layout']}`") md.append(f"- V3 판정: **{case['v3_rating']}** ({case['v3_score']}점)") md.append('') md.append('| | V3 가 올린 Top-1 | V1 이 잡은 1위 |') md.append('|---|---|---|') v3_top = case['v3_top'] v1_top = case.get('v1_top', {}) md.append( f"| Frame | {v3_top['frame_number']} | " f"{v1_top.get('frame_number', '—')} |" ) md.append( f"| fig_layout | `{v3_top['fig_layout']}` | " f"`{v1_top.get('fig_layout', '—')}` |" ) md.append( f"| structure_compat | **{v3_top['structure_compat']}** | " f"**{v1_top.get('structure_compat', '—')}** |" ) md.append( f"| structure_intent | {v3_top['template_intent']} | " f"{v1_top.get('template_intent', [])} |" ) md.append('') if v1_top and v3_top['structure_compat'] >= v1_top.get('structure_compat', 0): md.append( f"**진단**: V3 가 올린 Frame {v3_top['frame_number']} 의 구조 호환도가 " f"V1 의 합리적 1위 Frame {v1_top['frame_number']} 과 같거나 높음. " f"즉 _COMPAT 만으로는 **의미적으로 더 맞는 프레임을 골라낼 근거 부족**. " f"structure_intent 나 content_affinity 같은 **2차 신호** 가 없어서 " f"동일 layout family 안에서 분별이 안 됨." ) md.append('') # 6. 결론 + 개선 방향 md.append('## 6. 결론 + taxonomy v2 설계 방향') md.append('') md.append('### 주요 갭 3가지') md.append('') md.append( f"1. **_COMPAT 커버리지 {round(100 * total_defined_pairs / total_possible_pairs, 1)}% " f"— 2/3 쌍이 fallback 0.15**. V3 의 " f'"구분력" 자체가 약함. 수동 테이블이 아니라 **자동 계산 가능한 관계 그래프** 필요.' ) md.append( '2. **structure_intent 10개 — 어휘 얕음**. 같은 layout family 안에서 ' '프레임 간 의미 구분 불가. **더 세분화된 의도 taxonomy + controlled vocabulary** 필요.' ) md.append( '3. **"대안 레이아웃" 필드 없음**. 현재 스키마는 "이 layout 은 무엇인가" 만 기술. ' '"이 layout 이 못 담는 콘텐츠 유형 / 대체 가능한 layout" 을 추가해야 ' 'V3 가 대안 탐색을 할 수 있음.' ) md.append('') md.append('### v2 taxonomy 에 추가할 필드 (제안)') md.append('') md.append('- `content_affinity`: 이 layout 이 선호하는 콘텐츠 성격 enum ' '(concept_definition / quantitative_compare / persona_benefit / ' 'process_steps / policy_requirements / ...)') md.append('- `slot_semantic_role`: 각 slot 이 담는 의미적 역할 (주체/시점/원인/결과/예시/...)') md.append('- `alternative_patterns`: 이 layout 의 대안 layout 목록 + 대체 조건') md.append('- `structure_intent` controlled vocab 정의 (현재 free-form → enum)') md.append('- layout 관계 그래프 (수동 _COMPAT → 필드 기반 자동 계산)') md.append('') md_text = '\n'.join(md) MD_PATH.write_text(md_text, encoding='utf-8') # HTML style = """ body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1300px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; } h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; } h2 { margin-top: 2.5em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; } h3 { margin-top: 1.5em; color: #1a365d; } table { border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin: 0.5em 0 1em; } th, td { border: 1px solid #e2e8f0; padding: 8px 10px; text-align: left; vertical-align: top; } th { background: #1e293b; color: #fff; font-weight: 700; text-align: center; } code { background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; } strong { color: #0a6; } """ html_body = markdown.markdown(md_text, extensions=['tables']) html = f"""