#!/usr/bin/env python3 """종류가 요구하는 내용이 실제로 채워져 있는지 본다. `audit-records.py` 는 평문 칸 **안에 마크업이 있는지**만 본다. 칸이 아예 없거나 제목만 있고 비어 있는 것은 세지 않는다. Studio 는 빈 칸도 받아 주므로 그대로 저장되고, 화면에서는 제목만 남은 칸으로 보인다. **결정적으로 판정 가능한 것만 본다.** 칸이 있는가, 비어 있지 않은가, 계약에 없는 `##` 이 있는가(화면에 자리가 없어 통째로 사라진다), 종류가 요구하는 근거의 자리가 채워졌는가. 내용이 옳은지·인과가 맞는지는 보지 않는다 — 그것은 근거를 받은 검토 컨텍스트의 몫이다. **강제하지 않는 것 셋.** - 고정 목차. 본문(`## 본문`) 안의 절 구성은 글마다 다르다 - 자료 개수. 그림 몇 장·증거 몇 건을 요구하지 않는다 - 답. 답이 없는 QUESTION 은 정상이다. 물음과 확인된 사실과 답을 구할 방법만 요구한다 python3 scripts/check-required-content.py <프로젝트> python3 scripts/check-required-content.py --file <기록.md> """ from __future__ import annotations import argparse import collections import glob import os import re import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(ROOT, "scripts")) import techlog # noqa: E402 # 종류마다의 `##` 칸. 정본은 # .agents/skills/publishing-tech-log-to-studio/references/studio-form-map.md 다. # 계약에 없는 `##` 는 화면에 자리가 없어 통째로 사라진다 SECTIONS: dict[str, dict[str, tuple[str, ...]]] = { "case": {"required": ("관계", "문제", "결론", "검증 환경", "재현 조건", "본문"), "optional": ()}, "concept": {"required": ("관계", "본문"), "optional": ()}, "reference": {"required": ("관계", "목적", "규칙", "적용 조건", "예외"), "optional": ("예시",)}, "question": {"required": ("관계", "사실", "미지수", "다음 검증"), "optional": ("가정", "제약", "선택지")}, # Decision 의 틀에는 `관계` 가 없다 (templates/decision.md). 있으면 받되 요구하지 않는다 "decision": {"required": ("근거", "결정문", "판단 이유", "영향"), "optional": ("관계",)}, # 환경 구성의 절 구성은 Concept 과 같다 — 본문 밖의 칸이 없다. 「실행 절차·구성 값·확인 # 방법을 `##` 절로 적는다. **절 이름을 강제하지 않는다** — 프로젝트마다 셋업의 모양이 # 다르다」(`SetupInput.bodyMarkdown`)라 그 셋은 본문 구간 안에 있고 여기서 세지 않는다. # 본문 밖의 칸 `pinnedVersions` 는 절이 아니라 frontmatter 에 있다 (templates/setup.md) "setup": {"required": ("관계", "본문"), "optional": ()}, } # 본문이 있는 종류는 셋이다. 목록은 techlog 가 정한다 BODY_KINDS = techlog.BODY_KINDS # frontmatter 의 `kind` 는 Studio 가 쓰는 값이다. 폴더 이름과 하나가 다르다 — # decision/ 폴더의 기록은 `kind: PROJECT_DECISION` 이다 (templates/decision.md:3) KIND_ALIASES = techlog.DIR_OF_KIND # 종류가 요구하는 근거의 자리. 값이 옳은지가 아니라 **자리가 채워졌는지**만 본다 FRONTMATTER: dict[str, tuple[str, ...]] = { "case": ("sourceRevision",), "concept": ("basisVersion",), "reference": ("sourceRevision",), "question": ("questionStatus",), "decision": ("decisionStatus",), # 환경 구성에는 검증일 칸이 없다 — `lastVerifiedOn` 도 `verifiedOn` 도 계약에 없다. # 낡음을 말하는 것은 `pinnedVersions` 뿐이라(「어느 버전 위에서 이 절차가 성립했는지가 # 유효 범위다」 · `SetupDetailResponse`) Concept 의 `basisVersion` 과 같은 자리다 "setup": ("pinnedVersions",), } BODY_START, BODY_END = "", "" def _front_matter(text: str) -> tuple[dict, int]: """frontmatter 와 그것이 끝나는 줄 번호.""" if not text.startswith("---"): return {}, 0 end = text.find("\n---", 3) if end < 0: return {}, 0 out = {} for line in text[3:end].splitlines(): m = re.match(r"^([a-zA-Z_]+):\s*(.*)$", line) if m: out[m.group(1)] = m.group(2).strip().strip('"') return out, text[:end].count("\n") + 2 def _sections(text: str) -> dict[str, str]: """`## 이름` → 그 아래 내용. 본문 구간 안의 `##` 은 세지 않는다.""" body_a = text.find(BODY_START) body_b = text.find(BODY_END) out: dict[str, str] = {} order: list[tuple[str, int]] = [] for m in re.finditer(r"^##\s+(.+)$", text, re.M): if body_a >= 0 <= body_b and body_a < m.start() < body_b: continue # 본문 안의 절은 글마다 다르다. 강제하지 않는다 order.append((m.group(1).strip(), m.end())) for i, (name, start) in enumerate(order): stop = order[i + 1][1] - len(f"## {order[i + 1][0]}") if i + 1 < len(order) else len(text) chunk = text[start:stop] if name == "본문": chunk = chunk.replace(BODY_START, "").replace(BODY_END, "") out[name] = chunk.strip() return out def _summary(text: str, fm_end: int) -> str: """제목 바로 아래 첫 문단. Studio 의 `요약` 칸이다.""" rest = text.split("\n", fm_end)[-1] if fm_end else text m = re.search(r"^#\s+.+$", rest, re.M) if not m: return "" after = rest[m.end():] after = re.split(r"^##\s", after, maxsplit=1, flags=re.M)[0] for para in (p.strip() for p in after.split("\n\n")): if para and not para.startswith("