"""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