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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env python3
"""Execute a canonical skill-local runtime script from a repository wrapper."""
from __future__ import annotations
import os
import sys
from pathlib import Path
def execute(name: str) -> None:
root = Path(__file__).resolve().parent.parent
target = root / "skills" / "technical-doc-flow" / "scripts" / name
if not target.is_file():
print(f"error: canonical runtime script is missing: {target}", file=sys.stderr)
raise SystemExit(2)
os.execv(sys.executable, [sys.executable, str(target), *sys.argv[1:]])
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("build_quick_rules.py")
-361
View File
@@ -1,361 +0,0 @@
#!/usr/bin/env python3
"""Fail when release, path, command, and runtime contracts drift apart."""
from __future__ import annotations
import argparse
import ast
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
SEMVER_RE = re.compile(
r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
r"(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?"
r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
)
RUNTIME_TOOLS = (
"init_run.py",
"lint_document.py",
"update_run.py",
"verify_run.py",
"split_document.py",
"reassemble_document.py",
"build_quick_rules.py",
)
SCHEMA_BY_ARTIFACT = {
"00_run.json": "run.schema.json",
"01_sources.json": "sources.schema.json",
"02_reader_contract.json": "reader-contract.schema.json",
"03_evidence_map.json": "evidence-map.schema.json",
"04_logic_map.json": "logic-map.schema.json",
"05_term_ledger.json": "term-ledger.schema.json",
"08_logic_review.json": "review.schema.json",
"08_reader_review.json": "review.schema.json",
"08_lint.json": "lint-report.schema.json",
"09_final_report.json": "final-report.schema.json",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="릴리스와 정본 경로 계약의 동기화를 검사합니다.")
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
return parser.parse_args()
def load_json(path: Path, errors: list[str]) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
errors.append(f"JSON을 읽을 수 없음: {path}: {exc}")
return {}
if not isinstance(value, dict):
errors.append(f"JSON 최상위 값이 객체가 아님: {path}")
return {}
return value
def require_file(root: Path, relative: str, errors: list[str]) -> Path:
path = root / relative
if not path.is_file():
errors.append(f"필수 파일 누락: {relative}")
return path
def nested(value: dict[str, Any], *keys: str) -> Any:
current: Any = value
for key in keys:
if not isinstance(current, dict):
return None
current = current.get(key)
return current
def validate_version(root: Path, errors: list[str]) -> None:
version_path = require_file(root, "VERSION", errors)
try:
version = version_path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeError):
return
if not SEMVER_RE.fullmatch(version):
errors.append(f"VERSION이 SemVer가 아님: {version!r}")
manifests = {
".claude-plugin/plugin.json": [("version",)],
".claude-plugin/marketplace.json": [("metadata", "version")],
"gemini-extension.json": [("version",)],
}
for relative, paths in manifests.items():
value = load_json(require_file(root, relative, errors), errors)
for path in paths:
current: Any = value
for key in path:
if isinstance(current, list) and key.isdigit():
index = int(key)
current = current[index] if index < len(current) else None
elif isinstance(current, dict):
current = current.get(key)
else:
current = None
if current != version:
errors.append(f"버전 불일치: {relative}:{'.'.join(path)}={current!r}, VERSION={version!r}")
marketplace_path = require_file(root, ".claude-plugin/marketplace.json", errors)
marketplace = load_json(marketplace_path, errors)
plugins = marketplace.get("plugins")
matches = [
item
for item in plugins
if isinstance(item, dict) and item.get("name") == "technical-doc-flow"
] if isinstance(plugins, list) else []
if len(matches) != 1:
errors.append("marketplace에는 technical-doc-flow plugin 항목이 정확히 하나여야 함")
elif matches[0].get("version") != version:
errors.append(
"버전 불일치: .claude-plugin/marketplace.json의 technical-doc-flow plugin="
f"{matches[0].get('version')!r}, VERSION={version!r}"
)
def validate_manifest_paths(root: Path, errors: list[str]) -> None:
harness = load_json(require_file(root, "harness.json", errors), errors)
expected = {
"version_file": "VERSION",
"canonical_skill": "skills/technical-doc-flow/SKILL.md",
"quality_rules": "skills/technical-doc-flow/config/quality-rules.json",
"runtime_contract": "skills/technical-doc-flow/config/runtime-contract.json",
"schemas": "skills/technical-doc-flow/schemas",
"runtime_scripts": "skills/technical-doc-flow/scripts",
"quick_rules": "skills/technical-doc-flow/references/quick-rules.md",
}
for key, relative in expected.items():
if harness.get(key) != relative:
errors.append(f"harness.json {key} 불일치: {harness.get(key)!r}")
if not (root / relative).exists():
errors.append(f"harness.json 대상 누락: {relative}")
plugin = load_json(require_file(root, ".claude-plugin/plugin.json", errors), errors)
if plugin.get("skills") != ["./skills/"]:
errors.append("Claude plugin skills 경로는 ['./skills/']여야 함")
gemini = load_json(require_file(root, "gemini-extension.json", errors), errors)
if gemini.get("contextFileName") != "GEMINI.md":
errors.append("Gemini manifest contextFileName은 GEMINI.md여야 함")
def validate_runtime(root: Path, errors: list[str]) -> None:
skill = root / "skills" / "technical-doc-flow"
helper = skill / "scripts" / "harness_common.py"
if not helper.is_file():
errors.append("canonical runtime helper 누락: skills/technical-doc-flow/scripts/harness_common.py")
root_entry = root / "scripts" / "_runtime_entry.py"
if not root_entry.is_file():
errors.append("root runtime entry helper 누락: scripts/_runtime_entry.py")
for name in RUNTIME_TOOLS:
canonical = skill / "scripts" / name
wrapper = root / "scripts" / name
if not canonical.is_file():
errors.append(f"canonical runtime 누락: {canonical.relative_to(root)}")
if not wrapper.is_file():
errors.append(f"root wrapper 누락: {wrapper.relative_to(root)}")
else:
try:
tree = ast.parse(wrapper.read_text(encoding="utf-8"), filename=str(wrapper))
except (OSError, UnicodeError, SyntaxError) as exc:
errors.append(f"root wrapper를 파싱할 수 없음: {wrapper.relative_to(root)}: {exc}")
continue
body = tree.body
import_ok = (
len(body) == 2
and isinstance(body[0], ast.ImportFrom)
and body[0].module == "_runtime_entry"
and body[0].level == 0
and len(body[0].names) == 1
and body[0].names[0].name == "execute"
and body[0].names[0].asname is None
)
call = body[1].value if len(body) == 2 and isinstance(body[1], ast.Expr) else None
call_ok = (
isinstance(call, ast.Call)
and isinstance(call.func, ast.Name)
and call.func.id == "execute"
and len(call.args) == 1
and not call.keywords
and isinstance(call.args[0], ast.Constant)
and call.args[0].value == name
)
if not import_ok or not call_ok:
errors.append(f"root wrapper가 exact canonical execute template이 아님: {wrapper.relative_to(root)}")
try:
smoke = subprocess.run(
[sys.executable, str(wrapper), "--help"],
cwd=root,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=10,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as exc:
errors.append(f"root wrapper smoke 실행 실패: {wrapper.relative_to(root)}: {exc}")
else:
if smoke.returncode != 0 or "usage:" not in smoke.stdout.casefold():
errors.append(
f"root wrapper --help smoke 실패: {wrapper.relative_to(root)} "
f"(exit={smoke.returncode})"
)
contract = load_json(skill / "config" / "runtime-contract.json", errors)
try:
script_path = str(skill / "scripts")
if script_path not in sys.path:
sys.path.insert(0, script_path)
from harness_common import validate_with_schema
validate_with_schema(
contract,
"runtime-contract.schema.json",
"runtime contract",
skill / "schemas",
)
except (ImportError, OSError, RuntimeError, ValueError, KeyError) as exc:
errors.append(f"runtime-contract.schema.json 검증 실패: {exc}")
agents = contract.get("agents")
actual_agents = sorted(path.stem for path in (root / "agents").glob("doc-*.md"))
if not isinstance(agents, list) or sorted(agents) != actual_agents:
errors.append(f"runtime agent 목록 불일치: contract={agents!r}, files={actual_agents!r}")
artifacts = contract.get("artifacts")
if not isinstance(artifacts, dict):
errors.append("runtime contract artifacts 객체 누락")
return
names: set[str] = set()
for key in ("always", "light", "standard", "deep", "review_mode"):
values = artifacts.get(key)
if not isinstance(values, list):
errors.append(f"runtime contract artifacts.{key} 배열 누락")
continue
names.update(item for item in values if isinstance(item, str))
schema_dir = skill / "schemas"
allowed_artifacts = set(SCHEMA_BY_ARTIFACT) | {"01_input.md", "07_draft.md", "final.md"}
unknown_artifacts = names - allowed_artifacts
for artifact in sorted(unknown_artifacts):
errors.append(f"schema/verifier 매핑이 없는 runtime artifact: {artifact}")
json_artifacts: set[str] = set()
for artifact in sorted(names):
schema = SCHEMA_BY_ARTIFACT.get(artifact)
if schema:
json_artifacts.add(artifact)
if not (schema_dir / schema).is_file():
errors.append(f"artifact schema 누락: {artifact} -> {schema}")
verifier_path = skill / "scripts" / "verify_run.py"
if verifier_path.is_file():
try:
verifier_tree = ast.parse(verifier_path.read_text(encoding="utf-8"))
declared: set[str] | None = None
declared_schemas: dict[str, str] | None = None
for node in verifier_tree.body:
if not isinstance(node, ast.Assign):
continue
names_in_assignment = {
target.id for target in node.targets if isinstance(target, ast.Name)
}
if "JSON_ARTIFACTS" in names_in_assignment:
literal = ast.literal_eval(node.value)
declared = set(literal) if isinstance(literal, (set, list, tuple)) else None
if "ARTIFACT_SCHEMAS" in names_in_assignment:
literal = ast.literal_eval(node.value)
declared_schemas = literal if isinstance(literal, dict) else None
if declared is None:
errors.append("verify_run.py JSON_ARTIFACTS 선언을 정적으로 확인할 수 없음")
elif not json_artifacts <= declared:
errors.append(
"verifier JSON artifact 목록 누락: "
+ ", ".join(sorted(json_artifacts - declared))
)
expected_schemas = {
artifact: SCHEMA_BY_ARTIFACT[artifact] for artifact in json_artifacts
}
if declared_schemas is None:
errors.append("verify_run.py ARTIFACT_SCHEMAS 선언을 정적으로 확인할 수 없음")
elif declared_schemas != expected_schemas:
errors.append(
"verifier artifact schema mapping 불일치: "
f"expected={expected_schemas!r}, actual={declared_schemas!r}"
)
except (OSError, UnicodeError, SyntaxError, ValueError) as exc:
errors.append(f"verify_run.py JSON_ARTIFACTS를 검사할 수 없음: {exc}")
def validate_skill_references(root: Path, errors: list[str]) -> None:
skill_dir = root / "skills" / "technical-doc-flow"
skill_path = require_file(root, "skills/technical-doc-flow/SKILL.md", errors)
try:
text = skill_path.read_text(encoding="utf-8")
except (OSError, UnicodeError):
return
for relative in (
"references/quick-rules.md",
"references/artifact-contracts.md",
"references/logic-flow.md",
"references/reader-contract.md",
"references/terminology-policy.md",
"references/section-playbook.md",
"references/evidence-policy.md",
"references/quality-rubric.md",
):
if relative not in text and Path(relative).name not in text:
errors.append(f"SKILL.md가 reference를 선언하지 않음: {relative}")
if not (skill_dir / relative).is_file():
errors.append(f"SKILL reference 누락: {relative}")
for name in ("init_run.py", "lint_document.py", "verify_run.py", "split_document.py", "reassemble_document.py"):
if name not in text:
errors.append(f"SKILL.md가 runtime 도구를 참조하지 않음: {name}")
def validate_gemini_commands(root: Path, errors: list[str]) -> None:
for relative in ("commands/technical-doc.toml", "commands/technical-doc-review.toml"):
path = require_file(root, relative, errors)
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeError):
continue
if "technical-doc-flow" not in text or "활성화" not in text:
errors.append(f"Gemini command가 등록 skill 활성화를 요구하지 않음: {relative}")
if "skills/technical-doc-flow/SKILL.md" in text or "${extensionPath}" in text:
errors.append(f"Gemini command에 cwd 의존 경로가 있음: {relative}")
if "{{args}}" not in text:
errors.append(f"Gemini command가 사용자 args를 전달하지 않음: {relative}")
def main() -> int:
args = parse_args()
root = args.root.expanduser().resolve()
errors: list[str] = []
if not root.is_dir():
print(f"input error: 저장소 루트가 없습니다: {root}", file=sys.stderr)
return 2
validate_version(root, errors)
validate_manifest_paths(root, errors)
validate_runtime(root, errors)
validate_skill_references(root, errors)
validate_gemini_commands(root, errors)
for relative in ("install.sh", "uninstall.sh", "update.sh"):
path = require_file(root, relative, errors)
if path.exists() and not path.stat().st_mode & 0o111:
errors.append(f"실행 권한 누락: {relative}")
if errors:
for error in errors:
print(f"FAIL: {error}", file=sys.stderr)
return 1
print("release and path contracts are in sync")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("init_run.py")
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("lint_document.py")
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("reassemble_document.py")
+10
View File
@@ -0,0 +1,10 @@
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$env:PYTHONPATH = "$Root/src" + $(if ($env:PYTHONPATH) { ";$env:PYTHONPATH" } else { "" })
$Out = "$Root/examples/output/retry-policy-demo"
if (Test-Path $Out) { Remove-Item -Recurse -Force $Out }
python -m claridoc run `
--brief "$Root/examples/briefs/retry-policy-blog.json" `
--sources "$Root/examples/sources/retry-policy-sources.json" `
--config "$Root/config/pipeline.mock.json" `
--output $Out
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PYTHONPATH="$ROOT/src${PYTHONPATH:+:$PYTHONPATH}"
rm -rf "$ROOT/examples/output/retry-policy-demo"
python3 -m claridoc run \
--brief "$ROOT/examples/briefs/retry-policy-blog.json" \
--sources "$ROOT/examples/sources/retry-policy-sources.json" \
--config "$ROOT/config/pipeline.mock.json" \
--output "$ROOT/examples/output/retry-policy-demo"
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("split_document.py")
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PYTHONPATH="$ROOT/src${PYTHONPATH:+:$PYTHONPATH}"
python3 -m unittest discover -s "$ROOT/tests" -v
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("update_run.py")
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PYTHONPATH="$ROOT/src${PYTHONPATH:+:$PYTHONPATH}"
python3 -m unittest discover -s "$ROOT/tests" -v
python3 -m claridoc validate \
--brief "$ROOT/examples/briefs/retry-policy-blog.json" \
--sources "$ROOT/examples/sources/retry-policy-sources.json"
bash "$ROOT/scripts/run-demo.sh"
python3 - "$ROOT" <<'PY'
from __future__ import annotations
import ast
import hashlib
import json
import re
import sys
from pathlib import Path
root = Path(sys.argv[1]).resolve()
python_files = sorted((root / "src").rglob("*.py")) + sorted((root / "tests").rglob("*.py"))
for path in python_files:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path), feature_version=(3, 10))
json_files = [
path for path in sorted(root.rglob("*.json"))
if not any(part in {"build", "dist", "__pycache__"} for part in path.parts)
]
for path in json_files:
json.loads(path.read_text(encoding="utf-8"))
link_pattern = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
local_links = 0
for path in sorted(root.rglob("*.md")):
if any(part in {"build", "dist", "__pycache__"} for part in path.parts):
continue
for target in link_pattern.findall(path.read_text(encoding="utf-8")):
target = target.strip().split("#", 1)[0]
if not target or re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", target):
continue
local_links += 1
resolved = (path.parent / target).resolve()
if not resolved.exists():
raise SystemExit(f"broken local Markdown link: {path.relative_to(root)} -> {target}")
run_dir = root / "examples" / "output" / "retry-policy-demo"
run = json.loads((run_dir / "run.json").read_text(encoding="utf-8"))
if not run.get("passed"):
raise SystemExit("demo quality gate did not pass")
if not any("synthetic" in warning for warning in run.get("warnings", [])):
raise SystemExit("mock-run synthetic-score warning is missing")
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
for item in manifest["files"]:
artifact = run_dir / item["path"]
if artifact.stat().st_size != item["bytes"]:
raise SystemExit(f"manifest size mismatch: {item['path']}")
digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
if digest != item["sha256"]:
raise SystemExit(f"manifest hash mismatch: {item['path']}")
print(
"VERIFIED: "
f"{len(python_files)} Python files parse with Python 3.10 grammar; "
f"{len(json_files)} JSON files parse; {local_links} local Markdown links resolve; "
f"demo PASS at {run['final_score']:.1f}/100 (synthetic mock score); manifest hashes match."
)
PY
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from _runtime_entry import execute
execute("verify_run.py")