uofa-demo / src /uofa_cli /commands /validate.py
cloudronin's picture
push build context (uofa source + packs + space app)
a28ec65 verified
Raw
History Blame Contribute Delete
4.41 kB
"""uofa validate β€” SHACL validation (and optional integrity check) on all example UofA files."""
from __future__ import annotations
from pathlib import Path
from uofa_cli.output import step_header, result_line, color
from uofa_cli.shacl_friendly import run_shacl_multi, print_violations
from uofa_cli.integrity import verify_file
from uofa_cli import paths
HELP = "validate all examples against SHACL profiles"
_EXCLUDE_DIRS = {"templates"}
def add_arguments(parser):
parser.add_argument("--dir", type=Path, help="directory to scan (default: all pack examples)")
parser.add_argument("--verify", action="store_true",
help="also verify hash + signature integrity on each file")
parser.add_argument("--pubkey", type=Path, help="ed25519 public key (default: keys/research.pub)")
def run(args) -> int:
# Scan a single dir if specified, otherwise all pack example directories
if args.dir:
scan_dirs = [args.dir]
else:
scan_dirs = paths.all_example_dirs()
if not scan_dirs:
raise FileNotFoundError("No example directories found in packs/")
step_header("SHACL validation: all examples")
files = sorted(
f
for d in scan_dirs if d.exists()
for f in d.rglob("*.jsonld")
if not any(part in _EXCLUDE_DIRS for part in f.parts)
)
# Use first scan dir as reference for relative paths
examples = scan_dirs[0].parent if len(scan_dirs) == 1 else paths.find_repo_root()
if not files:
result_line("No .jsonld files found", False)
return 1
# When scanning all examples, use core-only shapes (no factor enum)
# unless a specific pack was explicitly requested via --pack.
# This avoids rejecting NASA examples when default pack is vv40.
if args.pack is None:
shacl_paths = [paths.shacl_schema()]
else:
shacl_paths = paths.all_shacl_schemas(active=paths.resolve_active_packs(args))
passed = 0
failed = 0
for f in files:
conforms, violations = run_shacl_multi(f, shacl_paths)
rel = f.relative_to(examples.parent) if examples.parent in f.parents else f
result_line(str(rel), conforms)
if conforms:
passed += 1
else:
failed += 1
print_violations(violations)
print()
total = passed + failed
if failed == 0:
result_line(f"All {total} example(s) conform", True)
else:
result_line(f"{failed}/{total} example(s) failed", False)
# ── Optional integrity verification ───────────────────────
if args.verify:
step_header("Integrity verification: all examples")
pubkey = args.pubkey or paths.default_pubkey()
if not pubkey.exists():
result_line("Public key not found", False, str(pubkey))
return 1
ctx = paths.context_file()
v_passed = 0
v_failed = 0
import json
for f in files:
rel = f.relative_to(examples.parent) if examples.parent in f.parents else f
try:
with open(f) as fh:
doc = json.load(fh)
declared_hash = doc.get("hash", "")
# Skip files with placeholder hashes (unsigned templates/starters)
if declared_hash.endswith("0" * 64):
result_line(str(rel), True, "unsigned β€” skipped")
v_passed += 1
continue
hash_ok, sig_ok = verify_file(f, pubkey, ctx)
ok = hash_ok and sig_ok
detail = ""
if not hash_ok:
detail = "hash mismatch"
elif not sig_ok:
detail = "signature invalid"
result_line(str(rel), ok, detail)
if ok:
v_passed += 1
else:
v_failed += 1
except Exception as exc:
result_line(str(rel), False, str(exc))
v_failed += 1
print()
v_total = v_passed + v_failed
if v_failed == 0:
result_line(f"All {v_total} example(s) verified", True)
else:
result_line(f"{v_failed}/{v_total} example(s) failed integrity check", False)
failed += v_failed
return 0 if failed == 0 else 1