use std::fmt;
use cortex_core::{EventId, EventSource};
use crate::schema::{MemoryCandidate, MemoryType, SessionReflection};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflectionAuthorityError {
pub memory_index: usize,
pub invariant: &'static str,
pub detail: String,
}
impl fmt::Display for ReflectionAuthorityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let memory_index = self.memory_index;
let invariant = self.invariant;
let detail = &self.detail;
write!(
f,
"memory_candidates[{memory_index}] authority rejected ({invariant}): {detail}"
)
}
}
impl std::error::Error for ReflectionAuthorityError {}
pub fn validate_reflection_authority<F, E>(
reflection: &SessionReflection,
mut source_lookup: F,
) -> Result<(), ReflectionAuthorityError>
where
F: FnMut(&EventId) -> Result<Option<EventSource>, E>,
E: fmt::Display,
{
for (memory_index, memory) in reflection.memory_candidates.iter().enumerate() {
if memory.memory_type != MemoryType::Strategic {
continue;
}
let mut known_sources = Vec::new();
for event_id in source_event_ids_for_memory(reflection, memory) {
match source_lookup(event_id) {
Ok(Some(source)) => known_sources.push(source),
Ok(None) => {}
Err(err) => {
return Err(ReflectionAuthorityError {
memory_index,
invariant: "reflection_authority.source_lookup_failed",
detail: format!("source lookup failed for {event_id}: {err}"),
});
}
}
}
if !known_sources.is_empty() && known_sources.iter().all(is_tool_source) {
return Err(ReflectionAuthorityError {
memory_index,
invariant: "reflection_authority.tool_origin_only_strategic_support",
detail: "strategic memory has only tool-origin source support".to_string(),
});
}
}
Ok(())
}
fn source_event_ids_for_memory<'a>(
reflection: &'a SessionReflection,
memory: &'a MemoryCandidate,
) -> impl Iterator<Item = &'a EventId> + 'a {
memory
.source_episode_indexes
.iter()
.filter_map(|idx| reflection.episode_candidates.get(*idx))
.flat_map(|episode| episode.source_event_ids.iter())
}
fn is_tool_source(source: &EventSource) -> bool {
matches!(source, EventSource::Tool { .. })
}
#[cfg(test)]
mod tests {
use cortex_core::TraceId;
use super::*;
use crate::schema::{EpisodeCandidate, InitialSalience};
fn event_id() -> EventId {
"evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"
.parse()
.expect("valid event id")
}
fn reflection(memory_type: MemoryType) -> SessionReflection {
SessionReflection {
trace_id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV"
.parse::<TraceId>()
.expect("valid trace id"),
episode_candidates: vec![EpisodeCandidate {
summary: "tool reported a local result".to_string(),
source_event_ids: vec![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,
claim: "Future plans should prefer this tool path.".to_string(),
source_episode_indexes: vec![0],
applies_when: vec!["planning".to_string()],
does_not_apply_when: vec!["tool evidence is uncorroborated".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 strategic_memory_with_only_tool_origin_support_is_rejected() {
let err = validate_reflection_authority(&reflection(MemoryType::Strategic), |_| {
Ok::<_, std::convert::Infallible>(Some(EventSource::Tool {
name: "mcp-tool".to_string(),
}))
})
.expect_err("tool-only strategic support must fail closed");
assert_eq!(err.memory_index, 0);
assert_eq!(
err.invariant,
"reflection_authority.tool_origin_only_strategic_support"
);
}
#[test]
fn strategic_memory_with_user_corroboration_is_allowed() {
validate_reflection_authority(&reflection(MemoryType::Strategic), |_| {
Ok::<_, std::convert::Infallible>(Some(EventSource::ManualCorrection))
})
.expect("operator/user source can corroborate strategic support");
}
#[test]
fn non_strategic_memory_with_tool_origin_support_is_allowed() {
validate_reflection_authority(&reflection(MemoryType::Semantic), |_| {
Ok::<_, std::convert::Infallible>(Some(EventSource::Tool {
name: "mcp-tool".to_string(),
}))
})
.expect("tool-origin support is not categorically banned");
}
}