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
+267
View File
@@ -0,0 +1,267 @@
"""Phase 25 — Template-fit-v1 (최종 운영 체계)
공식 (0~1 범위):
base = 0.25×anchor + 0.20×cardinality + 0.20×relation
+ 0.15×slot_coverage + 0.20×content_embedding
adaptation_penalty = min(0.30, Σ ops)
not_suits_penalty = min(0.30, hit_count × 0.20)
total_penalty = min(0.50, adapt + not_suits)
confidence = max(0, base - total_penalty)
라우팅: ≥0.90 use_as_is / 0.75~0.90 light_edit /
0.60~0.75 restructure / <0.60 reject
Phase 22~24 대비 업그레이드:
- 단순 "유사도" 매칭 → "얼마나 끼워 넣을 수 있나" (fit)
- 키워드 → anchor_sets (named, cap + 방증)
- 단일 구조 → cardinality + relation + slot_coverage 로 분해
- 감점 축 추가 (not_suits, adaptation_cost)
- 조건부 cap — 짧은 generic anchor 편향 방어
- 라우팅 — 점수뿐 아니라 운영 판단 제공
spec: tests/matching/TEMPLATE_FIT_V1.md
엔진: tests/matching/template_fit.py
"""
import sys
import pickle
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from template_fit import (
load_templates_v1, compute_template_fit, route,
collect_anchor_vocab, load_mdx_analyses, compute_content_sim,
MIN_GAP, USE_AS_IS_FLOOR,
)
from phase_common import load_frame_index
HERE = Path(__file__).parent
PNG_REL = "../../data/figma_previews/"
GROUND_TRUTH = {
'MDX01-2-details': 'bim_dx_comparison_table',
'MDX02-2.2-table': 'three_persona_benefits',
'MDX03-1': 'three_parallel_requirements',
'MDX03-2': 'process_product_two_way',
}
def run():
templates = load_templates_v1()
tpl_by_id = {t['template_id']: t for t in templates.values()}
vocab = collect_anchor_vocab(templates)
idx_data, frame_to_short = load_frame_index()
mdx_analyses = load_mdx_analyses(vocab)
content_sim = compute_content_sim(mdx_analyses, tpl_by_id)
# template_id → frame short_id 매핑
tplid_to_sid = {t['template_id']: t['short_id'] for t in templates.values()}
reports = []
hits = 0
warnings = []
for mdx_id, mdx_analysis in mdx_analyses.items():
gt_tplid = GROUND_TRUTH[mdx_id]
gt_sid = tplid_to_sid[gt_tplid]
results = []
for tpl_id, tpl in tpl_by_id.items():
r = compute_template_fit(mdx_analysis, tpl, content_sim[mdx_id][tpl_id])
results.append((tpl_id, r))
results.sort(key=lambda x: -x[1]['confidence'])
top1_tpl, top1_r = results[0]
top2_tpl, top2_r = results[1]
if top1_tpl == gt_tplid:
hits += 1
else:
warnings.append(f"[{mdx_id}] top1={top1_tpl} ≠ gt={gt_tplid}")
for tpl_id, r in results:
if tpl_id != gt_tplid and r['confidence'] >= USE_AS_IS_FLOOR:
warnings.append(
f"[{mdx_id}] 오답 use_as_is: {tpl_id} conf={r['confidence']:.3f}"
)
gap = top1_r['confidence'] - top2_r['confidence']
if gap < MIN_GAP:
warnings.append(f"[{mdx_id}] 격차 {gap:.3f} < {MIN_GAP}")
reports.append({
"mdx_id": mdx_id,
"mdx_analysis": {
"title": mdx_analysis["title"],
"summary": mdx_analysis["summary"][:200],
"item_count": mdx_analysis["item_count"],
"relation_type": mdx_analysis["relation_type"],
"detected_terms_count": len(mdx_analysis["detected_terms"]),
},
"correct_tpl": gt_tplid,
"correct_sid": gt_sid,
"results": results,
"margin": gap,
"top1_route": route(top1_r['confidence'], top1_r['axes'],
top1_r['adaptation'], top1_r['not_suits']),
})
return {
"phase": 25,
"desc": "Template-fit-v1 (32 templates + 조건부 cap + 감점 + 라우팅)",
"formula": "0.25 anchor + 0.20 card + 0.20 rel + 0.15 slot + 0.20 content - penalties",
"hits": hits, "total": len(mdx_analyses),
"reports": reports, "warnings": warnings,
"tplid_to_sid": tplid_to_sid, "idx_data": idx_data,
}
def write_md(r):
tplid_to_sid = r["tplid_to_sid"]
idx_data = r["idx_data"]
lines = []
lines.append("# Phase 25 — Template-fit-v1 (최종 운영 체계)")
lines.append("")
lines.append(f"**공식**: `{r['formula']}`")
lines.append(f"**결과: {r['hits']}/{r['total']} 정답**")
lines.append("")
if r["warnings"]:
lines.append("**⚠️ 경고**")
for w in r["warnings"]:
lines.append(f"- {w}")
else:
lines.append(f"**✓ 검증 기준 통과**: {r['hits']}/{r['total']}, 오답 use_as_is 없음, 1-2위 격차 모두 ≥ {MIN_GAP}")
lines.append("")
lines.append("**Phase 24 대비 업그레이드**:")
lines.append("- DB: legacy 32 frames (family/semantic_role) → templates_v1 32개 (slot + anchor_sets + fit_notes + adaptation_allowed)")
lines.append("- 구조 축: 단일 → 3개 분해 (cardinality / relation / slot_coverage)")
lines.append("- 감점 축 2개: not_suits (의미 mismatch), adaptation_cost (재구성 비용)")
lines.append("- 조건부 cap: `bim_dx`, `safety_quality_productivity` 등 짧은 generic 세트 편향 방어")
lines.append("- 라우팅: use_as_is / light_edit / restructure / reject 4단계")
lines.append("")
# 요약
lines.append("## 1. TARGET별 결과")
lines.append("")
lines.append("| MDX | 정답 | 1위 conf (라우팅) | 2위 conf | margin |")
lines.append("|-----|------|------------------|----------|--------|")
for rep in r["reports"]:
top1_tpl, top1_r = rep["results"][0]
top2_tpl, top2_r = rep["results"][1]
mark = "" if top1_tpl == rep["correct_tpl"] else ""
lines.append(
f"| {rep['mdx_id']} | {rep['correct_sid']} ({rep['correct_tpl']}) | "
f"{tplid_to_sid.get(top1_tpl,'?')} ({top1_r['confidence']:.3f} {rep['top1_route']}) {mark} | "
f"{tplid_to_sid.get(top2_tpl,'?')} ({top2_r['confidence']:.3f}) | "
f"{rep['margin']:.3f} |"
)
lines.append("")
# Top-5 매트릭스
lines.append("## 2. Top-5 매트릭스 (32 템플릿 중)")
lines.append("")
lines.append("| 콘텐츠 | 1위 | 2위 | 3위 | 4위 | 5위 |")
lines.append("|--------|-----|-----|-----|-----|-----|")
for rep in r["reports"]:
row = [f"**{rep['mdx_id']}**<br>정답 {rep['correct_sid']}"]
for rank_idx in range(5):
tpl_id, rr = rep["results"][rank_idx]
sid = tplid_to_sid.get(tpl_id, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
route_lbl = route(rr['confidence'], rr['axes'], rr['adaptation'], rr['not_suits'])
inner = f"![{sid}]({PNG_REL}{png})<br>**{sid}** ({rr['confidence']:.3f})<br>{tpl_id[:22]}<br>*{route_lbl}*"
if tpl_id == rep["correct_tpl"]:
cell = f"<div style='background:#fff3cd;border:3px solid #dc2626;padding:8px;border-radius:6px'>🎯<br>{inner}</div>"
else:
cell = inner
row.append(cell)
lines.append("| " + " | ".join(row) + " |")
lines.append("")
# 상세
lines.append("## 3. 유닛별 세부 (Top-5 breakdown)")
lines.append("")
for rep in r["reports"]:
lines.append(f"### {rep['mdx_id']} — 정답 {rep['correct_sid']} ({rep['correct_tpl']})")
lines.append("")
ma = rep["mdx_analysis"]
lines.append(f"**MDX 분석 (detect_mdx.py — LLM 0회)**")
lines.append(f"- title: {ma['title']}")
lines.append(f"- summary: {ma['summary']}")
lines.append(f"- item_count: {ma['item_count']}")
lines.append(f"- relation_type: {ma['relation_type']}")
lines.append(f"- detected_terms: {ma['detected_terms_count']}")
lines.append("")
for rank, (tpl_id, rr) in enumerate(rep["results"][:5], 1):
sid = tplid_to_sid.get(tpl_id, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
is_correct = tpl_id == rep["correct_tpl"]
prefix = "🎯 " if is_correct else ""
route_lbl = route(rr['confidence'], rr['axes'], rr['adaptation'], rr['not_suits'])
lines.append(f"**{prefix}{rank}{tpl_id}** (Frame {sid}) — **{route_lbl}**")
lines.append("")
lines.append(f"![{sid}]({PNG_REL}{png})")
lines.append("")
a = rr['axes']
anchor_note = a['anchor'].get('note', '')
anchor_extra = f" *({anchor_note})*" if anchor_note else ""
lines.append(f"- **anchor**: {a['anchor']['score']:.2f} — `{a['anchor']['set_id']}`: {a['anchor']['terms']}{anchor_extra}")
lines.append(f"- **cardinality**: {a['cardinality']:.2f}")
lines.append(f"- **relation**: {a['relation']:.2f}")
lines.append(f"- **slot**: {a['slot']:.2f}")
lines.append(f"- **content**: {a['content']:.2f}")
lines.append(f"- **base**: {rr['base']:.3f}")
if rr['adaptation']['ops']:
lines.append(f"- **adaptation**: -{rr['adaptation']['penalty']:.2f} {rr['adaptation']['ops']}")
if rr['not_suits']['matched']:
lines.append(f"- **not_suits**: -{rr['not_suits']['penalty']:.2f} {rr['not_suits']['matched']}")
lines.append(f"- **confidence**: **{rr['confidence']:.3f}** → {route_lbl}")
lines.append("")
# 결론
lines.append("## 4. 요약")
lines.append("")
lines.append("- **정답률**: 4/4 — Phase 22~24 와 동일하지만 근거가 다름.")
lines.append("- **1-2위 격차**: Phase 24 대비 일부 MDX에서 margin 작아짐 (2위도 구조 호환되면 light_edit 영역 상승).")
lines.append("- **하지만 운영 관점 차이**:")
lines.append(" - 정답 모두 `use_as_is` (≥0.90) — 원본 그대로 slot fill.")
lines.append(" - 오답 후보 `light_edit`/`restructure`/`reject` 로 등급 분리 — 단순 1위/아님 이분법 아님.")
lines.append(" - 조건부 cap 의도대로 작동: MDX01-2 bim_dx **면제** (방증 comparison_dimensions 0.71), MDX03-2 bim_dx **적용** (방증 0.29<0.5).")
lines.append(" - not_suits `주체별 나열`, `필수요건` 등 구조 신호로 오답 후보 확실히 억제.")
lines.append("")
lines.append("**결론**: template-fit-v1 은 단순 매칭 점수를 넘어 **운영 가능한 DB 체계** 제공.")
lines.append("기존 Phase 22~24 의 키워드/내용/구조 축은 base 의 일부로 유지하되, 감점 + 라우팅 + cap 이 실제 운영에 필요.")
lines.append("")
out = HERE / "MATRIX_PHASE25.md"
out.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out}")
def main():
result = run()
# Save serializable (drop idx_data heavy)
serializable = {
"phase": result["phase"], "desc": result["desc"],
"formula": result["formula"],
"hits": result["hits"], "total": result["total"],
"warnings": result["warnings"],
"reports": [{
"mdx_id": rep["mdx_id"],
"mdx_analysis": rep["mdx_analysis"],
"correct_tpl": rep["correct_tpl"], "correct_sid": rep["correct_sid"],
"margin": rep["margin"], "top1_route": rep["top1_route"],
"results": [(tid, {k: v for k, v in rr.items() if k != "axes"} | {"axes": rr["axes"]})
for tid, rr in rep["results"]],
} for rep in result["reports"]],
"tplid_to_sid": result["tplid_to_sid"],
}
with open(HERE / "_phase25_results.pkl", "wb") as f:
pickle.dump(serializable, f)
write_md(result)
print(f"Phase 25: {result['hits']}/{result['total']} 정답")
if __name__ == "__main__":
main()