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
272const fn eligible_for_public_digest(mem: &Memory) -> bool {
277 !mem.metadata.is_protected && !mem.metadata.is_private && !mem.metadata.model_exclude
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284pub struct ColdRecord {
285 pub id: MemoryId,
287 pub galaxy: Galaxy,
289 pub content_hash: String,
291 pub cold_stored_at: DateTime<Utc>,
293 pub outer_rim_distance: f32,
295 pub distance_factors: OuterRimFactors,
297 pub digest_id: Option<MemoryId>,
299 pub lineage_notes: Option<String>,
301 pub codec: CompressionCodec,
303 pub uncompressed_size: usize,
305 pub compressed_payload: Vec<u8>,
307 pub version: u64,
309 pub tags: Vec<String>,
311 pub title: Option<String>,
313 pub snippet: String,
315}
316
317impl ColdRecord {
318 pub fn new(
320 mem: &Memory,
321 outer_rim_distance: f32,
322 distance_factors: OuterRimFactors,
323 digest_id: Option<MemoryId>,
324 lineage_notes: Option<String>,
325 codec: CompressionCodec,
326 ) -> Result<Self> {
327 let (compressed_payload, uncompressed_size) = compress_memory(mem, codec)?;
328 let snippet = create_snippet(&mem.content);
329
330 Ok(Self {
331 id: mem.metadata.id,
332 galaxy: mem.metadata.galaxy,
333 content_hash: mem.metadata.content_hash.clone(),
334 cold_stored_at: Utc::now(),
335 outer_rim_distance,
336 distance_factors,
337 digest_id,
338 lineage_notes,
339 codec,
340 uncompressed_size,
341 compressed_payload,
342 version: mem.metadata.version,
343 tags: mem.metadata.tags.clone(),
344 title: mem.metadata.title.clone(),
345 snippet,
346 })
347 }
348
349 #[must_use]
351 pub fn summary(&self) -> ColdRecordSummary {
352 ColdRecordSummary {
353 id: self.id,
354 galaxy: self.galaxy,
355 content_hash: self.content_hash.clone(),
356 cold_stored_at: self.cold_stored_at,
357 outer_rim_distance: self.outer_rim_distance,
358 digest_id: self.digest_id,
359 uncompressed_size: self.uncompressed_size,
360 compressed_size: self.compressed_payload.len(),
361 codec: self.codec,
362 tags: self.tags.clone(),
363 title: self.title.clone(),
364 snippet: self.snippet.clone(),
365 }
366 }
367
368 pub fn decompress(&self) -> Result<Memory> {
370 decompress_memory(&self.compressed_payload, self.codec)
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
376pub struct ColdRecordSummary {
377 pub id: MemoryId,
379 pub galaxy: Galaxy,
381 pub content_hash: String,
383 pub cold_stored_at: DateTime<Utc>,
385 pub outer_rim_distance: f32,
387 pub digest_id: Option<MemoryId>,
389 pub uncompressed_size: usize,
391 pub compressed_size: usize,
393 pub codec: CompressionCodec,
395 pub tags: Vec<String>,
397 pub title: Option<String>,
399 pub snippet: String,
401}
402
403#[derive(Debug, Clone, Default)]
407pub struct ColdQuery {
408 pub galaxy: Option<Galaxy>,
410 pub tags: Vec<String>,
412 pub min_distance: Option<f32>,
414 pub stored_after: Option<DateTime<Utc>>,
416 pub stored_before: Option<DateTime<Utc>>,
418 pub content_substring: Option<String>,
420 pub limit: usize,
422}
423
424#[derive(Debug, Default, Clone)]
430pub struct ColdDiscoveryOutcome {
431 pub scanned: usize,
433 pub candidates: usize,
435 pub matched: usize,
437 pub integrity_rejected: usize,
439 pub private_skipped: usize,
441 pub non_current_skipped: usize,
443 pub records: Vec<ColdRecord>,
445}
446
447impl ColdQuery {
448 #[must_use]
450 pub fn new() -> Self {
451 Self {
452 limit: 100,
453 ..Default::default()
454 }
455 }
456
457 #[must_use]
459 pub const fn with_galaxy(mut self, galaxy: Galaxy) -> Self {
460 self.galaxy = Some(galaxy);
461 self
462 }
463
464 #[must_use]
466 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
467 self.tags = tags;
468 self
469 }
470
471 #[must_use]
473 pub const fn with_min_distance(mut self, min_dist: f32) -> Self {
474 self.min_distance = Some(min_dist);
475 self
476 }
477
478 #[must_use]
480 pub const fn with_limit(mut self, limit: usize) -> Self {
481 self.limit = limit;
482 self
483 }
484
485 #[must_use]
487 pub fn with_content_substring(mut self, substring: impl Into<String>) -> Self {
488 self.content_substring = Some(substring.into().to_lowercase());
489 self
490 }
491
492 #[must_use]
494 pub fn matches(&self, summary: &ColdRecordSummary) -> bool {
495 if let Some(g) = self.galaxy {
496 if summary.galaxy != g {
497 return false;
498 }
499 }
500 if let Some(min_d) = self.min_distance {
501 if summary.outer_rim_distance < min_d {
502 return false;
503 }
504 }
505 if let Some(after) = self.stored_after {
506 if summary.cold_stored_at < after {
507 return false;
508 }
509 }
510 if let Some(before) = self.stored_before {
511 if summary.cold_stored_at > before {
512 return false;
513 }
514 }
515 for tag in &self.tags {
516 if !summary.tags.iter().any(|t| t == tag) {
517 return false;
518 }
519 }
520 if let Some(sub) = &self.content_substring {
521 let in_title = summary
522 .title
523 .as_ref()
524 .is_some_and(|t| t.to_lowercase().contains(sub));
525 let in_snippet = summary.snippet.to_lowercase().contains(sub);
526 if !in_title && !in_snippet {
527 return false;
528 }
529 }
530 true
531 }
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct PhagicDigestReport {
539 pub galaxy: Galaxy,
541 pub memories_examined: usize,
543 pub outer_rim_candidates: usize,
545 pub memories_digested: usize,
547 pub digest_memory_id: Option<MemoryId>,
549 pub total_raw_bytes: usize,
551 pub total_cold_bytes: usize,
553 pub compression_ratio: f32,
555 pub average_outer_rim_distance: f32,
557}
558
559pub struct PhagicDigester {
563 config: PhagicConfig,
564}
565
566impl PhagicDigester {
567 #[must_use]
569 pub const fn new(config: PhagicConfig) -> Self {
570 Self { config }
571 }
572
573 #[must_use]
575 pub fn default_config() -> Self {
576 Self::new(PhagicConfig::default())
577 }
578
579 #[must_use]
581 pub const fn config(&self) -> &PhagicConfig {
582 &self.config
583 }
584
585 #[must_use]
587 pub fn calculate_distance(&self, mem: &Memory, now: DateTime<Utc>) -> OuterRimFactors {
588 calculate_outer_rim_distance(mem, &self.config, now)
589 }
590
591 pub fn scan_outer_rim(
593 &self,
594 store: &MemoryStore,
595 galaxy: Galaxy,
596 ) -> Result<Vec<(Memory, OuterRimFactors)>> {
597 let memories = store.scan(galaxy, 10_000)?;
598 let now = Utc::now();
599 let mut candidates = Vec::new();
600
601 for mem in memories {
602 if !eligible_for_public_digest(&mem) {
605 continue;
606 }
607
608 let factors = self.calculate_distance(&mem, now);
609 if factors.distance >= self.config.outer_rim_threshold {
610 candidates.push((mem, factors));
611 }
612 }
613
614 candidates.sort_by(|a, b| {
616 b.1.distance
617 .partial_cmp(&a.1.distance)
618 .unwrap_or(std::cmp::Ordering::Equal)
619 });
620
621 Ok(candidates)
622 }
623
624 pub fn digest_cluster(
627 &self,
628 store: &MemoryStore,
629 search: Option<&SearchEngine>,
630 galaxy: Galaxy,
631 cluster: &[(Memory, OuterRimFactors)],
632 ) -> Result<PhagicDigestReport> {
633 let eligible: Vec<&(Memory, OuterRimFactors)> = cluster
639 .iter()
640 .filter(|(mem, _)| eligible_for_public_digest(mem))
641 .collect();
642 if eligible.is_empty() {
643 return Ok(PhagicDigestReport {
644 galaxy,
645 memories_examined: 0,
646 outer_rim_candidates: 0,
647 memories_digested: 0,
648 digest_memory_id: None,
649 total_raw_bytes: 0,
650 total_cold_bytes: 0,
651 compression_ratio: 0.0,
652 average_outer_rim_distance: 0.0,
653 });
654 }
655
656 let cluster_id = Uuid::new_v4();
657 let now = Utc::now();
658
659 let mut tag_counts: HashMap<String, usize> = HashMap::new();
661 let mut total_distance = 0.0;
662 let mut min_time = now;
663 let mut max_time = DateTime::<Utc>::MIN_UTC;
664
665 for entry in &eligible {
666 let (mem, factors) = *entry;
667 total_distance += factors.distance;
668 if mem.metadata.created_at < min_time {
669 min_time = mem.metadata.created_at;
670 }
671 if mem.metadata.created_at > max_time {
672 max_time = mem.metadata.created_at;
673 }
674 for tag in &mem.metadata.tags {
675 if !tag.starts_with("phagic:") && !tag.starts_with("cold_source:") {
676 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
677 }
678 }
679 }
680
681 let avg_distance = total_distance / eligible.len() as f32;
682
683 let mut top_tags: Vec<(String, usize)> = tag_counts.into_iter().collect();
684 top_tags.sort_by_key(|a| std::cmp::Reverse(a.1));
685 let prominent_tags: Vec<String> =
686 top_tags.into_iter().take(6).map(|(tag, _)| tag).collect();
687
688 let mut digest_content = format!(
690 "# Phagic Thematic Digest: {} Outer-Rim Condensation\n\n\
691 **Cluster ID**: `{cluster_id}`\n\
692 **Galaxy**: `{}`\n\
693 **Digested Memories**: {} items safely migrated to Cold Storage\n\
694 **Temporal Span**: {} to {}\n\
695 **Average Outer-Rim Distance**: {:.3}\n\
696 **Prominent Themes**: {}\n\n\
697 ## Distilled Semantic Abstract\n\
698 This thematic node preserves the collective semantic essence of {} outer-rim memories\n\
699 from galaxy {}. The original memories have been gently transitioned into compressed\n\
700 cold storage without data loss, retaining complete lineage and full thawability.\n\n\
701 ## Lineage Pointers (Thawable Cold Records)\n",
702 galaxy.db_name(),
703 galaxy.db_name(),
704 eligible.len(),
705 min_time.format("%Y-%m-%d %H:%M:%S UTC"),
706 max_time.format("%Y-%m-%d %H:%M:%S UTC"),
707 avg_distance,
708 if prominent_tags.is_empty() {
709 "none".to_string()
710 } else {
711 prominent_tags.join(", ")
712 },
713 eligible.len(),
714 galaxy.db_name(),
715 );
716
717 use std::fmt::Write as _;
718 for entry in &eligible {
719 let (mem, factors) = *entry;
720 let snippet = create_snippet(&mem.content);
721 let _ = writeln!(
722 digest_content,
723 "- **[Memory `{}`]** (Distance: {:.2}): {snippet}",
724 mem.metadata.id, factors.distance
725 );
726 }
727
728 let mut digest_tags = prominent_tags;
729 digest_tags.push("phagic:digest".to_string());
730 digest_tags.push(format!("phagic:cluster:{cluster_id}"));
731
732 let digest_title = format!(
733 "Phagic Digest: {} ({} items)",
734 galaxy.db_name(),
735 cluster.len()
736 );
737 let mut digest_mem = Memory::new(galaxy, digest_content)
738 .with_tags(digest_tags)
739 .with_importance(0.65)
740 .with_memory_type(MemoryType::Symbolic)
741 .with_source("phagic:digestion".to_string(), 0.9);
742 digest_mem.metadata.tier = Tier::Semantic;
743 digest_mem.metadata.title = Some(digest_title);
744 digest_mem.metadata.topic = Some(format!("{}:phagic_digest", galaxy.db_name()));
745
746 store.put(galaxy, &digest_mem)?;
748
749 if let Some(engine) = search {
751 if let Ok(mut writer_guard) = engine.writer() {
752 let _ = engine.add_document(
753 &mut writer_guard,
754 &digest_mem.metadata.id.to_string(),
755 galaxy.db_name(),
756 &digest_mem.content,
757 &digest_mem.metadata.tags,
758 digest_mem.metadata.created_at.timestamp(),
759 );
760 let _ = engine.commit(&mut writer_guard);
761 }
762 }
763
764 let digest_id = digest_mem.metadata.id;
765
766 let mut total_raw_bytes = 0;
768 let mut total_cold_bytes = 0;
769
770 for entry in &eligible {
771 let (mem, factors) = *entry;
772 let notes = format!("Digested in phagic cluster {cluster_id} (digest_id: {digest_id})");
773 let cold_rec = store.freeze_to_cold(
774 search,
775 mem.metadata.id,
776 factors.distance,
777 factors.clone(),
778 Some(digest_id),
779 Some(notes),
780 self.config.compression_codec,
781 )?;
782 total_raw_bytes += cold_rec.uncompressed_size;
783 total_cold_bytes += cold_rec.compressed_payload.len();
784 }
785
786 let compression_ratio = if total_raw_bytes > 0 {
787 total_cold_bytes as f32 / total_raw_bytes as f32
788 } else {
789 0.0
790 };
791
792 Ok(PhagicDigestReport {
793 galaxy,
794 memories_examined: eligible.len(),
795 outer_rim_candidates: eligible.len(),
796 memories_digested: eligible.len(),
797 digest_memory_id: Some(digest_id),
798 total_raw_bytes,
799 total_cold_bytes,
800 compression_ratio,
801 average_outer_rim_distance: avg_distance,
802 })
803 }
804
805 pub fn digest_galaxy(
807 &self,
808 store: &MemoryStore,
809 search: Option<&SearchEngine>,
810 galaxy: Galaxy,
811 ) -> Result<PhagicDigestReport> {
812 let candidates = self.scan_outer_rim(store, galaxy)?;
813 let count = store.count(galaxy).unwrap_or(0);
814
815 if candidates.is_empty() {
816 return Ok(PhagicDigestReport {
817 galaxy,
818 memories_examined: count,
819 outer_rim_candidates: 0,
820 memories_digested: 0,
821 digest_memory_id: None,
822 total_raw_bytes: 0,
823 total_cold_bytes: 0,
824 compression_ratio: 0.0,
825 average_outer_rim_distance: 0.0,
826 });
827 }
828
829 let batch_size = candidates.len().min(self.config.max_digest_batch);
831 let batch = &candidates[..batch_size];
832
833 let mut report = self.digest_cluster(store, search, galaxy, batch)?;
834 report.memories_examined = count;
835 report.outer_rim_candidates = candidates.len();
836 Ok(report)
837 }
838
839 pub fn digest_all(
841 &self,
842 store: &MemoryStore,
843 search: Option<&SearchEngine>,
844 ) -> Result<Vec<PhagicDigestReport>> {
845 let mut reports = Vec::new();
846 for galaxy in Galaxy::all() {
847 match galaxy {
848 Galaxy::Substrate
849 | Galaxy::Dharma
850 | Galaxy::Karma
851 | Galaxy::Embeddings
852 | Galaxy::Associations => continue,
853 _ => {}
854 }
855 if store.count(galaxy).unwrap_or(0) == 0 {
856 continue;
857 }
858 reports.push(self.digest_galaxy(store, search, galaxy)?);
859 }
860 Ok(reports)
861 }
862
863 pub fn thaw(
865 &self,
866 store: &MemoryStore,
867 search: Option<&SearchEngine>,
868 id: MemoryId,
869 ) -> Result<Memory> {
870 store.thaw_from_cold(search, id)
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use tempfile::tempdir;
878
879 fn test_store() -> MemoryStore {
880 let tmp = tempdir().unwrap();
881 MemoryStore::open_default(tmp.path()).unwrap()
882 }
883
884 #[test]
885 fn outer_rim_distance_calculation_monotonic() {
886 let config = PhagicConfig::default();
887 let now = Utc::now();
888
889 let mut core_mem = Memory::new(Galaxy::Codex, "Vital core memory".into())
891 .with_importance(0.95)
892 .with_neuro_score(0.9)
893 .with_emotional_valence(0.8, 0.9);
894 core_mem.metadata.access_count = 10;
895 core_mem.metadata.accessed_at = now;
896
897 let core_factors = calculate_outer_rim_distance(&core_mem, &config, now);
898 assert!(
899 core_factors.distance < 0.25,
900 "Core memory distance should be low: {}",
901 core_factors.distance
902 );
903
904 let mut rim_mem = Memory::new(Galaxy::Codex, "Faded distant note".into())
906 .with_importance(0.05)
907 .with_neuro_score(0.1)
908 .with_emotional_valence(0.0, 0.0);
909 rim_mem.metadata.access_count = 0;
910 rim_mem.metadata.recall_count = 0;
911 rim_mem.metadata.accessed_at = now - chrono::Duration::days(120);
912
913 let rim_factors = calculate_outer_rim_distance(&rim_mem, &config, now);
914 assert!(
915 rim_factors.distance >= config.outer_rim_threshold,
916 "Rim memory distance should exceed threshold: {}",
917 rim_factors.distance
918 );
919 assert!(
920 rim_factors.distance > core_factors.distance,
921 "Rim memory must be strictly further out than core memory"
922 );
923 }
924
925 #[test]
926 fn protected_memory_is_anchored_at_core() {
927 let config = PhagicConfig::default();
928 let now = Utc::now();
929
930 let mut protected_mem = Memory::new(Galaxy::Codex, "Protected sacred memory".into())
931 .with_protection(true)
932 .with_importance(0.01); protected_mem.metadata.accessed_at = now - chrono::Duration::days(365);
934
935 let factors = calculate_outer_rim_distance(&protected_mem, &config, now);
936 assert_eq!(
937 factors.distance, 0.0,
938 "Protected memory must always have distance 0.0"
939 );
940 }
941
942 #[test]
943 fn cold_record_compression_roundtrip() {
944 let mem = Memory::new(
945 Galaxy::Research,
946 "Extensive research logs on cosmic non-destructive phagocytosis and entropy reversal"
947 .into(),
948 )
949 .with_tags(vec!["physics".into(), "entropy".into(), "phagic".into()])
950 .with_importance(0.3);
951
952 for codec in [
953 CompressionCodec::Gzip,
954 CompressionCodec::Deflate,
955 CompressionCodec::Msgpack,
956 ] {
957 let factors = OuterRimFactors {
958 age_factor: 0.8,
959 access_factor: 0.9,
960 resonance_factor: 0.7,
961 emotional_factor: 0.9,
962 importance_factor: 0.7,
963 distance: 0.81,
964 };
965
966 let cold = ColdRecord::new(&mem, 0.81, factors, None, Some("test notes".into()), codec)
967 .unwrap();
968 assert_eq!(cold.id, mem.metadata.id);
969 assert_eq!(cold.galaxy, Galaxy::Research);
970
971 let decompressed = cold.decompress().unwrap();
972 assert_eq!(decompressed.metadata.id, mem.metadata.id);
973 assert_eq!(decompressed.content, mem.content);
974 assert_eq!(decompressed.metadata.tags, mem.metadata.tags);
975 assert_eq!(decompressed.metadata.importance, mem.metadata.importance);
976 }
977 }
978
979 #[test]
980 fn freeze_and_thaw_zero_data_loss() {
981 let store = test_store();
982 let galaxy = Galaxy::Codex;
983
984 let content = "Detailed architectural blueprint for WhiteMagic non-destructive storage";
985 let mem = Memory::new(galaxy, content.into())
986 .with_tags(vec!["architecture".into(), "v9".into()])
987 .with_importance(0.2);
988 let id = mem.metadata.id;
989
990 store.put(galaxy, &mem).unwrap();
991 assert!(store.get(galaxy, id).unwrap().is_some());
992
993 let digester = PhagicDigester::default_config();
995 let factors = digester.calculate_distance(&mem, Utc::now());
996 let cold_record = store
997 .freeze_to_cold(
998 None,
999 id,
1000 factors.distance,
1001 factors,
1002 None,
1003 Some("unit test freeze".into()),
1004 CompressionCodec::Gzip,
1005 )
1006 .unwrap();
1007
1008 assert_eq!(cold_record.id, id);
1009
1010 assert!(store.get(galaxy, id).unwrap().is_none());
1012
1013 let cold_found = store.get_cold_record(id).unwrap();
1015 assert!(cold_found.is_some());
1016 let cold_rec = cold_found.unwrap();
1017 assert_eq!(cold_rec.content_hash, mem.metadata.content_hash);
1018
1019 let (found_galaxy, found_mem, is_cold) = store.find_anywhere(id).unwrap().unwrap();
1021 assert_eq!(found_galaxy, galaxy);
1022 assert_eq!(found_mem.content, content);
1023 assert!(is_cold);
1024
1025 let thawed = store.thaw_from_cold(None, id).unwrap();
1027 assert_eq!(thawed.metadata.id, id);
1028 assert_eq!(thawed.content, content);
1029 assert_eq!(
1030 thawed.metadata.tags,
1031 vec!["architecture", "v9", "thawed:phagic"]
1032 );
1033 assert_eq!(thawed.metadata.tier, Tier::Episodic);
1034
1035 assert!(store.get(galaxy, id).unwrap().is_some());
1037
1038 assert!(store.get_cold_record(id).unwrap().is_none());
1040
1041 let (_, _, is_cold_after) = store.find_anywhere(id).unwrap().unwrap();
1043 assert!(!is_cold_after);
1044 }
1045
1046 #[test]
1047 fn phagic_digest_cluster_synthesizes_thematic_node() {
1048 let store = test_store();
1049 let galaxy = Galaxy::Journals;
1050 let now = Utc::now();
1051
1052 let mut ids = Vec::new();
1054 for i in 1..=3 {
1055 let mut mem = Memory::new(
1056 galaxy,
1057 format!("Ancient journal reflection #{i} about distant journeys"),
1058 )
1059 .with_tags(vec!["travel".into(), "reflection".into()])
1060 .with_importance(0.05);
1061 mem.metadata.accessed_at = now - chrono::Duration::days(150);
1062 mem.metadata.access_count = 0;
1063 store.put(galaxy, &mem).unwrap();
1064 ids.push(mem.metadata.id);
1065 }
1066
1067 let digester = PhagicDigester::default_config();
1068 let report = digester.digest_galaxy(&store, None, galaxy).unwrap();
1069
1070 assert_eq!(report.memories_digested, 3);
1071 assert!(report.digest_memory_id.is_some());
1072 assert!(report.total_cold_bytes > 0);
1073 assert!(report.total_raw_bytes > report.total_cold_bytes);
1074
1075 for id in &ids {
1077 assert!(store.get(galaxy, *id).unwrap().is_none());
1078 assert!(store.get_cold_record(*id).unwrap().is_some());
1079 }
1080
1081 let digest_id = report.digest_memory_id.unwrap();
1083 let digest_mem = store.get(galaxy, digest_id).unwrap().unwrap();
1084 assert_eq!(digest_mem.metadata.tier, Tier::Semantic);
1085 assert!(digest_mem.content.contains("Phagic Thematic Digest"));
1086 assert!(
1087 digest_mem
1088 .metadata
1089 .tags
1090 .contains(&"phagic:digest".to_string())
1091 );
1092
1093 let thawed = digester.thaw(&store, None, ids[0]).unwrap();
1095 assert_eq!(thawed.metadata.id, ids[0]);
1096 assert!(store.get(galaxy, ids[0]).unwrap().is_some());
1097 assert!(store.get_cold_record(ids[0]).unwrap().is_none());
1098 }
1099
1100 #[test]
1101 fn direct_digest_cluster_does_not_launder_restricted_source_material() {
1102 let store = test_store();
1103 let galaxy = Galaxy::Codex;
1104 let factors = OuterRimFactors {
1105 age_factor: 1.0,
1106 access_factor: 1.0,
1107 resonance_factor: 1.0,
1108 emotional_factor: 1.0,
1109 importance_factor: 1.0,
1110 distance: 1.0,
1111 };
1112
1113 let public =
1114 Memory::new(galaxy, "public digest source".into()).with_tags(vec!["public-tag".into()]);
1115 let mut private = Memory::new(galaxy, "PRIVATE-SOURCE-CONTENT".into())
1116 .with_tags(vec!["private-source-tag".into()]);
1117 private.metadata.is_private = true;
1118 let mut model_excluded = Memory::new(galaxy, "MODEL-EXCLUDED-SOURCE-CONTENT".into())
1119 .with_tags(vec!["model-excluded-source-tag".into()]);
1120 model_excluded.metadata.model_exclude = true;
1121 let protected = Memory::new(galaxy, "PROTECTED-SOURCE-CONTENT".into())
1122 .with_tags(vec!["protected-source-tag".into()])
1123 .with_protection(true);
1124
1125 for memory in [&public, &private, &model_excluded, &protected] {
1126 store.put(galaxy, memory).unwrap();
1127 }
1128 let restricted = [private.clone(), model_excluded.clone(), protected.clone()];
1129 let cluster = vec![
1130 (public.clone(), factors.clone()),
1131 (private, factors.clone()),
1132 (model_excluded, factors.clone()),
1133 (protected, factors),
1134 ];
1135
1136 let report = PhagicDigester::default_config()
1140 .digest_cluster(&store, None, galaxy, &cluster)
1141 .unwrap();
1142 assert_eq!(report.memories_digested, 1);
1143 let digest = store
1144 .get(galaxy, report.digest_memory_id.unwrap())
1145 .unwrap()
1146 .unwrap();
1147 assert!(!digest.metadata.is_private);
1148 assert!(!digest.metadata.model_exclude);
1149 assert!(digest.content.contains("public digest source"));
1150 for source in &restricted {
1151 assert!(!digest.content.contains(&source.content));
1152 assert!(!digest.content.contains(&source.metadata.id.to_string()));
1153 for tag in &source.metadata.tags {
1154 assert!(!digest.metadata.tags.contains(tag));
1155 assert!(!digest.content.contains(tag));
1156 }
1157 let retained = store.get(galaxy, source.metadata.id).unwrap().unwrap();
1158 assert_eq!(
1159 serde_json::to_value(retained).unwrap(),
1160 serde_json::to_value(source).unwrap()
1161 );
1162 assert!(store.get_cold_record(source.metadata.id).unwrap().is_none());
1163 }
1164
1165 assert!(store.get(galaxy, public.metadata.id).unwrap().is_none());
1166 assert!(store.get_cold_record(public.metadata.id).unwrap().is_some());
1167 }
1168}