from __future__ import annotations import json import hashlib import shutil import subprocess import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] FIXTURES = ROOT / "tests" / "fixtures" def run_cli(script: str, *args: object, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(ROOT / "scripts" / script), *(str(arg) for arg in args)], cwd=cwd or ROOT, text=True, capture_output=True, check=False, ) def read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def write_json(path: Path, value: Any) -> None: path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def init_run( tmp_path: Path, *, route: str = "light", mode: str = "write", with_draft: bool = False, audience: str | None = None, ) -> Path: brief = tmp_path / "request.md" brief.write_text("캐시 실패를 설명하는 문서를 작성해 주세요.\n", encoding="utf-8") command: list[object] = [ "--brief", brief, "--kind", "explanation", "--route", route, "--mode", mode, "--workspace", tmp_path / "workspace", "--date", "2026-07-23", ] if audience is not None: command.extend(["--audience", audience]) if with_draft or mode in {"revise", "review"}: draft = tmp_path / "original.md" shutil.copyfile(FIXTURES / "good" / "document.md", draft) command.extend(["--draft", draft]) result = run_cli("init_run.py", *command) assert result.returncode == 0, result.stderr return Path(result.stdout.split("\t", 1)[0]) def install_good_contracts(run_dir: Path) -> None: mapping = { "reader-contract.json": "02_reader_contract.json", "logic-map.json": "04_logic_map.json", "term-ledger.json": "05_term_ledger.json", "document.md": "07_draft.md", } for source, target in mapping.items(): shutil.copyfile(FIXTURES / "good" / source, run_dir / target) manifest = read_json(run_dir / "00_run.json") omissions = [] if manifest["route_hint"] == "light": omissions.append( { "artifact": "03_evidence_map.json", "reason": "light fixture는 별도 evidence curation을 생략한다.", } ) if manifest["mode"] != "review": omissions.extend( [ { "artifact": "08_logic_review.json", "reason": "light write/revise fixture는 별도 logic review를 생략한다.", }, { "artifact": "08_reader_review.json", "reason": "light write/revise fixture는 별도 reader review를 생략한다.", }, ] ) if manifest["mode"] == "review": omissions.append( { "artifact": "final.md", "reason": "review mode는 publishable final 문서를 만들지 않는다.", } ) manifest["omissions"] = omissions write_json(run_dir / "00_run.json", manifest) def write_reviews(run_dir: Path) -> None: draft_path = run_dir / "07_draft.md" draft_hash = hashlib.sha256(draft_path.read_bytes()).hexdigest() review_inputs = { "input_sha256": hashlib.sha256((run_dir / "01_input.md").read_bytes()).hexdigest(), "sources_sha256": hashlib.sha256((run_dir / "01_sources.json").read_bytes()).hexdigest(), "reader_contract_sha256": hashlib.sha256( (run_dir / "02_reader_contract.json").read_bytes() ).hexdigest(), "evidence_map_sha256": ( hashlib.sha256((run_dir / "03_evidence_map.json").read_bytes()).hexdigest() if (run_dir / "03_evidence_map.json").is_file() else None ), "logic_map_sha256": hashlib.sha256( (run_dir / "04_logic_map.json").read_bytes() ).hexdigest(), "term_ledger_sha256": hashlib.sha256( (run_dir / "05_term_ledger.json").read_bytes() ).hexdigest(), } for review_type, filename in ( ("logic", "08_logic_review.json"), ("reader", "08_reader_review.json"), ): write_json( run_dir / filename, { "schema_version": "1.0", "review_type": review_type, "document": {"path": str(draft_path), "sha256": draft_hash}, "inputs": review_inputs, "verdict": "pass", "findings": [], }, ) def write_evidence(run_dir: Path) -> None: write_json( run_dir / "03_evidence_map.json", { "schema_version": "1.0", "claims": [ { "id": "C1", "statement": "동시 재계산이 부하를 키운다.", "status": "source_backed", "source_ids": ["brief"], "source_locations": [ {"source_id": "brief", "locator": "line 1"} ], "does_not_support": ["모든 장애 제거"], "load_bearing": True, }, { "id": "C2", "statement": "검증에는 한계가 있다.", "status": "assumption", "source_ids": [], "source_locations": [], "does_not_support": ["운영 효과"], "load_bearing": True, "label": "가정", }, ], }, )