Files
CM95/tools/cm95_s52_runtime_capability_audit.py
2026-09-19 16:42:03 +08:00

213 lines
6.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
SCHEMA = "navsea.cm95.s52-runtime-capability-audit/v1"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Audit runtime portrayal kinds required by a release-scoped "
"S-52 style usage audit against a concrete target capability registry."
)
)
parser.add_argument("--plan", required=True)
parser.add_argument("--style-audit", required=True)
parser.add_argument("--capabilities", required=True)
parser.add_argument("--output", required=True)
parser.add_argument(
"--required-kind",
action="append",
default=[],
help=(
"Optional explicit runtime kind. When omitted, every non-zero "
"runtime_coverage kind in the style audit is required."
),
)
return parser.parse_args()
def load_json(path: Path) -> dict[str, Any]:
obj = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(obj, dict):
raise SystemExit(f"JSON root must be object: {path}")
return obj
def verify_evidence(
project_root: Path,
kind: str,
capability: dict[str, Any],
) -> list[dict[str, Any]]:
failures: list[dict[str, Any]] = []
evidence = capability.get("evidence") or []
if not evidence:
failures.append(
{
"kind": kind,
"failure": "implemented_capability_has_no_evidence",
}
)
return failures
for item in evidence:
if not isinstance(item, dict):
failures.append(
{
"kind": kind,
"failure": "evidence_entry_not_object",
}
)
continue
raw_path = item.get("path")
if not raw_path:
failures.append(
{
"kind": kind,
"failure": "evidence_path_missing",
}
)
continue
path = (project_root / str(raw_path)).resolve()
if not path.is_file():
failures.append(
{
"kind": kind,
"path": str(raw_path),
"failure": "evidence_file_missing",
}
)
continue
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
failures.append(
{
"kind": kind,
"path": str(raw_path),
"failure": f"evidence_read_error:{type(exc).__name__}",
}
)
continue
for marker in item.get("contains") or []:
if str(marker) not in text:
failures.append(
{
"kind": kind,
"path": str(raw_path),
"marker": str(marker),
"failure": "evidence_marker_missing",
}
)
return failures
def main() -> None:
args = parse_args()
plan_path = Path(args.plan)
style_audit_path = Path(args.style_audit)
capability_path = Path(args.capabilities)
plan = load_json(plan_path)
style_audit = load_json(style_audit_path)
registry = load_json(capability_path)
if registry.get("schema") != (
"navsea.cm95.s52-runtime-capability-registry/v1"
):
raise SystemExit("runtime capability registry schema mismatch")
runtime_coverage = style_audit.get("runtime_coverage") or {}
observed = {
str(kind): int((row or {}).get("feature_count") or 0)
for kind, row in runtime_coverage.items()
if int((row or {}).get("feature_count") or 0) > 0
}
if args.required_kind:
required = sorted(set(str(item) for item in args.required_kind))
else:
required = sorted(observed)
capabilities = registry.get("capabilities") or {}
missing: list[str] = []
noncompliant: list[dict[str, Any]] = []
evidence_failures: list[dict[str, Any]] = []
checked: list[dict[str, Any]] = []
project_root = Path(__file__).resolve().parents[1]
for kind in required:
capability = capabilities.get(kind)
if not isinstance(capability, dict):
missing.append(kind)
continue
status = str(capability.get("status") or "")
checked.append(
{
"kind": kind,
"feature_count": int(observed.get(kind, 0)),
"status": status,
"fidelity": capability.get("fidelity"),
}
)
if status != "IMPLEMENTED":
noncompliant.append(
{
"kind": kind,
"status": status,
"fidelity": capability.get("fidelity"),
}
)
continue
evidence_failures.extend(
verify_evidence(project_root, kind, capability)
)
failures: list[str] = []
if style_audit.get("hard_gate_failures"):
failures.append("style_audit_hard_gate_failures")
if missing:
failures.append("runtime_capability_missing")
if noncompliant:
failures.append("runtime_capability_not_implemented")
if evidence_failures:
failures.append("runtime_capability_evidence_failure")
result = {
"schema": SCHEMA,
"status": "PASS" if not failures else "FAIL",
"plan": str(plan_path),
"plan_id": plan.get("plan_id"),
"style_audit": str(style_audit_path),
"capability_registry": str(capability_path),
"target": registry.get("target"),
"observed_runtime_kinds": observed,
"required_runtime_kinds": required,
"checked_runtime_kinds": checked,
"missing_runtime_kinds": missing,
"noncompliant_runtime_kinds": noncompliant,
"evidence_failure_count": len(evidence_failures),
"evidence_failures": evidence_failures[:100],
"failures": failures,
}
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if result["status"] != "PASS":
raise SystemExit(2)
if __name__ == "__main__":
main()