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
+372
View File
@@ -0,0 +1,372 @@
"""Phase 18~21 일괄 실행 + 각 Phase 별 MD/HTML 생성
Phase 18: 키워드만 (synonym X)
Phase 19: synonym + 키워드
Phase 20: synonym + 키워드 + 내용
Phase 21: synonym + 키워드 + 내용 + 구조
"""
import sys
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from phase_common import (
TARGET_UNITS, load_synonyms, load_32_frames, compute_df_idf_tier,
extract_mdx_keywords, keyword_score, content_scores_batch,
detect_mdx_layout_v2, structural_match_v2,
load_target_units, load_frame_index,
)
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
def match_phase(phase_num, use_synonyms, use_content, use_structure,
units_full, units_title, frames, df, idf, tier, vocab, synonyms,
idx_data, frame_to_short):
"""매칭 수행 + 결과 반환"""
# 가중치 설정
if phase_num == 18:
w_kw, w_content, w_struct = 1.0, 0.0, 0.0
elif phase_num == 19:
w_kw, w_content, w_struct = 1.0, 0.0, 0.0
elif phase_num == 20:
w_kw, w_content, w_struct = 0.5, 0.5, 0.0
elif phase_num == 21:
w_kw, w_content, w_struct = 0.5, 0.3, 0.2
reports = []
hits = 0
for uid, display, correct_sid, *_ in TARGET_UNITS:
text = units_full[uid]
title = units_title[uid]
syns = synonyms if use_synonyms else None
# MDX 키워드
mdx_kws = extract_mdx_keywords(text, vocab, synonyms=syns)
# 내용 (제목 계층)
mdx_content_query = title
# MDX 구조
mdx_layout = detect_mdx_layout_v2(text) if use_structure else ""
# 내용 점수 (한 번에)
c_scores = content_scores_batch(mdx_content_query, frames) if use_content else {}
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.get(fid, 0.0) if use_content else 0.0
s_s = structural_match_v2(mdx_layout, v["layout"]) if use_structure else 0.0
final = w_kw * k_s + w_content * c_s + w_struct * s_s
ranked.append((fid, final, {"kw": k_s, "content": c_s, "struct": s_s,
"inter": sorted(inter), "final": final}))
ranked.sort(key=lambda x: -x[1])
top3 = ranked[:3]
top_sid = frame_to_short.get(top3[0][0], "?")
if top_sid == correct_sid:
hits += 1
reports.append({
"uid": uid, "display": display, "correct_sid": correct_sid,
"mdx_kws": mdx_kws, "mdx_content": mdx_content_query,
"mdx_layout": mdx_layout if use_structure else None,
"top3": top3,
})
return reports, hits, (w_kw, w_content, w_struct)
def write_md(phase_num, description, reports, hits, weights, frames,
idx_data, frame_to_short):
w_kw, w_content, w_struct = weights
png_rel = "../../data/figma_previews/"
lines = []
lines.append(f"# Phase {phase_num}{description}")
lines.append("")
# 공식
formula_parts = []
if w_kw > 0: formula_parts.append(f"{w_kw} × 키워드")
if w_content > 0: formula_parts.append(f"{w_content} × 내용")
if w_struct > 0: formula_parts.append(f"{w_struct} × 구조")
lines.append(f"**공식**: `점수 = {' + '.join(formula_parts)}`")
lines.append("")
lines.append(f"**결과: {hits}/4 정답**")
lines.append("")
# 1. 수행 프로세스 (Phase 17 스타일 — 가로 Step 표 + 예시)
lines.append("## 1. 수행 프로세스")
lines.append("")
one_liner = {
18: "MDX에서 형태소 추출 → Figma 키워드 DB와 겹침만으로 매칭 (baseline).",
19: "MDX에 동의어 치환(synonym) 전처리 추가 → Figma 키워드 DB와 겹침으로 매칭.",
20: "키워드 매칭 + 제목 내용 의미 유사도(Cross-encoder) 합산.",
21: "키워드 + 내용 + 구조(표 헤더/섹션 감지)까지 3축 합산.",
}
lines.append(f"**{one_liner.get(phase_num, '')}**")
lines.append("")
# 헤더: Step1~5
step_titles = ["Step 1. Figma 정리", "Step 2. 단어 가중치 정리",
"Step 3. MDX 키워드 추출", "Step 4. 유사도 계산", "Step 5. 최종 선정"]
lines.append("| " + " | ".join(step_titles) + " |")
lines.append("|" + "|".join(["-------"] * 5) + "|")
# 내용 행 — phase별 축 반영
s3 = ("원문 →<br>**synonym 치환** →<br>Kiwi 형태소 추출 →<br>Figma vocab 교집합"
if phase_num >= 19 else
"원문 →<br>Kiwi 형태소 추출 →<br>Figma vocab 교집합")
formula_parts = []
if w_kw > 0: formula_parts.append(f"{w_kw} × 키워드")
if w_content > 0: formula_parts.append(f"{w_content} × 내용")
if w_struct > 0: formula_parts.append(f"{w_struct} × 구조")
s4 = f"공식:<br>`점수 = {' + '.join(formula_parts)}`<br>"
s4 += "① 키워드: IDF 가중 Jaccard"
if w_content > 0: s4 += "<br>② 내용: ko-reranker<br>(Cross-encoder 로컬)"
if w_struct > 0: s4 += "<br>③ 구조: MDX 표헤더/섹션 감지 → Figma layout family 매칭"
lines.append(
"| 각 Figma 프레임에 analysis.md 작성<br>(내용 1문장 + 후보 키워드 15~25개)"
" | 32개 프레임에서 키워드 등장 빈도(df) 기반 3단계 가중치<br>핵심×1.0, 중간×0.5, 일반×0.1"
f" | {s3}"
f" | {s4}"
" | 32개 프레임 점수 계산 → 1위 선정"
" |"
)
# MDX 유닛별 실제 추출 키워드 표시 (Phase 18/19 비교용)
if phase_num in (18, 19):
pass # 예시 행 이후에 추가 섹션 삽입
syn_example = ("'필수 요건''필수조건'<br>'디지털 전환''DX'<br>"
if phase_num >= 19 else "")
ex_kw = ("0.289" if phase_num == 18 else
"0.289" if phase_num == 19 else
"0.289" if phase_num == 20 else
"0.289")
ex_content = "0.731" if w_content > 0 else "-"
ex_struct = "1.00" if w_struct > 0 else "-"
ex_final_parts = [f"{w_kw}×0.289"]
if w_content > 0: ex_final_parts.append(f"{w_content}×0.731")
if w_struct > 0: ex_final_parts.append(f"{w_struct}×1.00")
# 실제 계산
ex_final_val = w_kw * 0.289 + w_content * 0.731 + w_struct * 1.00
ex_final = " + ".join(ex_final_parts) + f" = **{ex_final_val:.3f}**"
lines.append(
"| Frame 13 (필수조건)<br>"
f"![13]({png_rel}13.png)<br>"
"내용: \"DX 시행 3대 필수조건\"<br>키워드: 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건…"
" | '필수조건' 1/32→핵심×1.0<br>'여건' 1/32→핵심×1.0<br>'디지털' 5/32→중간×0.5"
f" | {syn_example}MDX03-1 본문 →<br>Kiwi 127개 →<br>Figma vocab ∩ 22개:<br>DX, 필수, 요건, 기술, 사람, 여건…"
f" | MDX03-1 ↔ Frame 13<br>키워드: {ex_kw}<br>"
+ (f"내용: {ex_content}<br>" if w_content > 0 else "")
+ (f"구조: {ex_struct}<br>" if w_struct > 0 else "")
+ f"최종: {ex_final}"
" | MDX03-1 결과<br>1위 Frame 13<br>2위 Frame 15<br>3위 Frame 18"
" |"
)
lines.append("")
# 1-B. Figma 기준 키워드 pool (Phase 18/19에만)
if phase_num in (18, 19):
# vocab + tier 정보 필요
from phase_common import compute_df_idf_tier
df, idf, tier, N = compute_df_idf_tier(frames)
vocab_all = sorted(set().union(*[set(f["keywords"]) for f in frames.values()]))
core = sorted([k for k in vocab_all if tier.get(k) == "core"], key=lambda k: -df.get(k, 0))
mid = sorted([k for k in vocab_all if tier.get(k) == "mid"], key=lambda k: -df.get(k, 0))
gen = sorted([k for k in vocab_all if tier.get(k) == "general"], key=lambda k: -df.get(k, 0))
lines.append("## 1-B. Figma 기준 추출 키워드 (매칭 pool)")
lines.append("")
lines.append(f"**32개 프레임 analysis.md의 후보 키워드 합집합 = {len(vocab_all)}개** "
f"(이 pool이 매칭의 기준이며 **Phase 18/19 동일**)")
lines.append("")
lines.append(f"- 핵심 (1~3 프레임만 등장, `×1.0`): **{len(core)}개**")
lines.append(f"- 중간 (4~9 프레임, `×0.5`): **{len(mid)}개**")
lines.append(f"- 일반 (10+ 프레임, `×0.1`): **{len(gen)}개**")
lines.append("")
if phase_num == 19:
lines.append("> **Phase 19 차이점**: 이 pool은 동일하지만, MDX 본문을 Kiwi 처리 전에 "
"`synonyms.yaml` (8개)로 치환. 예: MDX의 `\"필수 요건\"` → `\"필수조건\"` "
"→ pool에 있는 canonical 표기로 정규화 → 교집합 성공.")
lines.append("")
lines.append("**synonyms.yaml (Phase 19 적용되는 치환 규칙):**")
lines.append("")
lines.append("| Canonical (pool에 이미 존재) | MDX에서 치환 대상 Variants |")
lines.append("|------|-----------|")
for canonical, variants in synonyms.items():
vs = ", ".join(f"`{v}`" for v in variants)
lines.append(f"| `{canonical}` | {vs} |")
lines.append("")
# 핵심 키워드 예시 (빈도 상위 30)
lines.append("### 핵심 키워드 예시 (distinctive, 1~3 프레임만 등장) — 상위 30개")
lines.append("")
lines.append(", ".join(f"`{k}`" for k in core[:30]))
lines.append(f"<br>... (전체 {len(core)}개 중 30개)")
lines.append("")
# 중간 키워드 전체
if mid:
lines.append(f"### 중간 키워드 전체 ({len(mid)}개, 4~9 프레임)")
lines.append("")
lines.append(", ".join(f"`{k}` (df={df.get(k, 0)})" for k in mid))
lines.append("")
# 일반 키워드
if gen:
lines.append(f"### 일반 키워드 ({len(gen)}개, 10+ 프레임)")
lines.append("")
lines.append(", ".join(f"`{k}` (df={df.get(k, 0)})" for k in gen))
lines.append("")
else:
lines.append("### 일반 키워드")
lines.append("")
lines.append("(없음 — corpus 규모가 작아 10+ 프레임 등장 키워드 없음)")
lines.append("")
# 2. 테스트 결과 (매트릭스)
lines.append("## 2. 테스트 결과")
lines.append("")
lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |")
lines.append("|--------|-------|-------|-------|")
for r in reports:
row = [f"**{r['display']}**<br>정답 Frame **{r['correct_sid']}**"]
for rank_idx in range(3):
fid, score, bd = r["top3"][rank_idx]
sid = frame_to_short.get(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] + ""
inner = f"![{sid}]({png_rel}{png})<br>**{sid}** ({score:.3f})<br>{title}"
if sid == r["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("")
# 3. 테스트 결과 세부
lines.append("## 3. 테스트 결과 세부")
lines.append("")
for r in reports:
lines.append(f"### {r['display']} — 정답 Frame **{r['correct_sid']}**")
lines.append("")
lines.append("**MDX 추출 사항**")
lines.append("")
lines.append(f"- **내용**: {r['mdx_content']}")
lines.append(f"- **키워드** ({len(r['mdx_kws'])}개): {', '.join(sorted(r['mdx_kws']))}")
if r.get("mdx_layout"):
lines.append(f"- **구조**: {r['mdx_layout']}")
lines.append("")
for rank, (fid, score, bd) in enumerate(r["top3"], 1):
sid = frame_to_short.get(fid, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
fig_content = frames[fid].get("content", "")
fig_layout = frames[fid].get("layout", "")
common = ", ".join(bd["inter"]) if bd["inter"] else "(없음)"
is_correct = sid == r["correct_sid"]
if is_correct:
lines.append(f"**<span style='background:#fff3cd;border:2px solid #dc2626;padding:2px 8px;border-radius:3px'>🎯 {rank}위 Frame {sid} (정답)</span>**")
else:
lines.append(f"**{rank}위 Frame {sid}**")
lines.append("")
lines.append(f"![{sid}]({png_rel}{png})")
lines.append("")
lines.append(f"- **매칭 내용**: {fig_content}")
lines.append(f"- **매칭 키워드**: {common}")
# 구조 축이 있을 때 Figma 구조 + 매칭 사유 표시
if w_struct > 0:
mdx_layout = r.get("mdx_layout") or ""
sc = bd["struct"]
if sc >= 1.0:
verdict = "정확 일치 (동일 구조)"
elif sc >= 0.75:
verdict = "매우 호환 (2/3열 동일, 표현만 다름)"
elif sc >= 0.5:
verdict = "부분 호환 (일부 축 공유)"
elif sc >= 0.3:
verdict = "약한 호환 (컬럼 수/의도 다름)"
else:
verdict = "거의 무관"
lines.append(f"- **Figma 구조**: `{fig_layout}`")
lines.append(f"- **구조 매칭 사유**: `{mdx_layout}` ↔ `{fig_layout}` → **{verdict}** → 점수 {sc:.2f}")
# 축별 점수 (해당 축만)
score_parts = [f"키워드 {bd['kw']:.3f}"]
if w_content > 0: score_parts.append(f"내용 {bd['content']:.3f}")
if w_struct > 0: score_parts.append(f"구조 {bd['struct']:.2f}")
lines.append(f"- **축별 점수**: {' / '.join(score_parts)}")
lines.append(f"- **매칭 점수 (최종)**: **{score:.3f}**")
lines.append("")
return "\n".join(lines)
def main():
synonyms = load_synonyms()
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()
print(f"Figma: {N}개, Vocab: {len(vocab)}")
print(f"Tier: 핵심 {sum(1 for t in tier.values() if t=='core')}개, "
f"중간 {sum(1 for t in tier.values() if t=='mid')}개, "
f"일반 {sum(1 for t in tier.values() if t=='general')}")
print()
configs = [
(18, "키워드만 (baseline)", False, False, False),
(19, "synonym + 키워드", True, False, False),
(20, "synonym + 키워드 + 내용", True, True, False),
(21, "synonym + 키워드 + 내용 + 구조", True, True, True),
]
results_all = {}
for phase_num, desc, use_syn, use_cont, use_str in configs:
print(f"=== Phase {phase_num}: {desc} ===")
reports, hits, weights = match_phase(
phase_num, use_syn, use_cont, use_str,
units_full, units_title, frames, df, idf, tier, vocab, synonyms,
idx_data, frame_to_short,
)
results_all[phase_num] = {"reports": reports, "hits": hits, "weights": weights, "desc": desc}
for r in reports:
top_sid = frame_to_short.get(r["top3"][0][0], "?")
mark = "" if top_sid == r["correct_sid"] else ""
print(f" {r['uid']}(정답={r['correct_sid']}) → Top1={top_sid} ({r['top3'][0][1]:.3f}) {mark}")
print(f"{hits}/4")
print()
md = write_md(phase_num, desc, reports, hits, weights, frames,
idx_data, frame_to_short)
out = Path(__file__).parent / f"MATRIX_PHASE{phase_num}.md"
out.write_text(md, encoding="utf-8")
# results_all 저장 (phase22에서 재사용)
import pickle
pkl = Path(__file__).parent / "_phase18_21_results.pkl"
# pickle엔 reports만 직렬화하려면 lambda 등 빼야 — simplify
# just store minimal for phase22
serializable = {}
for pn, d in results_all.items():
serializable[pn] = {
"desc": d["desc"], "hits": d["hits"], "weights": d["weights"],
"reports": [{
"uid": r["uid"], "display": r["display"], "correct_sid": r["correct_sid"],
"mdx_kws_count": len(r["mdx_kws"]),
"top3": [(fid, score, frame_to_short.get(fid, "?"), bd) for fid, score, bd in r["top3"]],
} for r in d["reports"]]
}
with open(pkl, "wb") as f:
pickle.dump(serializable, f)
print(f"\n저장: {pkl}")
print("\n각 Phase MD 생성 완료.")
if __name__ == "__main__":
main()