#!/usr/bin/env python3 """Build semantic auditor requests and validate grounded verdict results. This runtime deliberately does not infer assertions or verdicts. It binds the auditor's work to deterministic surface/candidate bytes, revalidates proof manifests, and applies the blocking policy from the ontology. """ from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import re import sys from typing import Any, Iterable, Mapping import proof_manifest import semantic_candidate_builder import semantic_surface_extractor ASSERTION_REQUEST_SCHEMA = "semantic-assertion-request/v1" VERDICT_REQUEST_SCHEMA = "semantic-verdict-request/v1" AUDIT_RESULT_SCHEMA = "semantic-audit-result/v1" VALIDATED_RESULT_SCHEMA = "semantic-audit-validation-result/v1" HEX_SHA256 = re.compile(r"^[0-9a-f]{64}$") class SemanticAuditError(ValueError): """Audit request/result bytes violate the deterministic contract.""" def canonical_json_bytes(value: Any) -> bytes: return semantic_surface_extractor.canonical_json_bytes(value) def _sha(value: Any) -> str: return hashlib.sha256(canonical_json_bytes(value)).hexdigest() def build_assertion_request(extraction: Mapping[str, Any], ontology: Mapping[str, Any]) -> dict[str, Any]: coverage = extraction.get("coverage", {}) if extraction.get("findings") and any(item.get("severity") == "error" for item in extraction["findings"]): raise SemanticAuditError("cannot request assertions for uncovered semantic surfaces") if coverage.get("eligible_surface_blocks") != coverage.get("extracted_surface_blocks", 0) + coverage.get("explicitly_excluded_blocks", 0): raise SemanticAuditError("surface coverage is incomplete") return { "schema_version": ASSERTION_REQUEST_SCHEMA, "subject": extraction["path"], "mode": extraction["mode"], "document_sha256": extraction["document_sha256"], "surface_manifest_sha256": _sha(extraction), "ontology_sha256": _sha(ontology), "predicate_ontology": list(ontology["predicates"]), "surfaces": list(extraction["surfaces"]), "explicitly_excluded": list(extraction["excluded"]), "output_schema": semantic_candidate_builder.ASSERTION_SCHEMA, } def build_verdict_request(candidate_result: Mapping[str, Any], *, explicit_blocking: Iterable[Mapping[str, Any]] = ()) -> dict[str, Any]: if candidate_result.get("schema_version") != semantic_candidate_builder.RESULT_SCHEMA or candidate_result.get("status") != "PASS": raise SemanticAuditError("candidate result must be semantic-candidate-result/v1 PASS") blocking: list[dict[str, str]] = [] for index, item in enumerate(explicit_blocking): if not isinstance(item, Mapping) or set(item) != {"code", "message"}: raise SemanticAuditError(f"explicit_blocking[{index}] must contain code and message") code, message = item.get("code"), item.get("message") if not isinstance(code, str) or not code or not isinstance(message, str) or not message: raise SemanticAuditError(f"explicit_blocking[{index}] fields must be non-empty strings") blocking.append({"code": code, "message": message}) return { "schema_version": VERDICT_REQUEST_SCHEMA, "subject": candidate_result["subject"], "mode": candidate_result["mode"], "document_sha256": candidate_result["document_sha256"], "candidate_manifest_sha256": _sha(candidate_result), "ontology_sha256": candidate_result["ontology_sha256"], "coverage": dict(candidate_result["coverage"]), "assertions": list(candidate_result["assertions"]), "candidates": list(candidate_result["candidates"]), "explicit_blocking": sorted(blocking, key=lambda item: (item["code"], item["message"])), "output_schema": AUDIT_RESULT_SCHEMA, } def _resolve_reference(root: Path, reference: Mapping[str, Any], run_root: Path | None) -> tuple[Path, str]: if set(reference) != {"namespace", "path", "sha256"}: raise SemanticAuditError("proof_manifest must contain namespace, path, and sha256") namespace, value, expected = reference.get("namespace"), reference.get("path"), reference.get("sha256") if namespace not in {"repo", "run"}: raise SemanticAuditError("proof manifest namespace must be repo or run") base = root if namespace == "repo" else run_root if base is None: raise SemanticAuditError("run proof manifest requires run_root") if not isinstance(value, str) or not value or "\\" in value or Path(value).is_absolute(): raise SemanticAuditError("proof manifest path must be relative POSIX") path = (base / value).resolve() try: path.relative_to(base.resolve()) except ValueError as exc: raise SemanticAuditError("proof manifest path escapes its namespace") from exc if not path.is_file(): raise SemanticAuditError("proof manifest does not exist") if not isinstance(expected, str) or not HEX_SHA256.fullmatch(expected): raise SemanticAuditError("proof manifest sha256 is invalid") observed = hashlib.sha256(path.read_bytes()).hexdigest() if observed != expected: raise SemanticAuditError("proof manifest hash mismatch") return path, expected def _verify_evidence(verdict: Mapping[str, Any], candidate: Mapping[str, Any], assertions: Mapping[str, Mapping[str, Any]]) -> None: for key, assertion_key in (("evidence_a", "assertion_a"), ("evidence_b", "assertion_b")): evidence = verdict.get(key) assertion = assertions[candidate[assertion_key]] if not isinstance(evidence, Mapping) or set(evidence) != {"quote", "line_start", "line_end"}: raise SemanticAuditError(f"{key} must contain quote and exact line range") if ( evidence.get("quote") != assertion["quote"] or evidence.get("line_start") != assertion["line_start"] or evidence.get("line_end") != assertion["line_end"] ): raise SemanticAuditError(f"{key} does not match its grounded assertion") def _verify_negative_proof( root: Path, run_root: Path | None, profiles: set[str], verdict: Mapping[str, Any], candidate: Mapping[str, Any], assertions: Mapping[str, Mapping[str, Any]], ) -> str: reference = verdict.get("proof_manifest") if not isinstance(reference, Mapping): raise SemanticAuditError("negative semantic verdict requires proof_manifest") path, digest = _resolve_reference(root, reference, run_root) try: manifest = json.loads(path.read_text(encoding="utf-8")) verified = proof_manifest.verify_manifest(manifest, root, profiles, run_root=run_root) except (OSError, UnicodeError, json.JSONDecodeError, proof_manifest.ManifestValidationError) as exc: raise SemanticAuditError(f"proof manifest revalidation failed: {exc}") from exc if verified["verification"]["status"] != "PASS" or verified["verification"]["fail_count"] != 0: raise SemanticAuditError("proof manifest is not PASS") expected = { (candidate["candidate_id"], "assertion_a", assertions[candidate["assertion_a"]]["quote"]), (candidate["candidate_id"], "assertion_b", assertions[candidate["assertion_b"]]["quote"]), } observed = { (item["finding"]["id"], item["finding"]["role"], item["source"]["quote_utf8"]) for item in verified["proofs"] } if not expected.issubset(observed): raise SemanticAuditError("proof manifest does not bind both candidate assertions") return digest def validate_result( root: Path, request: Mapping[str, Any], result: Any, *, ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY, profiles_path: Path = proof_manifest.DEFAULT_PROFILES, run_root: Path | None = None, ) -> dict[str, Any]: root = root.resolve(strict=True) ontology = semantic_candidate_builder.load_ontology(root, ontology_path) request_fields = { "schema_version", "subject", "mode", "document_sha256", "candidate_manifest_sha256", "ontology_sha256", "coverage", "assertions", "candidates", "explicit_blocking", "output_schema", } if not isinstance(request, Mapping) or set(request) != request_fields or request.get("schema_version") != VERDICT_REQUEST_SCHEMA: raise SemanticAuditError(f"expected {VERDICT_REQUEST_SCHEMA}") subject = request.get("subject") if not isinstance(subject, str) or not subject or Path(subject).is_absolute() or ".." in Path(subject).parts: raise SemanticAuditError("verdict request subject must be a canonical repo-relative path") document = Path(os.path.abspath(root / subject)) try: document.relative_to(root) document.resolve(strict=True).relative_to(root) except (ValueError, FileNotFoundError) as exc: raise SemanticAuditError("verdict request subject escapes repository") from exc if not document.is_file(): raise SemanticAuditError("verdict request subject does not exist") current_document_sha = hashlib.sha256(document.read_bytes()).hexdigest() if request.get("document_sha256") != current_document_sha: raise SemanticAuditError("verdict request is stale for current document bytes") policy = semantic_surface_extractor.load_policy(root) extraction = semantic_surface_extractor.extract_document(root, document, policy) if extraction["mode"] != request.get("mode"): raise SemanticAuditError("verdict request mode differs from current document policy") assertion_result = { "schema_version": semantic_candidate_builder.ASSERTION_SCHEMA, "subject": subject, "mode": request["mode"], "surface_manifest_sha256": _sha(extraction), "assertions": request.get("assertions"), } rebuilt = semantic_candidate_builder.build(root, extraction, assertion_result, ontology_path) for field in ("ontology_sha256", "coverage", "assertions", "candidates"): if request.get(field) != rebuilt[field]: raise SemanticAuditError(f"verdict request {field} differs from deterministic reconstruction") if request.get("candidate_manifest_sha256") != _sha(rebuilt): raise SemanticAuditError("verdict request candidate manifest hash is stale or forged") if request.get("output_schema") != AUDIT_RESULT_SCHEMA: raise SemanticAuditError("verdict request output schema mismatch") required = {"schema_version", "request_sha256", "subject", "mode", "auditor", "verdicts"} if not isinstance(result, dict) or set(result) != required or result.get("schema_version") != AUDIT_RESULT_SCHEMA: raise SemanticAuditError(f"audit result must contain exact {AUDIT_RESULT_SCHEMA} fields") if result.get("request_sha256") != _sha(request): raise SemanticAuditError("audit result is not bound to current verdict request") if result.get("subject") != request.get("subject") or result.get("mode") != request.get("mode"): raise SemanticAuditError("audit result subject/mode mismatch") auditor = result.get("auditor") if not isinstance(auditor, dict) or set(auditor) != {"contract_version", "model_id", "run_id"}: raise SemanticAuditError("auditor must contain contract_version/model_id/run_id") if auditor.get("contract_version") != ontology["auditor_contract_version"]: raise SemanticAuditError("auditor contract version mismatch") if any(not isinstance(auditor.get(key), str) or not auditor[key] for key in ("model_id", "run_id")): raise SemanticAuditError("auditor model_id/run_id must be non-empty") raw_verdicts = result.get("verdicts") if not isinstance(raw_verdicts, list): raise SemanticAuditError("verdicts must be an array") candidates = {item["candidate_id"]: item for item in request["candidates"]} assertions = {item["assertion_id"]: item for item in request["assertions"]} seen: set[str] = set() verified_findings: list[dict[str, Any]] = [] dropped: list[dict[str, Any]] = [] positive = {"CONSISTENT", "COMPLEMENTARY", "CONTEXTUAL_VARIANT"} negative = {"AMBIGUOUS_AUTHORITY", "RESTATEMENT_DRIFT", "CONTRADICTION"} profiles_source = profiles_path if profiles_path.is_absolute() else root / profiles_path profiles = proof_manifest.load_allowed_profiles(profiles_source.resolve(strict=True)) for index, item in enumerate(raw_verdicts): fields = {"candidate_id", "verdict", "rationale", "evidence_a", "evidence_b", "proof_manifest"} if not isinstance(item, dict) or set(item) != fields: raise SemanticAuditError(f"verdicts[{index}] has missing or unknown fields") candidate_id = item.get("candidate_id") verdict = item.get("verdict") if candidate_id not in candidates or candidate_id in seen: raise SemanticAuditError(f"verdicts[{index}] has unknown or duplicate candidate_id") seen.add(candidate_id) if verdict not in ontology["verdicts"]: raise SemanticAuditError(f"verdicts[{index}] is outside exact verdict set") if not isinstance(item.get("rationale"), str) or not item["rationale"].strip(): raise SemanticAuditError(f"verdicts[{index}].rationale must be non-empty") candidate = candidates[candidate_id] _verify_evidence(item, candidate, assertions) if verdict in positive: if item.get("proof_manifest") is not None: raise SemanticAuditError("passing verdict must not claim a finding proof") continue try: proof_sha = _verify_negative_proof(root, run_root, profiles, item, candidate, assertions) except SemanticAuditError as exc: dropped.append({"candidate_id": candidate_id, "verdict": verdict, "reason": str(exc)}) continue verified_findings.append({ "candidate_id": candidate_id, "verdict": verdict, "rationale": item["rationale"], "evidence_a": dict(item["evidence_a"]), "evidence_b": dict(item["evidence_b"]), "proof_manifest_sha256": proof_sha, }) missing = sorted(set(candidates) - seen) if missing: raise SemanticAuditError(f"candidate verdict coverage is incomplete: {missing}") mode = str(request["mode"]) explicit = list(request.get("explicit_blocking", [])) blocking = len(explicit) + sum( item["verdict"] == "CONTRADICTION" or (mode == "hub" and item["verdict"] == "AMBIGUOUS_AUTHORITY") for item in verified_findings ) readiness = sum(item["verdict"] == "RESTATEMENT_DRIFT" for item in verified_findings) # A negative judgment without replayable proof is not evidence of a # contradiction, but it is also not a certifiable clean audit. Treat # dropped candidates as fail-closed in every mode so a local certificate # cannot hide an auditor-raised contradiction merely because its proof # reference was omitted or stale. status = "PASS" if blocking == 0 and readiness == 0 and not dropped else "FAIL" return { "schema_version": VALIDATED_RESULT_SCHEMA, "status": status, "subject": request["subject"], "mode": mode, "document_sha256": request["document_sha256"], "request_sha256": _sha(request), "ontology_sha256": request["ontology_sha256"], "coverage": { "eligible_surfaces": request["coverage"]["eligible_surfaces"], "processed_surfaces": request["coverage"]["processed_surfaces"], "candidate_pairs": len(candidates), "processed_pairs": len(seen), "dropped_pairs": len(dropped), }, "findings": verified_findings, "dropped": dropped, "explicit_blocking": explicit, "counts": { "blocking": blocking, "readiness_blocking": readiness, "verified_findings": len(verified_findings), "dropped_pairs": len(dropped), }, "auditor": dict(auditor), } def _load(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def _one_extraction(value: Any) -> Mapping[str, Any]: if isinstance(value, dict) and {"path", "mode", "surfaces", "coverage"}.issubset(value): return value documents = value.get("documents") if isinstance(value, dict) else None if not isinstance(documents, list) or len(documents) != 1 or not isinstance(documents[0], dict): raise SemanticAuditError("assertion request input must contain exactly one extracted document") return documents[0] def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("phase", choices=("assertion-request", "verdict-request", "validate")) parser.add_argument("input", type=Path) parser.add_argument("result", type=Path, nargs="?") parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2]) parser.add_argument("--run-root", type=Path) parser.add_argument("--explicit-blocking", type=Path) args = parser.parse_args(argv) try: root = args.root.resolve(strict=True) if args.phase == "assertion-request": extraction = _one_extraction(_load(args.input)) ontology = semantic_candidate_builder.load_ontology(root) output = build_assertion_request(extraction, ontology) elif args.phase == "verdict-request": blocking: Iterable[Mapping[str, Any]] = () if args.explicit_blocking is not None: value = _load(args.explicit_blocking) if not isinstance(value, list): raise SemanticAuditError("explicit blocking input must be an array") blocking = value output = build_verdict_request(_load(args.input), explicit_blocking=blocking) else: if args.result is None: raise SemanticAuditError("validate requires request and result paths") output = validate_result(root, _load(args.input), _load(args.result), run_root=args.run_root) exit_code = 0 if output.get("status", "PASS") == "PASS" else 1 except ( SemanticAuditError, semantic_candidate_builder.SemanticCandidateError, semantic_surface_extractor.SemanticSurfaceError, proof_manifest.ManifestValidationError, OSError, UnicodeError, json.JSONDecodeError, ) as exc: output = {"schema_version": VALIDATED_RESULT_SCHEMA, "status": "ERROR", "errors": [{"code": "SEMANTIC_AUDIT_ERROR", "message": str(exc)}]} exit_code = 2 json.dump(output, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True) sys.stdout.write("\n") return exit_code if __name__ == "__main__": raise SystemExit(main())