memory_crystal/
telephone.rs1use std::collections::HashMap;
2
3use crate::encoder::ConstraintExtractor;
4use crate::tile::Tile;
5
6pub struct TelephoneChain {
9 tiles: Vec<Tile>,
10 fact_tracker: FactTracker,
11 original_facts: HashMap<String, String>,
12}
13
14impl TelephoneChain {
15 pub fn new(source: &str) -> Self {
17 let extractor = ConstraintExtractor;
18 let original_facts = extractor.extract(source);
19
20 let encoder = crate::encoder::TileEncoder::default();
21 let tile = encoder.encode(source, &crate::tile::SalienceMap::default());
22
23 let mut fact_tracker = FactTracker::new(&original_facts);
24 fact_tracker.record_round(&tile);
25
26 Self {
27 tiles: vec![tile],
28 fact_tracker,
29 original_facts,
30 }
31 }
32
33 pub fn add_round(&mut self, reconstruction: &str) -> &Tile {
35 let encoder = crate::encoder::TileEncoder::default();
36 let parent_id = self.tiles.last().unwrap().id.clone();
37
38 let mut tile = encoder.encode(reconstruction, &crate::tile::SalienceMap::default());
39 tile.generation = self.tiles.len() as u32;
40 tile.parent_id = Some(parent_id);
41
42 self.fact_tracker.record_round(&tile);
43 self.tiles.push(tile);
44 self.tiles.last().unwrap()
45 }
46
47 pub fn len(&self) -> usize {
49 self.tiles.len()
50 }
51
52 pub fn is_empty(&self) -> bool {
54 self.tiles.is_empty()
55 }
56
57 pub fn get(&self, round: usize) -> Option<&Tile> {
59 self.tiles.get(round)
60 }
61
62 pub fn crystallization_point(&self) -> Option<usize> {
66 if self.tiles.len() < 3 {
67 return None;
68 }
69
70 let mut prev_rate = 1.0;
71 for round in 0..self.tiles.len() {
72 let rate = self.fact_tracker.survival_rate(round);
73 let drop = prev_rate - rate;
74 if drop < 0.05 && round > 0 {
76 return Some(round);
77 }
78 prev_rate = rate;
79 }
80
81 None
82 }
83
84 pub fn fact_timeline(&self) -> HashMap<String, Vec<bool>> {
86 self.fact_tracker.timeline.clone()
87 }
88
89 pub fn original_facts(&self) -> &HashMap<String, String> {
91 &self.original_facts
92 }
93}
94
95pub struct FactTracker {
97 facts: HashMap<String, String>,
98 timeline: HashMap<String, Vec<bool>>,
99}
100
101impl FactTracker {
102 pub fn new(facts: &HashMap<String, String>) -> Self {
104 Self {
105 facts: facts.clone(),
106 timeline: facts.keys().map(|k| (k.clone(), vec![])).collect(),
107 }
108 }
109
110 pub fn record_round(&mut self, tile: &Tile) {
112 let tile_text = format!("{} {}", tile.summary, tile.constraints.values().cloned().collect::<Vec<_>>().join(" ")).to_lowercase();
113
114 for (key, original_value) in &self.facts {
115 let survived = tile_text.contains(&original_value.to_lowercase());
116 self.timeline.entry(key.clone()).or_default().push(survived);
117 }
118 }
119
120 pub fn survival_rate(&self, round: usize) -> f64 {
122 if self.timeline.is_empty() {
123 return 1.0;
124 }
125
126 let survived: usize = self
127 .timeline
128 .values()
129 .map(|v| v.get(round).copied().unwrap_or(false) as usize)
130 .sum();
131
132 survived as f64 / self.timeline.len() as f64
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn telephone_chain_basic() {
142 let chain = TelephoneChain::new("Alice went to Seattle on March 15th to work on Project Alpha.");
143 assert_eq!(chain.len(), 1);
144 assert!(!chain.tiles[0].summary.is_empty());
145 }
146
147 #[test]
148 fn add_rounds() {
149 let mut chain = TelephoneChain::new("Alice discovered a critical bug in the production system.");
150 chain.add_round("Alice found a bug in production.");
151 chain.add_round("There was a bug found.");
152 assert_eq!(chain.len(), 3);
153
154 assert_eq!(chain.tiles[0].generation, 0);
156 assert_eq!(chain.tiles[1].generation, 1);
157 assert_eq!(chain.tiles[2].generation, 2);
158
159 assert!(chain.tiles[0].parent_id.is_none());
161 assert_eq!(chain.tiles[1].parent_id.as_ref(), Some(&chain.tiles[0].id));
162 }
163
164 #[test]
165 fn fact_timeline_tracks() {
166 let chain = TelephoneChain::new("Alice went to Seattle for the important meeting.");
167 let timeline = chain.fact_timeline();
168 assert!(!timeline.is_empty());
169
170 for (_, rounds) in &timeline {
172 assert!(!rounds.is_empty());
173 }
174 }
175
176 #[test]
177 fn survival_rate_declines() {
178 let mut chain = TelephoneChain::new("Alice reported a critical emergency in Seattle on March 15th.");
179 let rate_0 = chain.fact_tracker.survival_rate(0);
180
181 chain.add_round("Someone reported something somewhere.");
182 let rate_1 = chain.fact_tracker.survival_rate(1);
183
184 assert!(rate_0 >= 0.0 && rate_0 <= 1.0);
187 assert!(rate_1 >= 0.0 && rate_1 <= 1.0);
188 }
189
190 #[test]
191 fn crystallization_point_needs_multiple_rounds() {
192 let chain = TelephoneChain::new("Test content");
193 assert!(chain.crystallization_point().is_none());
194 }
195}