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>
This commit is contained in:
2026-07-02 17:03:42 +09:00
co-authored by Claude Opus 4.8
parent 97b7833a1b
commit b836e79ee1
527 changed files with 673036 additions and 717 deletions
@@ -0,0 +1,426 @@
"""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()