//! Typed pai-axiom <-> Cortex trust exchange envelopes (ADR 0042 / 0043).
//!
//! These structs are the receiver-side shape for the three pai-axiom
//! schemas that drive Cortex's field-level admission gate:
//!
//! - `cortex_context_trust` (Cortex -> pai-axiom, mirrored back through the
//!   boundary for consumption checks).
//! - `axiom_execution_trust` (pai-axiom -> Cortex execution receipt).
//! - `authority_feedback_loop` (cross-system loop record).
//!
//! The module is shape-only: it deserializes the envelope, runs per-field
//! validators that emit *stable invariant names*, and produces a typed
//! report. It does not persist state, does not grant authority, and does
//! not run the admission gate itself.
//!
//! Every struct uses `#[serde(deny_unknown_fields)]` so that schema drift
//! at the producer side fails closed at the parse boundary. Stable
//! invariant names follow `<schema_name>.<field_path>.<failure_class>`.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Stable schema string for [`CortexContextTrust`] envelopes.
pub const CORTEX_CONTEXT_TRUST_SCHEMA: &str = "cortex_context_trust";

/// Stable schema string for [`AxiomExecutionTrust`] envelopes.
pub const AXIOM_EXECUTION_TRUST_SCHEMA: &str = "axiom_execution_trust";

/// Stable schema string for [`AuthorityFeedbackLoop`] envelopes.
pub const AUTHORITY_FEEDBACK_LOOP_SCHEMA: &str = "authority_feedback_loop";

/// Stable schema version supported on the Cortex receiver side.
pub const TRUST_EXCHANGE_SCHEMA_VERSION: u16 = 1;

// ---------------------------------------------------------------------------
// Cortex context trust envelope
// ---------------------------------------------------------------------------

/// Cortex context trust envelope (`CORTEX_CONTEXT_TRUST.schema.json` v1).
///
/// `compatibility_trust_label` is intentionally kept on the struct but
/// classified as display-only by [`Self::validate`]: per
/// `CORTEX_AXIOM_TRUST_EXCHANGE_COMPATIBILITY.json` it MUST NOT satisfy
/// any behavior-changing gate. Cortex consumes the decomposed fields.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CortexContextTrust {
    /// Schema discriminator. Must equal [`CORTEX_CONTEXT_TRUST_SCHEMA`].
    pub schema: String,
    /// Schema version. Must equal [`TRUST_EXCHANGE_SCHEMA_VERSION`].
    pub version: u16,
    /// Stable receiver-side reference id (optional in pai-axiom's wire,
    /// supplied by the boundary tool when present).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cortex_context_trust_ref: Option<String>,
    /// Stable context identifier.
    pub context_id: String,
    /// Coarse compatibility label kept for back-compat — display only.
    pub compatibility_trust_label: CompatibilityTrustLabel,
    /// Proof closure state and failing edges.
    pub proof_state: ContextProofState,
    /// Truth ceiling permitted to the receiving consumer.
    pub truth_ceiling: TruthCeiling,
    /// Semantic trust decomposition.
    pub semantic_trust: ContextSemanticTrust,
    /// Provenance references (optional in some fixtures).
    #[serde(default)]
    pub provenance_refs: Vec<String>,
    /// Contradiction posture for the context.
    #[serde(default = "ContradictionState::default_unknown")]
    pub contradiction_state: ContradictionState,
    /// Promotion posture for the context.
    #[serde(default = "PromotionState::default_candidate")]
    pub promotion_state: PromotionState,
    /// Quarantine posture for the context.
    pub quarantine_state: ContextQuarantineState,
    /// Redaction posture and references.
    pub redaction_state: ContextRedactionState,
    /// Confidence value and scale (optional in some fixtures).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidence: Option<ContextConfidence>,
    /// Policy decision record.
    pub policy_result: ContextPolicyResult,
    /// Allowed claim language vocabulary.
    #[serde(default)]
    pub allowed_claim_language: Vec<ContextAllowedClaimLanguage>,
    /// Forbidden authority-bearing uses.
    #[serde(default)]
    pub forbidden_uses: Vec<ContextForbiddenUse>,
    /// Allowed use vocabulary.
    #[serde(default)]
    pub allowed_use: Vec<ContextAllowedUse>,
    /// Evidence references backing the context.
    #[serde(default)]
    pub evidence_refs: Vec<String>,
    /// Source anchors backing the context.
    pub source_anchors: Vec<ContextSourceAnchor>,
    /// Residual risk strings copied from the producer.
    #[serde(default)]
    pub residual_risk: Vec<String>,
}

/// Coarse compatibility label vocabulary (display only).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompatibilityTrustLabel {
    /// Compatibility alias only.
    Untrusted,
    /// Compatibility alias only.
    Advisory,
    /// Compatibility alias only.
    Observed,
    /// Compatibility alias only.
    Validated,
    /// Compatibility alias only.
    AuthorityClaimed,
}

/// Proof closure state inside a Cortex context envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextProofState {
    /// Proof closure state value.
    pub state: ContextProofStateValue,
    /// Failing edges names. Must be present (even if empty).
    pub failing_edges: Vec<String>,
    /// Proof references for the closure.
    pub proof_refs: Vec<String>,
}

/// Proof closure state vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextProofStateValue {
    /// No proof was supplied.
    Missing,
    /// Proof is partial.
    Partial,
    /// Proof is closed.
    Closed,
    /// Proof failed.
    Failed,
}

/// Truth ceiling vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TruthCeiling {
    /// No truth claim permitted.
    None,
    /// Advisory only.
    Advisory,
    /// Observed only.
    Observed,
    /// Validated.
    Validated,
    /// Already promoted by Cortex (display only at the receiver).
    PromotedByCortex,
}

impl Serialize for TruthCeiling {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.wire_string())
    }
}

impl TruthCeiling {
    /// Stable wire string mirroring the JSON Schema enum.
    #[must_use]
    pub const fn wire_string(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Advisory => "advisory",
            Self::Observed => "observed",
            Self::Validated => "validated",
            Self::PromotedByCortex => "promoted-by-cortex",
        }
    }
}

/// Decomposed semantic trust block.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextSemanticTrust {
    /// Provenance class for the context.
    pub provenance_class: ContextProvenanceClass,
    /// Trust weight in [0, 1].
    pub trust_weight: f64,
    /// Free-form weighting basis explanation.
    pub weighting_basis: String,
}

/// Provenance class vocabulary inside context trust.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextProvenanceClass {
    /// Unknown provenance.
    Unknown,
    /// Claimed but unverified.
    Claimed,
    /// Observed.
    Observed,
    /// Derived from named premises.
    Derived,
    /// Curated.
    Curated,
    /// Already promoted (display only).
    Promoted,
}

/// Contradiction state vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContradictionState {
    /// No contradiction known.
    NoneKnown,
    /// Contradictions remain unresolved.
    Unresolved,
    /// Contradictions were resolved.
    Resolved,
    /// Contradiction posture unknown.
    Unknown,
}

impl ContradictionState {
    fn default_unknown() -> Self {
        Self::Unknown
    }
}

/// Promotion state vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PromotionState {
    /// Candidate only.
    Candidate,
    /// Observed.
    Observed,
    /// Validated.
    Validated,
    /// Already promoted (display only).
    Promoted,
    /// Stale.
    Stale,
    /// Quarantined.
    Quarantined,
}

impl PromotionState {
    fn default_candidate() -> Self {
        Self::Candidate
    }
}

/// Quarantine state vocabulary for context envelopes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextQuarantineState {
    /// Clear of quarantine.
    Clear,
    /// Quarantined.
    Quarantined,
    /// Derived from a quarantined ancestor.
    DerivedFromQuarantined,
    /// Posture unknown.
    Unknown,
}

impl ContextQuarantineState {
    /// Whether this state propagates quarantine into Cortex.
    #[must_use]
    pub const fn propagates_quarantine(self) -> bool {
        matches!(
            self,
            Self::Quarantined | Self::DerivedFromQuarantined | Self::Unknown
        )
    }
}

/// Redaction state block on a context envelope.
///
/// The `blocks_critical_premise` field is intentionally accepted because
/// some pai-axiom fixtures emit it as a redaction-policy extension. It is
/// not authoritative on the Cortex receiver — Cortex consumes
/// [`Self::status`] and [`Self::redaction_refs`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextRedactionState {
    /// Redaction status value.
    pub status: ContextRedactionStatus,
    /// Redaction references.
    pub redaction_refs: Vec<String>,
    /// Optional extension marker (some pai-axiom fixtures emit this).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub blocks_critical_premise: Option<bool>,
}

/// Redaction status vocabulary on context envelopes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextRedactionStatus {
    /// No redaction.
    None,
    /// Redacted.
    Redacted,
    /// Partially redacted.
    PartiallyRedacted,
    /// Redaction posture unknown.
    Unknown,
}

/// Confidence block on context envelopes.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextConfidence {
    /// Confidence value (numeric or discrete string).
    pub value: ContextConfidenceValue,
    /// Confidence scale.
    pub scale: ContextConfidenceScale,
}

/// Confidence value — either numeric (0..=1) or a discrete word.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ContextConfidenceValue {
    /// Numeric confidence in [0, 1].
    Numeric(f64),
    /// Discrete label such as "low", "medium", "high".
    Discrete(String),
}

/// Confidence scale vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextConfidenceScale {
    /// Discrete label set.
    Discrete,
    /// Numeric scale.
    Numeric,
}

/// Policy result block on a context envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextPolicyResult {
    /// Stable decision identifier.
    pub decision_id: String,
    /// Policy result value.
    pub result: ContextPolicyResultValue,
}

/// Policy result vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextPolicyResultValue {
    /// Policy denied.
    Deny,
    /// Policy requires review.
    ReviewRequired,
    /// Policy partial.
    Partial,
    /// Policy allowed.
    Allow,
}

/// Allowed claim language vocabulary on a context envelope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextAllowedClaimLanguage {
    /// Candidate claims permitted.
    Candidate,
    /// Observed claims permitted.
    Observed,
    /// Validated claims permitted.
    Validated,
}

/// Forbidden authority-bearing uses on a context envelope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextForbiddenUse {
    /// Execution authority forbidden.
    ExecutionPermission,
    /// Durable truth promotion forbidden.
    DurableTruthPromotion,
    /// Release claim forbidden.
    ReleaseClaim,
}

/// Allowed use vocabulary on a context envelope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextAllowedUse {
    /// Render only.
    RenderOnly,
    /// Advisory reasoning.
    AdvisoryReasoning,
    /// Planning input.
    PlanningInput,
}

/// Source anchor on a context envelope.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContextSourceAnchor {
    /// Stable source identifier.
    pub source_id: String,
    /// Source type vocabulary.
    pub source_type: ContextSourceAnchorType,
    /// Stable source reference.
    pub r#ref: String,
    /// `sha256:<hex>` hash.
    pub hash: String,
}

/// Source anchor type vocabulary on context envelopes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextSourceAnchorType {
    /// Cortex event.
    Event,
    /// Memory.
    Memory,
    /// Principle.
    Principle,
    /// Doctrine.
    Doctrine,
    /// Ledger entry.
    Ledger,
    /// Context pack.
    ContextPack,
    /// External anchor.
    External,
}

// ---------------------------------------------------------------------------
// pai-axiom execution trust envelope
// ---------------------------------------------------------------------------

/// pai-axiom execution trust envelope (`AXIOM_EXECUTION_TRUST.schema.json` v1).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AxiomExecutionTrust {
    /// Schema discriminator. Must equal [`AXIOM_EXECUTION_TRUST_SCHEMA`].
    pub schema: String,
    /// Schema version. Must equal [`TRUST_EXCHANGE_SCHEMA_VERSION`].
    pub version: u16,
    /// Optional stable reference (not in the raw schema, supplied by some
    /// boundary tools).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub axiom_execution_trust_ref: Option<String>,
    /// Stable action identifier.
    pub action_id: String,
    /// Execution trust level.
    pub execution_trust_level: ExecutionTrustLevel,
    /// Repo trust block.
    pub repo_trust: RepoTrust,
    /// Actor attestation block.
    pub actor_attestation: ActorAttestation,
    /// Policy decision block.
    pub policy_decision: ExecutionPolicyDecision,
    /// Token scope block.
    pub token_scope: TokenScope,
    /// Tool provenance block.
    pub tool_provenance: ExecutionToolProvenance,
    /// Source anchors backing the execution receipt.
    pub source_anchors: Vec<ExecutionSourceAnchor>,
    /// Runtime mode label.
    pub runtime_mode: String,
    /// Evidence references.
    #[serde(default)]
    pub evidence_refs: Vec<String>,
    /// Residual risk strings.
    #[serde(default)]
    pub residual_risk: Vec<String>,
}

/// Execution trust level vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionTrustLevel {
    /// Developer-only.
    Dev,
    /// Locally unsigned.
    LocalUnsigned,
    /// Signed locally.
    SignedLocal,
    /// Anchored externally.
    ExternallyAnchored,
    /// Authority-grade.
    AuthorityGrade,
}

/// Repo trust block.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RepoTrust {
    /// Repo trust result.
    pub result: RepoTrustResult,
    /// Stable evaluation reference.
    pub evaluation_ref: String,
}

/// Repo trust result vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepoTrustResult {
    /// Repo trusted.
    Trusted,
    /// Repo trust partial.
    Partial,
    /// Repo untrusted.
    Untrusted,
}

/// Actor attestation block.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActorAttestation {
    /// Stable actor identity reference.
    pub identity_ref: String,
    /// Stable attestation reference.
    pub attestation_ref: String,
    /// Operator approval reference.
    pub operator_approval_ref: String,
    /// `sha256:<hex>` operator approval hash.
    pub operator_approval_hash: String,
}

/// Policy decision block on execution receipts.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionPolicyDecision {
    /// Stable decision identifier.
    pub decision_id: String,
    /// Policy decision value.
    pub result: ExecutionPolicyResult,
}

/// Policy decision result vocabulary on execution receipts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionPolicyResult {
    /// Policy denied.
    Deny,
    /// Policy review required.
    ReviewRequired,
    /// Policy partial.
    Partial,
    /// Policy allowed.
    Allow,
}

/// Token scope block.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TokenScope {
    /// Stable token identifier.
    pub token_id: String,
    /// Token audience.
    pub audience: String,
    /// `sha256:<hex>` scope hash.
    pub scope_hash: String,
    /// `sha256:<hex>` operation hash.
    pub operation_hash: String,
    /// `sha256:<hex>` manifest hash.
    pub manifest_hash: String,
    /// `sha256:<hex>` request hash.
    pub request_hash: String,
    /// RFC 3339 token expiry.
    pub expires_at: DateTime<Utc>,
    /// Token revocation result.
    pub revocation_result: TokenRevocationResult,
}

/// Token revocation result vocabulary.
///
/// `Inactive` is accepted as an extension state observed in some pai-axiom
/// fixtures (`inactive-token-axiom-execution-trust.json`); it is treated as
/// equivalent to `Revoked` for admission gating.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenRevocationResult {
    /// Token active.
    Active,
    /// Token revoked.
    Revoked,
    /// Token inactive (fixture extension).
    Inactive,
    /// Token state unknown.
    Unknown,
}

impl TokenRevocationResult {
    /// Whether this revocation state must reject admission.
    #[must_use]
    pub const fn must_reject(self) -> bool {
        matches!(self, Self::Revoked | Self::Inactive)
    }
}

/// Tool provenance block on execution receipts.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionToolProvenance {
    /// Stable tool identifier.
    pub tool_id: String,
    /// Tool version string.
    pub tool_version: String,
    /// Command reference.
    pub command_ref: String,
    /// 40-hex source commit.
    pub source_commit: String,
    /// Dependency lock reference.
    pub dependency_lock_ref: String,
}

/// Source anchor on an execution receipt.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionSourceAnchor {
    /// Stable source identifier.
    pub source_id: String,
    /// Source type vocabulary on execution receipts.
    pub source_type: ExecutionSourceAnchorType,
    /// Stable source reference.
    pub r#ref: String,
    /// `sha256:<hex>` hash.
    pub hash: String,
}

/// Source anchor type vocabulary on execution receipts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionSourceAnchorType {
    /// Command anchor.
    Command,
    /// File anchor.
    File,
    /// Test anchor.
    Test,
    /// Ledger anchor.
    Ledger,
    /// Runtime anchor.
    Runtime,
    /// Operator approval anchor.
    Approval,
}

// ---------------------------------------------------------------------------
// Authority feedback loop envelope
// ---------------------------------------------------------------------------

/// Authority feedback loop record (`AUTHORITY_FEEDBACK_LOOP.schema.json` v1).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthorityFeedbackLoop {
    /// Schema discriminator.
    pub schema: String,
    /// Schema version.
    pub version: u16,
    /// Optional stable receiver-side reference.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub authority_feedback_loop_ref: Option<String>,
    /// Stable loop identifier.
    pub loop_id: String,
    /// Loop start timestamp.
    pub started_at: DateTime<Utc>,
    /// Initiating context block.
    pub initiating_context: FeedbackInitiatingContext,
    /// AXIOM action block.
    pub axiom_action: FeedbackAxiomAction,
    /// Returned artifact descriptors.
    pub returned_artifacts: Vec<FeedbackReturnedArtifact>,
    /// Amplification risk level.
    pub amplification_risk: AmplificationRisk,
    /// Independent evidence references.
    #[serde(default)]
    pub independent_evidence_refs: Vec<String>,
    /// External grounding references.
    #[serde(default)]
    pub external_grounding_refs: Vec<String>,
    /// Contradiction scan reference.
    pub contradiction_scan_ref: String,
    /// Loop quarantine state.
    pub quarantine_state: ContextQuarantineState,
    /// Confidence ceiling permitted on loop output.
    pub confidence_ceiling: ConfidenceCeiling,
    /// Same-loop promotion permission. Must be `false`.
    pub same_loop_promotion_allowed: bool,
    /// Authority claims block (optional only in the strictest pai-axiom
    /// negative fixtures; required in valid emission).
    pub authority_claims: FeedbackAuthorityClaims,
    /// Target-domain validation block.
    pub target_domain_validation: TargetDomainValidation,
    /// Residual risk strings.
    #[serde(default)]
    pub residual_risk: Vec<String>,
}

/// Initiating-context block on a feedback loop record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeedbackInitiatingContext {
    /// Stable initiating context identifier.
    pub context_id: String,
    /// Reference to the matching Cortex context trust envelope.
    pub cortex_context_trust_ref: String,
}

/// AXIOM action block on a feedback loop record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeedbackAxiomAction {
    /// Stable action identifier.
    pub action_id: String,
    /// Reference to the matching pai-axiom execution trust envelope.
    pub axiom_execution_trust_ref: String,
}

/// Returned-artifact descriptor on a feedback loop record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeedbackReturnedArtifact {
    /// Stable artifact identifier.
    pub artifact_id: String,
    /// Lineage reference.
    pub lineage_ref: String,
    /// Lifecycle state of the artifact.
    pub lifecycle_state: ArtifactLifecycleState,
    /// Reproducibility level of the artifact.
    pub reproducibility_level: ReproducibilityLevel,
}

/// Lifecycle state vocabulary for returned artifacts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactLifecycleState {
    /// Candidate only.
    Candidate,
    /// Observed.
    Observed,
    /// Validated.
    Validated,
    /// Promoted.
    Promoted,
    /// Stale.
    Stale,
    /// Quarantined.
    Quarantined,
}

/// Reproducibility level vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReproducibilityLevel {
    /// Deterministic.
    Deterministic,
    /// Bounded nondeterministic.
    BoundedNondeterministic,
    /// Observational.
    Observational,
    /// Non-reproducible.
    NonReproducible,
}

impl Serialize for ReproducibilityLevel {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.wire_string())
    }
}

impl ReproducibilityLevel {
    /// Stable wire string.
    #[must_use]
    pub const fn wire_string(self) -> &'static str {
        match self {
            Self::Deterministic => "deterministic",
            Self::BoundedNondeterministic => "bounded-nondeterministic",
            Self::Observational => "observational",
            Self::NonReproducible => "non-reproducible",
        }
    }
}

/// Amplification risk vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmplificationRisk {
    /// Low.
    Low,
    /// Medium.
    Medium,
    /// High.
    High,
    /// Critical.
    Critical,
}

/// Confidence ceiling vocabulary on the feedback loop record.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfidenceCeiling {
    /// Untrusted.
    Untrusted,
    /// Advisory.
    Advisory,
    /// Observed.
    Observed,
    /// Validated.
    Validated,
}

/// Authority claims block.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeedbackAuthorityClaims {
    /// Durable truth promotion claim status.
    pub durable_truth_promotion: AuthorityClaimStatus,
    /// Full execution authority claim status.
    pub full_execution_authority: AuthorityClaimStatus,
    /// Whether review is required.
    pub review_required: bool,
}

/// Authority claim status vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityClaimStatus {
    /// Denied.
    Denied,
    /// Review required.
    ReviewRequired,
    /// Eligible after independent validation.
    EligibleAfterIndependentValidation,
}

impl AuthorityClaimStatus {
    /// Whether the producer claimed authority-grade promotion. Cortex
    /// rejects this regardless of any other contributor.
    #[must_use]
    pub const fn claims_authority(self) -> bool {
        matches!(self, Self::EligibleAfterIndependentValidation)
    }
}

/// Target-domain validation block.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TargetDomainValidation {
    /// Whether validation is required (must be `true`).
    pub required: bool,
    /// Reference to the independent validation, if produced.
    pub independent_validation_ref: Option<String>,
    /// Validation result.
    pub result: TargetDomainValidationResult,
}

/// Target-domain validation result vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TargetDomainValidationResult {
    /// Pending validation.
    Pending,
    /// Validation passed.
    Pass,
    /// Validation failed.
    Fail,
    /// Validation partial.
    Partial,
}

// ---------------------------------------------------------------------------
// Named quarantine outputs (ADR 0042 §7)
// ---------------------------------------------------------------------------

/// Per-source quarantine output as required by ADR 0042 §7.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QuarantineOutput {
    /// Stable invariant name explaining why this output is quarantined.
    pub invariant: String,
    /// Operator-facing reason.
    pub reason: String,
    /// Optional reference back to the source artifact.
    pub source_ref: Option<String>,
}

impl QuarantineOutput {
    /// Construct a quarantine output.
    #[must_use]
    pub fn new(invariant: impl Into<String>, reason: impl Into<String>) -> Self {
        Self {
            invariant: invariant.into(),
            reason: reason.into(),
            source_ref: None,
        }
    }

    /// Attach a stable source reference.
    #[must_use]
    pub fn with_source_ref(mut self, source_ref: impl Into<String>) -> Self {
        self.source_ref = Some(source_ref.into());
        self
    }
}

/// Named per-source quarantine outputs, mirroring ADR 0042 §7.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NamedQuarantineOutputs {
    /// Source-context quarantine (Cortex side).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_context: Option<QuarantineOutput>,
    /// Token revocation quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub token_revocation: Option<QuarantineOutput>,
    /// Repo trust quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repo_trust: Option<QuarantineOutput>,
    /// Policy denial quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_denial: Option<QuarantineOutput>,
    /// Target-domain validation quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_validation: Option<QuarantineOutput>,
    /// Derived artifact quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub derived_artifact: Option<QuarantineOutput>,
    /// Contradiction-state quarantine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub contradiction: Option<QuarantineOutput>,
}

impl NamedQuarantineOutputs {
    /// Whether any of the seven named outputs is populated.
    #[must_use]
    pub fn any_present(&self) -> bool {
        self.source_context.is_some()
            || self.token_revocation.is_some()
            || self.repo_trust.is_some()
            || self.policy_denial.is_some()
            || self.target_validation.is_some()
            || self.derived_artifact.is_some()
            || self.contradiction.is_some()
    }

    /// Iterate over every populated stable invariant name.
    pub fn invariants(&self) -> impl Iterator<Item = &str> {
        [
            self.source_context.as_ref(),
            self.token_revocation.as_ref(),
            self.repo_trust.as_ref(),
            self.policy_denial.as_ref(),
            self.target_validation.as_ref(),
            self.derived_artifact.as_ref(),
            self.contradiction.as_ref(),
        ]
        .into_iter()
        .flatten()
        .map(|q| q.invariant.as_str())
    }
}

// ---------------------------------------------------------------------------
// Field validation errors (stable invariant names)
// ---------------------------------------------------------------------------

/// Stable field-level invariant failure.
///
/// `invariant` is the dot-pathed stable name (e.g.
/// `axiom_execution_trust.token_scope.audience.missing`) consumed by the
/// admission gate and by external pai-axiom acceptance tests.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TrustExchangeFieldError {
    /// Stable dot-pathed invariant name.
    pub invariant: String,
    /// Operator-facing reason.
    pub reason: String,
}

impl TrustExchangeFieldError {
    /// Construct a field-level error.
    #[must_use]
    pub fn new(invariant: impl Into<String>, reason: impl Into<String>) -> Self {
        Self {
            invariant: invariant.into(),
            reason: reason.into(),
        }
    }
}

/// Result type for field-level validation across the trust exchange envelopes.
pub type TrustExchangeValidation = Result<(), Vec<TrustExchangeFieldError>>;

// ---------------------------------------------------------------------------
// Wrappers — pai-axiom fixtures wrap the inner envelope in a single key
// ---------------------------------------------------------------------------

/// Envelope shape matching pai-axiom's wrapped fixtures
/// (`{ "cortex_context_trust": { ... } }`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CortexContextTrustEnvelope {
    /// Wrapped cortex context trust object.
    pub cortex_context_trust: CortexContextTrust,
}

/// Envelope shape matching pai-axiom's wrapped fixtures
/// (`{ "axiom_execution_trust": { ... } }`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AxiomExecutionTrustEnvelope {
    /// Wrapped axiom execution trust object.
    pub axiom_execution_trust: AxiomExecutionTrust,
}

// ---------------------------------------------------------------------------
// Parse helpers — accept wrapped or unwrapped form
// ---------------------------------------------------------------------------

/// Parse a Cortex context trust envelope from either the wrapped pai-axiom
/// fixture shape `{ "cortex_context_trust": { ... } }` or the unwrapped
/// schema-bare object form.
pub fn parse_cortex_context_trust(
    input: &str,
) -> Result<CortexContextTrust, TrustExchangeFieldError> {
    let value: serde_json::Value = serde_json::from_str(input).map_err(|err| {
        TrustExchangeFieldError::new(
            "cortex_context_trust.envelope.invalid_json",
            format!("invalid JSON: {err}"),
        )
    })?;
    let inner = match value.get("cortex_context_trust") {
        Some(inner) => inner.clone(),
        None => value,
    };
    serde_json::from_value(inner).map_err(|err| {
        TrustExchangeFieldError::new(
            "cortex_context_trust.envelope.schema_drift",
            format!("envelope failed schema check: {err}"),
        )
    })
}

/// Parse a pai-axiom execution trust envelope from either the wrapped form
/// `{ "axiom_execution_trust": { ... } }` or the bare object form.
pub fn parse_axiom_execution_trust(
    input: &str,
) -> Result<AxiomExecutionTrust, TrustExchangeFieldError> {
    let value: serde_json::Value = serde_json::from_str(input).map_err(|err| {
        TrustExchangeFieldError::new(
            "axiom_execution_trust.envelope.invalid_json",
            format!("invalid JSON: {err}"),
        )
    })?;
    let inner = match value.get("axiom_execution_trust") {
        Some(inner) => inner.clone(),
        None => value,
    };
    serde_json::from_value(inner).map_err(|err| {
        TrustExchangeFieldError::new(
            "axiom_execution_trust.envelope.schema_drift",
            format!("envelope failed schema check: {err}"),
        )
    })
}

/// Parse an authority feedback loop record. The feedback loop fixture is
/// not wrapped in pai-axiom's emission, so we accept the bare object only.
pub fn parse_authority_feedback_loop(
    input: &str,
) -> Result<AuthorityFeedbackLoop, TrustExchangeFieldError> {
    serde_json::from_str(input).map_err(|err| {
        TrustExchangeFieldError::new(
            "authority_feedback_loop.envelope.schema_drift",
            format!("envelope failed schema check: {err}"),
        )
    })
}

// ---------------------------------------------------------------------------
// Receiver-side axiom source-commit freshness gate
// ---------------------------------------------------------------------------
//
// The first live Cortex ↔ axiom admission exchange against axiom packet SHA
// `9a15d281` (record at `docs/transcripts/AXIOM_LIVE_EXCHANGE_2026-05-13/
// SUMMARY.md`) surfaced one real receiver-side gap: the
// `stale-pai-axiom-sha` fixture was admitted by Cortex despite the packet's
// `axiom_execution_trust.tool_provenance.source_commit` pointing at a stale
// axiom SHA. The axiom team's fixture expected `freshness_refusal` keyed on
// `axiom_execution_trust.tool_provenance.source_commit.stale`; Cortex
// returned `decision=admit_candidate`, exit 0.
//
// This module section is the receiver-side closure: a small, additive
// freshness gate Cortex callers can run alongside the existing structural
// validation. The accept set is owned at the Cortex side (ADR 0042
// acceptance pin + subsequent known-good axiom syncs); the env-var override
// lets operators rotate the pin without a re-deploy.

/// Stable invariant emitted when the axiom-execution-trust envelope's
/// `tool_provenance.source_commit` is not on the receiver-side acceptance
/// list. Downstream tooling (operator scripts, axiom-side test harnesses)
/// greps on this string.
pub const AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT: &str =
    "axiom_execution_trust.tool_provenance.source_commit.stale";

/// Environment variable that, when set, REPLACES the default
/// [`DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS`] accept-list with a
/// comma-separated set of operator-supplied 40-hex commit SHAs.
///
/// Setting this to the empty string is equivalent to setting it to an empty
/// list — the freshness gate will then refuse every source_commit. Operators
/// using this surface MUST supply at least one SHA.
///
/// Whitespace around each SHA is trimmed; the value is normalised to
/// lowercase before comparison.
pub const CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV: &str = "CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS";

/// Default Cortex-side accept-list for the
/// `axiom_execution_trust.tool_provenance.source_commit` freshness gate.
///
/// Three production axiom SHAs are listed by virtue of being either the ADR
/// 0042 acceptance pin or a subsequent axiom-side sync the Cortex team has
/// already accepted as known-good:
///
/// - `44b9a25dfbe5a93e64120f53b65730828d1af91c` — ADR 0042 acceptance pin.
/// - `062d0d42ac5ef300fa3e04ef5b49b14864babcdd` — axiom team's alignment
///   acknowledgement to Cortex receiver-readiness at `0e67a32`
///   (2026-05-13).
/// - `9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a` — axiom team's V2 live-
///   exchange payload corpus delivery (2026-05-13).
///
/// One test-fixture placeholder SHA is also listed so the Cortex-side
/// fixtures (which use `aaaa…aaaa` deliberately to flag the value as a
/// test placeholder) continue to admit. The fixture SHA is well-known and
/// carries no real authority — including it on the accept-list has no
/// security impact because any payload reaching the freshness gate has
/// already passed the upstream structural validation that an attacker
/// cannot trivially forge.
///
/// To rotate without re-deploying, set
/// [`CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV`].
pub const DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS: &[&str] = &[
    // ADR 0042 acceptance pin.
    "44b9a25dfbe5a93e64120f53b65730828d1af91c",
    // Axiom alignment ack to Cortex 0e67a32 readiness (2026-05-13).
    "062d0d42ac5ef300fa3e04ef5b49b14864babcdd",
    // Axiom V2 live-exchange payload corpus delivery (2026-05-13).
    "9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a",
    // Test-fixture placeholder; see doc-comment above for the rationale.
    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];

/// Resolve the active accept-list for the
/// [`AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT`] freshness gate.
///
/// Resolution order:
///
/// 1. If [`CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV`] is set, parse it as a
///    comma-separated list of 40-hex SHAs (whitespace trimmed, lowercase
///    normalised, empty entries discarded).
/// 2. Otherwise, return [`DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS`] as a
///    `Vec<String>`.
///
/// Returned SHAs are lower-cased; callers using
/// [`is_axiom_source_commit_fresh`] do not need to lower-case the candidate
/// value separately.
#[must_use]
pub fn accepted_axiom_source_commits() -> Vec<String> {
    if let Ok(raw) = std::env::var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV) {
        return raw
            .split(',')
            .map(|piece| piece.trim().to_ascii_lowercase())
            .filter(|piece| !piece.is_empty())
            .collect();
    }
    DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
        .iter()
        .map(|sha| (*sha).to_string())
        .collect()
}

/// Check whether a candidate `source_commit` is on the supplied
/// receiver-side accept-list.
///
/// Comparison is case-insensitive on the candidate; the accept-list is
/// assumed to be lower-cased (the canonical form returned by
/// [`accepted_axiom_source_commits`]). The candidate must also pass the
/// 40-character lowercase-hex structural check
/// (`tool_provenance.source_commit.invalid_format`) before reaching this
/// gate; this function does NOT re-validate format.
#[must_use]
pub fn is_axiom_source_commit_fresh(candidate: &str, accepted: &[String]) -> bool {
    let lowered = candidate.to_ascii_lowercase();
    accepted.iter().any(|sha| sha == &lowered)
}

// ---------------------------------------------------------------------------
// Field validators — stable invariant names
// ---------------------------------------------------------------------------

const SHA256_PATTERN_PREFIX: &str = "sha256:";
const SHA256_HEX_LEN: usize = 64;

fn is_sha256_hash(value: &str) -> bool {
    if let Some(hex) = value.strip_prefix(SHA256_PATTERN_PREFIX) {
        hex.len() == SHA256_HEX_LEN
            && hex
                .chars()
                .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
    } else {
        false
    }
}

fn is_commit_sha(value: &str) -> bool {
    value.len() == 40
        && value
            .chars()
            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
}

fn push(errors: &mut Vec<TrustExchangeFieldError>, invariant: &str, reason: impl Into<String>) {
    errors.push(TrustExchangeFieldError::new(invariant, reason));
}

fn require_nonblank(
    errors: &mut Vec<TrustExchangeFieldError>,
    value: &str,
    invariant: &str,
    reason: &str,
) {
    if value.trim().is_empty() {
        push(errors, invariant, reason);
    }
}

impl CortexContextTrust {
    /// Validate the decomposed fields required for Cortex admission.
    ///
    /// Returns a list of stable invariant failures. The `compatibility_trust_label`
    /// is always treated as display-only and never satisfies a required field.
    pub fn validate(&self) -> TrustExchangeValidation {
        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();

        if self.schema != CORTEX_CONTEXT_TRUST_SCHEMA {
            push(
                &mut errors,
                "cortex_context_trust.schema.mismatch",
                format!(
                    "schema must be `{CORTEX_CONTEXT_TRUST_SCHEMA}`, got `{}`",
                    self.schema
                ),
            );
        }
        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
            push(
                &mut errors,
                "cortex_context_trust.version.mismatch",
                format!(
                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
                    self.version
                ),
            );
        }

        require_nonblank(
            &mut errors,
            &self.context_id,
            "cortex_context_trust.context_id.missing",
            "context_id must be a non-empty string",
        );

        // proof_state.* (each subfield contributes its own invariant)
        if self
            .proof_state
            .failing_edges
            .iter()
            .any(|e| e.trim().is_empty())
        {
            push(
                &mut errors,
                "cortex_context_trust.proof_state.failing_edges.blank_entry",
                "proof_state.failing_edges entries must not be blank",
            );
        }
        if self
            .proof_state
            .proof_refs
            .iter()
            .any(|e| e.trim().is_empty())
        {
            push(
                &mut errors,
                "cortex_context_trust.proof_state.proof_refs.blank_entry",
                "proof_state.proof_refs entries must not be blank",
            );
        }
        if matches!(
            self.proof_state.state,
            ContextProofStateValue::Missing | ContextProofStateValue::Failed
        ) {
            push(
                &mut errors,
                "cortex_context_trust.proof_state.state.unusable",
                "proof_state.state must be `closed` or `partial` for Cortex consumption",
            );
        }

        // semantic_trust.*
        require_nonblank(
            &mut errors,
            &self.semantic_trust.weighting_basis,
            "cortex_context_trust.semantic_trust.weighting_basis.missing",
            "semantic_trust.weighting_basis must be a non-empty string",
        );
        if !(0.0..=1.0).contains(&self.semantic_trust.trust_weight) {
            push(
                &mut errors,
                "cortex_context_trust.semantic_trust.trust_weight.out_of_range",
                "semantic_trust.trust_weight must be in [0, 1]",
            );
        }
        if matches!(
            self.semantic_trust.provenance_class,
            ContextProvenanceClass::Unknown
        ) {
            push(
                &mut errors,
                "cortex_context_trust.semantic_trust.provenance_class.unknown",
                "semantic_trust.provenance_class must be a known class",
            );
        }

        // redaction_state.*
        if matches!(self.redaction_state.status, ContextRedactionStatus::Unknown) {
            push(
                &mut errors,
                "cortex_context_trust.redaction_state.status.unknown",
                "redaction_state.status must be a known status",
            );
        }
        if self
            .redaction_state
            .redaction_refs
            .iter()
            .any(|e| e.trim().is_empty())
        {
            push(
                &mut errors,
                "cortex_context_trust.redaction_state.redaction_refs.blank_entry",
                "redaction_state.redaction_refs entries must not be blank",
            );
        }

        // policy_result.*
        require_nonblank(
            &mut errors,
            &self.policy_result.decision_id,
            "cortex_context_trust.policy_result.decision_id.missing",
            "policy_result.decision_id must be a non-empty string",
        );

        // allowed_claim_language / forbidden_uses minimum coverage
        if self.allowed_claim_language.len() < 3 {
            push(
                &mut errors,
                "cortex_context_trust.allowed_claim_language.insufficient_coverage",
                "allowed_claim_language must include candidate, observed, and validated",
            );
        }
        if self.forbidden_uses.len() < 3 {
            push(
                &mut errors,
                "cortex_context_trust.forbidden_uses.insufficient_coverage",
                "forbidden_uses must include execution_permission, durable_truth_promotion, release_claim",
            );
        }

        // source_anchors.*
        if self.source_anchors.is_empty() {
            push(
                &mut errors,
                "cortex_context_trust.source_anchors.missing",
                "source_anchors must contain at least one anchor",
            );
        }
        for anchor in &self.source_anchors {
            require_nonblank(
                &mut errors,
                &anchor.source_id,
                "cortex_context_trust.source_anchors.source_id.missing",
                "source_anchors[].source_id must be a non-empty string",
            );
            require_nonblank(
                &mut errors,
                &anchor.r#ref,
                "cortex_context_trust.source_anchors.ref.missing",
                "source_anchors[].ref must be a non-empty string",
            );
            if !is_sha256_hash(&anchor.hash) {
                push(
                    &mut errors,
                    "cortex_context_trust.source_anchors.hash.invalid_format",
                    "source_anchors[].hash must match sha256:<64 lowercase hex>",
                );
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

impl AxiomExecutionTrust {
    /// Validate the decomposed fields required for Cortex admission of a
    /// pai-axiom execution receipt.
    pub fn validate(&self) -> TrustExchangeValidation {
        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();

        if self.schema != AXIOM_EXECUTION_TRUST_SCHEMA {
            push(
                &mut errors,
                "axiom_execution_trust.schema.mismatch",
                format!(
                    "schema must be `{AXIOM_EXECUTION_TRUST_SCHEMA}`, got `{}`",
                    self.schema
                ),
            );
        }
        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
            push(
                &mut errors,
                "axiom_execution_trust.version.mismatch",
                format!(
                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
                    self.version
                ),
            );
        }

        require_nonblank(
            &mut errors,
            &self.action_id,
            "axiom_execution_trust.action_id.missing",
            "action_id must be a non-empty string",
        );

        // repo_trust.*
        require_nonblank(
            &mut errors,
            &self.repo_trust.evaluation_ref,
            "axiom_execution_trust.repo_trust.evaluation_ref.missing",
            "repo_trust.evaluation_ref must be a non-empty string",
        );

        // actor_attestation.*
        require_nonblank(
            &mut errors,
            &self.actor_attestation.identity_ref,
            "axiom_execution_trust.actor_attestation.identity_ref.missing",
            "actor_attestation.identity_ref must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.actor_attestation.attestation_ref,
            "axiom_execution_trust.actor_attestation.attestation_ref.missing",
            "actor_attestation.attestation_ref must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.actor_attestation.operator_approval_ref,
            "axiom_execution_trust.actor_attestation.operator_approval_ref.missing",
            "actor_attestation.operator_approval_ref must be a non-empty string",
        );
        if !is_sha256_hash(&self.actor_attestation.operator_approval_hash) {
            push(
                &mut errors,
                "axiom_execution_trust.actor_attestation.operator_approval_hash.invalid_format",
                "actor_attestation.operator_approval_hash must match sha256:<64 lowercase hex>",
            );
        }

        // policy_decision.*
        require_nonblank(
            &mut errors,
            &self.policy_decision.decision_id,
            "axiom_execution_trust.policy_decision.decision_id.missing",
            "policy_decision.decision_id must be a non-empty string",
        );

        // token_scope.*
        require_nonblank(
            &mut errors,
            &self.token_scope.token_id,
            "axiom_execution_trust.token_scope.token_id.missing",
            "token_scope.token_id must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.token_scope.audience,
            "axiom_execution_trust.token_scope.audience.missing",
            "token_scope.audience must be a non-empty string",
        );
        for (field, value, invariant) in [
            (
                "scope_hash",
                &self.token_scope.scope_hash,
                "axiom_execution_trust.token_scope.scope_hash.invalid_format",
            ),
            (
                "operation_hash",
                &self.token_scope.operation_hash,
                "axiom_execution_trust.token_scope.operation_hash.invalid_format",
            ),
            (
                "manifest_hash",
                &self.token_scope.manifest_hash,
                "axiom_execution_trust.token_scope.manifest_hash.invalid_format",
            ),
            (
                "request_hash",
                &self.token_scope.request_hash,
                "axiom_execution_trust.token_scope.request_hash.invalid_format",
            ),
        ] {
            if !is_sha256_hash(value) {
                push(
                    &mut errors,
                    invariant,
                    format!("token_scope.{field} must match sha256:<64 lowercase hex>"),
                );
            }
        }

        // tool_provenance.*
        require_nonblank(
            &mut errors,
            &self.tool_provenance.tool_id,
            "axiom_execution_trust.tool_provenance.tool_id.missing",
            "tool_provenance.tool_id must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.tool_provenance.tool_version,
            "axiom_execution_trust.tool_provenance.tool_version.missing",
            "tool_provenance.tool_version must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.tool_provenance.command_ref,
            "axiom_execution_trust.tool_provenance.command_ref.missing",
            "tool_provenance.command_ref must be a non-empty string",
        );
        if !is_commit_sha(&self.tool_provenance.source_commit) {
            push(
                &mut errors,
                "axiom_execution_trust.tool_provenance.source_commit.invalid_format",
                "tool_provenance.source_commit must be a 40-character lowercase hex SHA",
            );
        }
        require_nonblank(
            &mut errors,
            &self.tool_provenance.dependency_lock_ref,
            "axiom_execution_trust.tool_provenance.dependency_lock_ref.missing",
            "tool_provenance.dependency_lock_ref must be a non-empty string",
        );

        // source_anchors.*
        if self.source_anchors.is_empty() {
            push(
                &mut errors,
                "axiom_execution_trust.source_anchors.missing",
                "source_anchors must contain at least one anchor",
            );
        }
        for anchor in &self.source_anchors {
            require_nonblank(
                &mut errors,
                &anchor.source_id,
                "axiom_execution_trust.source_anchors.source_id.missing",
                "source_anchors[].source_id must be a non-empty string",
            );
            require_nonblank(
                &mut errors,
                &anchor.r#ref,
                "axiom_execution_trust.source_anchors.ref.missing",
                "source_anchors[].ref must be a non-empty string",
            );
            if !is_sha256_hash(&anchor.hash) {
                push(
                    &mut errors,
                    "axiom_execution_trust.source_anchors.hash.invalid_format",
                    "source_anchors[].hash must match sha256:<64 lowercase hex>",
                );
            }
        }

        // runtime_mode
        require_nonblank(
            &mut errors,
            &self.runtime_mode,
            "axiom_execution_trust.runtime_mode.missing",
            "runtime_mode must be a non-empty string",
        );

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    /// Check whether the token is expired relative to `now`.
    #[must_use]
    pub fn token_expired_at(&self, now: DateTime<Utc>) -> bool {
        self.token_scope.expires_at < now
    }
}

impl AuthorityFeedbackLoop {
    /// Validate decomposed fields required at the Cortex consumer.
    pub fn validate(&self) -> TrustExchangeValidation {
        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();

        if self.schema != AUTHORITY_FEEDBACK_LOOP_SCHEMA {
            push(
                &mut errors,
                "authority_feedback_loop.schema.mismatch",
                format!(
                    "schema must be `{AUTHORITY_FEEDBACK_LOOP_SCHEMA}`, got `{}`",
                    self.schema
                ),
            );
        }
        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
            push(
                &mut errors,
                "authority_feedback_loop.version.mismatch",
                format!(
                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
                    self.version
                ),
            );
        }

        require_nonblank(
            &mut errors,
            &self.loop_id,
            "authority_feedback_loop.loop_id.missing",
            "loop_id must be a non-empty string",
        );

        require_nonblank(
            &mut errors,
            &self.initiating_context.context_id,
            "authority_feedback_loop.initiating_context.context_id.missing",
            "initiating_context.context_id must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.initiating_context.cortex_context_trust_ref,
            "authority_feedback_loop.initiating_context.cortex_context_trust_ref.missing",
            "initiating_context.cortex_context_trust_ref must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.axiom_action.action_id,
            "authority_feedback_loop.axiom_action.action_id.missing",
            "axiom_action.action_id must be a non-empty string",
        );
        require_nonblank(
            &mut errors,
            &self.axiom_action.axiom_execution_trust_ref,
            "authority_feedback_loop.axiom_action.axiom_execution_trust_ref.missing",
            "axiom_action.axiom_execution_trust_ref must be a non-empty string",
        );

        if self.returned_artifacts.is_empty() {
            push(
                &mut errors,
                "authority_feedback_loop.returned_artifacts.missing",
                "returned_artifacts must contain at least one artifact",
            );
        }
        for artifact in &self.returned_artifacts {
            require_nonblank(
                &mut errors,
                &artifact.artifact_id,
                "authority_feedback_loop.returned_artifacts.artifact_id.missing",
                "returned_artifacts[].artifact_id must be a non-empty string",
            );
            require_nonblank(
                &mut errors,
                &artifact.lineage_ref,
                "authority_feedback_loop.returned_artifacts.lineage_ref.missing",
                "returned_artifacts[].lineage_ref must be a non-empty string",
            );
        }

        require_nonblank(
            &mut errors,
            &self.contradiction_scan_ref,
            "authority_feedback_loop.contradiction_scan_ref.missing",
            "contradiction_scan_ref must be a non-empty string",
        );

        if !self.target_domain_validation.required {
            push(
                &mut errors,
                "authority_feedback_loop.target_domain_validation.required.must_be_true",
                "target_domain_validation.required must be true",
            );
        }

        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    /// Hard structural Cortex refusal: same-loop promotion must be `false`.
    /// Returns `true` when the loop record is structurally inadmissible.
    #[must_use]
    pub const fn violates_same_loop_invariant(&self) -> bool {
        self.same_loop_promotion_allowed
    }

    /// Hard structural Cortex refusal: authority claims signaling durable
    /// truth or full execution authority must be rejected.
    #[must_use]
    pub const fn claims_durable_authority(&self) -> bool {
        self.authority_claims
            .durable_truth_promotion
            .claims_authority()
            || self
                .authority_claims
                .full_execution_authority
                .claims_authority()
    }
}

// ---------------------------------------------------------------------------
// Tests — round-trip with the pai-axiom valid fixtures, stable invariant names
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;

    const VALID_CTX: &str =
        include_str!("../tests/fixtures/pai-axiom/valid-cortex-context-trust.json");
    const VALID_EXEC: &str =
        include_str!("../tests/fixtures/pai-axiom/valid-axiom-execution-trust.json");

    #[test]
    fn parse_valid_cortex_context_trust_round_trips() {
        let envelope = parse_cortex_context_trust(VALID_CTX).expect("valid fixture parses");
        assert_eq!(envelope.schema, CORTEX_CONTEXT_TRUST_SCHEMA);
        assert_eq!(envelope.version, TRUST_EXCHANGE_SCHEMA_VERSION);
        assert_eq!(envelope.context_id, "ctx_valid_behavior_change");
        assert_eq!(envelope.quarantine_state, ContextQuarantineState::Clear);
        assert!(matches!(
            envelope.proof_state.state,
            ContextProofStateValue::Closed
        ));
        envelope.validate().expect("valid fixture validates");

        let reserialized = serde_json::to_value(&envelope).unwrap();
        assert_eq!(reserialized["schema"], "cortex_context_trust");
        assert_eq!(reserialized["truth_ceiling"], "validated");
    }

    #[test]
    fn parse_valid_axiom_execution_trust_round_trips() {
        let envelope = parse_axiom_execution_trust(VALID_EXEC).expect("valid fixture parses");
        assert_eq!(envelope.schema, AXIOM_EXECUTION_TRUST_SCHEMA);
        assert_eq!(envelope.version, TRUST_EXCHANGE_SCHEMA_VERSION);
        assert_eq!(envelope.action_id, "action_valid_authority_grade");
        assert_eq!(envelope.token_scope.audience, "cortex-admission");
        assert_eq!(
            envelope.token_scope.revocation_result,
            TokenRevocationResult::Active
        );
        envelope.validate().expect("valid fixture validates");

        let reserialized = serde_json::to_value(&envelope).unwrap();
        assert_eq!(reserialized["schema"], "axiom_execution_trust");
        assert_eq!(reserialized["execution_trust_level"], "authority_grade");
    }

    #[test]
    fn missing_token_audience_emits_stable_invariant() {
        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
        let mut inner = value["axiom_execution_trust"].clone();
        inner["token_scope"]["audience"] = serde_json::Value::String(String::new());

        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
        let errors = envelope.validate().expect_err("empty audience must fail");
        assert!(errors
            .iter()
            .any(|e| e.invariant == "axiom_execution_trust.token_scope.audience.missing"));
    }

    #[test]
    fn missing_operator_approval_hash_emits_stable_invariant() {
        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
        let mut inner = value["axiom_execution_trust"].clone();
        inner["actor_attestation"]["operator_approval_hash"] =
            serde_json::Value::String("not-a-hash".to_string());

        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
        let errors = envelope.validate().expect_err("bad hash must fail");
        assert!(errors.iter().any(|e| e.invariant
            == "axiom_execution_trust.actor_attestation.operator_approval_hash.invalid_format"));
    }

    #[test]
    fn invalid_source_commit_emits_stable_invariant() {
        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
        let mut inner = value["axiom_execution_trust"].clone();
        inner["tool_provenance"]["source_commit"] =
            serde_json::Value::String("not-a-commit".to_string());

        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
        let errors = envelope.validate().expect_err("bad commit must fail");
        assert!(errors
            .iter()
            .any(|e| e.invariant
                == "axiom_execution_trust.tool_provenance.source_commit.invalid_format"));
    }

    #[test]
    fn proof_state_failed_emits_stable_invariant() {
        let value: serde_json::Value = serde_json::from_str(VALID_CTX).unwrap();
        let mut inner = value["cortex_context_trust"].clone();
        inner["proof_state"]["state"] = serde_json::Value::String("failed".to_string());
        inner["proof_state"]["failing_edges"] = serde_json::json!(["proof.audit.missing"]);

        let envelope: CortexContextTrust = serde_json::from_value(inner).unwrap();
        let errors = envelope
            .validate()
            .expect_err("failed proof state must fail");
        assert!(errors
            .iter()
            .any(|e| e.invariant == "cortex_context_trust.proof_state.state.unusable"));
    }

    #[test]
    fn extra_top_level_field_fails_closed() {
        let json = serde_json::json!({
            "schema": "cortex_context_trust",
            "version": 1,
            "context_id": "ctx",
            "compatibility_trust_label": "validated",
            "proof_state": {"state": "closed", "failing_edges": [], "proof_refs": ["p"]},
            "truth_ceiling": "validated",
            "semantic_trust": {"provenance_class": "observed", "trust_weight": 1, "weighting_basis": "x"},
            "quarantine_state": "clear",
            "redaction_state": {"status": "none", "redaction_refs": []},
            "policy_result": {"decision_id": "p", "result": "allow"},
            "source_anchors": [{
                "source_id": "s",
                "source_type": "event",
                "ref": "r",
                "hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
            }],
            "rogue_field": true
        });
        let err = serde_json::from_value::<CortexContextTrust>(json)
            .expect_err("deny_unknown_fields must reject rogue field");
        assert!(err.to_string().contains("rogue_field"));
    }

    #[test]
    fn revoked_token_is_must_reject() {
        assert!(TokenRevocationResult::Revoked.must_reject());
        assert!(TokenRevocationResult::Inactive.must_reject());
        assert!(!TokenRevocationResult::Active.must_reject());
    }

    #[test]
    fn token_expiry_check_uses_injected_now() {
        let mut envelope = parse_axiom_execution_trust(VALID_EXEC).unwrap();
        envelope.token_scope.expires_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
        let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
        assert!(envelope.token_expired_at(now));
    }

    #[test]
    fn same_loop_promotion_invariant_is_structural() {
        let loop_record = AuthorityFeedbackLoop {
            schema: AUTHORITY_FEEDBACK_LOOP_SCHEMA.to_string(),
            version: 1,
            authority_feedback_loop_ref: None,
            loop_id: "loop_x".to_string(),
            started_at: Utc::now(),
            initiating_context: FeedbackInitiatingContext {
                context_id: "ctx".to_string(),
                cortex_context_trust_ref: "ref".to_string(),
            },
            axiom_action: FeedbackAxiomAction {
                action_id: "act".to_string(),
                axiom_execution_trust_ref: "ref".to_string(),
            },
            returned_artifacts: vec![FeedbackReturnedArtifact {
                artifact_id: "art".to_string(),
                lineage_ref: "lin".to_string(),
                lifecycle_state: ArtifactLifecycleState::Candidate,
                reproducibility_level: ReproducibilityLevel::Observational,
            }],
            amplification_risk: AmplificationRisk::Low,
            independent_evidence_refs: vec![],
            external_grounding_refs: vec![],
            contradiction_scan_ref: "scan".to_string(),
            quarantine_state: ContextQuarantineState::Clear,
            confidence_ceiling: ConfidenceCeiling::Advisory,
            same_loop_promotion_allowed: true,
            authority_claims: FeedbackAuthorityClaims {
                durable_truth_promotion: AuthorityClaimStatus::Denied,
                full_execution_authority: AuthorityClaimStatus::Denied,
                review_required: true,
            },
            target_domain_validation: TargetDomainValidation {
                required: true,
                independent_validation_ref: None,
                result: TargetDomainValidationResult::Pending,
            },
            residual_risk: vec![],
        };
        assert!(loop_record.violates_same_loop_invariant());
        assert!(!loop_record.claims_durable_authority());
    }

    #[test]
    fn named_quarantine_outputs_iterate_invariants() {
        let outputs = NamedQuarantineOutputs {
            source_context: Some(QuarantineOutput::new(
                "axiom.admission.quarantine.propagated",
                "source quarantine",
            )),
            token_revocation: Some(QuarantineOutput::new(
                "axiom_execution_trust.token_scope.revoked",
                "revoked",
            )),
            ..Default::default()
        };
        let names: Vec<&str> = outputs.invariants().collect();
        assert_eq!(names.len(), 2);
        assert!(names.contains(&"axiom.admission.quarantine.propagated"));
        assert!(names.contains(&"axiom_execution_trust.token_scope.revoked"));
    }

    #[test]
    fn quarantine_state_propagation_predicate() {
        assert!(ContextQuarantineState::Quarantined.propagates_quarantine());
        assert!(ContextQuarantineState::DerivedFromQuarantined.propagates_quarantine());
        assert!(ContextQuarantineState::Unknown.propagates_quarantine());
        assert!(!ContextQuarantineState::Clear.propagates_quarantine());
    }

    // -----------------------------------------------------------------
    // Receiver-side source-commit freshness gate
    // -----------------------------------------------------------------

    /// Guard the stable invariant string against accidental rename. Operator
    /// scripts and the axiom-side test harness grep on this exact value.
    #[test]
    fn source_commit_stale_invariant_is_stable() {
        assert_eq!(
            AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT,
            "axiom_execution_trust.tool_provenance.source_commit.stale"
        );
    }

    /// Guard the env-var name against rename. Operators set this in their
    /// shell; renaming silently breaks deployments.
    #[test]
    fn accepted_source_commits_env_var_name_is_stable() {
        assert_eq!(
            CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV,
            "CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS"
        );
    }

    #[test]
    fn default_accept_list_includes_adr_0042_pin() {
        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
            .contains(&"44b9a25dfbe5a93e64120f53b65730828d1af91c"));
    }

    #[test]
    fn default_accept_list_includes_known_good_v2_corpus() {
        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
            .contains(&"9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a"));
        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
            .contains(&"062d0d42ac5ef300fa3e04ef5b49b14864babcdd"));
    }

    #[test]
    fn default_accept_list_entries_are_well_formed_commits() {
        for sha in DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS {
            assert!(
                is_commit_sha(sha),
                "default accept-list entry `{sha}` must be a 40-char lowercase-hex commit SHA",
            );
        }
    }

    #[test]
    fn freshness_predicate_admits_accepted_sha() {
        let accepted: Vec<String> = DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
            .iter()
            .map(|sha| (*sha).to_string())
            .collect();
        assert!(is_axiom_source_commit_fresh(
            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
            &accepted
        ));
    }

    #[test]
    fn freshness_predicate_refuses_unknown_sha() {
        let accepted: Vec<String> = DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
            .iter()
            .map(|sha| (*sha).to_string())
            .collect();
        assert!(!is_axiom_source_commit_fresh(
            "0000000000000000000000000000000000000000",
            &accepted
        ));
        assert!(!is_axiom_source_commit_fresh(
            "1234567890abcdef1234567890abcdef12345678",
            &accepted
        ));
    }

    #[test]
    fn freshness_predicate_is_case_insensitive_on_candidate() {
        let accepted = vec!["44b9a25dfbe5a93e64120f53b65730828d1af91c".to_string()];
        assert!(is_axiom_source_commit_fresh(
            "44B9A25DFBE5A93E64120F53B65730828D1AF91C",
            &accepted
        ));
        assert!(is_axiom_source_commit_fresh(
            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
            &accepted
        ));
    }

    #[test]
    fn freshness_predicate_empty_accept_list_refuses_everything() {
        let accepted: Vec<String> = Vec::new();
        assert!(!is_axiom_source_commit_fresh(
            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
            &accepted
        ));
    }

    /// Verify env-var parsing semantics. We run this against a temporary
    /// override; the test restores the prior env state at the end so other
    /// tests in this module see the default.
    #[test]
    fn accepted_axiom_source_commits_env_var_replaces_default() {
        // Race-free: this test mutates a process-global env var, so it
        // must not be parallelised with other tests reading the same var.
        // No other test in this module reads `accepted_axiom_source_commits`
        // through the env-var path (the freshness-predicate tests build
        // their accept-list inline), so a single-test guard is sufficient.
        let prior = std::env::var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV).ok();

        std::env::set_var(
            CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV,
            "  AAAAaaaaAAAAaaaaAAAAaaaaAAAAaaaaAAAAaaaa , 1111111111111111111111111111111111111111  ,,",
        );
        let resolved = accepted_axiom_source_commits();
        assert_eq!(resolved.len(), 2);
        assert!(resolved.contains(&"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()));
        assert!(resolved.contains(&"1111111111111111111111111111111111111111".to_string()));
        // The ADR 0042 default pin MUST be excluded — env var REPLACES the
        // default, it does not extend it.
        assert!(!resolved.contains(&"44b9a25dfbe5a93e64120f53b65730828d1af91c".to_string()));

        // Restore prior state so neighbouring tests are unaffected.
        match prior {
            Some(value) => std::env::set_var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV, value),
            None => std::env::remove_var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV),
        }
    }
}
