use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Context, Result};
use crate::index::storage::ChunkStore;
#[derive(Debug, Clone)]
pub struct GodNode {
pub chunk_id: u64,
pub centrality: f64,
pub symbol: String,
pub file: String,
pub start_line: u32,
pub end_line: u32,
pub semantic_role: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Community {
pub label: String,
pub size: usize,
pub member_files: Vec<String>,
pub entry_points: Vec<EntryPoint>,
}
#[derive(Debug, Clone)]
pub struct EntryPoint {
pub symbol: String,
pub file: String,
pub start_line: u32,
pub end_line: u32,
}
#[derive(Debug, Clone)]
pub struct Boundary {
pub from: String,
pub to: String,
pub edge_count: u64,
}
#[derive(Debug, Clone, Default)]
pub struct ArchOverview {
pub god_nodes: Vec<GodNode>,
pub communities: Vec<Community>,
pub boundaries: Vec<Boundary>,
}
pub const ARCH_TINY_REPO_THRESHOLD: u64 = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArchBudget {
pub god_nodes: usize,
pub communities: usize,
pub boundaries: usize,
pub deep_examples_max: usize,
pub exhaustive_max: usize,
}
impl ArchBudget {
pub const MEDIUM: ArchBudget = ArchBudget {
god_nodes: 10,
communities: 5,
boundaries: 25,
deep_examples_max: 3,
exhaustive_max: 25,
};
}
pub fn budget_for_chunk_count(chunks: usize) -> ArchBudget {
if chunks <= 2_000 {
ArchBudget {
god_nodes: 5,
communities: 3,
boundaries: 10,
deep_examples_max: 1,
exhaustive_max: 12,
}
} else if chunks <= 10_000 {
ArchBudget {
god_nodes: 10,
communities: 5,
boundaries: 25,
deep_examples_max: 3,
exhaustive_max: 25,
}
} else {
ArchBudget {
god_nodes: 15,
communities: 8,
boundaries: 40,
deep_examples_max: 5,
exhaustive_max: 40,
}
}
}
pub fn query_top_centrality(db_path: &Path, n: usize) -> Result<Vec<(u64, f64)>> {
let conn =
rusqlite::Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("Failed to open {} read-only", db_path.display()))?;
let has_table: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='chunk_centrality'",
[],
|row| row.get::<_, i64>(0).map(|n| n != 0),
)
.unwrap_or(false);
if !has_table {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT chunk_id, structural_centrality FROM chunk_centrality \
ORDER BY structural_centrality DESC LIMIT ?1",
)?;
let rows = stmt.query_map([n as i64], |row| {
Ok((row.get::<_, i64>(0)? as u64, row.get::<_, f64>(1)?))
})?;
let mut out = Vec::with_capacity(n);
for r in rows {
out.push(r?);
}
Ok(out)
}
pub fn build_god_nodes(store: &ChunkStore, db_path: &Path, n: usize) -> Result<Vec<GodNode>> {
let raw = query_top_centrality(db_path, n)?;
let mut out = Vec::with_capacity(raw.len());
for (cid, centrality) in raw {
let Ok(chunk) = store.get_chunk(cid) else {
continue;
};
let symbol = chunk
.symbol_name()
.map_or_else(|| chunk.file_path.display().to_string(), str::to_string);
let semantic_role = store
.get_structured_meta(cid)
.ok()
.flatten()
.and_then(|m| m.semantic_role)
.map(|r| r.as_label().to_string());
out.push(GodNode {
chunk_id: cid,
centrality,
symbol,
file: chunk.file_path.display().to_string(),
start_line: chunk.start_line,
end_line: chunk.end_line,
semantic_role,
});
}
Ok(out)
}
pub fn build_communities(store: &ChunkStore, db_path: &Path) -> Result<Vec<Community>> {
build_communities_n(store, db_path, 5)
}
pub fn build_communities_n(
store: &ChunkStore,
db_path: &Path,
max_communities: usize,
) -> Result<Vec<Community>> {
if max_communities == 0 {
return Ok(Vec::new());
}
let seeds = match query_top_centrality(db_path, 200) {
Ok(s) if !s.is_empty() => s,
_ => return Ok(Vec::new()),
};
let seed_ids: Vec<u64> = seeds.iter().map(|(id, _)| *id).collect();
let call_out = store.get_call_edges_from(&seed_ids)?;
let call_in = store.get_call_edges_to(&seed_ids)?;
let mut adj: HashMap<u64, HashSet<u64>> = HashMap::new();
let add_edge = |a: u64, b: u64, adj: &mut HashMap<u64, HashSet<u64>>| {
adj.entry(a).or_default().insert(b);
adj.entry(b).or_default().insert(a);
};
for sid in &seed_ids {
adj.entry(*sid).or_default();
}
for (a, b) in &call_out {
add_edge(*a, *b, &mut adj);
}
for (a, b) in &call_in {
add_edge(*a, *b, &mut adj);
}
let mut visited: HashSet<u64> = HashSet::new();
let mut components: Vec<Vec<u64>> = Vec::new();
let mut nodes: Vec<u64> = adj.keys().copied().collect();
nodes.sort_unstable(); for node in nodes {
if visited.contains(&node) {
continue;
}
let mut component = Vec::new();
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if !visited.insert(n) {
continue;
}
component.push(n);
if let Some(neighbors) = adj.get(&n) {
let mut sorted: Vec<u64> = neighbors.iter().copied().collect();
sorted.sort_unstable();
for nb in sorted {
if !visited.contains(&nb) {
stack.push(nb);
}
}
}
}
components.push(component);
}
components.sort_by_key(|c| std::cmp::Reverse(c.len()));
components.truncate(max_communities);
let score_by_id: HashMap<u64, f64> = seeds.iter().copied().collect();
let mut out = Vec::with_capacity(components.len());
for (idx, component) in components.iter().enumerate() {
if component.is_empty() {
continue;
}
let mut member_files: Vec<String> = Vec::new();
let mut member_seen: HashSet<String> = HashSet::new();
let mut entry_points: Vec<EntryPoint> = Vec::new();
let mut sorted: Vec<u64> = component.clone();
sorted.sort_by(|a, b| {
score_by_id
.get(b)
.partial_cmp(&score_by_id.get(a))
.unwrap_or(std::cmp::Ordering::Equal)
});
for &cid in sorted.iter().take(component.len().min(20)) {
if let Ok(chunk) = store.get_chunk(cid) {
let p = chunk.file_path.display().to_string();
if member_seen.insert(p.clone()) {
member_files.push(p);
}
if entry_points.len() < 3 {
let name = chunk
.symbol_name()
.map_or_else(|| chunk.file_path.display().to_string(), str::to_string);
entry_points.push(EntryPoint {
symbol: name,
file: chunk.file_path.display().to_string(),
start_line: chunk.start_line,
end_line: chunk.end_line,
});
}
}
if member_files.len() >= 8 {
break;
}
}
out.push(Community {
label: format!("community-{}", idx + 1),
size: component.len(),
member_files,
entry_points,
});
}
Ok(out)
}
pub fn build_boundaries(db_path: &Path) -> Result<Vec<Boundary>> {
build_boundaries_n(db_path, 25)
}
pub fn build_boundaries_n(db_path: &Path, max_boundaries: usize) -> Result<Vec<Boundary>> {
if max_boundaries == 0 {
return Ok(Vec::new());
}
let conn =
rusqlite::Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("Failed to open {} read-only", db_path.display()))?;
let has_table: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='module_edges'",
[],
|row| row.get::<_, i64>(0).map(|n| n != 0),
)
.unwrap_or(false);
if !has_table {
return Ok(Vec::new());
}
let mut counts: HashMap<(String, String), u64> = HashMap::new();
{
let mut stmt = conn.prepare("SELECT importer_path, imported_path FROM module_edges")?;
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
for r in rows {
let (a, b) = r?;
let da = top_level_dir(&a);
let db = top_level_dir(&b);
if da == db {
continue;
}
*counts.entry((da, db)).or_insert(0) += 1;
}
}
let mut sorted: Vec<((String, String), u64)> = counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
sorted.truncate(max_boundaries);
Ok(sorted
.into_iter()
.map(|((from, to), edge_count)| Boundary {
from,
to,
edge_count,
})
.collect())
}
pub fn top_level_dir(p: &str) -> String {
Path::new(p)
.components()
.next()
.and_then(|c| c.as_os_str().to_str())
.unwrap_or("")
.to_string()
}
pub fn build_arch_overview(
store: &ChunkStore,
db_path: &Path,
budget: Option<ArchBudget>,
) -> Result<ArchOverview> {
let b = budget.unwrap_or(ArchBudget::MEDIUM);
Ok(ArchOverview {
god_nodes: build_god_nodes(store, db_path, b.god_nodes)?,
communities: build_communities_n(store, db_path, b.communities)?,
boundaries: build_boundaries_n(db_path, b.boundaries)?,
})
}
pub fn query_pattern_matches(
db_path: &Path,
pattern: &str,
language: Option<&str>,
max: usize,
) -> Result<Vec<(u64, String, String)>> {
let conn =
rusqlite::Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_context(|| format!("Failed to open {} read-only", db_path.display()))?;
let has_table: bool = conn
.query_row(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='pattern_matches'",
[],
|row| row.get::<_, i64>(0).map(|n| n != 0),
)
.unwrap_or(false);
if !has_table {
return Ok(Vec::new());
}
let mut out: Vec<(u64, String, String)> = Vec::new();
if let Some(lang) = language {
let mut stmt = conn.prepare(
"SELECT chunk_id, pattern_name, language FROM pattern_matches \
WHERE pattern_name = ?1 AND language = ?2 LIMIT ?3",
)?;
let rows = stmt.query_map(rusqlite::params![pattern, lang, max as i64], |row| {
Ok((
row.get::<_, i64>(0)? as u64,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
for r in rows {
out.push(r?);
}
} else {
let mut stmt = conn.prepare(
"SELECT chunk_id, pattern_name, language FROM pattern_matches \
WHERE pattern_name = ?1 LIMIT ?2",
)?;
let rows = stmt.query_map(rusqlite::params![pattern, max as i64], |row| {
Ok((
row.get::<_, i64>(0)? as u64,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
for r in rows {
out.push(r?);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_level_dir_extracts_first_component() {
assert_eq!(top_level_dir("crates/semantex-mcp/src/server.rs"), "crates");
assert_eq!(top_level_dir("src/main.rs"), "src");
assert_eq!(top_level_dir("Cargo.toml"), "Cargo.toml");
assert_eq!(top_level_dir(""), "");
}
#[test]
fn query_top_centrality_handles_missing_table() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let result = query_top_centrality(tmp.path(), 10).unwrap();
assert!(result.is_empty());
}
#[test]
fn build_boundaries_handles_missing_table() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let result = build_boundaries(tmp.path()).unwrap();
assert!(result.is_empty());
}
#[test]
fn query_pattern_matches_handles_missing_table() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let result = query_pattern_matches(tmp.path(), "rust.drop_impl", None, 3).unwrap();
assert!(result.is_empty());
}
#[test]
fn budget_small_tier_for_500_chunks() {
let b = budget_for_chunk_count(500);
assert_eq!(
b,
ArchBudget {
god_nodes: 5,
communities: 3,
boundaries: 10,
deep_examples_max: 1,
exhaustive_max: 12,
}
);
}
#[test]
fn budget_small_tier_for_zero_chunks() {
let b = budget_for_chunk_count(0);
assert_eq!(b.god_nodes, 5);
assert_eq!(b.deep_examples_max, 1);
}
#[test]
fn budget_small_tier_upper_bound() {
let b = budget_for_chunk_count(2_000);
assert_eq!(b.god_nodes, 5);
assert_eq!(b.exhaustive_max, 12);
}
#[test]
fn budget_medium_tier_lower_bound() {
let b = budget_for_chunk_count(2_001);
assert_eq!(b.god_nodes, 10);
assert_eq!(b.exhaustive_max, 25);
}
#[test]
fn budget_medium_tier_upper_bound() {
let b = budget_for_chunk_count(10_000);
assert_eq!(b.god_nodes, 10);
assert_eq!(b.boundaries, 25);
}
#[test]
fn budget_large_tier_for_15k_chunks() {
let b = budget_for_chunk_count(15_000);
assert_eq!(
b,
ArchBudget {
god_nodes: 15,
communities: 8,
boundaries: 40,
deep_examples_max: 5,
exhaustive_max: 40,
}
);
}
#[test]
fn arch_budget_medium_matches_phase4_defaults() {
assert_eq!(ArchBudget::MEDIUM.god_nodes, 10);
assert_eq!(ArchBudget::MEDIUM.communities, 5);
assert_eq!(ArchBudget::MEDIUM.boundaries, 25);
}
#[test]
fn exhaustive_max_replaces_hardcoded_30() {
assert_eq!(budget_for_chunk_count(1_000).exhaustive_max, 12);
assert_eq!(budget_for_chunk_count(5_000).exhaustive_max, 25);
assert_eq!(budget_for_chunk_count(15_000).exhaustive_max, 40);
assert_ne!(budget_for_chunk_count(1_000).exhaustive_max, 30);
assert_ne!(budget_for_chunk_count(5_000).exhaustive_max, 30);
assert_ne!(budget_for_chunk_count(15_000).exhaustive_max, 30);
}
#[test]
fn build_communities_n_respects_cap() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let result = query_top_centrality(tmp.path(), 5).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn build_communities_n_zero_cap_returns_empty() {
let tmp = tempfile::TempDir::new().unwrap();
let db_path = tmp.path().join("chunks.db");
let store = ChunkStore::open(&db_path).unwrap();
let result = build_communities_n(&store, &db_path, 0).unwrap();
assert!(result.is_empty());
}
#[test]
fn build_boundaries_n_zero_cap_returns_empty() {
let tmp = tempfile::TempDir::new().unwrap();
let db_path = tmp.path().join("chunks.db");
let _ = ChunkStore::open(&db_path).unwrap();
let result = build_boundaries_n(&db_path, 0).unwrap();
assert!(result.is_empty());
}
#[test]
fn build_arch_overview_with_tiny_budget_skips_sections() {
let tmp = tempfile::TempDir::new().unwrap();
let db_path = tmp.path().join("chunks.db");
let store = ChunkStore::open(&db_path).unwrap();
let tiny_budget = ArchBudget {
god_nodes: 5,
communities: 0,
boundaries: 0,
deep_examples_max: 1,
exhaustive_max: 12,
};
let overview = build_arch_overview(&store, &db_path, Some(tiny_budget)).unwrap();
assert!(
overview.communities.is_empty(),
"tiny-repo budget must produce zero communities"
);
assert!(
overview.boundaries.is_empty(),
"tiny-repo budget must produce zero boundaries"
);
assert!(overview.god_nodes.len() <= 5);
}
#[test]
fn budget_for_100_chunks_falls_in_small_tier() {
let b = budget_for_chunk_count(100);
assert_eq!(b.god_nodes, 5);
assert_eq!(b.communities, 3);
assert_eq!(b.boundaries, 10);
}
#[test]
fn build_god_nodes_reads_symbol_from_chunk_not_structured_meta() {
use crate::chunking::structured_meta::StructuredChunkMeta;
use crate::types::{AstNodeKind, Chunk, ChunkType};
let tmp = tempfile::TempDir::new().unwrap();
let db_path = tmp.path().join("chunks.db");
let store = ChunkStore::open(&db_path).unwrap();
let mut meta = StructuredChunkMeta {
name: Some("WrongName".to_string()),
kind: Some("function".to_string()),
..StructuredChunkMeta::default()
};
meta.generate_nl_summary();
let chunk = Chunk {
id: 0,
file_path: std::path::PathBuf::from("src/lib.rs"),
start_line: 1,
end_line: 5,
content: "fn FooBar() {}".to_string(),
chunk_type: ChunkType::AstNode {
name: "FooBar".to_string(),
kind: AstNodeKind::Function,
language: "rust".to_string(),
structured_meta: Some(Box::new(meta)),
},
};
let chunk_id = store.insert_chunk(&chunk, 0xfeed_face, 0).unwrap();
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS chunk_centrality (
chunk_id INTEGER PRIMARY KEY,
structural_centrality REAL NOT NULL
);",
)
.unwrap();
conn.execute(
"INSERT INTO chunk_centrality(chunk_id, structural_centrality) VALUES (?1, 0.9)",
rusqlite::params![chunk_id as i64],
)
.unwrap();
drop(conn);
let god_nodes = build_god_nodes(&store, &db_path, 5).unwrap();
assert_eq!(god_nodes.len(), 1, "should have one god node");
assert_eq!(
god_nodes[0].symbol, "FooBar",
"build_god_nodes must read symbol via Chunk::symbol_name(), \
got {:?} (expected `FooBar` from chunk.chunk_type.AstNode.name)",
god_nodes[0].symbol
);
assert_ne!(
god_nodes[0].symbol, "WrongName",
"build_god_nodes leaked the structured_meta.name override — \
readers must use Chunk::symbol_name() as the writer-side truth"
);
}
#[test]
fn build_god_nodes_falls_back_to_file_path_for_text_window_chunks() {
use crate::types::{Chunk, ChunkType};
let tmp = tempfile::TempDir::new().unwrap();
let db_path = tmp.path().join("chunks.db");
let store = ChunkStore::open(&db_path).unwrap();
let chunk = Chunk {
id: 0,
file_path: std::path::PathBuf::from("docs/notes.md"),
start_line: 1,
end_line: 20,
content: "long-form documentation".to_string(),
chunk_type: ChunkType::TextWindow { window_index: 0 },
};
let chunk_id = store.insert_chunk(&chunk, 0xc0de_d00d, 0).unwrap();
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS chunk_centrality (
chunk_id INTEGER PRIMARY KEY,
structural_centrality REAL NOT NULL
);",
)
.unwrap();
conn.execute(
"INSERT INTO chunk_centrality(chunk_id, structural_centrality) VALUES (?1, 0.5)",
rusqlite::params![chunk_id as i64],
)
.unwrap();
drop(conn);
let god_nodes = build_god_nodes(&store, &db_path, 5).unwrap();
assert_eq!(god_nodes.len(), 1);
assert_eq!(
god_nodes[0].symbol, "docs/notes.md",
"non-AstNode chunks must fall back to the file path"
);
}
}