158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify that R2 workflow sources are connected to deterministic gateways."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
from typing import Any, Iterable
|
|
|
|
|
|
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
|
|
RESULT_SCHEMA = "workflow-connection-result/v1"
|
|
WRITER_SOURCES = (
|
|
Path("harness/source/agents/bodies/wiki-doc-author.md"),
|
|
Path("harness/source/agents/bodies/wiki-source-summarizer.md"),
|
|
)
|
|
GLOBAL_REPORT_SOURCE = Path(".agents/plugins/wiki-superpowers/skills/wiki-workflow/SKILL.md")
|
|
WRITER_REQUIRED = (
|
|
"harness/runtime/document_commit.py",
|
|
"document-commit/v1",
|
|
"document-commit-result/v1",
|
|
"--dry-run",
|
|
"plan_sha256",
|
|
"--expected-plan-sha256",
|
|
"--apply",
|
|
)
|
|
WRITER_FORBIDDEN = (
|
|
"자동 rollback 미구현",
|
|
"**C4. Parent hub Cluster 갱신**",
|
|
"**M5. Parent hub Cluster 점검**",
|
|
"### Step 6: Parent hub Cluster 갱신",
|
|
)
|
|
REPORT_REQUIRED = (
|
|
"proof-request/v1",
|
|
"harness/runtime/proof_runner.py",
|
|
"proof-runner-result/v1",
|
|
"proof-manifest/v1",
|
|
"manifest_sha256",
|
|
"proof_count",
|
|
"pass_count",
|
|
"fail_count",
|
|
"1~3",
|
|
"실패",
|
|
)
|
|
SEMANTIC_WORKFLOW_SOURCES = (
|
|
Path("harness/source/skills/project-spec.md"),
|
|
Path("harness/source/skills/branch-spec.md"),
|
|
Path("harness/source/skills/sync.md"),
|
|
)
|
|
SEMANTIC_REQUIRED = (
|
|
"semantic_surface_extractor.py",
|
|
"semantic_candidate_builder.py",
|
|
"wiki-semantic-coherence-auditor",
|
|
"semantic_audit.py",
|
|
"semantic certificate",
|
|
)
|
|
PARENT_CERTIFICATE_SOURCE = Path("harness/source/skills/branch-from-project.md")
|
|
PARENT_CERTIFICATE_REQUIRED = (
|
|
"semantic_certificate.py",
|
|
"--mode hub",
|
|
"--path raw/project-notes/<project>.md",
|
|
)
|
|
|
|
|
|
class ConnectionCheckError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _finding(code: str, path: Path, token: str) -> dict[str, str]:
|
|
return {"code": code, "path": path.as_posix(), "token": token}
|
|
|
|
|
|
def _require_tokens(path: Path, text: str, tokens: Iterable[str]) -> list[dict[str, str]]:
|
|
return [_finding("MISSING_REQUIRED_CONNECTION", path, token) for token in tokens if token not in text]
|
|
|
|
|
|
def _report_sources(root: Path) -> list[Path]:
|
|
body_root = root / "harness/source/agents/bodies"
|
|
sources = [
|
|
path.relative_to(root)
|
|
for path in body_root.glob("*.md")
|
|
if "§7.1" in path.read_text(encoding="utf-8") or "Self-Grep" in path.read_text(encoding="utf-8")
|
|
]
|
|
sources.append(GLOBAL_REPORT_SOURCE)
|
|
return sorted(set(sources), key=lambda item: item.as_posix())
|
|
|
|
|
|
def check(root: Path) -> dict[str, Any]:
|
|
root = root.resolve(strict=True)
|
|
findings: list[dict[str, str]] = []
|
|
for relative in WRITER_SOURCES:
|
|
path = root / relative
|
|
if not path.is_file():
|
|
findings.append(_finding("MISSING_CONNECTION_SOURCE", relative, ""))
|
|
continue
|
|
text = path.read_text(encoding="utf-8")
|
|
findings.extend(_require_tokens(relative, text, WRITER_REQUIRED))
|
|
findings.extend(
|
|
_finding("FORBIDDEN_DIRECT_WRITE_CONTRACT", relative, token)
|
|
for token in WRITER_FORBIDDEN
|
|
if token in text
|
|
)
|
|
|
|
for relative in SEMANTIC_WORKFLOW_SOURCES:
|
|
path = root / relative
|
|
if not path.is_file():
|
|
findings.append(_finding("MISSING_CONNECTION_SOURCE", relative, ""))
|
|
continue
|
|
findings.extend(_require_tokens(relative, path.read_text(encoding="utf-8"), SEMANTIC_REQUIRED))
|
|
parent_path = root / PARENT_CERTIFICATE_SOURCE
|
|
if not parent_path.is_file():
|
|
findings.append(_finding("MISSING_CONNECTION_SOURCE", PARENT_CERTIFICATE_SOURCE, ""))
|
|
else:
|
|
findings.extend(_require_tokens(PARENT_CERTIFICATE_SOURCE, parent_path.read_text(encoding="utf-8"), PARENT_CERTIFICATE_REQUIRED))
|
|
|
|
reporters = _report_sources(root)
|
|
for relative in reporters:
|
|
path = root / relative
|
|
if not path.is_file():
|
|
findings.append(_finding("MISSING_CONNECTION_SOURCE", relative, ""))
|
|
continue
|
|
findings.extend(_require_tokens(relative, path.read_text(encoding="utf-8"), REPORT_REQUIRED))
|
|
|
|
findings.sort(key=lambda item: (item["path"], item["code"], item["token"]))
|
|
return {
|
|
"schema_version": RESULT_SCHEMA,
|
|
"status": "PASS" if not findings else "FAIL",
|
|
"writer_sources": len(WRITER_SOURCES),
|
|
"semantic_workflow_sources": len(SEMANTIC_WORKFLOW_SOURCES) + 1,
|
|
"report_sources": len(reporters),
|
|
"findings": findings,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
result = check(args.root)
|
|
exit_code = 0 if result["status"] == "PASS" else 1
|
|
except (ConnectionCheckError, OSError, UnicodeError, ValueError) as exc:
|
|
result = {
|
|
"schema_version": RESULT_SCHEMA,
|
|
"status": "ERROR",
|
|
"errors": [{"code": "WORKFLOW_CONNECTION_ERROR", "message": str(exc)}],
|
|
}
|
|
exit_code = 2
|
|
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
|
sys.stdout.write("\n")
|
|
return exit_code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|