crtx-reflect 0.1.1

Reflection orchestration, prompts, candidate parsing, and schema validation.
Documentation
//! Reflection authority gates for source-origin-sensitive memory candidates.
//!
//! Reflection output is an interpretation over already-recorded events. A
//! tool-originated event may support tactical facts about a tool result, but it
//! cannot, by itself, justify a strategic memory that conditions future action.

use std::fmt;

use cortex_core::{EventId, EventSource};

use crate::schema::{MemoryCandidate, MemoryType, SessionReflection};

/// Deterministic authority failure for one reflected memory candidate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflectionAuthorityError {
    /// Zero-based index into `SessionReflection::memory_candidates`.
    pub memory_index: usize,
    /// Stable machine-readable invariant that failed.
    pub invariant: &'static str,
    /// Operator-facing failure detail.
    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 {}

/// Validate source-origin authority constraints that require store-backed event
/// source lookup.
///
/// Missing source rows are not treated as non-tool corroboration here. Other
/// proof-closure lanes decide whether unresolved anchors can be used by durable
/// consumers. This gate only enforces the known, narrower rule that a
/// `Strategic` memory cannot rest solely on known tool-origin events.
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");
    }
}