use std::collections::{BTreeMap, BTreeSet, HashMap};
use serde::Serialize;
use super::{EdgeType, Graph, NodeLayer};
pub const CLUSTER_ORDER: [&str; 10] = [
"Loop Core",
"Reasoning",
"Action",
"Cognition",
"Safety & Verify",
"Evolution",
"Interface",
"Observability",
"Eval / Bench",
"Foundation",
];
pub fn taxonomy_outline(graph: &Graph) -> String {
let mut by_cluster: BTreeMap<&str, BTreeSet<String>> = BTreeMap::new();
for node in graph.nodes.iter().filter(|n| n.layer == NodeLayer::Code) {
let component = component_of(&node.id);
let cluster = cluster_of(&component);
by_cluster.entry(cluster).or_default().insert(component);
}
let mut out = String::from(
"# Architectural taxonomy — 10 clusters (perceive -> reason -> act -> verify -> learn)\n",
);
for cluster in CLUSTER_ORDER {
let Some(members) = by_cluster.get(cluster) else {
continue;
};
if members.is_empty() {
continue;
}
out.push_str(&format!(
"## {} — {}\n",
cluster,
members.iter().cloned().collect::<Vec<_>>().join(", ")
));
}
out.trim_end().to_string()
}
pub fn cluster_of(component: &str) -> &'static str {
match component {
"agent" | "orchestration" | "cli" | "input" | "interview" => "Loop Core",
"api" | "tokens" | "token_count" | "tool_parser" | "llm_doctor" => "Reasoning",
"tools" | "computer" | "mcp" | "lsp" => "Action",
"cognitive" | "memory" | "consolidation" | "session" => "Cognition",
"safety" | "testing" | "self_healing" | "supervision" => "Safety & Verify",
"evolve" | "evolution" | "swl" | "analysis" => "Evolution",
"ui" | "output" | "templates" => "Interface",
"observability" | "doctor" | "devops" => "Observability",
"bench_harness" | "vlm_bench" | "test_support" | "bin" => "Eval / Bench",
_ => "Foundation",
}
}
pub fn component_of(node_id: &str) -> String {
let s = node_id.strip_prefix("crate::").unwrap_or(node_id);
s.split("::").next().unwrap_or(s).to_string()
}
#[derive(Debug, Clone, Serialize)]
pub struct ClusterNode {
pub id: String,
pub layer: String,
pub tokens: usize,
pub lines: usize,
pub files: usize,
pub member_count: usize,
pub members: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ClusterEdge {
pub from: String,
pub to: String,
pub weight: usize,
pub edge_type: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ClusteredGraph {
pub nodes: Vec<ClusterNode>,
pub edges: Vec<ClusterEdge>,
}
#[derive(Default)]
struct Agg {
tokens: usize,
lines: usize,
files: usize,
components: BTreeSet<String>,
}
pub fn clustered(graph: &Graph) -> ClusteredGraph {
let mut aggs: BTreeMap<&'static str, Agg> = BTreeMap::new();
let mut node_cluster: HashMap<&str, &'static str> = HashMap::new();
for node in &graph.nodes {
if node.layer != NodeLayer::Code {
continue;
}
let component = component_of(&node.id);
let cluster = cluster_of(&component);
node_cluster.insert(node.id.as_str(), cluster);
let agg = aggs.entry(cluster).or_default();
agg.tokens += node.tokens;
agg.lines += node.lines;
agg.files += node.files;
agg.components.insert(component);
}
let mut weights: BTreeMap<(&'static str, &'static str), usize> = BTreeMap::new();
for edge in &graph.edges {
if !matches!(edge.edge_type, EdgeType::DependsOn) {
continue;
}
let (Some(&fc), Some(&tc)) = (
node_cluster.get(edge.from.as_str()),
node_cluster.get(edge.to.as_str()),
) else {
continue;
};
if fc != tc {
*weights.entry((fc, tc)).or_default() += 1;
}
}
let nodes = aggs
.into_iter()
.map(|(cluster, agg)| ClusterNode {
id: cluster.to_string(),
layer: "cluster".to_string(),
tokens: agg.tokens,
lines: agg.lines,
files: agg.files,
member_count: agg.components.len(),
members: agg.components.into_iter().collect(),
})
.collect();
let edges = weights
.into_iter()
.map(|((from, to), weight)| ClusterEdge {
from: from.to_string(),
to: to.to_string(),
weight,
edge_type: "DependsOn".to_string(),
})
.collect();
ClusteredGraph { nodes, edges }
}