180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
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": "가정",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
|