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
+226
View File
@@ -0,0 +1,226 @@
"""Phase 16 — 3축 매칭 (구조 × 내용 × 핵심키워드) 테스트
5개 프레임 수동 태깅(v2 스키마) + 4개 MDX 유닛 매칭
매칭 공식:
score = w_kw × 핵심키워드_Jaccard + w_struct × 구조_일치 + w_content × 내용_Cross점수
MDX 측 (코드로 실시간 추출):
- 구조: _detect_mdx_layout (본문 블릿 수, 표 유무)
- 내용: MDX 제목 (대제목 / 중제목 / 소제목)
- 핵심키워드: Kiwi 형태소 중 corpus-rare만 (IDF 기반)
"""
import sys
import math
import collections
import json
import re
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from common import load_figma_texts
from extract_units import extract_units
from methods import (
_get_kiwi, _extract_content_tokens,
_detect_mdx_layout, _get_cross_encoder,
)
from phase10 import extract_titles_only_mdx, TARGET_UNITS
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
PREVIEW_DIR = ROOT / "data" / "figma_previews"
def load_v2():
p = Path(__file__).parent / "metadata_db_v2_sample.yaml"
with open(p, encoding="utf-8") as f:
return yaml.safe_load(f)["frames"]
def extract_mdx_3axis(unit_id, units_dict, mdx_titles):
"""MDX에서 코드로 3축 추출"""
text = units_dict[unit_id]
# 구조
structure = _detect_mdx_layout(text)
# 내용 = 대/중/소 제목 합친 것
content = mdx_titles[unit_id]
# 핵심키워드: Kiwi 내용어 전체 (corpus-rare 필터는 후속에서 vocab 기반)
kiwi = _get_kiwi()
tokens = _extract_content_tokens(text, kiwi)
return structure, content, set(tokens)
def jaccard_weighted(set_a, set_b, vocab_idf):
"""IDF 가중 Jaccard (vocab_idf가 있으면 사용, 없으면 단순 Jaccard)"""
if not set_a or not set_b:
return 0.0
inter = set_a & set_b
union = set_a | set_b
if vocab_idf:
num = sum(vocab_idf.get(c, 0.5) for c in inter)
den = sum(vocab_idf.get(c, 0.5) for c in union)
return num / den if den > 0 else 0
return len(inter) / len(union)
def structural_match(mdx_structure, fig_structure):
"""구조 일치도: exact=1, 유사 layout=0.5, 불일치=0"""
if not mdx_structure or not fig_structure:
return 0.0
if mdx_structure == fig_structure:
return 1.0
# 유사 그룹 (예: 3col-parallel vs persona-3col는 둘 다 3열)
if "3col" in mdx_structure and "3col" in fig_structure:
return 0.5
if "2col" in mdx_structure and "2col" in fig_structure:
return 0.5
if "parallel" in mdx_structure and "parallel" in fig_structure:
return 0.3
return 0.0
def cross_content_score(mdx_content, fig_content):
"""내용 의미 유사도 — Cross-encoder"""
model = _get_cross_encoder()
raw = model.predict([[mdx_content, fig_content]], show_progress_bar=False)
# sigmoid
import math as _m
return 1 / (1 + _m.exp(-float(raw[0])))
def method_3axis(mdx_structure, mdx_content, mdx_keywords,
v2_frames, w_kw=0.5, w_struct=0.2, w_content=0.3, verbose=False):
"""v2 프레임 대상 3축 가중 매칭"""
# IDF: v2_frames 전체의 핵심키워드 corpus에서
df = collections.Counter()
N = len(v2_frames)
for fid, entry in v2_frames.items():
for c in entry.get("핵심키워드", []):
df[c] += 1
idf = {c: math.log(N / cnt) if cnt > 0 else 0 for c, cnt in df.items()}
# MDX 핵심키워드 = Kiwi 토큰 중 v2 vocab에 있는 것만 (canonicalization)
vocab = set()
for entry in v2_frames.values():
vocab.update(entry.get("핵심키워드", []))
vocab.update(entry.get("일반키워드", []))
mdx_kw_canonical = mdx_keywords & vocab
scores = []
breakdown = {}
for fid, entry in v2_frames.items():
fig_kw = set(entry.get("핵심키워드", []))
fig_struct = entry.get("구조", "")
fig_content = entry.get("내용", "")
kw_s = jaccard_weighted(mdx_kw_canonical, fig_kw, idf)
struct_s = structural_match(mdx_structure, fig_struct)
content_s = cross_content_score(mdx_content, fig_content) if fig_content else 0.0
final = w_kw * kw_s + w_struct * struct_s + w_content * content_s
scores.append((fid, final))
breakdown[fid] = {
"kw": kw_s,
"struct": struct_s,
"content": content_s,
"final": final,
"inter_kw": sorted(mdx_kw_canonical & fig_kw),
}
return sorted(scores, key=lambda x: -x[1]), breakdown
def main():
units = extract_units()
v2 = load_v2()
mdx_titles = {}
for uid, _, _, fname, mid, sub in TARGET_UNITS:
mdx_titles[uid] = extract_titles_only_mdx(fname, mid, sub)
with open(PREVIEW_DIR / "index.json", encoding="utf-8") as f:
idx_data = json.load(f)
frame_to_short = {info["frame_id"]: sid for sid, info in idx_data.items()}
# 5 프레임에 대한 short ID 미리 확인
for fid in v2:
sid = frame_to_short.get(fid, "?")
print(f" v2 프레임 {fid} = short {sid} ({v2[fid]['구조']})")
print()
png_rel = "../../data/figma_previews/"
lines = []
lines.append("# Phase 16 — 3축 매칭 (구조×내용×핵심키워드) 테스트")
lines.append("")
lines.append("**5개 프레임 수동 v2 태깅** (13, 14, 15, 18, 29)")
lines.append("**4개 MDX 유닛** 매칭 테스트")
lines.append("")
lines.append(f"**매칭 공식**: `score = 0.5 × 핵심키워드_Jaccard + 0.2 × 구조_일치 + 0.3 × 내용_Cross`")
lines.append("")
# 결과 요약 전체
lines.append("## 결과 요약")
lines.append("")
lines.append("| MDX 유닛 | 정답 | Top-1 | 결과 |")
lines.append("|---------|------|-------|------|")
detail_sections = []
for uid, display, correct_sid, *_ in TARGET_UNITS:
mdx_struct, mdx_content, mdx_kw = extract_mdx_3axis(uid, units, mdx_titles)
ranked, breakdown = method_3axis(mdx_struct, mdx_content, mdx_kw, v2)
top_fid, top_score = ranked[0]
top_sid = frame_to_short.get(top_fid, "?")
mark = "" if top_sid == correct_sid else ""
lines.append(f"| {display} | **{correct_sid}** | **{top_sid}** ({top_score:.3f}) | {mark} |")
# 상세 섹션
d = []
d.append(f"### {display} — 정답 Frame **{correct_sid}**")
d.append("")
d.append(f"**MDX 추출 (코드):**")
d.append(f"- 구조: `{mdx_struct}`")
d.append(f"- 내용: `{mdx_content}`")
d.append(f"- 핵심키워드 (Kiwi 추출, {len(mdx_kw)}개 중 v2 vocab 교집합): "
f"`{sorted(mdx_kw & set().union(*[set(e.get('핵심키워드', []) + e.get('일반키워드', [])) for e in v2.values()]))[:15]}`")
d.append("")
d.append("**5개 후보 점수 분해:**")
d.append("")
d.append("| Frame | 핵심키워드 | 구조 | 내용 | 최종 | 공통키워드 |")
d.append("|-------|----------|------|------|------|-----------|")
for fid, _ in ranked:
sid = frame_to_short.get(fid, "?")
b = breakdown[fid]
mark_row = "" if sid == correct_sid else ""
common_kw = ", ".join(b["inter_kw"][:8])
d.append(f"| **{sid}**{mark_row} | {b['kw']:.3f} | {b['struct']:.2f} | "
f"{b['content']:.3f} | **{b['final']:.3f}** | {common_kw} |")
d.append("")
detail_sections.append("\n".join(d))
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 유닛별 상세 (3축 점수 분해)")
lines.append("")
for s in detail_sections:
lines.append(s)
out_path = Path(__file__).parent / "MATRIX_PHASE16.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out_path}")
# 콘솔 요약
print("\n=== 3축 매칭 결과 ===")
for uid, display, correct_sid, *_ in TARGET_UNITS:
mdx_struct, mdx_content, mdx_kw = extract_mdx_3axis(uid, units, mdx_titles)
ranked, _ = method_3axis(mdx_struct, mdx_content, mdx_kw, v2)
top_sid = frame_to_short.get(ranked[0][0], "?")
mark = "" if top_sid == correct_sid else ""
print(f" {uid}(정답={correct_sid}): Top-1={top_sid} ({ranked[0][1]:.3f}) {mark}")
if __name__ == "__main__":
main()