- 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>
196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
"""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
|