lean-ctx 3.9.18

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
//! Spatial pressure fields aggregated from stigmergic pheromone signals.
//!
//! Computes per-path activity pressure used to detect contested hotspots
//! and prioritize context for multi-agent workflows.

use std::collections::{HashMap, HashSet};

use super::signal::{PheromoneSignal, SignalKind};

/// Aggregated pressure from pheromone signals at a location.
#[derive(Debug, Clone, Default)]
pub struct PressureField {
    /// Total signal strength from all agents.
    pub total_strength: f64,
    /// Number of distinct agents with signals here.
    pub agent_count: usize,
    /// Dominant signal kind (most total strength).
    pub dominant_kind: Option<SignalKind>,
    /// Whether any agent is actively working here.
    pub is_contested: bool,
}

/// Map of file paths to their pressure fields.
#[derive(Debug, Clone, Default)]
pub struct PressureMap {
    /// Per-path aggregated pressure from all current signals.
    pub fields: HashMap<String, PressureField>,
}

impl PressureMap {
    /// Compute pressure fields from all current signals.
    ///
    /// Aggregates strength by path, counts distinct agents, and selects the
    /// dominant [`SignalKind`] per location.
    pub fn from_signals(signals: &[PheromoneSignal]) -> Self {
        let mut fields = HashMap::<String, PressureField>::new();
        let mut agents = HashMap::<String, HashSet<&str>>::new();
        let mut strengths = HashMap::<String, [f64; 6]>::new();

        for signal in signals {
            let field = fields.entry(signal.path.clone()).or_default();
            field.total_strength += signal.strength;
            field.is_contested |= signal.kind == SignalKind::Active;

            agents
                .entry(signal.path.clone())
                .or_default()
                .insert(&signal.agent_id);
            strengths.entry(signal.path.clone()).or_default()[kind_index(signal.kind)] +=
                signal.strength;
        }

        for (path, field) in &mut fields {
            field.agent_count = agents.get(path).map_or(0, HashSet::len);
            field.dominant_kind = strengths.get(path).and_then(dominant_kind);
        }

        Self { fields }
    }

    /// Get pressure for a specific path.
    pub fn pressure_at(&self, path: &str) -> PressureField {
        self.fields.get(path).cloned().unwrap_or_default()
    }

    /// Files with the highest pressure (most agent activity).
    pub fn hotspots(&self, top_n: usize) -> Vec<(&str, &PressureField)> {
        let mut hotspots: Vec<_> = self
            .fields
            .iter()
            .map(|(path, field)| (path.as_str(), field))
            .collect();
        hotspots.sort_by(|(path_a, field_a), (path_b, field_b)| {
            field_b
                .total_strength
                .total_cmp(&field_a.total_strength)
                .then_with(|| path_a.cmp(path_b))
        });
        hotspots.truncate(top_n);
        hotspots
    }
}

fn kind_index(kind: SignalKind) -> usize {
    match kind {
        SignalKind::Active => 0,
        SignalKind::Complexity => 1,
        SignalKind::ReviewNeeded => 2,
        SignalKind::Issue => 3,
        SignalKind::Completed => 4,
        SignalKind::Exploration => 5,
    }
}

fn dominant_kind(strengths: &[f64; 6]) -> Option<SignalKind> {
    const KINDS: [SignalKind; 6] = [
        SignalKind::Active,
        SignalKind::Complexity,
        SignalKind::ReviewNeeded,
        SignalKind::Issue,
        SignalKind::Completed,
        SignalKind::Exploration,
    ];
    strengths
        .iter()
        .enumerate()
        .max_by(|(index_a, strength_a), (index_b, strength_b)| {
            strength_a
                .total_cmp(strength_b)
                .then_with(|| index_b.cmp(index_a))
        })
        .map(|(index, _)| KINDS[index])
}

#[cfg(test)]
mod tests {
    use chrono::Utc;

    use super::*;

    fn signal(agent_id: &str, path: &str, kind: SignalKind, strength: f64) -> PheromoneSignal {
        PheromoneSignal {
            agent_id: agent_id.to_string(),
            kind,
            path: path.to_string(),
            symbol: None,
            strength,
            deposited_at: Utc::now(),
            note: None,
        }
    }

    #[test]
    fn empty_signals_gives_empty_map() {
        let map = PressureMap::from_signals(&[]);

        assert!(map.fields.is_empty());
        assert_eq!(map.pressure_at("missing.rs").agent_count, 0);
    }

    #[test]
    fn single_signal_creates_field() {
        let map =
            PressureMap::from_signals(&[signal("agent-1", "src/lib.rs", SignalKind::Active, 0.8)]);

        let field = map.pressure_at("src/lib.rs");
        assert_eq!(field.total_strength, 0.8);
        assert_eq!(field.agent_count, 1);
        assert_eq!(field.dominant_kind, Some(SignalKind::Active));
        assert!(field.is_contested);
    }

    #[test]
    fn multiple_agents_increase_pressure() {
        let map = PressureMap::from_signals(&[
            signal("agent-1", "src/lib.rs", SignalKind::Complexity, 0.7),
            signal("agent-2", "src/lib.rs", SignalKind::Complexity, 0.6),
            signal("agent-1", "src/lib.rs", SignalKind::Completed, 0.2),
        ]);

        let field = map.pressure_at("src/lib.rs");
        assert!((field.total_strength - 1.5).abs() < 1e-10);
        assert_eq!(field.agent_count, 2);
        assert_eq!(field.dominant_kind, Some(SignalKind::Complexity));
        assert!(!field.is_contested);
    }

    #[test]
    fn hotspots_returns_top_n() {
        let map = PressureMap::from_signals(&[
            signal("agent-1", "low.rs", SignalKind::Completed, 0.2),
            signal("agent-2", "high.rs", SignalKind::Active, 0.9),
            signal("agent-3", "medium.rs", SignalKind::Issue, 0.5),
        ]);

        let hotspots = map.hotspots(2);

        assert_eq!(hotspots.len(), 2);
        assert_eq!(hotspots[0].0, "high.rs");
        assert_eq!(hotspots[1].0, "medium.rs");
    }
}