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
+328
View File
@@ -0,0 +1,328 @@
"""Phase 17 — 32개 프레임 analysis.md 전체 로드 + 3축 매칭 (runtime df 분류)
Phase 16은 5개 v2 샘플 yaml로 검증했는데, 이제 실전 32개 analysis.md로 재검증.
analysis.md = 정적 (구조/내용/후보키워드만)
runtime에 32개 로드 → df 계산 → 핵심/중간/일반 자동 분류 → 매칭
"""
import sys
import re
import math
import collections
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
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")
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
PREVIEW_DIR = ROOT / "data" / "figma_previews"
def parse_analysis_md(path):
"""analysis.md 파싱 → {layout, detail, content, keywords, ...}"""
text = path.read_text(encoding="utf-8")
result = {"keywords": [], "layout": "", "detail": "", "content": ""}
# Sections by ## headers
sections = re.split(r"\n## ", text)
for sec in sections[1:]:
lines = sec.split("\n", 1)
heading = lines[0].strip()
body = lines[1].strip() if len(lines) > 1 else ""
if heading == "구조":
for ln in body.split("\n"):
m = re.match(r"- \*\*layout\*\*:\s*(.+)", ln)
if m:
result["layout"] = m.group(1).strip()
m = re.match(r"- \*\*detail\*\*:\s*(.+)", ln)
if m:
result["detail"] = m.group(1).strip()
elif heading == "내용":
# 첫 문장 블록
result["content"] = body.split("\n\n")[0].strip()
elif heading == "후보 키워드":
# 쉼표로 분리
raw = body.split("\n\n")[0]
kws = [k.strip() for k in raw.split(",") if k.strip()]
result["keywords"] = kws
return result
def load_32_frames():
"""32개 프레임의 analysis.md를 로드"""
frames = {}
for d in sorted(BLOCKS_DIR.iterdir()):
if not d.is_dir():
continue
fid = d.name
if not fid.startswith("1171"):
continue
a = d / "analysis.md"
if a.exists():
frames[fid] = parse_analysis_md(a)
return frames
def compute_df_classes(frames):
"""전체 corpus에서 df 계산 → 후보 키워드별 핵심/중간/일반 분류
비율 기준: df/N ≤ 0.1 = 핵심, ≥ 0.3 = 일반, 그 사이 = 중간"""
df = collections.Counter()
N = len(frames)
for fid, v in frames.items():
for kw in set(v["keywords"]):
df[kw] += 1
tier = {}
for kw, cnt in df.items():
ratio = cnt / N
if ratio <= 0.10:
tier[kw] = "core"
elif ratio >= 0.30:
tier[kw] = "general"
else:
tier[kw] = "mid"
idf = {kw: math.log(N / cnt) if cnt > 0 else 0 for kw, cnt in df.items()}
return df, tier, idf, N
def extract_mdx_keywords(mdx_text, vocabulary):
"""MDX에서 Kiwi로 추출 후 vocabulary 교집합 (canonicalization)"""
kiwi = _get_kiwi()
tokens = _extract_content_tokens(mdx_text, kiwi)
return set(tokens) & vocabulary
def structural_match(mdx_layout, fig_layout):
if not mdx_layout or not fig_layout:
return 0.0
if mdx_layout == fig_layout:
return 1.0
if "3col" in mdx_layout and "3col" in fig_layout:
return 0.5
if "persona-3col" in fig_layout and "3col" in mdx_layout:
return 0.5
if "2col" in mdx_layout and "2col" in fig_layout:
return 0.5
if "table" in mdx_layout and "table" in fig_layout:
return 0.5
return 0.0
def match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier,
w_kw=0.5, w_struct=0.2, w_content=0.3):
"""3축 매칭 — 핵심/중간/일반 가중 Jaccard + 구조 + 내용(Cross-encoder)"""
model = _get_cross_encoder()
# 내용 쌍 일괄 예측
pairs = [[mdx_content, frames[fid]["content"]] for fid in frames]
fids = list(frames.keys())
content_raw = model.predict(pairs, show_progress_bar=False)
# sigmoid로 [0,1] 정규화
content_scores = {fids[i]: 1 / (1 + math.exp(-float(content_raw[i])))
for i in range(len(fids))}
scores = []
for fid, v in frames.items():
fig_kws = set(v["keywords"])
fig_layout = v["layout"]
# 핵심키워드 Jaccard with tier weights
# core: idf full, mid: idf*0.5, general: idf*0.1
inter = mdx_kws & fig_kws
union = mdx_kws | fig_kws
tier_weight = {"core": 1.0, "mid": 0.5, "general": 0.1}
num = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in inter)
den = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in union)
kw_s = num / den if den > 0 else 0
struct_s = structural_match(mdx_layout, fig_layout)
content_s = content_scores[fid]
final = w_kw * kw_s + w_struct * struct_s + w_content * content_s
scores.append((fid, final, {
"kw": kw_s, "struct": struct_s, "content": content_s,
"final": final, "inter": sorted(inter),
}))
scores.sort(key=lambda x: -x[1])
return scores
def main():
units = extract_units()
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()}
# 32 frames 로드
frames = load_32_frames()
print(f"로드된 프레임: {len(frames)}")
# df 분류
df, tier, idf, N = compute_df_classes(frames)
print(f"corpus: N={N}")
core_count = sum(1 for t in tier.values() if t == "core")
mid_count = sum(1 for t in tier.values() if t == "mid")
gen_count = sum(1 for t in tier.values() if t == "general")
print(f"키워드 분류: 핵심 {core_count}개, 중간 {mid_count}개, 일반 {gen_count}개 (총 {len(tier)}개)")
# vocabulary
vocab = set()
for v in frames.values():
vocab.update(v["keywords"])
print(f"vocabulary 크기: {len(vocab)}")
# 일반(general) 키워드 상위 10개 (어떤 게 일반어로 분류됐나)
general_kws = sorted([kw for kw, t in tier.items() if t == "general"],
key=lambda k: -df[k])
print(f"일반 키워드 (df 많음 상위): {general_kws[:10]}")
print()
# 4 MDX 유닛 매칭
print("=" * 70)
print("4개 MDX 유닛 × 32개 프레임 3축 매칭")
print("=" * 70)
hits = 0
reports = []
for uid, display, correct_sid, *_ in TARGET_UNITS:
text = units[uid]
mdx_layout = _detect_mdx_layout(text)
mdx_content = mdx_titles[uid]
mdx_kws = extract_mdx_keywords(text, vocab)
ranked = match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier)
top3 = ranked[:3]
top_fid, top_score, top_bd = top3[0]
top_sid = frame_to_short.get(top_fid, "?")
mark = "" if top_sid == correct_sid else ""
if top_sid == correct_sid:
hits += 1
print(f"{uid} (정답={correct_sid})")
print(f" MDX 구조={mdx_layout}, 키워드 {len(mdx_kws)}개 ({sorted(mdx_kws)[:8]}...)")
print(f" Top1: Frame {top_sid} ({top_score:.3f}) {mark}")
print(f" kw={top_bd['kw']:.3f}, struct={top_bd['struct']:.2f}, content={top_bd['content']:.3f}")
reports.append((uid, display, correct_sid, top3, mdx_layout, mdx_content, mdx_kws))
print(f"\n=== 총 {hits}/4 ===")
# MD 리포트
png_rel = "../../data/figma_previews/"
def frame_cell(sid, score):
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] + ""
return f"![{sid}]({png_rel}{png})<br>**{sid}** ({score:.3f})<br>{title}"
lines = []
lines.append("# Phase 17 — MDX ↔ Figma 매칭 체계")
lines.append("")
# ═══ 1. 수행 프로세스 (요약) ═══
lines.append("## 1. 수행 프로세스 (요약)")
lines.append("")
# 헤더 한 줄: Step N + 제목 결합. 아래 행: 내용 / 예시
lines.append("| Step 1. Figma 정리 | Step 2. 단어 가중치 정리 | Step 3. MDX 키워드 추출 | Step 4. 유사도 계산 | Step 5. 최종 선정 |")
lines.append("|-------------------|------------------------|-----------------------|---------------------|-------------------|")
# 내용 행
lines.append(
"| 각 Figma 프레임마다 analysis.md 작성 (내용 요약 1문장 + 후보 키워드 15~25개)"
" | 32개 프레임에서 각 키워드 등장 프레임 수(df) 세어 3단계 가중치 부여<br>핵심(1~3개)×1.0, 중간(4~9개)×0.5, 일반(10개↑)×0.1"
" | MDX 본문을 Kiwi 형태소 분석기로 추출 → Figma 키워드 DB에 있는 단어만 남김"
" | 공식: `점수 = 0.5 × 키워드 겹침 + 0.3 × 내용 유사도`<br>① 키워드: IDF 가중 Jaccard<br>② 내용: ko-reranker(Cross-encoder) 로컬 모델"
" | 32개 프레임 전부 점수 계산 → Top-1 선정"
" |"
)
# 예시 행
lines.append(
"| Frame 13 (필수조건)<br>"
f"![13]({png_rel}13.png)<br>"
"내용: \"DX 시행 3대 필수조건 — 기술/사람/자연\"<br>키워드: 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건, 기반, 창의성, 3요소…"
" | '필수조건' 1/32 → 핵심 ×1.0<br>'여건' 1/32 → 핵심 ×1.0<br>'디지털' 5/32 → 중간 ×0.5"
" | MDX03-1 원문: \"DX 시행을 위한 필수 요건 — 기술·사람·자연\"<br>Kiwi → 127개 토큰<br>Figma DB 교집합 → 22개: DX, 필수, 요건, 기술, 디지털, 사람, 역량, 자연, 여건…"
" | MDX03-1 vs Frame 13<br>키워드 점수: 0.289<br>내용 점수: 0.731<br>최종: 0.5×0.289 + 0.3×0.731 = **0.364**"
" | MDX03-1 결과<br>1위 Frame 13 → 0.364<br>2위 Frame 15 → 0.269<br>3위 Frame 18 → 0.193"
" |"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 2. 테스트 결과")
lines.append("")
lines.append(f"**결과: {hits}/4 정답**")
lines.append("")
lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |")
lines.append("|--------|-------|-------|-------|")
for uid, display, correct_sid, top3_full, mdx_layout, mdx_content, mdx_kws in reports:
row = [f"**{display}**<br>정답 Frame **{correct_sid}**"]
for rank_idx in range(3):
fid, score, bd = top3_full[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 == 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("---")
lines.append("")
lines.append("## 3. 테스트 결과 세부")
lines.append("")
for uid, display, correct_sid, top3_full, mdx_layout, mdx_content, mdx_kws in reports:
text = units[uid]
ranked = match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier)
top3 = ranked[:3]
lines.append(f"### {display} — 정답 Frame **{correct_sid}**")
lines.append("")
lines.append("**MDX 추출 사항**")
lines.append("")
lines.append(f"- **내용**: {mdx_content}")
lines.append(f"- **키워드** ({len(mdx_kws)}개): {', '.join(sorted(mdx_kws))}")
lines.append("")
# 순위별 매칭 내용 / 키워드 / 점수
for rank, (fid, score, bd) in enumerate(top3, 1):
sid = frame_to_short.get(fid, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
fig_content = frames[fid].get("content", "")
common_kws = ", ".join(bd["inter"]) if bd["inter"] else "(없음)"
is_correct = sid == 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_kws}")
lines.append(f"- **매칭 점수**: **{score:.3f}**")
lines.append("")
out_path = Path(__file__).parent / "MATRIX_PHASE17.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"\n완료: {out_path}")
if __name__ == "__main__":
main()