Skip to main content

cortex_reflect/
authority.rs

1//! Reflection authority gates for source-origin-sensitive memory candidates.
2//!
3//! Reflection output is an interpretation over already-recorded events. A
4//! tool-originated event may support tactical facts about a tool result, but it
5//! cannot, by itself, justify a strategic memory that conditions future action.
6
7use std::fmt;
8
9use cortex_core::{EventId, EventSource};
10
11use crate::schema::{MemoryCandidate, MemoryType, SessionReflection};
12
13/// Deterministic authority failure for one reflected memory candidate.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ReflectionAuthorityError {
16    /// Zero-based index into `SessionReflection::memory_candidates`.
17    pub memory_index: usize,
18    /// Stable machine-readable invariant that failed.
19    pub invariant: &'static str,
20    /// Operator-facing failure detail.
21    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
38/// Validate source-origin authority constraints that require store-backed event
39/// source lookup.
40///
41/// Missing source rows are not treated as non-tool corroboration here. Other
42/// proof-closure lanes decide whether unresolved anchors can be used by durable
43/// consumers. This gate only enforces the known, narrower rule that a
44/// `Strategic` memory cannot rest solely on known tool-origin events.
45pub 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}