1#![forbid(unsafe_code)]
12
13use async_trait::async_trait;
14
15use serde_json::{Value, json};
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18use wm_core::{Context, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
19use wm_memory::{AssociationStore, MemoryStore};
20
21use super::common::{galaxy_name, parse_galaxy};
22
23pub struct AssociationMineTool {
29 store: Arc<MemoryStore>,
30 stats: ToolStats,
31 effects: EffectRow,
32}
33
34impl AssociationMineTool {
35 pub fn new(store: Arc<MemoryStore>) -> Self {
36 Self {
37 store,
38 stats: ToolStats::default(),
39 effects: EffectRow {
40 writes: vec![Resource::Galaxy("associations".into())],
41 ..Default::default()
42 },
43 }
44 }
45}
46
47#[async_trait]
48impl Tool for AssociationMineTool {
49 fn name(&self) -> &str {
50 "association.mine"
51 }
52 fn gana(&self) -> Gana {
53 Gana::Net
54 }
55 fn effects(&self) -> &EffectRow {
56 &self.effects
57 }
58 fn description(&self) -> &str {
59 "Mine cross-galaxy associations using keyword overlap analysis"
60 }
61 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
62 let min_strength = args
63 .get("min_strength")
64 .and_then(Value::as_f64)
65 .unwrap_or(0.3) as f32;
66 let limit_per_galaxy = args
67 .get("limit")
68 .and_then(serde_json::Value::as_u64)
69 .unwrap_or(100) as usize;
70 let max_comparisons = args
71 .get("max_comparisons")
72 .and_then(serde_json::Value::as_u64)
73 .unwrap_or(50_000) as usize;
74
75 let env = self.store.env();
76 let assoc_store = AssociationStore::open(env)?;
77
78 let mut all_memories: Vec<(Galaxy, &str, wm_memory::Memory)> = Vec::new();
80 for galaxy in Galaxy::memory_galaxies() {
81 let mems = self.store.scan(galaxy, limit_per_galaxy)?;
82 for mem in mems {
83 all_memories.push((galaxy, galaxy_name(galaxy), mem));
84 }
85 }
86
87 let mut proposed = 0u32;
88 let mut cross_galaxy_links = 0u32;
89 let mut same_galaxy_links = 0u32;
90 let mut comparisons = 0u32;
91
92 for i in 0..all_memories.len() {
93 if comparisons as usize >= max_comparisons {
94 break;
95 }
96 for j in (i + 1)..all_memories.len() {
97 comparisons += 1;
98 if comparisons as usize >= max_comparisons {
99 break;
100 }
101 let (g1, _, ref a) = all_memories[i];
102 let (g2, _, ref b) = all_memories[j];
103 let a_words: HashSet<&str> = a.content.split_whitespace().collect();
104 let b_words: HashSet<&str> = b.content.split_whitespace().collect();
105 let intersection = a_words.intersection(&b_words).count();
106 let union = a_words.union(&b_words).count();
107 if union > 0 && intersection > 2 {
108 let strength = intersection as f32 / union as f32;
109 if strength > min_strength {
110 if assoc_store
112 .get(env, a.metadata.id, b.metadata.id)
113 .ok()
114 .flatten()
115 .is_none()
116 {
117 let assoc = wm_memory::Association::new(
118 a.metadata.id,
119 b.metadata.id,
120 wm_memory::LinkType::Related,
121 strength,
122 );
123 let _ = assoc_store.put(env, &assoc);
124 proposed += 1;
125 if g1 == g2 {
126 same_galaxy_links += 1;
127 } else {
128 cross_galaxy_links += 1;
129 }
130 }
131 }
132 }
133 }
134 }
135
136 Ok(json!({
137 "status": "success",
138 "memories_scanned": all_memories.len(),
139 "proposed_associations": proposed,
140 "cross_galaxy_links": cross_galaxy_links,
141 "same_galaxy_links": same_galaxy_links,
142 "min_strength": min_strength,
143 "comparisons": comparisons,
144 "truncated": comparisons as usize >= max_comparisons,
145 }))
146 }
147 fn stats(&self) -> &ToolStats {
148 &self.stats
149 }
150}
151
152pub struct PatternDetectTool {
157 store: Arc<MemoryStore>,
158 stats: ToolStats,
159 effects: EffectRow,
160}
161
162impl PatternDetectTool {
163 pub fn new(store: Arc<MemoryStore>) -> Self {
164 Self {
165 store,
166 stats: ToolStats::default(),
167 effects: EffectRow::pure(),
168 }
169 }
170}
171
172#[async_trait]
173impl Tool for PatternDetectTool {
174 fn name(&self) -> &str {
175 "pattern.detect"
176 }
177 fn gana(&self) -> Gana {
178 Gana::Net
179 }
180 fn effects(&self) -> &EffectRow {
181 &self.effects
182 }
183 fn description(&self) -> &str {
184 "Detect structural patterns (hubs, bridges, chains) in the association graph"
185 }
186 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
187 let top_k = args
188 .get("top_k")
189 .and_then(serde_json::Value::as_u64)
190 .unwrap_or(10) as usize;
191
192 let env = self.store.env();
193 let assoc_store = AssociationStore::open(env)?;
194
195 let mut degree_map: HashMap<uuid::Uuid, (u32, u32)> = HashMap::new();
197 let mut all_assocs: Vec<wm_memory::Association> = Vec::new();
198
199 for galaxy in Galaxy::memory_galaxies() {
201 let mems = self.store.scan(galaxy, 10000)?;
202 for mem in mems {
203 let outgoing = assoc_store
204 .find_from(env, mem.metadata.id)
205 .unwrap_or_default();
206 let incoming = assoc_store
207 .find_to(env, mem.metadata.id)
208 .unwrap_or_default();
209 let out_deg = outgoing.len() as u32;
210 let in_deg = incoming.len() as u32;
211 if out_deg > 0 || in_deg > 0 {
212 degree_map.insert(mem.metadata.id, (in_deg, out_deg));
213 }
214 all_assocs.extend(outgoing);
215 all_assocs.extend(incoming);
216 }
217 }
218
219 let mut hubs: Vec<(uuid::Uuid, u32)> = degree_map
221 .iter()
222 .map(|(id, (ind, outd))| (*id, ind + outd))
223 .collect();
224 hubs.sort_by_key(|x| std::cmp::Reverse(x.1));
225 let top_hubs: Vec<Value> = hubs
226 .iter()
227 .take(top_k)
228 .map(|(id, deg)| {
229 json!({
230 "memory_id": id,
231 "total_degree": deg,
232 })
233 })
234 .collect();
235
236 let mut chains: Vec<Value> = Vec::new();
238 let temporal_assocs: Vec<&wm_memory::Association> = all_assocs
239 .iter()
240 .filter(|a| a.link_type == wm_memory::LinkType::Temporal)
241 .collect();
242 let mut temporal_map: HashMap<uuid::Uuid, uuid::Uuid> = HashMap::new();
243 for a in &temporal_assocs {
244 temporal_map.insert(a.source, a.target);
245 }
246 for a in &temporal_assocs {
247 let mut chain = vec![a.source, a.target];
248 let mut current = a.target;
249 while let Some(&next) = temporal_map.get(¤t) {
250 if chain.contains(&next) {
251 break;
252 }
253 chain.push(next);
254 current = next;
255 }
256 if chain.len() >= 3 {
257 chains.push(json!({
258 "chain": chain.iter().map(std::string::ToString::to_string).collect::<Vec<_>>(),
259 "length": chain.len(),
260 }));
261 }
262 }
263
264 let mut bridge_counts: HashMap<uuid::Uuid, u32> = HashMap::new();
266 for a in &all_assocs {
267 let source_galaxy = self.find_memory_galaxy(a.source);
268 let target_galaxy = self.find_memory_galaxy(a.target);
269 if let (Some(sg), Some(tg)) = (source_galaxy, target_galaxy) {
270 if sg != tg {
271 *bridge_counts.entry(a.source).or_insert(0) += 1;
272 *bridge_counts.entry(a.target).or_insert(0) += 1;
273 }
274 }
275 }
276 let mut bridges: Vec<(uuid::Uuid, u32)> = bridge_counts.into_iter().collect();
277 bridges.sort_by_key(|x| std::cmp::Reverse(x.1));
278 let top_bridges: Vec<Value> = bridges
279 .iter()
280 .take(top_k)
281 .map(|(id, count)| {
282 json!({
283 "memory_id": id,
284 "cross_galaxy_links": count,
285 })
286 })
287 .collect();
288
289 Ok(json!({
290 "status": "success",
291 "total_nodes": degree_map.len(),
292 "total_edges": all_assocs.len(),
293 "hubs": top_hubs,
294 "chains": chains,
295 "bridges": top_bridges,
296 }))
297 }
298 fn stats(&self) -> &ToolStats {
299 &self.stats
300 }
301}
302
303impl PatternDetectTool {
304 fn find_memory_galaxy(&self, id: uuid::Uuid) -> Option<Galaxy> {
305 Galaxy::memory_galaxies()
306 .into_iter()
307 .find(|&galaxy| self.store.get(galaxy, id).ok().flatten().is_some())
308 }
309}
310
311pub struct EmergenceReportTool {
317 store: Arc<MemoryStore>,
318 stats: ToolStats,
319 effects: EffectRow,
320}
321
322impl EmergenceReportTool {
323 pub fn new(store: Arc<MemoryStore>) -> Self {
324 Self {
325 store,
326 stats: ToolStats::default(),
327 effects: EffectRow::pure(),
328 }
329 }
330}
331
332#[async_trait]
333impl Tool for EmergenceReportTool {
334 fn name(&self) -> &str {
335 "emergence.report"
336 }
337 fn gana(&self) -> Gana {
338 Gana::Net
339 }
340 fn effects(&self) -> &EffectRow {
341 &self.effects
342 }
343 fn description(&self) -> &str {
344 "Detailed emergence analysis with tag frequency distribution and trend detection"
345 }
346 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
347 let mut tag_counts: HashMap<String, usize> = HashMap::new();
348 let mut total_memories = 0usize;
349
350 for galaxy in Galaxy::memory_galaxies() {
351 let mems = self.store.scan(galaxy, 10000)?;
352 for mem in mems {
353 total_memories += 1;
354 for tag in &mem.metadata.tags {
355 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
356 }
357 }
358 }
359
360 let total_tags: usize = tag_counts.values().sum();
361 let unique_tags = tag_counts.len();
362
363 let mut sorted_tags: Vec<(String, usize)> = tag_counts.into_iter().collect();
365 sorted_tags.sort_by_key(|x| std::cmp::Reverse(x.1));
366
367 let dominant: Vec<Value> = sorted_tags
368 .iter()
369 .filter(|(_, count)| *count as f64 / total_memories.max(1) as f64 > 0.1)
370 .take(20)
371 .map(|(tag, count)| {
372 json!({
373 "tag": tag,
374 "count": count,
375 "frequency": (*count as f64 / total_memories.max(1) as f64 * 100.0).round() / 100.0,
376 })
377 })
378 .collect();
379
380 let emerging: Vec<Value> = sorted_tags
381 .iter()
382 .filter(|(_, count)| *count >= 2 && *count <= 5)
383 .take(20)
384 .map(|(tag, count)| {
385 json!({
386 "tag": tag,
387 "count": count,
388 })
389 })
390 .collect();
391
392 let rare: Vec<Value> = sorted_tags
393 .iter()
394 .filter(|(_, count)| *count == 1)
395 .take(20)
396 .map(|(tag, _)| json!({"tag": tag}))
397 .collect();
398
399 Ok(json!({
400 "status": "success",
401 "total_memories": total_memories,
402 "unique_tags": unique_tags,
403 "total_tag_instances": total_tags,
404 "dominant_tags": dominant,
405 "emerging_tags": emerging,
406 "rare_tags": rare,
407 "tag_diversity": (unique_tags as f64 / total_memories.max(1) as f64 * 100.0).round() / 100.0,
408 }))
409 }
410 fn stats(&self) -> &ToolStats {
411 &self.stats
412 }
413}
414
415pub struct NetworkStatsTool {
420 store: Arc<MemoryStore>,
421 stats: ToolStats,
422 effects: EffectRow,
423}
424
425impl NetworkStatsTool {
426 pub fn new(store: Arc<MemoryStore>) -> Self {
427 Self {
428 store,
429 stats: ToolStats::default(),
430 effects: EffectRow::pure(),
431 }
432 }
433}
434
435#[async_trait]
436impl Tool for NetworkStatsTool {
437 fn name(&self) -> &str {
438 "network.stats"
439 }
440 fn gana(&self) -> Gana {
441 Gana::Net
442 }
443 fn effects(&self) -> &EffectRow {
444 &self.effects
445 }
446 fn description(&self) -> &str {
447 "Global association network statistics (nodes, edges, density, degree distribution)"
448 }
449 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
450 let env = self.store.env();
451 let assoc_store = AssociationStore::open(env)?;
452
453 let total_edges = assoc_store.count(env)?;
454
455 let mut degree_map: HashMap<uuid::Uuid, (u32, u32)> = HashMap::new();
457 let mut link_type_counts: HashMap<&'static str, u32> = HashMap::new();
458 let mut total_weight: f32 = 0.0;
459
460 for galaxy in Galaxy::memory_galaxies() {
461 let mems = self.store.scan(galaxy, 10000)?;
462 for mem in mems {
463 let outgoing = assoc_store
464 .find_from(env, mem.metadata.id)
465 .unwrap_or_default();
466 let incoming = assoc_store
467 .find_to(env, mem.metadata.id)
468 .unwrap_or_default();
469 let out_deg = outgoing.len() as u32;
470 let in_deg = incoming.len() as u32;
471 if out_deg > 0 || in_deg > 0 {
472 let entry = degree_map.entry(mem.metadata.id).or_insert((0, 0));
473 entry.0 += in_deg;
474 entry.1 += out_deg;
475 }
476 for a in &outgoing {
477 *link_type_counts.entry(a.link_type.as_str()).or_insert(0) += 1;
478 total_weight += a.weight;
479 }
480 }
481 }
482
483 let total_nodes = degree_map.len();
484 let max_possible_edges = if total_nodes > 1 {
485 total_nodes * (total_nodes - 1)
486 } else {
487 0
488 };
489 let density = if max_possible_edges > 0 {
490 total_edges as f64 / max_possible_edges as f64
491 } else {
492 0.0
493 };
494
495 let mut degree_distribution: Vec<u32> =
497 degree_map.values().map(|(ind, outd)| ind + outd).collect();
498 degree_distribution.sort_unstable();
499 let avg_degree = if degree_distribution.is_empty() {
500 0.0
501 } else {
502 f64::from(degree_distribution.iter().sum::<u32>()) / degree_distribution.len() as f64
503 };
504 let max_degree = degree_distribution.iter().copied().max().unwrap_or(0);
505
506 let link_breakdown: Vec<Value> = link_type_counts
507 .iter()
508 .map(|(lt, count)| {
509 json!({
510 "link_type": lt,
511 "count": count,
512 })
513 })
514 .collect();
515
516 let avg_weight = if total_edges > 0 {
517 total_weight / total_edges as f32
518 } else {
519 0.0
520 };
521
522 Ok(json!({
523 "status": "success",
524 "nodes": total_nodes,
525 "edges": total_edges,
526 "density": (density * 10000.0).round() / 10000.0,
527 "avg_degree": (avg_degree * 100.0).round() / 100.0,
528 "max_degree": max_degree,
529 "avg_weight": (avg_weight * 100.0).round() / 100.0,
530 "link_type_breakdown": link_breakdown,
531 }))
532 }
533 fn stats(&self) -> &ToolStats {
534 &self.stats
535 }
536}
537
538pub struct NetworkCentralityTool {
543 store: Arc<MemoryStore>,
544 stats: ToolStats,
545 effects: EffectRow,
546}
547
548impl NetworkCentralityTool {
549 pub fn new(store: Arc<MemoryStore>) -> Self {
550 Self {
551 store,
552 stats: ToolStats::default(),
553 effects: EffectRow::pure(),
554 }
555 }
556}
557
558#[async_trait]
559impl Tool for NetworkCentralityTool {
560 fn name(&self) -> &str {
561 "network.centrality"
562 }
563 fn gana(&self) -> Gana {
564 Gana::Net
565 }
566 fn effects(&self) -> &EffectRow {
567 &self.effects
568 }
569 fn description(&self) -> &str {
570 "Compute degree centrality metrics for memories in the association graph"
571 }
572 async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
573 let top_k = args
574 .get("top_k")
575 .and_then(serde_json::Value::as_u64)
576 .unwrap_or(20) as usize;
577 let galaxy_str = args.get("galaxy").and_then(Value::as_str);
578
579 let env = self.store.env();
580 let assoc_store = AssociationStore::open(env)?;
581
582 let galaxies: Vec<Galaxy> = match galaxy_str {
583 Some(g) => vec![parse_galaxy(g)?],
584 None => Galaxy::memory_galaxies().to_vec(),
585 };
586
587 let mut centrality: Vec<(uuid::Uuid, u32, u32, u32)> = Vec::new();
588
589 for galaxy in &galaxies {
590 let mems = self.store.scan(*galaxy, 10000)?;
591 for mem in mems {
592 let outgoing = assoc_store
593 .find_from(env, mem.metadata.id)
594 .unwrap_or_default();
595 let incoming = assoc_store
596 .find_to(env, mem.metadata.id)
597 .unwrap_or_default();
598 let out_deg = outgoing.len() as u32;
599 let in_deg = incoming.len() as u32;
600 if out_deg > 0 || in_deg > 0 {
601 centrality.push((mem.metadata.id, in_deg, out_deg, in_deg + out_deg));
602 }
603 }
604 }
605
606 centrality.sort_by_key(|x| std::cmp::Reverse(x.3));
607
608 let max_degree = centrality.first().map_or(1, |c| c.3);
609
610 let top_nodes: Vec<Value> = centrality
611 .iter()
612 .take(top_k)
613 .map(|(id, ind, outd, total)| {
614 let centrality_score = if max_degree > 0 {
615 f64::from(*total) / f64::from(max_degree)
616 } else {
617 0.0
618 };
619 json!({
620 "memory_id": id,
621 "in_degree": ind,
622 "out_degree": outd,
623 "total_degree": total,
624 "centrality": (centrality_score * 1000.0).round() / 1000.0,
625 })
626 })
627 .collect();
628
629 Ok(json!({
630 "status": "success",
631 "total_nodes_with_edges": centrality.len(),
632 "max_degree": max_degree,
633 "top_nodes": top_nodes,
634 }))
635 }
636 fn stats(&self) -> &ToolStats {
637 &self.stats
638 }
639}
640
641pub struct NetworkClustersTool {
646 store: Arc<MemoryStore>,
647 stats: ToolStats,
648 effects: EffectRow,
649}
650
651impl NetworkClustersTool {
652 pub fn new(store: Arc<MemoryStore>) -> Self {
653 Self {
654 store,
655 stats: ToolStats::default(),
656 effects: EffectRow::pure(),
657 }
658 }
659}
660
661#[async_trait]
662impl Tool for NetworkClustersTool {
663 fn name(&self) -> &str {
664 "network.clusters"
665 }
666 fn gana(&self) -> Gana {
667 Gana::Net
668 }
669 fn effects(&self) -> &EffectRow {
670 &self.effects
671 }
672 fn description(&self) -> &str {
673 "Identify connected components and clusters in the association graph"
674 }
675 async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
676 let env = self.store.env();
677 let assoc_store = AssociationStore::open(env)?;
678
679 let mut node_set: HashSet<uuid::Uuid> = HashSet::new();
681 let mut edges: Vec<(uuid::Uuid, uuid::Uuid)> = Vec::new();
682
683 for galaxy in Galaxy::memory_galaxies() {
684 let mems = self.store.scan(galaxy, 10000)?;
685 for mem in mems {
686 let outgoing = assoc_store
687 .find_from(env, mem.metadata.id)
688 .unwrap_or_default();
689 for a in outgoing {
690 node_set.insert(a.source);
691 node_set.insert(a.target);
692 edges.push((a.source, a.target));
693 }
694 }
695 }
696
697 let mut parent: HashMap<uuid::Uuid, uuid::Uuid> = HashMap::new();
699 for &node in &node_set {
700 parent.insert(node, node);
701 }
702
703 fn find(parent: &mut HashMap<uuid::Uuid, uuid::Uuid>, x: uuid::Uuid) -> uuid::Uuid {
704 let mut current = x;
705 while parent[¤t] != current {
706 let p = parent[¤t];
707 parent.insert(current, p);
708 current = p;
709 }
710 current
711 }
712
713 for (a, b) in &edges {
714 let ra = find(&mut parent, *a);
715 let rb = find(&mut parent, *b);
716 if ra != rb {
717 parent.insert(ra, rb);
718 }
719 }
720
721 let mut cluster_sizes: HashMap<uuid::Uuid, usize> = HashMap::new();
723 for &node in &node_set {
724 let root = find(&mut parent, node);
725 *cluster_sizes.entry(root).or_insert(0) += 1;
726 }
727
728 let mut sizes: Vec<usize> = cluster_sizes.values().copied().collect();
729 sizes.sort_by(|a, b| b.cmp(a));
730
731 let num_clusters = sizes.len();
732 let largest_cluster = sizes.first().copied().unwrap_or(0);
733 let isolated_nodes = sizes.iter().filter(|&&s| s == 1).count();
734 let multi_node_clusters = sizes.iter().filter(|&&s| s > 1).count();
735
736 let top_clusters: Vec<Value> = sizes
737 .iter()
738 .take(10)
739 .enumerate()
740 .map(|(i, &size)| {
741 json!({
742 "cluster_rank": i + 1,
743 "size": size,
744 })
745 })
746 .collect();
747
748 Ok(json!({
749 "status": "success",
750 "total_nodes": node_set.len(),
751 "total_edges": edges.len(),
752 "num_clusters": num_clusters,
753 "largest_cluster_size": largest_cluster,
754 "isolated_nodes": isolated_nodes,
755 "multi_node_clusters": multi_node_clusters,
756 "top_clusters": top_clusters,
757 }))
758 }
759 fn stats(&self) -> &ToolStats {
760 &self.stats
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767 use tempfile::tempdir;
768
769 fn open_store() -> (tempfile::TempDir, Arc<MemoryStore>) {
770 let tmp = tempdir().unwrap();
771 let store = MemoryStore::open_default(tmp.path()).unwrap();
772 (tmp, Arc::new(store))
773 }
774
775 fn make_memory(galaxy: Galaxy, content: &str, tags: &[&str]) -> wm_memory::Memory {
776 let mut mem = wm_memory::Memory::new(galaxy, content.to_string());
777 mem.metadata.tags = tags.iter().map(std::string::ToString::to_string).collect();
778 mem
779 }
780
781 #[tokio::test]
782 async fn association_mine_basic() {
783 let (_tmp, store) = open_store();
784 let tool = AssociationMineTool::new(store.clone());
785
786 let m1 = make_memory(
787 Galaxy::Codex,
788 "rust is a fast systems programming language",
789 &["rust", "programming"],
790 );
791 let m2 = make_memory(
792 Galaxy::Codex,
793 "rust is a safe systems programming language",
794 &["rust", "programming"],
795 );
796 store.put(Galaxy::Codex, &m1).unwrap();
797 store.put(Galaxy::Codex, &m2).unwrap();
798
799 let mut ctx = Context::new(wm_core::BrainWave::Beta);
800 let result = tool.call(&mut ctx, json!({})).await.unwrap();
801 assert_eq!(result["status"], "success");
802 assert!(result["proposed_associations"].as_u64().unwrap() >= 1);
803 }
804
805 #[tokio::test]
806 async fn association_mine_with_min_strength() {
807 let (_tmp, store) = open_store();
808 let tool = AssociationMineTool::new(store.clone());
809
810 let m1 = make_memory(Galaxy::Codex, "rust fast systems", &["rust"]);
811 let m2 = make_memory(Galaxy::Codex, "python slow scripting", &["python"]);
812 store.put(Galaxy::Codex, &m1).unwrap();
813 store.put(Galaxy::Codex, &m2).unwrap();
814
815 let mut ctx = Context::new(wm_core::BrainWave::Beta);
816 let result = tool
817 .call(&mut ctx, json!({"min_strength": 0.9}))
818 .await
819 .unwrap();
820 assert_eq!(result["proposed_associations"], 0);
821 }
822
823 #[tokio::test]
824 async fn pattern_detect_empty_graph() {
825 let (_tmp, store) = open_store();
826 let tool = PatternDetectTool::new(store);
827 let mut ctx = Context::new(wm_core::BrainWave::Beta);
828 let result = tool.call(&mut ctx, json!({})).await.unwrap();
829 assert_eq!(result["status"], "success");
830 assert_eq!(result["total_nodes"], 0);
831 }
832
833 #[tokio::test]
834 async fn pattern_detect_finds_hubs() {
835 let (_tmp, store) = open_store();
836 let env = store.env();
837 let assoc_store = AssociationStore::open(env).unwrap();
838
839 let m1 = make_memory(Galaxy::Codex, "hub memory about rust", &["rust"]);
840 let m2 = make_memory(Galaxy::Codex, "spoke one about rust", &["rust"]);
841 let m3 = make_memory(Galaxy::Codex, "spoke two about rust", &["rust"]);
842 store.put(Galaxy::Codex, &m1).unwrap();
843 store.put(Galaxy::Codex, &m2).unwrap();
844 store.put(Galaxy::Codex, &m3).unwrap();
845
846 assoc_store
847 .put(
848 env,
849 &wm_memory::Association::new(
850 m1.metadata.id,
851 m2.metadata.id,
852 wm_memory::LinkType::Related,
853 0.8,
854 ),
855 )
856 .unwrap();
857 assoc_store
858 .put(
859 env,
860 &wm_memory::Association::new(
861 m1.metadata.id,
862 m3.metadata.id,
863 wm_memory::LinkType::Related,
864 0.7,
865 ),
866 )
867 .unwrap();
868
869 let tool = PatternDetectTool::new(store.clone());
870 let mut ctx = Context::new(wm_core::BrainWave::Beta);
871 let result = tool.call(&mut ctx, json!({})).await.unwrap();
872 assert_eq!(result["status"], "success");
873 assert!(result["total_nodes"].as_u64().unwrap() >= 3);
874 let hubs = result["hubs"].as_array().unwrap();
875 assert!(!hubs.is_empty());
876 let first_hub_degree = hubs[0]["total_degree"].as_u64().unwrap();
877 assert!(first_hub_degree >= 2);
878 }
879
880 #[tokio::test]
881 async fn emergence_report_basic() {
882 let (_tmp, store) = open_store();
883 let tool = EmergenceReportTool::new(store.clone());
884
885 store
886 .put(
887 Galaxy::Codex,
888 &make_memory(Galaxy::Codex, "rust fact", &["rust", "programming"]),
889 )
890 .unwrap();
891 store
892 .put(
893 Galaxy::Codex,
894 &make_memory(Galaxy::Codex, "python fact", &["python", "programming"]),
895 )
896 .unwrap();
897 store
898 .put(
899 Galaxy::Codex,
900 &make_memory(Galaxy::Codex, "rust again", &["rust"]),
901 )
902 .unwrap();
903
904 let mut ctx = Context::new(wm_core::BrainWave::Beta);
905 let result = tool.call(&mut ctx, json!({})).await.unwrap();
906 assert_eq!(result["status"], "success");
907 assert_eq!(result["total_memories"], 3);
908 assert!(result["unique_tags"].as_u64().unwrap() >= 2);
909 }
910
911 #[tokio::test]
912 async fn emergence_report_empty() {
913 let (_tmp, store) = open_store();
914 let tool = EmergenceReportTool::new(store);
915 let mut ctx = Context::new(wm_core::BrainWave::Beta);
916 let result = tool.call(&mut ctx, json!({})).await.unwrap();
917 assert_eq!(result["status"], "success");
918 assert_eq!(result["total_memories"], 0);
919 }
920
921 #[tokio::test]
922 async fn network_stats_empty() {
923 let (_tmp, store) = open_store();
924 let tool = NetworkStatsTool::new(store);
925 let mut ctx = Context::new(wm_core::BrainWave::Beta);
926 let result = tool.call(&mut ctx, json!({})).await.unwrap();
927 assert_eq!(result["status"], "success");
928 assert_eq!(result["nodes"], 0);
929 assert_eq!(result["edges"], 0);
930 }
931
932 #[tokio::test]
933 async fn network_stats_with_edges() {
934 let (_tmp, store) = open_store();
935 let env = store.env();
936 let assoc_store = AssociationStore::open(env).unwrap();
937
938 let m1 = make_memory(Galaxy::Codex, "memory one", &["test"]);
939 let m2 = make_memory(Galaxy::Codex, "memory two", &["test"]);
940 store.put(Galaxy::Codex, &m1).unwrap();
941 store.put(Galaxy::Codex, &m2).unwrap();
942
943 assoc_store
944 .put(
945 env,
946 &wm_memory::Association::new(
947 m1.metadata.id,
948 m2.metadata.id,
949 wm_memory::LinkType::Related,
950 0.5,
951 ),
952 )
953 .unwrap();
954
955 let tool = NetworkStatsTool::new(store.clone());
956 let mut ctx = Context::new(wm_core::BrainWave::Beta);
957 let result = tool.call(&mut ctx, json!({})).await.unwrap();
958 assert_eq!(result["status"], "success");
959 assert_eq!(result["edges"], 1);
960 assert!(result["nodes"].as_u64().unwrap() >= 2);
961 }
962
963 #[tokio::test]
964 async fn network_centrality_basic() {
965 let (_tmp, store) = open_store();
966 let env = store.env();
967 let assoc_store = AssociationStore::open(env).unwrap();
968
969 let m1 = make_memory(Galaxy::Codex, "hub memory", &["hub"]);
970 let m2 = make_memory(Galaxy::Codex, "spoke one", &["spoke"]);
971 let m3 = make_memory(Galaxy::Codex, "spoke two", &["spoke"]);
972 store.put(Galaxy::Codex, &m1).unwrap();
973 store.put(Galaxy::Codex, &m2).unwrap();
974 store.put(Galaxy::Codex, &m3).unwrap();
975
976 assoc_store
977 .put(
978 env,
979 &wm_memory::Association::new(
980 m1.metadata.id,
981 m2.metadata.id,
982 wm_memory::LinkType::Related,
983 0.8,
984 ),
985 )
986 .unwrap();
987 assoc_store
988 .put(
989 env,
990 &wm_memory::Association::new(
991 m1.metadata.id,
992 m3.metadata.id,
993 wm_memory::LinkType::Related,
994 0.7,
995 ),
996 )
997 .unwrap();
998
999 let tool = NetworkCentralityTool::new(store.clone());
1000 let mut ctx = Context::new(wm_core::BrainWave::Beta);
1001 let result = tool.call(&mut ctx, json!({"top_k": 5})).await.unwrap();
1002 assert_eq!(result["status"], "success");
1003 let nodes = result["top_nodes"].as_array().unwrap();
1004 assert!(!nodes.is_empty());
1005 let first_degree = nodes[0]["total_degree"].as_u64().unwrap();
1006 assert!(first_degree >= 2);
1007 }
1008
1009 #[tokio::test]
1010 async fn network_centrality_empty() {
1011 let (_tmp, store) = open_store();
1012 let tool = NetworkCentralityTool::new(store);
1013 let mut ctx = Context::new(wm_core::BrainWave::Beta);
1014 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1015 assert_eq!(result["status"], "success");
1016 assert_eq!(result["total_nodes_with_edges"], 0);
1017 }
1018
1019 #[tokio::test]
1020 async fn network_clusters_empty() {
1021 let (_tmp, store) = open_store();
1022 let tool = NetworkClustersTool::new(store);
1023 let mut ctx = Context::new(wm_core::BrainWave::Beta);
1024 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1025 assert_eq!(result["status"], "success");
1026 assert_eq!(result["total_nodes"], 0);
1027 assert_eq!(result["num_clusters"], 0);
1028 }
1029
1030 #[tokio::test]
1031 async fn network_clusters_connected() {
1032 let (_tmp, store) = open_store();
1033 let env = store.env();
1034 let assoc_store = AssociationStore::open(env).unwrap();
1035
1036 let m1 = make_memory(Galaxy::Codex, "node one", &["cluster"]);
1037 let m2 = make_memory(Galaxy::Codex, "node two", &["cluster"]);
1038 let m3 = make_memory(Galaxy::Codex, "node three", &["cluster"]);
1039 store.put(Galaxy::Codex, &m1).unwrap();
1040 store.put(Galaxy::Codex, &m2).unwrap();
1041 store.put(Galaxy::Codex, &m3).unwrap();
1042
1043 assoc_store
1045 .put(
1046 env,
1047 &wm_memory::Association::new(
1048 m1.metadata.id,
1049 m2.metadata.id,
1050 wm_memory::LinkType::Related,
1051 0.5,
1052 ),
1053 )
1054 .unwrap();
1055 assoc_store
1056 .put(
1057 env,
1058 &wm_memory::Association::new(
1059 m2.metadata.id,
1060 m3.metadata.id,
1061 wm_memory::LinkType::Related,
1062 0.5,
1063 ),
1064 )
1065 .unwrap();
1066
1067 let tool = NetworkClustersTool::new(store.clone());
1068 let mut ctx = Context::new(wm_core::BrainWave::Beta);
1069 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1070 assert_eq!(result["status"], "success");
1071 assert_eq!(result["num_clusters"], 1);
1072 assert_eq!(result["largest_cluster_size"], 3);
1073 assert_eq!(result["isolated_nodes"], 0);
1074 }
1075
1076 #[tokio::test]
1077 async fn network_clusters_isolated() {
1078 let (_tmp, store) = open_store();
1079 let env = store.env();
1080 let assoc_store = AssociationStore::open(env).unwrap();
1081
1082 let m1 = make_memory(Galaxy::Codex, "node one", &["solo"]);
1083 let m2 = make_memory(Galaxy::Codex, "node two", &["solo"]);
1084 store.put(Galaxy::Codex, &m1).unwrap();
1085 store.put(Galaxy::Codex, &m2).unwrap();
1086
1087 assoc_store
1089 .put(
1090 env,
1091 &wm_memory::Association::new(
1092 m1.metadata.id,
1093 m2.metadata.id,
1094 wm_memory::LinkType::Related,
1095 0.5,
1096 ),
1097 )
1098 .unwrap();
1099
1100 let tool = NetworkClustersTool::new(store.clone());
1101 let mut ctx = Context::new(wm_core::BrainWave::Beta);
1102 let result = tool.call(&mut ctx, json!({})).await.unwrap();
1103 assert_eq!(result["num_clusters"], 1);
1104 assert_eq!(result["isolated_nodes"], 0);
1105 }
1106}