Files
Aislo/resources/knowledge/original/_pipeline/verify_structure.py
T
eomsangdonandClaude Fable 5 6df07f60a1 fix(knowledge): 검토내역 8건 반영 — 상한 제거·행정규칙 부칙 복원·회귀검사 신설
타 AI 검토내역(tmp/기술문서DB_검토내역_260814.md) 전수 검증 후 동의분 적용.

파이프라인:
- fix_fences.py units[:20] / collect_law.py 별표 limit=80·변경이력 [:40] 상한 제거
- gen_md.py 기준목록 출력을 운영본(04_참조_법령기준_목록.md) 경로로 정정
- README 구경로(docs/raw/law) 3곳 현행화, ruff format 일괄 적용
- verify_structure.py 신설: XML↔md 부칙·목·별표·CHANGELOG 행수 회귀검사

신규 발견 결함 2건 정정 (검토내역에 없던 것):
- 행정규칙 XML은 부칙내용이 부칙 직속(부칙단위 래퍼 없음) — 변환기가 미처리해
  행정규칙 md 22건에 부칙 전무. patch_law_md 확장 후 부칙 138건 복원
- patch_law_md 부칙 dedup 키가 괄호형 타법개정 헤더 미매칭 — 재실행 시
  기존 부칙 1,724건 중복 추가되는 비멱등 결함. 정규식 보강

재수집 (law.go.kr):
- 대기환경보전법 시행규칙 별표 PDF 78건 추가 확보 (80→158, 전수)
- CHANGELOG 절단 3건 재생성 (고용보험법 시행령 42·국가계약법 시행령 41·
  대기환경 45회 — API 연혁과 전건 일치 확인)

기술문서·데이터:
- _최종검증로그: 55문서 스냅샷 범위 명시 (현재 65문서, 신설 10건 미검증 주지)
- 제비율.md frontmatter에 조달청 제비율(2026.04.13)·국민연금법 출처 보강
- 원가_입력변수_사전: 장비대금 지급보증을 공사종류별 표로 정정, csv→json 표기
- labor_const JSON: 원문 노임 118건 전건 일치 확인 후 해시만 재동기화
  (원문 변경분 = 신뢰도 플래그 컬럼뿐, 단가 무변)

검증: verify_structure 결함 0건, patch_law_md 재실행 +0 (멱등),
원가 JSON 해시 56건 중 잔여 불일치 1건(coef_2026 — 사용자 협의 대상)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:47:36 +09:00

129 lines
4.6 KiB
Python

# -*- coding: utf-8 -*-
"""법령 XML ↔ md 구조 회귀검사 (네트워크 미사용).
과거 3결함(부칙 [:20] 절단·목(가나다) 누락·별표 80건 상한)과 CHANGELOG [:40]
절단의 재발을 잡는다. 수집·보정 후 반드시 실행 (README W5).
검사 항목 (원본 XML 이 있는 모든 판본 md 대상):
1. 부칙 — XML 부칙단위 각 건의 앞부분이 md에 존재하는가
2. 목 — XML 목내용 각 건의 앞부분이 md에 존재하는가
3. 별표 — XML 다운로드 대상(삭제 제외·PDF 링크 보유) 수 ≤ 별표/ PDF 파일 수
4. 변경이력 — CHANGELOG.md 헤더 "개정 N회" == 표 행 수
사용: python verify_structure.py # 전수 검사, 결함 있으면 exit 1
python verify_structure.py -v # 통과 항목도 출력
"""
import re
import sys
import glob
import os
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent # ...\original
# md 대조는 공백·인용부호 제거 후 앞 30자 포함 여부로 판정.
# '>' 도 제거 — 부칙은 인용블록이라 빈 줄이 '>' 로 남아 공백만 제거하면 키가 어긋난다.
KEY_LEN = 30
def norm(s):
return re.sub(r"[\s>*`]+", "", s)
def check_pair(xml_path, errors, verbose):
md_path = xml_path.with_suffix(".md")
if not md_path.exists():
errors.append(f"{md_path}: md 판본 없음 (xml만 존재)")
return
md_norm = norm(md_path.read_text(encoding="utf-8"))
try:
root = ET.parse(xml_path).getroot()
except ET.ParseError as e:
errors.append(f"{xml_path}: XML 파싱 실패 — {e}")
return
for tag, label in (("부칙내용", "부칙"), ("목내용", "목")):
total = miss = 0
for e in root.iter(tag):
body = "".join(e.itertext()).strip()
if not body:
continue
total += 1
if norm(body)[:KEY_LEN] not in md_norm:
miss += 1
if miss:
errors.append(f"{md_path}: {label} {total}건 중 {miss}건 md 누락")
elif verbose and total:
print(f" OK {md_path.name}: {label} {total}건 전건 존재")
def check_별표(folder, root, errors, verbose):
별표 = root.find("별표")
if 별표 is None or not len(별표):
return
targets = 0
for b in 별표.findall("별표단위"):
title = (b.findtext("별표제목") or "").strip()
link = (b.findtext("별표서식PDF파일링크") or "").strip()
if title.startswith("삭제") or not link:
continue
targets += 1
saved = len(list((folder / "별표").glob("*.pdf"))) if (folder / "별표").is_dir() else 0
if saved < targets:
errors.append(
f"{folder}: 별표 PDF {targets}건 대상 중 {saved}건만 저장 (미수집 {targets - saved})"
)
elif verbose and targets:
print(f" OK {folder.name}: 별표 PDF {saved}/{targets}")
def check_changelog(folder, errors, verbose):
ch = folder / "CHANGELOG.md"
if not ch.exists():
return
text = ch.read_text(encoding="utf-8")
m = re.search(r"교본 이후 개정 (\d+)회", text)
if not m:
return
declared = int(m.group(1))
# 날짜는 점 구분(2026.02.01) — collect_law.py ymd() 출력 형식
rows = len(re.findall(r"^\| \d{4}[.\-]\d{2}[.\-]\d{2} \|", text, re.M))
if declared and rows != declared:
errors.append(f"{ch}: 헤더 개정 {declared}회 vs 표 {rows}행 — 이력 절단")
elif verbose and declared:
print(f" OK {ch.parent.name}: 변경이력 {rows}/{declared}")
def main():
verbose = "-v" in sys.argv
errors = []
folders = set()
for pat in ("**/현행_*.xml", "**/교본시점_*.xml"):
for x in glob.glob(str(ROOT / pat), recursive=True):
xp = Path(x)
check_pair(xp, errors, verbose)
folders.add(xp.parent)
for folder in sorted(folders):
# 별표 대상 수는 현행본 XML 기준 (최신 별표 구성)
curs = sorted(folder.glob("현행_*.xml"))
if curs:
try:
check_별표(folder, ET.parse(curs[-1]).getroot(), errors, verbose)
except ET.ParseError:
pass # check_pair 에서 이미 보고
check_changelog(folder, errors, verbose)
print(f"검사 폴더 {len(folders)}곳")
if errors:
print(f"결함 {len(errors)}건:")
for e in errors:
print(" ✗", e)
sys.exit(1)
print("결함 0건 — 구조 보존 확인")
if __name__ == "__main__":
main()