from __future__ import annotations import json from pathlib import Path from typing import Any from jsonschema import Draft202012Validator, FormatChecker from referencing import Registry, Resource from .claim_ladder import LEVEL_ORDER from .consent_gate import comparison_gate, public_exhibit_gate from .temporal_ratchet import validate_temporal_ratchet SCHEMA_FILES = { "creator_trace_capsule": "creator_trace_capsule_schema_v0_1_0.json", "temporal_event": "temporal_event_schema_v0_1_0.json", "evidence_asset_manifest": "evidence_asset_manifest_schema_v0_1_0.json", "consent_receipt": "consent_receipt_schema_v0_1_0.json", "causal_arc_packet": "causal_arc_packet_schema_v0_1_0.json", "cross_account_comparison_node": "cross_account_comparison_node_schema_v0_1_0.json", "metric_receipt": "metric_receipt_schema_v0_2_0.json", "holographic_comparison": "holographic_comparison_schema_v0_2_0.json", } def _load_schema(schema_dir: Path, filename: str) -> dict[str, Any]: return json.loads((schema_dir / filename).read_text(encoding="utf-8")) def build_registry(schema_dir: str | Path) -> tuple[Registry, dict[str, dict[str, Any]]]: root = Path(schema_dir) schemas = {key: _load_schema(root, filename) for key, filename in SCHEMA_FILES.items()} registry = Registry() # Register each schema under its literal filename, declared $id, and the # absolute URI produced when the creator capsule resolves local references. for key, filename in SCHEMA_FILES.items(): schema = schemas[key] resource = Resource.from_contents(schema) uris = { filename, schema.get("$id", filename), f"https://primordial.example/schema/{filename}", } for uri in uris: registry = registry.with_resource(uri, resource) return registry, schemas def schema_errors(instance: Any, schema_name: str, schema_dir: str | Path) -> list[str]: registry, schemas = build_registry(schema_dir) validator = Draft202012Validator(schemas[schema_name], registry=registry, format_checker=FormatChecker()) errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path)) rendered: list[str] = [] for error in errors: path = "/".join(str(p) for p in error.absolute_path) or "" rendered.append(f"{path}: {error.message}") return rendered def semantic_capsule_errors(capsule: dict[str, Any]) -> list[str]: errors: list[str] = [] events = capsule.get("temporal_events", []) errors.extend(validate_temporal_ratchet(events)) content_topology = capsule.get("content_topology", {}) if content_topology.get("system_inferred_labels") not in ([], None): errors.append("system_inferred_labels must remain empty.") asset_ids = {a.get("asset_id") for a in capsule.get("evidence_assets", [])} event_ids = {e.get("event_id") for e in events} for asset in capsule.get("evidence_assets", []): if asset.get("execution_blocked") is not True: errors.append(f"{asset.get('asset_id')}: execution_blocked must be true.") for linked in asset.get("linked_event_ids", []) or []: if linked not in event_ids: errors.append(f"{asset.get('asset_id')}: linked event {linked!r} does not exist.") for claim in capsule.get("claims", []): rank = LEVEL_ORDER.get(claim.get("claim_level", ""), 99) if rank >= 5 and claim.get("state") != "BLOCKED": errors.append(f"{claim.get('claim_id')}: L5/L6 claims must be BLOCKED in this prototype.") if rank >= 2 and claim.get("human_review_required") is not True: errors.append(f"{claim.get('claim_id')}: L2-L6 claims require human_review_required=true.") for basis in claim.get("basis", []) or []: if basis.startswith("ASSET_") and basis not in asset_ids: errors.append(f"{claim.get('claim_id')}: claim basis {basis!r} is not a known asset.") review_pack = capsule.get("review_pack", {}) blockers = review_pack.get("closure_blockers", []) or [] if capsule.get("loop_state") == "CLOSED_FOR_CURRENT_SCOPE" and blockers: errors.append("False closure: CLOSED_FOR_CURRENT_SCOPE cannot coexist with closure blockers.") consent = capsule.get("consent", {}) if consent.get("publication_requires_additional_review") is not True: errors.append("publication_requires_additional_review must be true.") return errors def validate_capsule(capsule: dict[str, Any], schema_dir: str | Path) -> list[str]: return schema_errors(capsule, "creator_trace_capsule", schema_dir) + semantic_capsule_errors(capsule) def validate_causal_arc(packet: dict[str, Any], schema_dir: str | Path) -> list[str]: errors = schema_errors(packet, "causal_arc_packet", schema_dir) if packet.get("human_review_required") is not True: errors.append("human_review_required must be true.") return errors def gate_receipt(consent_state: str, scope: str) -> dict[str, str | bool]: comparison_ok, comparison_reason = comparison_gate(consent_state, scope, human_review_approved=False) public_ok, public_reason = public_exhibit_gate(consent_state, scope, publication_review_approved=False) return { "comparison_allowed_now": comparison_ok, "comparison_state": comparison_reason, "public_exhibit_allowed_now": public_ok, "public_exhibit_state": public_reason, }