Shared memory and context tools for agentic work.
Code Rooms
// === m1nd-core/src/twins.rs ===
// @m1nd:temponizer:WIRING — new algorithm but bounded scope (signature + cosine)
// @m1nd:emca:pattern — EXECUTE(signatures) → MEASURE(isomorphic test) → ADJUST(field names)
// @m1nd:primitives — topology (degree), cosine_similarity (custom)
//
// RB-03 — Structural Twins: topological isomorphism detection.
// Computes a structural "fingerprint" for each node based on its graph
// neighbourhood (in-degree, out-degree, edge types, neighbor type distribution)
// and discovers pairs/clusters of nodes with highly similar signatures.
// Unlike semantic similarity, this is purely topological — two functions with
// completely different names/purposes but identical structural roles (e.g. two
// retry wrappers, two CRUD handlers) will be matched.
use crate::error::{M1ndError, M1ndResult};
use crate::graph::Graph;
use crate::types::{FiniteF32, NodeId, NodeType};
use serde::Serialize;
use std::collections::HashMap;
// ---------------------------------------------------------------------------
// Configuration
/// Configuration for structural twin detection.
#[derive(Clone, Debug)]
pub struct TwinConfig {
/// Minimum cosine similarity to consider a pair as twins [0.0, 1.0].
pub similarity_threshold: f32,
/// Maximum twins to return.
pub top_k: usize,
/// Which node types to compare (empty = all).
pub node_types: Vec<NodeType>,
/// File path prefix filter (empty = all).
pub scope: Option<String>,
/// Whether to include edge type distribution in signature.
pub use_edge_types: bool,
/// Minimum identifier-token Jaccard overlap between the two labels for a
/// pair to count as twins. Pure topological signatures saturate at cosine
/// 1.0 for any two same-shaped nodes (e.g. two unrelated structs), which
/// floods results with false positives. Requiring some shared name tokens
/// keeps real near-duplicates (`parse_a` ~ `parse_b`) and drops shape-only
/// coincidences (`BridgeRequest` ~ `BootMemoryInput`). 0.0 disables the gate.
pub name_overlap_min: f32,
}
impl Default for TwinConfig {
fn default() -> Self {
Self {
similarity_threshold: 0.80,
top_k: 50,
node_types: vec![],
scope: None,
use_edge_types: true,
name_overlap_min: 0.15,
// Result types
/// A detected structural twin pair.
#[derive(Clone, Debug, Serialize)]
pub struct TwinPair {
/// External ID of node A.
pub node_a_id: String,
/// Label of node A.
pub node_a_label: String,
/// External ID of node B.
pub node_b_id: String,
/// Label of node B.
pub node_b_label: String,
/// Cosine similarity of structural signatures.
pub similarity: f32,
/// Identifier-token Jaccard overlap of the two labels [0.0, 1.0].
pub name_overlap: f32,
/// Shared structural properties.
pub shared_properties: Vec<String>,
/// Structural signature for a single node.
struct StructuralSignature {
node_id: NodeId,
ext_id: String,
label: String,
features: Vec<f32>,
/// Result of twin detection.
pub struct TwinResult {
/// Twin pairs sorted by similarity descending.
pub pairs: Vec<TwinPair>,
/// Number of nodes analyzed.
pub nodes_analyzed: usize,
/// Number of signatures computed.
pub signatures_computed: usize,
/// Elapsed time in ms.
pub elapsed_ms: f64,
// Feature extraction
/// Convert NodeType to a numeric discriminant (safe alternative to `as u8`
/// since NodeType has a Custom(u8) variant making it non-fieldless).
fn node_type_as_f32(nt: &NodeType) -> f32 {
match nt {
NodeType::File => 0.0,
NodeType::Directory => 1.0,
NodeType::Function => 2.0,
NodeType::Class => 3.0,
NodeType::Struct => 4.0,
NodeType::Enum => 5.0,
NodeType::Type => 6.0,
NodeType::Module => 7.0,
NodeType::Reference => 8.0,
NodeType::Concept => 9.0,
NodeType::Material => 10.0,
NodeType::Process => 11.0,
NodeType::Product => 12.0,
NodeType::Supplier => 13.0,
NodeType::Regulatory => 14.0,
NodeType::System => 15.0,
NodeType::Cost => 16.0,
NodeType::Custom(v) => 17.0 + *v as f32,
/// Number of base features (before edge type expansion).
const BASE_FEATURES: usize = 8;
/// Compute structural signature features for a node.
fn compute_signature(
graph: &Graph,
node: NodeId,
n: usize,
edge_type_vocab: &HashMap<String, usize>,
use_edge_types: bool,
) -> Vec<f32> {
let edge_vocab_size = if use_edge_types {
edge_type_vocab.len()
} else {
0
};
let total_features = BASE_FEATURES + edge_vocab_size * 2; // outgoing + incoming edge types
let mut features = vec![0.0f32; total_features];
let idx = node.as_usize();
if idx >= n || !graph.finalized {
return features;
// Feature 0: out-degree
let out_range = graph.csr.out_range(node);
let out_degree = out_range.len() as f32;
features[0] = out_degree;
// Feature 1: in-degree
let in_range = graph.csr.in_range(node);
let in_degree = in_range.len() as f32;
features[1] = in_degree;
// Feature 2: degree ratio (in / (in + out))
let total_degree = in_degree + out_degree;
features[2] = if total_degree > 0.0 {
in_degree / total_degree
0.5
// Feature 3: pagerank
features[3] = graph.nodes.pagerank[idx].get();
// Feature 4: node type as one-hot position
features[4] = node_type_as_f32(&graph.nodes.node_type[idx]);
// Feature 5: change frequency
features[5] = graph.nodes.change_frequency[idx].get();
// Feature 6: number of tags
features[6] = graph.nodes.tags[idx].len() as f32;
// Feature 7: neighbor type distribution entropy
let mut type_counts: [f32; 8] = [0.0; 8]; // up to 8 NodeType variants
for edge_idx in out_range.clone() {
let target = graph.csr.targets[edge_idx];
let tidx = target.as_usize();
if tidx < n {
let t = node_type_as_f32(&graph.nodes.node_type[tidx]) as usize;
if t < type_counts.len() {
type_counts[t] += 1.0;
// Shannon entropy of neighbor type distribution
let total: f32 = type_counts.iter().sum();
if total > 0.0 {
let entropy: f32 = type_counts
.iter()
.filter(|&&c| c > 0.0)
.map(|&c| {
let p = c / total;
-p * p.ln()
})
.sum();
features[7] = entropy;
// Edge type distribution features (outgoing + incoming)
if use_edge_types {
for edge_idx in graph.csr.out_range(node) {
let rel = graph.strings.resolve(graph.csr.relations[edge_idx]);
if let Some(&vocab_idx) = edge_type_vocab.get(rel) {
features[BASE_FEATURES + vocab_idx] += 1.0;
for edge_idx in graph.csr.in_range(node) {
let fwd_idx = graph.csr.rev_edge_idx[edge_idx];
let rel = graph
.strings
.resolve(graph.csr.relations[fwd_idx.as_usize()]);
features[BASE_FEATURES + edge_vocab_size + vocab_idx] += 1.0;
features
/// Split an identifier label into lowercase tokens, breaking on camelCase
/// boundaries, digits, and non-alphanumeric separators (`_`, `::`, `.`, etc.).
/// e.g. `BridgeRequest` -> {bridge, request}; `parse_git_history` -> {parse, git, history}.
fn tokenize_label(label: &str) -> std::collections::HashSet<String> {
let mut tokens = std::collections::HashSet::new();
let mut cur = String::new();
let mut prev_lower = false;
for ch in label.chars() {
if ch.is_ascii_alphanumeric() {
// camelCase boundary: lowercase/digit followed by uppercase.
if prev_lower && ch.is_ascii_uppercase() && !cur.is_empty() {
tokens.insert(std::mem::take(&mut cur).to_ascii_lowercase());
cur.push(ch);
prev_lower = ch.is_ascii_lowercase() || ch.is_ascii_digit();
} else if !cur.is_empty() {
prev_lower = false;
if !cur.is_empty() {
tokens.insert(cur.to_ascii_lowercase());
tokens.remove("");
tokens
/// Jaccard overlap of the identifier tokens of two labels [0.0, 1.0].
fn name_overlap(label_a: &str, label_b: &str) -> f32 {
let a = tokenize_label(label_a);
let b = tokenize_label(label_b);
if a.is_empty() || b.is_empty() {
return 0.0;
let inter = a.intersection(&b).count() as f32;
let union = a.union(&b).count() as f32;
if union == 0.0 {
0.0
inter / union
/// Cosine similarity between two feature vectors.
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a < f32::EPSILON || norm_b < f32::EPSILON {
(dot / (norm_a * norm_b)).clamp(0.0, 1.0)
/// Describe the shared structural properties between two signatures.
fn describe_shared(a: &[f32], b: &[f32]) -> Vec<String> {
let mut props = Vec::new();
if a.len() < BASE_FEATURES || b.len() < BASE_FEATURES {
return props;
if (a[0] - b[0]).abs() < 0.5 {
props.push(format!("out_degree≈{:.0}", a[0]));
if (a[1] - b[1]).abs() < 0.5 {
props.push(format!("in_degree≈{:.0}", a[1]));
if (a[4] - b[4]).abs() < 0.5 {
props.push("same_node_type".to_string());
if (a[6] - b[6]).abs() < 0.5 {
props.push(format!("tag_count≈{:.0}", a[6]));
props
// Engine
/// Find structural twins in the graph.
pub fn find_twins(graph: &Graph, config: &TwinConfig) -> M1ndResult<TwinResult> {
let start = std::time::Instant::now();
let n = graph.num_nodes() as usize;
if n == 0 || !graph.finalized {
return Err(M1ndError::EmptyGraph);
// Build reverse map: NodeId -> external_id
let mut node_to_ext: Vec<String> = vec![String::new(); n];
for (interned, node_id) in &graph.id_to_node {
let idx = node_id.as_usize();
if idx < n {
node_to_ext[idx] = graph.strings.resolve(*interned).to_string();
// Build edge type vocabulary
let mut edge_type_vocab: HashMap<String, usize> = HashMap::new();
if config.use_edge_types {
for i in 0..n {
let nid = NodeId::new(i as u32);
for edge_idx in graph.csr.out_range(nid) {
.resolve(graph.csr.relations[edge_idx])
.to_string();
let len = edge_type_vocab.len();
edge_type_vocab.entry(rel).or_insert(len);
// Filter nodes by config
let candidate_nodes: Vec<NodeId> = (0..n)
.filter(|&i| {
// Type filter
if !config.node_types.is_empty()
&& !config.node_types.contains(&graph.nodes.node_type[i])
{
return false;
// Scope filter
if let Some(ref scope) = config.scope {
if !node_to_ext[i].contains(scope.as_str()) {
true
.map(|i| NodeId::new(i as u32))
.collect();
// Compute signatures
let signatures: Vec<StructuralSignature> = candidate_nodes
.map(|&nid| {
let idx = nid.as_usize();
StructuralSignature {
node_id: nid,
ext_id: node_to_ext[idx].clone(),
label: graph.strings.resolve(graph.nodes.label[idx]).to_string(),
features: compute_signature(graph, nid, n, &edge_type_vocab, config.use_edge_types),
let sigs_count = signatures.len();
// Pairwise comparison (O(n²) but capped by candidate count)
let mut pairs: Vec<TwinPair> = Vec::new();
let budget = 100_000usize; // max comparisons
let mut comparisons = 0usize;
for i in 0..signatures.len() {
if comparisons >= budget {
break;
for j in (i + 1)..signatures.len() {
comparisons += 1;
let sim = cosine_similarity(&signatures[i].features, &signatures[j].features);
if sim < config.similarity_threshold {
continue;
// Gate on identifier-token overlap: pure topological cosine saturates
// at 1.0 for any two same-shaped nodes, so require shared name tokens
// to drop shape-only coincidences while keeping real near-duplicates.
let overlap = name_overlap(&signatures[i].label, &signatures[j].label);
if overlap < config.name_overlap_min {
pairs.push(TwinPair {
node_a_id: signatures[i].ext_id.clone(),
node_a_label: signatures[i].label.clone(),
node_b_id: signatures[j].ext_id.clone(),
node_b_label: signatures[j].label.clone(),
similarity: sim,
name_overlap: overlap,
shared_properties: describe_shared(
&signatures[i].features,
&signatures[j].features,
),
});
// Sort by similarity descending and truncate
pairs.sort_by(|a, b| {
b.similarity
.partial_cmp(&a.similarity)
.unwrap_or(std::cmp::Ordering::Equal)
pairs.truncate(config.top_k);
Ok(TwinResult {
pairs,
nodes_analyzed: candidate_nodes.len(),
signatures_computed: sigs_count,
elapsed_ms: start.elapsed().as_secs_f64() * 1000.0,
// Tests
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::*;
use crate::types::{EdgeDirection, FiniteF32};
/// Build a graph with two structurally identical subgraphs:
/// handler_a → process_a → output_a
/// handler_b → process_b → output_b
fn build_twin_graph() -> Graph {
let mut g = Graph::new();
// Subgraph A
g.add_node(
"a_h",
"handler_a",
NodeType::Function,
&["handler"],
0.0,
0.5,
)
.unwrap();
g.add_node("a_p", "process_a", NodeType::Function, &["data"], 0.0, 0.3)
g.add_node("a_o", "output_a", NodeType::Function, &["output"], 0.0, 0.2)
// Subgraph B (structural twin)
"b_h",
"handler_b",
g.add_node("b_p", "process_b", NodeType::Function, &["data"], 0.0, 0.3)
g.add_node("b_o", "output_b", NodeType::Function, &["output"], 0.0, 0.2)
// Edges A
g.add_edge(
NodeId::new(0),
NodeId::new(1),
"calls",
FiniteF32::new(0.8),
EdgeDirection::Forward,
false,
FiniteF32::new(0.5),
NodeId::new(2),
FiniteF32::new(0.7),
FiniteF32::new(0.4),
// Edges B (same structure)
NodeId::new(3),
NodeId::new(4),
NodeId::new(5),
g.finalize().unwrap();
g
/// Build a graph with structurally different nodes.
fn build_different_graph() -> Graph {
// Hub node (high out-degree)
g.add_node("hub", "dispatcher", NodeType::Function, &["core"], 0.0, 0.9)
// Leaf nodes (low degree)
"leaf1",
"handler_1",
0.1,
"leaf2",
"handler_2",
"leaf3",
"handler_3",
// Hub fans out to all leaves
#[test]
fn cosine_sim_identical() {
let a = vec![1.0, 2.0, 3.0];
assert!((cosine_similarity(&a, &a) - 1.0).abs() < 0.001);
fn tokenize_splits_camel_and_snake() {
let t = tokenize_label("BridgeRequest");
assert!(t.contains("bridge") && t.contains("request"), "got {:?}", t);
let t2 = tokenize_label("parse_git_history");
assert!(t2.contains("parse") && t2.contains("git") && t2.contains("history"));
fn name_overlap_discriminates_shape_coincidences() {
// The real-world false positives the gate must kill: same graph shape,
// unrelated names -> ~0 overlap.
assert!(name_overlap("BridgeRequest", "BootMemoryInput") < 0.15);
assert!(name_overlap("normalize_tool_name", "parse_git_history") < 0.15);
// Real near-duplicates the gate must keep: shared name tokens.
assert!(name_overlap("handler_a", "handler_b") >= 0.15);
assert!(name_overlap("parse_config", "parse_settings") >= 0.15);
// Identical names (true cross-file dup) -> 1.0.
assert!((name_overlap("validate", "validate") - 1.0).abs() < 0.001);
fn cosine_sim_orthogonal() {
let a = vec![1.0, 0.0];
let b = vec![0.0, 1.0];
assert!(cosine_similarity(&a, &b).abs() < 0.001);
fn cosine_sim_zero_vectors() {
let a = vec![0.0, 0.0, 0.0];
assert!(cosine_similarity(&a, &a).abs() < 0.001);
fn twins_found_in_symmetric_graph() {
let g = build_twin_graph();
let config = TwinConfig {
similarity_threshold: 0.85,
..TwinConfig::default()
let result = find_twins(&g, &config).unwrap();
assert!(result.nodes_analyzed == 6);
// handler_a ↔ handler_b, process_a ↔ process_b, output_a ↔ output_b
// should all be detected as structural twins
assert!(
!result.pairs.is_empty(),
"Should detect twin pairs, got none"
);
// Check that at least one handler pair is found
let has_handler_twin = result
.pairs
.any(|p| (p.node_a_label.contains("handler") && p.node_b_label.contains("handler")));
has_handler_twin,
"Should detect handler_a ↔ handler_b twin, pairs: {:?}",
result.pairs
fn hub_not_twin_with_leaf() {
let g = build_different_graph();
similarity_threshold: 0.90,
// Hub should NOT be a twin with any leaf
let has_hub_leaf_twin = result
.any(|p| (p.node_a_label == "dispatcher" || p.node_b_label == "dispatcher"));
!has_hub_leaf_twin,
"Hub should not be twin with leaf at 0.9 threshold"
fn leaves_are_twins_of_each_other() {
// All 3 leaves should be twins of each other
let leaf_pairs: Vec<_> = result
.filter(|p| {
p.node_a_label.starts_with("handler_") && p.node_b_label.starts_with("handler_")
leaf_pairs.len() >= 2,
"Should find at least 2 leaf twin pairs, found {}",
leaf_pairs.len()
fn empty_graph_returns_error() {
let g = Graph::new();
let config = TwinConfig::default();
assert!(find_twins(&g, &config).is_err());
fn scope_filter_limits_analysis() {
scope: Some("nonexistent_scope".to_string()),
result.pairs.is_empty(),
"Scoped to nonexistent prefix should find no twins"