init: document-haness 하네스 설계
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:]])
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("build_quick_rules.py")
|
||||
@@ -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())
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("init_run.py")
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("lint_document.py")
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("reassemble_document.py")
|
||||
@@ -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
|
||||
Executable
+10
@@ -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"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("split_document.py")
|
||||
Executable
+5
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("update_run.py")
|
||||
Executable
+71
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from _runtime_entry import execute
|
||||
|
||||
execute("verify_run.py")
|
||||
Reference in New Issue
Block a user