Files
C.E.L_Slide_test2/tests/matching/pipeline_11_templates_v1_audit.py
T
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- 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>
2026-07-02 17:03:42 +09:00

427 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()