타 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>
158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""보관된 법령 XML 기준으로 기존 md 의 누락분만 보충한다 (네트워크 미사용).
|
|
|
|
기존 md 에는 수동·스크립트 후처리(박스표 → 마크다운 표, 수식, 이미지)가
|
|
적용돼 있어 전체 재렌더링은 그 결과를 파괴한다. 따라서 **추가만** 한다.
|
|
|
|
보충 대상 (2026-08-14 감사에서 확인된 변환 누락 2종):
|
|
1. 부칙 절단 — 과거 collect_law.py 가 `부칙단위[:20]` 만 출력해 최신 부칙이
|
|
통째로 빠졌다. md 에 없는 부칙단위를 뒤에 이어붙인다.
|
|
2. 목(가·나·다…) 미출력 — 목이 호가 아니라 항의 직속 자식으로 오는 구조를
|
|
처리하지 못해 누락됐다. 부모 호 줄 뒤에 들여쓰기 4칸으로 삽입한다.
|
|
|
|
사용:
|
|
python patch_law_md.py --dry # 변경량만 출력
|
|
python patch_law_md.py # 저장
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from collect_law import ROOT, T, clean
|
|
|
|
분류목록 = ("법률", "행정규칙")
|
|
STEM = re.compile(r"^(현행|교본시점)_\d{8}$")
|
|
# 괄호형 타법개정 헤더('부칙(통계법 시행령) <제20331호,…>')도 매칭 — 괄호형을
|
|
# 놓치면 키가 비어 매 실행마다 같은 부칙을 재추가한다 (2026-08-15 멱등성 결함 정정).
|
|
부칙헤더 = re.compile(r"부칙\s*(?:\([^)\n]*\))?\s*<[^>]*>")
|
|
|
|
|
|
def 부칙키(text):
|
|
"""'부칙 <제20903호,2025.4.2>' → 정규화 키."""
|
|
m = 부칙헤더.search(text or "")
|
|
return re.sub(r"\s+", "", m.group(0)) if m else None
|
|
|
|
|
|
def 목텍스트(mo):
|
|
return clean(T(mo, "목내용")) or clean(" ".join(x for x in mo.itertext() if x))
|
|
|
|
|
|
def patch(md_path, xml_path):
|
|
"""(부칙 추가수, 목 추가수, 목 앵커실패수)"""
|
|
root = ET.parse(xml_path).getroot()
|
|
text = md_path.read_text(encoding="utf-8")
|
|
lines = text.splitlines()
|
|
부칙추가, 목추가, 앵커실패 = 0, 0, 0
|
|
|
|
# ── 1) 목 삽입 (뒤에서부터 삽입해 인덱스 밀림 방지) ──
|
|
조문 = root.find("조문")
|
|
작업 = []
|
|
if 조문 is not None:
|
|
for j in 조문.findall("조문단위"):
|
|
for h in j.findall("항"):
|
|
앵커 = None # 직전 호 내용
|
|
for kid in h:
|
|
if kid.tag == "호":
|
|
앵커 = clean(T(kid, "호내용"))
|
|
for mo in kid.findall("목"):
|
|
작업.append((앵커, 목텍스트(mo)))
|
|
elif kid.tag == "목":
|
|
작업.append((앵커, 목텍스트(kid)))
|
|
for ho in j.findall("호"):
|
|
앵커 = clean(T(ho, "호내용"))
|
|
for mo in ho.findall("목"):
|
|
작업.append((앵커, 목텍스트(mo)))
|
|
|
|
# 앵커별로 묶어 한 번에 삽입
|
|
묶음 = {}
|
|
for 앵커, mv in 작업:
|
|
if not mv or mv in text: # 이미 있으면 건너뜀
|
|
continue
|
|
if not 앵커:
|
|
앵커실패 += 1
|
|
continue
|
|
묶음.setdefault(앵커, []).append(mv)
|
|
|
|
for 앵커, 목들 in 묶음.items():
|
|
idx = next((i for i, l in enumerate(lines) if l.strip() == 앵커.strip()), None)
|
|
if idx is None:
|
|
idx = next((i for i, l in enumerate(lines) if 앵커.strip() and 앵커.strip() in l), None)
|
|
if idx is None:
|
|
앵커실패 += len(목들)
|
|
continue
|
|
lines[idx + 1 : idx + 1] = [f" {mv}" for mv in 목들]
|
|
목추가 += len(목들)
|
|
|
|
# ── 2) 부칙 보충 ──
|
|
부칙 = root.find("부칙")
|
|
if 부칙 is not None:
|
|
# 법령(law)=부칙단위 래퍼 / 행정규칙(admrul)=부칙내용 직속 (2026-08-15 결함 정정
|
|
# — 직속형 미처리로 행정규칙 md 22건에 부칙이 통째로 빠져 있었다)
|
|
units = 부칙.findall("부칙단위") or 부칙.findall("부칙내용")
|
|
본문 = "\n".join(lines)
|
|
기존키 = {re.sub(r"\s+", "", m) for m in 부칙헤더.findall(본문)}
|
|
|
|
def 본문텍스트(b):
|
|
return T(b, "부칙내용") if b.tag == "부칙단위" else "".join(b.itertext())
|
|
|
|
누락 = [b for b in units if (부칙키(본문텍스트(b)) or "") not in 기존키]
|
|
if 누락:
|
|
if "## 부칙" not in 본문:
|
|
lines += ["", "## 부칙", ""]
|
|
add = []
|
|
for b in 누락:
|
|
body = clean(본문텍스트(b))
|
|
if not body.strip():
|
|
continue
|
|
for ln in body.split("\n"):
|
|
add.append(f"> {ln}" if ln.strip() else ">")
|
|
add.append("")
|
|
부칙추가 += 1
|
|
# 첨부파일 절이 뒤에 있으면 그 앞에, 없으면 끝에
|
|
pos = next((i for i, l in enumerate(lines) if l.startswith("## 첨부파일")), len(lines))
|
|
lines[pos:pos] = add
|
|
|
|
if 부칙추가 or 목추가:
|
|
md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
return 부칙추가, 목추가, 앵커실패
|
|
|
|
|
|
def main(dry=False):
|
|
합계 = [0, 0, 0]
|
|
파일 = 0
|
|
for 분류 in 분류목록:
|
|
base = ROOT / 분류
|
|
if not base.is_dir():
|
|
continue
|
|
for folder in sorted(p for p in base.iterdir() if p.is_dir()):
|
|
for xml_path in sorted(folder.glob("*.xml")):
|
|
if not STEM.match(xml_path.stem):
|
|
continue
|
|
md_path = xml_path.with_suffix(".md")
|
|
if not md_path.exists():
|
|
continue
|
|
if dry:
|
|
orig = md_path.read_text(encoding="utf-8")
|
|
b, m, f = patch(md_path, xml_path)
|
|
if dry:
|
|
md_path.write_text(orig, encoding="utf-8")
|
|
if b or m or f:
|
|
파일 += 1
|
|
print(
|
|
f" {분류}/{folder.name}/{md_path.name}: 부칙+{b} 목+{m}"
|
|
+ (f" (앵커실패 {f})" if f else "")
|
|
)
|
|
합계[0] += b
|
|
합계[1] += m
|
|
합계[2] += f
|
|
print(
|
|
f"\n파일 {파일}건 / 부칙 +{합계[0]} · 목 +{합계[1]}"
|
|
+ (f" · 앵커실패 {합계[2]}" if 합계[2] else "")
|
|
+ (" (dry-run — 원복함)" if dry else " 저장 완료")
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(dry="--dry" in sys.argv)
|