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
+193
View File
@@ -0,0 +1,193 @@
"""Phase 23 — 정제 anchor + content summary
공식: 점수 = 0.5 × 키워드 + 0.5 × 내용
- 키워드 축: keyword_base 기반 (Phase 22 과 동일)
- 내용 축: MDX summary ↔ frame.content 의 ko-sroberta cosine 유사도
MDX summary = title + 첫 일반 문단 + slot labels (detect_mdx.build_summary 사용)
(Phase 25 template-fit-v1 과 같은 summary 방식 — axis 간 consistency 확보)
"""
import sys
import pickle
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from phase_common import (
TARGET_UNITS, load_keyword_base, load_32_frames, compute_df_idf_tier,
extract_mdx_keywords, keyword_score,
load_target_units, load_frame_index,
)
from embeddings import embed_texts, cosine
from detect_mdx import detect_mdx_analysis
HERE = Path(__file__).parent
PNG_REL = "../../data/figma_previews/"
W_KW = 0.5
W_CONTENT = 0.5
def run():
kb = load_keyword_base()
frames = load_32_frames()
df, idf, tier, N = compute_df_idf_tier(frames)
vocab = set()
for v in frames.values():
vocab.update(v["keywords"])
units_full, units_title = load_target_units()
idx_data, frame_to_short = load_frame_index()
# Pre-embed all frame contents
fids = list(frames.keys())
frame_texts = [frames[fid].get("content", "") for fid in fids]
frame_vecs = embed_texts(frame_texts)
reports = []
hits = 0
for uid, display, correct_sid, *_ in TARGET_UNITS:
text = units_full[uid]
title = units_title[uid]
mdx_kws = extract_mdx_keywords(text, vocab, keyword_base=kb)
# content 축: detect_mdx.build_summary 기반 (title + 첫 문단 + slot labels)
analysis = detect_mdx_analysis(text, title, anchor_vocab=None)
mdx_summary = analysis['summary']
mdx_vec = embed_texts([mdx_summary])[0]
c_scores = {fids[i]: max(0.0, min(1.0, cosine(mdx_vec, frame_vecs[i])))
for i in range(len(fids))}
ranked = []
for fid, v in frames.items():
fig_kws = set(v["keywords"])
k_s, inter = keyword_score(mdx_kws, fig_kws, idf, tier)
c_s = c_scores[fid]
final = W_KW * k_s + W_CONTENT * c_s
ranked.append((fid, final, {
"kw": k_s, "content": c_s, "inter": sorted(inter),
}))
ranked.sort(key=lambda x: -x[1])
top_sid = frame_to_short.get(ranked[0][0], "?")
if top_sid == correct_sid:
hits += 1
margin = ranked[0][1] - ranked[1][1]
reports.append({
"uid": uid, "display": display, "correct_sid": correct_sid,
"mdx_kws": sorted(mdx_kws), "mdx_title": title,
"mdx_summary": mdx_summary,
"top3": ranked[:3], "margin": margin,
})
return {
"phase": 23, "desc": "정제 anchor + content summary",
"formula": f"{W_KW} × 키워드 + {W_CONTENT} × 내용(summary)",
"weights": (W_KW, W_CONTENT, 0.0),
"hits": hits, "reports": reports,
"frame_to_short": frame_to_short, "idx_data": idx_data, "frames": frames,
}
def write_md(r):
lines = []
lines.append("# Phase 23 — 정제 anchor + content summary")
lines.append("")
lines.append(f"**공식**: `{r['formula']}`")
lines.append(f"**결과: {r['hits']}/4 정답**")
lines.append("")
lines.append("**Phase 22 대비**:")
lines.append("- 내용(의미 유사도) 축 추가")
lines.append("- 내용: **MDX summary** ↔ frame.content 의 ko-sroberta cosine")
lines.append(" - summary = title + 첫 일반 문단 + slot labels (detect_mdx.build_summary 사용)")
lines.append(" - Phase 25 template-fit-v1 과 동일한 summary 방식 — axis consistency")
lines.append("")
# 요약
lines.append("## 1. TARGET별 결과")
lines.append("")
lines.append("| MDX | 정답 | 1위 (kw / content / 최종) | 2위 | margin |")
lines.append("|-----|------|---------------------------|-----|--------|")
for rep in r["reports"]:
top1 = r["frame_to_short"].get(rep["top3"][0][0], "?")
top2 = r["frame_to_short"].get(rep["top3"][1][0], "?")
mark = "" if top1 == rep["correct_sid"] else ""
bd1 = rep["top3"][0][2]
lines.append(
f"| {rep['display']} | {rep['correct_sid']} | "
f"{top1} ({bd1['kw']:.2f}/{bd1['content']:.2f}/{rep['top3'][0][1]:.3f}) {mark} | "
f"{top2} ({rep['top3'][1][1]:.3f}) | {rep['margin']:.3f} |"
)
lines.append("")
# Top-3 매트릭스
lines.append("## 2. Top-3 매트릭스")
lines.append("")
lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |")
lines.append("|--------|-------|-------|-------|")
for rep in r["reports"]:
row = [f"**{rep['display']}**<br>정답 Frame **{rep['correct_sid']}**"]
for rank_idx in range(3):
fid, score, _ = rep["top3"][rank_idx]
sid = r["frame_to_short"].get(fid, "?")
info = r["idx_data"].get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ")[:15] + ""
inner = f"![{sid}]({PNG_REL}{png})<br>**{sid}** ({score:.3f})<br>{title}"
if sid == rep["correct_sid"]:
cell = f"<div style='background:#fff3cd;border:3px solid #dc2626;padding:8px;border-radius:6px'>🎯 <b>정답</b><br>{inner}</div>"
else:
cell = inner
row.append(cell)
lines.append("| " + " | ".join(row) + " |")
lines.append("")
# 상세
lines.append("## 3. 유닛별 세부")
lines.append("")
for rep in r["reports"]:
lines.append(f"### {rep['display']} — 정답 Frame **{rep['correct_sid']}**")
lines.append("")
lines.append(f"**MDX 제목**: {rep['mdx_title']}")
lines.append(f"**MDX 키워드 ({len(rep['mdx_kws'])}개)**: {', '.join(rep['mdx_kws'])}")
lines.append("")
for rank, (fid, score, bd) in enumerate(rep["top3"], 1):
sid = r["frame_to_short"].get(fid, "?")
info = r["idx_data"].get(sid, {})
png = info.get("png", "")
fig_content = r["frames"][fid].get("content", "")
common = ", ".join(bd["inter"]) if bd["inter"] else "(없음)"
is_correct = sid == rep["correct_sid"]
prefix = "🎯 " if is_correct else ""
lines.append(f"**{prefix}{rank}위 Frame {sid}** {'(정답)' if is_correct else ''}")
lines.append("")
lines.append(f"![{sid}]({PNG_REL}{png})")
lines.append("")
lines.append(f"- **매칭 내용**: {fig_content}")
lines.append(f"- **매칭 키워드**: {common}")
lines.append(f"- **축별 점수**: 키워드 {bd['kw']:.3f} / 내용 {bd['content']:.3f}")
lines.append(f"- **최종**: **{score:.3f}** = {W_KW}×{bd['kw']:.3f} + {W_CONTENT}×{bd['content']:.3f}")
lines.append("")
# 한계
lines.append("## 4. 한계")
lines.append("")
lines.append("- **의미 유사 ≠ 구조 적합**: ko-sroberta는 '같은 도메인 주제'를 높게 본다. 하지만 3열 병렬과 2열 비교는 구조가 다름에도 cosine이 비슷할 수 있음.")
lines.append("- **구조 축 부재**: MDX 2개 병렬 vs Figma 3열 템플릿이 점수로 구분되지 않음.")
lines.append("- **재구성 불가 판정 없음**: 적합도가 낮아도 여전히 1위가 될 수 있음.")
lines.append("- **다음 단계**: Phase 25에서 legacy 구조 축을 추가.")
lines.append("")
out = HERE / "MATRIX_PHASE23.md"
out.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out}")
def main():
result = run()
with open(HERE / "_phase23_results.pkl", "wb") as f:
pickle.dump(result, f)
write_md(result)
print(f"Phase 23: {result['hits']}/4 정답")
if __name__ == "__main__":
main()