Skip to main content

cortex_reflect/
schema.rs

1//! Reflection JSON contracts from BUILD_SPEC §13.
2
3use cortex_core::{EventId, MemoryId, TraceId};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// Post-hoc reflection output for one trace.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9#[serde(deny_unknown_fields)]
10pub struct SessionReflection {
11    /// Trace being reflected.
12    pub trace_id: TraceId,
13    /// Candidate episodes extracted from source events.
14    pub episode_candidates: Vec<EpisodeCandidate>,
15    /// Candidate memories proposed from the reflected session.
16    pub memory_candidates: Vec<MemoryCandidate>,
17    /// Opaque contradiction candidates until the store-backed lane defines them.
18    pub contradictions: Vec<serde_json::Value>,
19    /// Opaque doctrine suggestions; reflection must never promote these directly.
20    pub doctrine_suggestions: Vec<serde_json::Value>,
21}
22
23/// Candidate episode summary produced by reflection.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct EpisodeCandidate {
27    /// Human-readable summary of the episode.
28    pub summary: String,
29    /// Source events supporting the episode.
30    pub source_event_ids: Vec<EventId>,
31    /// Domains observed in the episode.
32    pub domains: Vec<String>,
33    /// Entities mentioned in the episode.
34    pub entities: Vec<String>,
35    /// Optional interpretation of why the episode matters.
36    pub candidate_meaning: Option<String>,
37    /// Model confidence in `[0.0, 1.0]`.
38    #[schemars(range(min = 0.0, max = 1.0))]
39    pub confidence: f64,
40}
41
42/// Candidate memory type taxonomy from BUILD_SPEC §13.1.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "lowercase")]
45pub enum MemoryType {
46    /// Semantic fact or concept.
47    Semantic,
48    /// Episodic recollection.
49    Episodic,
50    /// Procedure or process.
51    Procedural,
52    /// Strategy or preference over action.
53    Strategic,
54    /// Affective signal.
55    Affective,
56    /// Correction learned from feedback.
57    Correction,
58}
59
60/// Salience dimensions assigned before durable memory acceptance.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
62#[serde(deny_unknown_fields)]
63pub struct InitialSalience {
64    /// Expected reuse value in `[0.0, 1.0]`.
65    #[schemars(range(min = 0.0, max = 1.0))]
66    pub reusability: f64,
67    /// Expected consequence of getting this wrong in `[0.0, 1.0]`.
68    #[schemars(range(min = 0.0, max = 1.0))]
69    pub consequence: f64,
70    /// Emotional or preference intensity in `[0.0, 1.0]`.
71    #[schemars(range(min = 0.0, max = 1.0))]
72    pub emotional_charge: f64,
73}
74
75/// Candidate memory proposed by reflection.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct MemoryCandidate {
79    /// Type of memory proposed.
80    pub memory_type: MemoryType,
81    /// Atomic claim to be considered by the human-gated memory path.
82    pub claim: String,
83    /// Zero-based indexes into [`SessionReflection::episode_candidates`].
84    pub source_episode_indexes: Vec<usize>,
85    /// Conditions where the claim applies.
86    pub applies_when: Vec<String>,
87    /// Conditions where the claim should not be applied.
88    pub does_not_apply_when: Vec<String>,
89    /// Model confidence in `[0.0, 1.0]`.
90    #[schemars(range(min = 0.0, max = 1.0))]
91    pub confidence: f64,
92    /// Initial salience proposal.
93    pub initial_salience: InitialSalience,
94}
95
96/// Batch wrapper for extracted principle candidates.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
98#[serde(deny_unknown_fields)]
99pub struct PrincipleCandidateBatch {
100    /// Proposed principles.
101    pub candidate_principles: Vec<PrincipleCandidate>,
102}
103
104/// Principle candidate from BUILD_SPEC §13.2.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
106#[serde(deny_unknown_fields)]
107pub struct PrincipleCandidate {
108    /// Principle statement.
109    pub statement: String,
110    /// Supporting accepted memories.
111    pub supporting_memory_ids: Vec<MemoryId>,
112    /// Contradicting accepted memories.
113    pub contradicting_memory_ids: Vec<MemoryId>,
114    /// Domains where the pattern was observed.
115    pub domains_observed: Vec<String>,
116    /// Conditions where the principle applies.
117    pub applies_when: Vec<String>,
118    /// Conditions where the principle should not be applied.
119    pub does_not_apply_when: Vec<String>,
120    /// Plausible narrower or competing explanations.
121    pub alternative_interpretations: Vec<String>,
122    /// Model confidence in `[0.0, 1.0]`.
123    #[schemars(range(min = 0.0, max = 1.0))]
124    pub confidence: f64,
125    /// Risk score for overgeneralisation in `[0.0, 1.0]`.
126    #[schemars(range(min = 0.0, max = 1.0))]
127    pub overgeneralisation_risk: f64,
128}
129
130impl SessionReflection {
131    /// Validate invariants that serde shape checks cannot express.
132    pub(crate) fn validate(&self) -> Result<(), String> {
133        for (idx, episode) in self.episode_candidates.iter().enumerate() {
134            validate_score(
135                episode.confidence,
136                &format!("episode_candidates[{idx}].confidence"),
137            )?;
138        }
139
140        let episode_len = self.episode_candidates.len();
141        for (idx, memory) in self.memory_candidates.iter().enumerate() {
142            validate_score(
143                memory.confidence,
144                &format!("memory_candidates[{idx}].confidence"),
145            )?;
146            validate_score(
147                memory.initial_salience.reusability,
148                &format!("memory_candidates[{idx}].initial_salience.reusability"),
149            )?;
150            validate_score(
151                memory.initial_salience.consequence,
152                &format!("memory_candidates[{idx}].initial_salience.consequence"),
153            )?;
154            validate_score(
155                memory.initial_salience.emotional_charge,
156                &format!("memory_candidates[{idx}].initial_salience.emotional_charge"),
157            )?;
158            for source_idx in &memory.source_episode_indexes {
159                if *source_idx >= episode_len {
160                    return Err(format!(
161                        "memory_candidates[{idx}].source_episode_indexes contains {source_idx}, but only {episode_len} episode candidates exist"
162                    ));
163                }
164            }
165        }
166
167        Ok(())
168    }
169}
170
171impl PrincipleCandidateBatch {
172    /// Validate invariants that serde shape checks cannot express.
173    pub(crate) fn validate(&self) -> Result<(), String> {
174        for (idx, candidate) in self.candidate_principles.iter().enumerate() {
175            validate_score(
176                candidate.confidence,
177                &format!("candidate_principles[{idx}].confidence"),
178            )?;
179            validate_score(
180                candidate.overgeneralisation_risk,
181                &format!("candidate_principles[{idx}].overgeneralisation_risk"),
182            )?;
183        }
184
185        Ok(())
186    }
187}
188
189fn validate_score(value: f64, field: &str) -> Result<(), String> {
190    if value.is_finite() && (0.0..=1.0).contains(&value) {
191        Ok(())
192    } else {
193        Err(format!("{field} must be between 0.0 and 1.0"))
194    }
195}