from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from src.phase_z2_mapper import _extract_bold_or_plain, split_source from src.phase_z2_pipeline import parse_mdx MDX_ROOT = ROOT / "samples" / "mdx" CONTRACT: dict[str, dict[str, Any]] = { "01. 건설산업 DX의 올바른 이해(0127).mdx": { "top_sections": ["1. 용어 정의", "2. 용어간 상호관계"], "internal_labels": { "01-1": ["건설산업", "BIM(Building Information Modeling) : 디지털 전환을 위한 핵심 기술", "DX(Digital Transformation) : 산업 패러다임의 변화"], "01-2": ["DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 프로세스를 혁신하는 상위개념", "DX와 BIM의 구분"], }, }, "02. DX의 시행 목표 및 기대효과.mdx": { "top_sections": ["1. DX의 궁극적 목표", "2. DX 기반 Process 혁신에 따른 주체별 기대효과"], "h3": { "02-2": ["2.1 업무 수행 과정(Process)의 변화", "2.2 DX 시행 주체별 기대효과"], }, "internal_labels": { "02-1": ["안전과 품질", "생산성 향상", "소통과 신뢰"], }, }, "03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx": { "top_sections": ["1. DX 시행을 위한 필수 요건", "2. Process의 혁신과 Product의 변화"], "h3": { "03-2": ["2.1 과정(Process)의 혁신", "2.2 결과(Product)의 변화"], }, "internal_labels": { "03-1": ["기술(디지털)", "사람(역량)", "자연(여건)"], }, }, "04. DX 지연 요인.mdx": { "top_sections": ["1. DX에 대한 인식", "2. DX 추진의 실태"], "h3": { "04-1": ["기술 및 소프트웨어 이해도", "효과와 효율성", "인력 및 교육", "경제적 부담", "실무 및 적용성"], "04-2": ["2.1 정책 및 발주 체계", "2.2 조직 및 수행 역량"], }, }, "05. 설계 방식의 왜곡.mdx": { "top_sections": ["1. 설계의 자동화", "2. S/W 중심 설계 방식"], "h3": { "05-2": ["1 기능·구조적 한계", "2 기술 역량의 왜곡"], }, "internal_labels": { "05-1": ["설계의 개념", "설계의 특성", "'설계 자동화'라는 용어의 오용과 모순"], }, }, } def _h3_titles(raw_content: str) -> list[str]: return [ m.group(1).strip() for m in re.finditer(r"(?m)^###\s+(.+?)\s*$", raw_content or "") ] def _top_bullet_labels(raw_content: str) -> list[str]: return [ _extract_bold_or_plain(top_line) for top_line, _nested in split_source("top_bullets", raw_content or "") ] def _contains_label(labels: list[str], expected: str) -> bool: return any(expected == label or expected in label for label in labels) def check_one(filename: str, spec: dict[str, Any]) -> dict[str, Any]: path = MDX_ROOT / filename title, sections, footer = parse_mdx(path) section_titles = [s.title for s in sections] errors: list[str] = [] expected_top = spec.get("top_sections", []) if section_titles != expected_top: errors.append( f"top_sections mismatch: expected={expected_top!r}, actual={section_titles!r}" ) section_reports = [] for section in sections: h3_titles = _h3_titles(section.raw_content) bullet_labels = _top_bullet_labels(section.raw_content) expected_h3 = (spec.get("h3") or {}).get(section.section_id) if expected_h3 is not None and h3_titles != expected_h3: errors.append( f"{section.section_id} h3 mismatch: expected={expected_h3!r}, actual={h3_titles!r}" ) expected_labels = (spec.get("internal_labels") or {}).get(section.section_id, []) missing_labels = [ label for label in expected_labels if not _contains_label(bullet_labels, label) ] if missing_labels: errors.append( f"{section.section_id} internal labels missing: {missing_labels!r}" ) section_reports.append({ "section_id": section.section_id, "title": section.title, "h3_titles": h3_titles, "top_bullet_labels_count": len(bullet_labels), "top_bullet_labels_sample": bullet_labels[:12], }) return { "filename": filename, "title": title, "has_footer": bool(footer), "passed": not errors, "errors": errors, "sections": section_reports, } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--write-report", type=Path) args = parser.parse_args() reports = [check_one(filename, spec) for filename, spec in CONTRACT.items()] result = { "passed": all(report["passed"] for report in reports), "reports": reports, } text = json.dumps(result, ensure_ascii=False, indent=2) print(text) if args.write_report: args.write_report.parent.mkdir(parents=True, exist_ok=True) args.write_report.write_text(text + "\n", encoding="utf-8") return 0 if result["passed"] else 1 if __name__ == "__main__": raise SystemExit(main())