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
+424
View File
@@ -0,0 +1,424 @@
"""Phase 13 — 방법별 매칭 키워드/청크/구조 진단 (유저 요청 포맷)
각 MDX 유닛에 대해 7개 방법 × 상위 3개 후보에 대해 "왜 이 결과가 나왔는가"를 상세히.
- TF-IDF / Kiwi+BM25 / Distinctive-Kiwi: 공유 키워드 + IDF/BM25 기여도
- Char 3-gram: 공유 3-gram 조각
- 구조 메타: 구조 피처 벡터 (행 수, 열 수, 블릿 수, 깊이, 이미지 수)
- 청킹+SBERT / 청킹+E5: 가장 유사한 MDX↔Figma 청크 쌍
- Cross: 단어 수준 설명 불가 → 원점수만
"""
import sys
import json
import math
import collections
import re
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from common import load_figma_texts, tokenize_simple, char_ngrams
from extract_units import extract_units
from methods import (
_get_kiwi, _extract_content_tokens, _chunk_text, _np_cosine,
_get_sbert, _get_e5, _get_cross_encoder,
method_tfidf, method_char_ngram, method_kiwi_bm25,
method_structural, method_sbert_chunk, method_e5_chunk,
method_cross_encoder, method_distinctive_kiwi,
_build_tfidf,
)
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
PREVIEW_DIR = ROOT / "data" / "figma_previews"
TARGET_UNITS = [
("MDX01-2-details", "1. (MDX 1) 팝업 — DX와 BIM의 구분", "18"),
("MDX02-2.2-table", "2. (MDX 2) 2.2 DX 시행 주체별 기대효과", "14"),
("MDX03-1", "3. (MDX 03) 1. DX 시행을 위한 필수요건", "13"),
("MDX03-2", "4. (MDX 03) 2. Process 혁신과 Product 변화", "29"),
]
def short_id(fid, frame_to_short):
return frame_to_short.get(str(fid), "?")
# ═══════════════════════════════════════
# 방법별 "왜 이 후보가 선택되었나" 진단기
# ═══════════════════════════════════════
def diagnose_tfidf(mdx_text, figma_texts, target_fids):
"""TF-IDF: 공유 단어 × IDF 기여도 상위"""
all_docs = {"__mdx__": mdx_text, **figma_texts}
vecs, df, N = _build_tfidf(all_docs)
mdx_vec = vecs["__mdx__"]
result = {}
for fid in target_fids:
figma_vec = vecs.get(fid, {})
common = []
for w, mdx_w in mdx_vec.items():
if w in figma_vec:
contrib = mdx_w * figma_vec[w]
common.append((w, mdx_w, figma_vec[w], contrib))
common.sort(key=lambda x: -x[3])
result[fid] = common[:10]
return result
def diagnose_char_ngram(mdx_text, figma_texts, target_fids, n=3):
"""Char 3-gram: 공유 n-gram 조각"""
mdx_set = set(char_ngrams(mdx_text, n))
result = {}
for fid in target_fids:
figma_set = set(char_ngrams(figma_texts.get(fid, ""), n))
common = mdx_set & figma_set
# 몇개 자주 나오는지 표시
result[fid] = sorted(common)[:20]
return result
def diagnose_bm25(mdx_text, figma_texts, target_fids):
"""Kiwi+BM25: Kiwi 형태소 공유 토큰 + TF-IDF 기여도 (BM25 정확 내부는 복잡하므로 TF-IDF 근사)"""
kiwi = _get_kiwi()
mdx_tokens = _extract_content_tokens(mdx_text, kiwi)
mdx_tf = collections.Counter(mdx_tokens)
# Corpus IDF
doc_tokens = {fid: _extract_content_tokens(text, kiwi) for fid, text in figma_texts.items()}
df = collections.Counter()
for toks in doc_tokens.values():
for w in set(toks):
df[w] += 1
N = len(figma_texts)
idf = {w: math.log(N / c) if c > 0 else 0 for w, c in df.items()}
result = {}
for fid in target_fids:
figma_tf = collections.Counter(doc_tokens.get(fid, []))
common = []
for w, m_c in mdx_tf.items():
if w in figma_tf and idf.get(w, 0) > 0:
contrib = m_c * figma_tf[w] * idf[w]
common.append((w, m_c, figma_tf[w], idf[w], contrib))
common.sort(key=lambda x: -x[4])
result[fid] = common[:10]
return result
def diagnose_distinctive(mdx_text, figma_texts, target_fids, general_threshold=0.5):
"""Distinctive-Kiwi: 공통어 제거 후 남은 키워드"""
kiwi = _get_kiwi()
doc_tokens = {fid: _extract_content_tokens(text, kiwi) for fid, text in figma_texts.items()}
df = collections.Counter()
for toks in doc_tokens.values():
for w in set(toks):
df[w] += 1
N = len(figma_texts)
general = {w for w, c in df.items() if c / N > general_threshold}
idf = {w: math.log(N / c) if c > 0 else 0 for w, c in df.items()}
mdx_tokens = [w for w in _extract_content_tokens(mdx_text, kiwi) if w not in general]
mdx_tf = collections.Counter(mdx_tokens)
result = {}
for fid in target_fids:
figma_tf = collections.Counter(
w for w in doc_tokens.get(fid, []) if w not in general
)
common = []
for w, m_c in mdx_tf.items():
if w in figma_tf:
contrib = m_c * figma_tf[w] * idf.get(w, 0)
common.append((w, m_c, figma_tf[w], idf.get(w, 0), contrib))
common.sort(key=lambda x: -x[4])
result[fid] = common[:10]
return result, general
def diagnose_structural(mdx_text, figma_texts, target_fids):
"""구조 메타: 피처 값 비교"""
def extract_features(text):
return {
"블릿 수": len(re.findall(r"^\s*[-*]\s", text, re.MULTILINE)),
"표 행 수": text.count("\n|"),
"헤더 수 (##)": text.count("\n##"),
"이미지 수": len(re.findall(r"!\[[^\]]*\]\([^)]+\)", text)),
"텍스트 길이": len(text),
}
mdx_feat = extract_features(mdx_text)
result = {}
for fid in target_fids:
figma_feat = extract_features(figma_texts.get(fid, ""))
result[fid] = {"mdx": mdx_feat, "figma": figma_feat}
return result, mdx_feat
def diagnose_sbert_chunk(mdx_text, figma_texts, target_fids):
"""청킹+SBERT: MDX↔Figma 가장 유사한 청크 쌍 top 3"""
model = _get_sbert()
q_chunks = _chunk_text(mdx_text)
q_vecs = model.encode(q_chunks, show_progress_bar=False)
result = {}
for fid in target_fids:
d_chunks = _chunk_text(figma_texts.get(fid, ""))
if not d_chunks:
result[fid] = []
continue
d_vecs = model.encode(d_chunks, show_progress_bar=False)
pairs = []
for i, qv in enumerate(q_vecs):
for j, dv in enumerate(d_vecs):
sim = _np_cosine(qv, dv)
pairs.append((sim, q_chunks[i], d_chunks[j]))
pairs.sort(key=lambda x: -x[0])
result[fid] = pairs[:3]
return result
def diagnose_e5_chunk(mdx_text, figma_texts, target_fids):
"""청킹+E5"""
model = _get_e5()
q_chunks = _chunk_text(mdx_text)
q_vecs = model.encode(["query: " + c for c in q_chunks],
show_progress_bar=False, normalize_embeddings=True)
result = {}
for fid in target_fids:
d_chunks = _chunk_text(figma_texts.get(fid, ""))
if not d_chunks:
result[fid] = []
continue
d_vecs = model.encode(["passage: " + c for c in d_chunks],
show_progress_bar=False, normalize_embeddings=True)
pairs = []
for i, qv in enumerate(q_vecs):
for j, dv in enumerate(d_vecs):
sim = _np_cosine(qv, dv)
pairs.append((sim, q_chunks[i], d_chunks[j]))
pairs.sort(key=lambda x: -x[0])
result[fid] = pairs[:3]
return result
# ═══════════════════════════════════════
# 메인
# ═══════════════════════════════════════
def trunc(s, n=45):
s = s.replace("\n", " ").strip()
return s if len(s) <= n else s[:n] + ""
def main():
units = extract_units()
figma = load_figma_texts()
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()}
png_rel = "../../data/figma_previews/"
lines = []
lines.append("# Phase 13 — 방법별 매칭 키워드/청크/구조 진단")
lines.append("")
lines.append("각 MDX 유닛 × 7개 방법 × Top-3 후보에 대해 "
"**왜 이 결과가 나왔는지**를 매칭 키워드/청크/구조 단위로 보여줍니다.")
lines.append("")
for uid, display, correct_id in TARGET_UNITS:
lines.append("---")
lines.append("")
lines.append(f"# {display}")
lines.append(f"**정답**: Frame **{correct_id}**")
lines.append("")
mdx_text = units[uid]
# 각 방법 Top-3 얻기
results_by_method = {}
results_by_method["TF-IDF"] = method_tfidf(mdx_text, figma)[:3]
results_by_method["Char 3-gram"] = method_char_ngram(mdx_text, figma)[:3]
results_by_method["Kiwi+BM25"] = method_kiwi_bm25(mdx_text, figma)[:3]
results_by_method["Distinctive-Kiwi"] = method_distinctive_kiwi(mdx_text, figma)[:3]
results_by_method["구조 메타"] = method_structural(mdx_text, figma)[:3]
results_by_method["청킹+SBERT"] = method_sbert_chunk(mdx_text, figma)[:3]
results_by_method["청킹+E5"] = method_e5_chunk(mdx_text, figma)[:3]
results_by_method["Cross"] = method_cross_encoder(mdx_text, figma)[:3]
# 각 방법별 Target IDs (Top-3)
all_target_ids = set()
for method, ranked in results_by_method.items():
for fid, _ in ranked:
all_target_ids.add(fid)
# 진단 계산 (한 번씩)
tfidf_diag = diagnose_tfidf(mdx_text, figma, all_target_ids)
charng_diag = diagnose_char_ngram(mdx_text, figma, all_target_ids)
bm25_diag = diagnose_bm25(mdx_text, figma, all_target_ids)
distinct_diag, general_words = diagnose_distinctive(mdx_text, figma, all_target_ids)
struct_diag, mdx_feat = diagnose_structural(mdx_text, figma, all_target_ids)
sbert_diag = diagnose_sbert_chunk(mdx_text, figma, all_target_ids)
e5_diag = diagnose_e5_chunk(mdx_text, figma, all_target_ids)
def frame_preview_cell(fid, score_str):
sid = frame_to_short.get(str(fid), "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ") or ""
if len(title) > 15:
title = title[:15] + ""
mark = "" if sid == correct_id else ""
return f"{mark}![{sid}]({png_rel}{png})<br>**{sid}** ({score_str})<br>{title}"
# 방법별 섹션
# 1. TF-IDF
lines.append("## 키워드 매칭 — TF-IDF")
lines.append("")
lines.append("각 후보 Figma와의 **공유 단어(IDF 기여도 순 상위 10개)**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["TF-IDF"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = tfidf_diag.get(fid, [])
if common:
lines.append("| 단어 | MDX TF-IDF | Figma TF-IDF | 기여도 |")
lines.append("|------|------------|--------------|--------|")
for w, mdx_w, figma_w, contrib in common:
lines.append(f"| {w} | {mdx_w:.3f} | {figma_w:.3f} | {contrib:.4f} |")
else:
lines.append("공유 단어 없음")
lines.append("")
# 2. Char 3-gram
lines.append("## 키워드 매칭 — Char 3-gram")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Char 3-gram"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = charng_diag.get(fid, [])
if common:
sample = ", ".join(f"`{s}`" for s in common[:20])
lines.append(f"**공유된 3-gram 조각 (상위 20개)**: {sample}")
else:
lines.append("공유 3-gram 없음")
lines.append("")
# 3. Kiwi+BM25
lines.append("## 키워드 매칭 — Kiwi+BM25")
lines.append("")
lines.append("Kiwi 형태소 추출 후 **공유 형태소 × IDF 기여도 상위 10개**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Kiwi+BM25"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — BM25 점수 {score:.1f}")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = bm25_diag.get(fid, [])
if common:
lines.append("| 형태소 | MDX TF | Figma TF | IDF | 기여도 |")
lines.append("|-------|--------|----------|-----|--------|")
for w, mc, fc, widf, contrib in common:
lines.append(f"| {w} | {mc} | {fc} | {widf:.2f} | {contrib:.2f} |")
else:
lines.append("공유 형태소 없음")
lines.append("")
# 4. Distinctive-Kiwi
lines.append("## 키워드 매칭 — Distinctive-Kiwi")
lines.append("")
lines.append(f"**공통어로 제거된 단어**: {', '.join(sorted(general_words))}")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Distinctive-Kiwi"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
common = distinct_diag.get(fid, [])
if common:
lines.append("| 고유 형태소 | MDX TF | Figma TF | IDF | 기여도 |")
lines.append("|----------|--------|----------|-----|--------|")
for w, mc, fc, widf, contrib in common:
lines.append(f"| {w} | {mc} | {fc} | {widf:.2f} | {contrib:.2f} |")
else:
lines.append("공유 고유 형태소 없음")
lines.append("")
# 5. 구조 메타
lines.append("## 구조 매칭 — 구조 메타")
lines.append("")
lines.append(f"**MDX 구조 피처**: {mdx_feat}")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["구조 메타"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
feat = struct_diag.get(fid, {}).get("figma", {})
lines.append("| 피처 | MDX | Figma |")
lines.append("|------|-----|-------|")
for key in mdx_feat:
lines.append(f"| {key} | {mdx_feat[key]} | {feat.get(key, 0)} |")
lines.append("")
# 6. 청킹+SBERT
lines.append("## 의미 매칭 — 청킹+SBERT")
lines.append("")
lines.append("MDX와 Figma 각각 블릿/섹션 단위로 쪼갠 뒤 **가장 유사한 청크 쌍 Top 3**")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["청킹+SBERT"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 평균 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
pairs = sbert_diag.get(fid, [])
if pairs:
lines.append("| 유사도 | MDX 청크 | Figma 청크 |")
lines.append("|--------|---------|------------|")
for sim, mc, fc in pairs:
lines.append(f"| {sim * 100:.0f}% | {trunc(mc, 40)} | {trunc(fc, 40)} |")
lines.append("")
# 7. 청킹+E5
lines.append("## 의미 매칭 — 청킹+E5")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["청킹+E5"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"### {rank}순위 Frame **{sid}**{mark} — 평균 유사도 {score * 100:.0f}%")
lines.append("")
lines.append(f"![{sid}]({png_rel}{idx_data.get(sid, {}).get('png', '')})")
lines.append("")
pairs = e5_diag.get(fid, [])
if pairs:
lines.append("| 유사도 | MDX 청크 | Figma 청크 |")
lines.append("|--------|---------|------------|")
for sim, mc, fc in pairs:
lines.append(f"| {sim * 100:.0f}% | {trunc(mc, 40)} | {trunc(fc, 40)} |")
lines.append("")
# 8. Cross
lines.append("## 의미 매칭 — Cross")
lines.append("")
lines.append("*Cross-encoder는 질의-문서 쌍을 통째로 채점해서 단어/청크 수준 설명 불가. 원 점수만 표시.*")
lines.append("")
for rank, (fid, score) in enumerate(results_by_method["Cross"], 1):
sid = frame_to_short.get(str(fid), "?")
mark = "" if sid == correct_id else ""
lines.append(f"- {rank}순위 Frame **{sid}**{mark} — 점수 {score * 100:.1f}%")
lines.append("")
out_path = Path(__file__).parent / "MATRIX_PHASE13.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out_path}")
if __name__ == "__main__":
main()