213 lines
6.4 KiB
Python
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()
|