"""Pure, dependency-free contracts for DiffusionGemma visual grounding. This module deliberately has no ComfyUI, Transformers, Torch, or network imports. It owns the host-verifiable parts of the Grounding Guard: settings, asset IDs, strict ledger validation, refusal-clause detection, guard decisions, compact reports, and JSON-only trace persistence. """ from __future__ import annotations import copy import json import math import re import uuid from dataclasses import asdict, dataclass, field from decimal import Decimal, DecimalException from datetime import datetime, timezone from pathlib import Path, PurePath from typing import Any, Mapping, Sequence GROUNDING_CONFIG_TYPE = "DG_GROUNDING_GUARD_CONFIG" GROUNDING_LEDGER_SCHEMA_ID = "dg-grounding-ledger/1" ASSET_REGISTRY_SCHEMA_ID = "dg-asset-registry/1" GROUNDING_REPORT_SCHEMA_ID = "dg-grounding-report/1" EXTERNAL_EVIDENCE_SCHEMA_ID = "dg-external-evidence/1" GROUNDING_EVIDENCE_SCHEMA_PATH = ( Path(__file__).resolve().parent / "schemas" / "grounding_evidence.schema.json" ) EXTERNAL_EVIDENCE_SCHEMA_PATH = ( Path(__file__).resolve().parent / "schemas" / "external_evidence.schema.json" ) GROUNDING_MODES = ("off", "audit", "strict") SAMPLING_PROFILES = ("checkpoint_defaults", "full_48_diagnostic") EVIDENCE_TOKEN_BUDGETS = ("auto", "768", "1024", "1280") ANALYSIS_STATUSES = ( "not_run", "not_applicable", "grounded", "uncertain", "refused", "transport_error", ) LEDGER_ANALYSIS_STATUSES = ("grounded", "uncertain", "refused", "transport_error") GUARD_DECISIONS = ("disabled", "not_applicable", "pass", "warn", "block") CONFIDENCE_LEVELS = ("high", "medium", "low") FACT_CATEGORIES = ( "identity", "appearance", "object", "count", "color", "text", "spatial", "environment", "lighting", "action", "motion", "camera", "composition", "temporal", "other", ) _FACT_CATEGORY_ALIASES = { # The model sometimes describes visible clothing with this intuitive label. # Keep the public ledger taxonomy canonical and normalize only this exact, # unambiguous alias at the model-output validation boundary. "wardrobe": "appearance", } KNOWN_EXTERNAL_PROVIDERS = frozenset( {"manual", "ocr", "detector", "pose", "tracking", "motion", "siglip"} ) DEFAULT_TRACE_SUBFOLDER = "diffusiongemma_grounding" MAX_COMPACT_REPORT_BYTES = 256 * 1024 MAX_TRACE_BYTES = 8 * 1024 * 1024 UINT64_MAX = (1 << 64) - 1 def _canonicalize_fact_categories( categories_value: Any, ) -> tuple[list[str] | None, tuple[tuple[str, str], ...]]: """Return canonical model-authored categories without accepting unknown values.""" if not isinstance(categories_value, list) or not categories_value: return None, () if any(not isinstance(category, str) for category in categories_value): return None, () # Preserve the existing strict rule for duplicate model output. A collision # introduced only by an approved alias (for example appearance + wardrobe) # is safely collapsed below. if len(set(categories_value)) != len(categories_value): return None, () canonical: list[str] = [] canonicalizations: list[tuple[str, str]] = [] seen: set[str] = set() for category in categories_value: normalized = _FACT_CATEGORY_ALIASES.get(category, category) if normalized not in FACT_CATEGORIES: return None, () if normalized != category: canonicalizations.append((category, normalized)) if normalized not in seen: canonical.append(normalized) seen.add(normalized) return canonical, tuple(canonicalizations) SAMPLING_PROFILE_SETTINGS: dict[str, dict[str, Any]] = { "checkpoint_defaults": { "max_denoising_steps": 48, "temperature_start": 0.8, "temperature_end": 0.4, "entropy_bound": 0.1, "token_stability_threshold": 1, "confidence_threshold": 0.005, "adaptive_stopping": True, }, "full_48_diagnostic": { "max_denoising_steps": 48, "temperature_start": 0.8, "temperature_end": 0.4, "entropy_bound": 0.1, "token_stability_threshold": 1, "confidence_threshold": 0.005, "adaptive_stopping": False, }, } _FACT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$") _CLAIM_TYPE_RE = re.compile(r"^[a-z][a-z0-9_.:-]{0,63}$") _ASSET_ID_RE = re.compile(r"^(?:image|picture|video):[1-9][0-9]*$") _MANIFEST_LINE_RE = re.compile( r"<(?PPicture|Video)\s+(?P[1-9][0-9]*)>\s*:\s*(?P[^\r\n]+)", re.IGNORECASE, ) _ROLE_SHARED_FACT_CATEGORIES = { "object", "count", "color", "text", "spatial", "other", } def _expand_role_category_family(categories: set[str]) -> set[str]: """Allow tightly coupled wording without weakening cross-role boundaries.""" expanded = set(categories) for family in ( {"identity", "appearance"}, {"environment", "lighting", "composition"}, {"action", "motion", "temporal"}, {"camera", "composition", "motion", "temporal"}, ): if categories & family: expanded.update(family) return expanded _ROLE_CATEGORY_GROUPS: tuple[tuple[re.Pattern[str], set[str]], ...] = ( ( re.compile( r"\b(?:identity|appearance|face|body|silhouette|wardrobe|accessor(?:y|ies)|" r"distinguishing|subject|character|protagonist|likeness|outfit|costume|attire|" r"garment)\b", re.IGNORECASE, ), {"identity", "appearance"}, ), ( re.compile( r"\b(?:environment|setting|background|location|production\s+design|style|palette|" r"lighting|scenery|architecture|landscape)\b", re.IGNORECASE, ), {"environment", "lighting", "composition"}, ), ( re.compile( r"\b(?:action|motion|pose|movement|timing|choreograph\w*|performance|gesture|" r"blocking|temporal|whole[-\s]?video\s+edit\w*|edit\w*|continuation|" r"continue\w*|cut|cuts|rhythm)\b", re.IGNORECASE, ), {"action", "motion", "composition", "temporal"}, ), ( re.compile(r"\b(?:camera|framing|shot|composition|lens|angle)\b", re.IGNORECASE), {"camera", "composition", "motion", "temporal"}, ), ( re.compile(r"\b(?:object|product|item|prop|logo|packaging)\b", re.IGNORECASE), {"object"}, ), ( re.compile(r"\b(?:count|number|quantity|how\s+many)\b", re.IGNORECASE), {"count"}, ), ( re.compile(r"\b(?:color|colour|hue|palette)\b", re.IGNORECASE), {"color"}, ), ( re.compile( r"\b(?:text|title|copy|lettering|logo|typography|label|caption|wording)\b", re.IGNORECASE, ), {"text"}, ), ( re.compile( r"\b(?:spatial|layout|position|placement|arrangement|left[-\s]?right|" r"top[-\s]?bottom|geometry)\b", re.IGNORECASE, ), {"spatial"}, ), ) _ROLE_FORBIDS_IDENTITY_RE = re.compile( r"(?:" r"\b(?:exclude|ignore|omit)\b[^.;\r\n]{0,48}\bidentity\b" r"|" r"\b(?:do\s+not|don't|must\s+not|should\s+not|never)\b" r"[^.;\r\n]{0,32}\b(?:copy|use|derive|take|transfer|source)\w*\b" r"[^.;\r\n]{0,48}\bidentity\b" r"|" r"\b(?:without|no)\b[^.;\r\n]{0,32}\bidentity\b" r"[^.;\r\n]{0,32}\b(?:transfer|source|copy|override|replacement)\b" r"|" r"\bidentity\b[^.;\r\n]{0,96}" r"(?:\b(?:must\s+not|should\s+not|does\s+not|is\s+not|cannot|can't|won't|" r"mustn't|shouldn't)\b[^.;\r\n]{0,32}\b(?:transfer|override|replace|copy|" r"source)\w*\b|\bnot\s+a\s+source\b)" r")", re.IGNORECASE, ) _EXPLICIT_ROLE_CATEGORIES_RE = re.compile( r"\[\s*dg\s*:\s*([a-z0-9_,\s-]+)\]", re.IGNORECASE, ) _CLAIM_CATEGORY_GROUPS: tuple[tuple[re.Pattern[str], set[str]], ...] = ( ( re.compile( r"\b(?:identity|appearance|face|facial|body\s+type|hair|wardrobe|clothing|" r"beard(?:ed)?|distinguishing\s+trait|same\s+person|likeness)\b", re.IGNORECASE, ), {"identity", "appearance"}, ), ( re.compile( r"\b(?:environment|setting|background|location|town[-\s]?square|production\s+design|" r"style|palette|lighting|scenery|architecture|landscape|plaza|cobblestone|" r"arches?|room|street|forest|building|interior|exterior)\b", re.IGNORECASE, ), {"environment", "lighting", "composition"}, ), ( re.compile( r"\b(?:action|motion|moves?|moving|walk\w*|run\w*|movement|timing|" r"choreograph\w*|performance|gesture|blocking|temporal|turn\w*|jump\w*|" r"raise\w*|lower\w*|enter\w*|exit\w*|fall\w*|dance\w*|" r"reach\w*|grab\w*|edit\w*|continuation|continue\w*|cuts?|rhythm)\b", re.IGNORECASE, ), {"action", "motion", "temporal"}, ), ( re.compile( r"\b(?:camera|framing|shot|lens|pan\w*|zoom\w*|dolly|tilt\w*|orbit\w*|composition)\b", re.IGNORECASE, ), {"camera", "composition", "motion"}, ), ( re.compile(r"\b(?:object|product|item|prop|logo|packaging)\b", re.IGNORECASE), {"object"}, ), ( re.compile( r"\b(?:count|number|quantity|one|two|three|four|five|six|seven|eight|nine|ten)\b|\b\d+\b", re.IGNORECASE, ), {"count"}, ), ( re.compile( r"\b(?:color|colour|hue|red|orange|yellow|green|blue|purple|violet|pink|" r"brown|black|white|gr[ae]y|gold|silver|scarlet|crimson|teal|cyan|magenta|ochre)\b", re.IGNORECASE, ), {"color"}, ), ( re.compile( r"\b(?:text|title|word|words|lettering|logo|typography|reads?|says?|spells?|" r"label|caption|wording)\b", re.IGNORECASE, ), {"text"}, ), ( re.compile( r"\b(?:spatial|layout|left|right|above|below|behind|front|center(?:ed)?|centred|" r"adjacent|next\s+to|position|placement|arrangement)\b", re.IGNORECASE, ), {"spatial"}, ), ) _CONCRETE_APPAREL_PATTERN = ( r"(?:coats?|jackets?|shirts?|dress(?:es)?(?!\s+(?:rehearsal|code)\b)|" r"outfits?|costumes?|attire|garments?|" r"accessor(?:y|ies))(?!-)" ) _PERSON_ROLE_PATTERN = ( r"(?:persons?|subjects?|characters?|protagonists?|actors?|performers?|couriers?|" r"models?|individuals?|m(?:a|e)n|wom(?:a|e)n|girls?|boys?|hero(?:es)?|villains?|" r"people)" ) _PROPER_NAME_PATTERN = r"(?-i:[A-Z][a-z]{1,31})" _PERSON_REFERENCE_PATTERN = ( rf"(?:{_PERSON_ROLE_PATTERN}|he|she|they|{_PROPER_NAME_PATTERN})" ) _CONCRETE_APPAREL_NOUN_RE = re.compile( rf"\b{_CONCRETE_APPAREL_PATTERN}\b", re.IGNORECASE, ) _APPAREL_NON_MODIFIER_WORDS = ( r"a|an|the|this|that|these|those|his|her|their|its|in|on|at|by|for|from|of|" r"to|with|without|within|into|onto|over|under|around|near|beside|behind|before|" r"after|across|through|past|as|while|and|or|but|which|who|whose|is|are|was|" r"were|be|been|being|has|have|had|do|does|did|will|would|can|could|should|" r"must|may|might|see|sees|show|shows|showing|hang|hangs|hanging|display|" r"displays|displaying|provide|provides|provided|supply|supplies|supplied|use|" r"uses|used|take|takes|took|define|defines|control|controls|determine|determines|" r"stand|stands|standing|turn|turns|walk|walks|flutter|flutters|remain|remains|" r"appear|appears|include|includes|contain|contains" ) _APPAREL_MODIFIER_TOKEN_PATTERN = ( rf"(?!(?:{_APPAREL_NON_MODIFIER_WORDS})\b)[a-z0-9]+(?:-[a-z0-9]+)*" ) _APPAREL_MODIFIER_SEQUENCE_PATTERN = ( rf"(?:{_APPAREL_MODIFIER_TOKEN_PATTERN}(?:\s+|,\s*)){{0,8}}" ) _APPAREL_PHRASE_PATTERN = ( rf"(?:(?:the|an?|this|that|these|those|his|her|their|its)\s+)?" rf"{_APPAREL_MODIFIER_SEQUENCE_PATTERN}{_CONCRETE_APPAREL_PATTERN}\b" ) _APPAREL_WEARER_CUE_RE = re.compile( rf"(?:\b{_PERSON_REFERENCE_PATTERN}\b\s*,?\s+" rf"(?:(?:is|are|was|were|has\s+been|had\s+been|will\s+be)\s+)?" rf"(?:[a-z]+ly\s+){{0,2}}" rf"(?:wear|wears|wearing|wore|dressed\s+in|clad\s+in|sporting|dons|donning)\s+" rf"{_APPAREL_PHRASE_PATTERN}" rf"|\b{_PERSON_REFERENCE_PATTERN}\b\s*,?\s+in\s+{_APPAREL_PHRASE_PATTERN}" rf"|\b(?:wearing|dressed\s+in|clad\s+in|donning)\s+{_APPAREL_PHRASE_PATTERN}" rf"|\b(?:wear|wears|wore|dons)\s+{_APPAREL_PHRASE_PATTERN}" rf"|\bsporting\s+(?:the|an?)\s+" rf"{_APPAREL_MODIFIER_SEQUENCE_PATTERN}{_CONCRETE_APPAREL_PATTERN}\b" rf"|{_APPAREL_PHRASE_PATTERN}\s+" rf"(?:(?:is|are|was|were)\s+)?worn\s+by\s+" rf"(?:(?:the|an?)\s+)?{_PERSON_REFERENCE_PATTERN}\b" rf"|{_APPAREL_PHRASE_PATTERN}\s+on\s+" rf"(?:(?:the|an?)\s+)?{_PERSON_REFERENCE_PATTERN}\b)", re.IGNORECASE, ) _PERSON_APPAREL_CUE_RE = re.compile( rf"(?:\b(?:his|her|their)\s+{_APPAREL_MODIFIER_SEQUENCE_PATTERN}" rf"{_CONCRETE_APPAREL_PATTERN}\b" rf"|\b{_PERSON_ROLE_PATTERN}['\u2019](?:s)?\s+" rf"{_APPAREL_MODIFIER_SEQUENCE_PATTERN}{_CONCRETE_APPAREL_PATTERN}\b)", re.IGNORECASE, ) _PROPER_NAME_POSSESSIVE_APPAREL_RE = re.compile( rf"\b{_PROPER_NAME_PATTERN}['\u2019]s\s+" rf"{_APPAREL_MODIFIER_SEQUENCE_PATTERN}{_CONCRETE_APPAREL_PATTERN}\b" ) _APPAREL_COMPOUND_WEARER_RE = re.compile( rf"(?:\b(?:[a-z0-9]+-)+(?:coated|jacketed|shirted|dressed|costumed)\s+" rf"{_PERSON_ROLE_PATTERN}\b" rf"|\b(?:coat|jacket|shirt|dress|outfit|costume|garment)-wearing\s+" rf"{_PERSON_ROLE_PATTERN}\b)", re.IGNORECASE, ) _APPAREL_TRANSFER_CUE_RE = re.compile( r"(?:\b(?:cop(?:y|ies|ied|ying)|deriv(?:e|es|ed|ing)|" r"transfer(?:s|red|ring)?|sourc(?:e|es|ed|ing)|preserv(?:e|es|ed|ing)|" r"suppl(?:y|ies|ied|ying)|provid(?:e|es|ed|ing))\b\s+" rf"(?:directly\s+)?{_APPAREL_PHRASE_PATTERN}" rf"|\b(?:uses?|takes?)\s+{_APPAREL_PHRASE_PATTERN}" rf"|{_APPAREL_PHRASE_PATTERN}\s*(?:,|\u2014)?\s*" r"(?:(?:that\s+)?(?:(?:is|are|was|were)|(?:must|should|will|can)\s+be|" r"(?:has|have|had)\s+been)\s+)?(?:directly\s+)?" r"(?:copied|derived|transferred|sourced|preserved|supplied|provided)\b)", re.IGNORECASE, ) _REFUSAL_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( ( "apology_refusal", re.compile( r"\bi(?:\s+am|['\u2019]m)\s+sorry\s*,?\s+but\s+i\s+(?:cannot|can['\u2019]t|won['\u2019]t)\b", re.IGNORECASE, ), ), ( "cannot_assist", re.compile( r"\bi\s+(?:cannot|can['\u2019]t|won['\u2019]t)\s+(?:" r"(?:assist|help|comply|provide|analy[sz]e|describe|process)\b|" r"(?:review|view)\s+(?:this|that|the)\s+(?:image|video|clip|media|content)\b)", re.IGNORECASE, ), ), ( "unable_to_assist", re.compile( r"\bi(?:\s+am|['\u2019]m)\s+unable\s+to\s+(?:" r"(?:assist|help|comply|provide|analy[sz]e|describe|process)\b|" r"(?:review|view)\s+(?:this|that|the)\s+(?:image|video|clip|media|content)\b)", re.IGNORECASE, ), ), ( "policy_refusal", re.compile( r"\b(?:this|that|your)\s+(?:request|content)\s+(?:violates|is\s+against|conflicts\s+with)\s+(?:my\s+)?(?:policy|policies|guidelines)\b", re.IGNORECASE, ), ), ( "must_decline", re.compile( r"\bi\s+(?:must|have\s+to)\s+(?:decline|refuse)\s+(?:this|that|the)\s+request\b", re.IGNORECASE, ), ), ( "apology_cannot", re.compile( r"\bi\s+apologi[sz]e\s*,?\s+but\s+i\s+(?:cannot|can['\u2019]t|won['\u2019]t)\b", re.IGNORECASE, ), ), ) _FORBIDDEN_TRACE_KEYS = frozenset( { "logits", "raw_logits", "logits_tensor", "pixel_values", "pixels", "image_tensor", "video_tensor", "video_frames", "raw_media", "image_bytes", "video_bytes", "media_bytes", } ) def _is_safe_pixel_tensor_descriptor(path: str, key: str, value: Any) -> bool: """Allow processor transport facts while continuing to reject pixel payloads.""" if not path.endswith(".pixel_tensors") or not key.startswith("pixel_values"): return False if not isinstance(value, Mapping) or set(value) != {"shape", "dtype"}: return False shape = value.get("shape") dtype = value.get("dtype") return bool( isinstance(shape, (list, tuple)) and shape and len(shape) <= 8 and all(isinstance(item, int) and not isinstance(item, bool) and item >= 0 for item in shape) and all(item <= 2_147_483_647 for item in shape) and isinstance(dtype, str) and 0 < len(dtype) <= 128 ) class GroundingGuardError(ValueError): """Base error for malformed Grounding Guard data.""" class GroundingLedgerError(GroundingGuardError): """Raised when a ledger cannot be extracted as strict JSON.""" @dataclass(frozen=True) class GroundingGuardConfig: mode: str = "audit" retry_on_uncertain: bool = True sampling_profile: str = "checkpoint_defaults" seed: int = 0 save_detailed_trace: bool = False trace_subfolder: str = DEFAULT_TRACE_SUBFOLDER external_evidence_json: str = "" evidence_token_budget: str = "auto" @property def sampling_settings(self) -> dict[str, Any]: return copy.deepcopy(SAMPLING_PROFILE_SETTINGS[self.sampling_profile]) def to_dict(self) -> dict[str, Any]: result = asdict(self) result["sampling_settings"] = self.sampling_settings return result @dataclass(frozen=True) class GroundingLedgerValidation: ledger: dict[str, Any] | None schema_valid: bool grounded: bool analysis_status: str model_analysis_status: str = "uncertain" errors: tuple[str, ...] = () warnings: tuple[str, ...] = () blocking_reasons: tuple[str, ...] = () covered_asset_ids: tuple[str, ...] = () temporal_regions: dict[str, tuple[str, ...]] = field(default_factory=dict) accepted_external_claim_count: int = 0 external_conflicts: tuple[str, ...] = () refusal_matches: tuple[str, ...] = () @property def valid(self) -> bool: """Compatibility alias for callers that treat verified grounding as valid.""" return self.grounded def to_dict(self) -> dict[str, Any]: return { "schema_valid": self.schema_valid, "grounded": self.grounded, "analysis_status": self.analysis_status, "model_analysis_status": self.model_analysis_status, "errors": list(self.errors), "warnings": list(self.warnings), "blocking_reasons": list(self.blocking_reasons), "covered_asset_ids": list(self.covered_asset_ids), "temporal_regions": { key: list(value) for key, value in self.temporal_regions.items() }, "accepted_external_claim_count": self.accepted_external_claim_count, "external_conflicts": list(self.external_conflicts), "refusal_matches": list(self.refusal_matches), } @dataclass(frozen=True) class GroundingGuardDecision: mode: str analysis_status: str decision: str would_block: bool blocked_reasons: tuple[str, ...] = () warnings: tuple[str, ...] = () def to_dict(self) -> dict[str, Any]: return { "mode": self.mode, "analysis_status": self.analysis_status, "decision": self.decision, "would_block": self.would_block, "blocked_reasons": list(self.blocked_reasons), "warnings": list(self.warnings), } def _as_mapping(value: Any) -> dict[str, Any]: if isinstance(value, GroundingGuardConfig): return asdict(value) if isinstance(value, Mapping): return dict(value) if isinstance(value, str) and value.strip(): try: parsed = json.loads(value) except json.JSONDecodeError: return {} return dict(parsed) if isinstance(parsed, Mapping) else {} return {} def _as_bool(value: Any, default: bool) -> bool: if isinstance(value, bool): return value if isinstance(value, (int, float)) and not isinstance(value, bool): return bool(value) if isinstance(value, str): lowered = value.strip().lower() if lowered in {"1", "true", "yes", "on"}: return True if lowered in {"0", "false", "no", "off"}: return False return default def _safe_trace_subfolder(value: Any) -> str: text = str(value or "").strip().replace("\\", "/") if not text: return DEFAULT_TRACE_SUBFOLDER pure = PurePath(text) if pure.is_absolute() or any(part in {"", ".", ".."} for part in pure.parts): return DEFAULT_TRACE_SUBFOLDER if re.match(r"^[A-Za-z]:", text): return DEFAULT_TRACE_SUBFOLDER return "/".join(pure.parts) def normalize_grounding_guard_config( value: GroundingGuardConfig | Mapping[str, Any] | str | None = None, **overrides: Any, ) -> GroundingGuardConfig: """Return a safe, canonical settings value. Invalid externally supplied modes fail toward ``audit`` rather than silently disabling the guard. Seeds are clamped to ComfyUI's unsigned 64-bit range. """ raw = _as_mapping(value) raw.update({key: item for key, item in overrides.items() if item is not None}) mode = str(raw.get("mode", "audit")).strip().lower() if mode not in GROUNDING_MODES: mode = "audit" profile = str(raw.get("sampling_profile", "checkpoint_defaults")).strip().lower() if profile not in SAMPLING_PROFILES: profile = "checkpoint_defaults" evidence_token_budget = str(raw.get("evidence_token_budget", "auto")).strip().lower() if evidence_token_budget not in EVIDENCE_TOKEN_BUDGETS: evidence_token_budget = "auto" try: seed = int(raw.get("seed", 0)) except (TypeError, ValueError, OverflowError): seed = 0 seed = max(0, min(UINT64_MAX, seed)) external = raw.get("external_evidence_json", "") if isinstance(external, (Mapping, list, tuple)): external_text = json.dumps(external, separators=(",", ":"), ensure_ascii=False) else: external_text = str(external or "").strip() return GroundingGuardConfig( mode=mode, retry_on_uncertain=_as_bool(raw.get("retry_on_uncertain"), True), sampling_profile=profile, seed=seed, save_detailed_trace=_as_bool(raw.get("save_detailed_trace"), False), trace_subfolder=_safe_trace_subfolder(raw.get("trace_subfolder")), external_evidence_json=external_text, evidence_token_budget=evidence_token_budget, ) def load_grounding_evidence_schema() -> dict[str, Any]: """Load the bundled strict ledger schema used in model instructions.""" value = json.loads(GROUNDING_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8")) schema_identity = value.get("properties", {}).get("schema", {}).get("const") if isinstance(value, dict) else None if schema_identity != GROUNDING_LEDGER_SCHEMA_ID: raise GroundingGuardError("bundled grounding evidence schema has an unexpected identity") return value def load_external_evidence_schema() -> dict[str, Any]: """Load the optional typed external-evidence envelope schema.""" value = json.loads(EXTERNAL_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8")) schema_identity = ( value.get("properties", {}).get("schema", {}).get("const") if isinstance(value, dict) else None ) if schema_identity != EXTERNAL_EVIDENCE_SCHEMA_ID: raise GroundingGuardError("bundled external evidence schema has an unexpected identity") return value def _safe_int(value: Any, default: int = 0) -> int: try: if isinstance(value, bool): return default return int(value) except (TypeError, ValueError, OverflowError): return default def _safe_float(value: Any, default: float = 0.0) -> float: try: result = float(value) except (TypeError, ValueError, OverflowError): return default return result if math.isfinite(result) else default def _manifest_roles(media_metadata: Mapping[str, Any]) -> dict[str, str]: roles: dict[str, str] = {} manifest = str(media_metadata.get("minimax_h3_reference_manifest", "") or "") for match in _MANIFEST_LINE_RE.finditer(manifest): prefix = "picture" if match.group("kind").lower() == "picture" else "video" roles[f"{prefix}:{int(match.group('number'))}"] = match.group("role").strip() return roles def _explicit_h3_role_categories(role: Any) -> set[str] | None: text = str(role or "").strip() match = _EXPLICIT_ROLE_CATEGORIES_RE.search(text) if not match: return None values = { item.strip().lower().replace("-", "_") for item in match.group(1).split(",") if item.strip() } if not values or "other" in values or not values.issubset(FACT_CATEGORIES): return set() return values def _strip_h3_role_annotation(role: Any) -> str: return re.sub(r"\s*\[\s*dg\s*:[^\]]*\]\s*", " ", str(role or "")).strip() def _h3_role_category_contract(role: Any) -> tuple[set[str], set[str]]: """Translate an H3 manifest role into a host-owned category contract. ``[dg:identity,appearance]`` annotations are authoritative. Untagged manifests retain conservative vocabulary inference for compatibility. """ text = str(role or "").strip() if not text: return set(), set() explicit = _explicit_h3_role_categories(text) if explicit is not None: return ( (_ROLE_SHARED_FACT_CATEGORIES | explicit, explicit) if explicit else (set(), set()) ) allowed = set(_ROLE_SHARED_FACT_CATEGORIES) role_specific: set[str] = set() matched = False forbids_identity = bool(_ROLE_FORBIDS_IDENTITY_RE.search(text)) for index, (pattern, categories) in enumerate(_ROLE_CATEGORY_GROUPS): if index == 0 and forbids_identity: continue if pattern.search(text): allowed.update(categories) role_specific.update(categories) matched = True return (allowed, role_specific) if matched else (set(), set()) def _claim_role_categories(claim: Any) -> set[str]: categories: set[str] = set() text = str(claim or "") for pattern, matched_categories in _CLAIM_CATEGORY_GROUPS: if pattern.search(text): categories.update(matched_categories) if _APPAREL_COMPOUND_WEARER_RE.search(text) or ( _CONCRETE_APPAREL_NOUN_RE.search(text) and ( _APPAREL_WEARER_CUE_RE.search(text) or _PERSON_APPAREL_CUE_RE.search(text) or _PROPER_NAME_POSSESSIVE_APPAREL_RE.search(text) or _APPAREL_TRANSFER_CUE_RE.search(text) ) ): categories.update({"identity", "appearance"}) return categories def _temporal_region(ordinal: int, sample_count: int) -> str: if sample_count <= 1: return "opening" if ordinal == 1: return "opening" if ordinal == sample_count: return "closing" position = (ordinal - 1) / max(1, sample_count - 1) if position < 1.0 / 3.0: return "opening" if position > 2.0 / 3.0: return "closing" return "middle" def _video_samples( positions: Sequence[int], media_metadata: Mapping[str, Any] ) -> list[dict[str, Any]]: indices_value = media_metadata.get("sampled_indices") indices = list(indices_value) if isinstance(indices_value, (list, tuple)) else [] explicit_timecodes_value = media_metadata.get("sampled_timecodes_seconds") explicit_timecodes = ( list(explicit_timecodes_value) if isinstance(explicit_timecodes_value, (list, tuple)) else [] ) source_fps = _safe_float(media_metadata.get("source_fps"), 0.0) sample_fps = max(0.001, _safe_float(media_metadata.get("sample_fps"), 1.0)) duration = max(0.0, _safe_float(media_metadata.get("duration_seconds"), 0.0)) sample_count = len(positions) samples: list[dict[str, Any]] = [] for offset, tensor_position in enumerate(positions): source_index = max(0, _safe_int(indices[offset], offset)) if offset < len(indices) else offset if offset < len(explicit_timecodes): timecode = max(0.0, _safe_float(explicit_timecodes[offset], 0.0)) elif source_fps > 0: timecode = source_index / source_fps elif duration > 0 and sample_count > 1: timecode = duration * offset / (sample_count - 1) else: timecode = offset / sample_fps ordinal = offset + 1 samples.append( { "sample_ordinal": ordinal, "tensor_batch_position": int(tensor_position), "source_frame_index": source_index, "timecode_seconds": round(timecode, 6), "temporal_region": _temporal_region(ordinal, sample_count), } ) return samples def _still_asset( asset_id: str, role: str, tensor_position: int, ) -> dict[str, Any]: return { "asset_id": asset_id, "kind": "image", "role": role or "visual_reference", "required_for_grounding": True, "tensor_batch_positions": [int(tensor_position)], "samples": [ { "sample_ordinal": 1, "tensor_batch_position": int(tensor_position), "source_frame_index": 0, "timecode_seconds": 0.0, "temporal_region": "opening", } ], } def build_asset_registry( media_metadata: Mapping[str, Any] | None, image_count: int, ) -> dict[str, Any]: """Build stable host-owned IDs for the batched visual inputs. ``image_count`` is the number of image tensors that will be handed to the processor. A sampled video therefore contributes one image per sampled frame. The function never trusts model-produced IDs. """ metadata = dict(media_metadata or {}) count = max(0, _safe_int(image_count, 0)) source = str(metadata.get("source", "none") or "none").strip().lower() synthesis_mode = str( metadata.get("media_synthesis_mode", "video_recreation") or "video_recreation" ).strip().lower() h3_ref = str(metadata.get("minimax_h3_mode", "") or "").strip().lower() == "ref2va" roles = _manifest_roles(metadata) warnings: list[str] = [] assets: list[dict[str, Any]] = [] if count == 0: declared_visual_asset_ids = sorted(roles) return { "schema": ASSET_REGISTRY_SCHEMA_ID, "source": source, "media_synthesis_mode": synthesis_mode, "minimax_h3_mode": "ref2va" if h3_ref else "", "expected_image_count": 0, "assigned_image_count": 0, "assets": [], "declared_visual_asset_ids": declared_visual_asset_ids, "unattached_declared_visual_asset_ids": declared_visual_asset_ids, "unexpected_attached_visual_asset_ids": [], "warnings": ( ["declared_visual_assets_are_not_attached"] if declared_visual_asset_ids else [] ), } if h3_ref: reference_count = _safe_int( metadata.get( "minimax_h3_reference_image_batch_count", metadata.get("reference_image_count", 0), ), 0, ) attachment_flag_present = "reference_image_backend_attached" in metadata reference_attached = ( bool(metadata.get("reference_image_backend_attached")) if attachment_flag_present else source in {"image", "image+video"} ) if reference_count and not reference_attached: warnings.append("unattached_reference_image_excluded_from_asset_registry") reference_count = 0 reference_count = max(0, min(count, reference_count)) if reference_count == 0 and source == "image": reference_count = count for index in range(reference_count): asset_id = f"picture:{index + 1}" assets.append( _still_asset( asset_id, roles.get(asset_id, f"H3 reference picture {index + 1}"), index, ) ) remaining = list(range(reference_count, count)) if remaining: video_asset = { "asset_id": "video:1", "kind": "video", "role": roles.get( "video:1", str(metadata.get("video_role", "H3 reference video")), ), "required_for_grounding": True, "tensor_batch_positions": remaining, "samples": _video_samples(remaining, metadata), } assets.append(video_asset) declared_picture_count = sum(key.startswith("picture:") for key in roles) if declared_picture_count and declared_picture_count != reference_count: warnings.append( "declared_picture_count_does_not_match_attached_reference_count" ) elif source in {"video", "image+video"}: attachment_flag_present = "reference_image_backend_attached" in metadata reference_attached = ( bool(metadata.get("reference_image_backend_attached")) if attachment_flag_present else source == "image+video" ) reference_count = ( max(0, _safe_int(metadata.get("reference_image_count"), 0)) if source == "image+video" and reference_attached else 0 ) if source == "image+video" and reference_attached and reference_count == 0: reference_count = 1 if metadata.get("reference_image_count") and not reference_attached: warnings.append("unattached_reference_image_excluded_from_asset_registry") reference_count = min(count, reference_count) for index in range(reference_count): role = str(metadata.get("image_role", "visual_reference")) assets.append(_still_asset(f"image:{index + 1}", role, index)) remaining = list(range(reference_count, count)) if remaining: assets.append( { "asset_id": "video:1", "kind": "video", "role": str(metadata.get("video_role", "primary_video_source")), "required_for_grounding": True, "tensor_batch_positions": remaining, "samples": _video_samples(remaining, metadata), } ) elif source == "video": warnings.append("video_source_has_no_sampled_frames") else: declared_count = max(0, _safe_int(metadata.get("reference_image_count"), count)) still_count = count if declared_count == 0 else min(count, declared_count) if still_count != count: warnings.append("unassigned_tensor_positions_treated_as_still_images") still_count = count per_image_roles = metadata.get("image_roles") if not isinstance(per_image_roles, list): per_image_roles = [] for index in range(still_count): role = ( str(per_image_roles[index]) if index < len(per_image_roles) and str(per_image_roles[index]).strip() else str(metadata.get("image_role", "primary_image_source")) ) assets.append(_still_asset(f"image:{index + 1}", role, index)) for asset in assets: raw_role = str(asset.get("role", "")) _allowed_categories, role_categories = _h3_role_category_contract( raw_role ) explicit_categories = _explicit_h3_role_categories(raw_role) asset["role"] = _strip_h3_role_annotation(raw_role) asset["grounding_role_categories"] = sorted(role_categories) asset["grounding_role_contract_source"] = ( "explicit" if explicit_categories else "invalid_explicit" if explicit_categories is not None else "inferred" if role_categories else "unrecognized" ) assigned_positions = sorted( position for asset in assets for position in asset.get("tensor_batch_positions", []) ) if assigned_positions != list(range(count)): warnings.append("asset_registry_does_not_cover_tensor_batch") declared_visual_asset_ids = sorted(roles) if h3_ref else [] attached_visual_asset_ids = sorted( str(asset.get("asset_id")) for asset in assets if str(asset.get("asset_id", "")).startswith(("picture:", "video:")) ) unattached_declared = ( sorted(set(declared_visual_asset_ids) - set(attached_visual_asset_ids)) if h3_ref else [] ) unexpected_attached = ( sorted(set(attached_visual_asset_ids) - set(declared_visual_asset_ids)) if h3_ref else [] ) if h3_ref and (unattached_declared or unexpected_attached): warnings.append("declared_visual_assets_do_not_match_attached_registry") return { "schema": ASSET_REGISTRY_SCHEMA_ID, "source": source, "media_synthesis_mode": synthesis_mode, "minimax_h3_mode": "ref2va" if h3_ref else "", "expected_image_count": count, "assigned_image_count": len(assigned_positions), "assets": assets, "declared_visual_asset_ids": declared_visual_asset_ids, "unattached_declared_visual_asset_ids": unattached_declared, "unexpected_attached_visual_asset_ids": unexpected_attached, "warnings": list(dict.fromkeys(warnings)), } class _DuplicateJSONKeyError(ValueError): pass class _NonFiniteJSONNumberError(ValueError): pass class _InvalidJSONNumberError(ValueError): pass def _parse_bounded_json_decimal(value: str) -> Decimal: """Decode a JSON number without Python's huge-int or Decimal edge crashes.""" if len(value) > 256: raise _InvalidJSONNumberError("JSON number exceeds 256 characters") try: number = Decimal(value) if not number.is_finite(): raise _InvalidJSONNumberError("JSON number must be finite") exponent = number.as_tuple().exponent if not isinstance(exponent, int) or abs(exponent) > 4096: raise _InvalidJSONNumberError("JSON number exponent is out of range") return number except _InvalidJSONNumberError: raise except (DecimalException, ValueError) as exc: raise _InvalidJSONNumberError("JSON number is invalid or out of range") from exc def _reject_duplicate_json_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: result: dict[str, Any] = {} for key, value in pairs: if key in result: raise _DuplicateJSONKeyError(f"duplicate JSON object key: {key}") result[key] = value return result def _reject_nonfinite_json_number(value: str) -> None: raise _NonFiniteJSONNumberError(f"non-finite JSON number: {value}") def extract_grounding_ledger( value: str | Mapping[str, Any], *, allow_combined: bool = True, ) -> dict[str, Any]: """Extract a ledger from exact JSON or a combined packet. Markdown fences, leading commentary, trailing data, and JSON salvage are intentionally rejected. Combined Director packets may contain the ledger under the exact ``grounding_ledger`` key. """ if isinstance(value, Mapping): try: parsed: Any = copy.deepcopy(dict(value)) except RecursionError as exc: raise GroundingLedgerError("grounding ledger exceeds the nesting limit") from exc elif isinstance(value, str): text = value.strip() if not text: raise GroundingLedgerError("grounding ledger is empty") decoder = json.JSONDecoder( object_pairs_hook=_reject_duplicate_json_keys, parse_float=_parse_bounded_json_decimal, parse_int=_parse_bounded_json_decimal, parse_constant=_reject_nonfinite_json_number, ) try: parsed, end = decoder.raw_decode(text) except _DuplicateJSONKeyError as exc: raise GroundingLedgerError(f"grounding ledger is not strict JSON: {exc}") from exc except _NonFiniteJSONNumberError as exc: raise GroundingLedgerError(f"grounding ledger is not strict JSON: {exc}") from exc except _InvalidJSONNumberError as exc: raise GroundingLedgerError(f"grounding ledger has an invalid JSON number: {exc}") from exc except RecursionError as exc: raise GroundingLedgerError("grounding ledger exceeds the JSON nesting limit") from exc except json.JSONDecodeError as exc: raise GroundingLedgerError(f"grounding ledger is not strict JSON: {exc.msg}") from exc if text[end:].strip(): raise GroundingLedgerError("grounding ledger contains trailing non-JSON content") else: raise GroundingLedgerError("grounding ledger must be a JSON object or JSON string") if not isinstance(parsed, Mapping): raise GroundingLedgerError("grounding ledger root must be an object") if parsed.get("schema") == GROUNDING_LEDGER_SCHEMA_ID: try: return copy.deepcopy(dict(parsed)) except RecursionError as exc: raise GroundingLedgerError("grounding ledger exceeds the nesting limit") from exc nested = parsed.get("grounding_ledger") if allow_combined and isinstance(nested, Mapping): try: return copy.deepcopy(dict(nested)) except RecursionError as exc: raise GroundingLedgerError("grounding ledger exceeds the nesting limit") from exc if not allow_combined: raise GroundingLedgerError("strict grounding evidence root must be the ledger object") raise GroundingLedgerError("grounding ledger object is missing") def detect_refusal_phrases(text: Any) -> list[str]: """Return complete refusal-clause labels, never isolated token matches.""" haystack = str(text or "") candidates: list[tuple[int, int, str]] = [] for label, pattern in _REFUSAL_PATTERNS: candidates.extend((match.start(), match.end(), label) for match in pattern.finditer(haystack)) selected: list[tuple[int, int, str]] = [] for candidate in sorted(candidates, key=lambda item: (-(item[1] - item[0]), item[0], item[2])): start, end, _label = candidate if any(start < other_end and other_start < end for other_start, other_end, _ in selected): continue selected.append(candidate) return [label for _start, _end, label in sorted(selected)] def _append_error(errors: list[str], blockers: list[str], error: str, blocker: str) -> None: errors.append(error) blockers.append(blocker) def _strict_text_list( value: Any, path: str, errors: list[str], blockers: list[str], ) -> None: if not isinstance(value, list): _append_error(errors, blockers, f"{path}:must_be_array", "schema_invalid") return if len(value) > 256: _append_error(errors, blockers, f"{path}:too_many_items", "schema_invalid") for index, item in enumerate(value): if not isinstance(item, str) or not item.strip() or len(item) > 2048: _append_error( errors, blockers, f"{path}[{index}]:must_be_nonempty_string_at_most_2048_chars", "schema_invalid", ) def _decimal_number(value: Any) -> Decimal | None: if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): return None if isinstance(value, float) and not math.isfinite(value): return None try: number = value if isinstance(value, Decimal) else Decimal(str(value)) return number if number.is_finite() else None except (DecimalException, ValueError): return None def _json_scalar(value: Any) -> bool: return value is None or isinstance(value, (str, bool)) or _decimal_number(value) is not None def _schema_integer( value: Any, minimum: int | None = None, maximum: int = (1 << 63) - 1, ) -> int | None: number = _decimal_number(value) try: if number is None or number != number.to_integral_value(): return None if number > maximum or (minimum is not None and number < minimum): return None return int(number) except (DecimalException, ValueError, OverflowError): return None def _canonical_decimal(value: Decimal) -> str: sign, digits_tuple, exponent = value.as_tuple() digits = list(digits_tuple) if not digits or all(digit == 0 for digit in digits): return "0e0" while len(digits) > 1 and digits[-1] == 0: digits.pop() exponent += 1 coefficient = "".join(str(digit) for digit in digits) return f"{'-' if sign else ''}{coefficient}e{exponent}" def _json_native_number(value: Any) -> int | float | None: number = _decimal_number(value) if number is None: return None try: if number == number.to_integral_value(): if number.adjusted() > 1024: return None return int(number) converted = float(number) except (DecimalException, ValueError, OverflowError): return None if not math.isfinite(converted) or Decimal(str(converted)) != number: return None return converted def _typed_claim_key( claim: Mapping[str, Any], assets: Mapping[str, Mapping[str, Any]] | None = None, ) -> tuple[str, str, int | None]: asset_id = str(claim.get("asset_id", "")) ordinal = claim.get("sample_ordinal") normalized_ordinal = _schema_integer(ordinal, 1) asset = assets.get(asset_id) if assets is not None else None if asset is not None and len(_sample_index(asset)) == 1: normalized_ordinal = None return ( asset_id, str(claim.get("claim_type", "")), normalized_ordinal, ) def _typed_value(value: Any) -> str: if isinstance(value, bool): value_type = "boolean" canonical_value: Any = value elif _decimal_number(value) is not None: value_type = "number" canonical_value = _canonical_decimal(_decimal_number(value)) elif value is None: value_type = "null" canonical_value = None else: value_type = "string" canonical_value = str(value) return json.dumps( {"type": value_type, "value": canonical_value}, sort_keys=True, separators=(",", ":"), ensure_ascii=False, ) def _decimal_to_json_native(value: Any) -> Any: if isinstance(value, Decimal): converted = _json_native_number(value) if converted is None: raise GroundingGuardError( "decimal value cannot be represented losslessly in the validated ledger" ) return converted if isinstance(value, list): return [_decimal_to_json_native(item) for item in value] if isinstance(value, tuple): return [_decimal_to_json_native(item) for item in value] if isinstance(value, Mapping): return { str(key): _decimal_to_json_native(item) for key, item in value.items() } return value def _contains_unrepresentable_decimal(value: Any) -> bool: if not isinstance(value, bool) and isinstance(value, (int, float, Decimal)): return _json_native_number(value) is None if isinstance(value, Mapping): return any(_contains_unrepresentable_decimal(item) for item in value.values()) if isinstance(value, (list, tuple)): return any(_contains_unrepresentable_decimal(item) for item in value) return False def parse_external_evidence( value: str | Mapping[str, Any] | None, ) -> tuple[list[dict[str, Any]], list[str]]: """Parse recognized typed claims; unknown/unstructured inputs stay advisory.""" if value is None or (isinstance(value, str) and not value.strip()): return [], [] if isinstance(value, Mapping): parsed: Any = dict(value) elif isinstance(value, str): try: parsed = json.loads( value, object_pairs_hook=_reject_duplicate_json_keys, parse_float=_parse_bounded_json_decimal, parse_int=_parse_bounded_json_decimal, parse_constant=_reject_nonfinite_json_number, ) except _DuplicateJSONKeyError: return [], ["external_evidence_duplicate_json_key"] except _NonFiniteJSONNumberError: return [], ["external_evidence_nonfinite_json_number"] except _InvalidJSONNumberError: return [], ["external_evidence_json_number_invalid_or_out_of_range"] except RecursionError: return [], ["external_evidence_json_nesting_limit"] except json.JSONDecodeError: return [], ["external_evidence_unstructured_or_invalid_json"] else: return [], ["external_evidence_unstructured"] if not isinstance(parsed, Mapping): return [], ["external_evidence_unstructured"] required_root = {"schema", "provider", "claims"} if set(parsed) != required_root: return [], ["external_evidence_envelope_invalid"] if parsed.get("schema") != EXTERNAL_EVIDENCE_SCHEMA_ID: return [], ["external_evidence_unknown_schema"] provider = parsed.get("provider") if not isinstance(provider, str) or provider not in KNOWN_EXTERNAL_PROVIDERS: return [], ["external_evidence_unknown_provider"] claims = parsed.get("claims") if not isinstance(claims, list) or len(claims) > 512: return [], ["external_evidence_claims_invalid"] valid: list[dict[str, Any]] = [] for index, claim in enumerate(claims): if not isinstance(claim, Mapping): return [], [f"external_evidence_claim_{index}_invalid"] allowed = { "asset_id", "claim_type", "value", "sample_ordinal", "source_frame_index", "timecode_seconds", "confidence", } required = {"asset_id", "claim_type", "value"} if set(claim) - allowed: return [], [f"external_evidence_claim_{index}_has_unknown_fields"] if required - set(claim): return [], [f"external_evidence_claim_{index}_missing_required_fields"] asset_id = claim.get("asset_id") claim_type = claim.get("claim_type") if ( not isinstance(asset_id, str) or not isinstance(claim_type, str) or not _ASSET_ID_RE.fullmatch(asset_id) or not _CLAIM_TYPE_RE.fullmatch(claim_type) ): return [], [f"external_evidence_claim_{index}_invalid_key"] claim_value = claim.get("value") if not _json_scalar(claim_value): return [], [f"external_evidence_claim_{index}_invalid_value"] if _decimal_number(claim_value) is not None and _schema_integer( claim_value, -(1 << 63), ) is None: return [], [f"external_evidence_claim_{index}_numeric_value_not_int64"] if asset_id.startswith("video:") and "sample_ordinal" not in claim: return [], [f"external_evidence_claim_{index}_video_sample_ordinal_required"] ordinal = claim.get("sample_ordinal") normalized_ordinal = _schema_integer(ordinal, 1) if "sample_ordinal" in claim and normalized_ordinal is None: return [], [f"external_evidence_claim_{index}_invalid_sample"] confidence = claim.get("confidence") if "confidence" in claim and confidence not in CONFIDENCE_LEVELS: return [], [f"external_evidence_claim_{index}_invalid_confidence"] frame = claim.get("source_frame_index") if "sample_ordinal" not in claim and ( "source_frame_index" in claim or "timecode_seconds" in claim ): return [], [f"external_evidence_claim_{index}_frame_or_time_requires_sample"] normalized_frame = _schema_integer(frame, 0) if "source_frame_index" in claim and normalized_frame is None: return [], [f"external_evidence_claim_{index}_invalid_frame"] timecode = claim.get("timecode_seconds") if "timecode_seconds" in claim and ( _decimal_number(timecode) is None or _decimal_number(timecode) < 0 ): return [], [f"external_evidence_claim_{index}_invalid_timecode"] normalized_claim = copy.deepcopy(dict(claim)) if "sample_ordinal" in normalized_claim: normalized_claim["sample_ordinal"] = normalized_ordinal if "source_frame_index" in normalized_claim: normalized_claim["source_frame_index"] = normalized_frame valid.append(normalized_claim) return valid, [] def _asset_index(registry: Mapping[str, Any]) -> dict[str, dict[str, Any]]: assets = registry.get("assets") if not isinstance(assets, list): return {} return { str(asset.get("asset_id")): dict(asset) for asset in assets if isinstance(asset, Mapping) and asset.get("asset_id") } def _sample_index(asset: Mapping[str, Any]) -> dict[int, dict[str, Any]]: samples = asset.get("samples") if not isinstance(samples, list): return {} return { int(sample.get("sample_ordinal")): dict(sample) for sample in samples if isinstance(sample, Mapping) and isinstance(sample.get("sample_ordinal"), int) and not isinstance(sample.get("sample_ordinal"), bool) } def _validate_evidence_reference( reference: Any, path: str, assets: Mapping[str, dict[str, Any]], errors: list[str], blockers: list[str], ) -> tuple[str | None, str | None]: if not isinstance(reference, Mapping): _append_error(errors, blockers, f"{path}:must_be_object", "schema_invalid") return None, None allowed = {"asset_id", "sample_ordinal", "source_frame_index", "timecode_seconds"} if set(reference) - allowed: _append_error(errors, blockers, f"{path}:unknown_fields", "schema_invalid") asset_id = reference.get("asset_id") if not isinstance(asset_id, str) or not _ASSET_ID_RE.fullmatch(asset_id): _append_error(errors, blockers, f"{path}.asset_id:invalid", "schema_invalid") return None, None video_missing_fields: set[str] = set() if asset_id.startswith("video:"): required = {"sample_ordinal", "source_frame_index", "timecode_seconds"} video_missing_fields = required - set(reference) if video_missing_fields: _append_error( errors, blockers, f"{path}:video_reference_missing:{','.join(sorted(video_missing_fields))}", "schema_invalid", ) blockers.append("invalid_frame_reference") asset = assets.get(asset_id) if asset is None: _append_error(errors, blockers, f"{path}.asset_id:unknown_asset:{asset_id}", "unknown_asset") return asset_id, None samples = _sample_index(asset) ordinal = reference.get("sample_ordinal") if video_missing_fields: return asset_id, None if "sample_ordinal" in reference and ordinal is None: _append_error(errors, blockers, f"{path}.sample_ordinal:null_not_allowed", "schema_invalid") return asset_id, None if ordinal is None: if "source_frame_index" in reference or "timecode_seconds" in reference: _append_error( errors, blockers, f"{path}:frame_or_time_requires_sample_ordinal", "schema_invalid", ) blockers.append("invalid_frame_reference") return asset_id, None normalized_ordinal = _schema_integer(ordinal, 1) if normalized_ordinal is None: _append_error(errors, blockers, f"{path}.sample_ordinal:invalid", "schema_invalid") return asset_id, None expected = samples.get(normalized_ordinal) if expected is None: _append_error( errors, blockers, f"{path}.sample_ordinal:out_of_bounds:{normalized_ordinal}", "invalid_frame_reference", ) return asset_id, None if "source_frame_index" in reference: frame = reference.get("source_frame_index") normalized_frame = _schema_integer(frame, 0) if normalized_frame is None: _append_error( errors, blockers, f"{path}.source_frame_index:invalid", "schema_invalid", ) elif normalized_frame != expected.get("source_frame_index"): _append_error( errors, blockers, f"{path}.source_frame_index:does_not_match_registry", "invalid_frame_reference", ) if "timecode_seconds" in reference: timecode = reference.get("timecode_seconds") normalized_timecode = _decimal_number(timecode) if ( normalized_timecode is None or normalized_timecode < 0 ): _append_error(errors, blockers, f"{path}.timecode_seconds:invalid", "schema_invalid") elif abs( normalized_timecode - (Decimal(str(expected.get("timecode_seconds", 0.0)))) ) > Decimal("0.075"): _append_error( errors, blockers, f"{path}.timecode_seconds:does_not_match_registry", "invalid_frame_reference", ) return asset_id, str(expected.get("temporal_region", "")) or None def _validate_typed_claim( claim: Any, path: str, assets: Mapping[str, dict[str, Any]], errors: list[str], blockers: list[str], ) -> dict[str, Any] | None: initial_error_count = len(errors) if not isinstance(claim, Mapping): _append_error(errors, blockers, f"{path}:must_be_object", "schema_invalid") return None allowed = {"asset_id", "claim_type", "value", "sample_ordinal"} if set(claim) - allowed: _append_error(errors, blockers, f"{path}:unknown_fields", "schema_invalid") if not {"asset_id", "claim_type", "value"}.issubset(claim): _append_error(errors, blockers, f"{path}:missing_required_fields", "schema_invalid") return None asset_id = claim.get("asset_id") claim_type = claim.get("claim_type") if not isinstance(asset_id, str) or not _ASSET_ID_RE.fullmatch(asset_id): _append_error(errors, blockers, f"{path}.asset_id:invalid", "schema_invalid") elif asset_id not in assets: _append_error(errors, blockers, f"{path}.asset_id:unknown_asset", "unknown_asset") if not isinstance(claim_type, str) or not _CLAIM_TYPE_RE.fullmatch(claim_type): _append_error(errors, blockers, f"{path}.claim_type:invalid", "schema_invalid") claim_value = claim.get("value") if not _json_scalar(claim_value): _append_error(errors, blockers, f"{path}.value:must_be_json_scalar", "schema_invalid") elif _decimal_number(claim_value) is not None and _schema_integer( claim_value, -(1 << 63), ) is None: _append_error( errors, blockers, f"{path}.value:numeric_value_must_be_signed_64_bit_integer", "schema_invalid", ) ordinal = claim.get("sample_ordinal") if isinstance(asset_id, str) and asset_id.startswith("video:") and "sample_ordinal" not in claim: _append_error( errors, blockers, f"{path}.sample_ordinal:required_for_video_claim", "schema_invalid", ) if "sample_ordinal" in claim and ordinal is None: _append_error(errors, blockers, f"{path}.sample_ordinal:null_not_allowed", "schema_invalid") elif ordinal is not None and _schema_integer(ordinal, 1) is None: _append_error(errors, blockers, f"{path}.sample_ordinal:invalid", "schema_invalid") elif ordinal is not None and isinstance(asset_id, str) and asset_id in assets: normalized_ordinal = _schema_integer(ordinal, 1) if normalized_ordinal not in _sample_index(assets[asset_id]): _append_error( errors, blockers, f"{path}.sample_ordinal:out_of_bounds:{normalized_ordinal}", "invalid_frame_reference", ) if len(errors) != initial_error_count: return None return copy.deepcopy(dict(claim)) def validate_grounding_ledger( ledger: Mapping[str, Any], asset_registry: Mapping[str, Any], external_evidence_json: str | Mapping[str, Any] | None = None, *, final_text: str = "", ) -> GroundingLedgerValidation: """Strictly validate a model ledger against host-created assets.""" candidate = copy.deepcopy(dict(ledger)) if isinstance(ledger, Mapping) else None errors: list[str] = [] warnings: list[str] = [] blockers: list[str] = [] covered_assets: set[str] = set() temporal_regions: dict[str, set[str]] = {} typed_claims: list[dict[str, Any]] = [] if candidate is None: return GroundingLedgerValidation( ledger=None, schema_valid=False, grounded=False, analysis_status="uncertain", errors=("ledger:must_be_object",), blocking_reasons=("schema_invalid",), ) required_top = { "schema", "analysis_status", "observed_facts", "inferred_facts", "creative_additions", "uncertainties", "grounding_failure_reasons", } allowed_top = required_top missing_top = required_top - set(candidate) unknown_top = set(candidate) - allowed_top if missing_top: _append_error( errors, blockers, f"ledger:missing_fields:{','.join(sorted(missing_top))}", "schema_invalid", ) if unknown_top: _append_error( errors, blockers, f"ledger:unknown_fields:{','.join(sorted(unknown_top))}", "schema_invalid", ) if candidate.get("schema") != GROUNDING_LEDGER_SCHEMA_ID: _append_error(errors, blockers, "ledger.schema:unsupported", "schema_invalid") reported_model_status = candidate.get("analysis_status") model_status = reported_model_status if model_status not in LEDGER_ANALYSIS_STATUSES: _append_error(errors, blockers, "ledger.analysis_status:invalid", "schema_invalid") model_status = "uncertain" for key in ( "inferred_facts", "creative_additions", "uncertainties", "grounding_failure_reasons", ): _strict_text_list(candidate.get(key), f"ledger.{key}", errors, blockers) assets = _asset_index(asset_registry) facts = candidate.get("observed_facts") if not isinstance(facts, list): _append_error(errors, blockers, "ledger.observed_facts:must_be_array", "schema_invalid") facts = [] elif len(facts) > 256: _append_error(errors, blockers, "ledger.observed_facts:too_many_items", "schema_invalid") seen_fact_ids: set[str] = set() high_medium_by_asset: dict[str, list[tuple[set[str], set[str]]]] = {} categories_by_asset: dict[str, set[str]] = {} claims_by_asset: dict[str, list[tuple[str, set[str]]]] = {} for fact_index, fact in enumerate(facts): path = f"ledger.observed_facts[{fact_index}]" if not isinstance(fact, Mapping): _append_error(errors, blockers, f"{path}:must_be_object", "schema_invalid") continue # Validation accepts generic Mapping inputs, but canonicalization below # must never assume that a caller-owned/custom mapping is writable. fact = dict(fact) facts[fact_index] = fact required_fact = {"fact_id", "claim", "confidence", "categories", "evidence"} allowed_fact = required_fact | {"typed_claims"} if set(fact) - allowed_fact: _append_error(errors, blockers, f"{path}:unknown_fields", "schema_invalid") if required_fact - set(fact): _append_error(errors, blockers, f"{path}:missing_required_fields", "schema_invalid") fact_id = fact.get("fact_id") if not isinstance(fact_id, str) or not _FACT_ID_RE.fullmatch(fact_id): _append_error(errors, blockers, f"{path}.fact_id:invalid", "schema_invalid") elif fact_id in seen_fact_ids: _append_error(errors, blockers, f"{path}.fact_id:duplicate", "schema_invalid") else: seen_fact_ids.add(fact_id) claim = fact.get("claim") if not isinstance(claim, str) or not claim.strip() or len(claim) > 2048: _append_error(errors, blockers, f"{path}.claim:invalid", "schema_invalid") confidence = fact.get("confidence") if confidence not in CONFIDENCE_LEVELS: _append_error(errors, blockers, f"{path}.confidence:invalid", "schema_invalid") categories_value = fact.get("categories") canonical_categories, category_canonicalizations = _canonicalize_fact_categories( categories_value ) if canonical_categories is None: _append_error(errors, blockers, f"{path}.categories:invalid", "schema_invalid") categories: set[str] = set() else: categories = set(canonical_categories) if category_canonicalizations: fact["categories"] = canonical_categories for source_category, target_category in category_canonicalizations: warnings.append( f"{path}.categories:canonicalized:" f"{source_category}->{target_category}" ) evidence_value = fact.get("evidence") if not isinstance(evidence_value, list) or not evidence_value or len(evidence_value) > 64: _append_error(errors, blockers, f"{path}.evidence:invalid", "schema_invalid") evidence_value = [] fact_assets: set[str] = set() fact_regions_by_asset: dict[str, set[str]] = {} fact_evidence_ordinals: dict[str, set[int | None]] = {} for evidence_index, evidence in enumerate(evidence_value): evidence_error_count = len(errors) asset_id, region = _validate_evidence_reference( evidence, f"{path}.evidence[{evidence_index}]", assets, errors, blockers, ) # The host registry is authoritative. Once a sample reference has # matched, publish its canonical integer/frame/time values instead # of preserving model-authored numeric spellings or precision. if ( len(errors) == evidence_error_count and asset_id in assets and isinstance(evidence, Mapping) ): normalized_ordinal = _schema_integer(evidence.get("sample_ordinal"), 1) expected_sample = _sample_index(assets[asset_id]).get(normalized_ordinal) if expected_sample is not None: normalized_evidence = dict(evidence) normalized_evidence["sample_ordinal"] = normalized_ordinal if "source_frame_index" in normalized_evidence: normalized_evidence["source_frame_index"] = expected_sample.get( "source_frame_index" ) if "timecode_seconds" in normalized_evidence: normalized_evidence["timecode_seconds"] = expected_sample.get( "timecode_seconds" ) evidence_value[evidence_index] = normalized_evidence evidence = normalized_evidence if asset_id in assets: fact_assets.add(asset_id) if isinstance(evidence, Mapping): ordinal = evidence.get("sample_ordinal") normalized_ordinal = _schema_integer(ordinal, 1) if normalized_ordinal in _sample_index(assets[asset_id]): fact_evidence_ordinals.setdefault(asset_id, set()).add( normalized_ordinal ) elif ( ordinal is None and assets[asset_id].get("kind") != "video" and "source_frame_index" not in evidence and "timecode_seconds" not in evidence ): fact_evidence_ordinals.setdefault(asset_id, set()).add(None) if region: fact_regions_by_asset.setdefault(asset_id, set()).add(region) for asset_id in fact_assets: categories_by_asset.setdefault(asset_id, set()).update(categories) claims_by_asset.setdefault(asset_id, []).append( (str(claim or ""), set(categories)) ) if isinstance(confidence, str) and confidence in ("high", "medium"): covered_assets.update(fact_assets) for asset_id in fact_assets: regions = fact_regions_by_asset.get(asset_id, set()) if regions: temporal_regions.setdefault(asset_id, set()).update(regions) high_medium_by_asset.setdefault(asset_id, []).append((categories, regions)) fact_typed = fact.get("typed_claims", []) if not isinstance(fact_typed, list) or len(fact_typed) > 64: _append_error(errors, blockers, f"{path}.typed_claims:invalid", "schema_invalid") else: for claim_index, typed_claim in enumerate(fact_typed): validated = _validate_typed_claim( typed_claim, f"{path}.typed_claims[{claim_index}]", assets, errors, blockers, ) if validated is not None: typed_asset_id = str(validated.get("asset_id", "")) typed_ordinal = _typed_claim_key(validated, assets)[2] bound_ordinals = fact_evidence_ordinals.get(typed_asset_id, set()) if typed_asset_id not in fact_assets or ( typed_ordinal is not None and typed_ordinal not in bound_ordinals ): _append_error( errors, blockers, f"{path}.typed_claims[{claim_index}]:not_bound_to_fact_evidence", "schema_invalid", ) else: typed_claims.append(validated) typed_values_by_key: dict[tuple[str, str, int | None], set[str]] = {} for typed_claim in typed_claims: typed_values_by_key.setdefault(_typed_claim_key(typed_claim, assets), set()).add( _typed_value(typed_claim.get("value")) ) internally_conflicting_typed_keys = sorted( (key for key, values in typed_values_by_key.items() if len(values) > 1), key=lambda item: ( item[0], item[1], -1 if item[2] is None else item[2], ), ) if internally_conflicting_typed_keys: rendered = [ f"{asset_id}:{claim_type}:{ordinal if ordinal is not None else 'asset'}" for asset_id, claim_type, ordinal in internally_conflicting_typed_keys ] _append_error( errors, blockers, "ledger.typed_claims:internal_conflict:" + ",".join(rendered), "schema_invalid", ) if model_status == "grounded" and not facts: _append_error(errors, blockers, "ledger.observed_facts:empty", "insufficient_coverage") required_assets = { asset_id for asset_id, asset in assets.items() if asset.get("required_for_grounding", True) } missing_coverage = sorted(required_assets - covered_assets) if model_status == "grounded" and missing_coverage: _append_error( errors, blockers, "ledger.observed_facts:missing_high_or_medium_coverage:" + ",".join(missing_coverage), "insufficient_coverage", ) for asset_id, asset in assets.items(): if asset.get("kind") != "video" or len(asset.get("samples", [])) <= 1: continue regions = temporal_regions.get(asset_id, set()) if model_status == "grounded" and len(regions) < 2: _append_error( errors, blockers, f"ledger.observed_facts:insufficient_temporal_regions:{asset_id}", "insufficient_coverage", ) if str(asset_registry.get("media_synthesis_mode", "")) == "image_identity_video_control": identity_assets = { asset_id for asset_id, asset in assets.items() if asset.get("kind") == "image" and "identity" in str(asset.get("role", "")).lower() } video_assets = { asset_id for asset_id, asset in assets.items() if asset.get("kind") == "video" } identity_ok = any( categories & {"identity", "appearance"} for asset_id in identity_assets for categories, _regions in high_medium_by_asset.get(asset_id, []) ) action_or_camera_ok = any( categories & {"action", "motion", "temporal", "camera", "composition"} for asset_id in video_assets for categories, _regions in high_medium_by_asset.get(asset_id, []) ) if model_status == "grounded" and not identity_ok: _append_error(errors, blockers, "ledger:missing_identity_image_evidence", "insufficient_coverage") if model_status == "grounded" and not action_or_camera_ok: _append_error( errors, blockers, "ledger:missing_video_action_or_camera_evidence", "insufficient_coverage", ) for asset_id in identity_assets: if categories_by_asset.get(asset_id, set()) & { "action", "motion", "temporal", "camera", }: _append_error(errors, blockers, f"ledger:role_cross_wired:{asset_id}", "role_cross_wired") for asset_id in video_assets: if categories_by_asset.get(asset_id, set()) & {"identity", "appearance"}: _append_error(errors, blockers, f"ledger:role_cross_wired:{asset_id}", "role_cross_wired") # H3 reference manifests assign each Picture/Video an explicit role. The # role comes from the host-created registry, not from model output. Every # role-bound fact must declare a role-specific category, and claim text is # independently screened so relabeling a swapped claim as ``other`` cannot # bypass the boundary. if str(asset_registry.get("minimax_h3_mode", "")) == "ref2va": declared_visual_assets = { str(asset_id) for asset_id in asset_registry.get("declared_visual_asset_ids", []) if isinstance(asset_id, str) } attached_visual_assets = { asset_id for asset_id in assets if asset_id.startswith(("picture:", "video:")) } if declared_visual_assets != attached_visual_assets: _append_error( errors, blockers, "ledger:h3_declared_attached_asset_mismatch:" f"missing={','.join(sorted(declared_visual_assets - attached_visual_assets)) or 'none'};" f"unexpected={','.join(sorted(attached_visual_assets - declared_visual_assets)) or 'none'}", "insufficient_coverage", ) for asset_id, asset in assets.items(): if not asset_id.startswith(("picture:", "video:")): continue if asset.get("grounding_role_contract_source") != "explicit": _append_error( errors, blockers, f"ledger:h3_role_contract_not_explicit:{asset_id}", "insufficient_coverage", ) continue stored_role_categories = asset.get("grounding_role_categories") if isinstance(stored_role_categories, list): role_specific = { str(category) for category in stored_role_categories if category in FACT_CATEGORIES and category != "other" } allowed_categories = ( _ROLE_SHARED_FACT_CATEGORIES | _expand_role_category_family(role_specific) ) else: allowed_categories, role_specific = _h3_role_category_contract( asset.get("role") ) if not role_specific: _append_error( errors, blockers, f"ledger:h3_role_contract_unrecognized:{asset_id}", "insufficient_coverage", ) continue for claim_text, declared_categories in claims_by_asset.get(asset_id, []): semantic_categories = _claim_role_categories(claim_text) effective_categories = declared_categories | semantic_categories disallowed = effective_categories - allowed_categories if disallowed: _append_error( errors, blockers, f"ledger:role_cross_wired:{asset_id}:{','.join(sorted(disallowed))}", "role_cross_wired", ) if not declared_categories & role_specific: _append_error( errors, blockers, f"ledger:role_specific_category_missing:{asset_id}", "role_cross_wired", ) external_claims, external_warnings = parse_external_evidence(external_evidence_json) warnings.extend(external_warnings) accepted_external_claims: list[dict[str, Any]] = [] external_by_key: dict[tuple[str, str, int | None], set[str]] = {} external_registry_valid = True for claim_index, claim in enumerate(external_claims): asset = assets.get(str(claim.get("asset_id", ""))) if asset is None: warnings.append(f"external_evidence_unknown_asset:{claim.get('asset_id')}") external_registry_valid = False continue ordinal = claim.get("sample_ordinal") if str(claim.get("asset_id", "")).startswith("video:") and ordinal is None: warnings.append( f"external_evidence_claim_{claim_index}_video_sample_ordinal_required" ) external_registry_valid = False continue if ordinal is not None: expected_sample = _sample_index(asset).get(int(ordinal)) if expected_sample is None: warnings.append(f"external_evidence_claim_{claim_index}_sample_out_of_bounds") external_registry_valid = False continue if ( "source_frame_index" in claim and claim.get("source_frame_index") != expected_sample.get("source_frame_index") ): warnings.append(f"external_evidence_claim_{claim_index}_frame_mismatch") external_registry_valid = False continue if "timecode_seconds" in claim and abs( _decimal_number(claim.get("timecode_seconds")) - Decimal(str(expected_sample.get("timecode_seconds", 0.0))) ) > Decimal("0.075"): warnings.append(f"external_evidence_claim_{claim_index}_timecode_mismatch") external_registry_valid = False continue elif "source_frame_index" in claim or "timecode_seconds" in claim: warnings.append(f"external_evidence_claim_{claim_index}_missing_sample_ordinal") external_registry_valid = False continue accepted_external_claims.append(claim) external_by_key.setdefault(_typed_claim_key(claim, assets), set()).add( _typed_value(claim.get("value")) ) if not external_registry_valid: accepted_external_claims.clear() external_by_key.clear() conflicts: list[str] = [] for key, values in external_by_key.items(): if len(values) > 1: conflicts.append( f"{key[0]}:{key[1]}:{key[2] if key[2] is not None else 'asset'}" ) for claim in typed_claims: key = _typed_claim_key(claim, assets) expected_values = external_by_key.get(key) if expected_values and _typed_value(claim.get("value")) not in expected_values: rendered_key = f"{key[0]}:{key[1]}:{key[2] if key[2] is not None else 'asset'}" conflicts.append(rendered_key) if conflicts: _append_error( errors, blockers, "ledger.typed_claims:external_conflict:" + ",".join(sorted(set(conflicts))), "external_typed_conflict", ) failure_reasons_for_refusal = candidate.get("grounding_failure_reasons", []) if not isinstance(failure_reasons_for_refusal, (list, tuple)): failure_reasons_for_refusal = () text_for_refusal = "\n".join( [ final_text, *(str(fact.get("claim", "")) for fact in facts if isinstance(fact, Mapping)), *( str(item) for item in failure_reasons_for_refusal if isinstance(item, str) ), ] ) refusal_matches = detect_refusal_phrases(text_for_refusal) if refusal_matches: blockers.append("refusal_detected") candidate_has_unrepresentable_decimal = _contains_unrepresentable_decimal(candidate) if candidate_has_unrepresentable_decimal: _append_error( errors, blockers, "ledger:numeric_value_not_losslessly_representable", "schema_invalid", ) unique_errors = tuple(dict.fromkeys(errors)) unique_warnings = tuple(dict.fromkeys(warnings)) unique_blockers = tuple(dict.fromkeys(blockers)) schema_valid = not any(reason == "schema_invalid" for reason in unique_blockers) if model_status == "transport_error": host_status = "transport_error" elif model_status == "refused" or refusal_matches: host_status = "refused" elif model_status == "grounded" and not unique_blockers: host_status = "grounded" else: host_status = "uncertain" candidate["analysis_status"] = host_status validated_candidate = ( None if candidate_has_unrepresentable_decimal else _decimal_to_json_native(candidate) ) return GroundingLedgerValidation( ledger=validated_candidate, schema_valid=schema_valid, grounded=host_status == "grounded", analysis_status=host_status, model_analysis_status=str(reported_model_status or "uncertain"), errors=unique_errors, warnings=unique_warnings, blocking_reasons=unique_blockers, covered_asset_ids=tuple(sorted(covered_assets)), temporal_regions={ asset_id: tuple(sorted(regions)) for asset_id, regions in sorted(temporal_regions.items()) }, accepted_external_claim_count=len(accepted_external_claims), external_conflicts=tuple(sorted(set(conflicts))), refusal_matches=tuple(refusal_matches), ) def _specific_block_reason( validation: GroundingLedgerValidation | None, *, transport_confirmed: bool, telemetry_error: str, telemetry_refusal: bool, ) -> tuple[str, str]: if telemetry_error: return "uncertain", "visual_grounding_telemetry_error" if not transport_confirmed: return "transport_error", "visual_transport_error" if telemetry_refusal or (validation and validation.analysis_status == "refused"): return "refused", "visual_grounding_refused" if validation is None or not validation.schema_valid: return "uncertain", "visual_grounding_schema_invalid" if validation.grounded: return "grounded", "" if "insufficient_coverage" in validation.blocking_reasons: return "uncertain", "visual_grounding_insufficient_coverage" if validation.analysis_status == "transport_error": return "transport_error", "visual_transport_error" return "uncertain", "visual_grounding_uncertain" def decide_grounding_guard( config: GroundingGuardConfig | Mapping[str, Any] | str | None, validation: GroundingLedgerValidation | None, *, media_present: bool, transport_confirmed: bool, telemetry_error: str = "", telemetry_refusal: bool = False, ) -> GroundingGuardDecision: """Map evidence into the public off/audit/strict decision contract.""" normalized = normalize_grounding_guard_config(config) if normalized.mode == "off": return GroundingGuardDecision( mode="off", analysis_status="not_run", decision="disabled", would_block=False, ) if not media_present: return GroundingGuardDecision( mode=normalized.mode, analysis_status="not_applicable", decision="not_applicable", would_block=False, ) status, specific = _specific_block_reason( validation, transport_confirmed=transport_confirmed, telemetry_error=str(telemetry_error or ""), telemetry_refusal=bool(telemetry_refusal), ) would_block = bool(specific) blocked_reasons = ( ("visual_grounding_unverified", specific) if specific else () ) warnings = list(validation.warnings if validation else ()) if telemetry_error: warnings.append(f"grounding telemetry failed: {telemetry_error}") if normalized.mode == "audit": decision = "warn" if would_block else "pass" else: decision = "block" if would_block else "pass" return GroundingGuardDecision( mode=normalized.mode, analysis_status=status, decision=decision, would_block=would_block, blocked_reasons=blocked_reasons, warnings=tuple(dict.fromkeys(warnings)), ) def _jsonable(value: Any, *, path: str = "trace") -> Any: if value is None or isinstance(value, (str, bool, int)): return value if isinstance(value, float): if not math.isfinite(value): raise GroundingGuardError(f"{path} contains a non-finite float") return value if isinstance(value, Path): return str(value) if hasattr(value, "to_dict") and callable(value.to_dict): return _jsonable(value.to_dict(), path=path) if isinstance(value, Mapping): result: dict[str, Any] = {} for key, item in value.items(): text_key = str(key) normalized_key = text_key.strip().lower() forbidden = ( normalized_key in _FORBIDDEN_TRACE_KEYS or normalized_key.startswith("pixel_values") ) if forbidden and not _is_safe_pixel_tensor_descriptor( path, normalized_key, item, ): raise GroundingGuardError(f"{path}.{text_key} may contain raw media or logits") result[text_key] = _jsonable(item, path=f"{path}.{text_key}") return result if isinstance(value, (list, tuple)): return [_jsonable(item, path=f"{path}[{index}]") for index, item in enumerate(value)] raise GroundingGuardError(f"{path} contains unsupported value type {type(value).__name__}") def _json_size(value: Any) -> int: # The graph-facing report is emitted with indentation, sorted keys, and # ASCII escaping. Measure that exact representation so the public STRING # output cannot exceed the advertised 256 KiB even when it contains # non-ASCII text or deeply nested telemetry. return len( json.dumps( value, indent=2, sort_keys=True, ensure_ascii=True, ).encode("utf-8") ) def _truncate_strings(value: Any, max_chars: int) -> Any: if isinstance(value, str): if len(value) <= max_chars: return value marker = "...[truncated]" if max_chars <= len(marker): return marker[:max_chars] return value[: max_chars - len(marker)] + marker if isinstance(value, list): return [_truncate_strings(item, max_chars) for item in value] if isinstance(value, dict): return {key: _truncate_strings(item, max_chars) for key, item in value.items()} return value def _bounded_sequence(value: Sequence[Any], max_items: int) -> list[Any]: """Keep deterministic endpoints when a report-only sequence must shrink.""" items = list(value) limit = max(0, int(max_items)) if len(items) <= limit: return items if limit == 0: return [] if limit == 1: return items[:1] head_count = (limit + 1) // 2 tail_count = limit - head_count return [*items[:head_count], *items[-tail_count:]] def _compact_json_tree(value: Any, *, max_chars: int, max_items: int) -> Any: """Compact arbitrary JSON report detail without changing scalar types.""" if isinstance(value, str): return _truncate_strings(value, max_chars) if isinstance(value, list): return [ _compact_json_tree(item, max_chars=max_chars, max_items=max_items) for item in _bounded_sequence(value, max_items) ] if isinstance(value, dict): ordered_items = sorted(value.items(), key=lambda item: str(item[0])) selected_items = _bounded_sequence(ordered_items, max_items) result: dict[str, Any] = {} for key, item in selected_items: original_key = str(key) compact_key = _truncate_strings(original_key, max_chars) if compact_key in result: suffix_index = 1 while True: suffix = f"~{suffix_index}" prefix_chars = max(0, max_chars - len(suffix)) collision_key = original_key[:prefix_chars] + suffix if collision_key not in result: compact_key = collision_key break suffix_index += 1 result[compact_key] = _compact_json_tree( item, max_chars=max_chars, max_items=max_items, ) omitted = len(ordered_items) - len(selected_items) if omitted: marker = "__dg_omitted_mapping_keys__" suffix = 1 while marker in value or marker in result: marker = f"__dg_omitted_mapping_keys_{suffix}__" suffix += 1 result[marker] = omitted return result return value def _compact_trajectory_summary( trajectory: Any, *, max_chars: int, max_items: int, ) -> tuple[Any, dict[str, int]]: """Retain the first/final snapshots and summarize omitted trajectory detail.""" if not isinstance(trajectory, dict): return ( _compact_json_tree( trajectory, max_chars=max_chars, max_items=max_items, ), {}, ) result = copy.deepcopy(trajectory) stats: dict[str, int] = {} snapshots = result.get("snapshots") if isinstance(snapshots, list): selected = _bounded_sequence(snapshots, 2) omitted = len(snapshots) - len(selected) result["snapshots"] = [ _compact_json_tree( item, max_chars=max_chars, max_items=max_items, ) for item in selected ] if omitted: result["snapshots_truncated"] = omitted result["truncated"] = True stats["snapshots_omitted"] = omitted calls = result.get("calls") if isinstance(calls, list): selected_calls = _bounded_sequence(calls, max_items) omitted_calls = len(calls) - len(selected_calls) result["calls"] = [ _compact_json_tree( item, max_chars=max_chars, max_items=max_items, ) for item in selected_calls ] if omitted_calls: result["calls_truncated"] = omitted_calls result["truncated"] = True stats["calls_omitted"] = omitted_calls for key, item in list(result.items()): if key not in {"snapshots", "calls"}: result[key] = _compact_json_tree( item, max_chars=max_chars, max_items=max_items, ) return result, stats def _compact_validated_ledger( ledger: Any, *, max_chars: int, max_facts: int, max_text_items: int, max_evidence: int, max_typed_claims: int, ) -> tuple[Any, dict[str, Any]]: """Bound a validated ledger while retaining its typed ledger structure.""" if not isinstance(ledger, dict): return ledger, {} result = copy.deepcopy(ledger) stats: dict[str, Any] = {} original_facts = result.get("observed_facts") if isinstance(original_facts, list): selected_facts = _bounded_sequence(original_facts, max_facts) compact_facts: list[Any] = [] omitted_evidence = 0 omitted_typed_claims = 0 for fact in selected_facts: if not isinstance(fact, dict): compact_facts.append( _compact_json_tree( fact, max_chars=max_chars, max_items=max_evidence, ) ) continue compact_fact = { key: _compact_json_tree( item, max_chars=max_chars, max_items=max_evidence, ) for key, item in fact.items() if key not in {"evidence", "typed_claims"} } evidence = fact.get("evidence") if isinstance(evidence, list): selected_evidence = _bounded_sequence(evidence, max_evidence) omitted_evidence += len(evidence) - len(selected_evidence) compact_fact["evidence"] = [ _compact_json_tree( item, max_chars=max_chars, max_items=max_evidence, ) for item in selected_evidence ] elif "evidence" in fact: compact_fact["evidence"] = _compact_json_tree( evidence, max_chars=max_chars, max_items=max_evidence, ) typed_claims = fact.get("typed_claims") if isinstance(typed_claims, list): selected_claims = _bounded_sequence(typed_claims, max_typed_claims) omitted_typed_claims += len(typed_claims) - len(selected_claims) compact_fact["typed_claims"] = [ _compact_json_tree( item, max_chars=max_chars, max_items=max_typed_claims, ) for item in selected_claims ] elif "typed_claims" in fact: compact_fact["typed_claims"] = _compact_json_tree( typed_claims, max_chars=max_chars, max_items=max_typed_claims, ) compact_facts.append(compact_fact) result["observed_facts"] = compact_facts stats["observed_facts_original"] = len(original_facts) stats["observed_facts_retained"] = len(compact_facts) if omitted_evidence: stats["evidence_references_omitted"] = omitted_evidence if omitted_typed_claims: stats["typed_claims_omitted"] = omitted_typed_claims text_list_stats: dict[str, dict[str, int]] = {} for key in ( "inferred_facts", "creative_additions", "uncertainties", "grounding_failure_reasons", ): values = result.get(key) if not isinstance(values, list): continue selected_values = _bounded_sequence(values, max_text_items) result[key] = [ _compact_json_tree( item, max_chars=max_chars, max_items=max_text_items, ) for item in selected_values ] text_list_stats[key] = { "original": len(values), "retained": len(selected_values), } if text_list_stats: stats["text_lists"] = text_list_stats for key, item in list(result.items()): if key not in { "observed_facts", "inferred_facts", "creative_additions", "uncertainties", "grounding_failure_reasons", }: result[key] = _compact_json_tree( item, max_chars=max_chars, max_items=max_text_items, ) return result, stats _REPORT_COMPACTION_PROFILES: tuple[dict[str, int], ...] = ( { "max_chars": 2048, "max_items": 128, "max_facts": 256, "max_text_items": 128, "max_evidence": 32, "max_typed_claims": 32, }, { "max_chars": 1024, "max_items": 64, "max_facts": 256, "max_text_items": 64, "max_evidence": 16, "max_typed_claims": 16, }, { "max_chars": 512, "max_items": 32, "max_facts": 192, "max_text_items": 32, "max_evidence": 8, "max_typed_claims": 8, }, { "max_chars": 256, "max_items": 16, "max_facts": 128, "max_text_items": 16, "max_evidence": 4, "max_typed_claims": 4, }, { "max_chars": 128, "max_items": 8, "max_facts": 96, "max_text_items": 8, "max_evidence": 2, "max_typed_claims": 2, }, { "max_chars": 64, "max_items": 4, "max_facts": 48, "max_text_items": 4, "max_evidence": 1, "max_typed_claims": 1, }, { "max_chars": 32, "max_items": 2, "max_facts": 16, "max_text_items": 2, "max_evidence": 1, "max_typed_claims": 1, }, ) def _compacted_report_candidate( report: dict[str, Any], profile: Mapping[str, int], *, original_bytes: int, profile_index: int, ) -> dict[str, Any]: max_chars = profile["max_chars"] max_items = profile["max_items"] candidate: dict[str, Any] = {} trajectory_stats: dict[str, int] = {} ledger_stats: dict[str, Any] = {} for key, value in report.items(): if key == "trajectory_summary": candidate[key], trajectory_stats = _compact_trajectory_summary( value, max_chars=max_chars, max_items=max_items, ) elif key == "validated_ledger": candidate[key], ledger_stats = _compact_validated_ledger( value, max_chars=max_chars, max_facts=profile["max_facts"], max_text_items=profile["max_text_items"], max_evidence=profile["max_evidence"], max_typed_claims=profile["max_typed_claims"], ) else: candidate[key] = _compact_json_tree( value, max_chars=max_chars, max_items=max_items, ) candidate["report_truncated"] = True candidate["compaction"] = { "schema": "dg-grounding-report-compaction/1", "original_bytes": original_bytes, "profile": profile_index, "trajectory": trajectory_stats, "validated_ledger": ledger_stats, } return candidate def _ledger_structure_only(ledger: Any) -> Any: """Last-resort ledger shape used only for unusually tiny custom limits.""" if not isinstance(ledger, dict): return ledger return { "schema": _truncate_strings( str(ledger.get("schema", GROUNDING_LEDGER_SCHEMA_ID)), 32 ), "analysis_status": _truncate_strings( str(ledger.get("analysis_status", "uncertain")), 16 ), "observed_facts": [], "inferred_facts": [], "creative_additions": [], "uncertainties": [], "grounding_failure_reasons": [], } def _structural_compact_report( report: dict[str, Any], *, original_bytes: int, ) -> dict[str, Any]: """Retain every public report section even after content is exhausted.""" result = { "schema": report.get("schema", GROUNDING_REPORT_SCHEMA_ID), "config": _compact_json_tree( report.get("config", {}), max_chars=16, max_items=1 ), "analysis_status": report.get("analysis_status", "uncertain"), "decision": report.get("decision", "warn"), "would_block": bool(report.get("would_block", True)), "grounding_guard_would_block": bool( report.get( "grounding_guard_would_block", report.get("would_block", True), ) ), "blocked_reasons": _compact_json_tree( report.get("blocked_reasons", []), max_chars=16, max_items=1 ), "asset_registry": _compact_json_tree( report.get("asset_registry", {}), max_chars=16, max_items=1 ), "transport": _compact_json_tree( report.get("transport", {}), max_chars=16, max_items=1 ), "validated_ledger": _ledger_structure_only( report.get("validated_ledger") ), "validation": _compact_json_tree( report.get("validation", {}), max_chars=16, max_items=1 ), "attempt_count": report.get("attempt_count", 0), "retry_reasons": _compact_json_tree( report.get("retry_reasons", []), max_chars=16, max_items=1 ), "trajectory_summary": {"truncated": True}, "effective_sampling": _compact_json_tree( report.get("effective_sampling", {}), max_chars=16, max_items=1 ), "timings": _compact_json_tree( report.get("timings", {}), max_chars=16, max_items=1 ), "forward_call_count": report.get("forward_call_count", 0), "warnings": _compact_json_tree( report.get("warnings", []), max_chars=16, max_items=1 ), "verification_level": _truncate_strings( str(report.get("verification_level", "unknown")), 16 ), "report_truncated": True, "compaction": { "schema": "dg-grounding-report-compaction/1", "original_bytes": original_bytes, "profile": "structural", }, } if "transport_proof" in report: result["transport_proof"] = _compact_json_tree( report["transport_proof"], max_chars=16, max_items=1 ) return result def compact_grounding_report( report: Mapping[str, Any], max_bytes: int = MAX_COMPACT_REPORT_BYTES, ) -> dict[str, Any]: """Bound metadata without dropping the public grounding report sections.""" if max_bytes < 2048: raise GroundingGuardError("compact report limit must be at least 2048 bytes") compact = _jsonable(report, path="report") original_bytes = _json_size(compact) if original_bytes <= max_bytes: return compact # Telemetry is diagnostic detail rather than grounding evidence. Discard # intermediate snapshots first, while retaining the first/final views. trajectory = compact.get("trajectory_summary") if isinstance(trajectory, dict): snapshots = trajectory.get("snapshots") if isinstance(snapshots, list) and len(snapshots) > 2: trajectory["snapshots"] = [snapshots[0], snapshots[-1]] trajectory["snapshots_truncated"] = len(snapshots) - 2 trajectory["truncated"] = True compact["report_truncated"] = True if _json_size(compact) <= max_bytes: return compact # Exhaust trajectory-only compaction before touching evidence. This keeps # a small validated ledger byte-for-byte intact when telemetry alone is # responsible for the overflow. trajectory_profiles = ( (4096, 64), (2048, 32), (1024, 16), (512, 8), (256, 4), (128, 2), (64, 1), ) if "trajectory_summary" in compact: original_trajectory = compact.get("trajectory_summary") trajectory_probe = dict(compact) trajectory_probe["trajectory_summary"] = {} trajectory_can_resolve_overflow = _json_size(trajectory_probe) <= max_bytes last_trajectory = original_trajectory profiles = list(enumerate(trajectory_profiles, start=1)) if not trajectory_can_resolve_overflow: profiles = profiles[-1:] for detail_index, (max_chars, max_items) in profiles: candidate = dict(compact) compacted_trajectory, trajectory_stats = _compact_trajectory_summary( original_trajectory, max_chars=max_chars, max_items=max_items, ) candidate["trajectory_summary"] = compacted_trajectory candidate["compaction"] = { "schema": "dg-grounding-report-compaction/1", "original_bytes": original_bytes, "profile": f"trajectory-{detail_index}", "trajectory": trajectory_stats, } if _json_size(candidate) <= max_bytes: return candidate last_trajectory = compacted_trajectory compact["trajectory_summary"] = last_trajectory # Validation diagnostics duplicate host conclusions already represented # by analysis_status/decision. Bound that detail before reducing the # validated evidence ledger itself. if "validation" in compact: original_validation = compact.get("validation") validation_probe = dict(compact) validation_probe["validation"] = {} validation_can_resolve_overflow = _json_size(validation_probe) <= max_bytes last_validation = original_validation profiles = list(enumerate(trajectory_profiles, start=1)) if not validation_can_resolve_overflow: profiles = profiles[-1:] for detail_index, (max_chars, max_items) in profiles: candidate = dict(compact) compacted_validation = _compact_json_tree( original_validation, max_chars=max_chars, max_items=max_items, ) candidate["validation"] = compacted_validation candidate["compaction"] = { "schema": "dg-grounding-report-compaction/1", "original_bytes": original_bytes, "profile": f"validation-{detail_index}", } if _json_size(candidate) <= max_bytes: return candidate last_validation = compacted_validation compact["validation"] = last_validation # A schema-valid ledger can still be several megabytes (and typed string # values are intentionally open-ended). Compact it in deterministic, # progressively tighter profiles rather than replacing the report with a # small status-only object. for profile_index, profile in enumerate(_REPORT_COMPACTION_PROFILES, start=1): candidate = _compacted_report_candidate( compact, profile, original_bytes=original_bytes, profile_index=profile_index, ) if _json_size(candidate) <= max_bytes: return candidate structural = _structural_compact_report( compact, original_bytes=original_bytes, ) if _json_size(structural) > max_bytes: raise GroundingGuardError("critical grounding report fields exceed size limit") return structural def build_grounding_report( config: GroundingGuardConfig | Mapping[str, Any] | str | None, asset_registry: Mapping[str, Any], decision: GroundingGuardDecision, validation: GroundingLedgerValidation | None = None, *, transport: Mapping[str, Any] | None = None, trajectory_summary: Mapping[str, Any] | None = None, attempt_count: int = 0, retry_reasons: Sequence[str] = (), timings: Mapping[str, Any] | None = None, forward_call_count: int = 0, warnings: Sequence[str] = (), verification_level: str = "transport+structured_self_report", effective_sampling: Mapping[str, Any] | None = None, max_bytes: int = MAX_COMPACT_REPORT_BYTES, ) -> dict[str, Any]: """Build the compact graph-facing report; detailed raw text stays in traces.""" normalized = normalize_grounding_guard_config(config) combined_warnings = [*decision.warnings, *warnings] report = { "schema": GROUNDING_REPORT_SCHEMA_ID, "config": normalized.to_dict(), "analysis_status": decision.analysis_status, "decision": decision.decision, "would_block": decision.would_block, "grounding_guard_would_block": decision.would_block, "blocked_reasons": list(decision.blocked_reasons), "asset_registry": copy.deepcopy(dict(asset_registry)), "transport": copy.deepcopy(dict(transport or {})), "validated_ledger": copy.deepcopy(validation.ledger) if validation else None, "validation": validation.to_dict() if validation else None, "attempt_count": max(0, _safe_int(attempt_count, 0)), "retry_reasons": [str(reason) for reason in retry_reasons], "trajectory_summary": copy.deepcopy(dict(trajectory_summary or {})), "effective_sampling": copy.deepcopy( dict(effective_sampling) if effective_sampling is not None else normalized.sampling_settings ), "timings": copy.deepcopy(dict(timings or {})), "forward_call_count": max(0, _safe_int(forward_call_count, 0)), "warnings": list(dict.fromkeys(str(item) for item in combined_warnings if str(item))), "verification_level": str(verification_level), } return compact_grounding_report(report, max_bytes=max_bytes) def grounding_report_json(report: Mapping[str, Any]) -> str: return json.dumps(report, indent=2, sort_keys=True, ensure_ascii=True) def persist_grounding_trace( trace: Mapping[str, Any], output_root: str | Path, trace_subfolder: str = DEFAULT_TRACE_SUBFOLDER, *, filename: str | None = None, max_bytes: int = MAX_TRACE_BYTES, ) -> Path: """Persist a JSON-only trace beneath an explicitly supplied Comfy output root.""" root = Path(output_root).expanduser().resolve() safe_subfolder = _safe_trace_subfolder(trace_subfolder) if safe_subfolder != str(trace_subfolder or "").strip().replace("\\", "/"): if str(trace_subfolder or "").strip() not in {"", DEFAULT_TRACE_SUBFOLDER}: raise GroundingGuardError("trace_subfolder must be a relative path without traversal") target_dir = (root / safe_subfolder).resolve() try: target_dir.relative_to(root) except ValueError as exc: raise GroundingGuardError("trace path escapes the Comfy output root") from exc if filename is None: timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ") chosen_name = f"grounding-{timestamp}-{uuid.uuid4().hex[:8]}.json" else: chosen_name = str(filename).strip() if not chosen_name or Path(chosen_name).name != chosen_name: raise GroundingGuardError("trace filename must be a plain filename") if not chosen_name.lower().endswith(".json"): chosen_name += ".json" payload = _jsonable(trace, path="trace") encoded = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=True).encode("utf-8") if len(encoded) > max_bytes: raise GroundingGuardError( f"grounding trace is {len(encoded)} bytes, above the {max_bytes}-byte limit" ) target_dir.mkdir(parents=True, exist_ok=True) path = target_dir / chosen_name with path.open("x", encoding="utf-8", newline="\n") as handle: handle.write(encoded.decode("utf-8")) handle.write("\n") return path __all__ = [ "ANALYSIS_STATUSES", "ASSET_REGISTRY_SCHEMA_ID", "EXTERNAL_EVIDENCE_SCHEMA_ID", "EXTERNAL_EVIDENCE_SCHEMA_PATH", "EVIDENCE_TOKEN_BUDGETS", "FACT_CATEGORIES", "GROUNDING_CONFIG_TYPE", "GROUNDING_EVIDENCE_SCHEMA_PATH", "GROUNDING_LEDGER_SCHEMA_ID", "GROUNDING_MODES", "GROUNDING_REPORT_SCHEMA_ID", "GUARD_DECISIONS", "GroundingGuardConfig", "GroundingGuardDecision", "GroundingGuardError", "GroundingLedgerError", "GroundingLedgerValidation", "SAMPLING_PROFILE_SETTINGS", "SAMPLING_PROFILES", "build_asset_registry", "build_grounding_report", "compact_grounding_report", "decide_grounding_guard", "detect_refusal_phrases", "extract_grounding_ledger", "grounding_report_json", "load_grounding_evidence_schema", "load_external_evidence_schema", "normalize_grounding_guard_config", "parse_external_evidence", "persist_grounding_trace", "validate_grounding_ledger", ]