use std::fmt;
use cortex_core::ProofState as CoreProofState;
use cortex_memory::{
AdmissionDecision, AdmissionProofState, AdmissionRejectionReason, AxiomImportClass,
AxiomMemoryAdmissionRequest, CandidateState, ContradictionScan, DurableAdmissionRefusal,
EvidenceClass, PhaseContext, RedactionStatus, SourceAnchor, SourceAnchorKind, ToolProvenance,
AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT,
};
use crate::schema::{MemoryCandidate, SessionReflection};
pub const REFLECTION_ADMISSION_PROOF_CLOSURE_INVARIANT: &str =
"cortex_reflect.admission.proof_closure";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReflectionAdmissionDisposition {
Reject,
Quarantine,
}
impl fmt::Display for ReflectionAdmissionDisposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Reject => f.write_str("reject"),
Self::Quarantine => f.write_str("quarantine"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflectionAdmissionError {
pub memory_index: usize,
pub disposition: ReflectionAdmissionDisposition,
pub reasons: Vec<AdmissionRejectionReason>,
}
impl fmt::Display for ReflectionAdmissionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let memory_index = self.memory_index;
let disposition = self.disposition;
let reasons = &self.reasons;
write!(
f,
"memory_candidates[{memory_index}] admission {disposition}: {reasons:?}"
)
}
}
impl std::error::Error for ReflectionAdmissionError {}
pub fn validate_reflection_admission(
reflection: &SessionReflection,
adapter_id: &str,
raw_hash: &str,
) -> Result<(), ReflectionAdmissionError> {
for (memory_index, memory) in reflection.memory_candidates.iter().enumerate() {
let request = admission_request_for_memory(reflection, memory, adapter_id, raw_hash);
match request.admission_decision() {
AdmissionDecision::AdmitCandidate => {}
AdmissionDecision::Reject { reasons } => {
return Err(ReflectionAdmissionError {
memory_index,
disposition: ReflectionAdmissionDisposition::Reject,
reasons,
});
}
AdmissionDecision::Quarantine { reasons } => {
return Err(ReflectionAdmissionError {
memory_index,
disposition: ReflectionAdmissionDisposition::Quarantine,
reasons,
});
}
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflectionDurableAdmissionRefusal {
pub memory_index: usize,
pub axiom_refusal: DurableAdmissionRefusal,
}
impl fmt::Display for ReflectionDurableAdmissionRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let memory_index = self.memory_index;
let axiom_refusal = &self.axiom_refusal;
write!(
f,
"invariant={REFLECTION_ADMISSION_PROOF_CLOSURE_INVARIANT} memory_candidates[{memory_index}] {axiom_refusal}"
)
}
}
impl std::error::Error for ReflectionDurableAdmissionRefusal {}
pub fn require_reflection_durable_admission_allowed(
reflection: &SessionReflection,
adapter_id: &str,
raw_hash: &str,
) -> Result<(), ReflectionDurableAdmissionRefusal> {
for (memory_index, memory) in reflection.memory_candidates.iter().enumerate() {
let request = admission_request_for_memory(reflection, memory, adapter_id, raw_hash);
if let Err(axiom_refusal) = request.require_durable_admission_allowed() {
return Err(ReflectionDurableAdmissionRefusal {
memory_index,
axiom_refusal,
});
}
}
Ok(())
}
#[must_use]
pub fn reflection_memory_proof_state(
reflection: &SessionReflection,
memory_index: usize,
adapter_id: &str,
raw_hash: &str,
) -> Option<CoreProofState> {
let memory = reflection.memory_candidates.get(memory_index)?;
let request = admission_request_for_memory(reflection, memory, adapter_id, raw_hash);
Some(request.proof_closure_report().state())
}
#[must_use]
pub const fn axiom_admission_proof_closure_invariant() -> &'static str {
AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT
}
#[must_use]
pub fn admission_request_for_memory(
reflection: &SessionReflection,
memory: &MemoryCandidate,
adapter_id: &str,
raw_hash: &str,
) -> AxiomMemoryAdmissionRequest {
let source_anchors = source_anchors_for_memory(reflection, memory);
let proof_state = if source_anchors.is_empty() {
AdmissionProofState::Unknown
} else {
AdmissionProofState::Partial
};
AxiomMemoryAdmissionRequest {
candidate_state: CandidateState::Candidate,
evidence_class: EvidenceClass::Inferred,
phase_context: PhaseContext::Check,
tool_provenance: ToolProvenance::new(
format!("cortex-reflect:{adapter_id}"),
raw_hash,
AxiomImportClass::AgentProcedure,
),
source_anchors,
redaction_status: RedactionStatus::Abstracted,
proof_state,
contradiction_scan: contradiction_scan_for_reflection(reflection),
explicit_non_promotion: true,
}
}
fn source_anchors_for_memory(
reflection: &SessionReflection,
memory: &MemoryCandidate,
) -> Vec<SourceAnchor> {
memory
.source_episode_indexes
.iter()
.filter_map(|idx| reflection.episode_candidates.get(*idx))
.flat_map(|episode| &episode.source_event_ids)
.map(|event_id| SourceAnchor::new(event_id.to_string(), SourceAnchorKind::Event))
.collect()
}
fn contradiction_scan_for_reflection(reflection: &SessionReflection) -> ContradictionScan {
if reflection.contradictions.is_empty() {
ContradictionScan::ScannedClean
} else {
ContradictionScan::OpenContradictions(
reflection
.contradictions
.iter()
.enumerate()
.map(|(idx, _)| format!("reflection.contradictions[{idx}]"))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use cortex_core::TraceId;
use cortex_memory::{AdmissionDecision, AdmissionRejectionReason};
use super::*;
use crate::schema::{EpisodeCandidate, InitialSalience, MemoryType};
fn valid_reflection() -> SessionReflection {
SessionReflection {
trace_id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV"
.parse::<TraceId>()
.expect("valid trace id"),
episode_candidates: vec![EpisodeCandidate {
summary: "Reflection summary".to_string(),
source_event_ids: vec!["evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"
.parse()
.expect("valid event id")],
domains: vec!["agents".to_string()],
entities: vec!["Cortex".to_string()],
candidate_meaning: Some("candidate meaning".to_string()),
confidence: 0.8,
}],
memory_candidates: vec![MemoryCandidate {
memory_type: MemoryType::Strategic,
claim: "Reflection memory remains candidate-only.".to_string(),
source_episode_indexes: vec![0],
applies_when: vec!["reflecting".to_string()],
does_not_apply_when: vec!["promoting".to_string()],
confidence: 0.8,
initial_salience: InitialSalience {
reusability: 0.5,
consequence: 0.5,
emotional_charge: 0.0,
},
}],
contradictions: Vec::new(),
doctrine_suggestions: Vec::new(),
}
}
#[test]
fn reflected_memory_builds_candidate_only_admission_request() {
let reflection = valid_reflection();
let request = admission_request_for_memory(
&reflection,
&reflection.memory_candidates[0],
"fixed",
"hash_01",
);
assert_eq!(request.candidate_state, CandidateState::Candidate);
assert_eq!(request.evidence_class, EvidenceClass::Inferred);
assert_eq!(request.phase_context, PhaseContext::Check);
assert_eq!(request.redaction_status, RedactionStatus::Abstracted);
assert_eq!(request.proof_state, AdmissionProofState::Partial);
assert!(request.explicit_non_promotion);
assert_eq!(request.source_anchors.len(), 1);
assert_eq!(
request.admission_decision(),
AdmissionDecision::AdmitCandidate
);
}
#[test]
fn reflected_memory_without_source_anchor_fails_admission() {
let mut reflection = valid_reflection();
reflection.episode_candidates[0].source_event_ids.clear();
let err = validate_reflection_admission(&reflection, "fixed", "hash_01")
.expect_err("missing anchors must fail admission");
assert_eq!(err.memory_index, 0);
assert_eq!(err.disposition, ReflectionAdmissionDisposition::Reject);
assert!(err
.reasons
.contains(&AdmissionRejectionReason::SourceAnchorRequired));
assert!(err
.reasons
.contains(&AdmissionRejectionReason::ProofStateRequired));
}
#[test]
fn reflected_memory_with_open_contradiction_is_quarantined() {
let mut reflection = valid_reflection();
reflection
.contradictions
.push(serde_json::json!({"claim": "conflict"}));
let err = validate_reflection_admission(&reflection, "fixed", "hash_01")
.expect_err("open contradictions must fail admission");
assert_eq!(err.memory_index, 0);
assert_eq!(err.disposition, ReflectionAdmissionDisposition::Quarantine);
assert_eq!(
err.reasons,
vec![AdmissionRejectionReason::OpenContradiction]
);
}
#[test]
fn reflection_durable_gate_refuses_default_partial_proof_state() {
let reflection = valid_reflection();
let err = require_reflection_durable_admission_allowed(&reflection, "fixed", "hash_01")
.expect_err("default reflection envelope is Partial; durable gate must refuse");
assert_eq!(err.memory_index, 0);
assert_eq!(err.axiom_refusal.proof_state, CoreProofState::Partial);
assert!(
err.to_string()
.contains(REFLECTION_ADMISSION_PROOF_CLOSURE_INVARIANT),
"refusal must carry stable invariant: {err}"
);
assert!(
err.to_string()
.contains(AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT),
"refusal must also surface the upstream AXIOM admission invariant: {err}"
);
}
#[test]
fn reflection_durable_gate_refuses_when_lineage_is_missing() {
let mut reflection = valid_reflection();
reflection.episode_candidates[0].source_event_ids.clear();
let err = require_reflection_durable_admission_allowed(&reflection, "fixed", "hash_01")
.expect_err("missing lineage must refuse durable promotion");
assert_eq!(err.memory_index, 0);
assert_eq!(err.axiom_refusal.proof_state, CoreProofState::Partial);
}
#[test]
fn reflection_memory_proof_state_helper_returns_observed_state() {
let reflection = valid_reflection();
let state = reflection_memory_proof_state(&reflection, 0, "fixed", "hash_01")
.expect("memory exists");
assert_eq!(state, CoreProofState::Partial);
assert!(reflection_memory_proof_state(&reflection, 99, "fixed", "hash_01").is_none());
}
#[test]
fn reflection_durable_gate_invariant_key_is_stable() {
assert_eq!(
REFLECTION_ADMISSION_PROOF_CLOSURE_INVARIANT,
"cortex_reflect.admission.proof_closure"
);
assert_eq!(
axiom_admission_proof_closure_invariant(),
AXIOM_ADMISSION_PROOF_CLOSURE_INVARIANT
);
}
}