"""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