1use chrono::{DateTime, Utc};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use uuid::Uuid;
24use wm_core::{CoreError, Galaxy, Result};
25
26use crate::memory::{Memory, MemoryId, MemoryType, Tier};
27use crate::search::SearchEngine;
28use crate::store::MemoryStore;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum CompressionCodec {
36 #[default]
38 Gzip,
39 Deflate,
41 Msgpack,
43}
44
45impl CompressionCodec {
46 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::Gzip => "gzip",
51 Self::Deflate => "deflate",
52 Self::Msgpack => "msgpack",
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct OuterRimFactors {
62 pub age_factor: f32,
64 pub access_factor: f32,
66 pub resonance_factor: f32,
68 pub emotional_factor: f32,
70 pub importance_factor: f32,
72 pub distance: f32,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PhagicConfig {
79 pub weight_age: f32,
81 pub weight_access: f32,
83 pub weight_resonance: f32,
85 pub weight_emotional: f32,
87 pub weight_importance: f32,
89 pub outer_rim_threshold: f32,
91 pub max_digest_batch: usize,
93 pub min_cluster_size: usize,
95 pub compression_codec: CompressionCodec,
97}
98
99impl Default for PhagicConfig {
100 fn default() -> Self {
101 Self {
102 weight_age: 0.25,
103 weight_access: 0.25,
104 weight_resonance: 0.15,
105 weight_emotional: 0.15,
106 weight_importance: 0.20,
107 outer_rim_threshold: 0.70,
108 max_digest_batch: 50,
109 min_cluster_size: 2,
110 compression_codec: CompressionCodec::Gzip,
111 }
112 }
113}
114
115#[must_use]
120pub fn calculate_outer_rim_distance(
121 mem: &Memory,
122 config: &PhagicConfig,
123 now: DateTime<Utc>,
124) -> OuterRimFactors {
125 if mem.metadata.is_protected {
126 return OuterRimFactors {
127 age_factor: 0.0,
128 access_factor: 0.0,
129 resonance_factor: 0.0,
130 emotional_factor: 0.0,
131 importance_factor: 0.0,
132 distance: 0.0,
133 };
134 }
135
136 let seconds_since_access = (now - mem.metadata.accessed_at).num_seconds().max(0);
138 let days_since = seconds_since_access as f32 / 86400.0;
139 let half_life = mem.metadata.half_life_days.max(1.0);
140 let age_factor = (1.0 - 0.5f32.powf(days_since / half_life)).clamp(0.0, 1.0);
141
142 let reads = (mem.metadata.access_count + mem.metadata.recall_count) as f32;
144 let access_factor = (1.0 / 0.5f32.mul_add(reads, 1.0)).clamp(0.0, 1.0);
145
146 let resonance_factor = (1.0 - mem.metadata.neuro_score.clamp(0.0, 1.0)).clamp(0.0, 1.0);
148
149 let salience =
151 (mem.metadata.emotional_valence.abs() * mem.metadata.emotional_weight).clamp(0.0, 1.0);
152 let emotional_factor = (1.0 - salience).clamp(0.0, 1.0);
153
154 let importance_factor = (1.0 - mem.metadata.importance.clamp(0.0, 1.0)).clamp(0.0, 1.0);
156
157 let sum_weights = config.weight_age
159 + config.weight_access
160 + config.weight_resonance
161 + config.weight_emotional
162 + config.weight_importance;
163
164 let distance = if sum_weights > 0.0 {
165 let raw = config.weight_importance.mul_add(
166 importance_factor,
167 config.weight_emotional.mul_add(
168 emotional_factor,
169 config.weight_resonance.mul_add(
170 resonance_factor,
171 config
172 .weight_access
173 .mul_add(access_factor, config.weight_age * age_factor),
174 ),
175 ),
176 );
177 (raw / sum_weights).clamp(0.0, 1.0)
178 } else {
179 0.5
180 };
181
182 OuterRimFactors {
183 age_factor,
184 access_factor,
185 resonance_factor,
186 emotional_factor,
187 importance_factor,
188 distance,
189 }
190}
191
192pub fn compress_memory(mem: &Memory, codec: CompressionCodec) -> Result<(Vec<u8>, usize)> {
196 let uncompressed = rmp_serde::to_vec_named(mem)
197 .map_err(|e| CoreError::Memory(format!("Cold storage serialization error: {e}")))?;
198 let uncompressed_size = uncompressed.len();
199
200 let compressed = match codec {
201 CompressionCodec::Gzip => {
202 use flate2::Compression;
203 use flate2::write::GzEncoder;
204 use std::io::Write;
205 let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
206 encoder
207 .write_all(&uncompressed)
208 .map_err(|e| CoreError::Memory(format!("Gzip compression error: {e}")))?;
209 encoder
210 .finish()
211 .map_err(|e| CoreError::Memory(format!("Gzip finish error: {e}")))?
212 }
213 CompressionCodec::Deflate => {
214 use flate2::Compression;
215 use flate2::write::DeflateEncoder;
216 use std::io::Write;
217 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
218 encoder
219 .write_all(&uncompressed)
220 .map_err(|e| CoreError::Memory(format!("Deflate compression error: {e}")))?;
221 encoder
222 .finish()
223 .map_err(|e| CoreError::Memory(format!("Deflate finish error: {e}")))?
224 }
225 CompressionCodec::Msgpack => uncompressed,
226 };
227
228 Ok((compressed, uncompressed_size))
229}
230
231pub fn decompress_memory(payload: &[u8], codec: CompressionCodec) -> Result<Memory> {
233 let uncompressed = match codec {
234 CompressionCodec::Gzip => {
235 use flate2::read::GzDecoder;
236 use std::io::Read;
237 let mut decoder = GzDecoder::new(payload);
238 let mut buf = Vec::new();
239 decoder
240 .read_to_end(&mut buf)
241 .map_err(|e| CoreError::Memory(format!("Gzip decompression error: {e}")))?;
242 buf
243 }
244 CompressionCodec::Deflate => {
245 use flate2::read::DeflateDecoder;
246 use std::io::Read;
247 let mut decoder = DeflateDecoder::new(payload);
248 let mut buf = Vec::new();
249 decoder
250 .read_to_end(&mut buf)
251 .map_err(|e| CoreError::Memory(format!("Deflate decompression error: {e}")))?;
252 buf
253 }
254 CompressionCodec::Msgpack => payload.to_vec(),
255 };
256
257 crate::codec::decode(&uncompressed)
258 .map_err(|e| CoreError::Memory(format!("Cold storage deserialization error: {e}")))
259}
260
261fn create_snippet(content: &str) -> String {
262 let trimmed = content.trim();
263 if trimmed.chars().count() <= 120 {
264 trimmed.to_string()
265 } else {
266 let mut s: String = trimmed.chars().take(117).collect();
267 s.push_str("...");
268 s
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct ColdRecord {
277 pub id: MemoryId,
279 pub galaxy: Galaxy,
281 pub content_hash: String,
283 pub cold_stored_at: DateTime<Utc>,
285 pub outer_rim_distance: f32,
287 pub distance_factors: OuterRimFactors,
289 pub digest_id: Option<MemoryId>,
291 pub lineage_notes: Option<String>,
293 pub codec: CompressionCodec,
295 pub uncompressed_size: usize,
297 pub compressed_payload: Vec<u8>,
299 pub version: u64,
301 pub tags: Vec<String>,
303 pub title: Option<String>,
305 pub snippet: String,
307}
308
309impl ColdRecord {
310 pub fn new(
312 mem: &Memory,
313 outer_rim_distance: f32,
314 distance_factors: OuterRimFactors,
315 digest_id: Option<MemoryId>,
316 lineage_notes: Option<String>,
317 codec: CompressionCodec,
318 ) -> Result<Self> {
319 let (compressed_payload, uncompressed_size) = compress_memory(mem, codec)?;
320 let snippet = create_snippet(&mem.content);
321
322 Ok(Self {
323 id: mem.metadata.id,
324 galaxy: mem.metadata.galaxy,
325 content_hash: mem.metadata.content_hash.clone(),
326 cold_stored_at: Utc::now(),
327 outer_rim_distance,
328 distance_factors,
329 digest_id,
330 lineage_notes,
331 codec,
332 uncompressed_size,
333 compressed_payload,
334 version: mem.metadata.version,
335 tags: mem.metadata.tags.clone(),
336 title: mem.metadata.title.clone(),
337 snippet,
338 })
339 }
340
341 #[must_use]
343 pub fn summary(&self) -> ColdRecordSummary {
344 ColdRecordSummary {
345 id: self.id,
346 galaxy: self.galaxy,
347 content_hash: self.content_hash.clone(),
348 cold_stored_at: self.cold_stored_at,
349 outer_rim_distance: self.outer_rim_distance,
350 digest_id: self.digest_id,
351 uncompressed_size: self.uncompressed_size,
352 compressed_size: self.compressed_payload.len(),
353 codec: self.codec,
354 tags: self.tags.clone(),
355 title: self.title.clone(),
356 snippet: self.snippet.clone(),
357 }
358 }
359
360 pub fn decompress(&self) -> Result<Memory> {
362 decompress_memory(&self.compressed_payload, self.codec)
363 }
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
368pub struct ColdRecordSummary {
369 pub id: MemoryId,
371 pub galaxy: Galaxy,
373 pub content_hash: String,
375 pub cold_stored_at: DateTime<Utc>,
377 pub outer_rim_distance: f32,
379 pub digest_id: Option<MemoryId>,
381 pub uncompressed_size: usize,
383 pub compressed_size: usize,
385 pub codec: CompressionCodec,
387 pub tags: Vec<String>,
389 pub title: Option<String>,
391 pub snippet: String,
393}
394
395#[derive(Debug, Clone, Default)]
399pub struct ColdQuery {
400 pub galaxy: Option<Galaxy>,
402 pub tags: Vec<String>,
404 pub min_distance: Option<f32>,
406 pub stored_after: Option<DateTime<Utc>>,
408 pub stored_before: Option<DateTime<Utc>>,
410 pub content_substring: Option<String>,
412 pub limit: usize,
414}
415
416impl ColdQuery {
417 #[must_use]
419 pub fn new() -> Self {
420 Self {
421 limit: 100,
422 ..Default::default()
423 }
424 }
425
426 #[must_use]
428 pub const fn with_galaxy(mut self, galaxy: Galaxy) -> Self {
429 self.galaxy = Some(galaxy);
430 self
431 }
432
433 #[must_use]
435 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
436 self.tags = tags;
437 self
438 }
439
440 #[must_use]
442 pub const fn with_min_distance(mut self, min_dist: f32) -> Self {
443 self.min_distance = Some(min_dist);
444 self
445 }
446
447 #[must_use]
449 pub const fn with_limit(mut self, limit: usize) -> Self {
450 self.limit = limit;
451 self
452 }
453
454 #[must_use]
456 pub fn with_content_substring(mut self, substring: impl Into<String>) -> Self {
457 self.content_substring = Some(substring.into().to_lowercase());
458 self
459 }
460
461 #[must_use]
463 pub fn matches(&self, summary: &ColdRecordSummary) -> bool {
464 if let Some(g) = self.galaxy {
465 if summary.galaxy != g {
466 return false;
467 }
468 }
469 if let Some(min_d) = self.min_distance {
470 if summary.outer_rim_distance < min_d {
471 return false;
472 }
473 }
474 if let Some(after) = self.stored_after {
475 if summary.cold_stored_at < after {
476 return false;
477 }
478 }
479 if let Some(before) = self.stored_before {
480 if summary.cold_stored_at > before {
481 return false;
482 }
483 }
484 for tag in &self.tags {
485 if !summary.tags.iter().any(|t| t == tag) {
486 return false;
487 }
488 }
489 if let Some(sub) = &self.content_substring {
490 let in_title = summary
491 .title
492 .as_ref()
493 .is_some_and(|t| t.to_lowercase().contains(sub));
494 let in_snippet = summary.snippet.to_lowercase().contains(sub);
495 if !in_title && !in_snippet {
496 return false;
497 }
498 }
499 true
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct PhagicDigestReport {
508 pub galaxy: Galaxy,
510 pub memories_examined: usize,
512 pub outer_rim_candidates: usize,
514 pub memories_digested: usize,
516 pub digest_memory_id: Option<MemoryId>,
518 pub total_raw_bytes: usize,
520 pub total_cold_bytes: usize,
522 pub compression_ratio: f32,
524 pub average_outer_rim_distance: f32,
526}
527
528pub struct PhagicDigester {
532 config: PhagicConfig,
533}
534
535impl PhagicDigester {
536 #[must_use]
538 pub const fn new(config: PhagicConfig) -> Self {
539 Self { config }
540 }
541
542 #[must_use]
544 pub fn default_config() -> Self {
545 Self::new(PhagicConfig::default())
546 }
547
548 #[must_use]
550 pub const fn config(&self) -> &PhagicConfig {
551 &self.config
552 }
553
554 #[must_use]
556 pub fn calculate_distance(&self, mem: &Memory, now: DateTime<Utc>) -> OuterRimFactors {
557 calculate_outer_rim_distance(mem, &self.config, now)
558 }
559
560 pub fn scan_outer_rim(
562 &self,
563 store: &MemoryStore,
564 galaxy: Galaxy,
565 ) -> Result<Vec<(Memory, OuterRimFactors)>> {
566 let memories = store.scan(galaxy, 10_000)?;
567 let now = Utc::now();
568 let mut candidates = Vec::new();
569
570 for mem in memories {
571 if mem.metadata.is_protected {
573 continue;
574 }
575
576 let factors = self.calculate_distance(&mem, now);
577 if factors.distance >= self.config.outer_rim_threshold {
578 candidates.push((mem, factors));
579 }
580 }
581
582 candidates.sort_by(|a, b| {
584 b.1.distance
585 .partial_cmp(&a.1.distance)
586 .unwrap_or(std::cmp::Ordering::Equal)
587 });
588
589 Ok(candidates)
590 }
591
592 pub fn digest_cluster(
595 &self,
596 store: &MemoryStore,
597 search: Option<&SearchEngine>,
598 galaxy: Galaxy,
599 cluster: &[(Memory, OuterRimFactors)],
600 ) -> Result<PhagicDigestReport> {
601 if cluster.is_empty() {
602 return Ok(PhagicDigestReport {
603 galaxy,
604 memories_examined: 0,
605 outer_rim_candidates: 0,
606 memories_digested: 0,
607 digest_memory_id: None,
608 total_raw_bytes: 0,
609 total_cold_bytes: 0,
610 compression_ratio: 0.0,
611 average_outer_rim_distance: 0.0,
612 });
613 }
614
615 let cluster_id = Uuid::new_v4();
616 let now = Utc::now();
617
618 let mut tag_counts: HashMap<String, usize> = HashMap::new();
620 let mut total_distance = 0.0;
621 let mut min_time = now;
622 let mut max_time = DateTime::<Utc>::MIN_UTC;
623
624 for (mem, factors) in cluster {
625 total_distance += factors.distance;
626 if mem.metadata.created_at < min_time {
627 min_time = mem.metadata.created_at;
628 }
629 if mem.metadata.created_at > max_time {
630 max_time = mem.metadata.created_at;
631 }
632 for tag in &mem.metadata.tags {
633 if !tag.starts_with("phagic:") && !tag.starts_with("cold_source:") {
634 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
635 }
636 }
637 }
638
639 let avg_distance = total_distance / cluster.len() as f32;
640
641 let mut top_tags: Vec<(String, usize)> = tag_counts.into_iter().collect();
642 top_tags.sort_by_key(|a| std::cmp::Reverse(a.1));
643 let prominent_tags: Vec<String> =
644 top_tags.into_iter().take(6).map(|(tag, _)| tag).collect();
645
646 let mut digest_content = format!(
648 "# Phagic Thematic Digest: {} Outer-Rim Condensation\n\n\
649 **Cluster ID**: `{cluster_id}`\n\
650 **Galaxy**: `{}`\n\
651 **Digested Memories**: {} items safely migrated to Cold Storage\n\
652 **Temporal Span**: {} to {}\n\
653 **Average Outer-Rim Distance**: {:.3}\n\
654 **Prominent Themes**: {}\n\n\
655 ## Distilled Semantic Abstract\n\
656 This thematic node preserves the collective semantic essence of {} outer-rim memories\n\
657 from galaxy {}. The original memories have been gently transitioned into compressed\n\
658 cold storage without data loss, retaining complete lineage and full thawability.\n\n\
659 ## Lineage Pointers (Thawable Cold Records)\n",
660 galaxy.db_name(),
661 galaxy.db_name(),
662 cluster.len(),
663 min_time.format("%Y-%m-%d %H:%M:%S UTC"),
664 max_time.format("%Y-%m-%d %H:%M:%S UTC"),
665 avg_distance,
666 if prominent_tags.is_empty() {
667 "none".to_string()
668 } else {
669 prominent_tags.join(", ")
670 },
671 cluster.len(),
672 galaxy.db_name(),
673 );
674
675 use std::fmt::Write as _;
676 for (mem, factors) in cluster {
677 let snippet = create_snippet(&mem.content);
678 let _ = writeln!(
679 digest_content,
680 "- **[Memory `{}`]** (Distance: {:.2}): {snippet}",
681 mem.metadata.id, factors.distance
682 );
683 }
684
685 let mut digest_tags = prominent_tags;
686 digest_tags.push("phagic:digest".to_string());
687 digest_tags.push(format!("phagic:cluster:{cluster_id}"));
688
689 let digest_title = format!(
690 "Phagic Digest: {} ({} items)",
691 galaxy.db_name(),
692 cluster.len()
693 );
694 let mut digest_mem = Memory::new(galaxy, digest_content)
695 .with_tags(digest_tags)
696 .with_importance(0.65)
697 .with_memory_type(MemoryType::Symbolic)
698 .with_source("phagic:digestion".to_string(), 0.9);
699 digest_mem.metadata.tier = Tier::Semantic;
700 digest_mem.metadata.title = Some(digest_title);
701 digest_mem.metadata.topic = Some(format!("{}:phagic_digest", galaxy.db_name()));
702
703 store.put(galaxy, &digest_mem)?;
705
706 if let Some(engine) = search {
708 if let Ok(mut writer_guard) = engine.writer() {
709 let _ = engine.add_document(
710 &mut writer_guard,
711 &digest_mem.metadata.id.to_string(),
712 galaxy.db_name(),
713 &digest_mem.content,
714 &digest_mem.metadata.tags,
715 digest_mem.metadata.created_at.timestamp(),
716 );
717 let _ = engine.commit(&mut writer_guard);
718 }
719 }
720
721 let digest_id = digest_mem.metadata.id;
722
723 let mut total_raw_bytes = 0;
725 let mut total_cold_bytes = 0;
726
727 for (mem, factors) in cluster {
728 let notes = format!("Digested in phagic cluster {cluster_id} (digest_id: {digest_id})");
729 let cold_rec = store.freeze_to_cold(
730 search,
731 mem.metadata.id,
732 factors.distance,
733 factors.clone(),
734 Some(digest_id),
735 Some(notes),
736 self.config.compression_codec,
737 )?;
738 total_raw_bytes += cold_rec.uncompressed_size;
739 total_cold_bytes += cold_rec.compressed_payload.len();
740 }
741
742 let compression_ratio = if total_raw_bytes > 0 {
743 total_cold_bytes as f32 / total_raw_bytes as f32
744 } else {
745 0.0
746 };
747
748 Ok(PhagicDigestReport {
749 galaxy,
750 memories_examined: cluster.len(),
751 outer_rim_candidates: cluster.len(),
752 memories_digested: cluster.len(),
753 digest_memory_id: Some(digest_id),
754 total_raw_bytes,
755 total_cold_bytes,
756 compression_ratio,
757 average_outer_rim_distance: avg_distance,
758 })
759 }
760
761 pub fn digest_galaxy(
763 &self,
764 store: &MemoryStore,
765 search: Option<&SearchEngine>,
766 galaxy: Galaxy,
767 ) -> Result<PhagicDigestReport> {
768 let candidates = self.scan_outer_rim(store, galaxy)?;
769 let count = store.count(galaxy).unwrap_or(0);
770
771 if candidates.is_empty() {
772 return Ok(PhagicDigestReport {
773 galaxy,
774 memories_examined: count,
775 outer_rim_candidates: 0,
776 memories_digested: 0,
777 digest_memory_id: None,
778 total_raw_bytes: 0,
779 total_cold_bytes: 0,
780 compression_ratio: 0.0,
781 average_outer_rim_distance: 0.0,
782 });
783 }
784
785 let batch_size = candidates.len().min(self.config.max_digest_batch);
787 let batch = &candidates[..batch_size];
788
789 let mut report = self.digest_cluster(store, search, galaxy, batch)?;
790 report.memories_examined = count;
791 report.outer_rim_candidates = candidates.len();
792 Ok(report)
793 }
794
795 pub fn digest_all(
797 &self,
798 store: &MemoryStore,
799 search: Option<&SearchEngine>,
800 ) -> Result<Vec<PhagicDigestReport>> {
801 let mut reports = Vec::new();
802 for galaxy in Galaxy::all() {
803 match galaxy {
804 Galaxy::Substrate
805 | Galaxy::Dharma
806 | Galaxy::Karma
807 | Galaxy::Embeddings
808 | Galaxy::Associations => continue,
809 _ => {}
810 }
811 if store.count(galaxy).unwrap_or(0) == 0 {
812 continue;
813 }
814 reports.push(self.digest_galaxy(store, search, galaxy)?);
815 }
816 Ok(reports)
817 }
818
819 pub fn thaw(
821 &self,
822 store: &MemoryStore,
823 search: Option<&SearchEngine>,
824 id: MemoryId,
825 ) -> Result<Memory> {
826 store.thaw_from_cold(search, id)
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833 use tempfile::tempdir;
834
835 fn test_store() -> MemoryStore {
836 let tmp = tempdir().unwrap();
837 MemoryStore::open_default(tmp.path()).unwrap()
838 }
839
840 #[test]
841 fn outer_rim_distance_calculation_monotonic() {
842 let config = PhagicConfig::default();
843 let now = Utc::now();
844
845 let mut core_mem = Memory::new(Galaxy::Codex, "Vital core memory".into())
847 .with_importance(0.95)
848 .with_neuro_score(0.9)
849 .with_emotional_valence(0.8, 0.9);
850 core_mem.metadata.access_count = 10;
851 core_mem.metadata.accessed_at = now;
852
853 let core_factors = calculate_outer_rim_distance(&core_mem, &config, now);
854 assert!(
855 core_factors.distance < 0.25,
856 "Core memory distance should be low: {}",
857 core_factors.distance
858 );
859
860 let mut rim_mem = Memory::new(Galaxy::Codex, "Faded distant note".into())
862 .with_importance(0.05)
863 .with_neuro_score(0.1)
864 .with_emotional_valence(0.0, 0.0);
865 rim_mem.metadata.access_count = 0;
866 rim_mem.metadata.recall_count = 0;
867 rim_mem.metadata.accessed_at = now - chrono::Duration::days(120);
868
869 let rim_factors = calculate_outer_rim_distance(&rim_mem, &config, now);
870 assert!(
871 rim_factors.distance >= config.outer_rim_threshold,
872 "Rim memory distance should exceed threshold: {}",
873 rim_factors.distance
874 );
875 assert!(
876 rim_factors.distance > core_factors.distance,
877 "Rim memory must be strictly further out than core memory"
878 );
879 }
880
881 #[test]
882 fn protected_memory_is_anchored_at_core() {
883 let config = PhagicConfig::default();
884 let now = Utc::now();
885
886 let mut protected_mem = Memory::new(Galaxy::Codex, "Protected sacred memory".into())
887 .with_protection(true)
888 .with_importance(0.01); protected_mem.metadata.accessed_at = now - chrono::Duration::days(365);
890
891 let factors = calculate_outer_rim_distance(&protected_mem, &config, now);
892 assert_eq!(
893 factors.distance, 0.0,
894 "Protected memory must always have distance 0.0"
895 );
896 }
897
898 #[test]
899 fn cold_record_compression_roundtrip() {
900 let mem = Memory::new(
901 Galaxy::Research,
902 "Extensive research logs on cosmic non-destructive phagocytosis and entropy reversal"
903 .into(),
904 )
905 .with_tags(vec!["physics".into(), "entropy".into(), "phagic".into()])
906 .with_importance(0.3);
907
908 for codec in [
909 CompressionCodec::Gzip,
910 CompressionCodec::Deflate,
911 CompressionCodec::Msgpack,
912 ] {
913 let factors = OuterRimFactors {
914 age_factor: 0.8,
915 access_factor: 0.9,
916 resonance_factor: 0.7,
917 emotional_factor: 0.9,
918 importance_factor: 0.7,
919 distance: 0.81,
920 };
921
922 let cold = ColdRecord::new(&mem, 0.81, factors, None, Some("test notes".into()), codec)
923 .unwrap();
924 assert_eq!(cold.id, mem.metadata.id);
925 assert_eq!(cold.galaxy, Galaxy::Research);
926
927 let decompressed = cold.decompress().unwrap();
928 assert_eq!(decompressed.metadata.id, mem.metadata.id);
929 assert_eq!(decompressed.content, mem.content);
930 assert_eq!(decompressed.metadata.tags, mem.metadata.tags);
931 assert_eq!(decompressed.metadata.importance, mem.metadata.importance);
932 }
933 }
934
935 #[test]
936 fn freeze_and_thaw_zero_data_loss() {
937 let store = test_store();
938 let galaxy = Galaxy::Codex;
939
940 let content = "Detailed architectural blueprint for WhiteMagic non-destructive storage";
941 let mem = Memory::new(galaxy, content.into())
942 .with_tags(vec!["architecture".into(), "v9".into()])
943 .with_importance(0.2);
944 let id = mem.metadata.id;
945
946 store.put(galaxy, &mem).unwrap();
947 assert!(store.get(galaxy, id).unwrap().is_some());
948
949 let digester = PhagicDigester::default_config();
951 let factors = digester.calculate_distance(&mem, Utc::now());
952 let cold_record = store
953 .freeze_to_cold(
954 None,
955 id,
956 factors.distance,
957 factors,
958 None,
959 Some("unit test freeze".into()),
960 CompressionCodec::Gzip,
961 )
962 .unwrap();
963
964 assert_eq!(cold_record.id, id);
965
966 assert!(store.get(galaxy, id).unwrap().is_none());
968
969 let cold_found = store.get_cold_record(id).unwrap();
971 assert!(cold_found.is_some());
972 let cold_rec = cold_found.unwrap();
973 assert_eq!(cold_rec.content_hash, mem.metadata.content_hash);
974
975 let (found_galaxy, found_mem, is_cold) = store.find_anywhere(id).unwrap().unwrap();
977 assert_eq!(found_galaxy, galaxy);
978 assert_eq!(found_mem.content, content);
979 assert!(is_cold);
980
981 let thawed = store.thaw_from_cold(None, id).unwrap();
983 assert_eq!(thawed.metadata.id, id);
984 assert_eq!(thawed.content, content);
985 assert_eq!(
986 thawed.metadata.tags,
987 vec!["architecture", "v9", "thawed:phagic"]
988 );
989 assert_eq!(thawed.metadata.tier, Tier::Episodic);
990
991 assert!(store.get(galaxy, id).unwrap().is_some());
993
994 assert!(store.get_cold_record(id).unwrap().is_none());
996
997 let (_, _, is_cold_after) = store.find_anywhere(id).unwrap().unwrap();
999 assert!(!is_cold_after);
1000 }
1001
1002 #[test]
1003 fn phagic_digest_cluster_synthesizes_thematic_node() {
1004 let store = test_store();
1005 let galaxy = Galaxy::Journals;
1006 let now = Utc::now();
1007
1008 let mut ids = Vec::new();
1010 for i in 1..=3 {
1011 let mut mem = Memory::new(
1012 galaxy,
1013 format!("Ancient journal reflection #{i} about distant journeys"),
1014 )
1015 .with_tags(vec!["travel".into(), "reflection".into()])
1016 .with_importance(0.05);
1017 mem.metadata.accessed_at = now - chrono::Duration::days(150);
1018 mem.metadata.access_count = 0;
1019 store.put(galaxy, &mem).unwrap();
1020 ids.push(mem.metadata.id);
1021 }
1022
1023 let digester = PhagicDigester::default_config();
1024 let report = digester.digest_galaxy(&store, None, galaxy).unwrap();
1025
1026 assert_eq!(report.memories_digested, 3);
1027 assert!(report.digest_memory_id.is_some());
1028 assert!(report.total_cold_bytes > 0);
1029 assert!(report.total_raw_bytes > report.total_cold_bytes);
1030
1031 for id in &ids {
1033 assert!(store.get(galaxy, *id).unwrap().is_none());
1034 assert!(store.get_cold_record(*id).unwrap().is_some());
1035 }
1036
1037 let digest_id = report.digest_memory_id.unwrap();
1039 let digest_mem = store.get(galaxy, digest_id).unwrap().unwrap();
1040 assert_eq!(digest_mem.metadata.tier, Tier::Semantic);
1041 assert!(digest_mem.content.contains("Phagic Thematic Digest"));
1042 assert!(
1043 digest_mem
1044 .metadata
1045 .tags
1046 .contains(&"phagic:digest".to_string())
1047 );
1048
1049 let thawed = digester.thaw(&store, None, ids[0]).unwrap();
1051 assert_eq!(thawed.metadata.id, ids[0]);
1052 assert!(store.get(galaxy, ids[0]).unwrap().is_some());
1053 assert!(store.get_cold_record(ids[0]).unwrap().is_none());
1054 }
1055}