cortex_reflect/
authority.rs1use std::fmt;
8
9use cortex_core::{EventId, EventSource};
10
11use crate::schema::{MemoryCandidate, MemoryType, SessionReflection};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ReflectionAuthorityError {
16 pub memory_index: usize,
18 pub invariant: &'static str,
20 pub detail: String,
22}
23
24impl fmt::Display for ReflectionAuthorityError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 let memory_index = self.memory_index;
27 let invariant = self.invariant;
28 let detail = &self.detail;
29 write!(
30 f,
31 "memory_candidates[{memory_index}] authority rejected ({invariant}): {detail}"
32 )
33 }
34}
35
36impl std::error::Error for ReflectionAuthorityError {}
37
38pub fn validate_reflection_authority<F, E>(
46 reflection: &SessionReflection,
47 mut source_lookup: F,
48) -> Result<(), ReflectionAuthorityError>
49where
50 F: FnMut(&EventId) -> Result<Option<EventSource>, E>,
51 E: fmt::Display,
52{
53 for (memory_index, memory) in reflection.memory_candidates.iter().enumerate() {
54 if memory.memory_type != MemoryType::Strategic {
55 continue;
56 }
57
58 let mut known_sources = Vec::new();
59 for event_id in source_event_ids_for_memory(reflection, memory) {
60 match source_lookup(event_id) {
61 Ok(Some(source)) => known_sources.push(source),
62 Ok(None) => {}
63 Err(err) => {
64 return Err(ReflectionAuthorityError {
65 memory_index,
66 invariant: "reflection_authority.source_lookup_failed",
67 detail: format!("source lookup failed for {event_id}: {err}"),
68 });
69 }
70 }
71 }
72
73 if !known_sources.is_empty() && known_sources.iter().all(is_tool_source) {
74 return Err(ReflectionAuthorityError {
75 memory_index,
76 invariant: "reflection_authority.tool_origin_only_strategic_support",
77 detail: "strategic memory has only tool-origin source support".to_string(),
78 });
79 }
80 }
81
82 Ok(())
83}
84
85fn source_event_ids_for_memory<'a>(
86 reflection: &'a SessionReflection,
87 memory: &'a MemoryCandidate,
88) -> impl Iterator<Item = &'a EventId> + 'a {
89 memory
90 .source_episode_indexes
91 .iter()
92 .filter_map(|idx| reflection.episode_candidates.get(*idx))
93 .flat_map(|episode| episode.source_event_ids.iter())
94}
95
96fn is_tool_source(source: &EventSource) -> bool {
97 matches!(source, EventSource::Tool { .. })
98}
99
100#[cfg(test)]
101mod tests {
102 use cortex_core::TraceId;
103
104 use super::*;
105 use crate::schema::{EpisodeCandidate, InitialSalience};
106
107 fn event_id() -> EventId {
108 "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV"
109 .parse()
110 .expect("valid event id")
111 }
112
113 fn reflection(memory_type: MemoryType) -> SessionReflection {
114 SessionReflection {
115 trace_id: "trc_01ARZ3NDEKTSV4RRFFQ69G5FAV"
116 .parse::<TraceId>()
117 .expect("valid trace id"),
118 episode_candidates: vec![EpisodeCandidate {
119 summary: "tool reported a local result".to_string(),
120 source_event_ids: vec![event_id()],
121 domains: vec!["agents".to_string()],
122 entities: vec!["Cortex".to_string()],
123 candidate_meaning: Some("candidate meaning".to_string()),
124 confidence: 0.8,
125 }],
126 memory_candidates: vec![MemoryCandidate {
127 memory_type,
128 claim: "Future plans should prefer this tool path.".to_string(),
129 source_episode_indexes: vec![0],
130 applies_when: vec!["planning".to_string()],
131 does_not_apply_when: vec!["tool evidence is uncorroborated".to_string()],
132 confidence: 0.8,
133 initial_salience: InitialSalience {
134 reusability: 0.5,
135 consequence: 0.5,
136 emotional_charge: 0.0,
137 },
138 }],
139 contradictions: Vec::new(),
140 doctrine_suggestions: Vec::new(),
141 }
142 }
143
144 #[test]
145 fn strategic_memory_with_only_tool_origin_support_is_rejected() {
146 let err = validate_reflection_authority(&reflection(MemoryType::Strategic), |_| {
147 Ok::<_, std::convert::Infallible>(Some(EventSource::Tool {
148 name: "mcp-tool".to_string(),
149 }))
150 })
151 .expect_err("tool-only strategic support must fail closed");
152
153 assert_eq!(err.memory_index, 0);
154 assert_eq!(
155 err.invariant,
156 "reflection_authority.tool_origin_only_strategic_support"
157 );
158 }
159
160 #[test]
161 fn strategic_memory_with_user_corroboration_is_allowed() {
162 validate_reflection_authority(&reflection(MemoryType::Strategic), |_| {
163 Ok::<_, std::convert::Infallible>(Some(EventSource::ManualCorrection))
164 })
165 .expect("operator/user source can corroborate strategic support");
166 }
167
168 #[test]
169 fn non_strategic_memory_with_tool_origin_support_is_allowed() {
170 validate_reflection_authority(&reflection(MemoryType::Semantic), |_| {
171 Ok::<_, std::convert::Infallible>(Some(EventSource::Tool {
172 name: "mcp-tool".to_string(),
173 }))
174 })
175 .expect("tool-origin support is not categorically banned");
176 }
177}