1use cortex_core::{
8 compose_policy_outcomes, evaluate_semantic_trust, BoundaryQuarantineState,
9 CapabilityTokenDecision, FailingEdge, PaiAxiomExecutionReceiptV1, PolicyContribution,
10 PolicyDecision, PolicyOutcome, ProofClosureReport, ProofEdge, ProofEdgeFailure, ProofEdgeKind,
11 ProofState as CoreProofState, ProvenanceClass, RuntimeIntegrityState, SemanticTrustInput,
12 SemanticTrustReport, SemanticUse,
13};
14use serde::{Deserialize, Serialize};
15
16pub const AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT: &str =
26 "cortex_memory.admission.axiom.proof_closure";
27
28pub const AXIOM_ADMISSION_PROOF_CLOSURE_RULE_ID: &str = "memory.admission.axiom.proof_closure";
30
31pub type AdmissionValidationResult<T> = Result<T, Vec<AdmissionRejectionReason>>;
33
34pub type AdmissionEnvelopeResult<T> = Result<T, AdmissionEnvelopeError>;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct AxiomMemoryAdmissionRequest {
40 pub candidate_state: CandidateState,
42 pub evidence_class: EvidenceClass,
44 pub phase_context: PhaseContext,
46 pub tool_provenance: ToolProvenance,
48 pub source_anchors: Vec<SourceAnchor>,
50 pub redaction_status: RedactionStatus,
52 pub proof_state: ProofState,
54 pub contradiction_scan: ContradictionScan,
56 pub explicit_non_promotion: bool,
58}
59
60impl AxiomMemoryAdmissionRequest {
61 pub fn from_json_envelope(input: &str) -> AdmissionEnvelopeResult<Self> {
68 serde_json::from_str(input).map_err(|err| AdmissionEnvelopeError::InvalidEnvelope {
69 message: err.to_string(),
70 })
71 }
72
73 pub fn validate(&self) -> AdmissionValidationResult<()> {
75 let mut reasons = Vec::new();
76 push_err(&mut reasons, require_candidate_state(self.candidate_state));
77 push_err(
78 &mut reasons,
79 require_admissible_evidence(self.evidence_class),
80 );
81 push_err(
82 &mut reasons,
83 require_axiom_origin_is_not_product_spec(&self.tool_provenance),
84 );
85 push_err(&mut reasons, require_source_anchors(&self.source_anchors));
86 push_err(
87 &mut reasons,
88 require_redaction_status(self.redaction_status),
89 );
90 push_err(&mut reasons, require_usable_proof_state(self.proof_state));
91 push_err(
92 &mut reasons,
93 require_contradiction_scan(&self.contradiction_scan),
94 );
95 push_err(
96 &mut reasons,
97 require_explicit_non_promotion(self.explicit_non_promotion),
98 );
99 push_err(&mut reasons, require_phase_context(self.phase_context));
100
101 if reasons.is_empty() {
102 Ok(())
103 } else {
104 Err(reasons)
105 }
106 }
107
108 #[must_use]
110 pub fn admission_decision(&self) -> AdmissionDecision {
111 match self.validate() {
112 Ok(()) => AdmissionDecision::AdmitCandidate,
113 Err(reasons)
114 if reasons
115 .iter()
116 .any(AdmissionRejectionReason::requires_rejection) =>
117 {
118 AdmissionDecision::Reject { reasons }
119 }
120 Err(reasons) => AdmissionDecision::Quarantine { reasons },
121 }
122 }
123
124 #[must_use]
127 pub fn policy_decision(&self) -> PolicyDecision {
128 match self.admission_decision() {
129 AdmissionDecision::AdmitCandidate => compose_policy_outcomes(
130 vec![PolicyContribution::new(
131 "memory.admission.allow",
132 PolicyOutcome::Allow,
133 "AXIOM memory submission passed admission gates",
134 )
135 .expect("static policy contribution is valid")],
136 None,
137 ),
138 AdmissionDecision::Reject { reasons } | AdmissionDecision::Quarantine { reasons } => {
139 let contributions = reasons
140 .into_iter()
141 .map(|reason| {
142 PolicyContribution::new(
143 reason.policy_rule_id(),
144 reason.policy_outcome(),
145 reason.policy_reason(),
146 )
147 .expect("static policy contribution is valid")
148 })
149 .collect();
150 compose_policy_outcomes(contributions, None)
151 }
152 }
153 }
154
155 #[must_use]
177 pub fn proof_closure_report(&self) -> ProofClosureReport {
178 let axis_anchor = "axiom.admission";
179 let axis_target = "proof_closure";
180 match self.proof_state {
181 ProofState::FullChainVerified => {
182 ProofClosureReport::full_chain_verified(vec![ProofEdge::new(
183 ProofEdgeKind::LineageClosure,
184 axis_anchor,
185 axis_target,
186 )
187 .with_evidence_ref("envelope.proof_state=full_chain_verified")])
188 }
189 ProofState::Partial => ProofClosureReport::from_edges(
190 Vec::new(),
191 vec![FailingEdge::missing(
192 ProofEdgeKind::LineageClosure,
193 axis_anchor,
194 "envelope proof_state is Partial; ADR 0036 forbids durable promotion",
195 )],
196 ),
197 ProofState::Broken => ProofClosureReport::from_edges(
198 Vec::new(),
199 vec![FailingEdge::broken(
200 ProofEdgeKind::HashChain,
201 axis_anchor,
202 axis_target,
203 ProofEdgeFailure::Mismatch,
204 "envelope proof_state is Broken; ADR 0036 fails closed",
205 )],
206 ),
207 ProofState::Unknown => ProofClosureReport::from_edges(
208 Vec::new(),
209 vec![FailingEdge::unresolved(
210 ProofEdgeKind::LineageClosure,
211 axis_anchor,
212 "envelope proof_state is Unknown; ADR 0036 forbids durable promotion",
213 )],
214 ),
215 }
216 }
217
218 #[must_use]
225 pub fn proof_closure_contribution(&self) -> PolicyContribution {
226 let report = self.proof_closure_report();
227 let (outcome, reason): (PolicyOutcome, &'static str) = match report.state() {
228 CoreProofState::FullChainVerified => (
229 PolicyOutcome::Allow,
230 "AXIOM admission envelope proof closure is fully verified",
231 ),
232 CoreProofState::Partial => (
233 PolicyOutcome::Quarantine,
234 "AXIOM admission envelope proof closure is partial; ADR 0036 forbids durable promotion",
235 ),
236 CoreProofState::Broken => (
237 PolicyOutcome::Reject,
238 "AXIOM admission envelope proof closure is broken; ADR 0036 fails closed",
239 ),
240 };
241 PolicyContribution::new(AXIOM_ADMISSION_PROOF_CLOSURE_RULE_ID, outcome, reason)
242 .expect("static admission proof closure contribution is valid")
243 }
244
245 pub fn require_durable_admission_allowed(&self) -> Result<(), DurableAdmissionRefusal> {
260 let report = self.proof_closure_report();
261 if report.is_full_chain_verified() {
262 Ok(())
263 } else {
264 Err(DurableAdmissionRefusal {
265 proof_state: report.state(),
266 })
267 }
268 }
269
270 #[must_use]
273 pub fn semantic_trust_report(
274 &self,
275 input: AdmissionSemanticTrustInput,
276 ) -> AdmissionSemanticTrustReport {
277 let provenance_class = self.semantic_provenance_class();
278 let unresolved_unknowns =
279 input.unresolved_semantic_unknowns || self.has_semantic_unknowns();
280 let semantic_input = SemanticTrustInput::new(input.intended_use)
281 .with_provenance([provenance_class])
282 .with_independent_source_families(input.independent_source_families)
283 .with_falsification_evidence(input.falsification_evidence)
284 .with_unresolved_unknowns(unresolved_unknowns);
285
286 AdmissionSemanticTrustReport {
287 intended_use: input.intended_use,
288 provenance_class,
289 semantic_trust: evaluate_semantic_trust(&semantic_input),
290 admission_decision: self.admission_decision(),
291 explicit_non_promotion: self.explicit_non_promotion,
292 }
293 }
294
295 #[must_use]
298 pub fn semantic_provenance_class(&self) -> ProvenanceClass {
299 match self.evidence_class {
300 EvidenceClass::Unknown => ProvenanceClass::UnknownProvenance,
301 EvidenceClass::Simulated => ProvenanceClass::SimulatedOrHypothetical,
302 EvidenceClass::Claimed => ProvenanceClass::ExternalClaimed,
303 EvidenceClass::Inferred => ProvenanceClass::SummaryDerived,
304 EvidenceClass::Observed => self.observed_semantic_provenance_class(),
305 }
306 }
307
308 fn observed_semantic_provenance_class(&self) -> ProvenanceClass {
309 match (
310 self.phase_context,
311 self.tool_provenance.import_class,
312 self.tool_provenance.tool_name.is_empty(),
313 self.tool_provenance.invocation_id.is_empty(),
314 ) {
315 (PhaseContext::WorkRecord, AxiomImportClass::AgentProcedure, _, _) => {
316 ProvenanceClass::RuntimeDerived
317 }
318 (_, AxiomImportClass::OperatorProtocol, false, false) => ProvenanceClass::ToolObserved,
322 (_, _, false, false) => ProvenanceClass::ToolObserved,
323 _ => ProvenanceClass::RuntimeDerived,
324 }
325 }
326
327 fn has_semantic_unknowns(&self) -> bool {
328 matches!(self.evidence_class, EvidenceClass::Unknown)
329 || matches!(self.phase_context, PhaseContext::Unknown)
330 || matches!(self.proof_state, ProofState::Unknown | ProofState::Broken)
331 || matches!(
332 self.contradiction_scan,
333 ContradictionScan::Incomplete | ContradictionScan::NotScanned
334 )
335 || self.source_anchors.is_empty()
336 || self
337 .source_anchors
338 .iter()
339 .any(|anchor| anchor.reference.trim().is_empty())
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub struct AdmissionSemanticTrustInput {
347 pub intended_use: SemanticUse,
349 pub independent_source_families: u16,
351 pub falsification_evidence: bool,
353 pub unresolved_semantic_unknowns: bool,
355}
356
357impl AdmissionSemanticTrustInput {
358 #[must_use]
360 pub const fn new(intended_use: SemanticUse) -> Self {
361 Self {
362 intended_use,
363 independent_source_families: 0,
364 falsification_evidence: false,
365 unresolved_semantic_unknowns: true,
366 }
367 }
368
369 #[must_use]
371 pub const fn with_independent_source_families(mut self, count: u16) -> Self {
372 self.independent_source_families = count;
373 self
374 }
375
376 #[must_use]
378 pub const fn with_falsification_evidence(mut self, present: bool) -> Self {
379 self.falsification_evidence = present;
380 self
381 }
382
383 #[must_use]
385 pub const fn with_unresolved_semantic_unknowns(mut self, present: bool) -> Self {
386 self.unresolved_semantic_unknowns = present;
387 self
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct AdmissionSemanticTrustReport {
397 pub intended_use: SemanticUse,
399 pub provenance_class: ProvenanceClass,
401 pub semantic_trust: SemanticTrustReport,
403 pub admission_decision: AdmissionDecision,
405 pub explicit_non_promotion: bool,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
411pub enum AdmissionEnvelopeError {
412 InvalidEnvelope {
414 message: String,
416 },
417}
418
419#[derive(Debug, Clone, PartialEq, Eq)]
427pub struct DurableAdmissionRefusal {
428 pub proof_state: CoreProofState,
430}
431
432impl std::fmt::Display for DurableAdmissionRefusal {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 let proof_state = self.proof_state;
435 write!(
436 f,
437 "invariant={AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT} AXIOM admission durable gate refused: proof closure must be FullChainVerified; observed {proof_state:?}"
438 )
439 }
440}
441
442impl std::error::Error for DurableAdmissionRefusal {}
443
444impl From<PaiAxiomExecutionReceiptV1> for AxiomMemoryAdmissionRequest {
445 fn from(receipt: PaiAxiomExecutionReceiptV1) -> Self {
446 let first_tool = receipt.tool_provenance.first();
447 let tool_name = first_tool
448 .map(|tool| tool.tool_name.clone())
449 .unwrap_or_else(|| "pai-axiom".to_string());
450 let invocation_id = first_tool
451 .map(|tool| tool.invocation_id.clone())
452 .unwrap_or_else(|| receipt.runtime_id.clone());
453 let evidence_class = receipt_evidence_class(&receipt);
454 let proof_state = receipt_proof_state(&receipt);
455 let explicit_non_promotion = receipt.explicit_non_promotion
456 && receipt.quarantine_state != BoundaryQuarantineState::Contaminated;
457 let source_anchors = receipt
458 .source_anchors
459 .into_iter()
460 .map(|anchor| SourceAnchor::new(anchor.reference, SourceAnchorKind::Artifact))
461 .collect();
462
463 Self {
464 candidate_state: CandidateState::Candidate,
465 evidence_class,
466 phase_context: PhaseContext::WorkRecord,
467 tool_provenance: ToolProvenance::new(
468 tool_name,
469 invocation_id,
470 AxiomImportClass::AgentProcedure,
471 ),
472 source_anchors,
473 redaction_status: RedactionStatus::Abstracted,
474 proof_state,
475 contradiction_scan: ContradictionScan::Incomplete,
476 explicit_non_promotion,
477 }
478 }
479}
480
481fn receipt_evidence_class(receipt: &PaiAxiomExecutionReceiptV1) -> EvidenceClass {
482 if receipt.tool_provenance.is_empty()
483 || matches!(
484 receipt.capability_token_state.decision,
485 CapabilityTokenDecision::Rejected
486 | CapabilityTokenDecision::Expired
487 | CapabilityTokenDecision::Revoked
488 )
489 {
490 EvidenceClass::Claimed
491 } else {
492 EvidenceClass::Observed
493 }
494}
495
496fn receipt_proof_state(receipt: &PaiAxiomExecutionReceiptV1) -> ProofState {
497 if matches!(
498 receipt.quarantine_state,
499 BoundaryQuarantineState::Contaminated
500 ) || matches!(
501 receipt.capability_token_state.decision,
502 CapabilityTokenDecision::Rejected
503 | CapabilityTokenDecision::Expired
504 | CapabilityTokenDecision::Revoked
505 ) || matches!(
506 receipt.execution_trust_state.runtime_integrity,
507 RuntimeIntegrityState::Compromised
508 ) {
509 ProofState::Broken
510 } else if matches!(
511 receipt.execution_trust_state.runtime_integrity,
512 RuntimeIntegrityState::VerifiedRelease | RuntimeIntegrityState::VerifiedProvenance
513 ) && matches!(
514 receipt.capability_token_state.decision,
515 CapabilityTokenDecision::Allowed | CapabilityTokenDecision::Warned
516 ) {
517 ProofState::Partial
518 } else {
519 ProofState::Unknown
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(rename_all = "snake_case")]
526pub enum CandidateState {
527 Candidate,
529 Active,
531 Principle,
533 Doctrine,
535 Unknown,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(rename_all = "snake_case")]
542pub enum EvidenceClass {
543 Observed,
545 Inferred,
547 Claimed,
549 Simulated,
551 Unknown,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
557#[serde(rename_all = "snake_case")]
558pub enum PhaseContext {
559 Model,
561 Act,
563 Check,
565 WorkRecord,
567 Unknown,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
573#[serde(rename_all = "snake_case")]
574pub enum AxiomImportClass {
575 OperatorProtocol,
577 AgentProcedure,
579 ProductSpecification,
581 TestEvalInput,
583 HistoricalReference,
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589pub struct ToolProvenance {
590 pub tool_name: String,
592 pub invocation_id: String,
594 pub import_class: AxiomImportClass,
596}
597
598impl ToolProvenance {
599 #[must_use]
601 pub fn new(
602 tool_name: impl Into<String>,
603 invocation_id: impl Into<String>,
604 import_class: AxiomImportClass,
605 ) -> Self {
606 Self {
607 tool_name: tool_name.into(),
608 invocation_id: invocation_id.into(),
609 import_class,
610 }
611 }
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
616pub struct SourceAnchor {
617 pub reference: String,
619 pub kind: SourceAnchorKind,
621}
622
623impl SourceAnchor {
624 #[must_use]
626 pub fn new(reference: impl Into<String>, kind: SourceAnchorKind) -> Self {
627 Self {
628 reference: reference.into(),
629 kind,
630 }
631 }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(rename_all = "snake_case")]
637pub enum SourceAnchorKind {
638 Event,
640 Trace,
642 Episode,
644 Audit,
646 Artifact,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
652#[serde(rename_all = "snake_case")]
653pub enum RedactionStatus {
654 Redacted,
656 Abstracted,
658 OperatorRawOptIn,
660 RawUnredacted,
662 Unknown,
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
668#[serde(rename_all = "snake_case")]
669pub enum ProofState {
670 FullChainVerified,
672 Partial,
674 Broken,
676 Unknown,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682#[serde(rename_all = "snake_case")]
683pub enum ContradictionScan {
684 ScannedClean,
686 OpenContradictions(Vec<String>),
688 Incomplete,
690 NotScanned,
692}
693
694#[derive(Debug, Clone, PartialEq, Eq)]
696pub enum AdmissionDecision {
697 AdmitCandidate,
699 Reject {
701 reasons: Vec<AdmissionRejectionReason>,
703 },
704 Quarantine {
706 reasons: Vec<AdmissionRejectionReason>,
708 },
709}
710
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub enum AdmissionRejectionReason {
714 CandidateStateRequired,
716 EvidenceClassRequired,
718 ToolProvenanceRequired,
720 ProductSpecificationImportRejected,
722 SourceAnchorRequired,
724 SourceAnchorBlank,
726 RedactionStatusRequired,
728 ProofStateRequired,
730 ContradictionScanRequired,
732 OpenContradiction,
734 ExplicitNonPromotionRequired,
736 PhaseContextRequired,
738}
739
740impl AdmissionRejectionReason {
741 const fn requires_rejection(&self) -> bool {
742 matches!(
743 self,
744 Self::CandidateStateRequired
745 | Self::ProductSpecificationImportRejected
746 | Self::RedactionStatusRequired
747 | Self::ProofStateRequired
748 | Self::ExplicitNonPromotionRequired
749 )
750 }
751
752 const fn policy_outcome(self) -> PolicyOutcome {
753 if self.requires_rejection() {
754 PolicyOutcome::Reject
755 } else {
756 PolicyOutcome::Quarantine
757 }
758 }
759
760 const fn policy_rule_id(self) -> &'static str {
761 match self {
762 Self::CandidateStateRequired => "memory.admission.candidate_state_required",
763 Self::EvidenceClassRequired => "memory.admission.evidence_class_required",
764 Self::ToolProvenanceRequired => "memory.admission.tool_provenance_required",
765 Self::ProductSpecificationImportRejected => {
766 "memory.admission.product_specification_import_rejected"
767 }
768 Self::SourceAnchorRequired => "memory.admission.source_anchor_required",
769 Self::SourceAnchorBlank => "memory.admission.source_anchor_blank",
770 Self::RedactionStatusRequired => "memory.admission.redaction_status_required",
771 Self::ProofStateRequired => "memory.admission.proof_state_required",
772 Self::ContradictionScanRequired => "memory.admission.contradiction_scan_required",
773 Self::OpenContradiction => "memory.admission.open_contradiction",
774 Self::ExplicitNonPromotionRequired => {
775 "memory.admission.explicit_non_promotion_required"
776 }
777 Self::PhaseContextRequired => "memory.admission.phase_context_required",
778 }
779 }
780
781 const fn policy_reason(self) -> &'static str {
782 match self {
783 Self::CandidateStateRequired => "AXIOM admission target must remain Candidate",
784 Self::EvidenceClassRequired => "AXIOM admission requires classified evidence",
785 Self::ToolProvenanceRequired => "AXIOM admission requires tool provenance",
786 Self::ProductSpecificationImportRejected => {
787 "AXIOM import cannot become Cortex product specification through memory admission"
788 }
789 Self::SourceAnchorRequired => "AXIOM admission requires at least one source anchor",
790 Self::SourceAnchorBlank => "AXIOM admission source anchors must not be blank",
791 Self::RedactionStatusRequired => {
792 "AXIOM admission requires redacted, abstracted, or operator-opted raw content"
793 }
794 Self::ProofStateRequired => "AXIOM admission requires supplied non-broken proof state",
795 Self::ContradictionScanRequired => {
796 "AXIOM admission requires completed contradiction scan"
797 }
798 Self::OpenContradiction => {
799 "AXIOM admission found open contradiction and must not enter clean candidate path"
800 }
801 Self::ExplicitNonPromotionRequired => "AXIOM admission requires explicit non-promotion",
802 Self::PhaseContextRequired => "AXIOM admission requires known AXIOM phase context",
803 }
804 }
805}
806
807pub fn require_candidate_state(state: CandidateState) -> Result<(), AdmissionRejectionReason> {
809 if state == CandidateState::Candidate {
810 Ok(())
811 } else {
812 Err(AdmissionRejectionReason::CandidateStateRequired)
813 }
814}
815
816pub fn require_admissible_evidence(
818 evidence_class: EvidenceClass,
819) -> Result<(), AdmissionRejectionReason> {
820 if evidence_class == EvidenceClass::Unknown {
821 Err(AdmissionRejectionReason::EvidenceClassRequired)
822 } else {
823 Ok(())
824 }
825}
826
827pub fn require_axiom_origin_is_not_product_spec(
829 provenance: &ToolProvenance,
830) -> Result<(), AdmissionRejectionReason> {
831 if provenance.tool_name.trim().is_empty() || provenance.invocation_id.trim().is_empty() {
832 return Err(AdmissionRejectionReason::ToolProvenanceRequired);
833 }
834
835 if provenance.import_class == AxiomImportClass::ProductSpecification {
836 return Err(AdmissionRejectionReason::ProductSpecificationImportRejected);
837 }
838
839 Ok(())
840}
841
842pub fn require_source_anchors(anchors: &[SourceAnchor]) -> Result<(), AdmissionRejectionReason> {
844 if anchors.is_empty() {
845 return Err(AdmissionRejectionReason::SourceAnchorRequired);
846 }
847
848 if anchors
849 .iter()
850 .any(|anchor| anchor.reference.trim().is_empty())
851 {
852 return Err(AdmissionRejectionReason::SourceAnchorBlank);
853 }
854
855 Ok(())
856}
857
858pub fn require_redaction_status(
860 redaction_status: RedactionStatus,
861) -> Result<(), AdmissionRejectionReason> {
862 match redaction_status {
863 RedactionStatus::Redacted
864 | RedactionStatus::Abstracted
865 | RedactionStatus::OperatorRawOptIn => Ok(()),
866 RedactionStatus::RawUnredacted | RedactionStatus::Unknown => {
867 Err(AdmissionRejectionReason::RedactionStatusRequired)
868 }
869 }
870}
871
872pub fn require_usable_proof_state(proof_state: ProofState) -> Result<(), AdmissionRejectionReason> {
874 match proof_state {
875 ProofState::FullChainVerified | ProofState::Partial => Ok(()),
876 ProofState::Broken | ProofState::Unknown => {
877 Err(AdmissionRejectionReason::ProofStateRequired)
878 }
879 }
880}
881
882pub fn require_contradiction_scan(
884 contradiction_scan: &ContradictionScan,
885) -> Result<(), AdmissionRejectionReason> {
886 match contradiction_scan {
887 ContradictionScan::ScannedClean => Ok(()),
888 ContradictionScan::OpenContradictions(_) => {
889 Err(AdmissionRejectionReason::OpenContradiction)
890 }
891 ContradictionScan::Incomplete | ContradictionScan::NotScanned => {
892 Err(AdmissionRejectionReason::ContradictionScanRequired)
893 }
894 }
895}
896
897pub fn require_explicit_non_promotion(
899 explicit_non_promotion: bool,
900) -> Result<(), AdmissionRejectionReason> {
901 if explicit_non_promotion {
902 Ok(())
903 } else {
904 Err(AdmissionRejectionReason::ExplicitNonPromotionRequired)
905 }
906}
907
908pub fn require_phase_context(phase_context: PhaseContext) -> Result<(), AdmissionRejectionReason> {
910 if phase_context == PhaseContext::Unknown {
911 Err(AdmissionRejectionReason::PhaseContextRequired)
912 } else {
913 Ok(())
914 }
915}
916
917fn push_err(
918 reasons: &mut Vec<AdmissionRejectionReason>,
919 result: Result<(), AdmissionRejectionReason>,
920) {
921 if let Err(reason) = result {
922 reasons.push(reason);
923 }
924}
925
926#[cfg(test)]
927mod tests {
928 use cortex_core::{
929 BoundaryQuarantineState, BoundarySourceAnchor, BoundaryToolInvocation, BoundaryToolOutcome,
930 CapabilityTokenDecision, CapabilityTokenState, ClaimCeiling, ExecutionTrustState,
931 OperatorApprovalState, PaiAxiomExecutionReceiptV1, ProvenanceClass, RuntimeIntegrityState,
932 SemanticTrustClass, SemanticUse,
933 };
934
935 use super::*;
936
937 fn token_state(decision: CapabilityTokenDecision) -> CapabilityTokenState {
938 CapabilityTokenState {
939 decision,
940 valid_structure: true,
941 audience_bound: true,
942 scope_bound: true,
943 operation_bound: true,
944 not_expired: true,
945 not_revoked: true,
946 policy_allowed: true,
947 attestation_linked: true,
948 }
949 }
950
951 fn receipt(decision: CapabilityTokenDecision) -> PaiAxiomExecutionReceiptV1 {
952 let mut receipt = PaiAxiomExecutionReceiptV1::new(
953 "axiom-runtime:v3.1/run_01",
954 token_state(decision),
955 ExecutionTrustState {
956 runtime_integrity: RuntimeIntegrityState::VerifiedRelease,
957 evidence_ref: Some("release:verified".to_string()),
958 },
959 OperatorApprovalState::ApprovedBound,
960 );
961 receipt.tool_provenance.push(BoundaryToolInvocation {
962 tool_name: "cargo".to_string(),
963 invocation_id: "tool_01".to_string(),
964 input_ref: Some("cmd:cargo test".to_string()),
965 output_ref: Some("log:passed".to_string()),
966 outcome: BoundaryToolOutcome::Succeeded,
967 });
968 receipt.source_anchors.push(BoundarySourceAnchor {
969 reference: "artifact:log:passed".to_string(),
970 kind: "artifact".to_string(),
971 });
972 receipt
973 }
974
975 fn valid_request() -> AxiomMemoryAdmissionRequest {
976 AxiomMemoryAdmissionRequest {
977 candidate_state: CandidateState::Candidate,
978 evidence_class: EvidenceClass::Observed,
979 phase_context: PhaseContext::Check,
980 tool_provenance: ToolProvenance::new(
981 "axiom-runtime",
982 "run_01",
983 AxiomImportClass::AgentProcedure,
984 ),
985 source_anchors: vec![SourceAnchor::new(
986 "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV",
987 SourceAnchorKind::Event,
988 )],
989 redaction_status: RedactionStatus::Abstracted,
990 proof_state: ProofState::Partial,
991 contradiction_scan: ContradictionScan::ScannedClean,
992 explicit_non_promotion: true,
993 }
994 }
995
996 #[test]
997 fn axiom_submission_requires_candidate_state_and_tool_provenance() {
998 let mut request = valid_request();
999 request.candidate_state = CandidateState::Active;
1000 request.tool_provenance.invocation_id = " ".into();
1001
1002 let reasons = request
1003 .validate()
1004 .expect_err("request must fail validation");
1005
1006 assert!(reasons.contains(&AdmissionRejectionReason::CandidateStateRequired));
1007 assert!(reasons.contains(&AdmissionRejectionReason::ToolProvenanceRequired));
1008 }
1009
1010 #[test]
1011 fn attempted_direct_active_admission_rejects() {
1012 let mut request = valid_request();
1013 request.candidate_state = CandidateState::Active;
1014
1015 assert_eq!(
1016 request.admission_decision(),
1017 AdmissionDecision::Reject {
1018 reasons: vec![AdmissionRejectionReason::CandidateStateRequired],
1019 }
1020 );
1021 }
1022
1023 #[test]
1024 fn missing_tool_provenance_is_quarantined() {
1025 let mut request = valid_request();
1026 request.tool_provenance.tool_name = " ".into();
1027 request.tool_provenance.invocation_id.clear();
1028
1029 assert_eq!(
1030 request.admission_decision(),
1031 AdmissionDecision::Quarantine {
1032 reasons: vec![AdmissionRejectionReason::ToolProvenanceRequired],
1033 }
1034 );
1035 }
1036
1037 #[test]
1038 fn narrative_without_source_anchors_is_quarantined() {
1039 let mut request = valid_request();
1040 request.source_anchors.clear();
1041
1042 assert_eq!(
1043 request.admission_decision(),
1044 AdmissionDecision::Quarantine {
1045 reasons: vec![AdmissionRejectionReason::SourceAnchorRequired],
1046 }
1047 );
1048 }
1049
1050 #[test]
1051 fn admitted_axiom_derived_memory_remains_candidate_only() {
1052 let request = valid_request();
1053
1054 assert_eq!(
1055 request.admission_decision(),
1056 AdmissionDecision::AdmitCandidate
1057 );
1058 assert_eq!(
1059 request.policy_decision().final_outcome,
1060 PolicyOutcome::Allow
1061 );
1062 assert_eq!(request.candidate_state, CandidateState::Candidate);
1063 assert!(request.explicit_non_promotion);
1064 }
1065
1066 #[test]
1067 fn missing_explicit_non_promotion_rejects() {
1068 let mut request = valid_request();
1069 request.explicit_non_promotion = false;
1070
1071 assert_eq!(
1072 request.admission_decision(),
1073 AdmissionDecision::Reject {
1074 reasons: vec![AdmissionRejectionReason::ExplicitNonPromotionRequired],
1075 }
1076 );
1077 }
1078
1079 #[test]
1080 fn product_spec_import_and_raw_unredacted_content_reject() {
1081 let mut request = valid_request();
1082 request.tool_provenance.import_class = AxiomImportClass::ProductSpecification;
1083 request.redaction_status = RedactionStatus::RawUnredacted;
1084
1085 let decision = request.admission_decision();
1086
1087 match decision {
1088 AdmissionDecision::Reject { reasons } => {
1089 assert!(
1090 reasons.contains(&AdmissionRejectionReason::ProductSpecificationImportRejected)
1091 );
1092 assert!(reasons.contains(&AdmissionRejectionReason::RedactionStatusRequired));
1093 }
1094 other => panic!("expected reject, got {other:?}"),
1095 }
1096 }
1097
1098 #[test]
1099 fn open_contradiction_cannot_be_cleanly_admitted() {
1100 let mut request = valid_request();
1101 request.contradiction_scan = ContradictionScan::OpenContradictions(vec!["ctr_01".into()]);
1102
1103 assert_eq!(
1104 request.admission_decision(),
1105 AdmissionDecision::Quarantine {
1106 reasons: vec![AdmissionRejectionReason::OpenContradiction],
1107 }
1108 );
1109 let policy = request.policy_decision();
1110 assert_eq!(policy.final_outcome, PolicyOutcome::Quarantine);
1111 assert_eq!(
1112 policy.contributing[0].rule_id.as_str(),
1113 "memory.admission.open_contradiction"
1114 );
1115 }
1116
1117 #[test]
1118 fn unscanned_contradictions_are_quarantined() {
1119 let mut request = valid_request();
1120 request.contradiction_scan = ContradictionScan::NotScanned;
1121
1122 assert_eq!(
1123 request.admission_decision(),
1124 AdmissionDecision::Quarantine {
1125 reasons: vec![AdmissionRejectionReason::ContradictionScanRequired],
1126 }
1127 );
1128 }
1129
1130 #[test]
1131 fn rejection_reasons_compose_to_policy_reject() {
1132 let mut request = valid_request();
1133 request.candidate_state = CandidateState::Doctrine;
1134 request.explicit_non_promotion = false;
1135
1136 let policy = request.policy_decision();
1137
1138 assert_eq!(policy.final_outcome, PolicyOutcome::Reject);
1139 assert_eq!(policy.contributing.len(), 2);
1140 assert!(policy.contributing.iter().any(|contribution| {
1141 contribution.rule_id.as_str() == "memory.admission.explicit_non_promotion_required"
1142 }));
1143 }
1144
1145 #[test]
1146 fn execution_receipt_enters_candidate_admission_only() {
1147 let request = AxiomMemoryAdmissionRequest::from(receipt(CapabilityTokenDecision::Allowed));
1148
1149 assert_eq!(request.candidate_state, CandidateState::Candidate);
1150 assert_eq!(request.evidence_class, EvidenceClass::Observed);
1151 assert_eq!(request.phase_context, PhaseContext::WorkRecord);
1152 assert_eq!(request.proof_state, ProofState::Partial);
1153 assert!(request.explicit_non_promotion);
1154
1155 assert_eq!(
1156 request.admission_decision(),
1157 AdmissionDecision::Quarantine {
1158 reasons: vec![AdmissionRejectionReason::ContradictionScanRequired],
1159 }
1160 );
1161 }
1162
1163 #[test]
1164 fn runtime_only_axiom_receipt_cannot_pass_high_force_semantic_use() {
1165 let mut request =
1166 AxiomMemoryAdmissionRequest::from(receipt(CapabilityTokenDecision::Allowed));
1167 request.contradiction_scan = ContradictionScan::ScannedClean;
1168
1169 let report = request.semantic_trust_report(
1170 AdmissionSemanticTrustInput::new(SemanticUse::HighForceDoctrine)
1171 .with_independent_source_families(2)
1172 .with_falsification_evidence(true)
1173 .with_unresolved_semantic_unknowns(false),
1174 );
1175
1176 assert_eq!(report.provenance_class, ProvenanceClass::RuntimeDerived);
1177 assert_eq!(
1178 report.semantic_trust.semantic_trust,
1179 SemanticTrustClass::SingleFamily
1180 );
1181 assert_eq!(report.semantic_trust.policy_outcome, PolicyOutcome::Reject);
1182 assert_eq!(report.admission_decision, AdmissionDecision::AdmitCandidate);
1183 assert!(report.explicit_non_promotion);
1184 }
1185
1186 #[test]
1187 fn admission_unknowns_warn_for_candidate_and_quarantine_for_default_context() {
1188 let mut request = valid_request();
1189 request.evidence_class = EvidenceClass::Unknown;
1190
1191 let candidate_report = request.semantic_trust_report(
1192 AdmissionSemanticTrustInput::new(SemanticUse::CandidateMemory)
1193 .with_unresolved_semantic_unknowns(true),
1194 );
1195 assert_eq!(
1196 candidate_report.provenance_class,
1197 ProvenanceClass::UnknownProvenance
1198 );
1199 assert_eq!(
1200 candidate_report.semantic_trust.semantic_trust,
1201 SemanticTrustClass::Unknown
1202 );
1203 assert_eq!(
1204 candidate_report.semantic_trust.policy_outcome,
1205 PolicyOutcome::Warn
1206 );
1207 assert_eq!(
1208 candidate_report.admission_decision,
1209 AdmissionDecision::Quarantine {
1210 reasons: vec![AdmissionRejectionReason::EvidenceClassRequired],
1211 }
1212 );
1213
1214 let context_report = request.semantic_trust_report(
1215 AdmissionSemanticTrustInput::new(SemanticUse::DefaultContext)
1216 .with_unresolved_semantic_unknowns(true),
1217 );
1218 assert_eq!(
1219 context_report.semantic_trust.policy_outcome,
1220 PolicyOutcome::Quarantine
1221 );
1222 assert_eq!(
1223 context_report.semantic_trust.claim_ceiling,
1224 ClaimCeiling::DevOnly
1225 );
1226 }
1227
1228 #[test]
1229 fn observed_admission_passes_high_force_only_with_corroboration_and_falsification() {
1230 let tool_request = valid_request();
1231
1232 let missing_falsification = tool_request.semantic_trust_report(
1233 AdmissionSemanticTrustInput::new(SemanticUse::HighForceDoctrine)
1234 .with_independent_source_families(2)
1235 .with_falsification_evidence(false)
1236 .with_unresolved_semantic_unknowns(false),
1237 );
1238 assert_eq!(
1239 missing_falsification.provenance_class,
1240 ProvenanceClass::ToolObserved
1241 );
1242 assert_eq!(
1243 missing_falsification.semantic_trust.policy_outcome,
1244 PolicyOutcome::Reject
1245 );
1246
1247 let corroborated = tool_request.semantic_trust_report(
1248 AdmissionSemanticTrustInput::new(SemanticUse::HighForceDoctrine)
1249 .with_independent_source_families(2)
1250 .with_falsification_evidence(true)
1251 .with_unresolved_semantic_unknowns(false),
1252 );
1253 assert_eq!(
1254 corroborated.semantic_trust.semantic_trust,
1255 SemanticTrustClass::FalsificationTested
1256 );
1257 assert_eq!(
1258 corroborated.semantic_trust.policy_outcome,
1259 PolicyOutcome::Allow
1260 );
1261 assert_eq!(
1262 corroborated.semantic_trust.claim_ceiling,
1263 ClaimCeiling::AuthorityGrade
1264 );
1265
1266 let mut operator_protocol_request = valid_request();
1267 operator_protocol_request.tool_provenance.import_class = AxiomImportClass::OperatorProtocol;
1268 let operator_protocol_report = operator_protocol_request.semantic_trust_report(
1269 AdmissionSemanticTrustInput::new(SemanticUse::HighForceDoctrine)
1270 .with_independent_source_families(2)
1271 .with_falsification_evidence(true)
1272 .with_unresolved_semantic_unknowns(false),
1273 );
1274 assert_eq!(
1275 operator_protocol_report.provenance_class,
1276 ProvenanceClass::ToolObserved
1277 );
1278 assert_eq!(
1279 operator_protocol_report.semantic_trust.policy_outcome,
1280 PolicyOutcome::Allow
1281 );
1282 }
1283
1284 #[test]
1285 fn semantic_allowance_does_not_replace_explicit_non_promotion() {
1286 let mut request = valid_request();
1287 request.tool_provenance.import_class = AxiomImportClass::OperatorProtocol;
1288 request.explicit_non_promotion = false;
1289
1290 let report = request.semantic_trust_report(
1291 AdmissionSemanticTrustInput::new(SemanticUse::HighForceDoctrine)
1292 .with_independent_source_families(2)
1293 .with_falsification_evidence(true)
1294 .with_unresolved_semantic_unknowns(false),
1295 );
1296
1297 assert_eq!(report.semantic_trust.policy_outcome, PolicyOutcome::Allow);
1298 assert_eq!(
1299 report.admission_decision,
1300 AdmissionDecision::Reject {
1301 reasons: vec![AdmissionRejectionReason::ExplicitNonPromotionRequired],
1302 }
1303 );
1304 assert!(!report.explicit_non_promotion);
1305 }
1306
1307 #[test]
1308 fn rejected_or_contaminated_receipt_fails_closed() {
1309 let mut receipt = receipt(CapabilityTokenDecision::Revoked);
1310 receipt.quarantine_state = BoundaryQuarantineState::Contaminated;
1311
1312 let request = AxiomMemoryAdmissionRequest::from(receipt);
1313
1314 assert_eq!(request.candidate_state, CandidateState::Candidate);
1315 assert_eq!(request.proof_state, ProofState::Broken);
1316 assert!(!request.explicit_non_promotion);
1317 match request.admission_decision() {
1318 AdmissionDecision::Reject { reasons } => {
1319 assert!(reasons.contains(&AdmissionRejectionReason::ProofStateRequired));
1320 assert!(reasons.contains(&AdmissionRejectionReason::ExplicitNonPromotionRequired));
1321 }
1322 other => panic!("expected reject, got {other:?}"),
1323 }
1324 }
1325
1326 #[test]
1327 fn receipt_without_tool_provenance_is_not_clean_admission() {
1328 let mut receipt = receipt(CapabilityTokenDecision::Allowed);
1329 receipt.tool_provenance.clear();
1330 receipt.runtime_id.clear();
1331
1332 let request = AxiomMemoryAdmissionRequest::from(receipt);
1333
1334 assert_eq!(request.evidence_class, EvidenceClass::Claimed);
1335 match request.admission_decision() {
1336 AdmissionDecision::Reject { reasons } | AdmissionDecision::Quarantine { reasons } => {
1337 assert!(reasons.contains(&AdmissionRejectionReason::ToolProvenanceRequired));
1338 }
1339 AdmissionDecision::AdmitCandidate => {
1340 panic!("missing provenance must not admit cleanly")
1341 }
1342 }
1343 }
1344
1345 #[test]
1346 fn generic_axiom_import_envelope_parses_to_candidate_request() {
1347 let json = serde_json::json!({
1348 "candidate_state": "candidate",
1349 "evidence_class": "observed",
1350 "phase_context": "check",
1351 "tool_provenance": {
1352 "tool_name": "codex",
1353 "invocation_id": "run_01",
1354 "import_class": "agent_procedure"
1355 },
1356 "source_anchors": [{
1357 "reference": "cmd:cargo test -p cortex-memory admission --lib",
1358 "kind": "artifact"
1359 }],
1360 "redaction_status": "abstracted",
1361 "proof_state": "partial",
1362 "contradiction_scan": "scanned_clean",
1363 "explicit_non_promotion": true
1364 })
1365 .to_string();
1366
1367 let request = AxiomMemoryAdmissionRequest::from_json_envelope(&json)
1368 .expect("valid generic import envelope parses");
1369
1370 assert_eq!(request.candidate_state, CandidateState::Candidate);
1371 assert_eq!(request.evidence_class, EvidenceClass::Observed);
1372 assert_eq!(
1373 request.admission_decision(),
1374 AdmissionDecision::AdmitCandidate
1375 );
1376 }
1377
1378 #[test]
1379 fn generic_axiom_import_envelope_rejects_unsupported_evidence_class() {
1380 let json = serde_json::json!({
1381 "candidate_state": "candidate",
1382 "evidence_class": "operator_truth",
1383 "phase_context": "check",
1384 "tool_provenance": {
1385 "tool_name": "codex",
1386 "invocation_id": "run_01",
1387 "import_class": "agent_procedure"
1388 },
1389 "source_anchors": [{
1390 "reference": "cmd:cargo test",
1391 "kind": "artifact"
1392 }],
1393 "redaction_status": "abstracted",
1394 "proof_state": "partial",
1395 "contradiction_scan": "scanned_clean",
1396 "explicit_non_promotion": true
1397 })
1398 .to_string();
1399
1400 let err = AxiomMemoryAdmissionRequest::from_json_envelope(&json)
1401 .expect_err("unsupported evidence class must not parse");
1402
1403 match err {
1404 AdmissionEnvelopeError::InvalidEnvelope { message } => {
1405 assert!(message.contains("operator_truth"), "message: {message}");
1406 }
1407 }
1408 }
1409
1410 #[test]
1411 fn generic_axiom_import_envelope_without_required_fields_cannot_admit() {
1412 let json = serde_json::json!({
1413 "candidate_state": "candidate",
1414 "evidence_class": "observed",
1415 "phase_context": "check",
1416 "tool_provenance": {
1417 "tool_name": "codex",
1418 "invocation_id": "run_01",
1419 "import_class": "agent_procedure"
1420 },
1421 "redaction_status": "abstracted",
1422 "proof_state": "partial",
1423 "contradiction_scan": "scanned_clean",
1424 "explicit_non_promotion": true
1425 })
1426 .to_string();
1427
1428 let err = AxiomMemoryAdmissionRequest::from_json_envelope(&json)
1429 .expect_err("missing source_anchors must not parse");
1430
1431 match err {
1432 AdmissionEnvelopeError::InvalidEnvelope { message } => {
1433 assert!(message.contains("source_anchors"), "message: {message}");
1434 }
1435 }
1436 }
1437
1438 #[test]
1439 fn generic_axiom_import_envelope_with_unscanned_contradictions_quarantines() {
1440 let json = serde_json::json!({
1441 "candidate_state": "candidate",
1442 "evidence_class": "observed",
1443 "phase_context": "check",
1444 "tool_provenance": {
1445 "tool_name": "codex",
1446 "invocation_id": "run_01",
1447 "import_class": "agent_procedure"
1448 },
1449 "source_anchors": [{
1450 "reference": "cmd:cargo test",
1451 "kind": "artifact"
1452 }],
1453 "redaction_status": "abstracted",
1454 "proof_state": "partial",
1455 "contradiction_scan": "not_scanned",
1456 "explicit_non_promotion": true
1457 })
1458 .to_string();
1459
1460 let request = AxiomMemoryAdmissionRequest::from_json_envelope(&json)
1461 .expect("known not_scanned posture parses");
1462
1463 assert_eq!(
1464 request.admission_decision(),
1465 AdmissionDecision::Quarantine {
1466 reasons: vec![AdmissionRejectionReason::ContradictionScanRequired],
1467 }
1468 );
1469 }
1470
1471 #[test]
1484 fn admission_proof_closure_report_maps_full_to_full_chain_verified() {
1485 let mut request = valid_request();
1486 request.proof_state = ProofState::FullChainVerified;
1487 let report = request.proof_closure_report();
1488 assert_eq!(report.state(), CoreProofState::FullChainVerified);
1489 assert!(report.failing_edges().is_empty());
1490 }
1491
1492 #[test]
1493 fn admission_proof_closure_report_maps_partial_and_unknown_to_partial() {
1494 for state in [ProofState::Partial, ProofState::Unknown] {
1495 let mut request = valid_request();
1496 request.proof_state = state;
1497 let report = request.proof_closure_report();
1498 assert_eq!(
1499 report.state(),
1500 CoreProofState::Partial,
1501 "{state:?} must map to typed Partial"
1502 );
1503 assert!(!report.is_full_chain_verified());
1504 assert!(!report.is_broken());
1505 }
1506 }
1507
1508 #[test]
1509 fn admission_proof_closure_report_maps_broken_to_broken() {
1510 let mut request = valid_request();
1511 request.proof_state = ProofState::Broken;
1512 let report = request.proof_closure_report();
1513 assert_eq!(report.state(), CoreProofState::Broken);
1514 assert!(report.is_broken());
1515 }
1516
1517 #[test]
1518 fn admission_proof_closure_contribution_outcomes_track_proof_state() {
1519 for (state, expected) in [
1520 (ProofState::FullChainVerified, PolicyOutcome::Allow),
1521 (ProofState::Partial, PolicyOutcome::Quarantine),
1522 (ProofState::Unknown, PolicyOutcome::Quarantine),
1523 (ProofState::Broken, PolicyOutcome::Reject),
1524 ] {
1525 let mut request = valid_request();
1526 request.proof_state = state;
1527 let contribution = request.proof_closure_contribution();
1528 assert_eq!(contribution.outcome, expected, "for {state:?}");
1529 assert_eq!(
1530 contribution.rule_id.as_str(),
1531 AXIOM_ADMISSION_PROOF_CLOSURE_RULE_ID
1532 );
1533 }
1534 }
1535
1536 #[test]
1537 fn admission_durable_gate_allows_full_chain_verified_only() {
1538 let mut request = valid_request();
1539 request.proof_state = ProofState::FullChainVerified;
1540 request
1541 .require_durable_admission_allowed()
1542 .expect("FullChainVerified must pass the durable gate");
1543 }
1544
1545 #[test]
1546 fn admission_durable_gate_refuses_partial_with_stable_invariant() {
1547 for state in [ProofState::Partial, ProofState::Unknown, ProofState::Broken] {
1548 let mut request = valid_request();
1549 request.proof_state = state;
1550 let err = request
1551 .require_durable_admission_allowed()
1552 .expect_err("non-full-chain proof state must refuse");
1553 assert!(
1554 err.to_string()
1555 .contains(AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT),
1556 "refusal must carry stable invariant for {state:?}: {err}"
1557 );
1558 }
1559 }
1560
1561 #[test]
1562 fn admission_proof_closure_invariant_keys_are_stable() {
1563 assert_eq!(
1564 AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT,
1565 "cortex_memory.admission.axiom.proof_closure"
1566 );
1567 assert_eq!(
1568 AXIOM_ADMISSION_PROOF_CLOSURE_RULE_ID,
1569 "memory.admission.axiom.proof_closure"
1570 );
1571 }
1572}