Files
KyeongminandClaude Opus 4.8 b836e79ee1 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>
2026-07-02 17:03:42 +09:00

373 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()