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
+195
View File
@@ -0,0 +1,195 @@
"""Phase 21b용 구조 ontology 기반 매칭
- detect_mdx_structure_v3(): MDX 본문에서 같은 schema 속성 emit
- structural_match_v3(): 속성 교집합 기반 score (0~1)
- 가중치는 tie-breaker 수준 (0.05~0.10)
- confidence high 일 때만 점수 부여
"""
import re
import yaml
from pathlib import Path
HERE = Path(__file__).parent
def load_ontology():
p = HERE / "structure_ontology.yaml"
with open(p, encoding="utf-8") as f:
return yaml.safe_load(f)["frames"]
# ═══ MDX 구조 감지 v3 ═══
def detect_mdx_structure_v3(text, mdx_title=""):
"""schema emit: {family, surface, columns, items, has_table, has_cards, semantic_role, confidence}"""
lines = text.split("\n")
result = {
"family": None, "surface": None, "semantic_role": None,
"columns": None, "items": None,
"has_table": False, "has_cards": False, "has_diagram": False,
"confidence": "low",
}
# 1. ### X.Y 서브섹션 감지
subs = [ln for ln in lines if re.match(r"^###\s+\d+\.\d+", ln)]
subsection_texts = [ln for ln in subs]
n_subs = len(subs)
# 2. 표 감지
table_header = None
for ln in lines:
stripped = ln.strip()
if re.match(r"^\|.*\|.*\|", stripped) and not re.match(r"^\|[\s\-:]+\|", stripped):
table_header = stripped
break
# 3. 최상위 볼드 블릿 수
top_bullets = [ln for ln in lines if re.match(r"^[-*]\s+\*\*", ln)]
n_bullets = len(top_bullets)
full_text = (text + " " + mdx_title).lower()
# ═══ semantic_role 감지 (figma_audit과 동일 — best-match + priority) ═══
from figma_audit import _SEMANTIC_HINT_PATTERNS, _ROLE_PRIORITY
candidates = []
for role_name, req_kws, min_req in _SEMANTIC_HINT_PATTERNS:
hits = sum(1 for k in req_kws if k in full_text)
if hits >= min_req:
ratio = hits / len(req_kws)
candidates.append((hits, ratio, role_name))
if candidates:
# 1) hits 많음 2) ratio 높음 3) priority 높음
candidates.sort(key=lambda x: (-x[0], -x[1], -_ROLE_PRIORITY.get(x[2], 0)))
result["semantic_role"] = candidates[0][2]
else:
result["semantic_role"] = None
# ═══ family / surface / columns 분기 ═══
# 우선순위: ### 서브섹션 > 표 > 블릿
if n_subs >= 2:
# 과정/결과 비교 → compare family
sub_lower = " ".join(subsection_texts).lower()
if any(kw in sub_lower for kw in ["과정", "결과", "process", "product"]):
result["family"] = "compare"
result["surface"] = "banner-plus-2col"
result["columns"] = 2
result["items"] = 2
result["confidence"] = "high"
elif n_subs == 2:
result["family"] = "compare"
result["surface"] = "2col-paired"
result["columns"] = 2
result["items"] = 2
result["confidence"] = "high"
else:
result["family"] = "list"
result["surface"] = "cards-3-horizontal"
result["columns"] = n_subs
result["items"] = n_subs
result["confidence"] = "medium"
elif table_header:
result["has_table"] = True
cols = [c.strip().replace("*", "").lower() for c in table_header.strip("|").split("|") if c.strip()]
n_cols = len(cols) - 1 # 첫 열 = label
col_text = " ".join(cols)
if any(kw in col_text for kw in ["발주자", "시공자", "설계자"]):
result["family"] = "cards"
result["surface"] = "table-persona-3col"
result["semantic_role"] = result.get("semantic_role") or "persona-benefits"
result["columns"] = 3
result["items"] = 3
result["has_cards"] = True
result["confidence"] = "high"
elif any(kw in col_text for kw in ["제조업", "건축", "토목"]) and "토목" in col_text:
result["family"] = "table"
result["surface"] = "table-3col"
result["columns"] = 3
result["items"] = 3
result["confidence"] = "high"
elif ("bim" in col_text) and ("dx" in col_text):
result["family"] = "compare"
result["surface"] = "table-multi-row"
result["columns"] = 2
result["confidence"] = "high"
else:
result["family"] = "table"
result["surface"] = f"table-{n_cols}col"
result["columns"] = n_cols
result["confidence"] = "medium"
elif n_bullets >= 1:
result["has_cards"] = True
if n_bullets == 3:
result["family"] = "list"
result["surface"] = "bullets-3"
result["columns"] = 3
result["items"] = 3
result["confidence"] = "high"
elif n_bullets == 2:
result["family"] = "compare"
result["surface"] = "bullets-2col"
result["columns"] = 2
result["items"] = 2
result["confidence"] = "medium"
elif n_bullets == 4:
result["family"] = "cards"
result["surface"] = "cards-4-grid"
result["columns"] = 4
result["items"] = 4
result["confidence"] = "medium"
else:
result["family"] = "list"
result["surface"] = "mixed-bullets"
result["items"] = n_bullets
result["confidence"] = "low"
else:
result["family"] = None
result["surface"] = "single-column"
result["confidence"] = "low"
return result
# ═══ 구조 매칭 v3 ═══
def structural_match_v3(mdx_schema, fig_schema):
"""속성 교집합 기반 score (0~1). confidence high + 주요 속성 일치시에만 점수 부여.
Tie-breaker 용도라 가중치는 호출 측에서 낮게 적용 (0.05~0.10 권장)."""
if not mdx_schema or not fig_schema:
return 0.0, []
# confidence 체크: low면 신뢰 안 함
if mdx_schema.get("confidence") == "low":
return 0.0, ["mdx confidence low → 0"]
# 속성 5개 비교
aligns = []
scores = []
# family (가장 중요)
m_fam = mdx_schema.get("family")
f_fam = fig_schema.get("family")
if m_fam and f_fam and m_fam == f_fam:
scores.append(0.40)
aligns.append(f"family:{m_fam}")
elif m_fam and f_fam:
aligns.append(f"family:{m_fam}{f_fam}")
# columns / items
m_cols = mdx_schema.get("columns") or mdx_schema.get("items")
f_cols = fig_schema.get("columns") or fig_schema.get("items")
if m_cols and f_cols and m_cols == f_cols:
scores.append(0.25)
aligns.append(f"columns:{m_cols}")
elif m_cols and f_cols:
aligns.append(f"columns:{m_cols}{f_cols}")
# has_table
if mdx_schema.get("has_table") is not None and fig_schema.get("has_table") is not None:
if mdx_schema["has_table"] == fig_schema["has_table"]:
scores.append(0.10)
aligns.append(f"has_table:{mdx_schema['has_table']}")
# has_cards
if mdx_schema.get("has_cards") is not None and fig_schema.get("has_cards") is not None:
if mdx_schema["has_cards"] == fig_schema["has_cards"]:
scores.append(0.10)
aligns.append(f"has_cards:{mdx_schema['has_cards']}")
# semantic_role (bonus)
m_role = mdx_schema.get("semantic_role")
f_role = fig_schema.get("semantic_role")
if m_role and f_role and m_role == f_role and m_role != "(검수 필요)":
scores.append(0.15)
aligns.append(f"role:{m_role}")
total = sum(scores)
return total, aligns