wm-tools 9.2.3

Curated tool implementations for the WhiteMagic MCP server.
Documentation
//! Constellation tools — detect, list.

#![forbid(unsafe_code)]

use async_trait::async_trait;

use super::common::galaxy_name;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Arc;
use wm_core::{Context, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
use wm_memory::MemoryStore;

pub struct ConstellationDetectTool {
    store: Arc<MemoryStore>,
    stats: ToolStats,
    effects: EffectRow,
}

impl ConstellationDetectTool {
    pub fn new(store: Arc<MemoryStore>) -> Self {
        Self {
            store,
            stats: ToolStats::default(),
            effects: EffectRow::read_only(vec![Resource::Galaxy("universal".into())]),
        }
    }
}

#[async_trait]
impl Tool for ConstellationDetectTool {
    fn name(&self) -> &str {
        "constellation.detect"
    }
    fn gana(&self) -> Gana {
        Gana::Star
    }
    fn effects(&self) -> &EffectRow {
        &self.effects
    }
    fn description(&self) -> &str {
        "Detect tag clusters (constellations) across galaxies"
    }
    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
        let min_cluster = args
            .get("min_cluster_size")
            .and_then(serde_json::Value::as_u64)
            .unwrap_or(3) as usize;
        let spatial = args.get("spatial").and_then(Value::as_bool).unwrap_or(true);
        if spatial {
            let mut detector = wm_cognitive::constellation::ConstellationDetector::default();
            if let Ok(report) = detector.detect(&self.store) {
                if !report.constellations.is_empty() {
                    let clusters: Vec<Value> = report
                        .constellations
                        .into_iter()
                        .map(|c| {
                            json!({
                                "name": c.name,
                                "count": c.size,
                                "centroid": [c.centroid.0, c.centroid.1, c.centroid.2],
                                "dominant_tags": c.dominant_tags,
                                "galaxies": c.galaxies.iter().map(|g| g.db_name()).collect::<Vec<_>>(),
                                "sample_memory_ids": c.memory_ids.into_iter().take(5).map(|id| id.to_string()).collect::<Vec<_>>()
                            })
                        })
                        .collect();

                    return Ok(json!({
                        "status": "success",
                        "mode": "spatial_density_clustering_5d",
                        "memories_analyzed": report.memories_analyzed,
                        "dense_cells": report.dense_cells,
                        "constellations": clusters.len(),
                        "clusters": clusters
                    }));
                }
            }
        }

        let mut tag_locations: HashMap<String, Vec<(String, uuid::Uuid)>> = HashMap::new();
        for galaxy in Galaxy::memory_galaxies() {
            let memories = self.store.scan(galaxy, 500)?;
            for mem in memories {
                for tag in &mem.metadata.tags {
                    tag_locations
                        .entry(tag.clone())
                        .or_default()
                        .push((galaxy_name(galaxy).to_string(), mem.metadata.id));
                }
            }
        }
        let constellations: Vec<Value> = tag_locations.iter()
            .filter(|(_, locs)| locs.len() >= min_cluster)
            .map(|(tag, locs)| json!({
                "tag": tag,
                "count": locs.len(),
                "galaxies": locs.iter().map(|(g, _)| g.clone()).collect::<std::collections::HashSet<_>>().len(),
            }))
            .collect();
        Ok(json!({
            "status": "success",
            "mode": "tag_frequency_clustering",
            "min_cluster_size": min_cluster,
            "constellations": constellations.len(),
            "clusters": constellations,
        }))
    }
    fn stats(&self) -> &ToolStats {
        &self.stats
    }
}

/// `constellation.list` — list all detected constellations.
pub struct ConstellationListTool {
    store: Arc<MemoryStore>,
    stats: ToolStats,
    effects: EffectRow,
}

impl ConstellationListTool {
    pub fn new(store: Arc<MemoryStore>) -> Self {
        Self {
            store,
            stats: ToolStats::default(),
            effects: EffectRow::read_only(vec![Resource::Galaxy("universal".into())]),
        }
    }
}

#[async_trait]
impl Tool for ConstellationListTool {
    fn name(&self) -> &str {
        "constellation.list"
    }
    fn gana(&self) -> Gana {
        Gana::Star
    }
    fn effects(&self) -> &EffectRow {
        &self.effects
    }
    fn description(&self) -> &str {
        "List all tag constellations (clusters with 3+ memories)"
    }
    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
        let mut tag_counts: HashMap<String, usize> = HashMap::new();
        for galaxy in Galaxy::memory_galaxies() {
            let memories = self.store.scan(galaxy, 500)?;
            for mem in memories {
                for tag in &mem.metadata.tags {
                    *tag_counts.entry(tag.clone()).or_insert(0) += 1;
                }
            }
        }
        let constellations: Vec<(String, usize)> =
            tag_counts.into_iter().filter(|(_, c)| *c >= 3).collect();
        Ok(json!({
            "status": "success",
            "total_tags": constellations.len(),
            "constellations": constellations.into_iter().map(|(tag, count)| json!({
                "tag": tag,
                "count": count,
            })).collect::<Vec<_>>(),
        }))
    }
    fn stats(&self) -> &ToolStats {
        &self.stats
    }
}