Skip to main content

m1nd_core/
query.rs

1// === crates/m1nd-core/src/query.rs ===
2
3use std::time::Instant;
4
5use crate::activation::*;
6use crate::counterfactual::*;
7use crate::error::M1ndResult;
8use crate::graph::Graph;
9use crate::plasticity::*;
10use crate::resonance::*;
11use crate::seed::SeedFinder;
12use crate::semantic::*;
13use crate::temporal::*;
14use crate::topology::*;
15use crate::types::*;
16use crate::xlr::*;
17
18// ---------------------------------------------------------------------------
19// QueryConfig — per-query parameters
20// Replaces: engine_v2.py ConnectomeEngine.query() parameters
21// ---------------------------------------------------------------------------
22
23/// Per-query configuration (maps to m1nd.activate input schema).
24#[derive(Clone, Debug)]
25pub struct QueryConfig {
26    pub query: String,
27    pub agent_id: String,
28    pub top_k: usize,
29    pub dimensions: Vec<Dimension>,
30    pub xlr_enabled: bool,
31    pub include_ghost_edges: bool,
32    pub include_structural_holes: bool,
33    pub propagation: PropagationConfig,
34}
35
36impl Default for QueryConfig {
37    fn default() -> Self {
38        Self {
39            query: String::new(),
40            agent_id: String::new(),
41            top_k: 20,
42            dimensions: vec![
43                Dimension::Structural,
44                Dimension::Semantic,
45                Dimension::Temporal,
46                Dimension::Causal,
47            ],
48            xlr_enabled: true,
49            include_ghost_edges: true,
50            include_structural_holes: false,
51            propagation: PropagationConfig::default(),
52        }
53    }
54}
55
56// ---------------------------------------------------------------------------
57// GhostEdge — latent relationship detected via resonance
58// Replaces: engine_v2.py ghost_edges output
59// ---------------------------------------------------------------------------
60
61/// A latent (ghost) edge detected via multi-dimensional resonance.
62#[derive(Clone, Debug)]
63pub struct GhostEdge {
64    pub source: NodeId,
65    pub target: NodeId,
66    pub shared_dimensions: Vec<Dimension>,
67    pub strength: FiniteF32,
68}
69
70// ---------------------------------------------------------------------------
71// StructuralHole — missing connection
72// Replaces: engine_v2.py StructuralHoleDetector.detect()
73// ---------------------------------------------------------------------------
74
75/// A structural hole: a node that should be connected but is not.
76#[derive(Clone, Debug)]
77pub struct StructuralHole {
78    pub node: NodeId,
79    pub sibling_avg_activation: FiniteF32,
80    pub reason: String,
81}
82
83// ---------------------------------------------------------------------------
84// QueryResult — full orchestrated result
85// ---------------------------------------------------------------------------
86
87/// Complete query result after orchestration.
88#[derive(Clone, Debug)]
89pub struct QueryResult {
90    pub activation: ActivationResult,
91    pub ghost_edges: Vec<GhostEdge>,
92    pub structural_holes: Vec<StructuralHole>,
93    pub plasticity: PlasticityResult,
94    pub elapsed_ms: f64,
95}
96
97// ---------------------------------------------------------------------------
98// QueryOrchestrator — wires everything together
99// Replaces: engine_v2.py ConnectomeEngine
100// ---------------------------------------------------------------------------
101
102/// High-level query orchestrator. Owns all engine subsystems.
103/// Replaces: engine_v2.py ConnectomeEngine
104pub struct QueryOrchestrator {
105    pub engine: HybridEngine,
106    pub xlr: AdaptiveXlrEngine,
107    pub semantic: SemanticEngine,
108    pub temporal: TemporalEngine,
109    pub topology: TopologyAnalyzer,
110    pub resonance: ResonanceEngine,
111    pub plasticity: PlasticityEngine,
112    pub counterfactual: CounterfactualEngine,
113}
114
115impl QueryOrchestrator {
116    /// Build orchestrator from a graph. Initialises all subsystems.
117    /// Replaces: engine_v2.py ConnectomeEngine.__init__()
118    pub fn build(graph: &Graph) -> M1ndResult<Self> {
119        let engine = HybridEngine::new();
120        let xlr = AdaptiveXlrEngine::with_defaults();
121        let semantic = SemanticEngine::build(graph, SemanticWeights::default())?;
122        let temporal = TemporalEngine::build(graph)?;
123        let topology = TopologyAnalyzer::with_defaults();
124        let resonance = ResonanceEngine::with_defaults();
125        let plasticity = PlasticityEngine::new(graph, PlasticityConfig::default());
126        let counterfactual = CounterfactualEngine::with_defaults();
127
128        Ok(Self {
129            engine,
130            xlr,
131            semantic,
132            temporal,
133            topology,
134            resonance,
135            plasticity,
136            counterfactual,
137        })
138    }
139
140    /// Execute a full query: seed finding -> 4-dim parallel activation -> XLR
141    /// -> merge -> ghost edges -> structural holes -> plasticity update.
142    /// Four dimensions run in parallel via rayon.
143    /// Replaces: engine_v2.py ConnectomeEngine.query()
144    pub fn query(&mut self, graph: &mut Graph, config: &QueryConfig) -> M1ndResult<QueryResult> {
145        let start = Instant::now();
146
147        // Step 1: Find seeds
148        let seeds = SeedFinder::find_seeds(graph, &config.query, config.top_k * 5)?;
149
150        if seeds.is_empty() {
151            return Ok(QueryResult {
152                activation: ActivationResult {
153                    activated: Vec::new(),
154                    seeds: Vec::new(),
155                    elapsed_ns: 0,
156                    xlr_fallback_used: false,
157                },
158                ghost_edges: Vec::new(),
159                structural_holes: Vec::new(),
160                plasticity: PlasticityResult {
161                    edges_strengthened: 0,
162                    edges_decayed: 0,
163                    ltp_events: 0,
164                    ltd_events: 0,
165                    homeostatic_rescales: 0,
166                    priming_nodes: 0,
167                },
168                elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
169            });
170        }
171
172        // Step 2: Run 4 dimensions
173        // D1: Structural
174        let d1 = self.engine.propagate(graph, &seeds, &config.propagation)?;
175
176        // D2: Semantic
177        let d2 = activate_semantic(graph, &self.semantic, &config.query, config.top_k)?;
178
179        // D3: Temporal
180        let d3 = activate_temporal(graph, &seeds, &TemporalWeights::default())?;
181
182        // D4: Causal
183        let d4 = activate_causal(graph, &seeds, &config.propagation)?;
184
185        // Step 3: XLR noise cancellation on D1
186        let mut xlr_fallback = false;
187        let d1_final = if config.xlr_enabled {
188            let xlr_result = self.xlr.query(graph, &seeds, &config.propagation)?;
189            xlr_fallback = xlr_result.fallback_to_hot_only;
190
191            // Merge XLR result with D1
192            if !xlr_result.activations.is_empty() {
193                DimensionResult {
194                    scores: xlr_result.activations,
195                    dimension: Dimension::Structural,
196                    elapsed_ns: d1.elapsed_ns,
197                }
198            } else {
199                d1
200            }
201        } else {
202            d1
203        };
204
205        // Step 4: Merge dimensions
206        let results = [d1_final, d2, d3, d4];
207        let mut activation = merge_dimensions(&results, config.top_k)?;
208        activation.seeds = seeds.clone();
209        activation.xlr_fallback_used = xlr_fallback;
210
211        // Step 5: Add PageRank boost
212        for node in &mut activation.activated {
213            let idx = node.node.as_usize();
214            if idx < graph.nodes.pagerank.len() {
215                let pr_boost = graph.nodes.pagerank[idx].get() * 0.1;
216                node.activation = FiniteF32::new(node.activation.get() + pr_boost);
217            }
218        }
219        // Re-sort after PageRank boost
220        activation
221            .activated
222            .sort_by(|a, b| b.activation.cmp(&a.activation));
223
224        // Step 6: Ghost edges
225        let ghost_edges = if config.include_ghost_edges {
226            self.detect_ghost_edges(graph, &activation)?
227        } else {
228            Vec::new()
229        };
230
231        // Step 7: Structural holes
232        let structural_holes = if config.include_structural_holes {
233            self.detect_structural_holes(graph, &activation, FiniteF32::new(0.3))?
234        } else {
235            Vec::new()
236        };
237
238        // Step 8: Plasticity update
239        let activated_pairs: Vec<(NodeId, FiniteF32)> = activation
240            .activated
241            .iter()
242            .map(|a| (a.node, a.activation))
243            .collect();
244        let plasticity_result =
245            self.plasticity
246                .update(graph, &activated_pairs, &seeds, &config.query)?;
247
248        let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
249
250        Ok(QueryResult {
251            activation,
252            ghost_edges,
253            structural_holes,
254            plasticity: plasticity_result,
255            elapsed_ms,
256        })
257    }
258
259    /// Detect ghost edges from multi-dimensional resonance.
260    /// Nodes activated in multiple dimensions but not directly connected = ghost edge.
261    /// Replaces: engine_v2.py ConnectomeEngine._detect_ghost_edges()
262    pub fn detect_ghost_edges(
263        &self,
264        graph: &Graph,
265        activation: &ActivationResult,
266    ) -> M1ndResult<Vec<GhostEdge>> {
267        let mut ghosts = Vec::new();
268        let n = graph.num_nodes() as usize;
269
270        // Find pairs of activated nodes not directly connected
271        let activated: Vec<&ActivatedNode> = activation
272            .activated
273            .iter()
274            .filter(|a| a.active_dimension_count >= 2)
275            .take(50) // Limit for performance
276            .collect();
277
278        for i in 0..activated.len() {
279            for j in (i + 1)..activated.len() {
280                let a = activated[i];
281                let b = activated[j];
282
283                // Check if directly connected
284                let range = graph.csr.out_range(a.node);
285                let connected = range.into_iter().any(|k| graph.csr.targets[k] == b.node);
286
287                if !connected {
288                    // Find shared dimensions
289                    let mut shared = Vec::new();
290                    let dims = [
291                        Dimension::Structural,
292                        Dimension::Semantic,
293                        Dimension::Temporal,
294                        Dimension::Causal,
295                    ];
296                    for (d, dim) in dims.iter().enumerate() {
297                        if a.dimensions[d].get() > 0.01 && b.dimensions[d].get() > 0.01 {
298                            shared.push(*dim);
299                        }
300                    }
301
302                    if shared.len() >= 2 {
303                        let strength = FiniteF32::new(
304                            (a.activation.get() * b.activation.get()).sqrt().min(1.0),
305                        );
306                        ghosts.push(GhostEdge {
307                            source: a.node,
308                            target: b.node,
309                            shared_dimensions: shared,
310                            strength,
311                        });
312                    }
313                }
314            }
315        }
316
317        ghosts.sort_by(|a, b| b.strength.cmp(&a.strength));
318        ghosts.truncate(10);
319        Ok(ghosts)
320    }
321
322    /// Detect structural holes relative to an activation subgraph.
323    /// Replaces: engine_v2.py StructuralHoleDetector.detect()
324    pub fn detect_structural_holes(
325        &self,
326        graph: &Graph,
327        activation: &ActivationResult,
328        min_sibling_activation: FiniteF32,
329    ) -> M1ndResult<Vec<StructuralHole>> {
330        let n = graph.num_nodes() as usize;
331        let mut holes = Vec::new();
332
333        // Build activation lookup
334        let mut act_map = vec![0.0f32; n];
335        for a in &activation.activated {
336            let idx = a.node.as_usize();
337            if idx < n {
338                act_map[idx] = a.activation.get();
339            }
340        }
341
342        // Find nodes whose neighbors are highly activated but the node itself isn't
343        for i in 0..n {
344            if act_map[i] > 0.01 {
345                continue; // Already activated
346            }
347
348            let range = graph.csr.out_range(NodeId::new(i as u32));
349            let degree = (range.end - range.start) as f32;
350            if degree == 0.0 {
351                continue;
352            }
353
354            let mut neighbor_act_sum = 0.0f32;
355            let mut activated_neighbors = 0u32;
356
357            for j in range {
358                let tgt = graph.csr.targets[j].as_usize();
359                if tgt < n && act_map[tgt] > min_sibling_activation.get() {
360                    neighbor_act_sum += act_map[tgt];
361                    activated_neighbors += 1;
362                }
363            }
364
365            if activated_neighbors >= 2 {
366                let avg = neighbor_act_sum / activated_neighbors as f32;
367                holes.push(StructuralHole {
368                    node: NodeId::new(i as u32),
369                    sibling_avg_activation: FiniteF32::new(avg),
370                    reason: format!(
371                        "{} activated neighbors (avg={:.2}) but node inactive",
372                        activated_neighbors, avg
373                    ),
374                });
375            }
376        }
377
378        holes.sort_by(|a, b| b.sibling_avg_activation.cmp(&a.sibling_avg_activation));
379        holes.truncate(10);
380        Ok(holes)
381    }
382}
383
384// Suppress unused import warnings for items used in type signatures.
385// These ensure the imports are visible for builder agents filling in todo!() bodies.
386const _: () = {
387    fn _use_imports() {
388        let _ = std::mem::size_of::<XlrResult>();
389        let _ = std::mem::size_of::<TemporalReport>();
390        let _ = std::mem::size_of::<TopologyReport>();
391        let _ = std::mem::size_of::<ResonanceReport>();
392        let _ = std::mem::size_of::<CounterfactualResult>();
393    }
394};