init: document-haness 하네스 설계
This commit is contained in:
+50
-173
@@ -1,179 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from claridoc.models import Brief, SourcePack
|
||||
|
||||
|
||||
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": "가정",
|
||||
},
|
||||
],
|
||||
def brief_dict(document_type: str = "technical_blog") -> dict:
|
||||
return {
|
||||
"title": "A precise technical document",
|
||||
"document_type": document_type,
|
||||
"language": "en-US",
|
||||
"audience": {
|
||||
"roles": ["software engineers"],
|
||||
"prior_knowledge": ["basic programming"],
|
||||
"needs": ["a decision-ready explanation"],
|
||||
},
|
||||
)
|
||||
"reader_goal": "choose a safe implementation approach",
|
||||
"core_message": "Structure claims around reader questions and verify every important step.",
|
||||
"scope": ["one bounded implementation decision"],
|
||||
"non_scope": ["vendor-specific defaults"],
|
||||
"prerequisites": ["a test environment"],
|
||||
"required_topics": ["mechanism", "evidence", "trade-offs"],
|
||||
"constraints": {
|
||||
"target_words": 700,
|
||||
"tone": "direct and professional",
|
||||
"version_context": "checked 2026-07-23",
|
||||
"max_heading_depth": 3,
|
||||
"require_citations": True,
|
||||
"allow_external_knowledge": False,
|
||||
},
|
||||
"forbidden_claims": ["always safe"],
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
|
||||
def source_dict() -> dict:
|
||||
return {
|
||||
"sources": [
|
||||
{
|
||||
"id": "S1",
|
||||
"title": "Authoritative source",
|
||||
"url": "https://example.com/source",
|
||||
"publisher": "Example",
|
||||
"accessed": "2026-07-23",
|
||||
"facts": ["The bounded mechanism has an observable result."],
|
||||
"notes": "fixture",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def make_brief(document_type: str = "technical_blog") -> Brief:
|
||||
return Brief.from_dict(brief_dict(document_type))
|
||||
|
||||
|
||||
def make_sources() -> SourcePack:
|
||||
return SourcePack.from_dict(source_dict())
|
||||
|
||||
Reference in New Issue
Block a user