#!/usr/bin/env python3 """의미 검토가 받을 입력을 한 파일로 묶는다. 설계가 정한 분업이다 — 「결정적으로 판정 가능한 조건만 코드로 검사한다. 의미·인과·가독성은 근거를 받은 별도 검토 컨텍스트가 판단하고 확신이 없으면 보류한다」. 이 도구는 **판단하지 않는다.** 판단할 사람이 받을 것을 모은다. 담는 것 다섯. 1. **대상과 해시** — 기록·SSOT·증거·그림의 sha256. 검토가 끝난 뒤 파일이 바뀌면 그 판정은 이 해시에 안 맞는다 2. **검사기 버전** — 스킬의 `metadata.version` 과 검사기 파일의 내용 해시 3. **관문 결과** — 여기서 **실제로 돌려** 종료 코드를 적는다. 받아 적지 않는다 4. **주장 후보** — 본문에서 검증 가능한 문장을 기계로 뽑는다. 사람이 적은 주장 목록과 견주면 「본문에 있으나 주장 목록에는 빠진 것」이 보인다 5. **판정 기준** — 주장 종류마다 무엇이 있어야 하는지 (`quality-policy@1` §3) python3 scripts/review-package.py <프로젝트> --record <기록.md> -o <출력.json> """ from __future__ import annotations import argparse import hashlib import importlib.util import json import os import re import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # quality-policy@1 §3 — 주장 종류마다 필요한 근거와 허용되는 표현 CLAIM_KINDS = { "코드 구조·설정": {"근거": "저장소 ID, 커밋, 파일, 심볼 또는 범위", "허용": "지정된 버전에 이 구현·설정이 존재함"}, "실제 실행 동작": {"근거": "실행 ID, 환경·입력·명령·종료 코드, 원본 출력", "허용": "기록된 조건에서 관찰된 결과"}, "성능 비교": {"근거": "실험 코드·데이터·부하·반복 횟수·원시 측정치·집계 방식", "허용": "측정한 조건과 변동 범위 안의 비교"}, "개념·제품 동작": {"근거": "해당 버전의 공식 문서 또는 명세", "허용": "출처가 설명하는 적용 범위"}, "선택 이유·개인 경험": {"근거": "작성자 기록·ADR·승인된 메모", "허용": "기록에 있는 이유와 실제 수행한 일"}, "해석·가설": {"근거": "해석의 전제가 되는 근거와 아직 확인하지 못한 부분", "허용": "가능성·추정임을 명시한 설명"}, } # 검증 가능한 문장을 고르는 표지. 뜻을 보지 않고 표면만 본다 CLAIM_MARKS = ( (re.compile(r"\d"), "수치"), (re.compile(r"exit\s*=?\s*\d|종료 코드"), "종료 코드"), (re.compile(r"`[^`]+`"), "식별자"), (re.compile(r"(더|덜|보다|만큼|배|비해)\s"), "비교"), (re.compile(r"(때문|므로|따라서|그래서|원인)"), "인과"), (re.compile(r"(항상|절대|전부|모두|하나도|없다|never|always)"), "전칭"), ) BODY_START, BODY_END = "", "" def _sha256(path: str) -> str | None: try: with open(path, "rb") as fh: return hashlib.sha256(fh.read()).hexdigest() except OSError: return None def _front_matter_block(text: str) -> str: if not text.startswith("---"): return "" end = text.find("\n---", 3) return text[3:end] if end > 0 else "" def _listed(fm: str, key: str) -> list[str]: """`key:` 아래의 `- 값` 목록. `- key: x` 짝은 `file:` 쪽을 쓴다.""" out = [] grab = False for line in fm.splitlines(): if re.match(rf"^{key}:\s*$", line): grab = True continue if grab: if re.match(r"^\S", line): break m = re.match(r"^\s+-\s+(\S.*)$", line) or re.match(r"^\s+file:\s*(\S+)$", line) if m and not m.group(1).startswith("key:"): out.append(m.group(1).strip()) return out def _scalar(fm: str, key: str) -> str | None: m = re.search(rf"^{key}:\s*(.*)$", fm, re.M) return m.group(1).strip().strip('"') or None if m else None def _claim_candidates(text: str) -> list[dict]: a, b = text.find(BODY_START), text.find(BODY_END) region = text[a:b] if a >= 0 <= b else text offset = text[:a].count("\n") + 1 if a >= 0 else 0 out = [] in_fence = False for i, line in enumerate(region.splitlines()): if line.lstrip().startswith("```"): in_fence = not in_fence continue if in_fence or not line.strip() or line.lstrip().startswith(("#", "|", "