"""Schema-first external contract models for ACES artifact boundaries."""

from __future__ import annotations

import importlib
import json
import re
from collections.abc import Mapping
from copy import deepcopy
from datetime import UTC, datetime, timedelta
from enum import Enum
from functools import lru_cache
from typing import Annotated, Any, Literal
from urllib.parse import parse_qsl, urlsplit

from aces_sdl import VARIABLE_TOKEN_PATTERN
from aces_sdl.canonical import InstantiatedScenarioSnapshot
from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance
from aces_sdl.identifiers import PORTABLE_IDENTIFIER_JSON_SCHEMA, QUALIFIED_IDENTIFIER_MAX_LENGTH
from aces_sdl.observability_plane_semantics import classify_contract_plane
from aces_sdl.participant_attribution_semantics import (
    ParticipantAttributionCandidateKind,
    ParticipantAttributionOrderingBasisKind,
    ParticipantAttributionSupportClass,
)
from aces_sdl.participant_behavior import (
    ParticipantEffectClass,
    ParticipantFailureClass,
    ParticipantInteractionClass,
    ParticipantPreconditionClass,
)
from aces_sdl.participant_outcome_semantics import (
    OutcomeInterpretationSourceLayer,
    OutcomeInterpretationTargetLayer,
)
from aces_sdl.participant_temporal_semantics import (
    ParticipantTemporalEventPoint,
    ParticipantTimeDomain,
)
from aces_sdl.scenario import InstantiatedScenario, Scenario
from aces_sdl.schema_catalogs import HASHMAP_SECTIONS, RUNTIME_SERVICE_FAMILIES, RuntimeReferenceChild
from aces_sdl.value_parsing import VARIABLE_REFERENCE_SCHEMA_MARKER
from pydantic import BaseModel, ConfigDict, Field, GetJsonSchemaHandler, StrictInt, model_validator
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema

from .addressing import COMPILED_ADDRESS_JSON_SCHEMA, CompiledAddress
from .corpus import CONCEPT_AUTHORITY, corpus_family_root
from .manifest_authority import (
    BACKEND_SUPPORTED_CONTRACT_IDS,
    PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS,
    PROCESSOR_SUPPORTED_CONTRACT_IDS,
    PROCESSOR_SUPPORTED_SDL_VERSION_IDS,
    validate_backend_supported_contract_versions,
    validate_participant_implementation_supported_contract_versions,
    validate_participant_supported_contract_versions,
    validate_processor_supported_contract_versions,
    validate_processor_supported_sdl_versions,
)
from .participant_behavior import (
    ParticipantAdmissionDisposition,
    ParticipantBehaviorHistoryEventType,
    ParticipantLifecycleOperationState,
    ParticipantObservationStatus,
    ParticipantPhaseRealization,
    ParticipantRuntimeLifecyclePhase,
    participant_lifecycle_field_violation_messages,
)
from .planning import (
    PLAN_ADDRESS_ROOT_BY_DOMAIN,
    PLAN_RESOURCE_TYPES_BY_DOMAIN,
    RuntimeDomain,
    require_plan_operation_identity,
)
from .versions import (
    ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION,
    ATLAS_TACTICS_SOURCE_SCHEMA_VERSION,
    ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION,
    BACKEND_MANIFEST_V2_SCHEMA_VERSION,
    CONCEPT_FAMILIES_SCHEMA_VERSION,
    CONTROLLED_VOCABULARIES_SCHEMA_VERSION,
    EVALUATION_STATE_SCHEMA_VERSION,
    EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION,
    EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION,
    EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION,
    EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION,
    EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION,
    EXPERIMENT_RUN_SCHEMA_VERSION,
    EXPERIMENT_STUDY_SCHEMA_VERSION,
    EXPERIMENT_TASK_SCHEMA_VERSION,
    OPERATION_SCHEMA_VERSION,
    PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION,
    PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION,
    PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION,
    PROCESSOR_MANIFEST_V2_SCHEMA_VERSION,
    PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION,
    REFERENCE_MODELS_SCHEMA_VERSION,
    REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION,
    RUNTIME_SNAPSHOT_SCHEMA_VERSION,
    SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION,
    SEMANTIC_PROFILE_SCHEMA_VERSION,
    UCO_ALIGNMENT_SCHEMA_VERSION,
    WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION,
    WORKFLOW_STATE_SCHEMA_VERSION,
)
from .vocabulary import (
    ConceptFamilyId,
    ConceptProvenanceCategory,
    ParticipantFeatureSupportLevel,
    ProcessorFeature,
    RealizationSupportMode,
    WorkflowFeature,
    WorkflowStatePredicateFeature,
)


class ContractModel(BaseModel):
    """Base model for closed-world external contracts."""

    model_config = ConfigDict(extra="forbid")


NonEmptyString = Annotated[str, Field(min_length=1)]
BehavioralRelationId = Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]*$")]
BehavioralTaxonomyRevision = Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9.-]*$")]


class BehavioralClaimBindingModel(ContractModel):
    """A bounded claim tied to one revisioned behavioral-relation definition.

    This is deliberately a claim *binding*, not another relation registry. The
    catalog owns relation meaning; consumers supply the subject, carriers,
    quantifier/evidence boundary, assurance state, and explicit limitations.
    """

    taxonomy_id: NonEmptyString
    taxonomy_revision: BehavioralTaxonomyRevision
    relation_id: BehavioralRelationId
    subject: NonEmptyString
    left_carrier_ref: NonEmptyString | None = None
    right_carrier_ref: NonEmptyString | None = None
    observation_projection_ref: NonEmptyString | None = None
    observation_projection_revision: NonEmptyString | None = None
    quantifier_scope: Literal[
        "single-artifact",
        "finite-cases",
        "sampled-population",
        "all-admitted-inputs",
        "all-traces",
        "all-strategies",
    ]
    evidence_scope: Literal["structural", "finite", "statistical", "model-check", "proof"]
    evidence_boundary: NonEmptyString
    assurance_status: Literal[
        "defined",
        "implemented",
        "tested",
        "model-checked",
        "proved",
        "deliberately-unproved",
        "future",
    ]
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    limitations: list[NonEmptyString] = Field(min_length=1)
    explicit_non_claims: list[NonEmptyString] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_claim_strength(self) -> BehavioralClaimBindingModel:
        universal_scopes = {"all-admitted-inputs", "all-traces", "all-strategies"}
        if self.quantifier_scope in universal_scopes and self.evidence_scope not in {"model-check", "proof"}:
            raise ValueError("universal quantification requires model-check or proof evidence")
        if self.assurance_status == "proved" and self.evidence_scope != "proof":
            raise ValueError("proved assurance requires proof evidence")
        if self.assurance_status == "model-checked" and self.evidence_scope != "model-check":
            raise ValueError("model-checked assurance requires model-check evidence")
        projection_values = (self.observation_projection_ref, self.observation_projection_revision)
        if (projection_values[0] is None) != (projection_values[1] is None):
            raise ValueError("observation projection ref and revision must be supplied together")
        return self


Rfc3339DateTimeString = Annotated[
    str,
    Field(
        min_length=1,
        pattern=(
            r"^\d{4}-\d{2}-\d{2}[Tt](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)"
            r"(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$"
        ),
        json_schema_extra={"format": "date-time"},
    ),
]
CalendarDateString = Annotated[str, Field(pattern=r"^\d{4}-\d{2}-\d{2}$")]
HexDigestString = Annotated[str, Field(min_length=1, pattern=r"^[A-Fa-f0-9]+$")]
PrefixedDigestString = Annotated[
    str,
    Field(
        min_length=1,
        pattern=(
            r"^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|"
            r"sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$"
        ),
    ),
]
NonNegativeInteger = Annotated[int, Field(ge=0)]
PositiveInteger = Annotated[int, Field(ge=1)]
UnitIntervalFloat = Annotated[float, Field(gt=0, le=1)]
ClosedUnitIntervalFloat = Annotated[float, Field(ge=0, le=1)]
SemanticProfileId = Annotated[str, Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*-v[0-9]+$")]
SemanticAssumptionId = Annotated[str, Field(pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$")]
ReferenceModelId = Annotated[str, Field(pattern=r"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$")]
JsonPointerString = Annotated[str, Field(pattern=r"^#(?:/[A-Za-z0-9_.$~-]+)+$")]
JsonInstancePathString = Annotated[str, Field(pattern=r"^#(?:/[A-Za-z0-9_.$~-]+)*$")]
InstancePath = Annotated[str, Field(pattern=r"^[a-z_][a-z0-9_]*(?:\.(?:[a-z_][a-z0-9_]*|\*))*$")]
ControlledVocabularyTermId = Annotated[str, Field(pattern=r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$")]

_BACKEND_CONCEPT_BINDING_SCOPES = frozenset(
    {
        "capabilities.provisioner.supported_node_types",
        "capabilities.provisioner.supported_os_families",
        "capabilities.provisioner.supported_content_types",
        "capabilities.provisioner.supported_account_features",
        "capabilities.provisioner.supported_domain_profiles",
        "capabilities.orchestrator.supported_sections",
        "capabilities.evaluator.supported_sections",
        "capabilities.observation.supported_capture_kinds",
        "capabilities.observation.supported_channel_kinds",
        "capabilities.observation.supported_sealing_modes",
        "capabilities.participant_runtime.supported_participant_roles",
        "capabilities.participant_runtime.supported_behavior_features",
        "capabilities.participant_runtime.supported_interaction_features",
    }
)

_PROCESSOR_CONCEPT_BINDING_SCOPES = frozenset(
    {
        "capabilities.supported_sdl_versions",
        "capabilities.supported_features",
    }
)

_PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES = frozenset(
    {
        "implementation_kind",
        "capabilities.supported_participant_contracts",
        "capabilities.supported_decision_surface_modes",
        "capabilities.tool_affordance_expectations",
        "capabilities.exposure_policy_kinds",
    }
)

_CONTROLLED_VOCABULARY_GOVERNED_SCOPES = frozenset(
    {
        "behavior_specifications.behavior_mode",
        "behavior_specifications.ai_offensive_behavior_refs",
        "behavior_specifications.offensive_behavior_refs",
        "agents.interactive_access.channel",
        "capabilities.supported_features",
        "implementation_kind",
        "capabilities.supported_participant_contracts",
        "capabilities.supported_decision_surface_modes",
        "capabilities.tool_affordance_expectations",
        "capabilities.exposure_policy_kinds",
        "capabilities.orchestrator.supported_workflow_features",
        "capabilities.orchestrator.supported_workflow_state_predicates",
        *_BACKEND_CONCEPT_BINDING_SCOPES,
        *_PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES,
    }
)

_CHECKSUM_VALUE_PATTERNS = {
    "sha256": r"^[A-Fa-f0-9]{64}$",
    "sha384": r"^[A-Fa-f0-9]{96}$",
    "sha512": r"^[A-Fa-f0-9]{128}$",
    "blake3": r"^[A-Fa-f0-9]{64}$",
}

_RFC3339_DATETIME_PATTERN = re.compile(
    r"^\d{4}-\d{2}-\d{2}[Tt](?:[01]\d|2[0-3]):[0-5]\d:(?P<second>[0-5]\d|60)"
    r"(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$"
)
_VALID_UTC_LEAP_SECOND_DATES = frozenset(
    {
        (1972, 6, 30),
        (1972, 12, 31),
        (1973, 12, 31),
        (1974, 12, 31),
        (1975, 12, 31),
        (1976, 12, 31),
        (1977, 12, 31),
        (1978, 12, 31),
        (1979, 12, 31),
        (1981, 6, 30),
        (1982, 6, 30),
        (1983, 6, 30),
        (1985, 6, 30),
        (1987, 12, 31),
        (1989, 12, 31),
        (1990, 12, 31),
        (1992, 6, 30),
        (1993, 6, 30),
        (1994, 6, 30),
        (1995, 12, 31),
        (1997, 6, 30),
        (1998, 12, 31),
        (2005, 12, 31),
        (2008, 12, 31),
        (2012, 6, 30),
        (2015, 6, 30),
        (2016, 12, 31),
    }
)

_ACES_SEMANTIC_INVARIANT_PROFILE_URI = "https://aces.dev/schemas/semantic-invariants/v1"


def _canonical_digest(digest: str | None) -> str | None:
    return digest.casefold() if digest is not None else None


def _parse_rfc3339_datetime(field_name: str, value: str) -> datetime:
    match = _RFC3339_DATETIME_PATTERN.fullmatch(value)
    if match is None:
        raise ValueError(f"{field_name} must be a valid RFC 3339 date-time")
    normalized_value = f"{value[:-1]}+00:00" if value.endswith(("Z", "z")) else value
    if match.group("second") == "60":
        normalized_value = f"{normalized_value[:17]}59{normalized_value[19:]}"
    try:
        parsed = datetime.fromisoformat(normalized_value)
    except ValueError as exc:
        raise ValueError(f"{field_name} must be a valid RFC 3339 date-time") from exc
    if match.group("second") == "60":
        utc_leap_second = parsed.astimezone(UTC)
        if (
            (utc_leap_second.year, utc_leap_second.month, utc_leap_second.day) not in _VALID_UTC_LEAP_SECOND_DATES
            or utc_leap_second.hour != 23
            or utc_leap_second.minute != 59
            or utc_leap_second.second != 59
        ):
            raise ValueError(f"{field_name} must use a valid RFC 3339 leap-second instant")
        parsed += timedelta(seconds=1)
    return parsed


def _payload_get(payload: Any, field_name: str) -> Any:
    if isinstance(payload, Mapping):
        return payload.get(field_name)
    return getattr(payload, field_name, None)


def _validate_rfc3339_payload_field(payload: Any, field_name: str) -> None:
    value = _payload_get(payload, field_name)
    if value is not None:
        _parse_rfc3339_datetime(field_name, value)


def _validate_artifact_collection_created_at(field_name: str, artifacts: Any) -> None:
    for index, artifact in enumerate(artifacts or []):
        created_at = _payload_get(artifact, "created_at")
        if created_at is not None:
            _parse_rfc3339_datetime(f"{field_name}/{index}/created_at", created_at)


def _add_aces_invariant(
    json_schema: JsonSchemaValue,
    invariant_id: str,
    description: str,
    *,
    validator: str,
    inputs: list[dict[str, str]],
) -> None:
    invariants = json_schema.setdefault("x-aces-invariants", [])
    if isinstance(invariants, list):
        invariants.append(
            {
                "id": invariant_id,
                "description": description,
                "level": "error",
                "validator": validator,
                "inputs": inputs,
            }
        )


def _add_aces_plane(json_schema: JsonSchemaValue, contract_id: str) -> None:
    """Publish the carrier's single SEM-224 observability/evidence plane.

    Plane ownership is sourced from the carrier-oriented classifier so the
    portable ``x-aces-plane`` annotation cannot drift from
    ``aces_sdl.observability_plane_semantics`` (ADR-066 / SEM-224).
    """

    json_schema["x-aces-plane"] = classify_contract_plane(contract_id).value


def _schema_contains_aces_invariants(schema_node: Any) -> bool:
    if isinstance(schema_node, dict):
        if "x-aces-invariants" in schema_node:
            return True
        return any(_schema_contains_aces_invariants(value) for value in schema_node.values())
    if isinstance(schema_node, list):
        return any(_schema_contains_aces_invariants(value) for value in schema_node)
    return False


def _attach_aces_semantic_profile(contract_id: str, json_schema: dict[str, Any]) -> None:
    if _schema_contains_aces_invariants(json_schema):
        json_schema["x-aces-semantic-profile"] = {
            "id": "aces-semantic-invariants-v1",
            "uri": _ACES_SEMANTIC_INVARIANT_PROFILE_URI,
            "contract_id": contract_id,
            "keyword": "x-aces-invariants",
            "required": True,
            "entry_schema_contract_id": "aces-semantic-invariants-v1",
            "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel",
        }


def _attach_experiment_datetime_invariants(contract_id: str, json_schema: dict[str, Any]) -> None:
    invariant_by_contract = {
        "experiment-task-v1": (
            "task-archival-times-rfc3339-valid",
            "Task artifact_refs created_at values must be valid RFC 3339 date-times, including only known "
            "valid UTC leap-second instants.",
            "aces_contracts.contracts.validate_experiment_task_archival_datetimes",
            [{"contract_id": "experiment-task-v1", "instance_path": "#/artifact_refs"}],
        ),
        "experiment-apparatus-context-v1": (
            "apparatus-archival-times-rfc3339-valid",
            "Apparatus declared_at and observed_setup_evidence created_at values must be valid RFC 3339 "
            "date-times, including only known valid UTC leap-second instants.",
            "aces_contracts.contracts.validate_experiment_apparatus_context_archival_datetimes",
            [
                {"contract_id": "experiment-apparatus-context-v1", "instance_path": "#/declared_at"},
                {"contract_id": "experiment-apparatus-context-v1", "instance_path": "#/observed_setup_evidence"},
            ],
        ),
        "experiment-run-v1": (
            "run-archival-times-rfc3339-valid",
            "Run started_at, ended_at, invalidation invalidated_at, and evidence_artifacts created_at values "
            "must be valid RFC 3339 date-times, including only known valid UTC leap-second instants.",
            "aces_contracts.contracts.validate_experiment_run_archival_datetimes",
            [
                {"contract_id": "experiment-run-v1", "instance_path": "#/started_at"},
                {"contract_id": "experiment-run-v1", "instance_path": "#/ended_at"},
                {"contract_id": "experiment-run-v1", "instance_path": "#/invalidation"},
                {"contract_id": "experiment-run-v1", "instance_path": "#/evidence_artifacts"},
            ],
        ),
        "experiment-study-v1": (
            "study-archival-times-rfc3339-valid",
            "Study report_artifacts and export_artifacts created_at values must be valid RFC 3339 date-times, "
            "including only known valid UTC leap-second instants.",
            "aces_contracts.contracts.validate_experiment_study_archival_datetimes",
            [
                {"contract_id": "experiment-study-v1", "instance_path": "#/report_artifacts"},
                {"contract_id": "experiment-study-v1", "instance_path": "#/export_artifacts"},
            ],
        ),
    }
    invariant = invariant_by_contract.get(contract_id)
    if invariant is None:
        return
    invariant_id, description, validator, inputs = invariant
    _add_aces_invariant(
        json_schema,
        invariant_id,
        description,
        validator=validator,
        inputs=inputs,
    )


def _validate_reported_value_status(
    value_status: str,
    value: object | None,
    *,
    reported_message: str,
    non_reported_message: str,
) -> None:
    if value_status == "reported" and value is None:
        raise ValueError(reported_message)
    if value_status != "reported" and value is not None:
        raise ValueError(non_reported_message)


def _extend_reported_value_status_schema(json_schema: JsonSchemaValue) -> None:
    json_schema.setdefault("allOf", []).extend(
        [
            {
                "if": {
                    "properties": {"value_status": {"const": "reported"}},
                    "required": ["value_status"],
                },
                "then": {"required": ["value"], "properties": {"value": {"not": {"type": "null"}}}},
            },
            {
                "if": {
                    "properties": {"value_status": {"enum": ["missing", "withheld", "not-applicable"]}},
                    "required": ["value_status"],
                },
                "then": {"properties": {"value": {"type": "null"}}},
            },
        ]
    )


_DEFS_KEY = "$defs"
_INSTANTIATION_INVARIANT_CONTRACT_ID = "instantiated-scenario-v1"
_INSTANTIATED_SNAPSHOT_CONTRACT_ID = "instantiated-scenario-snapshot-v1"
_SDL_AUTHORING_CONTRACT_ID = "sdl-authoring-input-v1"
_SDL_IDENTIFIER_CONTRACT_IDS = frozenset(
    {
        _SDL_AUTHORING_CONTRACT_ID,
        _INSTANTIATION_INVARIANT_CONTRACT_ID,
        _INSTANTIATED_SNAPSHOT_CONTRACT_ID,
    }
)
_SCHEMA_MAP_KEYS = ("properties", "patternProperties", _DEFS_KEY)
_SCHEMA_SUBSCHEMA_KEYS = (
    "additionalProperties",
    "items",
    "contains",
    "anyOf",
    "allOf",
    "oneOf",
    "prefixItems",
)


def _portable_property_names(*, maximum: int = 64) -> dict[str, Any]:
    schema = deepcopy(PORTABLE_IDENTIFIER_JSON_SCHEMA)
    schema["maxLength"] = maximum
    return schema


def _qualified_property_names(*, local_maximum: int = 64) -> dict[str, Any]:
    local_tail = local_maximum - 1
    segment = r"(?:[a-z0-9][a-z0-9_-]{0,63}|__private)"
    return {
        "type": "string",
        "minLength": 1,
        "maxLength": QUALIFIED_IDENTIFIER_MAX_LENGTH,
        "pattern": rf"^(?:{segment}\.)*[a-z0-9][a-z0-9_-]{{0,{local_tail}}}$",
        "not": {"pattern": "[^a-z0-9_.-]"},
    }


def _resolve_local_ref(schema: dict[str, Any], node: object) -> dict[str, Any] | None:
    if not isinstance(node, dict):
        return None
    ref = node.get("$ref")
    if isinstance(ref, str) and ref.startswith("#/$defs/"):
        resolved = schema.get(_DEFS_KEY, {}).get(ref.removeprefix("#/$defs/"))
        return resolved if isinstance(resolved, dict) else None
    return node


def _collection_item_schema(
    schema: dict[str, Any], owner: dict[str, Any], collection_name: str
) -> dict[str, Any] | None:
    collection = owner.get("properties", {}).get(collection_name)
    collection = _resolve_local_ref(schema, collection)
    if not isinstance(collection, dict):
        return None
    items = collection.get("items")
    return _resolve_local_ref(schema, items)


def _constrain_runtime_children(
    schema: dict[str, Any],
    owner: dict[str, Any],
    children: tuple[RuntimeReferenceChild, ...],
) -> None:
    for child in children:
        child_schema = _collection_item_schema(schema, owner, child.collection_name)
        if child_schema is None:
            continue
        child_schema.setdefault("properties", {})[child.id_field] = _portable_property_names()
        _constrain_runtime_children(schema, child_schema, child.children)


def _attach_runtime_identifier_constraints(schema: dict[str, Any]) -> None:
    runtime = schema.get(_DEFS_KEY, {}).get("RuntimeConfiguration")
    if not isinstance(runtime, dict):
        return
    for family in RUNTIME_SERVICE_FAMILIES:
        item_schema = _collection_item_schema(schema, runtime, family.collection_name)
        if item_schema is None:
            continue
        if family.collection_name != "forwarding_agents":
            item_schema.setdefault("properties", {})[family.id_field] = _portable_property_names()
        _constrain_runtime_children(schema, item_schema, family.child_refs)


def _constrain_collection_item_field(
    owner: dict[str, Any],
    collection_name: str,
    field_name: str,
    field_schema: dict[str, Any],
) -> None:
    collection = owner.get("properties", {}).get(collection_name)
    if not isinstance(collection, dict):
        return
    items = collection.get("items")
    if not isinstance(items, dict):
        return
    collection["items"] = {
        "allOf": [
            items,
            {"properties": {field_name: field_schema}, "type": "object"},
        ]
    }


def _attach_instantiation_request_identifier_constraints(schema: dict[str, Any]) -> None:
    parameters = schema.get("properties", {}).get("parameters")
    if isinstance(parameters, dict):
        parameters["propertyNames"] = _portable_property_names()


def _attach_sdl_identifier_constraints(contract_id: str, schema: dict[str, Any]) -> None:
    if contract_id == "scenario-instantiation-request-v1":
        _attach_instantiation_request_identifier_constraints(schema)
    elif contract_id in _SDL_IDENTIFIER_CONTRACT_IDS:
        _attach_scenario_identifier_constraints(contract_id, schema)


def _scenario_schema_for_identifier_constraints(
    contract_id: str,
    schema: dict[str, Any],
) -> dict[str, Any] | None:
    if contract_id != _INSTANTIATED_SNAPSHOT_CONTRACT_ID:
        return schema
    nested = schema.get(_DEFS_KEY, {}).get("InstantiatedScenario")
    return nested if isinstance(nested, dict) else None


def _attach_scenario_identifier_constraints(contract_id: str, schema: dict[str, Any]) -> None:
    scenario_schema = _scenario_schema_for_identifier_constraints(contract_id, schema)
    if scenario_schema is None:
        return
    qualified = contract_id != _SDL_AUTHORING_CONTRACT_ID
    for section_name in HASHMAP_SECTIONS:
        section = scenario_schema.get("properties", {}).get(section_name)
        if not isinstance(section, dict):
            continue
        local_maximum = 35 if section_name == "nodes" else 64
        section["propertyNames"] = (
            _qualified_property_names(local_maximum=local_maximum)
            if qualified
            else _portable_property_names(maximum=local_maximum)
        )
    _attach_runtime_identifier_constraints(schema)
    forwarding_id_schema = _qualified_property_names() if qualified else _portable_property_names()
    _constrain_collection_item_field(
        scenario_schema,
        "forwarding_agents",
        "forwarding_agent_id",
        forwarding_id_schema,
    )
    runtime = schema.get(_DEFS_KEY, {}).get("RuntimeConfiguration")
    if isinstance(runtime, dict):
        _constrain_collection_item_field(
            runtime,
            "forwarding_agents",
            "forwarding_agent_id",
            _portable_property_names(),
        )


def _apply_string_token_constraint(node: dict[str, Any]) -> None:
    """Forbid the ``${name}`` token on a single free string subschema.

    Targets scalar ``"type": "string"`` only, skipping fixed ``enum`` / ``const``
    values, which avoids the nullable-string and fixed-value pitfalls. The
    ``${var}`` branch of every ``*_or_var`` field (``InfraNode.count``,
    ``ACLRule.ports``, ``SimpleProperties.internal`` …) is a bare
    ``{"type": "string"}`` branch, so it is covered.
    """
    if node.get("type") != "string" or "enum" in node or "const" in node:
        return
    constraint = {"pattern": VARIABLE_TOKEN_PATTERN}
    if "not" in node:
        node.setdefault("allOf", []).append({"not": constraint})
    else:
        node["not"] = constraint


def _child_subschemas(node: dict[str, Any]) -> list[Any]:
    """Return the applicator subschemas reachable from ``node``.

    Mapping keys are not substitution sites, so ``propertyNames`` is
    intentionally excluded.
    """
    children: list[Any] = []
    for key in _SCHEMA_MAP_KEYS:
        child = node.get(key)
        if isinstance(child, dict):
            children.extend(child.values())
    for key in _SCHEMA_SUBSCHEMA_KEYS:
        if key in node:
            children.append(node[key])
    return children


def _forbid_variable_tokens_in_strings(node: object) -> None:
    """Remove variable-only alternatives and forbid tokens in remaining strings."""
    if isinstance(node, list):
        for item in node:
            _forbid_variable_tokens_in_strings(item)
        return
    if not isinstance(node, dict):
        return
    _remove_variable_reference_union_branches(node)
    _apply_string_token_constraint(node)
    for child in _child_subschemas(node):
        _forbid_variable_tokens_in_strings(child)


def _remove_variable_reference_union_branches(node: dict[str, Any]) -> None:
    """Collapse authoring unions to their concrete branches for phase artifacts."""
    for keyword in ("anyOf", "oneOf"):
        branches = node.get(keyword)
        if not isinstance(branches, list):
            continue
        retained = [
            branch
            for branch in branches
            if not (isinstance(branch, dict) and branch.get(VARIABLE_REFERENCE_SCHEMA_MARKER) is True)
        ]
        if len(retained) == len(branches):
            continue
        node.pop(keyword)
        if len(retained) == 1 and isinstance(retained[0], dict):
            outer_keywords = dict(node)
            node.clear()
            node.update(retained[0])
            node.update(outer_keywords)
        else:
            node[keyword] = retained or [{"not": {}}]


def _attach_instantiation_invariants(contract_id: str, json_schema: dict[str, Any]) -> None:
    """Apply the no-substitution-token invariant to concrete SDL artifacts.

    An instantiated scenario is fully concrete, so its payload and canonical
    snapshot forbid unresolved ``${var}`` tokens in string values. The matching
    model-level invariant lives on ``InstantiatedScenario``.
    """
    if contract_id not in {
        _INSTANTIATION_INVARIANT_CONTRACT_ID,
        _INSTANTIATED_SNAPSHOT_CONTRACT_ID,
    }:
        return
    _forbid_variable_tokens_in_strings(json_schema)


def _schema_id_for_contract_id(contract_id: str) -> str:
    if contract_id == "aces-semantic-invariants-v1":
        return _ACES_SEMANTIC_INVARIANT_PROFILE_URI
    return f"https://aces.dev/schemas/{contract_id}.json"


def _attach_json_schema_metadata(contract_id: str, json_schema: dict[str, Any]) -> None:
    json_schema.setdefault("$schema", _JSON_SCHEMA_DRAFT_2020_12)
    json_schema.setdefault("$id", _schema_id_for_contract_id(contract_id))


def _attach_compiled_address_map_constraints(contract_id: str, json_schema: dict[str, Any]) -> None:
    if contract_id != "runtime-snapshot-v1":
        return
    entries = json_schema.get("properties", {}).get("entries")
    if not isinstance(entries, dict):
        return
    entries["propertyNames"] = deepcopy(COMPILED_ADDRESS_JSON_SCHEMA)
    entries["additionalProperties"] = False


_PLAN_CONTRACT_DOMAIN = {
    "provisioning-plan-v1": RuntimeDomain.PROVISIONING,
    "orchestration-plan-v1": RuntimeDomain.ORCHESTRATION,
    "evaluation-plan-v1": RuntimeDomain.EVALUATION,
}


def _attach_plan_identity_constraints(contract_id: str, json_schema: dict[str, Any]) -> None:
    domain = _PLAN_CONTRACT_DOMAIN.get(contract_id)
    if domain is None:
        return
    operation = json_schema.get(_DEFS_KEY, {}).get("PlanOperationModel")
    if not isinstance(operation, dict):
        return
    properties = operation.get("properties", {})
    address = properties.get("address")
    resource_type = properties.get("resource_type")
    if isinstance(address, dict):
        address.setdefault("allOf", []).append({"pattern": rf"^{PLAN_ADDRESS_ROOT_BY_DOMAIN[domain]}\."})
    if isinstance(resource_type, dict):
        resource_type["enum"] = sorted(PLAN_RESOURCE_TYPES_BY_DOMAIN[domain])


_SEMANTIC_PROFILE_PHASE_ALLOWED_BINDING_SCOPES = {
    "authoring": frozenset(),
    "exchange": frozenset(),
    "processing": _PROCESSOR_CONCEPT_BINDING_SCOPES,
    "execution": _BACKEND_CONCEPT_BINDING_SCOPES | _PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES,
}

_JSON_SCHEMA_KEY = "$schema"
_JSON_SCHEMA_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema"
_ACES_SEMANTIC_INVARIANTS_SCHEMA_VERSION = "aces-semantic-invariants/v1"


class AcesSemanticInvariantInputModel(ContractModel):
    """Input contract and instance path required by one ACES semantic invariant."""

    contract_id: NonEmptyString
    instance_path: JsonInstancePathString


class AcesSemanticInvariantEntryModel(ContractModel):
    """Machine-readable semantic invariant annotation entry."""

    id: NonEmptyString
    description: NonEmptyString
    level: Literal["error"]
    validator: NonEmptyString
    inputs: list[AcesSemanticInvariantInputModel] = Field(min_length=1)


class AcesSemanticInvariantProfileModel(ContractModel):
    """Published shape for ACES semantic-invariant annotations."""

    schema_version: Literal[_ACES_SEMANTIC_INVARIANTS_SCHEMA_VERSION]
    profile_id: Literal["aces-semantic-invariants-v1"]
    uri: Literal["https://aces.dev/schemas/semantic-invariants/v1"]
    keyword: Literal["x-aces-invariants"]
    invariant_entry_schema: Literal["#/$defs/AcesSemanticInvariantEntryModel"]
    profile_reference_schema: Literal["#/$defs/AcesSemanticInvariantProfileReferenceModel"]
    invariants: list[AcesSemanticInvariantEntryModel]


class AcesSemanticInvariantProfileReferenceModel(ContractModel):
    """Host-schema reference to the ACES semantic-invariant profile."""

    id: Literal["aces-semantic-invariants-v1"]
    uri: Literal["https://aces.dev/schemas/semantic-invariants/v1"]
    contract_id: NonEmptyString
    keyword: Literal["x-aces-invariants"]
    required: Literal[True]
    entry_schema_contract_id: Literal["aces-semantic-invariants-v1"]
    entry_schema_pointer: Literal["#/$defs/AcesSemanticInvariantEntryModel"]


def _aces_semantic_invariant_profile_schema_for_bundle() -> dict[str, Any]:
    json_schema = AcesSemanticInvariantProfileModel.model_json_schema()
    json_schema.setdefault(_DEFS_KEY, {})["AcesSemanticInvariantProfileReferenceModel"] = (
        AcesSemanticInvariantProfileReferenceModel.model_json_schema()
    )
    return json_schema


def _iter_aces_semantic_invariant_entries(schema_node: Any) -> list[dict[str, Any]]:
    entries: list[dict[str, Any]] = []
    if isinstance(schema_node, dict):
        invariants = schema_node.get("x-aces-invariants")
        if invariants is not None:
            if not isinstance(invariants, list):
                raise ValueError("x-aces-invariants must be an array")
            entries.extend(invariants)
        for value in schema_node.values():
            entries.extend(_iter_aces_semantic_invariant_entries(value))
    elif isinstance(schema_node, list):
        for value in schema_node:
            entries.extend(_iter_aces_semantic_invariant_entries(value))
    return entries


def _resolve_semantic_validator(validator: str) -> Any:
    parts = validator.split(".")
    for index in range(len(parts), 0, -1):
        module_name = ".".join(parts[:index])
        try:
            module_spec = importlib.util.find_spec(module_name)
        except (ModuleNotFoundError, ValueError):
            continue
        if module_spec is None:
            continue
        target: Any = importlib.import_module(module_name)
        try:
            for attr in parts[index:]:
                target = getattr(target, attr)
        except AttributeError as exc:
            raise ValueError(
                f"semantic invariant validator '{validator}' does not resolve to an importable object"
            ) from exc
        return target
    raise ValueError(f"semantic invariant validator '{validator}' does not resolve to an importable object")


def _validate_aces_semantic_invariant_annotations(
    *,
    contract_id: str,
    json_schema: dict[str, Any],
    known_contract_ids: frozenset[str],
) -> None:
    invariant_entries = _iter_aces_semantic_invariant_entries(json_schema)
    profile_payload = json_schema.get("x-aces-semantic-profile")
    if not invariant_entries:
        if profile_payload is not None:
            raise ValueError(f"schema '{contract_id}' declares a semantic profile without semantic invariants")
        return

    profile = AcesSemanticInvariantProfileReferenceModel.model_validate(profile_payload)
    if profile.contract_id != contract_id:
        raise ValueError(f"schema '{contract_id}' semantic profile contract_id must match the published contract id")

    seen_invariant_ids: set[str] = set()
    for invariant_payload in invariant_entries:
        invariant = AcesSemanticInvariantEntryModel.model_validate(invariant_payload)
        if not callable(_resolve_semantic_validator(invariant.validator)):
            raise ValueError(f"semantic invariant validator '{invariant.validator}' must resolve to a callable")
        if invariant.id in seen_invariant_ids:
            raise ValueError(f"schema '{contract_id}' has duplicate semantic invariant id '{invariant.id}'")
        seen_invariant_ids.add(invariant.id)
        for invariant_input in invariant.inputs:
            if invariant_input.contract_id not in known_contract_ids:
                raise ValueError(
                    f"semantic invariant '{invariant.id}' references unknown input contract "
                    f"'{invariant_input.contract_id}'"
                )


def validate_aces_semantic_invariant_annotations(contract_id: str, json_schema: dict[str, Any]) -> None:
    """Validate ACES semantic-invariant metadata on a published JSON Schema."""

    known_contract_ids = frozenset(schema_bundle())
    _validate_aces_semantic_invariant_annotations(
        contract_id=contract_id,
        json_schema=json_schema,
        known_contract_ids=known_contract_ids,
    )


class InstantiationRequestModel(ContractModel):
    schema_version: Literal[SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION] = (
        SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION
    )
    profile: str | None = None
    parameters: dict[str, Any] = Field(default_factory=dict)


class WorkflowStepStateModel(ContractModel):
    lifecycle: str
    outcome: str | None = None
    attempts: int


class WorkflowExecutionStateModel(ContractModel):
    state_schema_version: Literal[WORKFLOW_STATE_SCHEMA_VERSION] = WORKFLOW_STATE_SCHEMA_VERSION
    workflow_status: str
    run_id: str
    started_at: str
    updated_at: str
    terminal_reason: str | None = None
    compensation_status: str
    compensation_started_at: str | None = None
    compensation_updated_at: str | None = None
    compensation_failures: list[dict[str, Any]] = Field(default_factory=list)
    steps: dict[str, WorkflowStepStateModel] = Field(default_factory=dict)


class WorkflowHistoryEventModel(ContractModel):
    event_type: str
    timestamp: str
    step_name: str | None = None
    branch_name: str | None = None
    join_step: str | None = None
    outcome: str | None = None
    details: dict[str, Any] = Field(default_factory=dict)


class WorkflowCancellationRequestModel(ContractModel):
    schema_version: Literal[WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION] = WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION
    run_id: str | None = None
    reason: str = "cancelled by operator"


class EvaluationResultStateModel(ContractModel):
    state_schema_version: Literal[EVALUATION_STATE_SCHEMA_VERSION] = EVALUATION_STATE_SCHEMA_VERSION
    resource_type: str
    run_id: str
    status: str
    observed_at: str
    updated_at: str
    passed: bool | None = None
    score: float | int | None = None
    max_score: int | None = None
    detail: str | None = None
    evidence_refs: list[str] = Field(default_factory=list)


class EvaluationHistoryEventModel(ContractModel):
    event_type: str
    timestamp: str
    status: str
    passed: bool | None = None
    score: float | int | None = None
    max_score: int | None = None
    detail: str | None = None
    evidence_refs: list[str] = Field(default_factory=list)
    details: dict[str, Any] = Field(default_factory=dict)


class PropositionTruthOutcome(str, Enum):
    """Portable proposition-evaluation outcome domain.

    ``unsupported`` is a realization disposition included in the portable
    envelope, not a logical truth value.
    """

    TRUE = "true"
    FALSE = "false"
    UNKNOWN = "unknown"
    UNSUPPORTED = "unsupported"


class PropositionEvaluationBasis(str, Enum):
    DECLARED_STATE = "declared_state"
    OBSERVED_STATE = "observed_state"


class PropositionAssertionPolarity(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"


class PropositionIndeterminacyReason(str, Enum):
    MISSING_EVIDENCE = "missing_evidence"
    STALE_EVIDENCE = "stale_evidence"
    PARTIAL_EVIDENCE = "partial_evidence"
    CONFLICTING_EVIDENCE = "conflicting_evidence"
    REDACTED_EVIDENCE = "redacted_evidence"
    LOSSY_EVIDENCE = "lossy_evidence"
    PROBE_FAILURE = "probe_failure"


class PropositionLossKind(str, Enum):
    MISSING = "missing"
    STALE = "stale"
    PARTIAL = "partial"
    CONFLICTING = "conflicting"
    REDACTED = "redacted"
    LOSSY = "lossy"
    PROBE_FAILURE = "probe_failure"


class PropositionProbeBindingModel(ContractModel):
    """Provenance for one capability-bound proposition realization."""

    binding_id: NonEmptyString
    implementation_id: NonEmptyString
    implementation_version: NonEmptyString
    artifact_digest: Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")]
    backend_manifest_ref: NonEmptyString
    proposition_address: NonEmptyString
    capability_refs: list[NonEmptyString] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_unique_capabilities(self) -> PropositionProbeBindingModel:
        if len(set(self.capability_refs)) != len(self.capability_refs):
            raise ValueError("probe binding capability_refs must be unique")
        return self


class PropositionTemporalContextModel(ContractModel):
    """Governed boundary and clock context used for one evaluation."""

    boundary_ref: NonEmptyString
    time_domain: NonEmptyString
    clock_authority: NonEmptyString


class PropositionLossDisclosureModel(ContractModel):
    kind: PropositionLossKind
    within_admissible_bound: bool


class PropositionTruthResultModel(ContractModel):
    """Evidence-bearing truth result separate from evaluator lifecycle state."""

    schema_version: Literal[PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION] = PROPOSITION_TRUTH_RESULT_SCHEMA_VERSION
    result_id: NonEmptyString
    proposition_address: NonEmptyString
    assertion_address: NonEmptyString
    assertion_polarity: PropositionAssertionPolarity
    proposition_outcome: PropositionTruthOutcome
    assertion_outcome: PropositionTruthOutcome
    evaluation_basis: PropositionEvaluationBasis
    indeterminacy_reason: PropositionIndeterminacyReason | None = None
    probe_binding: PropositionProbeBindingModel | None = None
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    declared_artifact_digest: Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")] | None = None
    temporal_context: PropositionTemporalContextModel | None = None
    loss_disclosures: list[PropositionLossDisclosureModel] = Field(default_factory=list)
    unsupported_capability_refs: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_truth_result(self) -> PropositionTruthResultModel:
        self._validate_unique_refs()
        self._validate_polarity()
        self._validate_outcome_explanation()
        self._validate_basis_evidence()
        self._validate_loss_bounds()
        return self

    def _validate_unique_refs(self) -> None:
        for field_name in ("evidence_refs", "unsupported_capability_refs"):
            refs = getattr(self, field_name)
            if len(set(refs)) != len(refs):
                raise ValueError(f"{field_name} must be unique")

    def _validate_polarity(self) -> None:
        expected = self.proposition_outcome
        if self.assertion_polarity is PropositionAssertionPolarity.NEGATIVE:
            if expected is PropositionTruthOutcome.TRUE:
                expected = PropositionTruthOutcome.FALSE
            elif expected is PropositionTruthOutcome.FALSE:
                expected = PropositionTruthOutcome.TRUE
        if self.assertion_outcome is not expected:
            raise ValueError(
                f"{self.assertion_polarity.value} assertion outcome must preserve or invert the proposition outcome"
            )

    def _validate_outcome_explanation(self) -> None:
        if self.proposition_outcome is PropositionTruthOutcome.UNKNOWN:
            if self.indeterminacy_reason is None:
                raise ValueError("unknown proposition outcome requires indeterminacy_reason")
        elif self.indeterminacy_reason is not None:
            raise ValueError("indeterminacy_reason is valid only for unknown outcomes")
        if self.proposition_outcome is PropositionTruthOutcome.UNSUPPORTED:
            if not self.unsupported_capability_refs:
                raise ValueError("unsupported outcome requires unsupported_capability_refs")
        elif self.unsupported_capability_refs:
            raise ValueError("unsupported_capability_refs are valid only for unsupported outcomes")

    def _validate_basis_evidence(self) -> None:
        decided = self.proposition_outcome in {PropositionTruthOutcome.TRUE, PropositionTruthOutcome.FALSE}
        if decided and self.evaluation_basis is PropositionEvaluationBasis.OBSERVED_STATE:
            self._validate_observed_evidence()
        if self.probe_binding is not None and self.probe_binding.proposition_address != self.proposition_address:
            raise ValueError("probe_binding proposition_address must match proposition_address")
        if (
            decided
            and self.evaluation_basis is PropositionEvaluationBasis.DECLARED_STATE
            and self.declared_artifact_digest is None
        ):
            raise ValueError("declared-state decided truth requires declared_artifact_digest")

    def _validate_observed_evidence(self) -> None:
        if self.probe_binding is None:
            raise ValueError("observed-state decided truth requires probe_binding")
        if not self.evidence_refs:
            raise ValueError("observed-state decided truth requires evidence_refs")
        if self.temporal_context is None:
            raise ValueError("observed-state decided truth requires temporal_context")

    def _validate_loss_bounds(self) -> None:
        decided = self.proposition_outcome in {PropositionTruthOutcome.TRUE, PropositionTruthOutcome.FALSE}
        if decided and any(not disclosure.within_admissible_bound for disclosure in self.loss_disclosures):
            raise ValueError("lossy or partial evidence outside the admitted bound cannot decide truth")

    def semantic_claim(self) -> tuple[object, ...]:
        """Return the backend-independent portion used for equivalence checks."""

        return (
            self.proposition_address,
            self.assertion_address,
            self.assertion_polarity,
            self.proposition_outcome,
            self.assertion_outcome,
            self.evaluation_basis,
            self.indeterminacy_reason,
            self.temporal_context,
            tuple(self.loss_disclosures),
            tuple(self.unsupported_capability_refs),
        )

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        schema = handler(core_schema)
        schema = handler.resolve_ref_schema(schema)
        decided = {"enum": ["true", "false"]}
        loss_items = deepcopy(schema["properties"]["loss_disclosures"]["items"])
        loss_items["properties"] = {"within_admissible_bound": {"const": True}}
        loss_items["required"] = ["within_admissible_bound"]
        schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {
                            "evaluation_basis": {"const": "observed_state"},
                            "proposition_outcome": decided,
                        },
                        "required": ["evaluation_basis", "proposition_outcome"],
                    },
                    "then": {
                        "properties": {
                            "probe_binding": {"not": {"type": "null"}},
                            "evidence_refs": {"minItems": 1},
                            "temporal_context": {"not": {"type": "null"}},
                        }
                    },
                },
                {
                    "if": {
                        "properties": {"proposition_outcome": {"const": "unknown"}},
                        "required": ["proposition_outcome"],
                    },
                    "then": {"properties": {"indeterminacy_reason": {"not": {"type": "null"}}}},
                    "else": {"properties": {"indeterminacy_reason": {"type": "null"}}},
                },
                {
                    "if": {
                        "properties": {"proposition_outcome": {"const": "unsupported"}},
                        "required": ["proposition_outcome"],
                    },
                    "then": {"properties": {"unsupported_capability_refs": {"minItems": 1}}},
                    "else": {"properties": {"unsupported_capability_refs": {"maxItems": 0}}},
                },
                {
                    "if": {
                        "properties": {"proposition_outcome": decided},
                        "required": ["proposition_outcome"],
                    },
                    "then": {"properties": {"loss_disclosures": {"items": loss_items}}},
                },
            ]
        )
        return schema


class ParticipantEpisodeStateModel(ContractModel):
    state_schema_version: Literal[PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION] = PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION
    participant_address: str
    episode_id: str
    sequence_number: int
    status: str
    terminal_reason: str | None = None
    initialized_at: str
    updated_at: str
    terminated_at: str | None = None
    last_control_action: str
    previous_episode_id: str | None = None


class ParticipantEpisodeHistoryEventModel(ContractModel):
    event_type: str
    timestamp: str
    participant_address: str
    episode_id: str
    sequence_number: int
    terminal_reason: str | None = None
    control_action: str | None = None
    details: dict[str, Any] = Field(default_factory=dict)


class ParticipantObservationDetailsModel(ContractModel):
    visible_refs: list[NonEmptyString] = Field(default_factory=list)
    disclosed_refs: list[NonEmptyString] = Field(default_factory=list)
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)


class ParticipantActionPreconditionResultModel(ContractModel):
    precondition_id: NonEmptyString
    precondition_class: ParticipantPreconditionClass
    status: Literal["satisfied", "unsatisfied", "unresolved"]
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    action_contract_address: NonEmptyString
    observation_point: NonEmptyString
    support_refs: list[NonEmptyString] = Field(default_factory=list)
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantActionEffectResultModel(ContractModel):
    effect_id: NonEmptyString
    effect_class: ParticipantEffectClass
    description: NonEmptyString
    target_refs: list[NonEmptyString] = Field(default_factory=list)
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantActionResultModel(ContractModel):
    status: Literal["accepted", "rejected", "withheld", "succeeded", "failed", "partial_success", "unknown"]
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    action_instance_id: NonEmptyString
    action_contract_address: NonEmptyString
    observation_point: NonEmptyString
    preconditions: list[ParticipantActionPreconditionResultModel] = Field(default_factory=list)
    effects: list[ParticipantActionEffectResultModel] = Field(default_factory=list)
    failure_class: ParticipantFailureClass | None = None
    observations: list[NonEmptyString] = Field(default_factory=list)
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantTemporalRuntimeContextModel(ContractModel):
    temporal_contract_id: NonEmptyString
    time_domain: ParticipantTimeDomain
    clock_authority: NonEmptyString
    event_points: list[ParticipantTemporalEventPoint] = Field(min_length=1)
    observation_point: NonEmptyString
    backend_disclosure_refs: list[NonEmptyString] = Field(default_factory=list)
    reset_boundary: NonEmptyString | None = None
    replay_boundary: NonEmptyString | None = None


class ParticipantAttributionCandidateModel(ContractModel):
    candidate_kind: ParticipantAttributionCandidateKind
    ref: NonEmptyString
    description: NonEmptyString


class ParticipantAttributionOrderingBasisModel(ContractModel):
    basis_kind: ParticipantAttributionOrderingBasisKind
    relation_ref: NonEmptyString
    description: NonEmptyString
    ordered_event_refs: list[NonEmptyString] = Field(default_factory=list)


class ParticipantAttributionEvidenceBasisModel(ContractModel):
    capture_apparatus: NonEmptyString
    granularity: NonEmptyString
    loss_model: NonEmptyString
    redaction_policy: NonEmptyString
    observer_effects: list[NonEmptyString] = Field(min_length=1)


class ParticipantAttributionEdgeModel(ContractModel):
    edge_id: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    observation_point: NonEmptyString
    cause_candidate: ParticipantAttributionCandidateModel
    effect_candidate: ParticipantAttributionCandidateModel
    ordering_basis: ParticipantAttributionOrderingBasisModel
    evidence_basis: ParticipantAttributionEvidenceBasisModel
    support_class: ParticipantAttributionSupportClass
    confidence: NonEmptyString
    strength: NonEmptyString
    limitations: list[NonEmptyString] = Field(min_length=1)
    evidence_refs: list[NonEmptyString] = Field(min_length=1)
    interpretation_rule_ref: NonEmptyString | None = None


class ParticipantOutcomeSourceRecordModel(ContractModel):
    source_id: NonEmptyString
    source_layer: OutcomeInterpretationSourceLayer
    ref: NonEmptyString
    observed_value: NonEmptyString
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    provenance_refs: list[NonEmptyString] = Field(default_factory=list)
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantOutcomeTargetRecordModel(ContractModel):
    target_id: NonEmptyString
    target_layer: OutcomeInterpretationTargetLayer
    ref: NonEmptyString
    interpreted_value: NonEmptyString
    evidence_refs: list[NonEmptyString] = Field(min_length=1)
    limitations: list[NonEmptyString] = Field(min_length=1)
    governance_ref: NonEmptyString | None = None
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantOutcomeInterpretationRecordModel(ContractModel):
    interpretation_id: NonEmptyString
    rule_address: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    observation_point: NonEmptyString
    source_bindings: list[ParticipantOutcomeSourceRecordModel] = Field(min_length=1)
    target_bindings: list[ParticipantOutcomeTargetRecordModel] = Field(min_length=1)
    evidence_refs: list[NonEmptyString] = Field(min_length=1)
    limitations: list[NonEmptyString] = Field(min_length=1)
    diagnostics: list[NonEmptyString] = Field(default_factory=list)


class ParticipantBehaviorHistoryEventModel(ContractModel):
    event_type: ParticipantBehaviorHistoryEventType
    timestamp: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    action_instance_id: NonEmptyString
    action_contract_address: NonEmptyString | None = None
    observation_boundary_address: NonEmptyString | None = None
    observation_status: ParticipantObservationStatus | None = None
    actor_provenance: NonEmptyString | None = None
    lifecycle_phase: ParticipantRuntimeLifecyclePhase | None = None
    phase_realization: ParticipantPhaseRealization | None = None
    admission_disposition: ParticipantAdmissionDisposition | None = None
    operation_ref: NonEmptyString | None = None
    operation_state: ParticipantLifecycleOperationState | None = None
    state_transition_kind: NonEmptyString | None = None
    post_state_digest: NonEmptyString | None = None
    joint_action_set_id: NonEmptyString | None = None
    realized_order: StrictInt | None = Field(default=None, ge=0)
    interaction_class: ParticipantInteractionClass | None = None
    interaction_ref: NonEmptyString | None = None
    shared_state_refs: list[NonEmptyString] = Field(default_factory=list)
    action_result: ParticipantActionResultModel | None = None
    attribution_edges: list[ParticipantAttributionEdgeModel] = Field(default_factory=list)
    outcome_interpretations: list[ParticipantOutcomeInterpretationRecordModel] = Field(default_factory=list)
    temporal_contexts: list[ParticipantTemporalRuntimeContextModel] = Field(default_factory=list)
    details: ParticipantObservationDetailsModel = Field(default_factory=ParticipantObservationDetailsModel)

    @model_validator(mode="after")
    def _validate_lifecycle_fields(self) -> ParticipantBehaviorHistoryEventModel:
        messages = participant_lifecycle_field_violation_messages(
            event_type=self.event_type,
            lifecycle_phase=self.lifecycle_phase,
            phase_realization=self.phase_realization,
            admission_disposition=self.admission_disposition,
            operation_ref=self.operation_ref,
            operation_state=self.operation_state,
        )
        if messages:
            raise ValueError(messages[0])
        return self


ParticipantRuntimeOrderingBasis = Literal[
    "total_order",
    "partial_order",
    "simultaneous",
    "serialized_backend_order",
    "simulation_tick",
    "control_plane_order",
    "logical_clock",
    "vector_clock",
    "wall_clock_only",
    "unknown",
    "unsupported",
]
ParticipantRuntimeMappingLoss = Literal[
    "none",
    "private_apparatus_detail",
    "source_fields_omitted",
    "semantics_approximated",
    "redacted_by_policy",
    "temporal_detail_collapsed",
    "unknown",
    "unsupported",
]
ParticipantRuntimeInformationGuarantee = Literal[
    "observation_only",
    "history_consistent",
    "perfect_recall",
    "lossy_projection",
    "unknown",
    "unsupported",
]
ParticipantRuntimeDeliveryBasis = Literal[
    "emission_is_delivery",
    "runtime_delivery",
    "participant_acknowledgement",
    "external_delivery",
    "unknown",
    "unsupported",
]
ParticipantRuntimeConflictPolicy = Literal[
    "coordinate",
    "serialize",
    "reject",
    "retry",
    "withhold",
    "merge",
    "rollback",
    "disclose_weak_guarantee",
    "unsupported",
]
ParticipantRuntimeConflictClass = Literal["none", "read_write", "write_write", "unsupported"]
ParticipantRuntimeJointActionConflictPolicy = Literal[
    "none",
    "coordinate",
    "serialize",
    "reject",
    "retry",
    "withhold",
    "merge",
    "rollback",
    "disclose_weak_guarantee",
    "unsupported",
]
ParticipantRuntimeIsolationGuarantee = Literal["none", "serializable", "snapshot", "causal", "unsupported"]
ParticipantRuntimeAtomicityScope = Literal["single_object", "multi_object", "coordination_interval", "unsupported"]
ParticipantRuntimeTimeManagementMode = Literal[
    "display",
    "pacing",
    "lookahead",
    "rollback",
    "devs",
    "fmi",
    "backend_serialized",
    "unsupported",
]
ParticipantRuntimeTimeClaimStrength = Literal["display", "bounded", "exact", "unsupported"]


class EventClassificationModel(ContractModel):
    """ACES-native normalized event classification tuple (ADR-054)."""

    category_uid: int
    category_name: NonEmptyString
    class_uid: int
    class_name: NonEmptyString
    activity_id: int
    activity_name: NonEmptyString
    type_uid: int
    type_name: NonEmptyString
    severity_id: int
    severity: NonEmptyString


class SourceStatusModel(ContractModel):
    """Normalized source status claim for one participant runtime record."""

    status_id: int
    status: NonEmptyString
    status_code: NonEmptyString
    status_detail: NonEmptyString
    source_status_label: NonEmptyString
    source_status_mapping: NonEmptyString


class SourcePipelineModel(ContractModel):
    """Source product, identity, and pipeline-time facts for a mapped record."""

    product_ref: NonEmptyString
    product_version: NonEmptyString | None = None
    log_provider: NonEmptyString | None = None
    log_source: NonEmptyString | None = None
    log_name: NonEmptyString | None = None
    original_event_uid: NonEmptyString | None = None
    original_time: Rfc3339DateTimeString | None = None
    processed_time: Rfc3339DateTimeString | None = None
    logged_time: Rfc3339DateTimeString | None = None
    transmit_time: Rfc3339DateTimeString | None = None
    correlation_uid: NonEmptyString | None = None
    sequence: NonNegativeInteger | None = None


class RawDataIntegrityModel(ContractModel):
    """Hash, size, and truncation facts for raw data behind a runtime claim."""

    raw_data_hash: PrefixedDigestString | None = None
    raw_data_hash_algorithm: NonEmptyString | None = None
    raw_data_size: NonNegativeInteger | None = None
    raw_data_is_truncated: bool | None = None
    raw_data_untruncated_size: NonNegativeInteger | None = None


class ParticipantRuntimeBaseEnvelopeModel(ContractModel):
    """Shared ADR-054 base envelope for participant-runtime family carriers.

    Every published carrier in the ``participant-runtime`` family embeds this
    envelope exactly once: identity, classification and source status,
    participant/episode scoping, the three distinct timestamps with clock
    authority, ordering, actor/source refs, raw-data integrity, confidence,
    provenance/evidence refs, and the marking surface. No carrier redefines
    local identity, versioning, marking, or extension semantics.
    """

    event_id: NonEmptyString
    schema_name: NonEmptyString
    schema_version: NonEmptyString
    event_type: NonEmptyString
    extension_policy: NonEmptyString
    event_classification: EventClassificationModel | None = None
    source_status: SourceStatusModel | None = None
    participant_address: NonEmptyString | None = None
    episode_id: NonEmptyString | None = None
    sequence_number: NonNegativeInteger | None = None
    occurred_at: Rfc3339DateTimeString
    recorded_at: Rfc3339DateTimeString
    ingested_at: Rfc3339DateTimeString
    clock_authority: NonEmptyString
    temporal_context: NonEmptyString | None = None
    ordering_basis: ParticipantRuntimeOrderingBasis
    logical_order_ref: NonEmptyString | None = None
    predecessor_event_refs: list[NonEmptyString] = Field(default_factory=list)
    actor_ref: NonEmptyString
    producer_ref: NonEmptyString
    source_system_ref: NonEmptyString | None = None
    source_record_ref: NonEmptyString | None = None
    source_raw_ref: NonEmptyString | None = None
    source_pipeline: SourcePipelineModel | None = None
    raw_data_integrity: RawDataIntegrityModel | None = None
    confidence: ClosedUnitIntervalFloat | None = None
    provenance_refs: list[NonEmptyString] = Field(default_factory=list)
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)
    marking_definition_refs: list[NonEmptyString] = Field(default_factory=list)
    object_marking_refs: list[NonEmptyString] = Field(default_factory=list)
    markings: list[NonEmptyString] = Field(default_factory=list)
    granular_markings: dict[NonEmptyString, list[NonEmptyString]] = Field(default_factory=dict)
    redaction_policy_ref: NonEmptyString | None = None
    authorization_scope: NonEmptyString


class ParticipantLifecycleEventModel(ParticipantRuntimeBaseEnvelopeModel):
    """RUN-306 lifecycle boundary record for one participant action event."""

    phase: ParticipantRuntimeLifecyclePhase
    phase_realization: ParticipantPhaseRealization
    admission_disposition: ParticipantAdmissionDisposition | None = None
    operation_ref: NonEmptyString | None = None
    action_ref: NonEmptyString | None = None
    action_contract_ref: NonEmptyString | None = None
    command_ref: NonEmptyString | None = None
    actor_provenance: NonEmptyString
    action_validity_basis_ref: NonEmptyString | None = None
    observation_refs: list[NonEmptyString] = Field(default_factory=list)
    shared_state_read_refs: list[NonEmptyString] = Field(default_factory=list)
    shared_state_write_refs: list[NonEmptyString] = Field(default_factory=list)
    emitted_state_update_refs: list[NonEmptyString] = Field(default_factory=list)
    attribution_edge_refs: list[NonEmptyString] = Field(default_factory=list)
    outcome_interpretation_refs: list[NonEmptyString] = Field(default_factory=list)
    joint_action_set_ref: NonEmptyString | None = None
    source_status_label: NonEmptyString | None = None
    mapping_loss: ParticipantRuntimeMappingLoss | None = None
    mapping_loss_detail: NonEmptyString | None = None


class ParticipantObservationLossDescriptorModel(ContractModel):
    """Declared projection-loss facts for one participant-visible observation."""

    kind: NonEmptyString
    fields_redacted: list[NonEmptyString] = Field(default_factory=list)


class ParticipantObservationStochasticContextModel(ContractModel):
    """Seed and randomization-policy references behind one observation."""

    seed_ref: NonEmptyString | None = None
    randomization_policy_ref: NonEmptyString | None = None


class ParticipantObservationEnvelopeModel(ParticipantRuntimeBaseEnvelopeModel):
    """SEM-210 participant-visible observation record with explicit guarantees."""

    observation_ref: NonEmptyString
    phase_ref: NonEmptyString | None = None
    visibility_projection_ref: NonEmptyString
    information_guarantee: ParticipantRuntimeInformationGuarantee
    delivery_basis: ParticipantRuntimeDeliveryBasis
    delivery_point_ref: NonEmptyString | None = None
    delivered_at: Rfc3339DateTimeString | None = None
    action_observation_history_ref: NonEmptyString | None = None
    information_state_ref: NonEmptyString | None = None
    hidden_state_refs: list[NonEmptyString] = Field(default_factory=list)
    centralized_state_refs: list[NonEmptyString] = Field(default_factory=list)
    loss_descriptor: ParticipantObservationLossDescriptorModel | None = None
    stochastic_context: ParticipantObservationStochasticContextModel | None = None
    noise_model_ref: NonEmptyString | None = None
    reconstruction_algorithm_ref: NonEmptyString | None = None
    reconstruction_proof_ref: NonEmptyString | None = None
    belief_support_ref: NonEmptyString | None = None
    redacted_field_refs: list[NonEmptyString] = Field(default_factory=list)


class ParticipantSharedStateAccessModel(ContractModel):
    """RUN-307 read/write access record over one shared-state address."""

    state_address: NonEmptyString
    access_kind: Literal["read", "write", "read_write"]
    read_revision: NonEmptyString | None = None
    write_revision: NonEmptyString | None = None
    read_digest: PrefixedDigestString | None = None
    write_digest: PrefixedDigestString | None = None
    snapshot_ref: NonEmptyString | None = None
    access_purpose: NonEmptyString
    atomic_group_ref: NonEmptyString | None = None
    evidence_refs: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_revision_markers(self) -> ParticipantSharedStateAccessModel:
        if self.access_kind in {"read", "read_write"} and self.read_revision is None and self.read_digest is None:
            raise ValueError("shared state read access requires read_revision or read_digest")
        if self.access_kind in {"write", "read_write"} and self.write_revision is None and self.write_digest is None:
            raise ValueError("shared state write access requires write_revision or write_digest")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {"access_kind": {"enum": ["read", "read_write"]}},
                        "required": ["access_kind"],
                    },
                    "then": {
                        "anyOf": [
                            {"required": ["read_revision"], "properties": {"read_revision": {"type": "string"}}},
                            {"required": ["read_digest"], "properties": {"read_digest": {"type": "string"}}},
                        ]
                    },
                },
                {
                    "if": {
                        "properties": {"access_kind": {"enum": ["write", "read_write"]}},
                        "required": ["access_kind"],
                    },
                    "then": {
                        "anyOf": [
                            {"required": ["write_revision"], "properties": {"write_revision": {"type": "string"}}},
                            {"required": ["write_digest"], "properties": {"write_digest": {"type": "string"}}},
                        ]
                    },
                },
            ]
        )
        return json_schema


class ParticipantSharedStateRecordModel(ParticipantRuntimeBaseEnvelopeModel):
    """RUN-307 versioned shared operational state-change report."""

    state_address: NonEmptyString
    state_scope: NonEmptyString
    state_kind: NonEmptyString
    revision: NonEmptyString | None = None
    digest: PrefixedDigestString | None = None
    predecessor_revision_refs: list[NonEmptyString] = Field(default_factory=list)
    conflict_policy: ParticipantRuntimeConflictPolicy
    visibility_projection_basis: NonEmptyString | None = None
    provenance: NonEmptyString
    value_ref: NonEmptyString | None = None
    accesses: list[ParticipantSharedStateAccessModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_revision_marker(self) -> ParticipantSharedStateRecordModel:
        if self.revision is None and self.digest is None:
            raise ValueError("participant shared state record requires revision or digest")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["revision"], "properties": {"revision": {"type": "string"}}},
                {"required": ["digest"], "properties": {"digest": {"type": "string"}}},
            ]
        )
        return json_schema


class ParticipantJointActionAccessSetModel(ContractModel):
    """Read/write footprint for one member event in a joint action record."""

    member_event_ref: NonEmptyString
    shared_state_read_refs: list[NonEmptyString] = Field(default_factory=list)
    shared_state_write_refs: list[NonEmptyString] = Field(default_factory=list)
    exclusive_resource_refs: list[NonEmptyString] = Field(default_factory=list)
    visibility_effect_refs: list[NonEmptyString] = Field(default_factory=list)
    evidence_stream_refs: list[NonEmptyString] = Field(default_factory=list)


def _exact_string_permutation(values: list[str], expected: set[str]) -> bool:
    return len(values) == len(expected) and set(values) == expected


def _joint_action_actual_conflict(access_sets: list[ParticipantJointActionAccessSetModel]) -> str:
    read_write_conflict = False
    for left_index, left in enumerate(access_sets):
        left_reads = set(left.shared_state_read_refs)
        left_writes = set(left.shared_state_write_refs) | {f"resource:{ref}" for ref in left.exclusive_resource_refs}
        for right in access_sets[left_index + 1 :]:
            right_reads = set(right.shared_state_read_refs)
            right_writes = set(right.shared_state_write_refs) | {
                f"resource:{ref}" for ref in right.exclusive_resource_refs
            }
            if left_writes & right_writes:
                return "write_write"
            if (left_writes & right_reads) or (left_reads & right_writes):
                read_write_conflict = True
    return "read_write" if read_write_conflict else "none"


def _joint_action_member_ref_set(member_event_refs: list[str]) -> set[str]:
    member_refs = list(member_event_refs)
    member_ref_set = set(member_refs)
    if len(member_refs) != len(member_ref_set):
        raise ValueError("joint action member_event_refs must be unique")
    return member_ref_set


def _validate_joint_action_members(record: Any, member_ref_set: set[str]) -> None:
    access_event_refs = [access.member_event_ref for access in record.access_sets]
    if not _exact_string_permutation(access_event_refs, member_ref_set):
        raise ValueError("joint action access_sets must cover member_event_refs exactly once")
    if record.realized_order and not _exact_string_permutation(record.realized_order, member_ref_set):
        raise ValueError("joint action realized_order must be an exact permutation of member_event_refs")


def _validate_joint_action_disclosure(record: Any) -> None:
    if record.unsupported_disclosure and record.exact_concurrency_claim:
        raise ValueError("unsupported concurrency disclosure cannot carry an exact concurrency claim")
    if record.exact_concurrency_claim and record.time_management_context_ref is None:
        raise ValueError("exact concurrency claims require time_management_context_ref")


def _joint_action_unsupported_policy_applies(record: Any) -> bool:
    if record.conflict_policy != "unsupported":
        return False
    if not record.unsupported_disclosure or record.exact_concurrency_claim:
        raise ValueError("unsupported conflict_policy requires unsupported_disclosure and no exact claim")
    return True


def _validate_joint_action_conflict(record: Any, actual_conflict: str) -> None:
    if not record.unsupported_disclosure and record.conflict_class != actual_conflict:
        raise ValueError("joint action conflict_class must match declared access-set conflicts")
    if record.conflict_class == "none" and actual_conflict != "none":
        raise ValueError("joint action conflict_class cannot be none when access sets conflict")
    _validate_joint_action_conflict_policy(record, actual_conflict)
    _validate_joint_action_atomicity(record, actual_conflict)


def _validate_joint_action_conflict_policy(record: Any, actual_conflict: str) -> None:
    if record.isolation_guarantee == "serializable" and not record.realized_order:
        raise ValueError("serializable joint action isolation requires realized_order")
    if record.conflict_policy == "serialize" and not record.realized_order:
        raise ValueError("serialize conflict_policy requires realized_order")
    if record.conflict_policy == "retry" and (record.retry_limit is None or not record.rollback_event_refs):
        raise ValueError("retry conflict_policy requires retry_limit and rollback_event_refs")
    if record.conflict_policy == "none" and actual_conflict != "none":
        raise ValueError("none conflict_policy is only valid when access sets do not conflict")


def _validate_joint_action_atomicity(record: Any, actual_conflict: str) -> None:
    has_recovery_evidence = bool(record.realized_order or record.rollback_event_refs)
    if record.atomicity_scope == "multi_object" and actual_conflict != "none" and not has_recovery_evidence:
        raise ValueError("multi_object conflicting joint actions require realized_order or rollback_event_refs")


class ParticipantJointActionRecordModel(ParticipantRuntimeBaseEnvelopeModel):
    """RUN-308 joint action / concurrency record over behavior events."""

    joint_action_set_id: NonEmptyString
    member_event_refs: list[NonEmptyString] = Field(min_length=1)
    access_sets: list[ParticipantJointActionAccessSetModel] = Field(min_length=1)
    conflict_class: ParticipantRuntimeConflictClass
    conflict_policy: ParticipantRuntimeJointActionConflictPolicy
    isolation_guarantee: ParticipantRuntimeIsolationGuarantee
    atomicity_scope: ParticipantRuntimeAtomicityScope
    realized_order: list[NonEmptyString] = Field(default_factory=list)
    simultaneity_group_ref: NonEmptyString | None = None
    time_management_context_ref: NonEmptyString | None = None
    participant_observation_refs: list[NonEmptyString] = Field(default_factory=list)
    rollback_event_refs: list[NonEmptyString] = Field(default_factory=list)
    retry_limit: NonNegativeInteger | None = None
    timeout_policy_ref: NonEmptyString | None = None
    fairness_policy_ref: NonEmptyString | None = None
    unsupported_disclosure: bool = False
    exact_concurrency_claim: bool = False

    @model_validator(mode="after")
    def _validate_joint_action_record(self) -> ParticipantJointActionRecordModel:
        member_ref_set = _joint_action_member_ref_set(self.member_event_refs)
        _validate_joint_action_members(self, member_ref_set)
        _validate_joint_action_disclosure(self)
        if _joint_action_unsupported_policy_applies(self):
            return self

        actual_conflict = _joint_action_actual_conflict(self.access_sets)
        _validate_joint_action_conflict(self, actual_conflict)
        return self


def _validate_time_management_claim(context: Any) -> None:
    if context.unsupported_disclosure and context.claim_strength == "exact":
        raise ValueError("unsupported time-management disclosure cannot carry an exact claim")
    if context.basis == "wall_clock_only" and context.claim_strength != "display":
        raise ValueError("wall_clock_only time basis supports display claims only")
    if context.claim_strength in {"bounded", "exact"} and context.clock_ref is None:
        raise ValueError("bounded or exact time-management claims require clock_ref")


def _validate_time_management_mode(context: Any) -> None:
    if context.mode == "backend_serialized":
        _validate_backend_serialized_time_management(context)
    if context.mode == "lookahead" and context.lookahead is None:
        raise ValueError("lookahead mode requires lookahead")
    if context.mode == "pacing" and context.advance_by is None:
        raise ValueError("pacing mode requires advance_by")
    if context.mode == "rollback" and not context.rollback_event_refs:
        raise ValueError("rollback mode requires rollback_event_refs")
    if context.mode in {"devs", "fmi"} and (context.clock_ref is None or context.basis == "wall_clock_only"):
        raise ValueError("devs and fmi modes require a non-wall-clock basis and clock_ref")
    if context.mode == "unsupported" and not context.unsupported_disclosure:
        raise ValueError("unsupported time-management mode requires unsupported_disclosure")


def _validate_backend_serialized_time_management(context: Any) -> None:
    if not context.backend_serialized or context.basis != "serialized_backend_order" or context.clock_ref is None:
        raise ValueError("backend_serialized mode requires serialized_backend_order basis and clock_ref")


class ParticipantTimeManagementContextModel(ParticipantRuntimeBaseEnvelopeModel):
    """RUN-308 time-management basis for concurrent or distributed runtime claims."""

    context_id: NonEmptyString
    mode: ParticipantRuntimeTimeManagementMode
    claim_strength: ParticipantRuntimeTimeClaimStrength
    basis: ParticipantRuntimeOrderingBasis
    clock_ref: NonEmptyString | None = None
    lookahead: NonNegativeInteger | None = None
    advance_by: PositiveInteger | None = None
    rollback_event_refs: list[NonEmptyString] = Field(default_factory=list)
    unsupported_disclosure: bool = False
    backend_serialized: bool = False

    @model_validator(mode="after")
    def _validate_time_management_context(self) -> ParticipantTimeManagementContextModel:
        _validate_time_management_claim(self)
        _validate_time_management_mode(self)
        return self


class ParticipantOutcomeReportSourceModel(ContractModel):
    """SEM-215 grounding source for one participant outcome report."""

    source_kind: Literal["action_result", "episode_status", "evidence"]
    source_ref: NonEmptyString


class ParticipantOutcomeReportStateRelationshipModel(ContractModel):
    """Declared relationship between an outcome report and downstream state."""

    relationship_kind: Literal["scenario_state", "workflow_state", "objective_window", "evaluation_input"]
    target_ref: NonEmptyString
    relationship_basis: Literal["declared", "interpretation_rule"]


class ParticipantOutcomeReportModel(ParticipantRuntimeBaseEnvelopeModel):
    """SEM-215 outcome interpretation report.

    The carrier deliberately has no score, reward, or objective-success
    field: reward and return remain ADR-054 step signals, and objective and
    evaluation results remain their own contract surfaces.
    """

    outcome_id: NonEmptyString
    interpretation_rule_ref: NonEmptyString
    outcome_sources: list[ParticipantOutcomeReportSourceModel] = Field(min_length=1)
    state_relationships: list[ParticipantOutcomeReportStateRelationshipModel] = Field(min_length=1)


class ParticipantStatusViewEpisodeStateModel(ContractModel):
    """Scope-projected episode state embedded in API-408 status views.

    The view carries `participant_address` and `episode_id` once at the top
    level; the embedded record cannot restate them, so a nested record scoped
    to another participant or episode is structurally unrepresentable.
    """

    state_schema_version: Literal[PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION] = PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION
    sequence_number: int
    status: str
    terminal_reason: str | None = None
    initialized_at: str
    updated_at: str
    terminated_at: str | None = None
    last_control_action: str
    previous_episode_id: str | None = None


class ParticipantHistoryViewEpisodeEventModel(ContractModel):
    """Scope-projected episode history event embedded in API-408 history views."""

    event_type: str
    timestamp: str
    sequence_number: int
    terminal_reason: str | None = None
    control_action: str | None = None
    details: dict[str, Any] = Field(default_factory=dict)


class ParticipantHistoryViewBehaviorEventModel(ContractModel):
    """Scope-projected behavior history event embedded in API-408 history views."""

    event_type: ParticipantBehaviorHistoryEventType
    timestamp: NonEmptyString
    action_instance_id: NonEmptyString
    action_contract_address: NonEmptyString | None = None
    observation_boundary_address: NonEmptyString | None = None
    observation_status: ParticipantObservationStatus | None = None
    actor_provenance: NonEmptyString | None = None
    lifecycle_phase: ParticipantRuntimeLifecyclePhase | None = None
    phase_realization: ParticipantPhaseRealization | None = None
    admission_disposition: ParticipantAdmissionDisposition | None = None
    operation_ref: NonEmptyString | None = None
    operation_state: ParticipantLifecycleOperationState | None = None
    state_transition_kind: NonEmptyString | None = None
    post_state_digest: NonEmptyString | None = None
    joint_action_set_id: NonEmptyString | None = None
    realized_order: StrictInt | None = Field(default=None, ge=0)
    interaction_class: ParticipantInteractionClass | None = None
    interaction_ref: NonEmptyString | None = None
    shared_state_refs: list[NonEmptyString] = Field(default_factory=list)
    action_result: ParticipantActionResultModel | None = None
    attribution_edges: list[ParticipantAttributionEdgeModel] = Field(default_factory=list)
    outcome_interpretations: list[ParticipantOutcomeInterpretationRecordModel] = Field(default_factory=list)
    temporal_contexts: list[ParticipantTemporalRuntimeContextModel] = Field(default_factory=list)
    details: ParticipantObservationDetailsModel = Field(default_factory=ParticipantObservationDetailsModel)

    @model_validator(mode="after")
    def _validate_lifecycle_fields(self) -> ParticipantHistoryViewBehaviorEventModel:
        messages = participant_lifecycle_field_violation_messages(
            event_type=self.event_type,
            lifecycle_phase=self.lifecycle_phase,
            phase_realization=self.phase_realization,
            admission_disposition=self.admission_disposition,
            operation_ref=self.operation_ref,
            operation_state=self.operation_state,
        )
        if messages:
            raise ValueError(messages[0])
        return self


VIEW_SCOPE_PROJECTED_FIELDS: tuple[str, ...] = ("participant_address", "episode_id")
"""Recorded-contract fields carried once at the view level and removed from
records embedded in API-408 retrieval projections."""


class ParticipantStatusViewModel(ContractModel):
    """API-408 retrieval projection of one participant's episode status."""

    view_id: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString | None = None
    generated_at: Rfc3339DateTimeString
    source_snapshot_ref: NonEmptyString
    episode_state: ParticipantStatusViewEpisodeStateModel | None = None
    open_operation_refs: list[NonEmptyString] = Field(default_factory=list)
    visibility_projection_ref: NonEmptyString
    marking_definition_refs: list[NonEmptyString] = Field(default_factory=list)
    redaction_policy_ref: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_episode_scope(self) -> ParticipantStatusViewModel:
        if self.episode_state is not None and self.episode_id is None:
            raise ValueError("episode_id is required when episode_state is embedded")
        return self


def _check_history_record_scope_binding(
    key: str,
    value: object,
    path: str,
    *,
    participant_address: str,
    episode_id: str,
) -> None:
    """Raise if a nested record scope key conflicts with the history view scope."""
    if not isinstance(value, str):
        return
    if key == "participant_address" and value != participant_address:
        raise ValueError(f"{path}.{key} '{value}' does not match the view participant_address '{participant_address}'")
    if key == "episode_id" and value != episode_id:
        raise ValueError(f"{path}.{key} '{value}' does not match the view episode_id '{episode_id}'")


def _walk_history_record_scope(
    node: object,
    path: str,
    *,
    participant_address: str,
    episode_id: str,
) -> None:
    """Recursively bind nested recorded-contract scope to the history view scope."""
    if isinstance(node, dict):
        for key, value in node.items():
            _check_history_record_scope_binding(
                key, value, path, participant_address=participant_address, episode_id=episode_id
            )
            _walk_history_record_scope(
                value, f"{path}.{key}", participant_address=participant_address, episode_id=episode_id
            )
    elif isinstance(node, list):
        for index, item in enumerate(node):
            _walk_history_record_scope(
                item, f"{path}[{index}]", participant_address=participant_address, episode_id=episode_id
            )


class ParticipantHistoryViewModel(ContractModel):
    """API-408 retrieval projection of participant episode/behavior history."""

    view_id: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString
    generated_at: Rfc3339DateTimeString
    source_snapshot_ref: NonEmptyString
    episode_history: list[ParticipantHistoryViewEpisodeEventModel] = Field(default_factory=list)
    behavior_history: list[ParticipantHistoryViewBehaviorEventModel] = Field(default_factory=list)
    visibility_projection_ref: NonEmptyString
    redaction_policy_ref: NonEmptyString | None = None
    completeness: Literal["complete", "truncated", "filtered"]
    completeness_basis: NonEmptyString | None = None
    marking_definition_refs: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_completeness_basis(self) -> ParticipantHistoryViewModel:
        if self.completeness != "complete" and self.completeness_basis is None:
            raise ValueError("completeness_basis is required when completeness is not 'complete'")
        return self

    @model_validator(mode="after")
    def _validate_nested_record_scope(self) -> ParticipantHistoryViewModel:
        """Recursively bind nested recorded-contract scope to the view scope.

        The direct embedded event shapes are scope-projected, but behavior
        events cite recorded semantic records (action results and their
        preconditions, attribution edges, outcome interpretations) that carry
        their own `participant_address`/`episode_id`. A one-participant view
        must not smuggle records scoped to another participant or episode
        through those subrecords.
        """

        for field_name, events in (
            ("episode_history", self.episode_history),
            ("behavior_history", self.behavior_history),
        ):
            for index, event in enumerate(events):
                _walk_history_record_scope(
                    event.model_dump(mode="python"),
                    f"{field_name}[{index}]",
                    participant_address=self.participant_address,
                    episode_id=self.episode_id,
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"completeness": {"enum": ["truncated", "filtered"]}},
                    "required": ["completeness"],
                },
                "then": {
                    "required": ["completeness_basis"],
                    "properties": {"completeness_basis": {"type": "string", "minLength": 1}},
                },
            }
        )
        return json_schema


ParticipantContextAudienceScope = Literal[
    "participant_visible",
    "operator_visible",
    "evaluator_visible",
    "auditor_visible",
]
ParticipantContextParticipantScope = Literal["participant_local"]
ParticipantContextSourceLayer = Literal[
    "source_snapshot",
    "participant_observation",
    "participant_behavior_history",
    "participant_episode_state",
    "participant_status_view",
    "participant_history_view",
    "evidence_record",
    "derived_measure",
    "control_plane_operation",
]
ParticipantContextTemporalRelation = Literal[
    "same_observation_point",
    "bounded_staleness",
    "historical_replay",
]
ParticipantContextComparabilityClass = Literal[
    "portable_equivalent",
    "portable_with_disclosed_weakening",
    "backend_specific_non_comparable",
]


class ParticipantContextSourceLayerModel(ContractModel):
    """One governed source layer consumed by a SEM-214 context view."""

    source_id: NonEmptyString
    source_layer: ParticipantContextSourceLayer
    ref: NonEmptyString
    temporal_relation: ParticipantContextTemporalRelation
    observation_point: NonEmptyString | None = None
    freshness_basis_ref: NonEmptyString | None = None
    evidence_refs: list[NonEmptyString] = Field(min_length=1)
    provenance_refs: list[NonEmptyString] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_temporal_basis(self) -> ParticipantContextSourceLayerModel:
        if self.temporal_relation == "bounded_staleness" and self.freshness_basis_ref is None:
            raise ValueError("freshness_basis_ref is required for bounded_staleness source layers")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"temporal_relation": {"const": "bounded_staleness"}},
                    "required": ["temporal_relation"],
                },
                "then": {
                    "required": ["freshness_basis_ref"],
                    "properties": {"freshness_basis_ref": {"type": "string", "minLength": 1}},
                },
            }
        )
        return json_schema


class ParticipantContextTransformationModel(ContractModel):
    """Governed transformation relation for a SEM-214 context view."""

    transformation_rule_ref: NonEmptyString
    description: NonEmptyString
    input_source_ids: list[NonEmptyString] = Field(min_length=1)
    output_semantics_ref: NonEmptyString | None = None


class ParticipantContextComparabilityModel(ContractModel):
    """Explicit comparability claim for a SEM-214 context view."""

    comparability_class: ParticipantContextComparabilityClass
    comparison_basis_ref: NonEmptyString
    backend_disclosure_refs: list[NonEmptyString] = Field(default_factory=list)
    limitations: list[NonEmptyString] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_disclosed_weakening(self) -> ParticipantContextComparabilityModel:
        if (
            self.comparability_class in {"portable_with_disclosed_weakening", "backend_specific_non_comparable"}
            and not self.backend_disclosure_refs
        ):
            raise ValueError("backend_disclosure_refs are required when comparability is weakened or backend-specific")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {
                        "comparability_class": {
                            "enum": [
                                "portable_with_disclosed_weakening",
                                "backend_specific_non_comparable",
                            ]
                        }
                    },
                    "required": ["comparability_class"],
                },
                "then": {
                    "required": ["backend_disclosure_refs"],
                    "properties": {"backend_disclosure_refs": {"minItems": 1}},
                },
            }
        )
        return json_schema


class ParticipantContextViewModel(ContractModel):
    """API-408 derived operational context view with SEM-214 semantics."""

    view_id: NonEmptyString
    participant_address: NonEmptyString
    episode_id: NonEmptyString | None = None
    generated_at: Rfc3339DateTimeString
    source_snapshot_ref: NonEmptyString
    view_ref: NonEmptyString
    meaning_ref: NonEmptyString
    participant_scope: ParticipantContextParticipantScope
    audience_scope: ParticipantContextAudienceScope
    observation_point: NonEmptyString
    derived_from_refs: list[NonEmptyString] = Field(min_length=1)
    source_layers: list[ParticipantContextSourceLayerModel] = Field(min_length=1)
    transformation: ParticipantContextTransformationModel
    comparability: ParticipantContextComparabilityModel
    evidence_refs: list[NonEmptyString] = Field(min_length=1)
    provenance_refs: list[NonEmptyString] = Field(min_length=1)
    semantic_limitations: list[NonEmptyString] = Field(min_length=1)
    derivation_basis_ref: NonEmptyString | None = None
    payload_ref: NonEmptyString | None = None
    visibility_projection_ref: NonEmptyString
    marking_definition_refs: list[NonEmptyString] = Field(default_factory=list)
    redaction_policy_ref: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_sem214_source_binding(self) -> ParticipantContextViewModel:
        source_ids = [source.source_id for source in self.source_layers]
        if len(set(source_ids)) != len(source_ids):
            raise ValueError("context view source_layers source_id values must be unique")
        unknown_inputs = sorted(set(self.transformation.input_source_ids) - set(source_ids))
        if unknown_inputs:
            raise ValueError(
                "context view transformation input_source_ids must reference source_layers: "
                + ", ".join(unknown_inputs)
            )
        available_refs = set(self.derived_from_refs) | {self.source_snapshot_ref}
        missing_refs = sorted(source.ref for source in self.source_layers if source.ref not in available_refs)
        if missing_refs:
            raise ValueError(
                "context view source layer refs must be listed in derived_from_refs or source_snapshot_ref: "
                + ", ".join(missing_refs)
            )
        return self

    @model_validator(mode="after")
    def _validate_sem216_audience_boundary(self) -> ParticipantContextViewModel:
        # SEM-216 B1/B2: archived evidence and derived evaluation/adjudication outputs are
        # distinct strata from an audience-specific view. A participant-visible view may only
        # draw on an evidence_record or derived_measure source layer through a governed view
        # rule (derivation_basis_ref), under a redaction policy (redaction_policy_ref); the
        # archival source must be consumed by the transformation rather than passed through raw;
        # and the disclosed payload must be the transformed output, never the raw archival ref.
        #
        # The required-ref clauses are also published as a schema allOf so schema-only consumers
        # enforce them; the relational mediation and payload-aliasing clauses cannot be expressed
        # in JSON Schema and are published as x-aces-invariants (see __get_pydantic_json_schema__).
        if self.audience_scope != "participant_visible":
            return self
        archival_layers = [
            source for source in self.source_layers if source.source_layer in {"evidence_record", "derived_measure"}
        ]
        if not archival_layers:
            return self
        if self.derivation_basis_ref is None:
            raise ValueError(
                "participant-visible context views that draw on archival evidence_record or derived_measure "
                "source layers must declare a derivation_basis_ref governed view rule"
            )
        if self.redaction_policy_ref is None:
            raise ValueError(
                "participant-visible context views that draw on archival evidence_record or derived_measure "
                "source layers must declare a redaction_policy_ref"
            )
        mediated = set(self.transformation.input_source_ids)
        unmediated = sorted(source.source_id for source in archival_layers if source.source_id not in mediated)
        if unmediated:
            raise ValueError(
                "participant-visible archival source layers must be mediated by the transformation view rule; "
                "unmediated source ids: " + ", ".join(unmediated)
            )
        if self.payload_ref is not None:
            raw_archival_refs = {source.ref for source in archival_layers}
            raw_archival_refs.update(ref for source in archival_layers for ref in source.evidence_refs)
            if self.payload_ref in raw_archival_refs:
                raise ValueError(
                    "participant-visible context views must not set payload_ref to a raw archival "
                    "evidence_record/derived_measure source ref; payload_ref must identify the transformed, "
                    "redacted view output produced under the governed view rule"
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {
                        "audience_scope": {"const": "participant_visible"},
                        "source_layers": {
                            "contains": {
                                "properties": {"source_layer": {"enum": ["evidence_record", "derived_measure"]}},
                                "required": ["source_layer"],
                            }
                        },
                    },
                    "required": ["audience_scope", "source_layers"],
                },
                "then": {
                    "required": ["derivation_basis_ref", "redaction_policy_ref"],
                    "properties": {
                        "derivation_basis_ref": {"type": "string", "minLength": 1},
                        "redaction_policy_ref": {"type": "string", "minLength": 1},
                    },
                },
            }
        )
        # SEM-216 relational obligations that standard JSON Schema cannot express are published
        # as ACES semantic invariants so schema-only consumers see the full portable contract and
        # the validator that enforces it (mirrors the experiment-core x-aces-invariants pattern).
        _add_aces_invariant(
            json_schema,
            "context-view-sem216-archival-source-mediated",
            "Participant-visible context views drawing on an archival evidence_record or derived_measure "
            "source layer must mediate that source through transformation.input_source_ids.",
            validator="aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary",
            inputs=[{"contract_id": "participant-context-view-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "context-view-sem216-payload-not-raw-archival",
            "Participant-visible context views must not set payload_ref to a raw archival evidence_record or "
            "derived_measure source ref; payload_ref must identify the transformed, redacted view output.",
            validator="aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary",
            inputs=[{"contract_id": "participant-context-view-v1", "instance_path": "#/payload_ref"}],
        )
        return json_schema


class PlanOperationModel(ContractModel):
    action: str
    address: CompiledAddress
    resource_type: str
    payload: dict[str, Any] = Field(default_factory=dict)
    ordering_dependencies: list[CompiledAddress] = Field(default_factory=list)
    refresh_dependencies: list[CompiledAddress] = Field(default_factory=list)


def _require_unique_operation_addresses(operations: list[PlanOperationModel]) -> None:
    addresses = [operation.address for operation in operations]
    if len(addresses) != len(set(addresses)):
        raise ValueError("Plan operation addresses must be unique")


def _require_operation_identities(operations: list[PlanOperationModel], domain: RuntimeDomain) -> None:
    for operation in operations:
        require_plan_operation_identity(domain, operation.address, operation.resource_type)


def _require_startup_order_addresses(
    operations: list[PlanOperationModel],
    startup_order: list[str],
) -> None:
    if len(startup_order) != len(set(startup_order)):
        raise ValueError("Plan startup_order addresses must be unique")
    operation_addresses = {operation.address for operation in operations}
    unknown = set(startup_order) - operation_addresses
    if unknown:
        raise ValueError("Plan startup_order must reference admitted operation addresses")


class RealizationEnvelopeIdentityModel(ContractModel):
    """Immutable realization-envelope identity carried across runtime contracts."""

    contract_id: Literal["realization-envelope-v1"] = "realization-envelope-v1"
    envelope_id: NonEmptyString
    schema_version: Literal["realization-envelope/v1"] = "realization-envelope/v1"
    digest: Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")]
    configuration_digest: Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")]


class ProvisioningPlanModel(ContractModel):
    operations: list[PlanOperationModel] = Field(default_factory=list)
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)
    realization_envelope: RealizationEnvelopeIdentityModel | None = None

    @model_validator(mode="after")
    def _validate_operation_addresses(self) -> ProvisioningPlanModel:
        _require_unique_operation_addresses(self.operations)
        _require_operation_identities(self.operations, RuntimeDomain.PROVISIONING)
        return self


class OrchestrationPlanModel(ContractModel):
    operations: list[PlanOperationModel] = Field(default_factory=list)
    startup_order: list[CompiledAddress] = Field(default_factory=list)
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_operation_addresses(self) -> OrchestrationPlanModel:
        _require_unique_operation_addresses(self.operations)
        _require_operation_identities(self.operations, RuntimeDomain.ORCHESTRATION)
        _require_startup_order_addresses(self.operations, self.startup_order)
        return self


class EvaluationPlanModel(ContractModel):
    operations: list[PlanOperationModel] = Field(default_factory=list)
    startup_order: list[CompiledAddress] = Field(default_factory=list)
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_operation_addresses(self) -> EvaluationPlanModel:
        _require_unique_operation_addresses(self.operations)
        _require_operation_identities(self.operations, RuntimeDomain.EVALUATION)
        _require_startup_order_addresses(self.operations, self.startup_order)
        return self


class SnapshotEntryModel(ContractModel):
    address: CompiledAddress
    domain: str
    resource_type: str
    payload: dict[str, Any] = Field(default_factory=dict)
    ordering_dependencies: list[CompiledAddress] = Field(default_factory=list)
    refresh_dependencies: list[CompiledAddress] = Field(default_factory=list)
    status: str = "ready"


class RealizationProvenanceEntryModel(ContractModel):
    """SEM-218 invariant I5: provenance for one realized realization concern.

    Distinguishes ``author-declared`` / ``processor-derived`` / ``backend-realized``
    origins for a realized concern recorded on the snapshot's result / history
    surfaces. Carries field-path and kind references only (never the realized
    value, per the SEM-218 host-exposure gate). Kept distinct from ADR-054
    lifecycle ``phase_realization`` and API-407 participant feature support.
    """

    address: NonEmptyString
    field_path: NonEmptyString
    domain: NonEmptyString
    requirement_kind: NonEmptyString
    explicitness: ExplicitnessClass
    provenance: ExplicitnessProvenance
    governing_scope: NonEmptyString | None = None


class RuntimeSnapshotEnvelopeModel(ContractModel):
    """Published envelope for a live runtime snapshot.

    Participant episode surfaces (``participant_episode_results`` and
    ``participant_episode_history``) are both keyed by the stable
    ``participant_address`` of the participant the state/history belongs
    to. SEM-208 participant behavior history is keyed the same way and
    records action, observation, and state-transition events with compiled
    behavior-contract addresses. The episode results map carries the
    currently-live episode state per participant; prior episodes survive only
    through append-only history streams and the ``previous_episode_id`` chain
    on each state.
    """

    schema_version: Literal[RUNTIME_SNAPSHOT_SCHEMA_VERSION] = RUNTIME_SNAPSHOT_SCHEMA_VERSION
    entries: dict[CompiledAddress, SnapshotEntryModel] = Field(default_factory=dict)
    orchestration_results: dict[str, WorkflowExecutionStateModel] = Field(default_factory=dict)
    orchestration_history: dict[str, list[WorkflowHistoryEventModel]] = Field(default_factory=dict)
    evaluation_results: dict[str, EvaluationResultStateModel] = Field(default_factory=dict)
    evaluation_history: dict[str, list[EvaluationHistoryEventModel]] = Field(default_factory=dict)
    proposition_truth_results: dict[str, PropositionTruthResultModel] = Field(default_factory=dict)
    participant_episode_results: dict[str, ParticipantEpisodeStateModel] = Field(default_factory=dict)
    participant_episode_history: dict[str, list[ParticipantEpisodeHistoryEventModel]] = Field(default_factory=dict)
    participant_behavior_history: dict[str, list[ParticipantBehaviorHistoryEventModel]] = Field(default_factory=dict)
    shared_state_records: dict[str, ParticipantSharedStateRecordModel] = Field(default_factory=dict)
    shared_state_history: dict[str, list[ParticipantSharedStateRecordModel]] = Field(default_factory=dict)
    joint_action_records: dict[str, ParticipantJointActionRecordModel] = Field(default_factory=dict)
    time_management_contexts: dict[str, ParticipantTimeManagementContextModel] = Field(default_factory=dict)
    realization_provenance: list[RealizationProvenanceEntryModel] = Field(default_factory=list)
    realization_envelope: RealizationEnvelopeIdentityModel | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_entry_addresses(self) -> RuntimeSnapshotEnvelopeModel:
        for map_key, entry in self.entries.items():
            if map_key != entry.address:
                raise ValueError("Runtime snapshot entries map key must equal embedded address")
        return self


class OperationReceiptModel(ContractModel):
    schema_version: Literal[OPERATION_SCHEMA_VERSION] = OPERATION_SCHEMA_VERSION
    operation_id: str
    domain: str
    submitted_at: str
    accepted: bool
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)


class OperationStatusModel(ContractModel):
    schema_version: Literal[OPERATION_SCHEMA_VERSION] = OPERATION_SCHEMA_VERSION
    operation_id: str
    domain: str
    state: str
    submitted_at: str
    updated_at: str
    diagnostics: list[dict[str, Any]] = Field(default_factory=list)
    changed_addresses: list[CompiledAddress] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_changed_addresses(self) -> OperationStatusModel:
        if len(self.changed_addresses) != len(set(self.changed_addresses)):
            raise ValueError("changed addresses must be unique")
        return self


class ProvisionerCapabilitiesModel(ContractModel):
    name: NonEmptyString
    supported_node_types: list[NonEmptyString] = Field(min_length=1)
    supported_os_families: list[NonEmptyString] = Field(min_length=1)
    supported_content_types: list[NonEmptyString] = Field(default_factory=list)
    supported_account_features: list[NonEmptyString] = Field(default_factory=list)
    supported_domain_profiles: list[NonEmptyString] = Field(default_factory=list)
    max_total_nodes: int | None = Field(default=None, gt=0)
    supports_acls: bool = False
    supports_accounts: bool = False
    supports_generated_artifacts: bool = False
    supports_persistent_volumes: bool = False
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_account_support(self) -> ProvisionerCapabilitiesModel:
        _validate_controlled_vocabulary_terms(
            "capabilities.provisioner.supported_node_types",
            self.supported_node_types,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.provisioner.supported_os_families",
            self.supported_os_families,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.provisioner.supported_content_types",
            self.supported_content_types,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.provisioner.supported_account_features",
            self.supported_account_features,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.provisioner.supported_domain_profiles",
            self.supported_domain_profiles,
        )
        if self.supports_accounts and not self.supported_account_features:
            raise ValueError("provisioners that support accounts must declare supported_account_features")
        if not self.supports_accounts and self.supported_account_features:
            raise ValueError("supported_account_features require supports_accounts=true")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {"supports_accounts": {"const": True}},
                        "required": ["supports_accounts"],
                    },
                    "then": {
                        "required": ["supported_account_features"],
                        "properties": {"supported_account_features": {"minItems": 1}},
                    },
                },
                {
                    "if": {
                        "properties": {"supports_accounts": {"const": False}},
                        "required": ["supports_accounts"],
                    },
                    "then": {
                        "properties": {"supported_account_features": {"maxItems": 0}},
                    },
                },
            ]
        )
        return json_schema


class OrchestratorCapabilitiesModel(ContractModel):
    name: NonEmptyString
    supported_sections: list[NonEmptyString] = Field(min_length=1)
    supports_workflows: bool = False
    supports_assertion_refs: bool = True
    supports_inject_bindings: bool = True
    supported_workflow_features: list[WorkflowFeature] = Field(default_factory=list)
    supported_workflow_state_predicates: list[WorkflowStatePredicateFeature] = Field(default_factory=list)
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_workflow_support(self) -> OrchestratorCapabilitiesModel:
        _validate_controlled_vocabulary_terms(
            "capabilities.orchestrator.supported_sections",
            self.supported_sections,
        )
        if self.supports_workflows:
            if "workflows" not in self.supported_sections:
                raise ValueError("orchestrators that support workflows must include 'workflows' in supported_sections")
            if not self.supported_workflow_features:
                raise ValueError("orchestrators that support workflows must declare supported_workflow_features")
        else:
            if "workflows" in self.supported_sections:
                raise ValueError("'workflows' in supported_sections requires supports_workflows=true")
            if self.supported_workflow_features:
                raise ValueError("supported_workflow_features require supports_workflows=true")
            if self.supported_workflow_state_predicates:
                raise ValueError("supported_workflow_state_predicates require supports_workflows=true")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {"supports_workflows": {"const": True}},
                        "required": ["supports_workflows"],
                    },
                    "then": {
                        "required": ["supported_workflow_features", "supported_sections"],
                        "properties": {
                            "supported_workflow_features": {"minItems": 1},
                            "supported_sections": {"contains": {"const": "workflows"}},
                        },
                    },
                },
                {
                    "if": {
                        "properties": {"supports_workflows": {"const": False}},
                        "required": ["supports_workflows"],
                    },
                    "then": {
                        "properties": {
                            "supported_workflow_features": {"maxItems": 0},
                            "supported_workflow_state_predicates": {"maxItems": 0},
                            "supported_sections": {"not": {"contains": {"const": "workflows"}}},
                        },
                    },
                },
            ]
        )
        return json_schema


class EvaluatorCapabilitiesModel(ContractModel):
    name: NonEmptyString
    supported_sections: list[NonEmptyString] = Field(min_length=1)
    supports_scoring: bool = True
    supports_objectives: bool = True
    supported_predicate_families: list[NonEmptyString] = Field(default_factory=list)
    supported_quantifiers: list[NonEmptyString] = Field(default_factory=list)
    supported_truth_outcomes: list[NonEmptyString] = Field(default_factory=list)
    supported_evidence_channels: list[NonEmptyString] = Field(default_factory=list)
    supported_time_domains: list[NonEmptyString] = Field(default_factory=list)
    preserves_binding_provenance: bool = False
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_evaluator_support(self) -> EvaluatorCapabilitiesModel:
        _validate_controlled_vocabulary_terms(
            "capabilities.evaluator.supported_sections",
            self.supported_sections,
        )
        if not self.supports_scoring and not self.supports_objectives:
            raise ValueError("evaluators must support scoring, objectives, or both")
        for field_name in (
            "supported_predicate_families",
            "supported_quantifiers",
            "supported_truth_outcomes",
            "supported_evidence_channels",
            "supported_time_domains",
        ):
            values = getattr(self, field_name)
            if len(set(values)) != len(values):
                raise ValueError(f"evaluator {field_name} must be unique")
        proposition_sections = {"propositions", "assertions"}
        if proposition_sections.intersection(self.supported_sections):
            if not proposition_sections.issubset(self.supported_sections):
                raise ValueError("evaluator proposition support requires propositions and assertions")
            if set(self.supported_truth_outcomes) != {"true", "false", "unknown", "unsupported"}:
                raise ValueError("evaluator proposition support requires all portable truth outcomes")
            if not all(
                (
                    self.supported_predicate_families,
                    self.supported_quantifiers,
                    self.supported_evidence_channels,
                    self.supported_time_domains,
                )
            ):
                raise ValueError("evaluator proposition support requires typed capability dimensions")
            if not self.preserves_binding_provenance:
                raise ValueError("evaluator proposition support requires binding provenance")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "not": {
                    "allOf": [
                        {
                            "properties": {"supports_scoring": {"const": False}},
                            "required": ["supports_scoring"],
                        },
                        {
                            "properties": {"supports_objectives": {"const": False}},
                            "required": ["supports_objectives"],
                        },
                    ]
                }
            }
        )
        return json_schema


class ApparatusIdentityModel(ContractModel):
    name: NonEmptyString
    version: NonEmptyString


class BackendCompatibilityModel(ContractModel):
    processors: list[NonEmptyString] = Field(min_length=1)


class ProcessorCompatibilityModel(ContractModel):
    backends: list[NonEmptyString] = Field(min_length=1)


class RealizationSupportDeclarationModel(ContractModel):
    domain: NonEmptyString
    support_mode: RealizationSupportMode
    supported_constraint_kinds: list[NonEmptyString] = Field(default_factory=list)
    supported_exact_requirement_kinds: list[NonEmptyString] = Field(default_factory=list)
    disclosure_kinds: list[NonEmptyString] = Field(min_length=1)
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_realization_support(self) -> RealizationSupportDeclarationModel:
        if not self.supported_constraint_kinds and not self.supported_exact_requirement_kinds:
            raise ValueError(
                "realization_support declarations must declare supported_constraint_kinds "
                "or supported_exact_requirement_kinds"
            )
        if self.support_mode == RealizationSupportMode.EXACT_ONLY and self.supported_constraint_kinds:
            raise ValueError("exact-only realization support must not declare supported_constraint_kinds")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "anyOf": [
                        {
                            "required": ["supported_constraint_kinds"],
                            "properties": {"supported_constraint_kinds": {"minItems": 1}},
                        },
                        {
                            "required": ["supported_exact_requirement_kinds"],
                            "properties": {"supported_exact_requirement_kinds": {"minItems": 1}},
                        },
                    ]
                },
                {
                    "if": {
                        "properties": {"support_mode": {"const": RealizationSupportMode.EXACT_ONLY.value}},
                        "required": ["support_mode"],
                    },
                    "then": {
                        "required": ["supported_exact_requirement_kinds"],
                        "properties": {
                            "supported_exact_requirement_kinds": {"minItems": 1},
                            "supported_constraint_kinds": {"maxItems": 0},
                        },
                    },
                },
            ]
        )
        return json_schema


class ConceptBindingEntryModel(ContractModel):
    """Binds a vocabulary surface in an artifact to a canonical concept family."""

    scope: NonEmptyString = Field(
        ...,
        pattern=r"^[a-z_][a-z0-9_.]*[a-z0-9_]$",
    )
    family: ConceptFamilyId


class ProcessorCapabilitiesV2Model(ContractModel):
    supported_sdl_versions: list[NonEmptyString] = Field(min_length=1)
    supported_features: list[ProcessorFeature] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_declared_authority(self) -> ProcessorCapabilitiesV2Model:
        validate_processor_supported_sdl_versions(self.supported_sdl_versions)
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema["properties"]["supported_sdl_versions"]["items"]["enum"] = list(PROCESSOR_SUPPORTED_SDL_VERSION_IDS)
        return json_schema


_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS = (
    "participant-runtime-behavior-features",
    "participant-runtime-interaction-features",
)


def _validate_participant_feature_support_term(feature: str) -> None:
    from .controlled_vocabularies import load_controlled_vocabulary_catalog

    catalog = load_controlled_vocabulary_catalog()
    for vocabulary_id in _PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS:
        definition = catalog.vocabularies[vocabulary_id]
        if feature in definition.terms:
            return
        if definition.extension_pattern is not None and re.fullmatch(definition.extension_pattern, feature):
            return
    joined = ", ".join(_PARTICIPANT_FEATURE_SUPPORT_VOCABULARY_IDS)
    raise ValueError(
        f"feature_support feature '{feature}' is not a governed term of {joined} "
        "and does not match the governed extension pattern"
    )


class ParticipantFeatureSupportModel(ContractModel):
    """API-407 per-feature participant runtime support declaration."""

    feature: NonEmptyString
    support_level: ParticipantFeatureSupportLevel
    constraint_refs: list[NonEmptyString] = Field(default_factory=list)
    disclosure_refs: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_feature_support_declaration(self) -> ParticipantFeatureSupportModel:
        _validate_participant_feature_support_term(self.feature)
        _validate_unique_string_values("constraint_refs", self.constraint_refs)
        _validate_unique_string_values("disclosure_refs", self.disclosure_refs)
        if self.support_level != ParticipantFeatureSupportLevel.EXACT and not self.disclosure_refs:
            raise ValueError(
                f"feature_support entry '{self.feature}' declares support_level "
                f"'{self.support_level.value}' below 'exact' and must carry at least one disclosure_refs entry"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        below_exact = [
            level.value for level in ParticipantFeatureSupportLevel if level != ParticipantFeatureSupportLevel.EXACT
        ]
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"support_level": {"enum": below_exact}},
                    "required": ["support_level"],
                },
                "then": {
                    "required": ["disclosure_refs"],
                    "properties": {"disclosure_refs": {"minItems": 1}},
                },
            }
        )
        return json_schema


class ParticipantRuntimeCapabilitiesModel(ContractModel):
    """Participant-episode lifecycle capability block (RUN-311).

    A backend that declares this block advertises that it implements
    the full participant episode control surface on the
    ``ParticipantRuntime`` protocol: ``initialize`` / ``reset`` /
    ``restart`` / ``terminate`` plus ``status`` / ``results`` /
    ``history``. Consumers of the manifest can infer the
    ``FULL_REMOTE_CONTROL_PLANE`` conformance profile from this block.

    API-405 support dimensions live here because they are backend apparatus
    claims: which participant roles, behavior features, and interaction
    features this participant runtime can actually realize.
    """

    name: NonEmptyString
    supported_participant_roles: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    supported_behavior_features: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    supported_interaction_features: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    feature_support: list[ParticipantFeatureSupportModel] = Field(default_factory=list)
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_api_405_declarations(self) -> ParticipantRuntimeCapabilitiesModel:
        _validate_unique_string_values("supported_participant_roles", self.supported_participant_roles)
        _validate_unique_string_values("supported_behavior_features", self.supported_behavior_features)
        _validate_unique_string_values("supported_interaction_features", self.supported_interaction_features)
        _validate_controlled_vocabulary_terms(
            "capabilities.participant_runtime.supported_participant_roles",
            self.supported_participant_roles,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.participant_runtime.supported_behavior_features",
            self.supported_behavior_features,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.participant_runtime.supported_interaction_features",
            self.supported_interaction_features,
        )
        return self

    @model_validator(mode="after")
    def _validate_api_407_feature_support(self) -> ParticipantRuntimeCapabilitiesModel:
        _validate_unique_string_values("feature_support", [entry.feature for entry in self.feature_support])
        supported_features = set(self.supported_behavior_features) | set(self.supported_interaction_features)
        for entry in self.feature_support:
            declared_unsupported = entry.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED
            if declared_unsupported and entry.feature in supported_features:
                raise ValueError(
                    f"feature_support entry '{entry.feature}' declares support_level 'unsupported' but the "
                    "feature is declared in supported_behavior_features or supported_interaction_features"
                )
        return self


class ObservationCapabilitiesModel(ContractModel):
    """EXP-715 backend observation and evidence-collection capability declaration."""

    name: NonEmptyString
    supported_capture_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    supported_channel_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    supported_evidence_contracts: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    supported_media_types: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    supported_sealing_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    supports_redaction: bool = False
    supports_loss_disclosure: bool = False
    supports_chain_of_custody: bool = False
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_observation_capability(self) -> ObservationCapabilitiesModel:
        _validate_unique_string_values("supported_capture_kinds", self.supported_capture_kinds)
        _validate_unique_string_values("supported_channel_kinds", self.supported_channel_kinds)
        _validate_unique_string_values("supported_evidence_contracts", self.supported_evidence_contracts)
        _validate_unique_string_values("supported_media_types", self.supported_media_types)
        _validate_unique_string_values("supported_sealing_modes", self.supported_sealing_modes)
        _validate_controlled_vocabulary_terms(
            "capabilities.observation.supported_capture_kinds",
            self.supported_capture_kinds,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.observation.supported_channel_kinds",
            self.supported_channel_kinds,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.observation.supported_sealing_modes",
            self.supported_sealing_modes,
        )
        validate_backend_supported_contract_versions(self.supported_evidence_contracts)
        for contract_id in self.supported_evidence_contracts:
            if not contract_id.startswith("experiment-"):
                raise ValueError("observation supported_evidence_contracts must be experiment contract ids")
        return self


class BackendCapabilitiesV2Model(ContractModel):
    provisioner: ProvisionerCapabilitiesModel
    orchestrator: OrchestratorCapabilitiesModel | None = None
    evaluator: EvaluatorCapabilitiesModel | None = None
    participant_runtime: ParticipantRuntimeCapabilitiesModel | None = None
    observation: ObservationCapabilitiesModel | None = None


class ProcessorManifestV2Model(ContractModel):
    schema_version: Literal[PROCESSOR_MANIFEST_V2_SCHEMA_VERSION] = PROCESSOR_MANIFEST_V2_SCHEMA_VERSION
    identity: ApparatusIdentityModel
    supported_contract_versions: list[NonEmptyString] = Field(min_length=1)
    compatibility: ProcessorCompatibilityModel
    concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1)
    constraints: dict[str, str] = Field(default_factory=dict)
    capabilities: ProcessorCapabilitiesV2Model

    @model_validator(mode="after")
    def _validate_unique_binding_scopes(self) -> ProcessorManifestV2Model:
        validate_processor_supported_contract_versions(self.supported_contract_versions)
        scopes = [binding.scope for binding in self.concept_bindings]
        if len(scopes) != len(set(scopes)):
            raise ValueError("concept_bindings must not contain duplicate scopes")
        _validate_canonical_concept_bindings(self, allowed_scopes=_PROCESSOR_CONCEPT_BINDING_SCOPES)
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema["properties"]["supported_contract_versions"]["items"]["enum"] = list(
            PROCESSOR_SUPPORTED_CONTRACT_IDS
        )
        return json_schema


class BackendManifestV2Model(ContractModel):
    schema_version: Literal[BACKEND_MANIFEST_V2_SCHEMA_VERSION] = BACKEND_MANIFEST_V2_SCHEMA_VERSION
    identity: ApparatusIdentityModel
    supported_contract_versions: list[NonEmptyString] = Field(min_length=1)
    compatibility: BackendCompatibilityModel
    realization_support: list[RealizationSupportDeclarationModel] = Field(min_length=1)
    realization_envelope: RealizationEnvelopeIdentityModel | None = None
    concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1)
    constraints: dict[str, str] = Field(default_factory=dict)
    capabilities: BackendCapabilitiesV2Model

    @model_validator(mode="after")
    def _validate_unique_binding_scopes(self) -> BackendManifestV2Model:
        validate_backend_supported_contract_versions(self.supported_contract_versions)
        envelope_contract_declared = "realization-envelope-v1" in self.supported_contract_versions
        if self.realization_envelope is not None and not envelope_contract_declared:
            raise ValueError("realization_envelope requires realization-envelope-v1 support")
        if envelope_contract_declared and self.realization_envelope is None:
            raise ValueError("realization-envelope-v1 support requires realization_envelope identity")
        scopes = [binding.scope for binding in self.concept_bindings]
        if len(scopes) != len(set(scopes)):
            raise ValueError("concept_bindings must not contain duplicate scopes")
        _validate_canonical_concept_bindings(self, allowed_scopes=_BACKEND_CONCEPT_BINDING_SCOPES)
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema["properties"]["supported_contract_versions"]["items"]["enum"] = list(BACKEND_SUPPORTED_CONTRACT_IDS)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {"realization_envelope": {"not": {"type": "null"}}},
                        "required": ["realization_envelope"],
                    },
                    "then": {
                        "properties": {
                            "supported_contract_versions": {"contains": {"const": "realization-envelope-v1"}}
                        }
                    },
                },
                {
                    "if": {
                        "properties": {
                            "supported_contract_versions": {"contains": {"const": "realization-envelope-v1"}}
                        },
                        "required": ["supported_contract_versions"],
                    },
                    "then": {
                        "properties": {"realization_envelope": {"not": {"type": "null"}}},
                        "required": ["realization_envelope"],
                    },
                },
            ]
        )
        return json_schema


class ParticipantImplementationCompatibilityModel(ContractModel):
    participant_runtimes: list[NonEmptyString] = Field(default_factory=list)
    processors: list[NonEmptyString] = Field(default_factory=list)
    backends: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_non_empty_compatibility(self) -> ParticipantImplementationCompatibilityModel:
        _validate_unique_string_values("participant_runtimes", self.participant_runtimes)
        _validate_unique_string_values("processors", self.processors)
        _validate_unique_string_values("backends", self.backends)
        if not (self.participant_runtimes or self.processors or self.backends):
            raise ValueError(
                "compatibility must declare at least one participant runtime, processor, or backend surface"
            )
        return self


class ParticipantImplementationCapabilitiesModel(ContractModel):
    supported_participant_contracts: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    supported_decision_surface_modes: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    tool_affordance_expectations: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    exposure_policy_kinds: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )

    @model_validator(mode="after")
    def _validate_participant_implementation_capabilities(self) -> ParticipantImplementationCapabilitiesModel:
        _validate_unique_string_values("supported_participant_contracts", self.supported_participant_contracts)
        _validate_unique_string_values("supported_decision_surface_modes", self.supported_decision_surface_modes)
        _validate_unique_string_values("tool_affordance_expectations", self.tool_affordance_expectations)
        _validate_unique_string_values("exposure_policy_kinds", self.exposure_policy_kinds)
        validate_participant_supported_contract_versions(self.supported_participant_contracts)
        _validate_controlled_vocabulary_terms(
            "capabilities.supported_participant_contracts",
            self.supported_participant_contracts,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.supported_decision_surface_modes",
            self.supported_decision_surface_modes,
        )
        _validate_controlled_vocabulary_terms(
            "capabilities.tool_affordance_expectations",
            self.tool_affordance_expectations,
        )
        _validate_controlled_vocabulary_terms("capabilities.exposure_policy_kinds", self.exposure_policy_kinds)
        return self


class ParticipantImplementationManifestModel(ContractModel):
    schema_version: Literal[PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION] = (
        PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION
    )
    identity: ApparatusIdentityModel
    implementation_kind: NonEmptyString
    supported_contract_versions: list[NonEmptyString] = Field(min_length=1)
    compatibility: ParticipantImplementationCompatibilityModel
    concept_bindings: list[ConceptBindingEntryModel] = Field(min_length=1)
    constraints: dict[str, str] = Field(default_factory=dict)
    capabilities: ParticipantImplementationCapabilitiesModel

    @model_validator(mode="after")
    def _validate_participant_implementation_manifest(self) -> ParticipantImplementationManifestModel:
        validate_participant_implementation_supported_contract_versions(self.supported_contract_versions)
        _validate_unique_string_values("supported_contract_versions", self.supported_contract_versions)
        _validate_controlled_vocabulary_terms("implementation_kind", [self.implementation_kind])
        unsupported = sorted(
            set(self.capabilities.supported_participant_contracts) - set(self.supported_contract_versions)
        )
        if unsupported:
            joined = ", ".join(unsupported)
            raise ValueError(
                "supported_participant_contracts must be declared in supported_contract_versions: " + joined
            )
        scopes = [binding.scope for binding in self.concept_bindings]
        if len(scopes) != len(set(scopes)):
            raise ValueError("concept_bindings must not contain duplicate scopes")
        _validate_canonical_concept_bindings(
            self,
            allowed_scopes=_PARTICIPANT_IMPLEMENTATION_CONCEPT_BINDING_SCOPES,
        )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema["properties"]["supported_contract_versions"]["items"]["enum"] = list(
            PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS
        )
        return json_schema


DigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")]


class ParticipantExposurePolicyModel(ContractModel):
    policy_id: NonEmptyString
    policy_version: NonEmptyString | None = None
    policy_digest: DigestString | None = None
    exposure_policy_kinds: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    disclosed_refs: list[NonEmptyString] = Field(default_factory=list)
    withheld_refs: list[NonEmptyString] = Field(default_factory=list)
    tool_affordance_refs: list[NonEmptyString] = Field(default_factory=list)
    visibility_scope_refs: list[NonEmptyString] = Field(default_factory=list)
    constraints: dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_exposure_policy(self) -> ParticipantExposurePolicyModel:
        _validate_unique_string_values("exposure_policy_kinds", self.exposure_policy_kinds)
        _validate_unique_string_values("disclosed_refs", self.disclosed_refs)
        _validate_unique_string_values("withheld_refs", self.withheld_refs)
        _validate_unique_string_values("tool_affordance_refs", self.tool_affordance_refs)
        _validate_unique_string_values("visibility_scope_refs", self.visibility_scope_refs)
        _validate_controlled_vocabulary_terms("capabilities.exposure_policy_kinds", self.exposure_policy_kinds)
        if not (self.disclosed_refs or self.withheld_refs or self.tool_affordance_refs or self.visibility_scope_refs):
            raise ValueError(
                "exposure policy must declare disclosed_refs, withheld_refs, "
                "tool_affordance_refs, or visibility_scope_refs"
            )
        return self


class ParticipantImplementationSelectionModel(ContractModel):
    participant_address: NonEmptyString
    implementation_identity: ApparatusIdentityModel
    manifest_ref: NonEmptyString
    manifest_digest: DigestString
    configuration_ref: NonEmptyString | None = None
    configuration_digest: DigestString | None = None
    selected_decision_surface_mode: NonEmptyString
    participant_contract_versions: list[NonEmptyString] = Field(
        min_length=1,
        json_schema_extra={"uniqueItems": True},
    )
    exposure_policy: ParticipantExposurePolicyModel

    @model_validator(mode="after")
    def _validate_participant_implementation_selection(self) -> ParticipantImplementationSelectionModel:
        _validate_unique_string_values("participant_contract_versions", self.participant_contract_versions)
        validate_participant_supported_contract_versions(self.participant_contract_versions)
        _validate_controlled_vocabulary_terms(
            "capabilities.supported_decision_surface_modes",
            [self.selected_decision_surface_mode],
        )
        return self


class ParticipantImplementationProvenanceModel(ContractModel):
    schema_version: Literal[PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION] = (
        PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION
    )
    run_id: NonEmptyString
    participant_implementations: list[ParticipantImplementationSelectionModel] = Field(min_length=1)
    processor_manifest_ref: NonEmptyString | None = None
    backend_manifest_ref: NonEmptyString | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_unique_participant_addresses(self) -> ParticipantImplementationProvenanceModel:
        addresses = [selection.participant_address for selection in self.participant_implementations]
        _validate_unique_string_values("participant_address", addresses)
        return self


class ExperimentReferenceModel(ContractModel):
    """Typed reference to an experiment-core or adjacent ACES artifact."""

    ref_kind: Literal[
        "processor",
        "backend",
        "participant-implementation",
        "scenario",
        "scenario-snapshot",
        "task",
        "authoring-input",
        "protocol",
        "apparatus-context",
        "run",
        "metric-definition",
        "result",
        "study",
        "manifest",
        "profile",
        "capability",
        "capture-spec",
        "evidence",
        "evidence-record",
        "derived-measure",
        "measurement-channel",
        "analysis-artifact",
        "other",
    ]
    ref_id: NonEmptyString
    ref_version: NonEmptyString | None = None
    ref_digest: PrefixedDigestString | None = None
    ref_path: NonEmptyString | None = None


class ExperimentScenarioReferenceModel(ExperimentReferenceModel):
    """Reference constrained to authored scenario material."""

    ref_kind: Literal["scenario", "scenario-snapshot"]

    @model_validator(mode="after")
    def _validate_generic_scenario_reference_scope(self) -> ExperimentScenarioReferenceModel:
        if self.ref_kind == "scenario" and (
            self.ref_version is not None or self.ref_digest is not None or self.ref_path is not None
        ):
            raise ValueError(
                "generic scenario references are id-only; use scenario-snapshot for version, digest, or path binding"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"ref_kind": {"const": "scenario"}},
                    "required": ["ref_kind"],
                },
                "then": {
                    "properties": {
                        "ref_version": {"type": "null"},
                        "ref_digest": {"type": "null"},
                        "ref_path": {"type": "null"},
                    }
                },
            }
        )
        return json_schema


class ExperimentTaskReferenceModel(ExperimentReferenceModel):
    """Reference constrained to an experiment task."""

    ref_kind: Literal["task"]

    @model_validator(mode="after")
    def _validate_task_reference_scope(self) -> ExperimentTaskReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("task references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentScenarioSnapshotReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a sealed scenario snapshot."""

    ref_kind: Literal["scenario-snapshot"]


class AssociatedArtifactParentReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a supported associated-artifact parent."""

    ref_kind: Literal[
        "scenario",
        "scenario-snapshot",
        "task",
        "authoring-input",
        "apparatus-context",
        "run",
        "study",
    ]

    @model_validator(mode="after")
    def _validate_associated_artifact_parent(self) -> AssociatedArtifactParentReferenceModel:
        if self.ref_kind == "scenario":
            if self.ref_version is not None or self.ref_digest is not None or self.ref_path is not None:
                raise ValueError("generic scenario parents are id-only; use scenario-snapshot for snapshot binding")
        elif self.ref_kind != "scenario-snapshot" and (self.ref_digest is not None or self.ref_path is not None):
            raise ValueError(
                "experiment associated-artifact parents must not carry ref_digest or ref_path without a "
                "normative parent canonicalization profile"
            )
        return self


class ExperimentManifestReferenceModel(ExperimentReferenceModel):
    """Reference constrained to an apparatus or capability manifest."""

    ref_kind: Literal["manifest"]
    subject_ref: ExperimentReferenceModel | None = None

    @model_validator(mode="after")
    def _validate_manifest_reference_scope(self) -> ExperimentManifestReferenceModel:
        if self.ref_path is not None:
            raise ValueError("manifest references must not carry ref_path")
        if self.subject_ref is not None and (
            self.subject_ref.ref_digest is not None or self.subject_ref.ref_path is not None
        ):
            raise ValueError("manifest subject_ref must not carry ref_digest or ref_path")
        if self.ref_digest is not None:
            if self.subject_ref is None or self.subject_ref.ref_kind not in {"processor", "backend"}:
                raise ValueError(
                    "digest-bound manifest references must use processor/backend subject_ref values "
                    "validated against concrete manifest payloads"
                )
            expected_manifest_version = (
                PROCESSOR_MANIFEST_V2_SCHEMA_VERSION
                if self.subject_ref.ref_kind == "processor"
                else BACKEND_MANIFEST_V2_SCHEMA_VERSION
            )
            if self.ref_version != expected_manifest_version:
                raise ValueError(
                    "digest-bound processor/backend manifest references must use the supported manifest schema version"
                )
        if self.subject_ref is not None and self.subject_ref.ref_kind in {"processor", "backend"}:
            if self.ref_id != self.subject_ref.ref_id:
                raise ValueError("processor/backend manifest references ref_id must match subject_ref.ref_id")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {"properties": {"ref_path": {"type": "null"}}},
                {
                    "if": {
                        "properties": {"ref_digest": {"type": "string"}},
                        "required": ["ref_digest"],
                    },
                    "then": {
                        "required": ["subject_ref"],
                        "properties": {
                            "subject_ref": {
                                "required": ["ref_kind"],
                                "properties": {"ref_kind": {"enum": ["processor", "backend"]}},
                            }
                        },
                    },
                },
                {
                    "if": {
                        "properties": {
                            "ref_digest": {"type": "string"},
                            "subject_ref": {
                                "properties": {"ref_kind": {"const": "processor"}},
                                "required": ["ref_kind"],
                            },
                        },
                        "required": ["ref_digest", "subject_ref"],
                    },
                    "then": {"properties": {"ref_version": {"const": PROCESSOR_MANIFEST_V2_SCHEMA_VERSION}}},
                },
                {
                    "if": {
                        "properties": {
                            "ref_digest": {"type": "string"},
                            "subject_ref": {
                                "properties": {"ref_kind": {"const": "backend"}},
                                "required": ["ref_kind"],
                            },
                        },
                        "required": ["ref_digest", "subject_ref"],
                    },
                    "then": {"properties": {"ref_version": {"const": BACKEND_MANIFEST_V2_SCHEMA_VERSION}}},
                },
                {
                    "if": {"properties": {"subject_ref": {"type": "object"}}, "required": ["subject_ref"]},
                    "then": {
                        "properties": {
                            "subject_ref": {
                                "properties": {
                                    "ref_digest": {"type": "null"},
                                    "ref_path": {"type": "null"},
                                }
                            }
                        }
                    },
                },
            ]
        )
        _add_aces_invariant(
            json_schema,
            "manifest-reference-digest-scope-valid",
            "Manifest digest qualifiers are limited to processor/backend manifest refs that can be checked "
            "against concrete manifest payload digests; manifest path qualifiers are not accepted in v1.",
            validator="aces_contracts.contracts.ExperimentManifestReferenceModel._validate_manifest_reference_scope",
            inputs=[
                {"contract_id": "experiment-task-v1", "instance_path": "#/$defs/ExperimentManifestReferenceModel"},
                {
                    "contract_id": "experiment-apparatus-context-v1",
                    "instance_path": "#/$defs/ExperimentManifestReferenceModel",
                },
                {"contract_id": "experiment-run-v1", "instance_path": "#/$defs/ExperimentManifestReferenceModel"},
            ],
        )
        return json_schema


class ExperimentProcessorReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a processor identity."""

    ref_kind: Literal["processor"]

    @model_validator(mode="after")
    def _validate_identity_reference_scope(self) -> ExperimentProcessorReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("processor identity references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentBackendReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a backend identity."""

    ref_kind: Literal["backend"]

    @model_validator(mode="after")
    def _validate_identity_reference_scope(self) -> ExperimentBackendReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("backend identity references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentEvidenceReferenceModel(ExperimentReferenceModel):
    """Reference constrained to evidence artifacts."""

    ref_kind: Literal["evidence"]


class ExperimentEvidenceSatisfactionReferenceModel(ExperimentEvidenceReferenceModel):
    """Evidence concept reference that an artifact claims to satisfy."""

    @model_validator(mode="after")
    def _validate_satisfaction_reference_scope(self) -> ExperimentEvidenceSatisfactionReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("artifact satisfies_refs must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentRunEvidenceArtifactReferenceModel(ExperimentEvidenceReferenceModel):
    """Run-internal reference to a concrete evidence artifact id."""

    @model_validator(mode="after")
    def _validate_run_evidence_artifact_reference_scope(self) -> ExperimentRunEvidenceArtifactReferenceModel:
        if any(field in self.model_fields_set for field in ("ref_version", "ref_digest", "ref_path")):
            raise ValueError("run result evidence_refs are artifact-id references and must not carry qualifiers")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_version", None)
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentCaptureSpecReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a declarative capture specification."""

    ref_kind: Literal["capture-spec"]

    @model_validator(mode="after")
    def _validate_capture_spec_reference_scope(self) -> ExperimentCaptureSpecReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("capture-spec references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentEvidenceRecordReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a raw captured evidence record."""

    ref_kind: Literal["evidence-record"]

    @model_validator(mode="after")
    def _validate_evidence_record_reference_scope(self) -> ExperimentEvidenceRecordReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("evidence-record references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentDerivedMeasureReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a derived measure or analysis output."""

    ref_kind: Literal["derived-measure"]

    @model_validator(mode="after")
    def _validate_derived_measure_reference_scope(self) -> ExperimentDerivedMeasureReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("derived-measure references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentMeasurementChannelReferenceModel(ExperimentReferenceModel):
    """Reference constrained to a declared measurement channel."""

    ref_kind: Literal["measurement-channel"]

    @model_validator(mode="after")
    def _validate_measurement_channel_reference_scope(self) -> ExperimentMeasurementChannelReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("measurement-channel references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentApparatusCompatibilityReferenceModel(ExperimentReferenceModel):
    """Profile or capability reference declared by apparatus compatibility metadata."""

    ref_kind: Literal["profile", "capability"]

    @model_validator(mode="after")
    def _validate_compatibility_reference_scope(self) -> ExperimentApparatusCompatibilityReferenceModel:
        if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set:
            raise ValueError("apparatus compatibility references must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        properties = json_schema.get("properties")
        if isinstance(properties, dict):
            properties.pop("ref_digest", None)
            properties.pop("ref_path", None)
        return json_schema


class ExperimentConditionAssignmentReferenceModel(ExperimentReferenceModel):
    """Auditable run-level reference that can ground a study condition assignment."""

    ref_kind: Literal[
        "processor",
        "backend",
        "participant-implementation",
        "scenario-snapshot",
        "task",
        "apparatus-context",
        "manifest",
        "profile",
        "capability",
        "measurement-channel",
    ]

    @model_validator(mode="after")
    def _validate_condition_reference_scope(self) -> ExperimentConditionAssignmentReferenceModel:
        if self.ref_digest is not None or self.ref_path is not None:
            raise ValueError(
                "condition assignment references are run-level criteria and must not carry ref_digest or ref_path; "
                "use task evidence requirements or validated apparatus manifest refs for digest/path-bound evidence"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "properties": {
                    "ref_digest": {"type": "null"},
                    "ref_path": {"type": "null"},
                },
            }
        )
        return json_schema


def _manifest_reference_key(
    reference: ExperimentManifestReferenceModel,
) -> tuple[
    str,
    str | None,
    str | None,
    str | None,
    str | None,
    str | None,
    str | None,
    str | None,
    str | None,
]:
    subject_ref = reference.subject_ref
    return (
        reference.ref_id,
        reference.ref_version,
        _canonical_digest(reference.ref_digest),
        reference.ref_path,
        subject_ref.ref_kind if subject_ref is not None else None,
        subject_ref.ref_id if subject_ref is not None else None,
        subject_ref.ref_version if subject_ref is not None else None,
        _canonical_digest(subject_ref.ref_digest) if subject_ref is not None else None,
        subject_ref.ref_path if subject_ref is not None else None,
    )


def _reference_satisfies_requirement(
    candidate: ExperimentReferenceModel, requirement: ExperimentReferenceModel
) -> bool:
    if candidate.ref_kind != requirement.ref_kind or candidate.ref_id != requirement.ref_id:
        return False
    if requirement.ref_version is not None and candidate.ref_version != requirement.ref_version:
        return False
    if requirement.ref_digest is not None and _canonical_digest(candidate.ref_digest) != _canonical_digest(
        requirement.ref_digest
    ):
        return False
    if requirement.ref_path is not None and candidate.ref_path != requirement.ref_path:
        return False

    requirement_subject = getattr(requirement, "subject_ref", None)
    if requirement_subject is None:
        return True
    candidate_subject = getattr(candidate, "subject_ref", None)
    if candidate_subject is None:
        return False
    return _reference_satisfies_requirement(candidate_subject, requirement_subject)


def _reference_identity_satisfies_requirement(
    candidate: ExperimentReferenceModel, requirement: ExperimentReferenceModel
) -> bool:
    if candidate.ref_kind != requirement.ref_kind or candidate.ref_id != requirement.ref_id:
        return False
    if requirement.ref_version is not None and candidate.ref_version != requirement.ref_version:
        return False

    requirement_subject = getattr(requirement, "subject_ref", None)
    if requirement_subject is None:
        return True
    candidate_subject = getattr(candidate, "subject_ref", None)
    if candidate_subject is None:
        return False
    return _reference_identity_satisfies_requirement(candidate_subject, requirement_subject)


def _experiment_reference_key(
    reference: ExperimentReferenceModel,
) -> tuple[Any, ...]:
    subject_ref = getattr(reference, "subject_ref", None)
    return (
        reference.ref_kind,
        reference.ref_id,
        reference.ref_version,
        _canonical_digest(getattr(reference, "ref_digest", None)),
        getattr(reference, "ref_path", None),
        _experiment_reference_key(subject_ref) if subject_ref is not None else None,
    )


def _validate_unique_experiment_references(
    field_name: str,
    references: list[ExperimentReferenceModel],
) -> None:
    seen: set[tuple[Any, ...]] = set()
    duplicates: list[str] = []
    for reference in references:
        key = _experiment_reference_key(reference)
        if key in seen:
            duplicates.append(_format_reference(reference))
        seen.add(key)
    if duplicates:
        joined = ", ".join(sorted(set(duplicates)))
        raise ValueError(f"{field_name} must not contain duplicates: {joined}")


def _identity_matches_reference(identity: ApparatusIdentityModel, reference: ExperimentReferenceModel) -> bool:
    if reference.ref_digest is not None or reference.ref_path is not None:
        return False
    if identity.name != reference.ref_id:
        return False
    return reference.ref_version is None or identity.version == reference.ref_version


def _format_reference(reference: ExperimentReferenceModel) -> str:
    if reference.ref_version is None:
        return f"{reference.ref_kind}:{reference.ref_id}"
    return f"{reference.ref_kind}:{reference.ref_id}@{reference.ref_version}"


class ExperimentChecksumModel(ContractModel):
    """Checksum metadata for an experiment-core artifact reference."""

    algorithm: Literal["sha256", "sha384", "sha512", "blake3"]
    value: HexDigestString

    @model_validator(mode="after")
    def _validate_checksum_length(self) -> ExperimentChecksumModel:
        if re.fullmatch(_CHECKSUM_VALUE_PATTERNS[self.algorithm], self.value) is None:
            raise ValueError(f"checksum value must match {self.algorithm} hex digest length")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {"properties": {"algorithm": {"const": algorithm}}, "required": ["algorithm"]},
                    "then": {"properties": {"value": {"pattern": pattern}}},
                }
                for algorithm, pattern in _CHECKSUM_VALUE_PATTERNS.items()
            ]
        )
        return json_schema


class ExperimentArtifactRefModel(ContractModel):
    """Reference to an artifact that supports a task, run, apparatus, or study."""

    artifact_id: NonEmptyString
    role: Literal[
        "protocol",
        "metric-definition",
        "scenario-snapshot",
        "manifest",
        "apparatus-evidence",
        "observation",
        "result",
        "analysis",
        "report",
        "export",
        "starter-file",
        "evaluator",
        "subtask",
        "gold-step",
        "milestone",
        "human-assistance",
        "scaffold",
        "baseline",
        "cost-resource-trace",
        "documentation",
        "operator-guide",
        "configuration",
        "profile",
        "dataset",
        "other",
    ]
    media_type: NonEmptyString
    uri: NonEmptyString
    checksum: ExperimentChecksumModel
    size_bytes: NonNegativeInteger
    created_at: Rfc3339DateTimeString
    source: NonEmptyString
    satisfies_refs: list[ExperimentEvidenceSatisfactionReferenceModel] = Field(default_factory=list)
    sensitivity: Literal["public", "internal", "restricted", "redacted"]
    description: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_artifact_created_at(self) -> ExperimentArtifactRefModel:
        _parse_rfc3339_datetime("created_at", self.created_at)
        return self


AssociatedArtifactSetDigestString = Annotated[str, Field(pattern=r"^sha256:[a-f0-9]{64}$")]
_ASSOCIATED_ARTIFACT_SECRET_QUERY_NAMES = frozenset(
    {
        "access_token",
        "api_key",
        "apikey",
        "auth",
        "credential",
        "key",
        "password",
        "secret",
        "sig",
        "signature",
        "token",
    }
)
_ASSOCIATED_ARTIFACT_SECRET_QUERY_FRAGMENTS = (
    "api-key",
    "api_key",
    "apikey",
    "credential",
    "password",
    "secret",
    "signature",
    "token",
)


def _validate_associated_artifact_uri(artifact_id: str, uri: str) -> None:
    parsed = urlsplit(uri)
    if not parsed.scheme or (parsed.scheme in {"http", "https"} and not parsed.netloc):
        raise ValueError(f"associated artifact {artifact_id!r} uri must be an absolute URI")
    if parsed.username is not None or parsed.password is not None:
        raise ValueError(f"associated artifact {artifact_id!r} uri must not contain credential userinfo")
    query_names = {name.casefold() for name, _value in parse_qsl(parsed.query, keep_blank_values=True)}
    secret_names = {
        name
        for name in query_names
        if name in _ASSOCIATED_ARTIFACT_SECRET_QUERY_NAMES
        or any(fragment in name for fragment in _ASSOCIATED_ARTIFACT_SECRET_QUERY_FRAGMENTS)
    }
    if secret_names:
        raise ValueError(f"associated artifact {artifact_id!r} uri must not contain secret-bearing query fields")


class AssociatedArtifactManifestModel(ContractModel):
    """One exact non-semantic artifact-reference set attached to one parent."""

    schema_version: Literal[ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION]
    manifest_id: NonEmptyString
    manifest_version: NonEmptyString
    canonicalization_profile: Literal["associated-artifact-set/v1"]
    scope: Literal["scenario", "experiment"]
    parent_ref: AssociatedArtifactParentReferenceModel
    artifacts: dict[NonEmptyString, ExperimentArtifactRefModel] = Field(min_length=1)
    set_digest: AssociatedArtifactSetDigestString

    @model_validator(mode="after")
    def _validate_associated_artifact_manifest(self) -> AssociatedArtifactManifestModel:
        scenario_kinds = {"scenario", "scenario-snapshot"}
        parent_is_scenario = self.parent_ref.ref_kind in scenario_kinds
        if (self.scope == "scenario") != parent_is_scenario:
            raise ValueError("associated-artifact scope and parent kind must agree")

        descriptor_owners: dict[tuple[Any, ...], str] = {}
        locator_claims: dict[str, tuple[Any, ...]] = {}
        for artifact_key, artifact in self.artifacts.items():
            if artifact_key != artifact.artifact_id:
                raise ValueError(
                    f"associated artifact map key {artifact_key!r} must equal embedded artifact_id "
                    f"{artifact.artifact_id!r}"
                )
            _validate_associated_artifact_uri(artifact.artifact_id, artifact.uri)
            descriptor_key = (
                artifact.role,
                artifact.media_type,
                artifact.uri,
                artifact.checksum.algorithm,
                artifact.checksum.value.casefold(),
                artifact.size_bytes,
                artifact.created_at,
                artifact.source,
                tuple(ref.model_dump_json() for ref in artifact.satisfies_refs),
                artifact.sensitivity,
                artifact.description,
            )
            prior_descriptor_owner = descriptor_owners.get(descriptor_key)
            if prior_descriptor_owner is not None:
                raise ValueError(
                    f"associated artifacts {prior_descriptor_owner!r} and {artifact.artifact_id!r} are exact "
                    "descriptor aliases; duplicate descriptors require one stable artifact id"
                )
            descriptor_owners[descriptor_key] = artifact.artifact_id

            locator_claim = (
                artifact.media_type,
                artifact.checksum.algorithm,
                artifact.checksum.value.casefold(),
                artifact.size_bytes,
            )
            prior_locator_claim = locator_claims.get(artifact.uri)
            if prior_locator_claim is not None and prior_locator_claim != locator_claim:
                raise ValueError("one associated-artifact locator must not carry conflicting payload claims")
            locator_claims[artifact.uri] = locator_claim
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {"properties": {"scope": {"const": "scenario"}}, "required": ["scope"]},
                    "then": {
                        "properties": {
                            "parent_ref": {
                                "properties": {
                                    "ref_kind": {"enum": ["scenario", "scenario-snapshot"]},
                                }
                            }
                        }
                    },
                },
                {
                    "if": {"properties": {"scope": {"const": "experiment"}}, "required": ["scope"]},
                    "then": {
                        "properties": {
                            "parent_ref": {
                                "properties": {
                                    "ref_kind": {
                                        "enum": ["task", "authoring-input", "apparatus-context", "run", "study"]
                                    },
                                }
                            }
                        }
                    },
                },
            ]
        )
        _add_aces_invariant(
            json_schema,
            "associated-artifact-parent-set-and-byte-binding",
            "Full conformance requires matching the concrete parent, recomputing the canonical set digest, "
            "and binding every checksum and size to an explicitly supplied bounded byte stream.",
            validator="aces_contracts.associated_artifacts.validate_associated_artifact_manifest",
            inputs=[{"contract_id": "associated-artifact-manifest-v1", "instance_path": "#"}],
        )
        return json_schema


class ExperimentValidityNoteModel(ContractModel):
    """Validity threat, limitation, or mitigation note for experiment interpretation."""

    category: Literal[
        "construct",
        "internal",
        "external",
        "conclusion",
        "statistical",
        "apparatus",
        "reproducibility",
        "security",
        "other",
    ]
    note: NonEmptyString
    mitigation: NonEmptyString | None = None


class ExperimentCaptureWindowModel(ContractModel):
    """Declarative scope/window over which evidence must be captured."""

    window_id: NonEmptyString
    window_kind: Literal["task", "run", "apparatus", "event", "interval", "manual"]
    starts_at: Rfc3339DateTimeString | None = None
    ends_at: Rfc3339DateTimeString | None = None
    trigger_ref: ExperimentReferenceModel | None = None
    description: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_capture_window(self) -> ExperimentCaptureWindowModel:
        if self.starts_at is None and self.ends_at is None and self.trigger_ref is None:
            raise ValueError("capture windows must declare starts_at, ends_at, or trigger_ref")
        if self.starts_at is not None and self.ends_at is not None:
            starts_at = _parse_rfc3339_datetime("starts_at", self.starts_at)
            ends_at = _parse_rfc3339_datetime("ends_at", self.ends_at)
            if ends_at < starts_at:
                raise ValueError("capture window ends_at must be greater than or equal to starts_at")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["starts_at"], "properties": {"starts_at": {"not": {"type": "null"}}}},
                {"required": ["ends_at"], "properties": {"ends_at": {"not": {"type": "null"}}}},
                {"required": ["trigger_ref"], "properties": {"trigger_ref": {"not": {"type": "null"}}}},
            ]
        )
        _add_aces_invariant(
            json_schema,
            "capture-window-interval-valid",
            "Capture window ends_at must not precede starts_at when both timestamps are present.",
            validator="aces_contracts.contracts.ExperimentCaptureWindowModel._validate_capture_window",
            inputs=[{"contract_id": "experiment-capture-spec-v1", "instance_path": "#/capture_windows"}],
        )
        return json_schema


class ExperimentCaptureRequirementModel(ContractModel):
    """One evidence capture requirement inside a capture specification."""

    requirement_id: NonEmptyString
    title: NonEmptyString
    capture_kind: Literal["artifact", "observation", "trace", "telemetry", "log", "packet-capture", "other"]
    capture_scope: Literal["task", "run", "apparatus", "participant", "backend", "processor", "network", "service"]
    channel_ref: ExperimentMeasurementChannelReferenceModel
    window_refs: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    expected_media_types: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    required_artifact_roles: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True})
    sensitivity: Literal["public", "internal", "restricted", "redacted"]
    redaction_policy: NonEmptyString | None = None
    integrity_requirements: list[NonEmptyString] = Field(min_length=1)
    retention_policy: NonEmptyString | None = None
    loss_disclosure_required: bool = True
    notes: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_capture_requirement(self) -> ExperimentCaptureRequirementModel:
        _validate_unique_string_values("window_refs", self.window_refs)
        _validate_unique_string_values("expected_media_types", self.expected_media_types)
        _validate_unique_string_values("required_artifact_roles", self.required_artifact_roles)
        return self


class ExperimentCaptureSpecModel(ContractModel):
    """Declarative EXP-707 specification of what experiment evidence to capture."""

    schema_version: Literal[EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION]
    capture_spec_id: NonEmptyString
    spec_version: NonEmptyString
    title: NonEmptyString
    description: NonEmptyString
    scope_refs: list[ExperimentReferenceModel] = Field(min_length=1)
    capture_windows: list[ExperimentCaptureWindowModel] = Field(min_length=1)
    capture_requirements: dict[NonEmptyString, ExperimentCaptureRequirementModel] = Field(min_length=1)
    validity_notes: list[ExperimentValidityNoteModel] = Field(default_factory=list)
    artifact_refs: list[ExperimentArtifactRefModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_capture_spec(self) -> ExperimentCaptureSpecModel:
        mismatches = [
            requirement_key
            for requirement_key, requirement in self.capture_requirements.items()
            if requirement.requirement_id != requirement_key
        ]
        if mismatches:
            joined = ", ".join(sorted(mismatches))
            raise ValueError(f"capture_requirements keys must match embedded requirement_id: {joined}")
        window_ids = {window.window_id for window in self.capture_windows}
        missing_window_refs = sorted(
            {
                window_ref
                for requirement in self.capture_requirements.values()
                for window_ref in requirement.window_refs
                if window_ref not in window_ids
            }
        )
        if missing_window_refs:
            joined = ", ".join(missing_window_refs)
            raise ValueError(f"capture requirement window_refs must resolve to capture_windows: {joined}")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "capture-requirement-key-matches-requirement-id",
            "Every capture_requirements object key must match the embedded requirement_id value, and window_refs "
            "must resolve to declared capture_windows.",
            validator="aces_contracts.contracts.ExperimentCaptureSpecModel._validate_capture_spec",
            inputs=[{"contract_id": "experiment-capture-spec-v1", "instance_path": "#"}],
        )
        _add_aces_plane(json_schema, "experiment-capture-spec-v1")
        return json_schema


class ExperimentRawEvidenceContentModel(ContractModel):
    """Raw captured payload reference or bounded summary for EXP-708 records."""

    artifact_ref: ExperimentArtifactRefModel | None = None
    content_uri: NonEmptyString | None = None
    content_checksum: ExperimentChecksumModel | None = None
    payload_summary: NonEmptyString | None = None
    loss_disclosure: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_raw_content(self) -> ExperimentRawEvidenceContentModel:
        if self.artifact_ref is None and self.content_uri is None and self.payload_summary is None:
            raise ValueError("raw evidence content must include artifact_ref, content_uri, or payload_summary")
        if self.content_uri is not None and self.content_checksum is None:
            raise ValueError("content_uri raw evidence must include content_checksum")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["artifact_ref"], "properties": {"artifact_ref": {"not": {"type": "null"}}}},
                {"required": ["content_uri"], "properties": {"content_uri": {"not": {"type": "null"}}}},
                {"required": ["payload_summary"], "properties": {"payload_summary": {"not": {"type": "null"}}}},
            ]
        )
        json_schema.setdefault("allOf", []).append(
            {
                "if": {"required": ["content_uri"], "properties": {"content_uri": {"not": {"type": "null"}}}},
                "then": {
                    "required": ["content_checksum"],
                    "properties": {"content_checksum": {"not": {"type": "null"}}},
                },
            }
        )
        return json_schema


class ExperimentEvidenceRecordModel(ContractModel):
    """Raw captured EXP-708 evidence record, distinct from derived measures."""

    schema_version: Literal[EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION]
    evidence_record_id: NonEmptyString
    record_version: NonEmptyString
    capture_spec_ref: ExperimentCaptureSpecReferenceModel
    capture_requirement_ref: NonEmptyString
    run_ref: ExperimentReferenceModel
    task_ref: ExperimentTaskReferenceModel | None = None
    apparatus_context_ref: ExperimentReferenceModel | None = None
    source_refs: list[ExperimentReferenceModel] = Field(min_length=1)
    evidence_kind: Literal["artifact", "observation", "trace", "telemetry", "log", "packet-capture", "other"]
    captured_at: Rfc3339DateTimeString
    capture_window_ref: NonEmptyString
    raw_content: ExperimentRawEvidenceContentModel
    sensitivity: Literal["public", "internal", "restricted", "redacted"]
    redaction_state: Literal["none", "redacted", "withheld"]
    provenance_refs: list[ExperimentReferenceModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_evidence_record(self) -> ExperimentEvidenceRecordModel:
        _parse_rfc3339_datetime("captured_at", self.captured_at)
        if self.redaction_state != "none" and self.raw_content.loss_disclosure is None:
            raise ValueError("redacted or withheld evidence records must include raw_content.loss_disclosure")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "evidence-record-raw-content-present",
            "Evidence records must carry raw content as an artifact reference, content URI with checksum, or bounded "
            "payload summary; redacted/withheld records must disclose loss.",
            validator="aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record",
            inputs=[{"contract_id": "experiment-evidence-record-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "evidence-record-captured-at-valid",
            "captured_at must be a valid RFC 3339 date-time.",
            validator="aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record",
            inputs=[{"contract_id": "experiment-evidence-record-v1", "instance_path": "#/captured_at"}],
        )
        # SEM-216 B4: redacted or withheld evidence records must disclose redaction/loss at the
        # evidence boundary. Publish the model rule as a portable schema constraint so any
        # consumer validating against the JSON Schema enforces the disclosure, not just the model.
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"redaction_state": {"enum": ["redacted", "withheld"]}},
                    "required": ["redaction_state"],
                },
                "then": {
                    "required": ["raw_content"],
                    "properties": {
                        "raw_content": {
                            "required": ["loss_disclosure"],
                            "properties": {"loss_disclosure": {"type": "string", "minLength": 1}},
                        }
                    },
                },
            }
        )
        _add_aces_plane(json_schema, "experiment-evidence-record-v1")
        return json_schema


class ExperimentDerivedMeasureMethodModel(ContractModel):
    """Method metadata for deriving measures from raw evidence."""

    method_id: NonEmptyString
    method_version: NonEmptyString
    name: NonEmptyString
    description: NonEmptyString | None = None
    parameters: list[ExperimentParameterModel] = Field(default_factory=list)


class ExperimentDerivedMeasureModel(ContractModel):
    """EXP-709 derived measure/evaluation output computed from raw evidence."""

    schema_version: Literal[EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION]
    derived_measure_id: NonEmptyString
    measure_version: NonEmptyString
    measure_kind: Literal["metric", "evaluation", "score", "summary", "analysis-output", "other"]
    metric_ref: ExperimentReferenceModel
    method: ExperimentDerivedMeasureMethodModel
    source_evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(min_length=1)
    generated_at: Rfc3339DateTimeString
    value_status: Literal["reported", "missing", "withheld", "not-applicable"]
    value: str | int | float | bool | None = None
    uncertainty: NonEmptyString | None = None
    limitations: list[NonEmptyString] = Field(default_factory=list)
    provenance_refs: list[ExperimentReferenceModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_derived_measure(self) -> ExperimentDerivedMeasureModel:
        _parse_rfc3339_datetime("generated_at", self.generated_at)
        _validate_reported_value_status(
            self.value_status,
            self.value,
            reported_message="reported derived measures must include value",
            non_reported_message="non-reported derived measures must not include value",
        )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _extend_reported_value_status_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "derived-measure-reported-value-present",
            "Reported derived measures must include a value; missing/withheld/not-applicable measures must not.",
            validator="aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure",
            inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "derived-measure-generated-at-valid",
            "generated_at must be a valid RFC 3339 date-time.",
            validator="aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure",
            inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#/generated_at"}],
        )
        _add_aces_plane(json_schema, "experiment-derived-measure-v1")
        return json_schema


class ExperimentRunTraceabilityModel(ContractModel):
    """Canonical run provenance links across capture, evidence, measures, and claims."""

    capture_spec_refs: list[ExperimentCaptureSpecReferenceModel] = Field(min_length=1)
    evidence_record_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(min_length=1)
    derived_measure_refs: list[ExperimentDerivedMeasureReferenceModel] = Field(default_factory=list)
    claim_refs: list[ExperimentReferenceModel] = Field(default_factory=list)
    notes: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_run_traceability(self) -> ExperimentRunTraceabilityModel:
        _validate_unique_experiment_references("traceability capture_spec_refs", self.capture_spec_refs)
        _validate_unique_experiment_references("traceability evidence_record_refs", self.evidence_record_refs)
        _validate_unique_experiment_references("traceability derived_measure_refs", self.derived_measure_refs)
        _validate_unique_experiment_references("traceability claim_refs", self.claim_refs)
        if self.claim_refs and not self.derived_measure_refs:
            raise ValueError("traceability claim_refs require at least one derived_measure_refs entry")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "run-traceability-refs-unique",
            "Run provenance traceability references must be duplicate-free, and claim refs must be grounded by "
            "at least one derived measure ref.",
            validator="aces_contracts.contracts.ExperimentRunTraceabilityModel._validate_run_traceability",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/traceability"}],
        )
        return json_schema


class ExperimentRealizedFormDisclosureModel(ContractModel):
    """Disclosure of one realized form chosen for an underspecified run concern."""

    concern_id: NonEmptyString
    concern_kind: Literal[
        "scenario-module",
        "processor-selection",
        "backend-selection",
        "participant-implementation",
        "apparatus-configuration",
        "parameter-default",
        "stochastic-control",
        "measurement-channel",
        "capture-window",
        "other",
    ]
    basis: Literal["author-declared", "processor-realized", "backend-realized", "operator-supplied", "observed"]
    realized_by_ref: ExperimentReferenceModel
    authored_ref: ExperimentReferenceModel | None = None
    realized_ref: ExperimentReferenceModel | None = None
    realized_value_summary: NonEmptyString | None = None
    disclosure: NonEmptyString
    evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_realized_form_disclosure(self) -> ExperimentRealizedFormDisclosureModel:
        if self.realized_ref is None and self.realized_value_summary is None:
            raise ValueError("realized form disclosures must include realized_ref or realized_value_summary")
        if self.basis == "processor-realized" and self.realized_by_ref.ref_kind != "processor":
            raise ValueError("processor-realized disclosures must use a processor realized_by_ref")
        if self.basis == "backend-realized" and self.realized_by_ref.ref_kind != "backend":
            raise ValueError("backend-realized disclosures must use a backend realized_by_ref")
        _validate_unique_experiment_references("realized form disclosure evidence_refs", self.evidence_refs)
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["realized_ref"], "properties": {"realized_ref": {"not": {"type": "null"}}}},
                {
                    "required": ["realized_value_summary"],
                    "properties": {"realized_value_summary": {"not": {"type": "null"}}},
                },
            ]
        )
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {"properties": {"basis": {"const": "processor-realized"}}, "required": ["basis"]},
                    "then": {
                        "properties": {
                            "realized_by_ref": {
                                "required": ["ref_kind"],
                                "properties": {"ref_kind": {"const": "processor"}},
                            }
                        }
                    },
                },
                {
                    "if": {"properties": {"basis": {"const": "backend-realized"}}, "required": ["basis"]},
                    "then": {
                        "properties": {
                            "realized_by_ref": {
                                "required": ["ref_kind"],
                                "properties": {"ref_kind": {"const": "backend"}},
                            }
                        }
                    },
                },
            ]
        )
        _add_aces_invariant(
            json_schema,
            "realized-form-disclosure-substantive",
            "Every realized-form disclosure must name a realized reference or value summary and use the right "
            "processor/backend realization authority for processor-realized and backend-realized concerns.",
            validator=(
                "aces_contracts.contracts.ExperimentRealizedFormDisclosureModel._validate_realized_form_disclosure"
            ),
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/realized_form_disclosures"}],
        )
        return json_schema


_SEM_225_PORTABLE_CARRIER_KINDS = frozenset(
    {
        "apparatus-context",
        "capture-spec",
        "derived-measure",
        "evidence-record",
        "manifest",
        "measurement-channel",
        "profile",
        "run",
        "scenario-snapshot",
    }
)


def _validate_sem_225_claim_evidence(
    classifications: set[str],
    evidence_refs: list[ExperimentEvidenceRecordReferenceModel],
) -> None:
    if classifications - {"apparatus_only"} and not evidence_refs:
        raise ValueError("environment, participant, or comparability augmentations require evidence_refs")


def _validate_sem_225_environment_visible(disclosure: ExperimentAugmentationDisclosureModel) -> None:
    if disclosure.environment_effect is None:
        raise ValueError("environment_visible augmentation disclosures require environment_effect")
    if not any(ref.ref_kind in _SEM_225_PORTABLE_CARRIER_KINDS for ref in disclosure.carrier_refs):
        raise ValueError("environment_visible augmentation disclosures require a portable carrier_ref")


def _validate_sem_225_participant_visible(disclosure: ExperimentAugmentationDisclosureModel) -> None:
    if disclosure.participant_visibility is None:
        raise ValueError("participant_visible augmentation disclosures require participant_visibility")
    if not disclosure.markings:
        raise ValueError("participant_visible augmentation disclosures require markings")


def _validate_sem_225_comparability_relevant(disclosure: ExperimentAugmentationDisclosureModel) -> None:
    if disclosure.comparability_effect is None:
        raise ValueError("comparability_relevant augmentation disclosures require comparability_effect")
    if disclosure.observer_effect is None:
        raise ValueError("comparability_relevant augmentation disclosures require observer_effect")


class ExperimentAugmentationDisclosureModel(ContractModel):
    """Disclosure for processor/backend augmentation used by a run."""

    augmentation_id: NonEmptyString
    purpose: Literal["evidence", "evaluation", "operational", "comparability", "other"]
    realization_layer: Literal[
        "processor",
        "backend",
        "apparatus",
        "runtime-environment",
        "participant-runtime",
        "measurement-channel",
        "analysis",
        "other",
    ]
    classifications: list[
        Literal["apparatus_only", "environment_visible", "participant_visible", "comparability_relevant"]
    ] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    augmented_by_ref: ExperimentReferenceModel
    carrier_refs: list[ExperimentReferenceModel] = Field(min_length=1)
    affected_refs: list[ExperimentReferenceModel] = Field(default_factory=list)
    evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(default_factory=list)
    disclosure_policy: NonEmptyString
    markings: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True})
    observer_effect: NonEmptyString | None = None
    environment_effect: NonEmptyString | None = None
    participant_visibility: NonEmptyString | None = None
    comparability_effect: NonEmptyString | None = None
    notes: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_augmentation_disclosure(self) -> ExperimentAugmentationDisclosureModel:
        _validate_unique_string_values("augmentation classifications", self.classifications)
        _validate_unique_string_values("augmentation disclosure markings", self.markings)
        _validate_unique_string_values("augmentation disclosure notes", self.notes)
        _validate_unique_experiment_references("augmentation disclosure carrier_refs", self.carrier_refs)
        _validate_unique_experiment_references("augmentation disclosure affected_refs", self.affected_refs)
        _validate_unique_experiment_references("augmentation disclosure evidence_refs", self.evidence_refs)

        if self.augmented_by_ref.ref_kind not in {"processor", "backend"}:
            raise ValueError("augmentation disclosures must use a processor or backend augmented_by_ref")

        classification_set = set(self.classifications)
        _validate_sem_225_claim_evidence(classification_set, self.evidence_refs)
        if "environment_visible" in classification_set:
            _validate_sem_225_environment_visible(self)
        if "participant_visible" in classification_set:
            _validate_sem_225_participant_visible(self)
        if "comparability_relevant" in classification_set:
            _validate_sem_225_comparability_relevant(self)
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "properties": {
                        "augmented_by_ref": {
                            "required": ["ref_kind"],
                            "properties": {"ref_kind": {"enum": ["processor", "backend"]}},
                        }
                    }
                },
                {
                    "if": {
                        "properties": {"classifications": {"contains": {"const": "environment_visible"}}},
                        "required": ["classifications"],
                    },
                    "then": {
                        "required": ["carrier_refs", "environment_effect", "evidence_refs"],
                        "properties": {
                            "environment_effect": {"type": "string", "minLength": 1},
                            "carrier_refs": {
                                "contains": {
                                    "required": ["ref_kind"],
                                    "properties": {"ref_kind": {"enum": sorted(_SEM_225_PORTABLE_CARRIER_KINDS)}},
                                }
                            },
                            "evidence_refs": {"minItems": 1},
                        },
                    },
                },
                {
                    "if": {
                        "properties": {"classifications": {"contains": {"const": "participant_visible"}}},
                        "required": ["classifications"],
                    },
                    "then": {
                        "required": ["participant_visibility", "markings", "evidence_refs"],
                        "properties": {
                            "participant_visibility": {"type": "string", "minLength": 1},
                            "markings": {"minItems": 1},
                            "evidence_refs": {"minItems": 1},
                        },
                    },
                },
                {
                    "if": {
                        "properties": {"classifications": {"contains": {"const": "comparability_relevant"}}},
                        "required": ["classifications"],
                    },
                    "then": {
                        "required": ["comparability_effect", "observer_effect", "evidence_refs"],
                        "properties": {
                            "comparability_effect": {"type": "string", "minLength": 1},
                            "observer_effect": {"type": "string", "minLength": 1},
                            "evidence_refs": {"minItems": 1},
                        },
                    },
                },
            ]
        )
        _add_aces_invariant(
            json_schema,
            "augmentation-disclosure-semantics-valid",
            "Augmentation disclosures must keep environment-visible, participant-visible, and "
            "comparability-relevant semantics explicit and must use processor/backend authority.",
            validator=(
                "aces_contracts.contracts.ExperimentAugmentationDisclosureModel._validate_augmentation_disclosure"
            ),
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/augmentation_disclosures"}],
        )
        return json_schema


class ExperimentMetricDefinitionModel(ContractModel):
    """Metric definition bound to a measured construct and unit of analysis."""

    metric_id: NonEmptyString
    metric_version: NonEmptyString
    name: NonEmptyString
    measured_construct: NonEmptyString
    unit_of_analysis: NonEmptyString
    value_kind: Literal["boolean", "integer", "number", "duration", "count", "category", "text"]
    direction: Literal["higher-is-better", "lower-is-better", "target", "descriptive"]
    aggregation: NonEmptyString | None = None
    missingness_policy: NonEmptyString | None = None
    uncertainty_policy: NonEmptyString | None = None
    evidence_requirements: list[ExperimentEvidenceReferenceModel] = Field(min_length=1)


class ExperimentEvaluationProtocolModel(ContractModel):
    """Evaluation protocol that binds metrics and observation requirements."""

    protocol_id: NonEmptyString
    protocol_version: NonEmptyString
    intent: NonEmptyString
    unit_of_analysis: NonEmptyString
    metric_definitions: dict[NonEmptyString, ExperimentMetricDefinitionModel] = Field(min_length=1)
    observation_requirements: list[ExperimentEvidenceReferenceModel] = Field(min_length=1)
    aggregation_policy: NonEmptyString | None = None
    acceptance_policy: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_metric_definition_keys(self) -> ExperimentEvaluationProtocolModel:
        mismatches = [
            metric_key
            for metric_key, definition in self.metric_definitions.items()
            if definition.metric_id != metric_key
        ]
        if mismatches:
            joined = ", ".join(sorted(mismatches))
            raise ValueError(f"metric_definitions keys must match embedded metric_id: {joined}")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "metric-definition-key-matches-metric-id",
            "Every metric_definitions object key must match the embedded metric_id value.",
            validator="aces_contracts.contracts.ExperimentEvaluationProtocolModel._validate_metric_definition_keys",
            inputs=[{"contract_id": "experiment-task-v1", "instance_path": "#/evaluation_protocol"}],
        )
        return json_schema


class ExperimentSplitAndLeakageControlsModel(ContractModel):
    """Controls for data partitioning, hidden material, and leakage risk."""

    partitioning_strategy: NonEmptyString | None = None
    grouping_constraints: list[NonEmptyString] = Field(default_factory=list)
    temporal_availability: NonEmptyString | None = None
    hidden_material_policy: NonEmptyString | None = None
    leakage_checks: list[NonEmptyString] = Field(default_factory=list)
    unresolved_risks: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_disclosure_surface(self) -> ExperimentSplitAndLeakageControlsModel:
        if not any(
            (
                self.partitioning_strategy,
                self.grouping_constraints,
                self.temporal_availability,
                self.hidden_material_policy,
                self.leakage_checks,
                self.unresolved_risks,
            )
        ):
            raise ValueError("split_and_leakage_controls must disclose at least one control, policy, or risk")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {
                    "required": ["partitioning_strategy"],
                    "properties": {"partitioning_strategy": {"not": {"type": "null"}}},
                },
                {
                    "required": ["temporal_availability"],
                    "properties": {"temporal_availability": {"not": {"type": "null"}}},
                },
                {
                    "required": ["hidden_material_policy"],
                    "properties": {"hidden_material_policy": {"not": {"type": "null"}}},
                },
                {"required": ["grouping_constraints"], "properties": {"grouping_constraints": {"minItems": 1}}},
                {"required": ["leakage_checks"], "properties": {"leakage_checks": {"minItems": 1}}},
                {"required": ["unresolved_risks"], "properties": {"unresolved_risks": {"minItems": 1}}},
            ]
        )
        return json_schema


class ExperimentApparatusConstraintModel(ContractModel):
    """Apparatus compatibility and capability constraints for a task."""

    allowed_processor_refs: list[ExperimentProcessorReferenceModel] = Field(default_factory=list)
    allowed_backend_refs: list[ExperimentBackendReferenceModel] = Field(default_factory=list)
    required_manifest_refs: list[ExperimentManifestReferenceModel] = Field(default_factory=list)
    required_capabilities: list[NonEmptyString] = Field(default_factory=list)
    notes: list[NonEmptyString] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_allowed_identity_manifest_refs(self) -> ExperimentApparatusConstraintModel:
        if not any(
            (
                self.allowed_processor_refs,
                self.allowed_backend_refs,
                self.required_manifest_refs,
                self.required_capabilities,
                self.notes,
            )
        ):
            raise ValueError("apparatus_constraints must declare at least one apparatus constraint or disclosure note")
        for manifest in self.required_manifest_refs:
            subject_ref = manifest.subject_ref
            if subject_ref is None or subject_ref.ref_kind not in {"processor", "backend"}:
                continue
            expected_manifest_version = (
                PROCESSOR_MANIFEST_V2_SCHEMA_VERSION
                if subject_ref.ref_kind == "processor"
                else BACKEND_MANIFEST_V2_SCHEMA_VERSION
            )
            if manifest.ref_version == expected_manifest_version:
                if manifest.ref_id != subject_ref.ref_id:
                    raise ValueError("processor/backend required_manifest_refs ref_id must match subject_ref.ref_id")
                if subject_ref.ref_digest is not None or subject_ref.ref_path is not None:
                    raise ValueError(
                        "processor/backend required_manifest_refs subject_ref must not carry ref_digest or ref_path"
                    )
        required_manifest_keys = {
            (
                manifest.subject_ref.ref_kind,
                manifest.subject_ref.ref_id,
                manifest.subject_ref.ref_version,
                manifest.ref_version,
            )
            for manifest in self.required_manifest_refs
            if manifest.subject_ref is not None
        }
        for ref in self.allowed_processor_refs:
            if (
                "processor",
                ref.ref_id,
                ref.ref_version,
                PROCESSOR_MANIFEST_V2_SCHEMA_VERSION,
            ) not in required_manifest_keys:
                raise ValueError(
                    "allowed_processor_refs entries must have a matching required_manifest_refs "
                    f"entry with processor subject_ref='{ref.ref_id}' and "
                    f"manifest ref_version='{PROCESSOR_MANIFEST_V2_SCHEMA_VERSION}'"
                )
        for ref in self.allowed_backend_refs:
            if (
                "backend",
                ref.ref_id,
                ref.ref_version,
                BACKEND_MANIFEST_V2_SCHEMA_VERSION,
            ) not in required_manifest_keys:
                raise ValueError(
                    "allowed_backend_refs entries must have a matching required_manifest_refs "
                    f"entry with backend subject_ref='{ref.ref_id}' and "
                    f"manifest ref_version='{BACKEND_MANIFEST_V2_SCHEMA_VERSION}'"
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["allowed_processor_refs"], "properties": {"allowed_processor_refs": {"minItems": 1}}},
                {"required": ["allowed_backend_refs"], "properties": {"allowed_backend_refs": {"minItems": 1}}},
                {"required": ["required_manifest_refs"], "properties": {"required_manifest_refs": {"minItems": 1}}},
                {"required": ["required_capabilities"], "properties": {"required_capabilities": {"minItems": 1}}},
                {"required": ["notes"], "properties": {"notes": {"minItems": 1}}},
            ]
        )
        _add_aces_invariant(
            json_schema,
            "apparatus-constraint-identity-manifest-resolves",
            "Every allowed processor/backend identity reference must have a matching required manifest ref_id "
            "with matching manifest id, subject identity, and manifest schema version.",
            validator="aces_contracts.contracts.ExperimentApparatusConstraintModel._validate_allowed_identity_manifest_refs",
            inputs=[{"contract_id": "experiment-task-v1", "instance_path": "#/apparatus_constraints"}],
        )
        return json_schema


class ExperimentTaskModel(ContractModel):
    """Experiment task contract that separates scenario material from protocol intent."""

    schema_version: Literal[EXPERIMENT_TASK_SCHEMA_VERSION]
    task_id: NonEmptyString
    task_version: NonEmptyString
    title: NonEmptyString
    description: NonEmptyString
    scenario_ref: ExperimentScenarioReferenceModel
    evaluation_protocol: ExperimentEvaluationProtocolModel
    intended_use: NonEmptyString
    non_use: list[NonEmptyString] = Field(default_factory=list)
    population_or_construct: NonEmptyString
    split_and_leakage_controls: ExperimentSplitAndLeakageControlsModel
    apparatus_constraints: ExperimentApparatusConstraintModel
    validity_notes: list[ExperimentValidityNoteModel] = Field(min_length=1)
    artifact_refs: list[ExperimentArtifactRefModel] = Field(min_length=1)


class ExperimentParameterModel(ContractModel):
    """Redaction-aware parameter captured for a task, run, or apparatus context."""

    name: NonEmptyString
    value: str | int | float | bool | None
    value_kind: Literal["configuration", "protocol", "apparatus", "analysis", "other"]
    redaction: Literal["none", "redacted", "withheld"] = "none"

    @model_validator(mode="after")
    def _validate_redacted_parameter_value(self) -> ExperimentParameterModel:
        if self.redaction != "none" and self.value is not None:
            raise ValueError("redacted or withheld experiment parameters must not include concrete values")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"redaction": {"enum": ["redacted", "withheld"]}},
                    "required": ["redaction"],
                },
                "then": {"properties": {"value": {"type": "null"}}},
            }
        )
        return json_schema


class ExperimentConditionAssignmentParameterModel(ExperimentParameterModel):
    """Auditable parameter value that can ground a study condition assignment."""

    value_kind: Literal["configuration", "protocol", "apparatus", "analysis"]
    redaction: Literal["none"] = "none"

    @model_validator(mode="after")
    def _validate_auditable_condition_parameter(self) -> ExperimentConditionAssignmentParameterModel:
        if self.redaction != "none":
            raise ValueError("condition assignment parameters must not be redacted or withheld")
        return self


class ExperimentStochasticControlModel(ContractModel):
    """Seed, randomization, sampling, or scheduler control for reproducibility."""

    control_id: NonEmptyString
    role: Literal["seed", "randomization", "sampling", "scheduler", "agent-policy", "other"]
    value: str | int | None = None
    description: NonEmptyString | None = None


class ExperimentClockContextModel(ContractModel):
    """Clock authority and time-domain metadata for run interpretation."""

    clock_id: NonEmptyString
    authority: NonEmptyString
    time_domain: Literal["wall-clock", "monotonic", "simulated", "logical", "other"]
    synchronization: NonEmptyString | None = None


class ExperimentApparatusComponentModel(ContractModel):
    """Identity and manifest context for one apparatus component."""

    component_kind: Literal[
        "processor",
        "backend",
        "participant-implementation",
        "host",
        "container",
        "vm",
        "network",
        "device",
        "measurement-channel",
        "other",
    ]
    identity: ApparatusIdentityModel
    manifest_ref: ExperimentManifestReferenceModel | None = None
    compatibility_refs: list[ExperimentApparatusCompatibilityReferenceModel] = Field(default_factory=list)
    observed: bool = False
    limitations: list[NonEmptyString] = Field(default_factory=list)


class ExperimentApparatusContextModel(ContractModel):
    """Run-scoped apparatus context for interpreting experiment evidence."""

    schema_version: Literal[EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION]
    apparatus_context_id: NonEmptyString
    context_version: NonEmptyString
    declared_at: Rfc3339DateTimeString
    components: dict[NonEmptyString, ExperimentApparatusComponentModel] = Field(min_length=2)
    selected_manifests: list[ExperimentManifestReferenceModel] = Field(min_length=1)
    compatibility_declarations: list[ExperimentApparatusCompatibilityReferenceModel] = Field(min_length=1)
    configuration_parameters: list[ExperimentParameterModel] = Field(min_length=1)
    stochastic_controls: list[ExperimentStochasticControlModel] = Field(min_length=1)
    clocks: list[ExperimentClockContextModel] = Field(min_length=1)
    measurement_channels: list[ExperimentMeasurementChannelReferenceModel] = Field(min_length=1)
    observed_setup_evidence: list[ExperimentArtifactRefModel] = Field(min_length=1)
    known_limitations: list[ExperimentValidityNoteModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_instrument_context(self) -> ExperimentApparatusContextModel:
        _parse_rfc3339_datetime("declared_at", self.declared_at)
        processor = self.components.get("processor")
        backend = self.components.get("backend")
        if processor is None or processor.component_kind != "processor":
            raise ValueError(
                "apparatus components must include a 'processor' component with component_kind='processor'"
            )
        if backend is None or backend.component_kind != "backend":
            raise ValueError("apparatus components must include a 'backend' component with component_kind='backend'")
        selected_manifest_subject_keys: dict[tuple[str, str, str | None, str | None], str] = {}
        for selected_manifest in self.selected_manifests:
            selected_subject_ref = selected_manifest.subject_ref
            if selected_subject_ref is None:
                continue
            selected_subject_key = (
                selected_subject_ref.ref_kind,
                selected_subject_ref.ref_id,
                selected_subject_ref.ref_version,
                selected_manifest.ref_version,
            )
            prior_manifest_id = selected_manifest_subject_keys.get(selected_subject_key)
            if prior_manifest_id is not None:
                raise ValueError(
                    "selected_manifests must not contain multiple manifest refs for the same subject identity "
                    f"and manifest schema version: {selected_subject_ref.ref_kind}:{selected_subject_ref.ref_id}"
                )
            selected_manifest_subject_keys[selected_subject_key] = selected_manifest.ref_id
        selected_manifest_keys = {_manifest_reference_key(ref) for ref in self.selected_manifests}
        canonical_component_manifest_keys: set[
            tuple[
                str,
                str | None,
                str | None,
                str | None,
                str | None,
                str | None,
                str | None,
                str | None,
                str | None,
            ]
        ] = set()
        for key, component in (("processor", processor), ("backend", backend)):
            if component.manifest_ref is None:
                raise ValueError(f"apparatus component '{key}' must include manifest_ref")
            subject_ref = component.manifest_ref.subject_ref
            if subject_ref is None:
                raise ValueError(f"apparatus component '{key}' manifest_ref must include subject_ref")
            if subject_ref.ref_kind != key:
                raise ValueError(f"apparatus component '{key}' manifest_ref subject_ref must use ref_kind='{key}'")
            if component.manifest_ref.ref_id != component.identity.name:
                raise ValueError(f"apparatus component '{key}' manifest_ref ref_id must match component identity")
            if subject_ref.ref_id != component.identity.name or subject_ref.ref_version != component.identity.version:
                raise ValueError(f"apparatus component '{key}' manifest_ref subject_ref must match component identity")
            if subject_ref.ref_digest is not None or subject_ref.ref_path is not None:
                raise ValueError(
                    f"apparatus component '{key}' manifest_ref subject_ref must not carry ref_digest or ref_path"
                )
            expected_manifest_version = (
                PROCESSOR_MANIFEST_V2_SCHEMA_VERSION if key == "processor" else BACKEND_MANIFEST_V2_SCHEMA_VERSION
            )
            if component.manifest_ref.ref_version != expected_manifest_version:
                raise ValueError(
                    f"apparatus component '{key}' manifest_ref must use ref_version='{expected_manifest_version}'"
                )
            component_manifest_key = _manifest_reference_key(component.manifest_ref)
            canonical_component_manifest_keys.add(component_manifest_key)
            if component_manifest_key not in selected_manifest_keys:
                raise ValueError(f"apparatus component '{key}' manifest_ref must be present in selected_manifests")
        participant_components = {
            key: component
            for key, component in self.components.items()
            if component.component_kind == "participant-implementation"
        }
        for key, component in participant_components.items():
            if component.manifest_ref is None:
                raise ValueError(f"participant implementation component '{key}' must include manifest_ref")
            subject_ref = component.manifest_ref.subject_ref
            if subject_ref is None:
                raise ValueError(f"participant implementation component '{key}' manifest_ref must include subject_ref")
            if subject_ref.ref_kind != "participant-implementation":
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref subject_ref must use "
                    "ref_kind='participant-implementation'"
                )
            if component.manifest_ref.ref_id != component.identity.name:
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref ref_id must match component identity"
                )
            if subject_ref.ref_id != component.identity.name or subject_ref.ref_version != component.identity.version:
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref subject_ref must match "
                    "component identity"
                )
            if subject_ref.ref_digest is not None or subject_ref.ref_path is not None:
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref subject_ref must not carry "
                    "ref_digest or ref_path"
                )
            if component.manifest_ref.ref_version != PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION:
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref must use "
                    f"ref_version='{PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION}'"
                )
            if _manifest_reference_key(component.manifest_ref) not in selected_manifest_keys:
                raise ValueError(
                    f"participant implementation component '{key}' manifest_ref must be present in selected_manifests"
                )
        for selected_manifest in self.selected_manifests:
            if (
                selected_manifest.ref_digest is not None
                and _manifest_reference_key(selected_manifest) not in canonical_component_manifest_keys
            ):
                raise ValueError(
                    "digest-qualified selected_manifests must be canonical processor/backend component manifest refs"
                )
        if not any(artifact.role == "apparatus-evidence" for artifact in self.observed_setup_evidence):
            raise ValueError("observed_setup_evidence must include at least one apparatus-evidence artifact")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        components_schema = json_schema.get("properties", {}).get("components")
        if isinstance(components_schema, dict):
            components_schema.setdefault("required", ["processor", "backend"])
            components_schema.setdefault("allOf", []).extend(
                [
                    {
                        "properties": {
                            "processor": {
                                "required": ["component_kind", "identity", "manifest_ref"],
                                "properties": {
                                    "component_kind": {"const": "processor"},
                                    "manifest_ref": {"type": "object"},
                                },
                            }
                        }
                    },
                    {
                        "properties": {
                            "backend": {
                                "required": ["component_kind", "identity", "manifest_ref"],
                                "properties": {
                                    "component_kind": {"const": "backend"},
                                    "manifest_ref": {"type": "object"},
                                },
                            }
                        }
                    },
                ]
            )
        _add_aces_invariant(
            json_schema,
            "canonical-apparatus-manifest-selected",
            "The canonical processor and backend component manifest_ref values must be present in selected_manifests; "
            "digest-qualified selected manifests must be canonical component manifests.",
            validator="aces_contracts.contracts.ExperimentApparatusContextModel._validate_instrument_context",
            inputs=[{"contract_id": "experiment-apparatus-context-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "apparatus-manifest-payload-identity-valid",
            "Canonical processor and backend manifest_ref values must resolve to manifest payloads with matching "
            "identities and mutual compatibility declarations.",
            validator="aces_contracts.contracts.validate_experiment_apparatus_context_against_manifests",
            inputs=[
                {"contract_id": "experiment-apparatus-context-v1", "instance_path": "#"},
                {"contract_id": "processor-manifest-v2", "instance_path": "#"},
                {"contract_id": "backend-manifest-v2", "instance_path": "#"},
            ],
        )
        return json_schema


def _component_identity_satisfies_allowed_refs(
    component: ExperimentApparatusComponentModel,
    allowed_refs: list[ExperimentReferenceModel],
) -> bool:
    return any(_identity_matches_reference(component.identity, allowed_ref) for allowed_ref in allowed_refs)


def _apparatus_capability_ids(apparatus_context: ExperimentApparatusContextModel) -> set[str]:
    capability_ids = {
        reference.ref_id
        for reference in apparatus_context.compatibility_declarations
        if reference.ref_kind == "capability"
    }
    for component in apparatus_context.components.values():
        capability_ids.update(
            reference.ref_id for reference in component.compatibility_refs if reference.ref_kind == "capability"
        )
    return capability_ids


def _validate_apparatus_context_satisfies_constraints(
    apparatus_constraints: ExperimentApparatusConstraintModel,
    apparatus_context: ExperimentApparatusContextModel,
) -> None:
    processor = apparatus_context.components["processor"]
    backend = apparatus_context.components["backend"]

    if apparatus_constraints.allowed_processor_refs and not _component_identity_satisfies_allowed_refs(
        processor,
        apparatus_constraints.allowed_processor_refs,
    ):
        allowed = ", ".join(_format_reference(reference) for reference in apparatus_constraints.allowed_processor_refs)
        raise ValueError(f"run apparatus processor identity must satisfy task allowed_processor_refs: {allowed}")

    if apparatus_constraints.allowed_backend_refs and not _component_identity_satisfies_allowed_refs(
        backend,
        apparatus_constraints.allowed_backend_refs,
    ):
        allowed = ", ".join(_format_reference(reference) for reference in apparatus_constraints.allowed_backend_refs)
        raise ValueError(f"run apparatus backend identity must satisfy task allowed_backend_refs: {allowed}")

    missing_manifests = sorted(
        _format_reference(required_manifest)
        for required_manifest in apparatus_constraints.required_manifest_refs
        if not any(
            _reference_satisfies_requirement(selected_manifest, required_manifest)
            for selected_manifest in apparatus_context.selected_manifests
        )
    )
    if missing_manifests:
        joined = ", ".join(missing_manifests)
        raise ValueError(f"run apparatus selected_manifests must satisfy task required_manifest_refs: {joined}")

    available_capabilities = _apparatus_capability_ids(apparatus_context)
    missing_capabilities = sorted(
        capability
        for capability in apparatus_constraints.required_capabilities
        if capability not in available_capabilities
    )
    if missing_capabilities:
        joined = ", ".join(missing_capabilities)
        raise ValueError(f"run apparatus capabilities must satisfy task required_capabilities: {joined}")


def _validate_component_manifest_payload(
    *,
    component_key: Literal["processor", "backend"],
    component: ExperimentApparatusComponentModel,
    manifest: ProcessorManifestV2Model | BackendManifestV2Model,
    expected_schema_version: str,
    supplied_digest: str | None,
) -> None:
    if component.manifest_ref is None:
        raise ValueError(f"apparatus component '{component_key}' must include manifest_ref")
    if component.manifest_ref.ref_id != manifest.identity.name:
        raise ValueError(f"apparatus component '{component_key}' manifest_ref ref_id must match manifest identity name")
    if component.manifest_ref.ref_version != expected_schema_version:
        raise ValueError(
            f"apparatus component '{component_key}' manifest_ref ref_version must match manifest schema_version"
        )
    if component.identity.name != manifest.identity.name or component.identity.version != manifest.identity.version:
        raise ValueError(f"apparatus component '{component_key}' identity must match manifest identity")
    subject_ref = component.manifest_ref.subject_ref
    if subject_ref is None:
        raise ValueError(f"apparatus component '{component_key}' manifest_ref must include subject_ref")
    if not _identity_matches_reference(manifest.identity, subject_ref):
        raise ValueError(f"apparatus component '{component_key}' manifest_ref subject_ref must match manifest identity")
    if component.manifest_ref.ref_digest is not None:
        if supplied_digest is None:
            raise ValueError(
                f"apparatus component '{component_key}' manifest_ref digest requires a supplied manifest payload digest"
            )
        if _canonical_digest(component.manifest_ref.ref_digest) != _canonical_digest(supplied_digest):
            raise ValueError(
                f"apparatus component '{component_key}' manifest_ref digest must match manifest payload digest"
            )


def validate_experiment_apparatus_context_against_manifests(
    apparatus_context: ExperimentApparatusContextModel,
    processor_manifest: ProcessorManifestV2Model,
    backend_manifest: BackendManifestV2Model,
    *,
    processor_manifest_digest: str | None = None,
    backend_manifest_digest: str | None = None,
) -> None:
    """Validate apparatus manifest references against concrete manifest payloads."""

    _validate_component_manifest_payload(
        component_key="processor",
        component=apparatus_context.components["processor"],
        manifest=processor_manifest,
        expected_schema_version=PROCESSOR_MANIFEST_V2_SCHEMA_VERSION,
        supplied_digest=processor_manifest_digest,
    )
    _validate_component_manifest_payload(
        component_key="backend",
        component=apparatus_context.components["backend"],
        manifest=backend_manifest,
        expected_schema_version=BACKEND_MANIFEST_V2_SCHEMA_VERSION,
        supplied_digest=backend_manifest_digest,
    )
    if backend_manifest.identity.name not in processor_manifest.compatibility.backends:
        raise ValueError(
            "processor manifest compatibility.backends must include the selected backend manifest identity"
        )
    if processor_manifest.identity.name not in backend_manifest.compatibility.processors:
        raise ValueError(
            "backend manifest compatibility.processors must include the selected processor manifest identity"
        )


class ExperimentResultSummaryModel(ContractModel):
    """Reported metric value summary and evidence links for an experiment run."""

    metric_id: NonEmptyString
    value: str | int | float | bool | None = None
    value_status: Literal["reported", "missing", "withheld", "not-applicable"]
    evidence_refs: list[ExperimentRunEvidenceArtifactReferenceModel] = Field(min_length=1)
    uncertainty: NonEmptyString | None = None
    notes: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_reported_value(self) -> ExperimentResultSummaryModel:
        _validate_reported_value_status(
            self.value_status,
            self.value,
            reported_message="reported result summaries must include value",
            non_reported_message="non-reported result summaries must not include value",
        )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _extend_reported_value_status_schema(json_schema)
        return json_schema


class ExperimentInvalidationModel(ContractModel):
    """Details explaining why an experiment run was invalidated."""

    invalidated_at: Rfc3339DateTimeString
    reason: NonEmptyString
    superseded_by: ExperimentReferenceModel | None = None

    @model_validator(mode="after")
    def _validate_invalidated_at(self) -> ExperimentInvalidationModel:
        _parse_rfc3339_datetime("invalidated_at", self.invalidated_at)
        return self


class ExperimentRunModel(ContractModel):
    """Archival provenance record for one execution of an experiment task."""

    schema_version: Literal[EXPERIMENT_RUN_SCHEMA_VERSION]
    run_id: NonEmptyString
    run_version: NonEmptyString
    task_ref: ExperimentTaskReferenceModel
    scenario_snapshot_ref: ExperimentScenarioSnapshotReferenceModel
    apparatus_context: ExperimentApparatusContextModel
    participant_implementation_provenance: ParticipantImplementationProvenanceModel | None = None
    parameter_set: list[ExperimentParameterModel] = Field(min_length=1)
    stochastic_controls: list[ExperimentStochasticControlModel] = Field(min_length=1)
    started_at: Rfc3339DateTimeString
    ended_at: Rfc3339DateTimeString
    clock_context: ExperimentClockContextModel
    run_status: Literal["sealed", "completed", "failed", "aborted", "invalidated", "superseded"]
    outcome_status: Literal["succeeded", "failed", "partial", "inconclusive", "not-evaluated"]
    traceability: ExperimentRunTraceabilityModel
    realized_form_disclosures: list[ExperimentRealizedFormDisclosureModel] = Field(default_factory=list)
    augmentation_disclosures: list[ExperimentAugmentationDisclosureModel] = Field(default_factory=list)
    evidence_artifacts: list[ExperimentArtifactRefModel] = Field(min_length=1)
    result_summaries: dict[NonEmptyString, ExperimentResultSummaryModel] = Field(min_length=1)
    deviations: list[NonEmptyString] = Field(default_factory=list)
    invalidation: ExperimentInvalidationModel | None = None
    used_refs: list[ExperimentReferenceModel] = Field(default_factory=list)
    generated_refs: list[ExperimentReferenceModel] = Field(default_factory=list)
    derived_from_refs: list[ExperimentReferenceModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_archival_run(self) -> ExperimentRunModel:
        started_at = _parse_rfc3339_datetime("started_at", self.started_at)
        ended_at = _parse_rfc3339_datetime("ended_at", self.ended_at)
        if ended_at < started_at:
            raise ValueError("ended_at must be greater than or equal to started_at")
        if self.run_status == "invalidated" and self.invalidation is None:
            raise ValueError("invalidated experiment runs must include invalidation details")
        if self.outcome_status == "succeeded" and not any(
            result.value_status == "reported" for result in self.result_summaries.values()
        ):
            raise ValueError("succeeded experiment runs must include at least one reported result summary")
        participant_components = [
            component
            for component in self.apparatus_context.components.values()
            if component.component_kind == "participant-implementation"
        ]
        if participant_components and self.participant_implementation_provenance is None:
            raise ValueError(
                "experiment runs with participant implementation apparatus components must include "
                "participant_implementation_provenance"
            )
        if self.participant_implementation_provenance is not None:
            if self.participant_implementation_provenance.run_id != self.run_id:
                raise ValueError("participant_implementation_provenance run_id must match experiment run_id")
            selected_identities = {
                (selection.implementation_identity.name, selection.implementation_identity.version)
                for selection in self.participant_implementation_provenance.participant_implementations
            }
            missing_component_identities = sorted(
                f"{component.identity.name}:{component.identity.version}"
                for component in participant_components
                if (component.identity.name, component.identity.version) not in selected_identities
            )
            if missing_component_identities:
                joined = ", ".join(missing_component_identities)
                raise ValueError(
                    "participant implementation apparatus components must resolve to "
                    f"participant_implementation_provenance selections: {joined}"
                )
        evidence_artifact_ids = {artifact.artifact_id for artifact in self.evidence_artifacts}
        missing_evidence_refs = sorted(
            {
                evidence_ref.ref_id
                for result in self.result_summaries.values()
                for evidence_ref in result.evidence_refs
                if evidence_ref.ref_id not in evidence_artifact_ids
            }
        )
        if missing_evidence_refs:
            joined = ", ".join(missing_evidence_refs)
            raise ValueError(f"result_summaries evidence_refs must resolve to evidence_artifacts: {joined}")
        traced_evidence_record_refs = {
            _experiment_reference_key(evidence_ref) for evidence_ref in self.traceability.evidence_record_refs
        }
        missing_disclosure_evidence_refs = sorted(
            _format_reference(evidence_ref)
            for disclosure in self.realized_form_disclosures
            for evidence_ref in disclosure.evidence_refs
            if _experiment_reference_key(evidence_ref) not in traced_evidence_record_refs
        )
        if missing_disclosure_evidence_refs:
            joined = ", ".join(missing_disclosure_evidence_refs)
            raise ValueError(
                f"realized_form_disclosures evidence_refs must be listed in traceability evidence_record_refs: {joined}"
            )
        _validate_unique_string_values(
            "augmentation_disclosures augmentation_id",
            [disclosure.augmentation_id for disclosure in self.augmentation_disclosures],
        )
        missing_augmentation_evidence_refs = sorted(
            _format_reference(evidence_ref)
            for disclosure in self.augmentation_disclosures
            for evidence_ref in disclosure.evidence_refs
            if _experiment_reference_key(evidence_ref) not in traced_evidence_record_refs
        )
        if missing_augmentation_evidence_refs:
            joined = ", ".join(missing_augmentation_evidence_refs)
            raise ValueError(
                f"augmentation_disclosures evidence_refs must be listed in traceability evidence_record_refs: {joined}"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"run_status": {"const": "invalidated"}},
                    "required": ["run_status"],
                },
                "then": {
                    "required": ["invalidation"],
                    "properties": {"invalidation": {"type": "object"}},
                },
            }
        )
        _add_aces_invariant(
            json_schema,
            "ended-at-not-before-started-at",
            "ended_at must be greater than or equal to started_at.",
            validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "result-evidence-ref-resolves",
            "Every result_summaries evidence_refs ref_id must match an evidence_artifacts artifact_id.",
            validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "participant-implementation-provenance-resolves",
            "Participant implementation apparatus components must resolve to run-level participant provenance.",
            validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "realized-form-evidence-refs-traced",
            "Every realized-form disclosure evidence ref must also appear in the run traceability evidence refs.",
            validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "augmentation-disclosure-evidence-refs-traced",
            "Every augmentation disclosure evidence ref must also appear in the run traceability evidence refs, "
            "and augmentation_id values must be unique within the run.",
            validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run",
            inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "task-run-protocol-binding-valid",
            "Run apparatus, result metric ids, and concrete evidence artifacts must satisfy the referenced task protocol.",
            validator="aces_contracts.contracts.validate_experiment_run_against_task",
            inputs=[
                {"contract_id": "experiment-task-v1", "instance_path": "#"},
                {"contract_id": "experiment-run-v1", "instance_path": "#"},
            ],
        )
        return json_schema


def _artifact_satisfies_evidence_reference(
    artifact: ExperimentArtifactRefModel,
    evidence_reference: ExperimentEvidenceReferenceModel,
) -> bool:
    direct_artifact_match = artifact.artifact_id == evidence_reference.ref_id and evidence_reference.ref_version is None
    semantic_evidence_match = any(
        _reference_identity_satisfies_requirement(satisfied_ref, evidence_reference)
        for satisfied_ref in artifact.satisfies_refs
    )
    if not direct_artifact_match and not semantic_evidence_match:
        return False
    if evidence_reference.ref_digest is not None:
        artifact_digest = f"{artifact.checksum.algorithm}:{artifact.checksum.value}"
        if _canonical_digest(artifact_digest) != _canonical_digest(evidence_reference.ref_digest):
            return False
    return evidence_reference.ref_path is None or artifact.uri == evidence_reference.ref_path


def validate_experiment_run_against_task(task: ExperimentTaskModel, run: ExperimentRunModel) -> None:
    """Validate cross-artifact task/run semantic invariants."""

    if run.task_ref.ref_id != task.task_id or run.task_ref.ref_version != task.task_version:
        raise ValueError("run task_ref must match task task_id and task_version")

    if task.scenario_ref.ref_kind == "scenario-snapshot":
        if not _reference_satisfies_requirement(run.scenario_snapshot_ref, task.scenario_ref):
            raise ValueError("run scenario_snapshot_ref must satisfy task scenario_ref")
    elif run.scenario_snapshot_ref.ref_id != task.scenario_ref.ref_id:
        raise ValueError("run scenario_snapshot_ref ref_id must match task scenario_ref ref_id")

    if task.apparatus_constraints is not None:
        _validate_apparatus_context_satisfies_constraints(task.apparatus_constraints, run.apparatus_context)

    metric_definitions = task.evaluation_protocol.metric_definitions
    missing_metric_ids = sorted(
        {result.metric_id for result in run.result_summaries.values() if result.metric_id not in metric_definitions}
    )
    if missing_metric_ids:
        joined = ", ".join(missing_metric_ids)
        raise ValueError(f"run result metric_id values must be declared by the task evaluation protocol: {joined}")

    evidence_artifacts_by_id = {artifact.artifact_id: artifact for artifact in run.evidence_artifacts}
    missing_observation_requirements = sorted(
        requirement.ref_id
        for requirement in task.evaluation_protocol.observation_requirements
        if not any(_artifact_satisfies_evidence_reference(artifact, requirement) for artifact in run.evidence_artifacts)
    )
    if missing_observation_requirements:
        joined = ", ".join(missing_observation_requirements)
        raise ValueError(f"run evidence_artifacts must satisfy task observation requirements: {joined}")

    missing_metric_evidence: list[str] = []
    for result_id, result in run.result_summaries.items():
        result_artifacts = [
            evidence_artifacts_by_id[evidence_ref.ref_id]
            for evidence_ref in result.evidence_refs
            if evidence_ref.ref_id in evidence_artifacts_by_id
        ]
        for requirement in metric_definitions[result.metric_id].evidence_requirements:
            if not any(_artifact_satisfies_evidence_reference(artifact, requirement) for artifact in result_artifacts):
                missing_metric_evidence.append(f"{result_id}:{requirement.ref_id}")
    if missing_metric_evidence:
        joined = ", ".join(sorted(missing_metric_evidence))
        raise ValueError(f"run result evidence_refs must satisfy task metric evidence requirements: {joined}")


class ExperimentStudyMembershipModel(ContractModel):
    """Typed member reference within a study or collection."""

    target_ref: ExperimentReferenceModel
    role: Literal[
        "primary-task",
        "comparison-task",
        "calibration-run",
        "evaluation-run",
        "baseline-result",
        "comparison-result",
        "evidence",
        "analysis",
        "other",
    ]
    grouping: NonEmptyString | None = None
    inclusion_rationale: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_role_target_kind(self) -> ExperimentStudyMembershipModel:
        allowed_ref_kinds = {
            "primary-task": {"task"},
            "comparison-task": {"task"},
            "calibration-run": {"run"},
            "evaluation-run": {"run"},
            "baseline-result": {"result"},
            "comparison-result": {"result"},
            "evidence": {"evidence"},
            "analysis": {"analysis-artifact"},
        }.get(self.role)
        if allowed_ref_kinds is not None and self.target_ref.ref_kind not in allowed_ref_kinds:
            expected = ", ".join(sorted(allowed_ref_kinds))
            raise ValueError(f"study membership role '{self.role}' requires target_ref.ref_kind in {{{expected}}}")
        if self.role in {"primary-task", "comparison-task", "calibration-run", "evaluation-run"} and (
            self.target_ref.ref_digest is not None or self.target_ref.ref_path is not None
        ):
            raise ValueError("study task/run membership target_ref values must not carry ref_digest or ref_path")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        role_kind_constraints = {
            ("primary-task", "comparison-task"): ["task"],
            ("calibration-run", "evaluation-run"): ["run"],
            ("baseline-result", "comparison-result"): ["result"],
            ("evidence",): ["evidence"],
            ("analysis",): ["analysis-artifact"],
        }
        json_schema.setdefault("allOf", []).extend(
            {
                "if": {"properties": {"role": {"enum": list(roles)}}, "required": ["role"]},
                "then": {
                    "properties": {
                        "target_ref": {
                            "required": ["ref_kind"],
                            "properties": {"ref_kind": {"enum": ref_kinds}},
                        }
                    }
                },
            }
            for roles, ref_kinds in role_kind_constraints.items()
        )
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {
                        "role": {"enum": ["primary-task", "comparison-task", "calibration-run", "evaluation-run"]}
                    },
                    "required": ["role"],
                },
                "then": {
                    "properties": {
                        "target_ref": {
                            "properties": {
                                "ref_digest": {"type": "null"},
                                "ref_path": {"type": "null"},
                            }
                        }
                    }
                },
            }
        )
        return json_schema


class ExperimentStudyFactorModel(ContractModel):
    """Treatment, control, blocking, or apparatus factor for study analysis."""

    name: NonEmptyString
    factor_kind: Literal["treatment", "control", "blocking", "stratification", "apparatus", "other"]
    levels: list[NonEmptyString] = Field(default_factory=list)


class ExperimentConditionAssignmentModel(ContractModel):
    """Concrete treatment-condition assignment criteria for study evaluation runs."""

    condition_id: NonEmptyString
    factor_levels: dict[NonEmptyString, NonEmptyString] = Field(min_length=1)
    required_refs: list[ExperimentConditionAssignmentReferenceModel] = Field(default_factory=list)
    required_parameters: list[ExperimentConditionAssignmentParameterModel] = Field(default_factory=list)
    description: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_run_level_criteria(self) -> ExperimentConditionAssignmentModel:
        if not self.required_refs and not self.required_parameters:
            raise ValueError("condition assignments must include required_refs or required_parameters")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("anyOf", []).extend(
            [
                {"required": ["required_refs"], "properties": {"required_refs": {"minItems": 1}}},
                {"required": ["required_parameters"], "properties": {"required_parameters": {"minItems": 1}}},
            ]
        )
        return json_schema


class ExperimentRunAllocationPlanModel(ContractModel):
    """Structured run allocation, replication, and assignment plan."""

    allocation_unit: NonEmptyString
    allocation_method: NonEmptyString
    compared_conditions: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True})
    condition_assignments: dict[NonEmptyString, ExperimentConditionAssignmentModel] = Field(min_length=1)
    target_runs_per_condition: PositiveInteger
    randomization_unit: NonEmptyString | None = None
    blocking_factors: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True})
    replication_policy: NonEmptyString
    stopping_rule: NonEmptyString | None = None

    @model_validator(mode="after")
    def _validate_condition_assignments(self) -> ExperimentRunAllocationPlanModel:
        condition_ids = set(self.compared_conditions)
        if len(condition_ids) != len(self.compared_conditions):
            raise ValueError("run_allocation compared_conditions must be unique")
        assignment_ids = set(self.condition_assignments)
        if assignment_ids != condition_ids:
            raise ValueError("run_allocation condition_assignments keys must match compared_conditions")
        mismatched_assignment_ids = sorted(
            assignment_key
            for assignment_key, assignment in self.condition_assignments.items()
            if assignment.condition_id != assignment_key
        )
        if mismatched_assignment_ids:
            joined = ", ".join(mismatched_assignment_ids)
            raise ValueError(f"run_allocation condition_assignments keys must match embedded condition_id: {joined}")
        if len(set(self.blocking_factors)) != len(self.blocking_factors):
            raise ValueError("run_allocation blocking_factors must be unique")
        if len(condition_ids) > 1:
            factor_levels_by_signature: dict[tuple[tuple[str, str], ...], list[str]] = {}
            criteria_by_signature: dict[
                tuple[
                    tuple[tuple[str, str, str | None, str | None, str | None], ...],
                    tuple[tuple[str, str, str, str], ...],
                ],
                list[str],
            ] = {}
            for condition_id, assignment in self.condition_assignments.items():
                factor_levels_signature = tuple(sorted(assignment.factor_levels.items()))
                factor_levels_by_signature.setdefault(factor_levels_signature, []).append(condition_id)
                signature = _condition_assignment_run_criteria_signature(assignment)
                criteria_by_signature.setdefault(signature, []).append(condition_id)
            duplicate_factor_level_conditions = sorted(
                ",".join(sorted(condition_ids))
                for condition_ids in factor_levels_by_signature.values()
                if len(condition_ids) > 1
            )
            if duplicate_factor_level_conditions:
                joined = "; ".join(duplicate_factor_level_conditions)
                raise ValueError(
                    "run_allocation condition_assignments must use distinct factor-level combinations across "
                    f"compared_conditions: {joined}"
                )
            duplicate_criteria_conditions = sorted(
                ",".join(sorted(condition_ids))
                for condition_ids in criteria_by_signature.values()
                if len(condition_ids) > 1
            )
            if duplicate_criteria_conditions:
                joined = "; ".join(duplicate_criteria_conditions)
                raise ValueError(
                    "run_allocation condition_assignments must use distinct run-level criteria across "
                    f"compared_conditions: {joined}"
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "run-allocation-condition-assignments-valid",
            "Run-allocation compared_conditions, condition_assignments keys, embedded condition ids, blocking "
            "factor ids, factor-level combinations, and run-level criteria signatures must be internally coherent.",
            validator="aces_contracts.contracts.ExperimentRunAllocationPlanModel._validate_condition_assignments",
            inputs=[{"contract_id": "experiment-study-v1", "instance_path": "#/run_allocation"}],
        )
        return json_schema


class ExperimentStatisticalMethodModel(ContractModel):
    """Statistical or estimation method declared before analysis."""

    method: NonEmptyString
    estimand: NonEmptyString
    unit_of_analysis: NonEmptyString
    comparison_family: NonEmptyString | None = None
    assumptions: list[NonEmptyString] = Field(min_length=1)


class ExperimentUncertaintyMethodModel(ContractModel):
    """Uncertainty reporting plan for study estimates."""

    method: NonEmptyString
    interval_level: UnitIntervalFloat
    procedure: NonEmptyString


class ExperimentMultipleComparisonPolicyModel(ContractModel):
    """Multiplicity policy for study-level families of comparisons."""

    family: NonEmptyString
    correction: NonEmptyString
    rationale: NonEmptyString


class ExperimentMissingDataPolicyModel(ContractModel):
    """Missing, failed, withheld, or not-applicable result handling."""

    missingness_assumption: NonEmptyString
    handling: NonEmptyString
    sensitivity_analysis: NonEmptyString | None = None


class ExperimentAnalysisPlanModel(ContractModel):
    """Analysis plan metadata for metrics, uncertainty, and missing data."""

    analysis_id: NonEmptyString
    description: NonEmptyString
    metrics: list[NonEmptyString] = Field(min_length=1)
    primary_metric: NonEmptyString
    statistical_method: ExperimentStatisticalMethodModel
    uncertainty_method: ExperimentUncertaintyMethodModel
    multiple_comparison_policy: ExperimentMultipleComparisonPolicyModel
    missing_data_policy: ExperimentMissingDataPolicyModel

    @model_validator(mode="after")
    def _validate_primary_metric(self) -> ExperimentAnalysisPlanModel:
        if self.primary_metric not in self.metrics:
            raise ValueError("analysis_plan primary_metric must be included in metrics")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "analysis-plan-substantive-methods-required",
            "Analysis plans must name metrics plus structured statistical, uncertainty, multiplicity, "
            "and missing-data policies.",
            validator="aces_contracts.contracts.ExperimentAnalysisPlanModel._validate_primary_metric",
            inputs=[{"contract_id": "experiment-study-v1", "instance_path": "#/analysis_plan"}],
        )
        return json_schema


class ExperimentStudyModel(ContractModel):
    """Study or collection contract for grouping experiment artifacts."""

    schema_version: Literal[EXPERIMENT_STUDY_SCHEMA_VERSION]
    study_id: NonEmptyString
    study_version: NonEmptyString
    study_kind: Literal["study", "collection", "benchmark", "cohort"]
    title: NonEmptyString
    owner: NonEmptyString
    description: NonEmptyString
    purpose: NonEmptyString
    research_questions: list[NonEmptyString] = Field(default_factory=list)
    behavioral_claims: list[BehavioralClaimBindingModel] = Field(default_factory=list)
    membership: dict[NonEmptyString, ExperimentStudyMembershipModel] = Field(min_length=1)
    inclusion_criteria: list[NonEmptyString] = Field(min_length=1)
    factors: dict[NonEmptyString, ExperimentStudyFactorModel] = Field(default_factory=dict)
    run_allocation: ExperimentRunAllocationPlanModel | None = None
    analysis_plan: ExperimentAnalysisPlanModel | None = None
    validity_notes: list[ExperimentValidityNoteModel] = Field(default_factory=list)
    report_artifacts: list[ExperimentArtifactRefModel] = Field(default_factory=list)
    export_artifacts: list[ExperimentArtifactRefModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_claim_bearing_study(self) -> ExperimentStudyModel:
        from .behavioral_relations import load_behavioral_relation_catalog, validate_behavioral_claim_binding

        catalog = load_behavioral_relation_catalog()
        relation_ids = [claim.relation_id for claim in self.behavioral_claims]
        if len(relation_ids) != len(set(relation_ids)):
            raise ValueError("study behavioral claim relation ids must be unique")
        for claim in self.behavioral_claims:
            validate_behavioral_claim_binding(claim, catalog)
        if self.run_allocation is not None:
            undeclared_blocking_factors = sorted(
                factor_id for factor_id in self.run_allocation.blocking_factors if factor_id not in self.factors
            )
            if undeclared_blocking_factors:
                joined = ", ".join(undeclared_blocking_factors)
                raise ValueError(f"run_allocation blocking_factors must reference declared factors: {joined}")
            blocking_factors_without_levels = sorted(
                factor_id for factor_id in self.run_allocation.blocking_factors if not self.factors[factor_id].levels
            )
            if blocking_factors_without_levels:
                joined = ", ".join(blocking_factors_without_levels)
                raise ValueError(
                    f"run_allocation blocking_factors must reference factors with declared levels: {joined}"
                )
            invalid_blocking_factor_kinds = sorted(
                f"{factor_id}:{self.factors[factor_id].factor_kind}"
                for factor_id in self.run_allocation.blocking_factors
                if self.factors[factor_id].factor_kind not in {"blocking", "stratification", "apparatus", "control"}
            )
            if invalid_blocking_factor_kinds:
                joined = ", ".join(invalid_blocking_factor_kinds)
                raise ValueError(
                    "run_allocation blocking_factors must reference blocking, stratification, apparatus, "
                    f"or control factors: {joined}"
                )
            for assignment_key, assignment in self.run_allocation.condition_assignments.items():
                for factor_id, level in assignment.factor_levels.items():
                    factor = self.factors.get(factor_id)
                    if factor is None:
                        raise ValueError(
                            "run_allocation condition_assignments factor_levels must reference declared factors: "
                            f"{assignment_key}:{factor_id}"
                        )
                    if level not in factor.levels:
                        raise ValueError(
                            "run_allocation condition_assignments factor_levels must reference declared factor levels: "
                            f"{assignment_key}:{factor_id}:{level}"
                        )
        if self.study_kind in {"study", "benchmark"}:
            if not self.research_questions:
                raise ValueError("study and benchmark records must include at least one research question")
            if not self.behavioral_claims:
                raise ValueError("study and benchmark records must include at least one behavioral claim binding")
            if self.run_allocation is None:
                raise ValueError("study and benchmark records must include run_allocation")
            if self.analysis_plan is None:
                raise ValueError("study and benchmark records must include analysis_plan")
            if not self.validity_notes:
                raise ValueError("study and benchmark records must include validity_notes")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).append(
            {
                "if": {
                    "properties": {"study_kind": {"enum": ["study", "benchmark"]}},
                    "required": ["study_kind"],
                },
                "then": {
                    "required": [
                        "research_questions",
                        "behavioral_claims",
                        "run_allocation",
                        "analysis_plan",
                        "validity_notes",
                    ],
                    "properties": {
                        "research_questions": {"minItems": 1},
                        "behavioral_claims": {"minItems": 1},
                        "run_allocation": {"type": "object"},
                        "analysis_plan": {"type": "object"},
                        "validity_notes": {"minItems": 1},
                    },
                },
            }
        )
        _add_aces_invariant(
            json_schema,
            "claim-bearing-study-analysis-plan-required",
            "Study and benchmark records must include research questions, revisioned behavioral claim bindings, run "
            "allocation, a substantive analysis plan, and validity notes.",
            validator="aces_contracts.contracts.ExperimentStudyModel._validate_claim_bearing_study",
            inputs=[{"contract_id": "experiment-study-v1", "instance_path": "#"}],
        )
        _add_aces_invariant(
            json_schema,
            "study-analysis-metrics-grounded-in-task-protocols",
            "Study analysis_plan metrics must be declared by included experiment task protocols.",
            validator="aces_contracts.contracts.validate_experiment_study_against_tasks_and_runs",
            inputs=[
                {"contract_id": "experiment-study-v1", "instance_path": "#"},
                {"contract_id": "experiment-task-v1", "instance_path": "#"},
                {"contract_id": "experiment-run-v1", "instance_path": "#"},
            ],
        )
        _add_aces_invariant(
            json_schema,
            "study-analysis-metrics-covered-by-evaluation-run-results",
            "Study analysis_plan metrics must have result_summaries, including explicit missing/withheld "
            "statuses, in included evaluation runs.",
            validator="aces_contracts.contracts.validate_experiment_study_against_tasks_and_runs",
            inputs=[
                {"contract_id": "experiment-study-v1", "instance_path": "#"},
                {"contract_id": "experiment-task-v1", "instance_path": "#"},
                {"contract_id": "experiment-run-v1", "instance_path": "#"},
            ],
        )
        _add_aces_invariant(
            json_schema,
            "study-analysis-runs-eligible",
            "Study analysis_plan evaluation-run members must resolve unambiguously and exclude invalidated, "
            "superseded, and not-evaluated runs.",
            validator="aces_contracts.contracts.validate_experiment_study_against_tasks_and_runs",
            inputs=[
                {"contract_id": "experiment-study-v1", "instance_path": "#"},
                {"contract_id": "experiment-run-v1", "instance_path": "#"},
            ],
        )
        _add_aces_invariant(
            json_schema,
            "study-run-allocation-covered-by-evaluation-run-members",
            "Study run_allocation compared_conditions must be represented by eligible included evaluation-run "
            "membership groupings that meet target_runs_per_condition, use operational blocking factors, and satisfy "
            "exactly one distinct factor-level combination and auditable condition assignment.",
            validator="aces_contracts.contracts.validate_experiment_study_against_tasks_and_runs",
            inputs=[
                {"contract_id": "experiment-study-v1", "instance_path": "#"},
                {"contract_id": "experiment-task-v1", "instance_path": "#"},
                {"contract_id": "experiment-run-v1", "instance_path": "#"},
            ],
        )
        return json_schema


class ExperimentEpisodeControlModel(ContractModel):
    """Declarative episode execution controls for a planned experiment.

    Captures the pre-run execution-control facts — turn order, logical step
    count, and episode termination — that ADR-069 requires for CAGE-2
    execution-control equivalence but that the archival experiment-core
    contracts only record after a run has executed.
    """

    turn_order: Literal["sequential", "simultaneous", "round-robin", "scenario-defined", "other"]
    termination_rule: NonEmptyString
    max_steps: PositiveInteger | None = None
    termination_condition_refs: list[NonEmptyString] = Field(
        default_factory=list, json_schema_extra={"uniqueItems": True}
    )
    description: NonEmptyString | None = None


class ExperimentRedVariantSelectionModel(ContractModel):
    """Selection of one red-agent variant bound into a planned experiment."""

    variant_id: NonEmptyString
    agent_ref: NonEmptyString
    parameters: list[ExperimentParameterModel] = Field(default_factory=list)
    description: NonEmptyString | None = None


class ExperimentRunPlanModel(ContractModel):
    """Pre-run replication, stochastic, episode, and red-variant plan.

    Reuses the archival-family value models for stochastic controls, run
    allocation, and clock intent, and adds the authoring-only episode and
    red-variant selections. Exactly one of ``allocation`` (condition-based)
    or ``target_run_count`` (simple, no-condition) declares the run count.
    """

    stochastic_controls: list[ExperimentStochasticControlModel] = Field(min_length=1)
    episode_control: ExperimentEpisodeControlModel
    allocation: ExperimentRunAllocationPlanModel | None = None
    target_run_count: PositiveInteger | None = None
    red_variant_selections: dict[NonEmptyString, ExperimentRedVariantSelectionModel] = Field(default_factory=dict)
    clock_intent: ExperimentClockContextModel | None = None

    @model_validator(mode="after")
    def _validate_run_plan(self) -> ExperimentRunPlanModel:
        if (self.allocation is None) == (self.target_run_count is None):
            raise ValueError("run_plan must declare exactly one of allocation or target_run_count")
        for key, selection in self.red_variant_selections.items():
            if selection.variant_id != key:
                raise ValueError(
                    f"run_plan red_variant_selections key '{key}' must match embedded variant_id "
                    f"'{selection.variant_id}'"
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("oneOf", []).extend(
            [
                {
                    "required": ["allocation"],
                    "properties": {"allocation": {"not": {"type": "null"}}, "target_run_count": {"type": "null"}},
                },
                {
                    "required": ["target_run_count"],
                    "properties": {"target_run_count": {"not": {"type": "null"}}, "allocation": {"type": "null"}},
                },
            ]
        )
        _add_aces_invariant(
            json_schema,
            "run-plan-exactly-one-run-count-source",
            "A run plan must declare exactly one of allocation or target_run_count, and every red-variant "
            "selection map key must equal its embedded variant_id.",
            validator="aces_contracts.contracts.ExperimentRunPlanModel._validate_run_plan",
            inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#/run_plan"}],
        )
        return json_schema


class ExperimentSpecModel(ContractModel):
    """Pre-run experiment authoring input: a design that binds a task to a run plan.

    This is the authoring/input counterpart to the archival experiment-core
    outputs (run/study/apparatus-context). It references the separately
    authored task (and optionally a scenario snapshot) and declares the
    pre-run experimental design — apparatus intent, run plan, factors,
    intended capture, and validity notes — before any run executes. It is
    never a run, study, or apparatus-context record (ADR-055 / ADR-074).
    """

    schema_version: Literal[EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION]
    spec_id: NonEmptyString
    spec_version: NonEmptyString
    title: NonEmptyString
    description: NonEmptyString
    task_ref: ExperimentTaskReferenceModel
    run_plan: ExperimentRunPlanModel
    intended_scenario_ref: ExperimentScenarioReferenceModel | None = None
    apparatus_intent: ExperimentApparatusConstraintModel | None = None
    factors: dict[NonEmptyString, ExperimentStudyFactorModel] = Field(default_factory=dict)
    capture_spec_refs: list[ExperimentCaptureSpecReferenceModel] = Field(default_factory=list)
    validity_notes: list[ExperimentValidityNoteModel] = Field(default_factory=list)
    artifact_refs: list[ExperimentArtifactRefModel] = Field(default_factory=list)

    @model_validator(mode="after")
    def _validate_experiment_spec(self) -> ExperimentSpecModel:
        allocation = self.run_plan.allocation
        if allocation is not None:
            factor_names = set(self.factors)
            for blocking_factor in allocation.blocking_factors:
                if blocking_factor not in factor_names:
                    raise ValueError(
                        f"run_plan allocation blocking factor '{blocking_factor}' must be a declared factor"
                    )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        _add_aces_invariant(
            json_schema,
            "experiment-spec-blocking-factors-declared",
            "When a run plan declares an allocation with blocking factors, every blocking factor must be a "
            "declared experiment-spec factor.",
            validator="aces_contracts.contracts.ExperimentSpecModel._validate_experiment_spec",
            inputs=[{"contract_id": "experiment-authoring-input-v1", "instance_path": "#"}],
        )
        return json_schema


def validate_experiment_task_archival_datetimes(task: ExperimentTaskModel | Mapping[str, Any]) -> None:
    """Validate task-level archival timestamp semantics not carried by generic JSON Schema."""

    _validate_artifact_collection_created_at("artifact_refs", _payload_get(task, "artifact_refs"))


def validate_experiment_apparatus_context_archival_datetimes(
    apparatus_context: ExperimentApparatusContextModel | Mapping[str, Any],
) -> None:
    """Validate apparatus-context archival timestamp semantics not carried by generic JSON Schema."""

    _validate_rfc3339_payload_field(apparatus_context, "declared_at")
    _validate_artifact_collection_created_at(
        "observed_setup_evidence",
        _payload_get(apparatus_context, "observed_setup_evidence"),
    )


def validate_experiment_run_archival_datetimes(run: ExperimentRunModel | Mapping[str, Any]) -> None:
    """Validate run-level archival timestamp semantics not carried by generic JSON Schema."""

    _validate_rfc3339_payload_field(run, "started_at")
    _validate_rfc3339_payload_field(run, "ended_at")
    invalidation = _payload_get(run, "invalidation")
    if invalidation is not None:
        _validate_rfc3339_payload_field(invalidation, "invalidated_at")
    _validate_artifact_collection_created_at("evidence_artifacts", _payload_get(run, "evidence_artifacts"))


def validate_experiment_study_archival_datetimes(study: ExperimentStudyModel | Mapping[str, Any]) -> None:
    """Validate study-level archival timestamp semantics not carried by generic JSON Schema."""

    _validate_artifact_collection_created_at("report_artifacts", _payload_get(study, "report_artifacts"))
    _validate_artifact_collection_created_at("export_artifacts", _payload_get(study, "export_artifacts"))


def _task_reference_key(reference: ExperimentTaskReferenceModel) -> tuple[str, str | None]:
    return (reference.ref_id, reference.ref_version)


def _task_model_key(task: ExperimentTaskModel) -> tuple[str, str]:
    return (task.task_id, task.task_version)


def _run_model_key(run: ExperimentRunModel) -> tuple[str, str | None]:
    return (run.run_id, run.run_version)


def _task_ref_matches_task(reference: ExperimentReferenceModel, task: ExperimentTaskModel) -> bool:
    if reference.ref_digest is not None or reference.ref_path is not None:
        return False
    return (
        reference.ref_kind == "task"
        and reference.ref_id == task.task_id
        and (reference.ref_version is None or reference.ref_version == task.task_version)
    )


def _run_ref_matches_run(reference: ExperimentReferenceModel, run: ExperimentRunModel) -> bool:
    if reference.ref_digest is not None or reference.ref_path is not None:
        return False
    return (
        reference.ref_kind == "run"
        and reference.ref_id == run.run_id
        and (reference.ref_version is None or reference.ref_version == run.run_version)
    )


def _component_identity_matches_reference(
    component: ExperimentApparatusComponentModel,
    reference: ExperimentReferenceModel,
) -> bool:
    return component.component_kind == reference.ref_kind and _identity_matches_reference(component.identity, reference)


def _participant_selection_matches_reference(
    selection: ParticipantImplementationSelectionModel,
    reference: ExperimentReferenceModel,
) -> bool:
    return reference.ref_kind == "participant-implementation" and _identity_matches_reference(
        selection.implementation_identity,
        reference,
    )


def _reference_in_collection(
    references: list[ExperimentReferenceModel],
    requirement: ExperimentReferenceModel,
) -> bool:
    return any(_reference_satisfies_requirement(reference, requirement) for reference in references)


def _run_satisfies_condition_reference(run: ExperimentRunModel, requirement: ExperimentReferenceModel) -> bool:
    apparatus_context = run.apparatus_context
    if requirement.ref_kind in {"processor", "backend"}:
        return any(
            _component_identity_matches_reference(component, requirement)
            for component in apparatus_context.components.values()
        )
    if requirement.ref_kind == "participant-implementation":
        provenance = run.participant_implementation_provenance
        if provenance is None:
            return False
        return any(
            _participant_selection_matches_reference(selection, requirement)
            for selection in provenance.participant_implementations
        )
    if requirement.ref_kind == "task":
        return _reference_satisfies_requirement(run.task_ref, requirement)
    if requirement.ref_kind == "scenario-snapshot":
        return _reference_satisfies_requirement(run.scenario_snapshot_ref, requirement)
    if requirement.ref_kind == "apparatus-context":
        apparatus_context_ref = ExperimentReferenceModel(
            ref_kind="apparatus-context",
            ref_id=apparatus_context.apparatus_context_id,
            ref_version=apparatus_context.context_version,
        )
        return _reference_satisfies_requirement(apparatus_context_ref, requirement)
    if requirement.ref_kind == "manifest":
        return any(
            _reference_satisfies_requirement(selected_manifest, requirement)
            for selected_manifest in apparatus_context.selected_manifests
        )
    if requirement.ref_kind in {"profile", "capability"}:
        component_refs = [
            reference
            for component in apparatus_context.components.values()
            for reference in component.compatibility_refs
        ]
        return _reference_in_collection(
            apparatus_context.compatibility_declarations, requirement
        ) or _reference_in_collection(
            component_refs,
            requirement,
        )
    if requirement.ref_kind == "measurement-channel":
        return _reference_in_collection(apparatus_context.measurement_channels, requirement)
    if requirement.ref_kind == "evidence":
        return any(
            artifact.artifact_id == requirement.ref_id
            or any(
                _reference_satisfies_requirement(satisfied_ref, requirement)
                for satisfied_ref in artifact.satisfies_refs
            )
            for artifact in run.evidence_artifacts
        )
    return (
        _reference_in_collection(run.used_refs, requirement)
        or _reference_in_collection(run.generated_refs, requirement)
        or _reference_in_collection(run.derived_from_refs, requirement)
    )


def _parameter_satisfies_requirement(
    parameter: ExperimentParameterModel,
    requirement: ExperimentParameterModel,
) -> bool:
    return (
        parameter.name == requirement.name
        and parameter.value_kind == requirement.value_kind
        and parameter.value == requirement.value
    )


def _condition_assignment_run_criteria_signature(
    assignment: ExperimentConditionAssignmentModel,
) -> tuple[
    tuple[tuple[str, str, str | None, str | None, str | None], ...],
    tuple[tuple[str, str, str, str], ...],
]:
    reference_signature = tuple(
        sorted(
            (
                reference.ref_kind,
                reference.ref_id,
                reference.ref_version,
                _canonical_digest(reference.ref_digest),
                reference.ref_path,
            )
            for reference in assignment.required_refs
        )
    )
    parameter_signature = tuple(
        sorted(
            (
                parameter.name,
                parameter.value_kind,
                type(parameter.value).__name__,
                json.dumps(parameter.value, sort_keys=True, separators=(",", ":")),
            )
            for parameter in assignment.required_parameters
        )
    )
    return reference_signature, parameter_signature


def _run_satisfies_condition_assignment(
    run: ExperimentRunModel,
    assignment: ExperimentConditionAssignmentModel,
) -> list[str]:
    missing: list[str] = []
    missing.extend(
        _format_reference(reference)
        for reference in assignment.required_refs
        if not _run_satisfies_condition_reference(run, reference)
    )
    run_parameters = [*run.parameter_set, *run.apparatus_context.configuration_parameters]
    missing.extend(
        f"parameter:{parameter.name}:{parameter.value_kind}"
        for parameter in assignment.required_parameters
        if not any(_parameter_satisfies_requirement(candidate, parameter) for candidate in run_parameters)
    )
    return missing


def _run_is_eligible_for_study_analysis(run: ExperimentRunModel) -> bool:
    return run.run_status not in {"invalidated", "superseded"} and run.outcome_status != "not-evaluated"


def _validate_study_analysis_run_eligibility(
    study: ExperimentStudyModel,
    runs: list[ExperimentRunModel],
    evaluation_run_members: list[ExperimentStudyMembershipModel],
) -> None:
    if study.analysis_plan is None:
        return

    if not evaluation_run_members:
        raise ValueError("study analysis_plan requires at least one included evaluation-run membership")

    ambiguous_run_refs: list[str] = []
    ineligible_run_refs: list[str] = []
    for member in evaluation_run_members:
        reference_label = _format_reference(member.target_ref)
        matching_runs = [run for run in runs if _run_ref_matches_run(member.target_ref, run)]
        if len(matching_runs) > 1:
            ambiguous_run_refs.append(reference_label)
            continue
        for run in matching_runs:
            if not _run_is_eligible_for_study_analysis(run):
                ineligible_run_refs.append(f"{run.run_id}:{run.run_status}:{run.outcome_status}")

    if ambiguous_run_refs:
        joined = ", ".join(sorted(ambiguous_run_refs))
        raise ValueError(
            f"study evaluation-run memberships used for analysis must resolve to one supplied run artifact: {joined}"
        )
    if ineligible_run_refs:
        joined = ", ".join(sorted(ineligible_run_refs))
        raise ValueError(
            "study evaluation-run members used for analysis must not be invalidated, superseded, or not-evaluated: "
            f"{joined}"
        )


def _validate_study_run_allocation_coverage(
    study: ExperimentStudyModel,
    runs: list[ExperimentRunModel],
    evaluation_run_members: list[ExperimentStudyMembershipModel],
) -> None:
    allocation = study.run_allocation
    if allocation is None:
        return

    if not evaluation_run_members:
        raise ValueError("study run_allocation requires at least one included evaluation-run membership")

    condition_set = set(allocation.compared_conditions)
    grouped_run_keys: dict[str, set[tuple[str, str | None]]] = {
        condition: set() for condition in allocation.compared_conditions
    }
    condition_by_run_key: dict[tuple[str, str | None], str] = {}
    ungrouped_run_refs: list[str] = []
    unknown_groupings: list[str] = []
    ambiguous_run_refs: list[str] = []
    duplicate_run_assignments: list[str] = []
    ineligible_run_refs: list[str] = []
    unsatisfied_condition_runs: list[str] = []
    ambiguous_condition_runs: list[str] = []

    for member in evaluation_run_members:
        reference_label = _format_reference(member.target_ref)
        if member.grouping is None:
            ungrouped_run_refs.append(reference_label)
            continue
        if member.grouping not in condition_set:
            unknown_groupings.append(f"{reference_label}:{member.grouping}")
            continue

        matching_runs = [run for run in runs if _run_ref_matches_run(member.target_ref, run)]
        if len(matching_runs) > 1:
            ambiguous_run_refs.append(reference_label)
            continue
        for run in matching_runs:
            run_key = _run_model_key(run)
            prior_condition = condition_by_run_key.get(run_key)
            if prior_condition is not None:
                duplicate_run_assignments.append(f"{run.run_id}:{prior_condition},{member.grouping}")
                continue
            if not _run_is_eligible_for_study_analysis(run):
                ineligible_run_refs.append(f"{run.run_id}:{run.run_status}:{run.outcome_status}")
                continue
            assignment = allocation.condition_assignments[member.grouping]
            missing_condition_inputs = _run_satisfies_condition_assignment(run, assignment)
            if missing_condition_inputs:
                joined_missing_inputs = "|".join(sorted(missing_condition_inputs))
                unsatisfied_condition_runs.append(f"{run.run_id}:{member.grouping}:{joined_missing_inputs}")
                continue
            satisfied_other_conditions = sorted(
                condition_id
                for condition_id, candidate_assignment in allocation.condition_assignments.items()
                if condition_id != member.grouping
                and not _run_satisfies_condition_assignment(run, candidate_assignment)
            )
            if satisfied_other_conditions:
                joined_other_conditions = "|".join(satisfied_other_conditions)
                ambiguous_condition_runs.append(f"{run.run_id}:{member.grouping}:{joined_other_conditions}")
                continue
            condition_by_run_key[run_key] = member.grouping
            grouped_run_keys[member.grouping].add(run_key)

    if ungrouped_run_refs:
        joined = ", ".join(sorted(ungrouped_run_refs))
        raise ValueError(f"study run_allocation requires evaluation-run membership groupings: {joined}")
    if unknown_groupings:
        joined = ", ".join(sorted(unknown_groupings))
        raise ValueError(f"study evaluation-run groupings must be declared in compared_conditions: {joined}")
    if ambiguous_run_refs:
        joined = ", ".join(sorted(ambiguous_run_refs))
        raise ValueError(
            f"study evaluation-run membership references must resolve to one supplied run artifact: {joined}"
        )
    if duplicate_run_assignments:
        joined = ", ".join(sorted(duplicate_run_assignments))
        raise ValueError(f"study evaluation-run members must not assign the same run to multiple conditions: {joined}")
    if ineligible_run_refs:
        joined = ", ".join(sorted(ineligible_run_refs))
        raise ValueError(
            "study evaluation-run members used for analysis must not be invalidated, superseded, or not-evaluated: "
            f"{joined}"
        )
    if unsatisfied_condition_runs:
        joined = ", ".join(sorted(unsatisfied_condition_runs))
        raise ValueError(f"study evaluation-run members must satisfy their condition assignments: {joined}")
    if ambiguous_condition_runs:
        joined = ", ".join(sorted(ambiguous_condition_runs))
        raise ValueError(f"study evaluation-run members must satisfy exactly one condition assignment: {joined}")

    under_target_conditions = sorted(
        f"{condition}:{len(run_keys)}/{allocation.target_runs_per_condition}"
        for condition, run_keys in grouped_run_keys.items()
        if len(run_keys) < allocation.target_runs_per_condition
    )
    if under_target_conditions:
        joined = ", ".join(under_target_conditions)
        raise ValueError(
            f"study run_allocation target_runs_per_condition must be satisfied by included evaluation runs: {joined}"
        )


def validate_experiment_study_against_tasks_and_runs(
    study: ExperimentStudyModel,
    tasks: list[ExperimentTaskModel],
    runs: list[ExperimentRunModel] | None = None,
) -> None:
    """Validate study-level analysis semantics against concrete task/run artifacts."""

    runs = runs or []
    task_refs = [
        member.target_ref for member in study.membership.values() if member.role in {"primary-task", "comparison-task"}
    ]
    matched_tasks = [task for task in tasks if any(_task_ref_matches_task(reference, task) for reference in task_refs)]
    missing_task_refs = sorted(
        _format_reference(reference)
        for reference in task_refs
        if not any(_task_ref_matches_task(reference, task) for task in tasks)
    )
    if missing_task_refs:
        joined = ", ".join(missing_task_refs)
        raise ValueError(f"study task membership references must resolve to supplied task artifacts: {joined}")

    run_refs = [
        member.target_ref
        for member in study.membership.values()
        if member.role in {"calibration-run", "evaluation-run"}
    ]
    evaluation_run_members = [member for member in study.membership.values() if member.role == "evaluation-run"]
    evaluation_run_refs = [member.target_ref for member in evaluation_run_members]
    matched_runs = [run for run in runs if any(_run_ref_matches_run(reference, run) for reference in run_refs)]
    matched_evaluation_runs = [
        run for run in runs if any(_run_ref_matches_run(reference, run) for reference in evaluation_run_refs)
    ]
    missing_run_refs = sorted(
        _format_reference(reference)
        for reference in run_refs
        if not any(_run_ref_matches_run(reference, run) for run in runs)
    )
    if run_refs and missing_run_refs:
        joined = ", ".join(missing_run_refs)
        raise ValueError(f"study run membership references must resolve to supplied run artifacts: {joined}")

    _validate_study_analysis_run_eligibility(study, runs, evaluation_run_members)
    _validate_study_run_allocation_coverage(study, runs, evaluation_run_members)

    task_by_key = {_task_model_key(task): task for task in matched_tasks}
    for run in matched_runs:
        task = task_by_key.get(_task_reference_key(run.task_ref))
        if task is None:
            raise ValueError("study run members must reference a supplied study task artifact")
        validate_experiment_run_against_task(task, run)

    if study.analysis_plan is None:
        return
    declared_metric_ids = {
        metric_id for task in matched_tasks for metric_id in task.evaluation_protocol.metric_definitions
    }
    if not declared_metric_ids:
        raise ValueError("study analysis_plan metrics require supplied task protocol artifacts")
    ungrounded_metrics = sorted(
        metric_id for metric_id in study.analysis_plan.metrics if metric_id not in declared_metric_ids
    )
    if ungrounded_metrics:
        joined = ", ".join(ungrounded_metrics)
        raise ValueError(f"study analysis_plan metrics must be declared by included task protocols: {joined}")
    missing_run_metrics = sorted(
        f"{run.run_id}:{metric_id}"
        for run in matched_evaluation_runs
        for metric_id in study.analysis_plan.metrics
        if metric_id not in {result.metric_id for result in run.result_summaries.values()}
    )
    if missing_run_metrics:
        joined = ", ".join(missing_run_metrics)
        raise ValueError(
            "study analysis_plan metrics must have result_summaries, including explicit missing/withheld "
            f"statuses, in included evaluation runs: {joined}"
        )


class ConceptFamilyDefinitionModel(ContractModel):
    title: NonEmptyString
    description: NonEmptyString
    provenance: ConceptProvenanceCategory
    authority: str | None = Field(default=None, min_length=1)
    authority_reference: str | None = Field(default=None, min_length=1)
    extension_scope: str | None = Field(default=None, min_length=1)
    relation_rules: list[NonEmptyString] = Field(default_factory=list, min_length=1)
    non_ambiguity_constraints: list[NonEmptyString] = Field(default_factory=list, min_length=1)

    @model_validator(mode="after")
    def _validate_provenance_rules(self) -> ConceptFamilyDefinitionModel:
        if self.provenance in {ConceptProvenanceCategory.ADOPTED, ConceptProvenanceCategory.ADAPTED}:
            if self.authority is None or self.authority_reference is None:
                raise ValueError("adopted and adapted concept families require both authority and authority_reference")
        if self.provenance == ConceptProvenanceCategory.NATIVE and (
            self.authority is not None or self.authority_reference is not None
        ):
            raise ValueError("native concept families must not declare authority metadata")
        if self.provenance == ConceptProvenanceCategory.NATIVE:
            if self.extension_scope is None:
                raise ValueError("native concept families require extension_scope")
            if not self.relation_rules:
                raise ValueError("native concept families require relation_rules")
            if not self.non_ambiguity_constraints:
                raise ValueError("native concept families require non_ambiguity_constraints")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        json_schema.setdefault("allOf", []).extend(
            [
                {
                    "if": {
                        "properties": {"provenance": {"const": ConceptProvenanceCategory.ADOPTED.value}},
                        "required": ["provenance"],
                    },
                    "then": {
                        "required": ["authority", "authority_reference"],
                        "properties": {
                            "authority": {"type": "string", "minLength": 1},
                            "authority_reference": {"type": "string", "minLength": 1},
                        },
                    },
                },
                {
                    "if": {
                        "properties": {"provenance": {"const": ConceptProvenanceCategory.ADAPTED.value}},
                        "required": ["provenance"],
                    },
                    "then": {
                        "required": ["authority", "authority_reference"],
                        "properties": {
                            "authority": {"type": "string", "minLength": 1},
                            "authority_reference": {"type": "string", "minLength": 1},
                        },
                    },
                },
                {
                    "if": {
                        "properties": {"provenance": {"const": ConceptProvenanceCategory.NATIVE.value}},
                        "required": ["provenance"],
                    },
                    "then": {
                        "required": ["extension_scope", "relation_rules", "non_ambiguity_constraints"],
                        "properties": {
                            "extension_scope": {"type": "string", "minLength": 1},
                            "relation_rules": {"type": "array", "minItems": 1},
                            "non_ambiguity_constraints": {"type": "array", "minItems": 1},
                        },
                        "not": {
                            "anyOf": [
                                {"required": ["authority"]},
                                {"required": ["authority_reference"]},
                            ]
                        },
                    },
                },
            ]
        )
        return json_schema


class ConceptFamilyCatalogModel(ContractModel):
    schema_version: Literal[CONCEPT_FAMILIES_SCHEMA_VERSION] = CONCEPT_FAMILIES_SCHEMA_VERSION
    families: dict[NonEmptyString, ConceptFamilyDefinitionModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_family_keys(self) -> ConceptFamilyCatalogModel:
        if any(not family_id.strip() for family_id in self.families):
            raise ValueError("concept family identifiers must be non-empty")
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        families_schema = json_schema.get("properties", {}).get("families")
        if isinstance(families_schema, dict):
            families_schema.setdefault("propertyNames", {"minLength": 1})
        return json_schema


class ReferenceModelSchemaBindingModel(ContractModel):
    contract_id: NonEmptyString
    schema_pointer: JsonPointerString
    instance_path: InstancePath


class ReferenceModelDefinitionModel(ContractModel):
    title: NonEmptyString
    description: NonEmptyString
    concept_family: ConceptFamilyId
    authoritative_schema: ReferenceModelSchemaBindingModel
    reused_schemas: list[ReferenceModelSchemaBindingModel] = Field(default_factory=list)
    key_fields: list[NonEmptyString] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_reference_model_definition(self) -> ReferenceModelDefinitionModel:
        if len(self.key_fields) != len(set(self.key_fields)):
            raise ValueError("reference model key_fields must not contain duplicates")

        authoritative_key = (
            self.authoritative_schema.contract_id,
            self.authoritative_schema.schema_pointer,
            self.authoritative_schema.instance_path,
        )
        reused_keys = [
            (binding.contract_id, binding.schema_pointer, binding.instance_path) for binding in self.reused_schemas
        ]
        if len(reused_keys) != len(set(reused_keys)):
            raise ValueError("reference model reused_schemas must not contain duplicate schema bindings")
        if authoritative_key in set(reused_keys):
            raise ValueError("reference model reused_schemas must not repeat authoritative_schema")
        return self


class ReferenceModelCatalogModel(ContractModel):
    schema_version: Literal[REFERENCE_MODELS_SCHEMA_VERSION] = REFERENCE_MODELS_SCHEMA_VERSION
    models: dict[NonEmptyString, ReferenceModelDefinitionModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_reference_models(self) -> ReferenceModelCatalogModel:
        known_families = _authoritative_concept_family_ids()
        unknown_families = {
            model.concept_family for model in self.models.values() if model.concept_family not in known_families
        }
        if unknown_families:
            unknown = ", ".join(sorted(unknown_families))
            raise ValueError(f"reference models include unknown concept families: {unknown}")

        known_contracts = _known_contract_ids()
        unknown_contracts = {
            binding.contract_id
            for model in self.models.values()
            for binding in (model.authoritative_schema, *model.reused_schemas)
            if binding.contract_id not in known_contracts
        }
        if unknown_contracts:
            unknown = ", ".join(sorted(unknown_contracts))
            raise ValueError(f"reference models include unknown contract ids: {unknown}")

        for model_id, model in self.models.items():
            _validate_reference_model_schema_binding(
                model_id=model_id,
                binding_label="authoritative_schema",
                binding=model.authoritative_schema,
                key_fields=model.key_fields,
            )
            for index, binding in enumerate(model.reused_schemas):
                _validate_reference_model_schema_binding(
                    model_id=model_id,
                    binding_label=f"reused_schemas[{index}]",
                    binding=binding,
                    key_fields=model.key_fields,
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        models_schema = json_schema.get("properties", {}).get("models")
        if isinstance(models_schema, dict):
            models_schema.setdefault("propertyNames", {"minLength": 1})
        return json_schema


_UCO_NAMESPACE_BASE = "https://ontology.unifiedcyberontology.org/uco/"
UcoClassName = Annotated[str, Field(pattern=r"^[a-z][a-z0-9]*:[A-Z][A-Za-z0-9]*$")]


class UcoAlignmentTypeModel(ContractModel):
    uco_class: UcoClassName
    iri: NonEmptyString
    note: NonEmptyString

    @model_validator(mode="after")
    def _validate_canonical_iri(self) -> UcoAlignmentTypeModel:
        prefix, _, local = self.uco_class.partition(":")
        expected_iri = f"{_UCO_NAMESPACE_BASE}{prefix}/{local}"
        if self.iri != expected_iri:
            raise ValueError(
                f"uco_type iri must equal the canonical UCO IRI {expected_iri} for class "
                f"{self.uco_class!r}; got {self.iri!r}"
            )
        return self


class UcoFamilyAlignmentModel(ContractModel):
    concept_family: ConceptFamilyId
    provenance: ConceptProvenanceCategory
    uco_types: list[UcoAlignmentTypeModel] = Field(min_length=1)
    divergences: list[NonEmptyString]

    @model_validator(mode="after")
    def _validate_family_alignment(self) -> UcoFamilyAlignmentModel:
        if self.provenance == ConceptProvenanceCategory.NATIVE:
            raise ValueError(f"uco alignment family {self.concept_family!r} must be adopted or adapted, not native")
        if self.provenance == ConceptProvenanceCategory.ADAPTED and not self.divergences:
            raise ValueError(
                f"adapted uco alignment family {self.concept_family!r} must enumerate at least one divergence"
            )
        if self.provenance == ConceptProvenanceCategory.ADOPTED and self.divergences:
            raise ValueError(
                f"adopted uco alignment family {self.concept_family!r} must record an empty divergences list"
            )
        uco_classes = [uco_type.uco_class for uco_type in self.uco_types]
        if len(uco_classes) != len(set(uco_classes)):
            raise ValueError(f"uco alignment family {self.concept_family!r} must not repeat a uco_class")
        return self


class UcoAlignmentCatalogModel(ContractModel):
    schema_version: Literal[UCO_ALIGNMENT_SCHEMA_VERSION] = UCO_ALIGNMENT_SCHEMA_VERSION
    uco_version: NonEmptyString
    uco_reference: NonEmptyString
    review_scope: NonEmptyString
    alignments: dict[NonEmptyString, UcoFamilyAlignmentModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_alignment_catalog(self) -> UcoAlignmentCatalogModel:
        for family_id, alignment in self.alignments.items():
            if not family_id.strip():
                raise ValueError("uco alignment family identifiers must be non-empty")
            if alignment.concept_family != family_id:
                raise ValueError(
                    f"uco alignment entry key {family_id!r} must match its concept_family {alignment.concept_family!r}"
                )

        expected_provenance = _uco_cyber_concept_family_provenance()
        actual_ids = set(self.alignments)
        expected_ids = set(expected_provenance)
        missing = expected_ids - actual_ids
        unexpected = actual_ids - expected_ids
        if missing or unexpected:
            raise ValueError(
                "uco alignment must cover exactly the adopted/adapted UCO concept families declared in "
                f"concept-families-v1; missing: {sorted(missing)}; unexpected: {sorted(unexpected)}"
            )

        for family_id, alignment in self.alignments.items():
            declared = expected_provenance[family_id]
            if alignment.provenance.value != declared:
                raise ValueError(
                    f"uco alignment provenance for family {family_id!r} is "
                    f"{alignment.provenance.value!r} but concept-families-v1 declares {declared!r}"
                )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        alignments_schema = json_schema.get("properties", {}).get("alignments")
        if isinstance(alignments_schema, dict):
            alignments_schema.setdefault("propertyNames", {"minLength": 1})
        return json_schema


class ControlledVocabularySourceModel(ContractModel):
    provenance: Literal["adopted", "adapted"]
    authority: NonEmptyString
    authority_version: NonEmptyString
    source_artifact_ref: NonEmptyString
    source_url: NonEmptyString
    source_digest: PrefixedDigestString
    citation_urls: list[NonEmptyString] = Field(min_length=1)
    license_url: NonEmptyString
    license_notice: NonEmptyString


class ControlledVocabularyTermModel(ContractModel):
    title: NonEmptyString
    description: NonEmptyString
    source_id: NonEmptyString | None = None
    source_url: NonEmptyString | None = None


class ControlledVocabularyDefinitionModel(ContractModel):
    title: NonEmptyString
    description: NonEmptyString
    source: ControlledVocabularySourceModel | None = None
    kind: Literal["enumeration", "vocabulary"]
    governed_scopes: list[NonEmptyString] = Field(default_factory=list)
    extension_policy: Literal["closed", "governed-extension"]
    extension_pattern: NonEmptyString | None = None
    terms: dict[ControlledVocabularyTermId, ControlledVocabularyTermModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_vocabulary_definition(self) -> ControlledVocabularyDefinitionModel:
        unknown_scopes = set(self.governed_scopes) - _CONTROLLED_VOCABULARY_GOVERNED_SCOPES
        if unknown_scopes:
            unknown = ", ".join(sorted(unknown_scopes))
            raise ValueError(f"controlled vocabulary includes unknown governed scopes: {unknown}")

        if len(self.governed_scopes) != len(set(self.governed_scopes)):
            raise ValueError("controlled vocabulary governed_scopes must not contain duplicates")

        if self.kind == "enumeration" and self.extension_policy != "closed":
            raise ValueError("enumeration controlled vocabularies must use extension_policy='closed'")

        if self.extension_policy == "closed":
            if self.extension_pattern is not None:
                raise ValueError("closed controlled vocabularies must not declare extension_pattern")
            return self

        if not self.governed_scopes:
            raise ValueError("governed-extension controlled vocabularies must declare governed_scopes")
        if self.extension_pattern is None:
            raise ValueError("governed-extension controlled vocabularies must declare extension_pattern")
        try:
            re.compile(self.extension_pattern)
        except re.error as exc:
            raise ValueError("controlled vocabulary extension_pattern must be a valid regex") from exc
        return self


class ControlledVocabularyCatalogModel(ContractModel):
    schema_version: Literal[CONTROLLED_VOCABULARIES_SCHEMA_VERSION] = CONTROLLED_VOCABULARIES_SCHEMA_VERSION
    vocabularies: dict[NonEmptyString, ControlledVocabularyDefinitionModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_vocabulary_catalog(self) -> ControlledVocabularyCatalogModel:
        scope_to_vocabulary: dict[str, str] = {}
        for vocabulary_id, definition in self.vocabularies.items():
            for scope in definition.governed_scopes:
                previous = scope_to_vocabulary.setdefault(scope, vocabulary_id)
                if previous != vocabulary_id:
                    raise ValueError(
                        f"governed scope '{scope}' is declared by multiple vocabularies: {previous}, {vocabulary_id}"
                    )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        vocabularies_schema = json_schema.get("properties", {}).get("vocabularies")
        if isinstance(vocabularies_schema, dict):
            vocabularies_schema.setdefault("propertyNames", {"minLength": 1})
        return json_schema


class AttackEnterpriseTacticSourceTermModel(ContractModel):
    tactic_id: Annotated[str, Field(pattern=r"^TA[0-9]{4}$")]
    shortname: ControlledVocabularyTermId
    name: NonEmptyString
    description: NonEmptyString
    url: NonEmptyString
    stix_id: NonEmptyString


class AttackEnterpriseTacticsSourceModel(ContractModel):
    schema_version: Literal[ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION] = (
        ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION
    )
    source_authority: Literal["MITRE ATT&CK"]
    source_domain: Literal["enterprise-attack"]
    source_version: NonEmptyString
    source_url: NonEmptyString
    source_digest: PrefixedDigestString
    citation_urls: list[NonEmptyString] = Field(min_length=1)
    retrieved_at: CalendarDateString
    license_url: NonEmptyString
    license_notice: NonEmptyString
    tactics: list[AttackEnterpriseTacticSourceTermModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_attack_tactics_source(self) -> AttackEnterpriseTacticsSourceModel:
        tactic_ids = [tactic.tactic_id for tactic in self.tactics]
        if len(tactic_ids) != len(set(tactic_ids)):
            raise ValueError("ATT&CK Enterprise tactic source must not contain duplicate tactic_id values")

        shortnames = [tactic.shortname for tactic in self.tactics]
        if len(shortnames) != len(set(shortnames)):
            raise ValueError("ATT&CK Enterprise tactic source must not contain duplicate shortname values")
        return self


class AtlasTacticSourceTermModel(ContractModel):
    tactic_id: Annotated[str, Field(pattern=r"^AML\.TA[0-9]{4}$")]
    shortname: ControlledVocabularyTermId
    name: NonEmptyString
    description: NonEmptyString
    url: NonEmptyString
    position: PositiveInteger
    uuid: NonEmptyString
    created_date: CalendarDateString
    modified_date: CalendarDateString
    attack_reference_id: NonEmptyString | None = None
    attack_reference_url: NonEmptyString | None = None


class AtlasTacticsSourceModel(ContractModel):
    schema_version: Literal[ATLAS_TACTICS_SOURCE_SCHEMA_VERSION] = ATLAS_TACTICS_SOURCE_SCHEMA_VERSION
    source_authority: Literal["MITRE ATLAS"]
    source_version: NonEmptyString
    source_format_version: NonEmptyString
    source_url: NonEmptyString
    source_digest: PrefixedDigestString
    citation_urls: list[NonEmptyString] = Field(min_length=1)
    retrieved_at: CalendarDateString
    license_url: NonEmptyString
    license_notice: NonEmptyString
    collection_id: Literal["ATLAS-collection"]
    matrix_id: Literal["ATLAS-matrix"]
    tactics: list[AtlasTacticSourceTermModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_atlas_tactics_source(self) -> AtlasTacticsSourceModel:
        tactic_ids = [tactic.tactic_id for tactic in self.tactics]
        if len(tactic_ids) != len(set(tactic_ids)):
            raise ValueError("ATLAS tactic source must not contain duplicate tactic_id values")

        shortnames = [tactic.shortname for tactic in self.tactics]
        if len(shortnames) != len(set(shortnames)):
            raise ValueError("ATLAS tactic source must not contain duplicate shortname values")

        positions = [tactic.position for tactic in self.tactics]
        if len(positions) != len(set(positions)):
            raise ValueError("ATLAS tactic source must not contain duplicate position values")
        if positions != sorted(positions):
            raise ValueError("ATLAS tactic source tactics must be ordered by matrix position")
        return self


class SemanticBehaviorAssumptionModel(ContractModel):
    id: SemanticAssumptionId
    statement: NonEmptyString


class SemanticProfilePhaseModel(ContractModel):
    required_contracts: list[NonEmptyString] = Field(min_length=1)
    required_concept_families: list[ConceptFamilyId] = Field(min_length=1)
    required_bindings: list[ConceptBindingEntryModel] = Field(default_factory=list)
    behavior_assumptions: list[SemanticBehaviorAssumptionModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_phase_assumptions(self) -> SemanticProfilePhaseModel:
        if len(self.required_contracts) != len(set(self.required_contracts)):
            raise ValueError("semantic profile required_contracts must not contain duplicates")
        if len(self.required_concept_families) != len(set(self.required_concept_families)):
            raise ValueError("semantic profile required_concept_families must not contain duplicates")

        known_contracts = _known_contract_ids()
        unknown_contracts = set(self.required_contracts) - known_contracts
        if unknown_contracts:
            unknown = ", ".join(sorted(unknown_contracts))
            raise ValueError(f"semantic profile required_contracts include unknown contract ids: {unknown}")

        known_families = _authoritative_concept_family_ids()
        unknown_families = set(self.required_concept_families) - known_families
        if unknown_families:
            unknown = ", ".join(sorted(unknown_families))
            raise ValueError(f"semantic profile required_concept_families include unknown families: {unknown}")

        binding_scopes = [binding.scope for binding in self.required_bindings]
        if len(binding_scopes) != len(set(binding_scopes)):
            raise ValueError("semantic profile required_bindings must not contain duplicate scopes")

        undeclared_binding_families = {
            binding.family for binding in self.required_bindings if binding.family not in self.required_concept_families
        }
        if undeclared_binding_families:
            missing = ", ".join(sorted(undeclared_binding_families))
            raise ValueError(
                f"semantic profile required_bindings must use families declared in required_concept_families: {missing}"
            )

        assumption_ids = [assumption.id for assumption in self.behavior_assumptions]
        if len(assumption_ids) != len(set(assumption_ids)):
            raise ValueError("semantic profile behavior_assumptions must not contain duplicate ids")
        return self


class SemanticProfileModel(ContractModel):
    schema_version: Literal[SEMANTIC_PROFILE_SCHEMA_VERSION] = SEMANTIC_PROFILE_SCHEMA_VERSION
    profile_id: SemanticProfileId
    title: NonEmptyString
    description: NonEmptyString
    concept_catalog_version: Literal[CONCEPT_FAMILIES_SCHEMA_VERSION]
    authoring: SemanticProfilePhaseModel
    exchange: SemanticProfilePhaseModel
    processing: SemanticProfilePhaseModel
    execution: SemanticProfilePhaseModel

    @model_validator(mode="after")
    def _validate_phase_binding_scopes(self) -> SemanticProfileModel:
        for phase_name, allowed_scopes in _SEMANTIC_PROFILE_PHASE_ALLOWED_BINDING_SCOPES.items():
            phase = getattr(self, phase_name)
            declared_scopes = {binding.scope for binding in phase.required_bindings}
            invalid_scopes = declared_scopes - allowed_scopes
            if invalid_scopes:
                invalid = ", ".join(sorted(invalid_scopes))
                if allowed_scopes:
                    allowed = ", ".join(sorted(allowed_scopes))
                    raise ValueError(
                        f"semantic profile {phase_name} required_bindings include scopes outside the governed "
                        f"{phase_name} surfaces: {invalid}; allowed scopes: {allowed}"
                    )
                raise ValueError(
                    f"semantic profile {phase_name} does not define governed required_bindings surfaces: {invalid}"
                )
        return self


@lru_cache(maxsize=1)
def _authoritative_concept_family_ids() -> frozenset[str]:
    catalog_path = corpus_family_root(CONCEPT_AUTHORITY) / "concept-families-v1.json"
    payload = json.loads(catalog_path.read_text(encoding="utf-8"))
    catalog = ConceptFamilyCatalogModel.model_validate(payload)
    return frozenset(catalog.families)


@lru_cache(maxsize=1)
def _uco_cyber_concept_family_provenance() -> dict[str, str]:
    """Map each adopted/adapted family whose authority is UCO to its provenance.

    This is the single source of truth for UCO alignment coverage: the
    ``uco-alignment-v1`` catalog must declare exactly these families, so the
    cyber-domain family slice is never hard-coded in a second place.
    """

    catalog_path = corpus_family_root(CONCEPT_AUTHORITY) / "concept-families-v1.json"
    payload = json.loads(catalog_path.read_text(encoding="utf-8"))
    catalog = ConceptFamilyCatalogModel.model_validate(payload)
    return {
        family_id: family.provenance.value
        for family_id, family in catalog.families.items()
        if family.provenance in {ConceptProvenanceCategory.ADOPTED, ConceptProvenanceCategory.ADAPTED}
        and family.authority == "UCO"
    }


@lru_cache(maxsize=1)
def _known_contract_ids() -> frozenset[str]:
    return frozenset(schema_bundle())


def _decode_json_pointer_segment(segment: str) -> str:
    return segment.replace("~1", "/").replace("~0", "~")


def _resolve_schema_pointer(schema_root: dict[str, Any], pointer: str) -> dict[str, Any]:
    if not pointer.startswith("#/"):
        raise KeyError(pointer)

    current: Any = schema_root
    for raw_segment in pointer[2:].split("/"):
        segment = _decode_json_pointer_segment(raw_segment)
        if not isinstance(current, dict) or segment not in current:
            raise KeyError(pointer)
        current = current[segment]

    if not isinstance(current, dict):
        raise KeyError(pointer)
    return current


def _collapse_nullable_optional_schema(schema_node: dict[str, Any]) -> dict[str, Any]:
    """Collapse a nullable-optional ``anyOf`` wrapper to its single non-null branch.

    Pydantic renders an optional reference field (``X | None``) as a two-member
    ``anyOf`` whose branches are the referenced schema and ``{"type": "null"}``.
    Reference-model bindings address the underlying structure, so the resolver
    looks through that wrapper (for example ``nodes.*.runtime``, an optional
    ``RuntimeConfiguration``). Anything that is not exactly this nullable-optional
    shape is returned unchanged, so non-optional and non-reference schemas are
    unaffected.
    """
    any_of = schema_node.get("anyOf")
    if not isinstance(any_of, list) or len(any_of) != 2:
        return schema_node
    non_null = [branch for branch in any_of if not (isinstance(branch, dict) and branch.get("type") == "null")]
    null_branches = [branch for branch in any_of if isinstance(branch, dict) and branch.get("type") == "null"]
    if len(non_null) == 1 and len(null_branches) == 1 and isinstance(non_null[0], dict):
        return non_null[0]
    return schema_node


def _resolve_ref_schema(schema_root: dict[str, Any], schema_node: dict[str, Any]) -> dict[str, Any]:
    current = schema_node
    while True:
        if "$ref" in current:
            ref = current["$ref"]
            if not isinstance(ref, str):
                raise KeyError(ref)
            current = _resolve_schema_pointer(schema_root, ref)
            continue
        collapsed = _collapse_nullable_optional_schema(current)
        if collapsed is not current:
            current = collapsed
            continue
        return current


def _resolve_instance_path_schema(schema_root: dict[str, Any], instance_path: str) -> dict[str, Any]:
    current = schema_root
    for segment in instance_path.split("."):
        current = _resolve_ref_schema(schema_root, current)
        if segment == "*":
            additional_properties = current.get("additionalProperties")
            if not isinstance(additional_properties, dict):
                raise KeyError(instance_path)
            current = additional_properties
            continue

        properties = current.get("properties")
        if not isinstance(properties, dict) or segment not in properties or not isinstance(properties[segment], dict):
            raise KeyError(instance_path)
        current = properties[segment]
    return _resolve_ref_schema(schema_root, current)


def _validate_reference_model_schema_binding(
    *,
    model_id: str,
    binding_label: str,
    binding: ReferenceModelSchemaBindingModel,
    key_fields: list[str],
) -> None:
    schema_root = schema_bundle()[binding.contract_id]
    try:
        pointer_schema = _resolve_ref_schema(schema_root, _resolve_schema_pointer(schema_root, binding.schema_pointer))
    except KeyError as exc:
        raise ValueError(
            f"reference model {model_id} {binding_label} schema_pointer '{binding.schema_pointer}' "
            f"does not resolve within contract '{binding.contract_id}'"
        ) from exc

    try:
        instance_schema = _resolve_instance_path_schema(schema_root, binding.instance_path)
    except KeyError as exc:
        raise ValueError(
            f"reference model {model_id} {binding_label} instance_path '{binding.instance_path}' "
            f"does not resolve within contract '{binding.contract_id}'"
        ) from exc

    if pointer_schema != instance_schema:
        raise ValueError(
            f"reference model {model_id} {binding_label} instance_path '{binding.instance_path}' "
            f"does not resolve to schema_pointer '{binding.schema_pointer}' in contract '{binding.contract_id}'"
        )

    properties = pointer_schema.get("properties")
    if not isinstance(properties, dict):
        raise ValueError(
            f"reference model {model_id} {binding_label} schema_pointer '{binding.schema_pointer}' "
            "must resolve to an object schema with properties"
        )

    missing_key_fields = [field for field in key_fields if field not in properties]
    if missing_key_fields:
        missing = ", ".join(sorted(missing_key_fields))
        raise ValueError(
            f"reference model {model_id} key_fields are not declared by schema_pointer "
            f"'{binding.schema_pointer}' in contract '{binding.contract_id}': {missing}"
        )


def _scope_is_present(model: ContractModel, scope: str) -> bool:
    current: Any = model
    for segment in scope.split("."):
        if not isinstance(current, BaseModel):
            return False
        if segment not in type(current).model_fields:
            return False
        current = getattr(current, segment)
        if current is None:
            return False
    return True


def _validate_controlled_vocabulary_terms(scope: str, values: list[str]) -> None:
    if not values:
        return
    from .controlled_vocabularies import validate_controlled_vocabulary_scope_values

    validate_controlled_vocabulary_scope_values(scope, values)


def _validate_unique_string_values(field_name: str, values: list[str]) -> None:
    duplicates = sorted({value for value in values if values.count(value) > 1})
    if duplicates:
        joined = ", ".join(duplicates)
        raise ValueError(f"{field_name} must not contain duplicate values: {joined}")


def _validate_canonical_concept_bindings(model: ContractModel, *, allowed_scopes: frozenset[str]) -> None:
    family_ids = _authoritative_concept_family_ids()
    for binding in getattr(model, "concept_bindings", ()):
        if binding.family not in family_ids:
            raise ValueError(f"concept_bindings family '{binding.family}' is not defined in concept-families-v1")
        if binding.scope not in allowed_scopes:
            allowed = ", ".join(sorted(allowed_scopes))
            raise ValueError(
                f"concept_bindings scope '{binding.scope}' is not a governed manifest vocabulary surface; "
                f"allowed scopes: {allowed}"
            )
        if not _scope_is_present(model, binding.scope):
            raise ValueError(
                f"concept_bindings scope '{binding.scope}' does not resolve to a declared field in this manifest"
            )


def _backend_profile_schema_for_bundle() -> dict[str, Any]:
    """Lazily import :class:`BackendProfileModel` to avoid an import cycle.

    ``backend_profiles`` imports :class:`ContractModel` and ``NonEmptyString``
    from this module, so eager import at module load would cycle. The
    deferred import here keeps the schema bundle wired up while letting
    ``backend_profiles`` build on the same closed-world primitives the rest
    of the contracts surface uses.
    """

    from .backend_profiles import BackendProfileModel

    return BackendProfileModel.model_json_schema()


def _event_stream_schema(title: str, item_schema: dict[str, Any]) -> dict[str, Any]:
    item_schema = dict(item_schema)
    defs = item_schema.pop(_DEFS_KEY, None)
    schema = {
        _JSON_SCHEMA_KEY: _JSON_SCHEMA_DRAFT_2020_12,
        "title": title,
        "type": "array",
        "items": item_schema,
    }
    if defs:
        schema[_DEFS_KEY] = defs
    return schema


REUSABLE_ASSET_FAMILIES: tuple[str, ...] = (
    "reusable_scenario",
    "associated_artifact_set",
    "sdl_module",
    "experiment_task",
    "experiment_study",
    "behavior_vocabulary",
    "participant_manifest",
    "evidence_artifact",
)
_REUSABLE_ASSET_FAMILY_SET = frozenset(REUSABLE_ASSET_FAMILIES)

ReusableAssetFamily = Literal[
    "reusable_scenario",
    "associated_artifact_set",
    "sdl_module",
    "experiment_task",
    "experiment_study",
    "behavior_vocabulary",
    "participant_manifest",
    "evidence_artifact",
]

# Trust/authenticity/integrity evidence classes (GOV-913). Grounded in the
# supply-chain standards recorded in ADR-071: integrity_digest (in-toto/OCI/C2PA
# hard binding), authenticity_signature (Sigstore/DSSE/COSE + TUF thresholds),
# provenance_lock_record (SLSA resolvedDependencies / module lock records),
# governance_source (TUF-delegation-style authoritative vocabulary origin), and
# artifact_checksum (OCI descriptor / SBOM component hash for content artifacts).
ReusableAssetEvidenceClass = Literal[
    "integrity_digest",
    "authenticity_signature",
    "provenance_lock_record",
    "governance_source",
    "artifact_checksum",
]
REUSABLE_ASSET_EVIDENCE_CLASSES: tuple[str, ...] = (
    "integrity_digest",
    "authenticity_signature",
    "provenance_lock_record",
    "governance_source",
    "artifact_checksum",
)
_INTEGRITY_EVIDENCE_CLASSES = frozenset({"integrity_digest", "artifact_checksum"})

ReusableAssetEnforcement = Literal["required", "recommended", "optional"]


class ReusableAssetEvidenceRequirementModel(ContractModel):
    """One evidence-class expectation an asset family must satisfy.

    ``mechanism_ref`` names the *existing* ACES mechanism that carries the
    evidence (e.g. ``aces.lock.json`` digest pins, ``ExperimentChecksumModel``,
    ``controlled-vocabularies-v1.source``). GOV-913 declares policy over the
    incumbent mechanisms; it does not introduce a parallel evidence store, so
    this contract never carries the evidence payload itself — only the
    requirement and a reference to where the evidence lives.
    """

    evidence_class: ReusableAssetEvidenceClass
    enforcement: ReusableAssetEnforcement
    mechanism_ref: NonEmptyString
    description: NonEmptyString


class ReusableAssetAuthenticityPolicyModel(ContractModel):
    """Trusted-signer set + M-of-N threshold for signature-bearing families.

    Threshold trust (TUF) means no single key compromise forges an asset; the
    ``trusted_signer_set_ref`` points at the governed signer set (e.g. a
    ``RegistryTrustPolicy`` trusted-signer declaration) and never embeds key
    material — portable artifacts carry public verification material only.
    """

    trusted_signer_set_ref: NonEmptyString
    threshold: PositiveInteger


def _reusable_asset_duplicate_evidence_classes(
    requirements: list[ReusableAssetEvidenceRequirementModel],
) -> list[str]:
    classes = [requirement.evidence_class for requirement in requirements]
    return sorted({value for value in classes if classes.count(value) > 1})


def _reusable_asset_has_required_integrity(
    requirements: list[ReusableAssetEvidenceRequirementModel],
) -> bool:
    return any(
        requirement.evidence_class in _INTEGRITY_EVIDENCE_CLASSES and requirement.enforcement == "required"
        for requirement in requirements
    )


def _reusable_asset_signature_enforced(
    requirements: list[ReusableAssetEvidenceRequirementModel],
) -> bool:
    return any(
        requirement.evidence_class == "authenticity_signature"
        and requirement.enforcement in {"required", "recommended"}
        for requirement in requirements
    )


def _reusable_asset_has_required_governance_source(
    requirements: list[ReusableAssetEvidenceRequirementModel],
) -> bool:
    return any(
        requirement.evidence_class == "governance_source" and requirement.enforcement == "required"
        for requirement in requirements
    )


class ReusableAssetFamilyTrustPolicyModel(ContractModel):
    """Per-family trust/authenticity/integrity policy for a reusable asset."""

    asset_family: ReusableAssetFamily
    identity_basis: NonEmptyString
    evidence_requirements: list[ReusableAssetEvidenceRequirementModel] = Field(min_length=1)
    authenticity_policy: ReusableAssetAuthenticityPolicyModel | None = None

    @model_validator(mode="after")
    def _validate_family_policy(self) -> ReusableAssetFamilyTrustPolicyModel:
        duplicates = _reusable_asset_duplicate_evidence_classes(self.evidence_requirements)
        if duplicates:
            raise ValueError(f"asset family {self.asset_family!r} declares duplicate evidence classes: {duplicates}")

        if not _reusable_asset_has_required_integrity(self.evidence_requirements):
            raise ValueError(
                f"asset family {self.asset_family!r} must declare a required integrity evidence "
                "class (integrity_digest or artifact_checksum); integrity is the GOV-913 baseline"
            )

        signature_enforced = _reusable_asset_signature_enforced(self.evidence_requirements)
        if signature_enforced and self.authenticity_policy is None:
            raise ValueError(
                f"asset family {self.asset_family!r} enforces authenticity_signature but declares no "
                "authenticity_policy; a trusted-signer set and threshold are required (identity is not "
                "authenticity)"
            )
        if not signature_enforced and self.authenticity_policy is not None:
            raise ValueError(
                f"asset family {self.asset_family!r} declares an authenticity_policy without a "
                "required/recommended authenticity_signature requirement"
            )

        if self.asset_family == "behavior_vocabulary" and not _reusable_asset_has_required_governance_source(
            self.evidence_requirements
        ):
            raise ValueError(
                "asset family 'behavior_vocabulary' must declare a required governance_source "
                "evidence class; authoritative origin is a first-class evidence class for "
                "reusable governed vocabularies"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        # The published schema is the portable contract external consumers
        # validate against; the security invariants must live in the schema
        # itself, not only in these Python validators (issue #115 review).
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        constraints: list[dict[str, Any]] = []
        # Every family MUST require at least one integrity evidence class.
        constraints.append(
            {
                "properties": {
                    "evidence_requirements": {
                        "contains": {
                            "type": "object",
                            "required": ["evidence_class", "enforcement"],
                            "properties": {
                                "evidence_class": {"enum": ["integrity_digest", "artifact_checksum"]},
                                "enforcement": {"const": "required"},
                            },
                        }
                    }
                },
                "required": ["evidence_requirements"],
            }
        )
        # Each evidence class may appear at most once within a family.
        for evidence_class in REUSABLE_ASSET_EVIDENCE_CLASSES:
            constraints.append(
                {
                    "properties": {
                        "evidence_requirements": {
                            "contains": {
                                "type": "object",
                                "required": ["evidence_class"],
                                "properties": {"evidence_class": {"const": evidence_class}},
                            },
                            "minContains": 0,
                            "maxContains": 1,
                        }
                    }
                }
            )
        # Enforced authenticity_signature requires a threshold-backed policy.
        constraints.append(
            {
                "if": {
                    "properties": {
                        "evidence_requirements": {
                            "contains": {
                                "type": "object",
                                "required": ["evidence_class", "enforcement"],
                                "properties": {
                                    "evidence_class": {"const": "authenticity_signature"},
                                    "enforcement": {"enum": ["required", "recommended"]},
                                },
                            }
                        }
                    },
                    "required": ["evidence_requirements"],
                },
                "then": {"required": ["authenticity_policy"]},
            }
        )
        # behavior_vocabulary MUST carry a required governance_source (authoritative
        # origin is a first-class evidence class for governed reusable semantics).
        constraints.append(
            {
                "if": {
                    "properties": {"asset_family": {"const": "behavior_vocabulary"}},
                    "required": ["asset_family"],
                },
                "then": {
                    "properties": {
                        "evidence_requirements": {
                            "contains": {
                                "type": "object",
                                "required": ["evidence_class", "enforcement"],
                                "properties": {
                                    "evidence_class": {"const": "governance_source"},
                                    "enforcement": {"const": "required"},
                                },
                            }
                        }
                    },
                    "required": ["evidence_requirements"],
                },
            }
        )
        json_schema.setdefault("allOf", []).extend(constraints)
        return json_schema


class ReusableAssetTrustPolicyModel(ContractModel):
    """Ecosystem trust/authenticity/integrity policy over reusable assets (GOV-913).

    A declarative, expectation-based policy: it declares, per asset family, the
    integrity/authenticity/provenance/governance evidence the ecosystem requires,
    referencing the existing ACES mechanisms that carry that evidence. It is not a
    per-asset trust record and it invents no cryptography. See
    ``specs/authority/reusable-asset-trust-integrity.md`` (normative) and ADR-071.
    """

    schema_version: Literal[REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION] = REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION
    policy_id: NonEmptyString
    families: list[ReusableAssetFamilyTrustPolicyModel] = Field(min_length=1)

    @model_validator(mode="after")
    def _validate_trust_policy(self) -> ReusableAssetTrustPolicyModel:
        declared = [family.asset_family for family in self.families]
        duplicates = sorted({value for value in declared if declared.count(value) > 1})
        if duplicates:
            raise ValueError(f"reusable asset trust policy declares duplicate asset families: {duplicates}")

        declared_set = set(declared)
        missing = sorted(_REUSABLE_ASSET_FAMILY_SET - declared_set)
        if missing:
            raise ValueError(
                f"reusable asset trust policy must cover every canonical reusable asset family; missing: {missing}"
            )
        return self

    @classmethod
    def __get_pydantic_json_schema__(
        cls,
        core_schema: CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        # Encode complete-family-coverage in the portable schema: exactly the
        # canonical families, each present once (issue #115 review). With the
        # asset_family enum bounded to these values, min/max-items pinned to the
        # family count, and a `contains` clause per family, the only conforming
        # shape is a bijection onto the canonical set — matching the validator.
        json_schema = handler(core_schema)
        json_schema = handler.resolve_ref_schema(json_schema)
        families = json_schema.get("properties", {}).get("families")
        if isinstance(families, dict):
            family_count = len(REUSABLE_ASSET_FAMILIES)
            families["minItems"] = family_count
            families["maxItems"] = family_count
            families["allOf"] = [
                {
                    "contains": {
                        "type": "object",
                        "required": ["asset_family"],
                        "properties": {"asset_family": {"const": family}},
                    }
                }
                for family in REUSABLE_ASSET_FAMILIES
            ]
        return json_schema


def _raw_schema_bundle() -> dict[str, dict[str, Any]]:
    from aces_contracts.realization_envelope import BackendRealizationEnvelopeModel

    from .behavioral_relations import BehavioralRelationCatalogModel
    from .provenance import SDLLineageLedgerModel
    from .scientific_completeness import (
        ScientificCompletenessAssessmentModel,
        ScientificCompletenessTaxonomyModel,
    )

    return {
        "aces-semantic-invariants-v1": _aces_semantic_invariant_profile_schema_for_bundle(),
        "sdl-authoring-input-v1": Scenario.model_json_schema(),
        "instantiated-scenario-v1": InstantiatedScenario.model_json_schema(),
        "instantiated-scenario-snapshot-v1": InstantiatedScenarioSnapshot.model_json_schema(),
        "scenario-instantiation-request-v1": InstantiationRequestModel.model_json_schema(),
        "backend-manifest-v2": BackendManifestV2Model.model_json_schema(),
        "realization-envelope-v1": BackendRealizationEnvelopeModel.model_json_schema(),
        "processor-manifest-v2": ProcessorManifestV2Model.model_json_schema(),
        "participant-implementation-manifest-v1": ParticipantImplementationManifestModel.model_json_schema(),
        "participant-implementation-provenance-v1": ParticipantImplementationProvenanceModel.model_json_schema(),
        "concept-families-v1": ConceptFamilyCatalogModel.model_json_schema(),
        "behavioral-relations-v1": BehavioralRelationCatalogModel.model_json_schema(),
        "reference-models-v1": ReferenceModelCatalogModel.model_json_schema(),
        "uco-alignment-v1": UcoAlignmentCatalogModel.model_json_schema(),
        "controlled-vocabularies-v1": ControlledVocabularyCatalogModel.model_json_schema(),
        "attack-enterprise-tactics-source-v1": AttackEnterpriseTacticsSourceModel.model_json_schema(),
        "atlas-tactics-source-v1": AtlasTacticsSourceModel.model_json_schema(),
        "semantic-profile-v1": SemanticProfileModel.model_json_schema(),
        "backend-profile-v1": _backend_profile_schema_for_bundle(),
        "experiment-apparatus-context-v1": ExperimentApparatusContextModel.model_json_schema(),
        "experiment-authoring-input-v1": ExperimentSpecModel.model_json_schema(),
        "experiment-capture-spec-v1": ExperimentCaptureSpecModel.model_json_schema(),
        "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_json_schema(),
        "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_json_schema(),
        "experiment-run-v1": ExperimentRunModel.model_json_schema(),
        "experiment-study-v1": ExperimentStudyModel.model_json_schema(),
        "experiment-task-v1": ExperimentTaskModel.model_json_schema(),
        "provisioning-plan-v1": ProvisioningPlanModel.model_json_schema(),
        "orchestration-plan-v1": OrchestrationPlanModel.model_json_schema(),
        "evaluation-plan-v1": EvaluationPlanModel.model_json_schema(),
        "runtime-snapshot-v1": RuntimeSnapshotEnvelopeModel.model_json_schema(),
        "workflow-result-envelope-v1": WorkflowExecutionStateModel.model_json_schema(),
        "workflow-history-event-stream-v1": _event_stream_schema(
            "WorkflowHistoryEventStream",
            WorkflowHistoryEventModel.model_json_schema(),
        ),
        "workflow-cancellation-request-v1": WorkflowCancellationRequestModel.model_json_schema(),
        "evaluation-result-envelope-v1": EvaluationResultStateModel.model_json_schema(),
        "proposition-truth-result-v1": PropositionTruthResultModel.model_json_schema(),
        "sdl-lineage-ledger-v1": SDLLineageLedgerModel.model_json_schema(),
        "scientific-completeness-taxonomy-v1": ScientificCompletenessTaxonomyModel.model_json_schema(),
        "scientific-completeness-assessment-v1": ScientificCompletenessAssessmentModel.model_json_schema(),
        "evaluation-history-event-stream-v1": _event_stream_schema(
            "EvaluationHistoryEventStream",
            EvaluationHistoryEventModel.model_json_schema(),
        ),
        "participant-episode-state-envelope-v1": ParticipantEpisodeStateModel.model_json_schema(),
        "participant-episode-history-event-stream-v1": _event_stream_schema(
            "ParticipantEpisodeHistoryEventStream",
            ParticipantEpisodeHistoryEventModel.model_json_schema(),
        ),
        "participant-behavior-history-event-stream-v1": _event_stream_schema(
            "ParticipantBehaviorHistoryEventStream",
            ParticipantBehaviorHistoryEventModel.model_json_schema(),
        ),
        "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_json_schema(),
        "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_json_schema(),
        "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_json_schema(),
        "participant-joint-action-record-v1": ParticipantJointActionRecordModel.model_json_schema(),
        "participant-time-management-context-v1": ParticipantTimeManagementContextModel.model_json_schema(),
        "participant-outcome-report-v1": ParticipantOutcomeReportModel.model_json_schema(),
        "participant-status-view-v1": ParticipantStatusViewModel.model_json_schema(),
        "participant-history-view-v1": ParticipantHistoryViewModel.model_json_schema(),
        "participant-context-view-v1": ParticipantContextViewModel.model_json_schema(),
        "operation-receipt-v1": OperationReceiptModel.model_json_schema(),
        "operation-status-v1": OperationStatusModel.model_json_schema(),
        "associated-artifact-manifest-v1": AssociatedArtifactManifestModel.model_json_schema(),
        "reusable-asset-trust-policy-v1": ReusableAssetTrustPolicyModel.model_json_schema(),
    }


def schema_bundle() -> dict[str, dict[str, Any]]:
    """Return the repo-published JSON Schemas for external contracts."""

    bundle = _raw_schema_bundle()
    _add_aces_invariant(
        bundle["behavioral-relations-v1"],
        "behavioral-relations-reference-resolution",
        "Relation map keys, bibliography references, claim-surface relation references, and worked-example keys "
        "must resolve exactly inside one taxonomy revision.",
        validator="aces_contracts.behavioral_relations.BehavioralRelationCatalogModel",
        inputs=[{"contract_id": "behavioral-relations-v1", "instance_path": "#"}],
    )
    _add_aces_invariant(
        bundle["experiment-study-v1"],
        "study-behavioral-claim-catalog-resolution",
        "Every study behavioral claim must resolve against the canonical taxonomy revision, include a required "
        "observation projection, and keep bounded evidence out of universal quantifiers.",
        validator="aces_contracts.behavioral_relations.validate_behavioral_claim_binding",
        inputs=[
            {"contract_id": "experiment-study-v1", "instance_path": "#/behavioral_claims"},
            {"contract_id": "behavioral-relations-v1", "instance_path": "#"},
        ],
    )
    _add_aces_invariant(
        bundle["scientific-completeness-taxonomy-v1"],
        "scientific-completeness-taxonomy-rectangular",
        "Concern and profile ids must be unique, and every profile disposition "
        "map must exactly cover the taxonomy concern set.",
        validator="aces_contracts.scientific_completeness.ScientificCompletenessTaxonomyModel",
        inputs=[{"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"}],
    )
    _add_aces_invariant(
        bundle["scientific-completeness-taxonomy-v1"],
        "scientific-completeness-behavioral-claim-resolution",
        "Each profile must carry resolved behavioral claim bindings and disjoint, catalog-resolved nonclaimed "
        "relation ids.",
        validator="aces_contracts.scientific_completeness.CompletenessProfileModel.validate_behavioral_claims",
        inputs=[
            {"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#/profiles"},
            {"contract_id": "behavioral-relations-v1", "instance_path": "#"},
        ],
    )
    _add_aces_invariant(
        bundle["scientific-completeness-assessment-v1"],
        "scientific-completeness-assessment-status-evidence",
        "Concern ids must be unique and each delivery status must carry its "
        "required executable evidence, external binding, issue refs, or "
        "exclusion rationale.",
        validator="aces_contracts.scientific_completeness.ScientificCompletenessAssessmentModel",
        inputs=[{"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"}],
    )
    _add_aces_invariant(
        bundle["scientific-completeness-assessment-v1"],
        "scientific-completeness-taxonomy-assessment-join",
        "Assessment family, taxonomy revision, and concern ids must exactly "
        "match the joined taxonomy before completeness is computed.",
        validator="aces_contracts.scientific_completeness.evaluate_profile_completeness",
        inputs=[
            {"contract_id": "scientific-completeness-taxonomy-v1", "instance_path": "#"},
            {"contract_id": "scientific-completeness-assessment-v1", "instance_path": "#"},
        ],
    )
    for contract_id, json_schema in bundle.items():
        _attach_sdl_identifier_constraints(contract_id, json_schema)
        _attach_instantiation_invariants(contract_id, json_schema)
        _attach_experiment_datetime_invariants(contract_id, json_schema)
        _attach_json_schema_metadata(contract_id, json_schema)
        _attach_compiled_address_map_constraints(contract_id, json_schema)
        _attach_plan_identity_constraints(contract_id, json_schema)
        _attach_aces_semantic_profile(contract_id, json_schema)
    known_contract_ids = frozenset(bundle)
    for contract_id, json_schema in bundle.items():
        _validate_aces_semantic_invariant_annotations(
            contract_id=contract_id,
            json_schema=json_schema,
            known_contract_ids=known_contract_ids,
        )
    return bundle


__all__ = [
    "AcesSemanticInvariantEntryModel",
    "AcesSemanticInvariantInputModel",
    "AcesSemanticInvariantProfileModel",
    "AcesSemanticInvariantProfileReferenceModel",
    "ATTACK_ENTERPRISE_TACTICS_SOURCE_SCHEMA_VERSION",
    "ATLAS_TACTICS_SOURCE_SCHEMA_VERSION",
    "AttackEnterpriseTacticSourceTermModel",
    "AttackEnterpriseTacticsSourceModel",
    "AtlasTacticSourceTermModel",
    "AtlasTacticsSourceModel",
    "ASSOCIATED_ARTIFACT_MANIFEST_SCHEMA_VERSION",
    "AssociatedArtifactManifestModel",
    "AssociatedArtifactParentReferenceModel",
    "AssociatedArtifactSetDigestString",
    "BACKEND_MANIFEST_V2_SCHEMA_VERSION",
    "ApparatusIdentityModel",
    "BackendCompatibilityModel",
    "BackendManifestV2Model",
    "BackendCapabilitiesV2Model",
    "BehavioralClaimBindingModel",
    "BehavioralRelationId",
    "BehavioralTaxonomyRevision",
    "CONCEPT_FAMILIES_SCHEMA_VERSION",
    "ConceptBindingEntryModel",
    "ConceptFamilyCatalogModel",
    "ConceptFamilyDefinitionModel",
    "ConceptFamilyId",
    "ConceptProvenanceCategory",
    "CONTROLLED_VOCABULARIES_SCHEMA_VERSION",
    "ControlledVocabularyCatalogModel",
    "ControlledVocabularyDefinitionModel",
    "ControlledVocabularySourceModel",
    "ControlledVocabularyTermId",
    "ControlledVocabularyTermModel",
    "ContractModel",
    "ExperimentAnalysisPlanModel",
    "ExperimentApparatusCompatibilityReferenceModel",
    "ExperimentApparatusComponentModel",
    "ExperimentApparatusConstraintModel",
    "ExperimentApparatusContextModel",
    "ExperimentArtifactRefModel",
    "ExperimentAugmentationDisclosureModel",
    "ExperimentBackendReferenceModel",
    "ExperimentCaptureRequirementModel",
    "ExperimentCaptureSpecModel",
    "ExperimentCaptureSpecReferenceModel",
    "ExperimentCaptureWindowModel",
    "ExperimentChecksumModel",
    "ExperimentClockContextModel",
    "ExperimentConditionAssignmentParameterModel",
    "ExperimentConditionAssignmentReferenceModel",
    "ExperimentDerivedMeasureMethodModel",
    "ExperimentDerivedMeasureModel",
    "ExperimentDerivedMeasureReferenceModel",
    "ExperimentEpisodeControlModel",
    "ExperimentEvidenceRecordModel",
    "ExperimentEvidenceRecordReferenceModel",
    "ExperimentEvidenceReferenceModel",
    "ExperimentEvaluationProtocolModel",
    "ExperimentInvalidationModel",
    "ExperimentManifestReferenceModel",
    "ExperimentMeasurementChannelReferenceModel",
    "ExperimentMetricDefinitionModel",
    "ExperimentMissingDataPolicyModel",
    "ExperimentMultipleComparisonPolicyModel",
    "ExperimentParameterModel",
    "ExperimentProcessorReferenceModel",
    "ExperimentRealizedFormDisclosureModel",
    "ExperimentRedVariantSelectionModel",
    "ExperimentReferenceModel",
    "ExperimentResultSummaryModel",
    "ExperimentRunAllocationPlanModel",
    "ExperimentRunModel",
    "ExperimentRunPlanModel",
    "ExperimentRunTraceabilityModel",
    "ExperimentScenarioReferenceModel",
    "ExperimentScenarioSnapshotReferenceModel",
    "ExperimentSpecModel",
    "ExperimentSplitAndLeakageControlsModel",
    "ExperimentStatisticalMethodModel",
    "ExperimentStochasticControlModel",
    "ExperimentStudyFactorModel",
    "ExperimentStudyMembershipModel",
    "ExperimentStudyModel",
    "ExperimentTaskReferenceModel",
    "ExperimentTaskModel",
    "ExperimentUncertaintyMethodModel",
    "ExperimentValidityNoteModel",
    "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION",
    "EXPERIMENT_AUTHORING_INPUT_SCHEMA_VERSION",
    "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION",
    "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION",
    "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION",
    "EXPERIMENT_RUN_SCHEMA_VERSION",
    "EXPERIMENT_STUDY_SCHEMA_VERSION",
    "EXPERIMENT_TASK_SCHEMA_VERSION",
    "EvaluationHistoryEventModel",
    "EvaluationPlanModel",
    "EvaluationResultStateModel",
    "PropositionAssertionPolarity",
    "PropositionEvaluationBasis",
    "PropositionIndeterminacyReason",
    "PropositionLossDisclosureModel",
    "PropositionLossKind",
    "PropositionProbeBindingModel",
    "PropositionTemporalContextModel",
    "PropositionTruthOutcome",
    "PropositionTruthResultModel",
    "EVALUATION_STATE_SCHEMA_VERSION",
    "EvaluatorCapabilitiesModel",
    "EventClassificationModel",
    "InstantiationRequestModel",
    "OPERATION_SCHEMA_VERSION",
    "OperationReceiptModel",
    "OperationStatusModel",
    "ObservationCapabilitiesModel",
    "OrchestrationPlanModel",
    "OrchestratorCapabilitiesModel",
    "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION",
    "PARTICIPANT_IMPLEMENTATION_MANIFEST_V1_SCHEMA_VERSION",
    "PARTICIPANT_IMPLEMENTATION_PROVENANCE_V1_SCHEMA_VERSION",
    "ParticipantActionEffectResultModel",
    "ParticipantActionPreconditionResultModel",
    "ParticipantActionResultModel",
    "ParticipantAttributionCandidateModel",
    "ParticipantAttributionEdgeModel",
    "ParticipantAttributionEvidenceBasisModel",
    "ParticipantAttributionOrderingBasisModel",
    "ParticipantBehaviorHistoryEventModel",
    "ParticipantContextViewModel",
    "ParticipantEpisodeHistoryEventModel",
    "ParticipantEpisodeStateModel",
    "ParticipantExposurePolicyModel",
    "ParticipantFeatureSupportLevel",
    "ParticipantFeatureSupportModel",
    "ParticipantHistoryViewBehaviorEventModel",
    "ParticipantHistoryViewEpisodeEventModel",
    "ParticipantHistoryViewModel",
    "ParticipantImplementationCapabilitiesModel",
    "ParticipantImplementationCompatibilityModel",
    "ParticipantImplementationManifestModel",
    "ParticipantImplementationProvenanceModel",
    "ParticipantImplementationSelectionModel",
    "ParticipantJointActionAccessSetModel",
    "ParticipantJointActionRecordModel",
    "ParticipantLifecycleEventModel",
    "ParticipantObservationEnvelopeModel",
    "ParticipantObservationLossDescriptorModel",
    "ParticipantObservationStochasticContextModel",
    "ParticipantOutcomeInterpretationRecordModel",
    "ParticipantOutcomeReportModel",
    "ParticipantOutcomeReportSourceModel",
    "ParticipantOutcomeReportStateRelationshipModel",
    "ParticipantOutcomeSourceRecordModel",
    "ParticipantOutcomeTargetRecordModel",
    "ParticipantRuntimeBaseEnvelopeModel",
    "ParticipantRuntimeCapabilitiesModel",
    "ParticipantSharedStateAccessModel",
    "ParticipantSharedStateRecordModel",
    "ParticipantStatusViewEpisodeStateModel",
    "ParticipantStatusViewModel",
    "ParticipantTemporalRuntimeContextModel",
    "ParticipantTimeManagementContextModel",
    "VIEW_SCOPE_PROJECTED_FIELDS",
    "PlanOperationModel",
    "ProcessorFeature",
    "PROCESSOR_MANIFEST_V2_SCHEMA_VERSION",
    "ProcessorManifestV2Model",
    "ProcessorCompatibilityModel",
    "ProcessorCapabilitiesV2Model",
    "ProvisionerCapabilitiesModel",
    "ProvisioningPlanModel",
    "RealizationEnvelopeIdentityModel",
    "RawDataIntegrityModel",
    "RealizationProvenanceEntryModel",
    "RealizationSupportDeclarationModel",
    "RealizationSupportMode",
    "ReferenceModelCatalogModel",
    "ReferenceModelDefinitionModel",
    "ReferenceModelSchemaBindingModel",
    "REFERENCE_MODELS_SCHEMA_VERSION",
    "REUSABLE_ASSET_EVIDENCE_CLASSES",
    "REUSABLE_ASSET_FAMILIES",
    "REUSABLE_ASSET_TRUST_POLICY_SCHEMA_VERSION",
    "RUNTIME_SNAPSHOT_SCHEMA_VERSION",
    "ReusableAssetAuthenticityPolicyModel",
    "ReusableAssetEvidenceRequirementModel",
    "ReusableAssetFamilyTrustPolicyModel",
    "ReusableAssetTrustPolicyModel",
    "RuntimeSnapshotEnvelopeModel",
    "SCENARIO_INSTANTIATION_REQUEST_SCHEMA_VERSION",
    "SEMANTIC_PROFILE_SCHEMA_VERSION",
    "schema_bundle",
    "SemanticBehaviorAssumptionModel",
    "SemanticProfileModel",
    "SemanticProfilePhaseModel",
    "SnapshotEntryModel",
    "SourcePipelineModel",
    "SourceStatusModel",
    "UcoAlignmentCatalogModel",
    "UcoAlignmentTypeModel",
    "UcoFamilyAlignmentModel",
    "UCO_ALIGNMENT_SCHEMA_VERSION",
    "WorkflowCancellationRequestModel",
    "WORKFLOW_CANCELLATION_REQUEST_SCHEMA_VERSION",
    "WorkflowExecutionStateModel",
    "WorkflowFeature",
    "WorkflowHistoryEventModel",
    "WorkflowStatePredicateFeature",
    "WorkflowStepStateModel",
    "WORKFLOW_STATE_SCHEMA_VERSION",
    "validate_aces_semantic_invariant_annotations",
    "validate_experiment_apparatus_context_against_manifests",
    "validate_experiment_apparatus_context_archival_datetimes",
    "validate_experiment_run_against_task",
    "validate_experiment_run_archival_datetimes",
    "validate_experiment_study_against_tasks_and_runs",
    "validate_experiment_study_archival_datetimes",
    "validate_experiment_task_archival_datetimes",
]
