Skip to main content

cortex_core/
axiom_trust.rs

1//! Typed pai-axiom <-> Cortex trust exchange envelopes (ADR 0042 / 0043).
2//!
3//! These structs are the receiver-side shape for the three pai-axiom
4//! schemas that drive Cortex's field-level admission gate:
5//!
6//! - `cortex_context_trust` (Cortex -> pai-axiom, mirrored back through the
7//!   boundary for consumption checks).
8//! - `axiom_execution_trust` (pai-axiom -> Cortex execution receipt).
9//! - `authority_feedback_loop` (cross-system loop record).
10//!
11//! The module is shape-only: it deserializes the envelope, runs per-field
12//! validators that emit *stable invariant names*, and produces a typed
13//! report. It does not persist state, does not grant authority, and does
14//! not run the admission gate itself.
15//!
16//! Every struct uses `#[serde(deny_unknown_fields)]` so that schema drift
17//! at the producer side fails closed at the parse boundary. Stable
18//! invariant names follow `<schema_name>.<field_path>.<failure_class>`.
19
20use chrono::{DateTime, Utc};
21use serde::{Deserialize, Serialize};
22
23/// Stable schema string for [`CortexContextTrust`] envelopes.
24pub const CORTEX_CONTEXT_TRUST_SCHEMA: &str = "cortex_context_trust";
25
26/// Stable schema string for [`AxiomExecutionTrust`] envelopes.
27pub const AXIOM_EXECUTION_TRUST_SCHEMA: &str = "axiom_execution_trust";
28
29/// Stable schema string for [`AuthorityFeedbackLoop`] envelopes.
30pub const AUTHORITY_FEEDBACK_LOOP_SCHEMA: &str = "authority_feedback_loop";
31
32/// Stable schema version supported on the Cortex receiver side.
33pub const TRUST_EXCHANGE_SCHEMA_VERSION: u16 = 1;
34
35// ---------------------------------------------------------------------------
36// Cortex context trust envelope
37// ---------------------------------------------------------------------------
38
39/// Cortex context trust envelope (`CORTEX_CONTEXT_TRUST.schema.json` v1).
40///
41/// `compatibility_trust_label` is intentionally kept on the struct but
42/// classified as display-only by [`Self::validate`]: per
43/// `CORTEX_AXIOM_TRUST_EXCHANGE_COMPATIBILITY.json` it MUST NOT satisfy
44/// any behavior-changing gate. Cortex consumes the decomposed fields.
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct CortexContextTrust {
48    /// Schema discriminator. Must equal [`CORTEX_CONTEXT_TRUST_SCHEMA`].
49    pub schema: String,
50    /// Schema version. Must equal [`TRUST_EXCHANGE_SCHEMA_VERSION`].
51    pub version: u16,
52    /// Stable receiver-side reference id (optional in pai-axiom's wire,
53    /// supplied by the boundary tool when present).
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub cortex_context_trust_ref: Option<String>,
56    /// Stable context identifier.
57    pub context_id: String,
58    /// Coarse compatibility label kept for back-compat — display only.
59    pub compatibility_trust_label: CompatibilityTrustLabel,
60    /// Proof closure state and failing edges.
61    pub proof_state: ContextProofState,
62    /// Truth ceiling permitted to the receiving consumer.
63    pub truth_ceiling: TruthCeiling,
64    /// Semantic trust decomposition.
65    pub semantic_trust: ContextSemanticTrust,
66    /// Provenance references (optional in some fixtures).
67    #[serde(default)]
68    pub provenance_refs: Vec<String>,
69    /// Contradiction posture for the context.
70    #[serde(default = "ContradictionState::default_unknown")]
71    pub contradiction_state: ContradictionState,
72    /// Promotion posture for the context.
73    #[serde(default = "PromotionState::default_candidate")]
74    pub promotion_state: PromotionState,
75    /// Quarantine posture for the context.
76    pub quarantine_state: ContextQuarantineState,
77    /// Redaction posture and references.
78    pub redaction_state: ContextRedactionState,
79    /// Confidence value and scale (optional in some fixtures).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub confidence: Option<ContextConfidence>,
82    /// Policy decision record.
83    pub policy_result: ContextPolicyResult,
84    /// Allowed claim language vocabulary.
85    #[serde(default)]
86    pub allowed_claim_language: Vec<ContextAllowedClaimLanguage>,
87    /// Forbidden authority-bearing uses.
88    #[serde(default)]
89    pub forbidden_uses: Vec<ContextForbiddenUse>,
90    /// Allowed use vocabulary.
91    #[serde(default)]
92    pub allowed_use: Vec<ContextAllowedUse>,
93    /// Evidence references backing the context.
94    #[serde(default)]
95    pub evidence_refs: Vec<String>,
96    /// Source anchors backing the context.
97    pub source_anchors: Vec<ContextSourceAnchor>,
98    /// Residual risk strings copied from the producer.
99    #[serde(default)]
100    pub residual_risk: Vec<String>,
101}
102
103/// Coarse compatibility label vocabulary (display only).
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum CompatibilityTrustLabel {
107    /// Compatibility alias only.
108    Untrusted,
109    /// Compatibility alias only.
110    Advisory,
111    /// Compatibility alias only.
112    Observed,
113    /// Compatibility alias only.
114    Validated,
115    /// Compatibility alias only.
116    AuthorityClaimed,
117}
118
119/// Proof closure state inside a Cortex context envelope.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(deny_unknown_fields)]
122pub struct ContextProofState {
123    /// Proof closure state value.
124    pub state: ContextProofStateValue,
125    /// Failing edges names. Must be present (even if empty).
126    pub failing_edges: Vec<String>,
127    /// Proof references for the closure.
128    pub proof_refs: Vec<String>,
129}
130
131/// Proof closure state vocabulary.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
133#[serde(rename_all = "snake_case")]
134pub enum ContextProofStateValue {
135    /// No proof was supplied.
136    Missing,
137    /// Proof is partial.
138    Partial,
139    /// Proof is closed.
140    Closed,
141    /// Proof failed.
142    Failed,
143}
144
145/// Truth ceiling vocabulary.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
147#[serde(rename_all = "kebab-case")]
148pub enum TruthCeiling {
149    /// No truth claim permitted.
150    None,
151    /// Advisory only.
152    Advisory,
153    /// Observed only.
154    Observed,
155    /// Validated.
156    Validated,
157    /// Already promoted by Cortex (display only at the receiver).
158    PromotedByCortex,
159}
160
161impl Serialize for TruthCeiling {
162    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163        serializer.serialize_str(self.wire_string())
164    }
165}
166
167impl TruthCeiling {
168    /// Stable wire string mirroring the JSON Schema enum.
169    #[must_use]
170    pub const fn wire_string(self) -> &'static str {
171        match self {
172            Self::None => "none",
173            Self::Advisory => "advisory",
174            Self::Observed => "observed",
175            Self::Validated => "validated",
176            Self::PromotedByCortex => "promoted-by-cortex",
177        }
178    }
179}
180
181/// Decomposed semantic trust block.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183#[serde(deny_unknown_fields)]
184pub struct ContextSemanticTrust {
185    /// Provenance class for the context.
186    pub provenance_class: ContextProvenanceClass,
187    /// Trust weight in [0, 1].
188    pub trust_weight: f64,
189    /// Free-form weighting basis explanation.
190    pub weighting_basis: String,
191}
192
193/// Provenance class vocabulary inside context trust.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
195#[serde(rename_all = "snake_case")]
196pub enum ContextProvenanceClass {
197    /// Unknown provenance.
198    Unknown,
199    /// Claimed but unverified.
200    Claimed,
201    /// Observed.
202    Observed,
203    /// Derived from named premises.
204    Derived,
205    /// Curated.
206    Curated,
207    /// Already promoted (display only).
208    Promoted,
209}
210
211/// Contradiction state vocabulary.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum ContradictionState {
215    /// No contradiction known.
216    NoneKnown,
217    /// Contradictions remain unresolved.
218    Unresolved,
219    /// Contradictions were resolved.
220    Resolved,
221    /// Contradiction posture unknown.
222    Unknown,
223}
224
225impl ContradictionState {
226    fn default_unknown() -> Self {
227        Self::Unknown
228    }
229}
230
231/// Promotion state vocabulary.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
233#[serde(rename_all = "snake_case")]
234pub enum PromotionState {
235    /// Candidate only.
236    Candidate,
237    /// Observed.
238    Observed,
239    /// Validated.
240    Validated,
241    /// Already promoted (display only).
242    Promoted,
243    /// Stale.
244    Stale,
245    /// Quarantined.
246    Quarantined,
247}
248
249impl PromotionState {
250    fn default_candidate() -> Self {
251        Self::Candidate
252    }
253}
254
255/// Quarantine state vocabulary for context envelopes.
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum ContextQuarantineState {
259    /// Clear of quarantine.
260    Clear,
261    /// Quarantined.
262    Quarantined,
263    /// Derived from a quarantined ancestor.
264    DerivedFromQuarantined,
265    /// Posture unknown.
266    Unknown,
267}
268
269impl ContextQuarantineState {
270    /// Whether this state propagates quarantine into Cortex.
271    #[must_use]
272    pub const fn propagates_quarantine(self) -> bool {
273        matches!(
274            self,
275            Self::Quarantined | Self::DerivedFromQuarantined | Self::Unknown
276        )
277    }
278}
279
280/// Redaction state block on a context envelope.
281///
282/// The `blocks_critical_premise` field is intentionally accepted because
283/// some pai-axiom fixtures emit it as a redaction-policy extension. It is
284/// not authoritative on the Cortex receiver — Cortex consumes
285/// [`Self::status`] and [`Self::redaction_refs`].
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(deny_unknown_fields)]
288pub struct ContextRedactionState {
289    /// Redaction status value.
290    pub status: ContextRedactionStatus,
291    /// Redaction references.
292    pub redaction_refs: Vec<String>,
293    /// Optional extension marker (some pai-axiom fixtures emit this).
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub blocks_critical_premise: Option<bool>,
296}
297
298/// Redaction status vocabulary on context envelopes.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[serde(rename_all = "snake_case")]
301pub enum ContextRedactionStatus {
302    /// No redaction.
303    None,
304    /// Redacted.
305    Redacted,
306    /// Partially redacted.
307    PartiallyRedacted,
308    /// Redaction posture unknown.
309    Unknown,
310}
311
312/// Confidence block on context envelopes.
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314#[serde(deny_unknown_fields)]
315pub struct ContextConfidence {
316    /// Confidence value (numeric or discrete string).
317    pub value: ContextConfidenceValue,
318    /// Confidence scale.
319    pub scale: ContextConfidenceScale,
320}
321
322/// Confidence value — either numeric (0..=1) or a discrete word.
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324#[serde(untagged)]
325pub enum ContextConfidenceValue {
326    /// Numeric confidence in [0, 1].
327    Numeric(f64),
328    /// Discrete label such as "low", "medium", "high".
329    Discrete(String),
330}
331
332/// Confidence scale vocabulary.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum ContextConfidenceScale {
336    /// Discrete label set.
337    Discrete,
338    /// Numeric scale.
339    Numeric,
340}
341
342/// Policy result block on a context envelope.
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(deny_unknown_fields)]
345pub struct ContextPolicyResult {
346    /// Stable decision identifier.
347    pub decision_id: String,
348    /// Policy result value.
349    pub result: ContextPolicyResultValue,
350}
351
352/// Policy result vocabulary.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
354#[serde(rename_all = "snake_case")]
355pub enum ContextPolicyResultValue {
356    /// Policy denied.
357    Deny,
358    /// Policy requires review.
359    ReviewRequired,
360    /// Policy partial.
361    Partial,
362    /// Policy allowed.
363    Allow,
364}
365
366/// Allowed claim language vocabulary on a context envelope.
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
368#[serde(rename_all = "snake_case")]
369pub enum ContextAllowedClaimLanguage {
370    /// Candidate claims permitted.
371    Candidate,
372    /// Observed claims permitted.
373    Observed,
374    /// Validated claims permitted.
375    Validated,
376}
377
378/// Forbidden authority-bearing uses on a context envelope.
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
380#[serde(rename_all = "snake_case")]
381pub enum ContextForbiddenUse {
382    /// Execution authority forbidden.
383    ExecutionPermission,
384    /// Durable truth promotion forbidden.
385    DurableTruthPromotion,
386    /// Release claim forbidden.
387    ReleaseClaim,
388}
389
390/// Allowed use vocabulary on a context envelope.
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
392#[serde(rename_all = "snake_case")]
393pub enum ContextAllowedUse {
394    /// Render only.
395    RenderOnly,
396    /// Advisory reasoning.
397    AdvisoryReasoning,
398    /// Planning input.
399    PlanningInput,
400}
401
402/// Source anchor on a context envelope.
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404#[serde(deny_unknown_fields)]
405pub struct ContextSourceAnchor {
406    /// Stable source identifier.
407    pub source_id: String,
408    /// Source type vocabulary.
409    pub source_type: ContextSourceAnchorType,
410    /// Stable source reference.
411    pub r#ref: String,
412    /// `sha256:<hex>` hash.
413    pub hash: String,
414}
415
416/// Source anchor type vocabulary on context envelopes.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub enum ContextSourceAnchorType {
420    /// Cortex event.
421    Event,
422    /// Memory.
423    Memory,
424    /// Principle.
425    Principle,
426    /// Doctrine.
427    Doctrine,
428    /// Ledger entry.
429    Ledger,
430    /// Context pack.
431    ContextPack,
432    /// External anchor.
433    External,
434}
435
436// ---------------------------------------------------------------------------
437// pai-axiom execution trust envelope
438// ---------------------------------------------------------------------------
439
440/// pai-axiom execution trust envelope (`AXIOM_EXECUTION_TRUST.schema.json` v1).
441#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
442#[serde(deny_unknown_fields)]
443pub struct AxiomExecutionTrust {
444    /// Schema discriminator. Must equal [`AXIOM_EXECUTION_TRUST_SCHEMA`].
445    pub schema: String,
446    /// Schema version. Must equal [`TRUST_EXCHANGE_SCHEMA_VERSION`].
447    pub version: u16,
448    /// Optional stable reference (not in the raw schema, supplied by some
449    /// boundary tools).
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub axiom_execution_trust_ref: Option<String>,
452    /// Stable action identifier.
453    pub action_id: String,
454    /// Execution trust level.
455    pub execution_trust_level: ExecutionTrustLevel,
456    /// Repo trust block.
457    pub repo_trust: RepoTrust,
458    /// Actor attestation block.
459    pub actor_attestation: ActorAttestation,
460    /// Policy decision block.
461    pub policy_decision: ExecutionPolicyDecision,
462    /// Token scope block.
463    pub token_scope: TokenScope,
464    /// Tool provenance block.
465    pub tool_provenance: ExecutionToolProvenance,
466    /// Source anchors backing the execution receipt.
467    pub source_anchors: Vec<ExecutionSourceAnchor>,
468    /// Runtime mode label.
469    pub runtime_mode: String,
470    /// Evidence references.
471    #[serde(default)]
472    pub evidence_refs: Vec<String>,
473    /// Residual risk strings.
474    #[serde(default)]
475    pub residual_risk: Vec<String>,
476}
477
478/// Execution trust level vocabulary.
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
480#[serde(rename_all = "snake_case")]
481pub enum ExecutionTrustLevel {
482    /// Developer-only.
483    Dev,
484    /// Locally unsigned.
485    LocalUnsigned,
486    /// Signed locally.
487    SignedLocal,
488    /// Anchored externally.
489    ExternallyAnchored,
490    /// Authority-grade.
491    AuthorityGrade,
492}
493
494/// Repo trust block.
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(deny_unknown_fields)]
497pub struct RepoTrust {
498    /// Repo trust result.
499    pub result: RepoTrustResult,
500    /// Stable evaluation reference.
501    pub evaluation_ref: String,
502}
503
504/// Repo trust result vocabulary.
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507pub enum RepoTrustResult {
508    /// Repo trusted.
509    Trusted,
510    /// Repo trust partial.
511    Partial,
512    /// Repo untrusted.
513    Untrusted,
514}
515
516/// Actor attestation block.
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(deny_unknown_fields)]
519pub struct ActorAttestation {
520    /// Stable actor identity reference.
521    pub identity_ref: String,
522    /// Stable attestation reference.
523    pub attestation_ref: String,
524    /// Operator approval reference.
525    pub operator_approval_ref: String,
526    /// `sha256:<hex>` operator approval hash.
527    pub operator_approval_hash: String,
528}
529
530/// Policy decision block on execution receipts.
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(deny_unknown_fields)]
533pub struct ExecutionPolicyDecision {
534    /// Stable decision identifier.
535    pub decision_id: String,
536    /// Policy decision value.
537    pub result: ExecutionPolicyResult,
538}
539
540/// Policy decision result vocabulary on execution receipts.
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
542#[serde(rename_all = "snake_case")]
543pub enum ExecutionPolicyResult {
544    /// Policy denied.
545    Deny,
546    /// Policy review required.
547    ReviewRequired,
548    /// Policy partial.
549    Partial,
550    /// Policy allowed.
551    Allow,
552}
553
554/// Token scope block.
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(deny_unknown_fields)]
557pub struct TokenScope {
558    /// Stable token identifier.
559    pub token_id: String,
560    /// Token audience.
561    pub audience: String,
562    /// `sha256:<hex>` scope hash.
563    pub scope_hash: String,
564    /// `sha256:<hex>` operation hash.
565    pub operation_hash: String,
566    /// `sha256:<hex>` manifest hash.
567    pub manifest_hash: String,
568    /// `sha256:<hex>` request hash.
569    pub request_hash: String,
570    /// RFC 3339 token expiry.
571    pub expires_at: DateTime<Utc>,
572    /// Token revocation result.
573    pub revocation_result: TokenRevocationResult,
574}
575
576/// Token revocation result vocabulary.
577///
578/// `Inactive` is accepted as an extension state observed in some pai-axiom
579/// fixtures (`inactive-token-axiom-execution-trust.json`); it is treated as
580/// equivalent to `Revoked` for admission gating.
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
582#[serde(rename_all = "snake_case")]
583pub enum TokenRevocationResult {
584    /// Token active.
585    Active,
586    /// Token revoked.
587    Revoked,
588    /// Token inactive (fixture extension).
589    Inactive,
590    /// Token state unknown.
591    Unknown,
592}
593
594impl TokenRevocationResult {
595    /// Whether this revocation state must reject admission.
596    #[must_use]
597    pub const fn must_reject(self) -> bool {
598        matches!(self, Self::Revoked | Self::Inactive)
599    }
600}
601
602/// Tool provenance block on execution receipts.
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
604#[serde(deny_unknown_fields)]
605pub struct ExecutionToolProvenance {
606    /// Stable tool identifier.
607    pub tool_id: String,
608    /// Tool version string.
609    pub tool_version: String,
610    /// Command reference.
611    pub command_ref: String,
612    /// 40-hex source commit.
613    pub source_commit: String,
614    /// Dependency lock reference.
615    pub dependency_lock_ref: String,
616}
617
618/// Source anchor on an execution receipt.
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
620#[serde(deny_unknown_fields)]
621pub struct ExecutionSourceAnchor {
622    /// Stable source identifier.
623    pub source_id: String,
624    /// Source type vocabulary on execution receipts.
625    pub source_type: ExecutionSourceAnchorType,
626    /// Stable source reference.
627    pub r#ref: String,
628    /// `sha256:<hex>` hash.
629    pub hash: String,
630}
631
632/// Source anchor type vocabulary on execution receipts.
633#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
634#[serde(rename_all = "snake_case")]
635pub enum ExecutionSourceAnchorType {
636    /// Command anchor.
637    Command,
638    /// File anchor.
639    File,
640    /// Test anchor.
641    Test,
642    /// Ledger anchor.
643    Ledger,
644    /// Runtime anchor.
645    Runtime,
646    /// Operator approval anchor.
647    Approval,
648}
649
650// ---------------------------------------------------------------------------
651// Authority feedback loop envelope
652// ---------------------------------------------------------------------------
653
654/// Authority feedback loop record (`AUTHORITY_FEEDBACK_LOOP.schema.json` v1).
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
656#[serde(deny_unknown_fields)]
657pub struct AuthorityFeedbackLoop {
658    /// Schema discriminator.
659    pub schema: String,
660    /// Schema version.
661    pub version: u16,
662    /// Optional stable receiver-side reference.
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub authority_feedback_loop_ref: Option<String>,
665    /// Stable loop identifier.
666    pub loop_id: String,
667    /// Loop start timestamp.
668    pub started_at: DateTime<Utc>,
669    /// Initiating context block.
670    pub initiating_context: FeedbackInitiatingContext,
671    /// AXIOM action block.
672    pub axiom_action: FeedbackAxiomAction,
673    /// Returned artifact descriptors.
674    pub returned_artifacts: Vec<FeedbackReturnedArtifact>,
675    /// Amplification risk level.
676    pub amplification_risk: AmplificationRisk,
677    /// Independent evidence references.
678    #[serde(default)]
679    pub independent_evidence_refs: Vec<String>,
680    /// External grounding references.
681    #[serde(default)]
682    pub external_grounding_refs: Vec<String>,
683    /// Contradiction scan reference.
684    pub contradiction_scan_ref: String,
685    /// Loop quarantine state.
686    pub quarantine_state: ContextQuarantineState,
687    /// Confidence ceiling permitted on loop output.
688    pub confidence_ceiling: ConfidenceCeiling,
689    /// Same-loop promotion permission. Must be `false`.
690    pub same_loop_promotion_allowed: bool,
691    /// Authority claims block (optional only in the strictest pai-axiom
692    /// negative fixtures; required in valid emission).
693    pub authority_claims: FeedbackAuthorityClaims,
694    /// Target-domain validation block.
695    pub target_domain_validation: TargetDomainValidation,
696    /// Residual risk strings.
697    #[serde(default)]
698    pub residual_risk: Vec<String>,
699}
700
701/// Initiating-context block on a feedback loop record.
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
703#[serde(deny_unknown_fields)]
704pub struct FeedbackInitiatingContext {
705    /// Stable initiating context identifier.
706    pub context_id: String,
707    /// Reference to the matching Cortex context trust envelope.
708    pub cortex_context_trust_ref: String,
709}
710
711/// AXIOM action block on a feedback loop record.
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
713#[serde(deny_unknown_fields)]
714pub struct FeedbackAxiomAction {
715    /// Stable action identifier.
716    pub action_id: String,
717    /// Reference to the matching pai-axiom execution trust envelope.
718    pub axiom_execution_trust_ref: String,
719}
720
721/// Returned-artifact descriptor on a feedback loop record.
722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723#[serde(deny_unknown_fields)]
724pub struct FeedbackReturnedArtifact {
725    /// Stable artifact identifier.
726    pub artifact_id: String,
727    /// Lineage reference.
728    pub lineage_ref: String,
729    /// Lifecycle state of the artifact.
730    pub lifecycle_state: ArtifactLifecycleState,
731    /// Reproducibility level of the artifact.
732    pub reproducibility_level: ReproducibilityLevel,
733}
734
735/// Lifecycle state vocabulary for returned artifacts.
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
737#[serde(rename_all = "snake_case")]
738pub enum ArtifactLifecycleState {
739    /// Candidate only.
740    Candidate,
741    /// Observed.
742    Observed,
743    /// Validated.
744    Validated,
745    /// Promoted.
746    Promoted,
747    /// Stale.
748    Stale,
749    /// Quarantined.
750    Quarantined,
751}
752
753/// Reproducibility level vocabulary.
754#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
755#[serde(rename_all = "kebab-case")]
756pub enum ReproducibilityLevel {
757    /// Deterministic.
758    Deterministic,
759    /// Bounded nondeterministic.
760    BoundedNondeterministic,
761    /// Observational.
762    Observational,
763    /// Non-reproducible.
764    NonReproducible,
765}
766
767impl Serialize for ReproducibilityLevel {
768    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
769        serializer.serialize_str(self.wire_string())
770    }
771}
772
773impl ReproducibilityLevel {
774    /// Stable wire string.
775    #[must_use]
776    pub const fn wire_string(self) -> &'static str {
777        match self {
778            Self::Deterministic => "deterministic",
779            Self::BoundedNondeterministic => "bounded-nondeterministic",
780            Self::Observational => "observational",
781            Self::NonReproducible => "non-reproducible",
782        }
783    }
784}
785
786/// Amplification risk vocabulary.
787#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
788#[serde(rename_all = "snake_case")]
789pub enum AmplificationRisk {
790    /// Low.
791    Low,
792    /// Medium.
793    Medium,
794    /// High.
795    High,
796    /// Critical.
797    Critical,
798}
799
800/// Confidence ceiling vocabulary on the feedback loop record.
801#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
802#[serde(rename_all = "snake_case")]
803pub enum ConfidenceCeiling {
804    /// Untrusted.
805    Untrusted,
806    /// Advisory.
807    Advisory,
808    /// Observed.
809    Observed,
810    /// Validated.
811    Validated,
812}
813
814/// Authority claims block.
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
816#[serde(deny_unknown_fields)]
817pub struct FeedbackAuthorityClaims {
818    /// Durable truth promotion claim status.
819    pub durable_truth_promotion: AuthorityClaimStatus,
820    /// Full execution authority claim status.
821    pub full_execution_authority: AuthorityClaimStatus,
822    /// Whether review is required.
823    pub review_required: bool,
824}
825
826/// Authority claim status vocabulary.
827#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
828#[serde(rename_all = "snake_case")]
829pub enum AuthorityClaimStatus {
830    /// Denied.
831    Denied,
832    /// Review required.
833    ReviewRequired,
834    /// Eligible after independent validation.
835    EligibleAfterIndependentValidation,
836}
837
838impl AuthorityClaimStatus {
839    /// Whether the producer claimed authority-grade promotion. Cortex
840    /// rejects this regardless of any other contributor.
841    #[must_use]
842    pub const fn claims_authority(self) -> bool {
843        matches!(self, Self::EligibleAfterIndependentValidation)
844    }
845}
846
847/// Target-domain validation block.
848#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
849#[serde(deny_unknown_fields)]
850pub struct TargetDomainValidation {
851    /// Whether validation is required (must be `true`).
852    pub required: bool,
853    /// Reference to the independent validation, if produced.
854    pub independent_validation_ref: Option<String>,
855    /// Validation result.
856    pub result: TargetDomainValidationResult,
857}
858
859/// Target-domain validation result vocabulary.
860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
861#[serde(rename_all = "snake_case")]
862pub enum TargetDomainValidationResult {
863    /// Pending validation.
864    Pending,
865    /// Validation passed.
866    Pass,
867    /// Validation failed.
868    Fail,
869    /// Validation partial.
870    Partial,
871}
872
873// ---------------------------------------------------------------------------
874// Named quarantine outputs (ADR 0042 §7)
875// ---------------------------------------------------------------------------
876
877/// Per-source quarantine output as required by ADR 0042 §7.
878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
879#[serde(deny_unknown_fields)]
880pub struct QuarantineOutput {
881    /// Stable invariant name explaining why this output is quarantined.
882    pub invariant: String,
883    /// Operator-facing reason.
884    pub reason: String,
885    /// Optional reference back to the source artifact.
886    pub source_ref: Option<String>,
887}
888
889impl QuarantineOutput {
890    /// Construct a quarantine output.
891    #[must_use]
892    pub fn new(invariant: impl Into<String>, reason: impl Into<String>) -> Self {
893        Self {
894            invariant: invariant.into(),
895            reason: reason.into(),
896            source_ref: None,
897        }
898    }
899
900    /// Attach a stable source reference.
901    #[must_use]
902    pub fn with_source_ref(mut self, source_ref: impl Into<String>) -> Self {
903        self.source_ref = Some(source_ref.into());
904        self
905    }
906}
907
908/// Named per-source quarantine outputs, mirroring ADR 0042 §7.
909#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
910#[serde(deny_unknown_fields)]
911pub struct NamedQuarantineOutputs {
912    /// Source-context quarantine (Cortex side).
913    #[serde(default, skip_serializing_if = "Option::is_none")]
914    pub source_context: Option<QuarantineOutput>,
915    /// Token revocation quarantine.
916    #[serde(default, skip_serializing_if = "Option::is_none")]
917    pub token_revocation: Option<QuarantineOutput>,
918    /// Repo trust quarantine.
919    #[serde(default, skip_serializing_if = "Option::is_none")]
920    pub repo_trust: Option<QuarantineOutput>,
921    /// Policy denial quarantine.
922    #[serde(default, skip_serializing_if = "Option::is_none")]
923    pub policy_denial: Option<QuarantineOutput>,
924    /// Target-domain validation quarantine.
925    #[serde(default, skip_serializing_if = "Option::is_none")]
926    pub target_validation: Option<QuarantineOutput>,
927    /// Derived artifact quarantine.
928    #[serde(default, skip_serializing_if = "Option::is_none")]
929    pub derived_artifact: Option<QuarantineOutput>,
930    /// Contradiction-state quarantine.
931    #[serde(default, skip_serializing_if = "Option::is_none")]
932    pub contradiction: Option<QuarantineOutput>,
933}
934
935impl NamedQuarantineOutputs {
936    /// Whether any of the seven named outputs is populated.
937    #[must_use]
938    pub fn any_present(&self) -> bool {
939        self.source_context.is_some()
940            || self.token_revocation.is_some()
941            || self.repo_trust.is_some()
942            || self.policy_denial.is_some()
943            || self.target_validation.is_some()
944            || self.derived_artifact.is_some()
945            || self.contradiction.is_some()
946    }
947
948    /// Iterate over every populated stable invariant name.
949    pub fn invariants(&self) -> impl Iterator<Item = &str> {
950        [
951            self.source_context.as_ref(),
952            self.token_revocation.as_ref(),
953            self.repo_trust.as_ref(),
954            self.policy_denial.as_ref(),
955            self.target_validation.as_ref(),
956            self.derived_artifact.as_ref(),
957            self.contradiction.as_ref(),
958        ]
959        .into_iter()
960        .flatten()
961        .map(|q| q.invariant.as_str())
962    }
963}
964
965// ---------------------------------------------------------------------------
966// Field validation errors (stable invariant names)
967// ---------------------------------------------------------------------------
968
969/// Stable field-level invariant failure.
970///
971/// `invariant` is the dot-pathed stable name (e.g.
972/// `axiom_execution_trust.token_scope.audience.missing`) consumed by the
973/// admission gate and by external pai-axiom acceptance tests.
974#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
975#[serde(deny_unknown_fields)]
976pub struct TrustExchangeFieldError {
977    /// Stable dot-pathed invariant name.
978    pub invariant: String,
979    /// Operator-facing reason.
980    pub reason: String,
981}
982
983impl TrustExchangeFieldError {
984    /// Construct a field-level error.
985    #[must_use]
986    pub fn new(invariant: impl Into<String>, reason: impl Into<String>) -> Self {
987        Self {
988            invariant: invariant.into(),
989            reason: reason.into(),
990        }
991    }
992}
993
994/// Result type for field-level validation across the trust exchange envelopes.
995pub type TrustExchangeValidation = Result<(), Vec<TrustExchangeFieldError>>;
996
997// ---------------------------------------------------------------------------
998// Wrappers — pai-axiom fixtures wrap the inner envelope in a single key
999// ---------------------------------------------------------------------------
1000
1001/// Envelope shape matching pai-axiom's wrapped fixtures
1002/// (`{ "cortex_context_trust": { ... } }`).
1003#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1004#[serde(deny_unknown_fields)]
1005pub struct CortexContextTrustEnvelope {
1006    /// Wrapped cortex context trust object.
1007    pub cortex_context_trust: CortexContextTrust,
1008}
1009
1010/// Envelope shape matching pai-axiom's wrapped fixtures
1011/// (`{ "axiom_execution_trust": { ... } }`).
1012#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1013#[serde(deny_unknown_fields)]
1014pub struct AxiomExecutionTrustEnvelope {
1015    /// Wrapped axiom execution trust object.
1016    pub axiom_execution_trust: AxiomExecutionTrust,
1017}
1018
1019// ---------------------------------------------------------------------------
1020// Parse helpers — accept wrapped or unwrapped form
1021// ---------------------------------------------------------------------------
1022
1023/// Parse a Cortex context trust envelope from either the wrapped pai-axiom
1024/// fixture shape `{ "cortex_context_trust": { ... } }` or the unwrapped
1025/// schema-bare object form.
1026pub fn parse_cortex_context_trust(
1027    input: &str,
1028) -> Result<CortexContextTrust, TrustExchangeFieldError> {
1029    let value: serde_json::Value = serde_json::from_str(input).map_err(|err| {
1030        TrustExchangeFieldError::new(
1031            "cortex_context_trust.envelope.invalid_json",
1032            format!("invalid JSON: {err}"),
1033        )
1034    })?;
1035    let inner = match value.get("cortex_context_trust") {
1036        Some(inner) => inner.clone(),
1037        None => value,
1038    };
1039    serde_json::from_value(inner).map_err(|err| {
1040        TrustExchangeFieldError::new(
1041            "cortex_context_trust.envelope.schema_drift",
1042            format!("envelope failed schema check: {err}"),
1043        )
1044    })
1045}
1046
1047/// Parse a pai-axiom execution trust envelope from either the wrapped form
1048/// `{ "axiom_execution_trust": { ... } }` or the bare object form.
1049pub fn parse_axiom_execution_trust(
1050    input: &str,
1051) -> Result<AxiomExecutionTrust, TrustExchangeFieldError> {
1052    let value: serde_json::Value = serde_json::from_str(input).map_err(|err| {
1053        TrustExchangeFieldError::new(
1054            "axiom_execution_trust.envelope.invalid_json",
1055            format!("invalid JSON: {err}"),
1056        )
1057    })?;
1058    let inner = match value.get("axiom_execution_trust") {
1059        Some(inner) => inner.clone(),
1060        None => value,
1061    };
1062    serde_json::from_value(inner).map_err(|err| {
1063        TrustExchangeFieldError::new(
1064            "axiom_execution_trust.envelope.schema_drift",
1065            format!("envelope failed schema check: {err}"),
1066        )
1067    })
1068}
1069
1070/// Parse an authority feedback loop record. The feedback loop fixture is
1071/// not wrapped in pai-axiom's emission, so we accept the bare object only.
1072pub fn parse_authority_feedback_loop(
1073    input: &str,
1074) -> Result<AuthorityFeedbackLoop, TrustExchangeFieldError> {
1075    serde_json::from_str(input).map_err(|err| {
1076        TrustExchangeFieldError::new(
1077            "authority_feedback_loop.envelope.schema_drift",
1078            format!("envelope failed schema check: {err}"),
1079        )
1080    })
1081}
1082
1083// ---------------------------------------------------------------------------
1084// Receiver-side axiom source-commit freshness gate
1085// ---------------------------------------------------------------------------
1086//
1087// The first live Cortex ↔ axiom admission exchange against axiom packet SHA
1088// `9a15d281` (record at `docs/transcripts/AXIOM_LIVE_EXCHANGE_2026-05-13/
1089// SUMMARY.md`) surfaced one real receiver-side gap: the
1090// `stale-pai-axiom-sha` fixture was admitted by Cortex despite the packet's
1091// `axiom_execution_trust.tool_provenance.source_commit` pointing at a stale
1092// axiom SHA. The axiom team's fixture expected `freshness_refusal` keyed on
1093// `axiom_execution_trust.tool_provenance.source_commit.stale`; Cortex
1094// returned `decision=admit_candidate`, exit 0.
1095//
1096// This module section is the receiver-side closure: a small, additive
1097// freshness gate Cortex callers can run alongside the existing structural
1098// validation. The accept set is owned at the Cortex side (ADR 0042
1099// acceptance pin + subsequent known-good axiom syncs); the env-var override
1100// lets operators rotate the pin without a re-deploy.
1101
1102/// Stable invariant emitted when the axiom-execution-trust envelope's
1103/// `tool_provenance.source_commit` is not on the receiver-side acceptance
1104/// list. Downstream tooling (operator scripts, axiom-side test harnesses)
1105/// greps on this string.
1106pub const AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT: &str =
1107    "axiom_execution_trust.tool_provenance.source_commit.stale";
1108
1109/// Environment variable that, when set, REPLACES the default
1110/// [`DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS`] accept-list with a
1111/// comma-separated set of operator-supplied 40-hex commit SHAs.
1112///
1113/// Setting this to the empty string is equivalent to setting it to an empty
1114/// list — the freshness gate will then refuse every source_commit. Operators
1115/// using this surface MUST supply at least one SHA.
1116///
1117/// Whitespace around each SHA is trimmed; the value is normalised to
1118/// lowercase before comparison.
1119pub const CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV: &str = "CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS";
1120
1121/// Default Cortex-side accept-list for the
1122/// `axiom_execution_trust.tool_provenance.source_commit` freshness gate.
1123///
1124/// Three production axiom SHAs are listed by virtue of being either the ADR
1125/// 0042 acceptance pin or a subsequent axiom-side sync the Cortex team has
1126/// already accepted as known-good:
1127///
1128/// - `44b9a25dfbe5a93e64120f53b65730828d1af91c` — ADR 0042 acceptance pin.
1129/// - `062d0d42ac5ef300fa3e04ef5b49b14864babcdd` — axiom team's alignment
1130///   acknowledgement to Cortex receiver-readiness at `0e67a32`
1131///   (2026-05-13).
1132/// - `9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a` — axiom team's V2 live-
1133///   exchange payload corpus delivery (2026-05-13).
1134///
1135/// One test-fixture placeholder SHA is also listed so the Cortex-side
1136/// fixtures (which use `aaaa…aaaa` deliberately to flag the value as a
1137/// test placeholder) continue to admit. The fixture SHA is well-known and
1138/// carries no real authority — including it on the accept-list has no
1139/// security impact because any payload reaching the freshness gate has
1140/// already passed the upstream structural validation that an attacker
1141/// cannot trivially forge.
1142///
1143/// To rotate without re-deploying, set
1144/// [`CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV`].
1145pub const DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS: &[&str] = &[
1146    // ADR 0042 acceptance pin.
1147    "44b9a25dfbe5a93e64120f53b65730828d1af91c",
1148    // Axiom alignment ack to Cortex 0e67a32 readiness (2026-05-13).
1149    "062d0d42ac5ef300fa3e04ef5b49b14864babcdd",
1150    // Axiom V2 live-exchange payload corpus delivery (2026-05-13).
1151    "9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a",
1152    // Test-fixture placeholder; see doc-comment above for the rationale.
1153    "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1154];
1155
1156/// Resolve the active accept-list for the
1157/// [`AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT`] freshness gate.
1158///
1159/// Resolution order:
1160///
1161/// 1. If [`CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV`] is set, parse it as a
1162///    comma-separated list of 40-hex SHAs (whitespace trimmed, lowercase
1163///    normalised, empty entries discarded).
1164/// 2. Otherwise, return [`DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS`] as a
1165///    `Vec<String>`.
1166///
1167/// Returned SHAs are lower-cased; callers using
1168/// [`is_axiom_source_commit_fresh`] do not need to lower-case the candidate
1169/// value separately.
1170#[must_use]
1171pub fn accepted_axiom_source_commits() -> Vec<String> {
1172    if let Ok(raw) = std::env::var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV) {
1173        return raw
1174            .split(',')
1175            .map(|piece| piece.trim().to_ascii_lowercase())
1176            .filter(|piece| !piece.is_empty())
1177            .collect();
1178    }
1179    DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
1180        .iter()
1181        .map(|sha| (*sha).to_string())
1182        .collect()
1183}
1184
1185/// Check whether a candidate `source_commit` is on the supplied
1186/// receiver-side accept-list.
1187///
1188/// Comparison is case-insensitive on the candidate; the accept-list is
1189/// assumed to be lower-cased (the canonical form returned by
1190/// [`accepted_axiom_source_commits`]). The candidate must also pass the
1191/// 40-character lowercase-hex structural check
1192/// (`tool_provenance.source_commit.invalid_format`) before reaching this
1193/// gate; this function does NOT re-validate format.
1194#[must_use]
1195pub fn is_axiom_source_commit_fresh(candidate: &str, accepted: &[String]) -> bool {
1196    let lowered = candidate.to_ascii_lowercase();
1197    accepted.iter().any(|sha| sha == &lowered)
1198}
1199
1200// ---------------------------------------------------------------------------
1201// Field validators — stable invariant names
1202// ---------------------------------------------------------------------------
1203
1204const SHA256_PATTERN_PREFIX: &str = "sha256:";
1205const SHA256_HEX_LEN: usize = 64;
1206
1207fn is_sha256_hash(value: &str) -> bool {
1208    if let Some(hex) = value.strip_prefix(SHA256_PATTERN_PREFIX) {
1209        hex.len() == SHA256_HEX_LEN
1210            && hex
1211                .chars()
1212                .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
1213    } else {
1214        false
1215    }
1216}
1217
1218fn is_commit_sha(value: &str) -> bool {
1219    value.len() == 40
1220        && value
1221            .chars()
1222            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
1223}
1224
1225fn push(errors: &mut Vec<TrustExchangeFieldError>, invariant: &str, reason: impl Into<String>) {
1226    errors.push(TrustExchangeFieldError::new(invariant, reason));
1227}
1228
1229fn require_nonblank(
1230    errors: &mut Vec<TrustExchangeFieldError>,
1231    value: &str,
1232    invariant: &str,
1233    reason: &str,
1234) {
1235    if value.trim().is_empty() {
1236        push(errors, invariant, reason);
1237    }
1238}
1239
1240impl CortexContextTrust {
1241    /// Validate the decomposed fields required for Cortex admission.
1242    ///
1243    /// Returns a list of stable invariant failures. The `compatibility_trust_label`
1244    /// is always treated as display-only and never satisfies a required field.
1245    pub fn validate(&self) -> TrustExchangeValidation {
1246        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();
1247
1248        if self.schema != CORTEX_CONTEXT_TRUST_SCHEMA {
1249            push(
1250                &mut errors,
1251                "cortex_context_trust.schema.mismatch",
1252                format!(
1253                    "schema must be `{CORTEX_CONTEXT_TRUST_SCHEMA}`, got `{}`",
1254                    self.schema
1255                ),
1256            );
1257        }
1258        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
1259            push(
1260                &mut errors,
1261                "cortex_context_trust.version.mismatch",
1262                format!(
1263                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
1264                    self.version
1265                ),
1266            );
1267        }
1268
1269        require_nonblank(
1270            &mut errors,
1271            &self.context_id,
1272            "cortex_context_trust.context_id.missing",
1273            "context_id must be a non-empty string",
1274        );
1275
1276        // proof_state.* (each subfield contributes its own invariant)
1277        if self
1278            .proof_state
1279            .failing_edges
1280            .iter()
1281            .any(|e| e.trim().is_empty())
1282        {
1283            push(
1284                &mut errors,
1285                "cortex_context_trust.proof_state.failing_edges.blank_entry",
1286                "proof_state.failing_edges entries must not be blank",
1287            );
1288        }
1289        if self
1290            .proof_state
1291            .proof_refs
1292            .iter()
1293            .any(|e| e.trim().is_empty())
1294        {
1295            push(
1296                &mut errors,
1297                "cortex_context_trust.proof_state.proof_refs.blank_entry",
1298                "proof_state.proof_refs entries must not be blank",
1299            );
1300        }
1301        if matches!(
1302            self.proof_state.state,
1303            ContextProofStateValue::Missing | ContextProofStateValue::Failed
1304        ) {
1305            push(
1306                &mut errors,
1307                "cortex_context_trust.proof_state.state.unusable",
1308                "proof_state.state must be `closed` or `partial` for Cortex consumption",
1309            );
1310        }
1311
1312        // semantic_trust.*
1313        require_nonblank(
1314            &mut errors,
1315            &self.semantic_trust.weighting_basis,
1316            "cortex_context_trust.semantic_trust.weighting_basis.missing",
1317            "semantic_trust.weighting_basis must be a non-empty string",
1318        );
1319        if !(0.0..=1.0).contains(&self.semantic_trust.trust_weight) {
1320            push(
1321                &mut errors,
1322                "cortex_context_trust.semantic_trust.trust_weight.out_of_range",
1323                "semantic_trust.trust_weight must be in [0, 1]",
1324            );
1325        }
1326        if matches!(
1327            self.semantic_trust.provenance_class,
1328            ContextProvenanceClass::Unknown
1329        ) {
1330            push(
1331                &mut errors,
1332                "cortex_context_trust.semantic_trust.provenance_class.unknown",
1333                "semantic_trust.provenance_class must be a known class",
1334            );
1335        }
1336
1337        // redaction_state.*
1338        if matches!(self.redaction_state.status, ContextRedactionStatus::Unknown) {
1339            push(
1340                &mut errors,
1341                "cortex_context_trust.redaction_state.status.unknown",
1342                "redaction_state.status must be a known status",
1343            );
1344        }
1345        if self
1346            .redaction_state
1347            .redaction_refs
1348            .iter()
1349            .any(|e| e.trim().is_empty())
1350        {
1351            push(
1352                &mut errors,
1353                "cortex_context_trust.redaction_state.redaction_refs.blank_entry",
1354                "redaction_state.redaction_refs entries must not be blank",
1355            );
1356        }
1357
1358        // policy_result.*
1359        require_nonblank(
1360            &mut errors,
1361            &self.policy_result.decision_id,
1362            "cortex_context_trust.policy_result.decision_id.missing",
1363            "policy_result.decision_id must be a non-empty string",
1364        );
1365
1366        // allowed_claim_language / forbidden_uses minimum coverage
1367        if self.allowed_claim_language.len() < 3 {
1368            push(
1369                &mut errors,
1370                "cortex_context_trust.allowed_claim_language.insufficient_coverage",
1371                "allowed_claim_language must include candidate, observed, and validated",
1372            );
1373        }
1374        if self.forbidden_uses.len() < 3 {
1375            push(
1376                &mut errors,
1377                "cortex_context_trust.forbidden_uses.insufficient_coverage",
1378                "forbidden_uses must include execution_permission, durable_truth_promotion, release_claim",
1379            );
1380        }
1381
1382        // source_anchors.*
1383        if self.source_anchors.is_empty() {
1384            push(
1385                &mut errors,
1386                "cortex_context_trust.source_anchors.missing",
1387                "source_anchors must contain at least one anchor",
1388            );
1389        }
1390        for anchor in &self.source_anchors {
1391            require_nonblank(
1392                &mut errors,
1393                &anchor.source_id,
1394                "cortex_context_trust.source_anchors.source_id.missing",
1395                "source_anchors[].source_id must be a non-empty string",
1396            );
1397            require_nonblank(
1398                &mut errors,
1399                &anchor.r#ref,
1400                "cortex_context_trust.source_anchors.ref.missing",
1401                "source_anchors[].ref must be a non-empty string",
1402            );
1403            if !is_sha256_hash(&anchor.hash) {
1404                push(
1405                    &mut errors,
1406                    "cortex_context_trust.source_anchors.hash.invalid_format",
1407                    "source_anchors[].hash must match sha256:<64 lowercase hex>",
1408                );
1409            }
1410        }
1411
1412        if errors.is_empty() {
1413            Ok(())
1414        } else {
1415            Err(errors)
1416        }
1417    }
1418}
1419
1420impl AxiomExecutionTrust {
1421    /// Validate the decomposed fields required for Cortex admission of a
1422    /// pai-axiom execution receipt.
1423    pub fn validate(&self) -> TrustExchangeValidation {
1424        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();
1425
1426        if self.schema != AXIOM_EXECUTION_TRUST_SCHEMA {
1427            push(
1428                &mut errors,
1429                "axiom_execution_trust.schema.mismatch",
1430                format!(
1431                    "schema must be `{AXIOM_EXECUTION_TRUST_SCHEMA}`, got `{}`",
1432                    self.schema
1433                ),
1434            );
1435        }
1436        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
1437            push(
1438                &mut errors,
1439                "axiom_execution_trust.version.mismatch",
1440                format!(
1441                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
1442                    self.version
1443                ),
1444            );
1445        }
1446
1447        require_nonblank(
1448            &mut errors,
1449            &self.action_id,
1450            "axiom_execution_trust.action_id.missing",
1451            "action_id must be a non-empty string",
1452        );
1453
1454        // repo_trust.*
1455        require_nonblank(
1456            &mut errors,
1457            &self.repo_trust.evaluation_ref,
1458            "axiom_execution_trust.repo_trust.evaluation_ref.missing",
1459            "repo_trust.evaluation_ref must be a non-empty string",
1460        );
1461
1462        // actor_attestation.*
1463        require_nonblank(
1464            &mut errors,
1465            &self.actor_attestation.identity_ref,
1466            "axiom_execution_trust.actor_attestation.identity_ref.missing",
1467            "actor_attestation.identity_ref must be a non-empty string",
1468        );
1469        require_nonblank(
1470            &mut errors,
1471            &self.actor_attestation.attestation_ref,
1472            "axiom_execution_trust.actor_attestation.attestation_ref.missing",
1473            "actor_attestation.attestation_ref must be a non-empty string",
1474        );
1475        require_nonblank(
1476            &mut errors,
1477            &self.actor_attestation.operator_approval_ref,
1478            "axiom_execution_trust.actor_attestation.operator_approval_ref.missing",
1479            "actor_attestation.operator_approval_ref must be a non-empty string",
1480        );
1481        if !is_sha256_hash(&self.actor_attestation.operator_approval_hash) {
1482            push(
1483                &mut errors,
1484                "axiom_execution_trust.actor_attestation.operator_approval_hash.invalid_format",
1485                "actor_attestation.operator_approval_hash must match sha256:<64 lowercase hex>",
1486            );
1487        }
1488
1489        // policy_decision.*
1490        require_nonblank(
1491            &mut errors,
1492            &self.policy_decision.decision_id,
1493            "axiom_execution_trust.policy_decision.decision_id.missing",
1494            "policy_decision.decision_id must be a non-empty string",
1495        );
1496
1497        // token_scope.*
1498        require_nonblank(
1499            &mut errors,
1500            &self.token_scope.token_id,
1501            "axiom_execution_trust.token_scope.token_id.missing",
1502            "token_scope.token_id must be a non-empty string",
1503        );
1504        require_nonblank(
1505            &mut errors,
1506            &self.token_scope.audience,
1507            "axiom_execution_trust.token_scope.audience.missing",
1508            "token_scope.audience must be a non-empty string",
1509        );
1510        for (field, value, invariant) in [
1511            (
1512                "scope_hash",
1513                &self.token_scope.scope_hash,
1514                "axiom_execution_trust.token_scope.scope_hash.invalid_format",
1515            ),
1516            (
1517                "operation_hash",
1518                &self.token_scope.operation_hash,
1519                "axiom_execution_trust.token_scope.operation_hash.invalid_format",
1520            ),
1521            (
1522                "manifest_hash",
1523                &self.token_scope.manifest_hash,
1524                "axiom_execution_trust.token_scope.manifest_hash.invalid_format",
1525            ),
1526            (
1527                "request_hash",
1528                &self.token_scope.request_hash,
1529                "axiom_execution_trust.token_scope.request_hash.invalid_format",
1530            ),
1531        ] {
1532            if !is_sha256_hash(value) {
1533                push(
1534                    &mut errors,
1535                    invariant,
1536                    format!("token_scope.{field} must match sha256:<64 lowercase hex>"),
1537                );
1538            }
1539        }
1540
1541        // tool_provenance.*
1542        require_nonblank(
1543            &mut errors,
1544            &self.tool_provenance.tool_id,
1545            "axiom_execution_trust.tool_provenance.tool_id.missing",
1546            "tool_provenance.tool_id must be a non-empty string",
1547        );
1548        require_nonblank(
1549            &mut errors,
1550            &self.tool_provenance.tool_version,
1551            "axiom_execution_trust.tool_provenance.tool_version.missing",
1552            "tool_provenance.tool_version must be a non-empty string",
1553        );
1554        require_nonblank(
1555            &mut errors,
1556            &self.tool_provenance.command_ref,
1557            "axiom_execution_trust.tool_provenance.command_ref.missing",
1558            "tool_provenance.command_ref must be a non-empty string",
1559        );
1560        if !is_commit_sha(&self.tool_provenance.source_commit) {
1561            push(
1562                &mut errors,
1563                "axiom_execution_trust.tool_provenance.source_commit.invalid_format",
1564                "tool_provenance.source_commit must be a 40-character lowercase hex SHA",
1565            );
1566        }
1567        require_nonblank(
1568            &mut errors,
1569            &self.tool_provenance.dependency_lock_ref,
1570            "axiom_execution_trust.tool_provenance.dependency_lock_ref.missing",
1571            "tool_provenance.dependency_lock_ref must be a non-empty string",
1572        );
1573
1574        // source_anchors.*
1575        if self.source_anchors.is_empty() {
1576            push(
1577                &mut errors,
1578                "axiom_execution_trust.source_anchors.missing",
1579                "source_anchors must contain at least one anchor",
1580            );
1581        }
1582        for anchor in &self.source_anchors {
1583            require_nonblank(
1584                &mut errors,
1585                &anchor.source_id,
1586                "axiom_execution_trust.source_anchors.source_id.missing",
1587                "source_anchors[].source_id must be a non-empty string",
1588            );
1589            require_nonblank(
1590                &mut errors,
1591                &anchor.r#ref,
1592                "axiom_execution_trust.source_anchors.ref.missing",
1593                "source_anchors[].ref must be a non-empty string",
1594            );
1595            if !is_sha256_hash(&anchor.hash) {
1596                push(
1597                    &mut errors,
1598                    "axiom_execution_trust.source_anchors.hash.invalid_format",
1599                    "source_anchors[].hash must match sha256:<64 lowercase hex>",
1600                );
1601            }
1602        }
1603
1604        // runtime_mode
1605        require_nonblank(
1606            &mut errors,
1607            &self.runtime_mode,
1608            "axiom_execution_trust.runtime_mode.missing",
1609            "runtime_mode must be a non-empty string",
1610        );
1611
1612        if errors.is_empty() {
1613            Ok(())
1614        } else {
1615            Err(errors)
1616        }
1617    }
1618
1619    /// Check whether the token is expired relative to `now`.
1620    #[must_use]
1621    pub fn token_expired_at(&self, now: DateTime<Utc>) -> bool {
1622        self.token_scope.expires_at < now
1623    }
1624}
1625
1626impl AuthorityFeedbackLoop {
1627    /// Validate decomposed fields required at the Cortex consumer.
1628    pub fn validate(&self) -> TrustExchangeValidation {
1629        let mut errors: Vec<TrustExchangeFieldError> = Vec::new();
1630
1631        if self.schema != AUTHORITY_FEEDBACK_LOOP_SCHEMA {
1632            push(
1633                &mut errors,
1634                "authority_feedback_loop.schema.mismatch",
1635                format!(
1636                    "schema must be `{AUTHORITY_FEEDBACK_LOOP_SCHEMA}`, got `{}`",
1637                    self.schema
1638                ),
1639            );
1640        }
1641        if self.version != TRUST_EXCHANGE_SCHEMA_VERSION {
1642            push(
1643                &mut errors,
1644                "authority_feedback_loop.version.mismatch",
1645                format!(
1646                    "version must be {TRUST_EXCHANGE_SCHEMA_VERSION}, got {}",
1647                    self.version
1648                ),
1649            );
1650        }
1651
1652        require_nonblank(
1653            &mut errors,
1654            &self.loop_id,
1655            "authority_feedback_loop.loop_id.missing",
1656            "loop_id must be a non-empty string",
1657        );
1658
1659        require_nonblank(
1660            &mut errors,
1661            &self.initiating_context.context_id,
1662            "authority_feedback_loop.initiating_context.context_id.missing",
1663            "initiating_context.context_id must be a non-empty string",
1664        );
1665        require_nonblank(
1666            &mut errors,
1667            &self.initiating_context.cortex_context_trust_ref,
1668            "authority_feedback_loop.initiating_context.cortex_context_trust_ref.missing",
1669            "initiating_context.cortex_context_trust_ref must be a non-empty string",
1670        );
1671        require_nonblank(
1672            &mut errors,
1673            &self.axiom_action.action_id,
1674            "authority_feedback_loop.axiom_action.action_id.missing",
1675            "axiom_action.action_id must be a non-empty string",
1676        );
1677        require_nonblank(
1678            &mut errors,
1679            &self.axiom_action.axiom_execution_trust_ref,
1680            "authority_feedback_loop.axiom_action.axiom_execution_trust_ref.missing",
1681            "axiom_action.axiom_execution_trust_ref must be a non-empty string",
1682        );
1683
1684        if self.returned_artifacts.is_empty() {
1685            push(
1686                &mut errors,
1687                "authority_feedback_loop.returned_artifacts.missing",
1688                "returned_artifacts must contain at least one artifact",
1689            );
1690        }
1691        for artifact in &self.returned_artifacts {
1692            require_nonblank(
1693                &mut errors,
1694                &artifact.artifact_id,
1695                "authority_feedback_loop.returned_artifacts.artifact_id.missing",
1696                "returned_artifacts[].artifact_id must be a non-empty string",
1697            );
1698            require_nonblank(
1699                &mut errors,
1700                &artifact.lineage_ref,
1701                "authority_feedback_loop.returned_artifacts.lineage_ref.missing",
1702                "returned_artifacts[].lineage_ref must be a non-empty string",
1703            );
1704        }
1705
1706        require_nonblank(
1707            &mut errors,
1708            &self.contradiction_scan_ref,
1709            "authority_feedback_loop.contradiction_scan_ref.missing",
1710            "contradiction_scan_ref must be a non-empty string",
1711        );
1712
1713        if !self.target_domain_validation.required {
1714            push(
1715                &mut errors,
1716                "authority_feedback_loop.target_domain_validation.required.must_be_true",
1717                "target_domain_validation.required must be true",
1718            );
1719        }
1720
1721        if errors.is_empty() {
1722            Ok(())
1723        } else {
1724            Err(errors)
1725        }
1726    }
1727
1728    /// Hard structural Cortex refusal: same-loop promotion must be `false`.
1729    /// Returns `true` when the loop record is structurally inadmissible.
1730    #[must_use]
1731    pub const fn violates_same_loop_invariant(&self) -> bool {
1732        self.same_loop_promotion_allowed
1733    }
1734
1735    /// Hard structural Cortex refusal: authority claims signaling durable
1736    /// truth or full execution authority must be rejected.
1737    #[must_use]
1738    pub const fn claims_durable_authority(&self) -> bool {
1739        self.authority_claims
1740            .durable_truth_promotion
1741            .claims_authority()
1742            || self
1743                .authority_claims
1744                .full_execution_authority
1745                .claims_authority()
1746    }
1747}
1748
1749// ---------------------------------------------------------------------------
1750// Tests — round-trip with the pai-axiom valid fixtures, stable invariant names
1751// ---------------------------------------------------------------------------
1752
1753#[cfg(test)]
1754mod tests {
1755    use super::*;
1756    use chrono::TimeZone;
1757
1758    const VALID_CTX: &str =
1759        include_str!("../tests/fixtures/pai-axiom/valid-cortex-context-trust.json");
1760    const VALID_EXEC: &str =
1761        include_str!("../tests/fixtures/pai-axiom/valid-axiom-execution-trust.json");
1762
1763    #[test]
1764    fn parse_valid_cortex_context_trust_round_trips() {
1765        let envelope = parse_cortex_context_trust(VALID_CTX).expect("valid fixture parses");
1766        assert_eq!(envelope.schema, CORTEX_CONTEXT_TRUST_SCHEMA);
1767        assert_eq!(envelope.version, TRUST_EXCHANGE_SCHEMA_VERSION);
1768        assert_eq!(envelope.context_id, "ctx_valid_behavior_change");
1769        assert_eq!(envelope.quarantine_state, ContextQuarantineState::Clear);
1770        assert!(matches!(
1771            envelope.proof_state.state,
1772            ContextProofStateValue::Closed
1773        ));
1774        envelope.validate().expect("valid fixture validates");
1775
1776        let reserialized = serde_json::to_value(&envelope).unwrap();
1777        assert_eq!(reserialized["schema"], "cortex_context_trust");
1778        assert_eq!(reserialized["truth_ceiling"], "validated");
1779    }
1780
1781    #[test]
1782    fn parse_valid_axiom_execution_trust_round_trips() {
1783        let envelope = parse_axiom_execution_trust(VALID_EXEC).expect("valid fixture parses");
1784        assert_eq!(envelope.schema, AXIOM_EXECUTION_TRUST_SCHEMA);
1785        assert_eq!(envelope.version, TRUST_EXCHANGE_SCHEMA_VERSION);
1786        assert_eq!(envelope.action_id, "action_valid_authority_grade");
1787        assert_eq!(envelope.token_scope.audience, "cortex-admission");
1788        assert_eq!(
1789            envelope.token_scope.revocation_result,
1790            TokenRevocationResult::Active
1791        );
1792        envelope.validate().expect("valid fixture validates");
1793
1794        let reserialized = serde_json::to_value(&envelope).unwrap();
1795        assert_eq!(reserialized["schema"], "axiom_execution_trust");
1796        assert_eq!(reserialized["execution_trust_level"], "authority_grade");
1797    }
1798
1799    #[test]
1800    fn missing_token_audience_emits_stable_invariant() {
1801        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
1802        let mut inner = value["axiom_execution_trust"].clone();
1803        inner["token_scope"]["audience"] = serde_json::Value::String(String::new());
1804
1805        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
1806        let errors = envelope.validate().expect_err("empty audience must fail");
1807        assert!(errors
1808            .iter()
1809            .any(|e| e.invariant == "axiom_execution_trust.token_scope.audience.missing"));
1810    }
1811
1812    #[test]
1813    fn missing_operator_approval_hash_emits_stable_invariant() {
1814        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
1815        let mut inner = value["axiom_execution_trust"].clone();
1816        inner["actor_attestation"]["operator_approval_hash"] =
1817            serde_json::Value::String("not-a-hash".to_string());
1818
1819        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
1820        let errors = envelope.validate().expect_err("bad hash must fail");
1821        assert!(errors.iter().any(|e| e.invariant
1822            == "axiom_execution_trust.actor_attestation.operator_approval_hash.invalid_format"));
1823    }
1824
1825    #[test]
1826    fn invalid_source_commit_emits_stable_invariant() {
1827        let value: serde_json::Value = serde_json::from_str(VALID_EXEC).unwrap();
1828        let mut inner = value["axiom_execution_trust"].clone();
1829        inner["tool_provenance"]["source_commit"] =
1830            serde_json::Value::String("not-a-commit".to_string());
1831
1832        let envelope: AxiomExecutionTrust = serde_json::from_value(inner).unwrap();
1833        let errors = envelope.validate().expect_err("bad commit must fail");
1834        assert!(errors
1835            .iter()
1836            .any(|e| e.invariant
1837                == "axiom_execution_trust.tool_provenance.source_commit.invalid_format"));
1838    }
1839
1840    #[test]
1841    fn proof_state_failed_emits_stable_invariant() {
1842        let value: serde_json::Value = serde_json::from_str(VALID_CTX).unwrap();
1843        let mut inner = value["cortex_context_trust"].clone();
1844        inner["proof_state"]["state"] = serde_json::Value::String("failed".to_string());
1845        inner["proof_state"]["failing_edges"] = serde_json::json!(["proof.audit.missing"]);
1846
1847        let envelope: CortexContextTrust = serde_json::from_value(inner).unwrap();
1848        let errors = envelope
1849            .validate()
1850            .expect_err("failed proof state must fail");
1851        assert!(errors
1852            .iter()
1853            .any(|e| e.invariant == "cortex_context_trust.proof_state.state.unusable"));
1854    }
1855
1856    #[test]
1857    fn extra_top_level_field_fails_closed() {
1858        let json = serde_json::json!({
1859            "schema": "cortex_context_trust",
1860            "version": 1,
1861            "context_id": "ctx",
1862            "compatibility_trust_label": "validated",
1863            "proof_state": {"state": "closed", "failing_edges": [], "proof_refs": ["p"]},
1864            "truth_ceiling": "validated",
1865            "semantic_trust": {"provenance_class": "observed", "trust_weight": 1, "weighting_basis": "x"},
1866            "quarantine_state": "clear",
1867            "redaction_state": {"status": "none", "redaction_refs": []},
1868            "policy_result": {"decision_id": "p", "result": "allow"},
1869            "source_anchors": [{
1870                "source_id": "s",
1871                "source_type": "event",
1872                "ref": "r",
1873                "hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
1874            }],
1875            "rogue_field": true
1876        });
1877        let err = serde_json::from_value::<CortexContextTrust>(json)
1878            .expect_err("deny_unknown_fields must reject rogue field");
1879        assert!(err.to_string().contains("rogue_field"));
1880    }
1881
1882    #[test]
1883    fn revoked_token_is_must_reject() {
1884        assert!(TokenRevocationResult::Revoked.must_reject());
1885        assert!(TokenRevocationResult::Inactive.must_reject());
1886        assert!(!TokenRevocationResult::Active.must_reject());
1887    }
1888
1889    #[test]
1890    fn token_expiry_check_uses_injected_now() {
1891        let mut envelope = parse_axiom_execution_trust(VALID_EXEC).unwrap();
1892        envelope.token_scope.expires_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
1893        let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
1894        assert!(envelope.token_expired_at(now));
1895    }
1896
1897    #[test]
1898    fn same_loop_promotion_invariant_is_structural() {
1899        let loop_record = AuthorityFeedbackLoop {
1900            schema: AUTHORITY_FEEDBACK_LOOP_SCHEMA.to_string(),
1901            version: 1,
1902            authority_feedback_loop_ref: None,
1903            loop_id: "loop_x".to_string(),
1904            started_at: Utc::now(),
1905            initiating_context: FeedbackInitiatingContext {
1906                context_id: "ctx".to_string(),
1907                cortex_context_trust_ref: "ref".to_string(),
1908            },
1909            axiom_action: FeedbackAxiomAction {
1910                action_id: "act".to_string(),
1911                axiom_execution_trust_ref: "ref".to_string(),
1912            },
1913            returned_artifacts: vec![FeedbackReturnedArtifact {
1914                artifact_id: "art".to_string(),
1915                lineage_ref: "lin".to_string(),
1916                lifecycle_state: ArtifactLifecycleState::Candidate,
1917                reproducibility_level: ReproducibilityLevel::Observational,
1918            }],
1919            amplification_risk: AmplificationRisk::Low,
1920            independent_evidence_refs: vec![],
1921            external_grounding_refs: vec![],
1922            contradiction_scan_ref: "scan".to_string(),
1923            quarantine_state: ContextQuarantineState::Clear,
1924            confidence_ceiling: ConfidenceCeiling::Advisory,
1925            same_loop_promotion_allowed: true,
1926            authority_claims: FeedbackAuthorityClaims {
1927                durable_truth_promotion: AuthorityClaimStatus::Denied,
1928                full_execution_authority: AuthorityClaimStatus::Denied,
1929                review_required: true,
1930            },
1931            target_domain_validation: TargetDomainValidation {
1932                required: true,
1933                independent_validation_ref: None,
1934                result: TargetDomainValidationResult::Pending,
1935            },
1936            residual_risk: vec![],
1937        };
1938        assert!(loop_record.violates_same_loop_invariant());
1939        assert!(!loop_record.claims_durable_authority());
1940    }
1941
1942    #[test]
1943    fn named_quarantine_outputs_iterate_invariants() {
1944        let outputs = NamedQuarantineOutputs {
1945            source_context: Some(QuarantineOutput::new(
1946                "axiom.admission.quarantine.propagated",
1947                "source quarantine",
1948            )),
1949            token_revocation: Some(QuarantineOutput::new(
1950                "axiom_execution_trust.token_scope.revoked",
1951                "revoked",
1952            )),
1953            ..Default::default()
1954        };
1955        let names: Vec<&str> = outputs.invariants().collect();
1956        assert_eq!(names.len(), 2);
1957        assert!(names.contains(&"axiom.admission.quarantine.propagated"));
1958        assert!(names.contains(&"axiom_execution_trust.token_scope.revoked"));
1959    }
1960
1961    #[test]
1962    fn quarantine_state_propagation_predicate() {
1963        assert!(ContextQuarantineState::Quarantined.propagates_quarantine());
1964        assert!(ContextQuarantineState::DerivedFromQuarantined.propagates_quarantine());
1965        assert!(ContextQuarantineState::Unknown.propagates_quarantine());
1966        assert!(!ContextQuarantineState::Clear.propagates_quarantine());
1967    }
1968
1969    // -----------------------------------------------------------------
1970    // Receiver-side source-commit freshness gate
1971    // -----------------------------------------------------------------
1972
1973    /// Guard the stable invariant string against accidental rename. Operator
1974    /// scripts and the axiom-side test harness grep on this exact value.
1975    #[test]
1976    fn source_commit_stale_invariant_is_stable() {
1977        assert_eq!(
1978            AXIOM_EXECUTION_TRUST_SOURCE_COMMIT_STALE_INVARIANT,
1979            "axiom_execution_trust.tool_provenance.source_commit.stale"
1980        );
1981    }
1982
1983    /// Guard the env-var name against rename. Operators set this in their
1984    /// shell; renaming silently breaks deployments.
1985    #[test]
1986    fn accepted_source_commits_env_var_name_is_stable() {
1987        assert_eq!(
1988            CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV,
1989            "CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS"
1990        );
1991    }
1992
1993    #[test]
1994    fn default_accept_list_includes_adr_0042_pin() {
1995        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
1996            .contains(&"44b9a25dfbe5a93e64120f53b65730828d1af91c"));
1997    }
1998
1999    #[test]
2000    fn default_accept_list_includes_known_good_v2_corpus() {
2001        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
2002            .contains(&"9a15d281ddcc2bcf36956fbe6d6c5736d8ce706a"));
2003        assert!(DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
2004            .contains(&"062d0d42ac5ef300fa3e04ef5b49b14864babcdd"));
2005    }
2006
2007    #[test]
2008    fn default_accept_list_entries_are_well_formed_commits() {
2009        for sha in DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS {
2010            assert!(
2011                is_commit_sha(sha),
2012                "default accept-list entry `{sha}` must be a 40-char lowercase-hex commit SHA",
2013            );
2014        }
2015    }
2016
2017    #[test]
2018    fn freshness_predicate_admits_accepted_sha() {
2019        let accepted: Vec<String> = DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
2020            .iter()
2021            .map(|sha| (*sha).to_string())
2022            .collect();
2023        assert!(is_axiom_source_commit_fresh(
2024            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
2025            &accepted
2026        ));
2027    }
2028
2029    #[test]
2030    fn freshness_predicate_refuses_unknown_sha() {
2031        let accepted: Vec<String> = DEFAULT_ACCEPTED_AXIOM_SOURCE_COMMITS
2032            .iter()
2033            .map(|sha| (*sha).to_string())
2034            .collect();
2035        assert!(!is_axiom_source_commit_fresh(
2036            "0000000000000000000000000000000000000000",
2037            &accepted
2038        ));
2039        assert!(!is_axiom_source_commit_fresh(
2040            "1234567890abcdef1234567890abcdef12345678",
2041            &accepted
2042        ));
2043    }
2044
2045    #[test]
2046    fn freshness_predicate_is_case_insensitive_on_candidate() {
2047        let accepted = vec!["44b9a25dfbe5a93e64120f53b65730828d1af91c".to_string()];
2048        assert!(is_axiom_source_commit_fresh(
2049            "44B9A25DFBE5A93E64120F53B65730828D1AF91C",
2050            &accepted
2051        ));
2052        assert!(is_axiom_source_commit_fresh(
2053            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
2054            &accepted
2055        ));
2056    }
2057
2058    #[test]
2059    fn freshness_predicate_empty_accept_list_refuses_everything() {
2060        let accepted: Vec<String> = Vec::new();
2061        assert!(!is_axiom_source_commit_fresh(
2062            "44b9a25dfbe5a93e64120f53b65730828d1af91c",
2063            &accepted
2064        ));
2065    }
2066
2067    /// Verify env-var parsing semantics. We run this against a temporary
2068    /// override; the test restores the prior env state at the end so other
2069    /// tests in this module see the default.
2070    #[test]
2071    fn accepted_axiom_source_commits_env_var_replaces_default() {
2072        // Race-free: this test mutates a process-global env var, so it
2073        // must not be parallelised with other tests reading the same var.
2074        // No other test in this module reads `accepted_axiom_source_commits`
2075        // through the env-var path (the freshness-predicate tests build
2076        // their accept-list inline), so a single-test guard is sufficient.
2077        let prior = std::env::var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV).ok();
2078
2079        std::env::set_var(
2080            CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV,
2081            "  AAAAaaaaAAAAaaaaAAAAaaaaAAAAaaaaAAAAaaaa , 1111111111111111111111111111111111111111  ,,",
2082        );
2083        let resolved = accepted_axiom_source_commits();
2084        assert_eq!(resolved.len(), 2);
2085        assert!(resolved.contains(&"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()));
2086        assert!(resolved.contains(&"1111111111111111111111111111111111111111".to_string()));
2087        // The ADR 0042 default pin MUST be excluded — env var REPLACES the
2088        // default, it does not extend it.
2089        assert!(!resolved.contains(&"44b9a25dfbe5a93e64120f53b65730828d1af91c".to_string()));
2090
2091        // Restore prior state so neighbouring tests are unaffected.
2092        match prior {
2093            Some(value) => std::env::set_var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV, value),
2094            None => std::env::remove_var(CORTEX_AXIOM_ACCEPTED_SOURCE_COMMITS_ENV),
2095        }
2096    }
2097}