"""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']}**
정답 {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}** ({rr['confidence']:.3f})
{tpl_id[:22]}
*{route_lbl}*"
if tpl_id == rep["correct_tpl"]:
cell = f"