Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ea3-7939-7b63-8125-699f8b50bedd
418 lines
14 KiB
Python
418 lines
14 KiB
Python
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# requires-python = ">=3.11"
|
|
# dependencies = ["jsonschema>=4.23,<5", "PyYAML>=6,<7"]
|
|
# ///
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import jsonschema
|
|
import yaml
|
|
|
|
PROFILE_ID = "netkingdom-emission-cadence-security-profile-v0.1"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Finding:
|
|
level: str
|
|
code: str
|
|
event_class: str | None
|
|
message: str
|
|
|
|
|
|
def load_document(path: Path) -> Any:
|
|
with path.open(encoding="utf-8") as handle:
|
|
return yaml.safe_load(handle)
|
|
|
|
|
|
def _finding(level: str, code: str, event_class: str | None, message: str) -> Finding:
|
|
return Finding(level=level, code=code, event_class=event_class, message=message)
|
|
|
|
|
|
def _schema_findings(schema: Any, declaration: Any) -> list[Finding]:
|
|
try:
|
|
jsonschema.Draft202012Validator.check_schema(schema)
|
|
except jsonschema.SchemaError as exc:
|
|
return [_finding("MUST", "contract-schema-invalid", None, exc.message)]
|
|
|
|
validator = jsonschema.Draft202012Validator(
|
|
schema, format_checker=jsonschema.FormatChecker()
|
|
)
|
|
findings: list[Finding] = []
|
|
for error in sorted(
|
|
validator.iter_errors(declaration),
|
|
key=lambda item: tuple(str(part) for part in item.path),
|
|
):
|
|
location = "/" + "/".join(str(part) for part in error.path)
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"contract-validation-failed",
|
|
None,
|
|
f"{location}: {error.message}",
|
|
)
|
|
)
|
|
return findings
|
|
|
|
|
|
def _entries(declaration: Any) -> tuple[list[dict[str, Any]], list[Finding]]:
|
|
if not isinstance(declaration, dict):
|
|
return [], [
|
|
_finding(
|
|
"MUST", "declaration-not-object", None, "declaration must be an object"
|
|
)
|
|
]
|
|
raw_entries = declaration.get("sources")
|
|
if not isinstance(raw_entries, list):
|
|
return [], [
|
|
_finding(
|
|
"MUST",
|
|
"sources-unavailable",
|
|
None,
|
|
"the imported contract must expose cadence entries at /sources",
|
|
)
|
|
]
|
|
entries: list[dict[str, Any]] = []
|
|
for index, item in enumerate(raw_entries):
|
|
if not isinstance(item, dict):
|
|
return [], [
|
|
_finding(
|
|
"MUST",
|
|
"source-entry-not-object",
|
|
None,
|
|
f"/sources/{index} must be an object",
|
|
)
|
|
]
|
|
entries.append(item)
|
|
return entries, []
|
|
|
|
|
|
def evaluate_profile(
|
|
declaration: Any,
|
|
*,
|
|
load_bearing: set[str],
|
|
rare_load_bearing: set[str],
|
|
attributive: set[str],
|
|
) -> list[Finding]:
|
|
findings: list[Finding] = []
|
|
overlap = (load_bearing | rare_load_bearing) & attributive
|
|
for event_class in sorted(overlap):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"inventory-class-conflict",
|
|
event_class,
|
|
"the source inventory classifies the event as both load-bearing and attributive",
|
|
)
|
|
)
|
|
|
|
entries, structural = _entries(declaration)
|
|
if structural:
|
|
return findings + structural
|
|
|
|
indexed: dict[str, dict[str, Any]] = {}
|
|
source_ids: set[str] = set()
|
|
for entry in entries:
|
|
source_id = entry.get("source_id")
|
|
if isinstance(source_id, str):
|
|
if source_id in source_ids:
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"duplicate-source-id",
|
|
None,
|
|
"source_id values must be unique",
|
|
)
|
|
)
|
|
source_ids.add(source_id)
|
|
event_class = entry.get("event_class")
|
|
if not isinstance(event_class, str) or not event_class:
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"event-class-unavailable",
|
|
None,
|
|
"each cadence entry needs event_class",
|
|
)
|
|
)
|
|
continue
|
|
if event_class in indexed:
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"duplicate-event-class",
|
|
event_class,
|
|
"only one cadence entry is permitted per event class",
|
|
)
|
|
)
|
|
continue
|
|
indexed[event_class] = entry
|
|
|
|
required_load_bearing = load_bearing | rare_load_bearing
|
|
for event_class in sorted(required_load_bearing):
|
|
entry = indexed.get(event_class)
|
|
if entry is None:
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"load-bearing-cadence-missing",
|
|
event_class,
|
|
"a source-declared load-bearing class must declare cadence",
|
|
)
|
|
)
|
|
elif (
|
|
entry.get("extensions", {}).get("netkingdom", {}).get("evidence_class")
|
|
!= "load-bearing"
|
|
):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"evidence-class-mismatch",
|
|
event_class,
|
|
"cadence evidence_class must match the source inventory: load-bearing",
|
|
)
|
|
)
|
|
|
|
for event_class in sorted(attributive):
|
|
entry = indexed.get(event_class)
|
|
if entry is None:
|
|
findings.append(
|
|
_finding(
|
|
"SHOULD",
|
|
"attributive-cadence-missing",
|
|
event_class,
|
|
"an attributive class should declare cadence; completeness must not be claimed",
|
|
)
|
|
)
|
|
elif (
|
|
entry.get("extensions", {}).get("netkingdom", {}).get("evidence_class")
|
|
!= "attributive"
|
|
):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"evidence-class-mismatch",
|
|
event_class,
|
|
"cadence evidence_class must match the source inventory: attributive",
|
|
)
|
|
)
|
|
|
|
for event_class in sorted(rare_load_bearing):
|
|
entry = indexed.get(event_class)
|
|
if entry is None:
|
|
continue
|
|
if entry.get("form") != "heartbeat-or-reconciliation":
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rare-form-invalid",
|
|
event_class,
|
|
"rare load-bearing evidence requires heartbeat-or-reconciliation",
|
|
)
|
|
)
|
|
if (
|
|
entry.get("extensions", {}).get("netkingdom", {}).get("rate_monitoring")
|
|
!= "forbidden"
|
|
):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rare-rate-monitoring-not-forbidden",
|
|
event_class,
|
|
"rate_monitoring must be forbidden for rare load-bearing evidence",
|
|
)
|
|
)
|
|
heartbeat = entry.get("heartbeat")
|
|
if not isinstance(heartbeat, dict):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rare-heartbeat-missing",
|
|
event_class,
|
|
"rare load-bearing evidence requires a positive heartbeat",
|
|
)
|
|
)
|
|
else:
|
|
if not heartbeat.get("assertion"):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"heartbeat-assertion-missing",
|
|
event_class,
|
|
"heartbeat must carry a positive assertion",
|
|
)
|
|
)
|
|
if heartbeat.get("missing") != "finding":
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"heartbeat-missing-not-finding",
|
|
event_class,
|
|
"a missing heartbeat must be a finding",
|
|
)
|
|
)
|
|
reconciliation = entry.get("reconciliation", {})
|
|
local = reconciliation.get("compare_local")
|
|
observed = reconciliation.get("compare_observed")
|
|
divergence = reconciliation.get("divergence")
|
|
if local is None or observed is None:
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rare-reconciliation-missing",
|
|
event_class,
|
|
"reconciliation must compare source and evidence-engine counts",
|
|
)
|
|
)
|
|
if divergence != "finding":
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"reconciliation-divergence-not-finding",
|
|
event_class,
|
|
"reconciliation divergence must be a finding",
|
|
)
|
|
)
|
|
|
|
for event_class in sorted(required_load_bearing - rare_load_bearing):
|
|
entry = indexed.get(event_class)
|
|
if (
|
|
entry is None
|
|
or entry.get("extensions", {}).get("netkingdom", {}).get("evidence_class")
|
|
!= "load-bearing"
|
|
):
|
|
continue
|
|
if entry.get("form") == "expected-rate":
|
|
window = entry.get("window_seconds", entry.get("window"))
|
|
if (
|
|
not window
|
|
or (
|
|
isinstance(window, str)
|
|
and not any(int(part) > 0 for part in re.findall(r"\d+", window))
|
|
)
|
|
or isinstance(window, bool)
|
|
or (isinstance(window, (int, float)) and window <= 0)
|
|
):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rate-window-invalid",
|
|
event_class,
|
|
"expected-rate load-bearing evidence needs a positive window",
|
|
)
|
|
)
|
|
if (
|
|
isinstance(entry.get("expected_min"), bool)
|
|
or not isinstance(entry.get("expected_min"), int)
|
|
or entry["expected_min"] <= 0
|
|
):
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"expected-min-invalid",
|
|
event_class,
|
|
"expected-rate load-bearing evidence needs expected_min > 0",
|
|
)
|
|
)
|
|
if entry.get("drop_below") != "finding":
|
|
findings.append(
|
|
_finding(
|
|
"MUST",
|
|
"rate-drop-not-finding",
|
|
event_class,
|
|
"a drop below the declared load-bearing rate must be a finding",
|
|
)
|
|
)
|
|
|
|
return sorted(
|
|
findings,
|
|
key=lambda item: (item.level, item.code, item.event_class or "", item.message),
|
|
)
|
|
|
|
|
|
def build_report(
|
|
contract_schema: Any,
|
|
declaration: Any,
|
|
*,
|
|
contract_schema_path: str,
|
|
declaration_path: str,
|
|
load_bearing: set[str],
|
|
rare_load_bearing: set[str],
|
|
attributive: set[str],
|
|
fail_on_should: bool = False,
|
|
) -> dict[str, Any]:
|
|
findings = _schema_findings(contract_schema, declaration)
|
|
contract_valid = not findings
|
|
if contract_valid:
|
|
findings.extend(
|
|
evaluate_profile(
|
|
declaration,
|
|
load_bearing=load_bearing,
|
|
rare_load_bearing=rare_load_bearing,
|
|
attributive=attributive,
|
|
)
|
|
)
|
|
must_count = sum(item.level == "MUST" for item in findings)
|
|
should_count = sum(item.level == "SHOULD" for item in findings)
|
|
return {
|
|
"profile": PROFILE_ID,
|
|
"contract_schema": contract_schema_path,
|
|
"declaration": declaration_path,
|
|
"contract_valid": contract_valid,
|
|
"conformant": must_count == 0 and (not fail_on_should or should_count == 0),
|
|
"summary": {"must": must_count, "should": should_count},
|
|
"findings": [asdict(item) for item in findings],
|
|
}
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate an imported emission-cadence declaration against the NetKingdom profile."
|
|
)
|
|
parser.add_argument("declaration", type=Path)
|
|
parser.add_argument("--contract-schema", required=True, type=Path)
|
|
parser.add_argument(
|
|
"--load-bearing", action="append", default=[], metavar="EVENT_CLASS"
|
|
)
|
|
parser.add_argument(
|
|
"--rare-load-bearing", action="append", default=[], metavar="EVENT_CLASS"
|
|
)
|
|
parser.add_argument(
|
|
"--attributive", action="append", default=[], metavar="EVENT_CLASS"
|
|
)
|
|
parser.add_argument("--fail-on-should", action="store_true")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
try:
|
|
schema = load_document(args.contract_schema)
|
|
declaration = load_document(args.declaration)
|
|
except (OSError, yaml.YAMLError) as exc:
|
|
print(json.dumps({"error": str(exc)}, sort_keys=True), file=sys.stderr)
|
|
return 2
|
|
report = build_report(
|
|
schema,
|
|
declaration,
|
|
contract_schema_path=str(args.contract_schema),
|
|
declaration_path=str(args.declaration),
|
|
load_bearing=set(args.load_bearing),
|
|
rare_load_bearing=set(args.rare_load_bearing),
|
|
attributive=set(args.attributive),
|
|
fail_on_should=args.fail_on_should,
|
|
)
|
|
print(json.dumps(report, indent=2, sort_keys=True))
|
|
return 0 if report["conformant"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|