init: document-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 13:58:08 +09:00
parent d6f78f92a0
commit c39406bbdd
219 changed files with 7010 additions and 20052 deletions
+50 -173
View File
@@ -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())