graph: community isolation scoring + sort:isolation query
Add community_isolation() to Graph — computes per-community ratio of internal vs total edge weight. 1.0 = fully isolated, 0.0 = all edges external. New query: sort:isolation — sorts nodes by their community's isolation score, most isolated first. Useful for aiming organize agents at poorly-integrated knowledge clusters. New CLI: poc-memory graph communities [N] [--min-size M] — lists communities sorted by isolation with member preview. Reveals islands like the Shannon theory cluster (3 nodes, 100% isolated, 0 cross-edges) and large agent-journal clusters (20-30 nodes, 95% isolated). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0f126b709
commit
e6613f97bb
4 changed files with 141 additions and 0 deletions
|
|
@ -12,6 +12,16 @@ use crate::store::{Store, RelationType, StoreView};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
/// Community info for reporting
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommunityInfo {
|
||||
pub id: u32,
|
||||
pub members: Vec<String>,
|
||||
pub size: usize,
|
||||
pub isolation: f32,
|
||||
pub cross_edges: usize,
|
||||
}
|
||||
|
||||
/// Weighted edge in the graph
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Edge {
|
||||
|
|
@ -110,6 +120,75 @@ impl Graph {
|
|||
&self.communities
|
||||
}
|
||||
|
||||
/// Community isolation scores: for each community, what fraction of its
|
||||
/// total edge weight is internal (vs cross-community). Returns community_id → score
|
||||
/// where 1.0 = fully isolated (no external edges), 0.0 = all edges external.
|
||||
/// Singleton communities (1 node, no edges) get score 1.0.
|
||||
pub fn community_isolation(&self) -> HashMap<u32, f32> {
|
||||
// Accumulate internal and total edge weight per community
|
||||
let mut internal: HashMap<u32, f32> = HashMap::new();
|
||||
let mut total: HashMap<u32, f32> = HashMap::new();
|
||||
|
||||
for (key, edges) in &self.adj {
|
||||
let Some(&my_comm) = self.communities.get(key) else { continue };
|
||||
for edge in edges {
|
||||
let nbr_comm = self.communities.get(&edge.target).copied().unwrap_or(u32::MAX);
|
||||
*total.entry(my_comm).or_default() += edge.strength;
|
||||
if my_comm == nbr_comm {
|
||||
*internal.entry(my_comm).or_default() += edge.strength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut scores = HashMap::new();
|
||||
let all_communities: HashSet<u32> = self.communities.values().copied().collect();
|
||||
for &comm in &all_communities {
|
||||
let t = total.get(&comm).copied().unwrap_or(0.0);
|
||||
if t < 0.001 {
|
||||
scores.insert(comm, 1.0); // no edges = fully isolated
|
||||
} else {
|
||||
let i = internal.get(&comm).copied().unwrap_or(0.0);
|
||||
scores.insert(comm, i / t);
|
||||
}
|
||||
}
|
||||
scores
|
||||
}
|
||||
|
||||
/// Community info: id → (member keys, size, isolation score, cross-community edge count)
|
||||
pub fn community_info(&self) -> Vec<CommunityInfo> {
|
||||
let isolation = self.community_isolation();
|
||||
|
||||
// Group members by community
|
||||
let mut members: HashMap<u32, Vec<String>> = HashMap::new();
|
||||
for (key, &comm) in &self.communities {
|
||||
members.entry(comm).or_default().push(key.clone());
|
||||
}
|
||||
|
||||
// Count cross-community edges per community
|
||||
let mut cross_edges: HashMap<u32, usize> = HashMap::new();
|
||||
for (key, edges) in &self.adj {
|
||||
let Some(&my_comm) = self.communities.get(key) else { continue };
|
||||
for edge in edges {
|
||||
let nbr_comm = self.communities.get(&edge.target).copied().unwrap_or(u32::MAX);
|
||||
if my_comm != nbr_comm {
|
||||
*cross_edges.entry(my_comm).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result: Vec<CommunityInfo> = members.into_iter()
|
||||
.map(|(id, mut keys)| {
|
||||
keys.sort();
|
||||
let size = keys.len();
|
||||
let iso = isolation.get(&id).copied().unwrap_or(1.0);
|
||||
let cross = cross_edges.get(&id).copied().unwrap_or(0) / 2; // undirected
|
||||
CommunityInfo { id, members: keys, size, isolation: iso, cross_edges: cross }
|
||||
})
|
||||
.collect();
|
||||
result.sort_by(|a, b| b.isolation.total_cmp(&a.isolation));
|
||||
result
|
||||
}
|
||||
|
||||
/// Hub degree threshold: top 5% by degree
|
||||
pub fn hub_threshold(&self) -> usize {
|
||||
let mut degrees: Vec<usize> = self.keys.iter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue