- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
427 lines
18 KiB
Python
427 lines
18 KiB
Python
"""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"""<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head><meta charset="utf-8"><title>templates_v1 감사</title><style>{style}</style></head>
|
||
<body>
|
||
{html_body}
|
||
</body></html>"""
|
||
HTML_PATH.write_text(html, encoding='utf-8')
|
||
|
||
print("=" * 70)
|
||
print("templates_v1 감사 완료")
|
||
print("=" * 70)
|
||
print(f" md: {MD_PATH}")
|
||
print(f" html: {HTML_PATH}")
|
||
print()
|
||
print(f" _COMPAT 커버리지: {total_defined_pairs}/{total_possible_pairs} "
|
||
f"({round(100 * total_defined_pairs / total_possible_pairs, 1)}%)")
|
||
print(f" structure_intent 종류: {intent_stats['total_unique']}")
|
||
print(f" V3 실패 사례: {len(failure_cases)} 건")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|