memory_crystal/
decoder.rs1use crate::tile::{Tile, TileId};
2
3#[derive(Debug, Clone)]
5pub struct Reconstruction {
6 pub content: String,
7 pub tile_id: TileId,
8 pub confidence: f64,
9 pub inferred_facts: Vec<String>,
10 pub preserved_facts: Vec<String>,
11}
12
13pub struct TileDecoder {
15 pub context_window: usize,
16}
17
18impl Default for TileDecoder {
19 fn default() -> Self {
20 Self {
21 context_window: 4096,
22 }
23 }
24}
25
26impl TileDecoder {
27 pub fn decode(&self, tile: &Tile, context: &str) -> Reconstruction {
32 let context_lower = context.to_lowercase();
33
34 let preserved_facts: Vec<String> = tile
36 .constraints
37 .values()
38 .filter(|v| context_lower.contains(&v.to_lowercase()))
39 .cloned()
40 .collect();
41
42 let mut inferred_facts = Vec::new();
44 for cue in &tile.context_required {
45 if !context_lower.contains(&cue.to_lowercase()) {
46 inferred_facts.push(format!("missing context: {}", cue));
47 }
48 }
49
50 let mut content = tile.summary.clone();
52 if !preserved_facts.is_empty() {
53 content.push_str("\n\nVerified facts: ");
54 content.push_str(&preserved_facts.join(", "));
55 }
56
57 let total_cues = tile.context_required.len().max(1);
59 let matched_cues = tile
60 .context_required
61 .iter()
62 .filter(|c| context_lower.contains(&c.to_lowercase()))
63 .count();
64 let context_confidence = matched_cues as f64 / total_cues as f64;
65
66 let access_boost = (tile.access_count as f64).log2().max(0.0) * 0.05;
68 let confidence = (context_confidence * 0.7 + tile.valence * 0.2 + access_boost).clamp(0.0, 1.0);
69
70 Reconstruction {
71 content,
72 tile_id: tile.id.clone(),
73 confidence,
74 inferred_facts,
75 preserved_facts,
76 }
77 }
78
79 pub fn decode_collective(&self, tiles: &[&Tile], context: &str) -> Reconstruction {
81 if tiles.is_empty() {
82 return Reconstruction {
83 content: String::new(),
84 tile_id: TileId::new(),
85 confidence: 0.0,
86 inferred_facts: vec![],
87 preserved_facts: vec![],
88 };
89 }
90
91 let anchor = tiles
93 .iter()
94 .max_by(|a, b| a.valence.partial_cmp(&b.valence).unwrap_or(std::cmp::Ordering::Equal))
95 .unwrap();
96
97 let content_parts: Vec<String> = tiles.iter().map(|t| t.summary.clone()).collect();
98 let _context_lower = context.to_lowercase();
99
100 let mut all_preserved = Vec::new();
101 let mut all_inferred = Vec::new();
102 let mut total_confidence = 0.0;
103
104 for tile in tiles {
105 let rec = self.decode(tile, context);
106 all_preserved.extend(rec.preserved_facts);
107 all_inferred.extend(rec.inferred_facts);
108 total_confidence += rec.confidence;
109 }
110
111 all_preserved.sort();
112 all_preserved.dedup();
113 all_inferred.sort();
114 all_inferred.dedup();
115
116 let mut combined = content_parts.join("\n---\n");
118 if combined.len() > self.context_window {
119 combined.truncate(self.context_window);
120 }
121
122 if !all_preserved.is_empty() {
123 combined.push_str("\n\nPreserved facts: ");
124 combined.push_str(&all_preserved.join(", "));
125 }
126
127 let avg_confidence = total_confidence / tiles.len() as f64;
128
129 Reconstruction {
130 content: combined,
131 tile_id: anchor.id.clone(),
132 confidence: avg_confidence,
133 inferred_facts: all_inferred,
134 preserved_facts: all_preserved,
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use std::collections::HashMap;
143
144 fn test_tile(summary: &str, constraints: HashMap<String, String>, valence: f64) -> Tile {
145 Tile {
146 id: TileId::new(),
147 source_hash: String::new(),
148 constraints,
149 summary: summary.into(),
150 context_required: vec![],
151 valence,
152 created_at: chrono::Utc::now(),
153 accessed_at: chrono::Utc::now(),
154 access_count: 1,
155 generation: 0,
156 parent_id: None,
157 }
158 }
159
160 #[test]
161 fn decode_basic() {
162 let decoder = TileDecoder::default();
163 let tile = test_tile(
164 "Alice discovered a new algorithm.",
165 HashMap::from([("person".into(), "Alice".into())]),
166 0.8,
167 );
168 let rec = decoder.decode(&tile, "Alice was working on algorithms");
169 assert!(rec.confidence > 0.0);
170 assert!(rec.content.contains("Alice discovered"));
171 }
172
173 #[test]
174 fn decode_with_preserved_facts() {
175 let decoder = TileDecoder::default();
176 let tile = test_tile(
177 "Meeting notes",
178 HashMap::from([("place".into(), "Seattle".into())]),
179 0.5,
180 );
181 let rec = decoder.decode(&tile, "We went to Seattle for the meeting.");
182 assert!(rec.preserved_facts.contains(&"Seattle".to_string()));
183 }
184
185 #[test]
186 fn decode_collective() {
187 let decoder = TileDecoder::default();
188 let t1 = test_tile("First part", HashMap::new(), 0.6);
189 let t2 = test_tile("Second part", HashMap::new(), 0.9);
190 let rec = decoder.decode_collective(&[&t1, &t2], "context");
191 assert!(rec.content.contains("First part"));
192 assert!(rec.content.contains("Second part"));
193 assert!(rec.confidence > 0.0);
194 }
195
196 #[test]
197 fn decode_empty_tiles() {
198 let decoder = TileDecoder::default();
199 let rec = decoder.decode_collective(&[], "context");
200 assert_eq!(rec.confidence, 0.0);
201 assert!(rec.content.is_empty());
202 }
203
204 #[test]
205 fn decode_missing_context() {
206 let decoder = TileDecoder::default();
207 let mut tile = test_tile("Test", HashMap::new(), 0.5);
208 tile.context_required = vec!["Python".into(), "ML".into()];
209 let rec = decoder.decode(&tile, "We discussed Rust and databases");
210 assert!(rec
211 .inferred_facts
212 .iter()
213 .any(|f| f.contains("missing context")));
214 }
215}