use crate::{RealityGraph, Result};
use scc_core::kinds;
use scc_core::{entity_id, Provenance, Relationship};
use scc_store::Store;
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
pub const RELPREFIX: &str = "rel:comp:";
pub const BOUNDARY_DECLARED: &str = "declared";
pub const BOUNDARY_PACKAGE: &str = "package";
pub const BOUNDARY_DEPLOYMENT: &str = "deployment";
pub const BOUNDARY_CLI: &str = "cli";
pub const BOUNDARY_CODE_REGION: &str = "code-region";
pub const BOUNDARY_ROOT: &str = "root";
pub const LAYER_CODE_REGION: &str = "code_region";
pub const LAYER_COMPONENT: &str = "component";
pub const LAYER_SUBSYSTEM: &str = "subsystem";
pub const LAYER_SERVICE: &str = "service";
pub const MERGE_THRESHOLD: i32 = 6;
pub const SERVICE_THRESHOLD: i32 = 12;
pub(crate) fn boundary_rank(kind: &str) -> u8 {
match kind {
BOUNDARY_DECLARED => 3,
BOUNDARY_DEPLOYMENT => 2,
BOUNDARY_CLI => 2,
BOUNDARY_PACKAGE => 1,
_ => 0,
}
}
pub fn rel(parts: &[&str]) -> String {
let mut h = blake3::Hasher::new();
for p in parts {
h.update(p.as_bytes());
h.update(b"|");
}
format!("{RELPREFIX}{}", &h.finalize().to_hex()[..12])
}
pub fn parse_prov(s: &str) -> Provenance {
match s {
"RESOLVED" => Provenance::Resolved,
"EXTRACTED" => Provenance::Extracted,
"DECLARED" => Provenance::Declared,
"OBSERVED" => Provenance::Observed,
"INFERRED" => Provenance::Inferred,
_ => Provenance::Inferred,
}
}
pub fn prov_rank(p: Provenance) -> u8 {
match p {
Provenance::Resolved => 4,
Provenance::Observed => 4,
Provenance::Extracted => 3,
Provenance::Declared => 2,
Provenance::Inferred => 1,
Provenance::Stale => 0,
}
}
pub fn path_role(path: &str) -> Option<&'static str> {
for (depth, seg) in path.split('/').enumerate() {
match seg.to_ascii_lowercase().as_str() {
"tests" | "test" | "testing" | "__tests__" | "spec" | "specs" | "e2e" => {
return Some("test")
}
"fixtures" | "fixture" | "testdata" | "test-data" => return Some("fixture"),
"benchmarks" | "benchmark" | "benches" | "bench" => return Some("benchmark"),
"examples" | "example" | "samples" | "demo" => {
if depth <= 1 {
return Some("example");
}
}
"docs" | "doc" => return Some("docs"),
"config" | "conf" | "configuration" | "workflows" => return Some("config"),
"generated" | "__generated__" => return Some("generated"),
"vendor" | "third_party" | "third-party" | "node_modules" => return Some("vendor"),
"target" | "dist" | "build" | "out" | "coverage" => return Some("build"),
"tools" | "tooling" | "devtools" => return Some("tooling"),
"sdk" => return Some("sdk"),
_ => {}
}
}
None
}
pub fn is_production_path(path: &str) -> bool {
path_role(path).is_none()
}
pub fn component_role(paths: &[String]) -> &'static str {
let mut roles = std::collections::BTreeSet::new();
for p in paths {
if let Some(r) = path_role(p) {
roles.insert(r);
}
}
if roles.is_empty() {
return "production";
}
let all_covered = paths.iter().all(|p| path_role(p).is_some());
if roles.len() == 1 && all_covered {
return roles.into_iter().next().unwrap_or("production");
}
"mixed"
}
#[derive(Debug, Clone)]
pub struct ComponentCandidate {
pub name: String,
pub dirs: Vec<String>,
pub boundary_kind: String,
pub intent: Option<String>,
}
impl ComponentCandidate {
pub fn new(name: impl Into<String>, dirs: Vec<String>, boundary_kind: impl Into<String>) -> Self {
ComponentCandidate {
name: name.into(),
dirs,
boundary_kind: boundary_kind.into(),
intent: None,
}
}
}
pub fn component_for_path(path: &str, candidates: &[ComponentCandidate]) -> String {
let mut best: Option<(String, usize)> = None;
for c in candidates {
for d in &c.dirs {
let d = d.trim_end_matches('/');
if d.is_empty() {
continue;
}
if path == d || path.starts_with(&format!("{d}/")) {
let len = d.len();
if best.as_ref().map(|(_, bl)| len > *bl).unwrap_or(true) {
best = Some((c.name.clone(), len));
}
}
}
}
match best {
Some((name, _)) => name,
None => {
if path.contains('/') {
let seg = path.split('/').next().unwrap_or("root");
if seg == ".scc" {
"root".to_string()
} else {
seg.to_string()
}
} else {
"root".to_string()
}
}
}
}
fn norm_manifest_dir(p: &str) -> String {
let t = p.trim();
let t = t.strip_prefix("./").unwrap_or(t);
t.trim_end_matches('/').to_string()
}
fn toml_string_array(table: &str, key: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut rest = table;
while let Some(pos) = rest.find(key) {
let before_ok = pos == 0
|| !rest[..pos]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);
let after_trim = rest[pos + key.len()..].trim_start();
if before_ok && after_trim.starts_with('=') {
let mut scan = &after_trim[1..];
loop {
let open = scan.find('"');
let close = scan.find(']');
match (open, close) {
(Some(q), Some(c)) if q < c => match scan[q + 1..].find('"') {
Some(e) => {
out.push(scan[q + 1..q + 1 + e].to_string());
scan = &scan[q + 1 + e + 1..];
}
None => break,
},
_ => break, }
}
break;
}
rest = &rest[pos + key.len()..];
}
out
}
fn toml_table(text: &str, header: &str) -> Option<String> {
let mut start: Option<usize> = None;
let mut end = text.lines().count();
for (i, line) in text.lines().enumerate() {
let l = line.trim();
if start.is_none() {
if l == header {
start = Some(i);
}
continue;
}
if l.starts_with('[') && l.ends_with(']') {
end = i;
break;
}
}
let start = start?;
Some(
text.lines()
.skip(start)
.take(end.saturating_sub(start))
.collect::<Vec<_>>()
.join("\n"),
)
}
fn toml_package_name(text: &str) -> Option<String> {
let table = toml_table(text, "[package]")?;
for line in table.lines() {
let l = line.trim();
if let Some(eq) = l.find('=') {
if l[..eq].trim() == "name" {
let v = l[eq + 1..].trim();
if let Some(s) = v.strip_prefix('"') {
if let Some(e) = s.find('"') {
return Some(s[..e].to_string());
}
}
}
}
}
None
}
fn gowork_use_dirs(text: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let lines: Vec<&str> = text.lines().map(str::trim).collect();
let mut i = 0;
while i < lines.len() {
let toks: Vec<&str> = lines[i].split_whitespace().collect();
if toks.first() == Some(&"use") {
if toks.len() >= 2 && toks[1].starts_with('(') {
for t in &toks[1..] {
let t = t.trim_matches(|c| c == '(' || c == ')');
if !t.is_empty() {
out.push(norm_manifest_dir(t));
}
}
i += 1;
while i < lines.len() && lines[i] != ")" {
let l = lines[i];
if !l.is_empty() && !l.starts_with("//") {
out.push(norm_manifest_dir(l));
}
i += 1;
}
} else if toks.len() >= 2 {
out.push(norm_manifest_dir(toks[1]));
} else {
i += 1;
while i < lines.len() && lines[i] != ")" {
let l = lines[i];
if !l.is_empty() && !l.starts_with("//") {
out.push(norm_manifest_dir(l));
}
i += 1;
}
}
}
i += 1;
}
out
}
fn crate_name(root: &std::path::Path, dir: &str) -> String {
if let Ok(text) = std::fs::read_to_string(root.join(dir).join("Cargo.toml")) {
if let Some(n) = toml_package_name(&text) {
if !n.is_empty() {
return n;
}
}
}
dir.rsplit('/').next().unwrap_or(dir).to_string()
}
fn cargo_workspace_members(root: &std::path::Path) -> Vec<(String, String)> {
let text = match std::fs::read_to_string(root.join("Cargo.toml")) {
Ok(t) => t,
Err(_) => return Vec::new(),
};
let Some(ws) = toml_table(&text, "[workspace]") else {
return Vec::new();
};
let members = toml_string_array(&ws, "members");
if members.is_empty() {
return Vec::new();
}
let excludes: BTreeSet<String> =
toml_string_array(&ws, "exclude").into_iter().map(|e| norm_manifest_dir(&e)).collect();
let mut out: Vec<(String, String)> = Vec::new();
for m in members {
let m = norm_manifest_dir(&m);
if m.is_empty() || m == "." || excludes.contains(&m) {
continue;
}
if let Some(glob_dir) = m.strip_suffix("/*") {
let base = root.join(glob_dir);
let mut dirs: Vec<String> = std::fs::read_dir(&base)
.map(|rd| {
rd.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| !n.starts_with('.'))
.filter(|n| {
root.join(format!("{glob_dir}/{n}")).join("Cargo.toml").is_file()
})
.collect()
})
.unwrap_or_default();
dirs.sort();
for d in dirs {
let dir = format!("{glob_dir}/{d}");
if excludes.contains(&dir) {
continue;
}
out.push((crate_name(root, &dir), dir));
}
} else if root.join(&m).is_dir() {
out.push((crate_name(root, &m), m));
}
}
out
}
fn merge_manifest_candidate(out: &mut Vec<ComponentCandidate>, name: String, dir: String) {
if let Some(c) = out.iter_mut().find(|c| c.name == name) {
if !c.dirs.contains(&dir) {
c.dirs.push(dir);
}
if boundary_rank(BOUNDARY_PACKAGE) > boundary_rank(&c.boundary_kind) {
c.boundary_kind = BOUNDARY_PACKAGE.to_string();
}
} else {
out.push(ComponentCandidate {
name,
dirs: vec![dir],
boundary_kind: BOUNDARY_PACKAGE.to_string(), intent: None,
});
}
}
fn manifest_package_candidates(root: &std::path::Path) -> Vec<ComponentCandidate> {
let mut out: Vec<ComponentCandidate> = Vec::new();
for (name, dir) in cargo_workspace_members(root) {
merge_manifest_candidate(&mut out, name, dir);
}
for dir in gowork_use_dirs(&std::fs::read_to_string(root.join("go.work")).unwrap_or_default()) {
if dir.is_empty() || dir == "." || !root.join(&dir).is_dir() {
continue; }
let name = dir.rsplit('/').next().unwrap_or(&dir).to_string();
merge_manifest_candidate(&mut out, name, dir);
}
out
}
fn cli_package_dirs(graph: &RealityGraph) -> BTreeSet<String> {
let mut dirs: BTreeSet<String> = BTreeSet::new();
let note_file = |fname: &str, dirs: &mut BTreeSet<String>| {
let dir = match fname.rsplit_once('/') {
Some((parent, _)) if !parent.is_empty() => parent.to_string(),
_ => "root".to_string(),
};
dirs.insert(dir);
};
for f in graph.entities_of_kind(kinds::FILE) {
let has_cli = f
.attributes
.get("entrypoints")
.and_then(|v| v.as_array())
.map(|eps| eps.iter().any(|e| e.as_str() == Some("cli-subcommand")))
.unwrap_or(false);
if has_cli {
note_file(&f.name, &mut dirs);
}
}
for e in graph.entities_of_kind(kinds::SYMBOL) {
let is_cli = e
.attributes
.get("entrypoints")
.and_then(|v| v.as_array())
.map(|eps| eps.iter().any(|e| e.as_str() == Some("cli-subcommand")))
.unwrap_or(false)
|| e
.attributes
.get("cli_flags")
.and_then(|v| v.as_array())
.map(|fl| !fl.is_empty())
.unwrap_or(false);
if is_cli {
if let Some(file) = e.attributes.get("file").and_then(|v| v.as_str()) {
note_file(file, &mut dirs);
}
}
}
dirs
}
pub(crate) fn build_candidates(
graph: &RealityGraph,
store: &Store,
intent: &[(String, serde_json::Value)],
) -> Vec<ComponentCandidate> {
let mut candidates: Vec<ComponentCandidate> = Vec::new();
let mut declared_names: HashSet<String> = HashSet::new();
for (source, claim) in intent {
if source == "component" {
let name = claim["name"].as_str().unwrap_or("").to_string();
if name.is_empty() {
continue;
}
declared_names.insert(name.clone());
let mut dirs: Vec<String> = Vec::new();
if let Some(paths) = claim["paths"].as_array() {
for p in paths {
if let Some(s) = p.as_str() {
dirs.push(s.to_string());
}
}
}
dirs.push(name.clone()); candidates.push(ComponentCandidate {
name: name.clone(),
dirs,
boundary_kind: BOUNDARY_DECLARED.to_string(),
intent: Some(name),
});
}
}
for pkg in graph.entities_of_kind(kinds::PACKAGE) {
if let Some(path) = pkg.attributes.get("path").and_then(|v| v.as_str()) {
let name = pkg.name.clone();
if let Some(c) = candidates.iter_mut().find(|c| c.name == name) {
if !c.dirs.contains(&path.to_string()) {
c.dirs.push(path.to_string());
}
if boundary_rank(BOUNDARY_PACKAGE) > boundary_rank(&c.boundary_kind) {
c.boundary_kind = BOUNDARY_PACKAGE.to_string();
}
} else {
candidates.push(ComponentCandidate {
name,
dirs: vec![path.to_string()],
boundary_kind: BOUNDARY_PACKAGE.to_string(), intent: None,
});
}
}
}
for cand in manifest_package_candidates(&store.root) {
for dir in cand.dirs {
merge_manifest_candidate(&mut candidates, cand.name.clone(), dir);
}
}
for du in graph.entities_of_kind(kinds::DEPLOYMENT_UNIT) {
if let Some(ctx) = du.attributes.get("build_context").and_then(|v| v.as_str()) {
if ctx == "." || ctx == "./" {
continue;
}
let ctx = ctx.trim_start_matches("./");
let name = du.name.clone();
if let Some(c) = candidates.iter_mut().find(|c| c.name == name) {
if !c.dirs.contains(&ctx.to_string()) {
c.dirs.push(ctx.to_string());
}
if boundary_rank(BOUNDARY_DEPLOYMENT) > boundary_rank(&c.boundary_kind) {
c.boundary_kind = BOUNDARY_DEPLOYMENT.to_string();
}
} else {
candidates.push(ComponentCandidate {
name,
dirs: vec![ctx.to_string()],
boundary_kind: BOUNDARY_DEPLOYMENT.to_string(), intent: None,
});
}
}
}
for dir in cli_package_dirs(graph) {
if let Some(c) = candidates.iter_mut().find(|c| c.name == dir) {
if !c.dirs.contains(&dir) {
c.dirs.push(dir);
}
if boundary_rank(BOUNDARY_CLI) > boundary_rank(&c.boundary_kind) {
c.boundary_kind = BOUNDARY_CLI.to_string();
}
} else {
candidates.push(ComponentCandidate {
name: dir.clone(),
dirs: vec![dir.clone()],
boundary_kind: BOUNDARY_CLI.to_string(), intent: None,
});
}
}
let mut top_dirs: HashSet<String> = HashSet::new();
for f in graph.entities_of_kind(kinds::FILE) {
if f.name.contains('/') {
if let Some(seg) = f.name.split('/').next() {
if !seg.is_empty() {
top_dirs.insert(seg.to_string());
}
}
}
}
top_dirs.insert("root".to_string());
for d in &top_dirs {
if !candidates.iter().any(|c| c.name == *d) {
candidates.push(ComponentCandidate {
name: d.clone(),
dirs: vec![d.clone()],
boundary_kind: if d == "root" {
BOUNDARY_ROOT.to_string()
} else {
BOUNDARY_CODE_REGION.to_string()
}, intent: None,
});
}
}
candidates
}
pub fn compile_components(
graph: &RealityGraph,
store: &Store,
intent: &[(String, serde_json::Value)],
pairs: &[crate::cochange::CochangePair],
) -> Result<Vec<scc_core::Entity>> {
let repo_id = &store.repo_id;
let candidates = build_candidates(graph, store, intent);
let du_ctxs: Vec<(String, String)> = graph
.entities_of_kind(kinds::DEPLOYMENT_UNIT)
.into_iter()
.filter_map(|du| {
let ctx = du.attributes.get("build_context").and_then(|v| v.as_str())?;
if ctx == "." || ctx == "./" {
return None;
}
Some((du.name.clone(), ctx.trim_start_matches("./").to_string()))
})
.collect();
let mut du_ctxs_sorted = du_ctxs;
du_ctxs_sorted.sort_by(|a, b| b.1.len().cmp(&a.1.len()).then_with(|| a.0.cmp(&b.0)));
let clustering = crate::clustering::cluster_components(
graph,
store,
intent,
&candidates,
pairs,
&du_ctxs_sorted,
)?;
let files_in_component: BTreeMap<String, Vec<String>> = clustering.files_in_component;
let symbol_component: HashMap<String, String> = clustering.symbol_component;
let parent_per_comp: BTreeMap<String, String> = clustering.parent_per_comp;
let mut responsibilities: BTreeMap<String, Vec<(String, Provenance, f64)>> = BTreeMap::new();
type OwnershipClaim = (String, Provenance, f64, Vec<String>);
let mut owns: BTreeMap<String, Vec<OwnershipClaim>> = BTreeMap::new();
let mut depends: BTreeMap<String, Vec<(String, Provenance, f64, u32)>> = BTreeMap::new();
let mut symbols_per_comp: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut evidence_per_comp: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut retries_per_comp: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (sym_id, comp) in &symbol_component {
for r in graph.out_pred(sym_id, scc_core::predicates::HANDLES) {
if let Some(route) = graph.entities.get(&r.object) {
let method = route.attributes.get("method").and_then(|v| v.as_str()).unwrap_or("");
let path = route.attributes.get("path").and_then(|v| v.as_str()).unwrap_or("");
responsibilities.entry(comp.clone()).or_default().push((
format!("Handles {method} {path}"),
Provenance::Resolved,
1.0,
));
}
}
}
for (sym_id, comp) in &symbol_component {
for r in graph.out_pred(sym_id, scc_core::predicates::WRITES) {
owns.entry(comp.clone()).or_default().push((
r.object.clone(),
r.provenance,
r.confidence,
r.evidence.clone(),
));
}
}
for (sym_id, comp) in &symbol_component {
for r in graph.out_pred(sym_id, scc_core::predicates::CALLS) {
if let Some(target_comp) = symbol_component.get(&r.object) {
if target_comp != comp {
let entry = depends.entry(comp.clone()).or_default();
if let Some((_, p, c, n)) = entry
.iter_mut()
.find(|(t, _, _, _)| t == target_comp)
{
*n += 1;
if prov_rank(r.provenance) > prov_rank(*p) {
*p = r.provenance;
}
*c = c.max(r.confidence);
} else {
entry.push((target_comp.clone(), r.provenance, r.confidence, 1));
}
}
}
}
}
for v in owns.values_mut() {
v.sort_by(|a, b| a.0.cmp(&b.0));
}
for v in depends.values_mut() {
v.sort_by(|a, b| a.0.cmp(&b.0));
}
for (sym_id, comp) in &symbol_component {
if let Some(e) = graph.entities.get(sym_id) {
symbols_per_comp
.entry(comp.clone())
.or_default()
.push(e.name.clone());
evidence_per_comp
.entry(comp.clone())
.or_default()
.extend(e.evidence.clone());
if let Some(rp) = e.attributes.get("retry_policy").and_then(|v| v.as_str()) {
retries_per_comp
.entry(comp.clone())
.or_default()
.push(format!("{} ({rp})", e.name));
}
}
}
for v in symbols_per_comp.values_mut() {
v.sort();
v.dedup();
}
for v in evidence_per_comp.values_mut() {
v.sort();
v.dedup();
}
for v in retries_per_comp.values_mut() {
v.sort();
v.dedup();
}
let mut symbol_list: Vec<(String, String)> = symbol_component
.iter()
.map(|(s, c)| (s.clone(), c.clone()))
.collect();
symbol_list.sort();
let mut shared_writes: BTreeMap<(String, String), HashSet<String>> = BTreeMap::new();
let mut entrypoints: BTreeMap<String, usize> = BTreeMap::new();
let mut events: BTreeMap<String, usize> = BTreeMap::new();
let mut calls: BTreeMap<String, (usize, usize)> = BTreeMap::new();
for (sym_id, comp) in &symbol_list {
for r in graph.out_pred(sym_id, scc_core::predicates::WRITES) {
let target = if r.object.contains("/data/") {
graph
.entities
.get(&r.object)
.and_then(|e| e.attributes.get("store"))
.and_then(|v| v.as_str())
.map(|s| entity_id(repo_id, kinds::DATA_STORE, s))
.unwrap_or_else(|| r.object.clone())
} else {
r.object.clone()
};
shared_writes
.entry((comp.clone(), target))
.or_default()
.insert(sym_id.clone());
}
for _r in graph.out_pred(sym_id, scc_core::predicates::HANDLES) {
*entrypoints.entry(comp.clone()).or_insert(0) += 1;
}
for _r in graph
.out_pred(sym_id, scc_core::predicates::PUBLISHES)
.into_iter()
.chain(graph.out_pred(sym_id, scc_core::predicates::CONSUMES))
{
*events.entry(comp.clone()).or_insert(0) += 1;
}
for r in graph.out_pred(sym_id, scc_core::predicates::CALLS) {
let e = calls.entry(comp.clone()).or_insert((0, 0));
e.1 += 1;
if symbol_component.get(&r.object) == Some(comp) {
e.0 += 1;
}
}
}
let mut route_entities: BTreeMap<String, usize> = BTreeMap::new();
for (comp, files) in &files_in_component {
for fid in files {
for r in graph.out_pred(fid, scc_core::predicates::CONTAINS) {
if graph
.entities
.get(&r.object)
.map(|e| e.kind == kinds::ROUTE)
.unwrap_or(false)
{
*route_entities.entry(comp.clone()).or_insert(0) += 1;
}
}
}
}
let mut intent_resp: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut intent_owns: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (source, claim) in intent {
if source == "component" {
let name = claim["name"].as_str().unwrap_or("").to_string();
if name.is_empty() {
continue;
}
let paths: Vec<String> = claim["paths"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|p| p.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let mut targets: Vec<String> = Vec::new();
for c in &clustering.clusters {
let member_region = c.member_regions.iter().any(|&m| {
clustering
.regions
.get(m)
.map(|r| r.name == name)
.unwrap_or(false)
});
let covered = !paths.is_empty()
&& paths.iter().all(|p| {
c.dirs.iter().any(|d| {
let d = d.trim_end_matches('/');
p == d || p.starts_with(&format!("{d}/")) || d.starts_with(&format!("{p}/"))
})
});
if member_region || covered || c.name == name {
targets.push(c.name.clone());
}
}
if let Some(resp) = claim["responsibility"].as_array() {
for t in &targets {
for r in resp {
if let Some(s) = r.as_str() {
intent_resp.entry(t.clone()).or_default().push(s.to_string());
}
}
}
}
if let Some(o) = claim["owns"].as_array() {
for t in &targets {
for ow in o {
if let Some(s) = ow.as_str() {
intent_owns.entry(t.clone()).or_default().push(s.to_string());
}
}
}
}
}
}
let comp_names: Vec<String> = clustering
.clusters
.iter()
.map(|c| c.name.clone())
.collect();
let mut out: Vec<scc_core::Entity> = Vec::new();
for name in comp_names {
let id = entity_id(repo_id, kinds::COMPONENT, &name);
let mut e = scc_core::Entity::new(id.clone(), kinds::COMPONENT, name.clone());
let mut resp: Vec<serde_json::Value> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let push_resp = |text: String, prov: Provenance, conf: f64,
resp: &mut Vec<serde_json::Value>, seen: &mut HashSet<String>| {
if !seen.insert(text.clone()) {
return;
}
resp.push(json!({
"text": text,
"provenance": prov.as_str(),
"confidence": conf,
}));
};
if let Some(irs) = intent_resp.get(&name) {
for s in irs {
push_resp(s.clone(), Provenance::Declared, 1.0, &mut resp, &mut seen);
}
}
if let Some(rs) = responsibilities.get(&name) {
let mut sorted = rs.clone();
sorted.sort_by(|a, b| {
prov_rank(b.1)
.cmp(&prov_rank(a.1))
.then_with(|| a.0.cmp(&b.0))
});
for (text, prov, conf) in sorted {
push_resp(text, prov, conf, &mut resp, &mut seen);
}
}
e.attr("responsibility", json!(resp));
let cluster = clustering
.clusters
.iter()
.find(|c| c.name == name)
.expect("every compiled component is a clustering result");
let dirs = cluster.dirs.clone();
e.attr(
"implementation",
json!({
"paths": dirs,
"symbols": symbols_per_comp.get(&name).cloned().unwrap_or_default(),
}),
);
e.attr("role", json!(component_role(&dirs)));
let mut score: f64 = match cluster.boundary_kind.as_str() {
BOUNDARY_DEPLOYMENT => 5.0,
BOUNDARY_PACKAGE => 4.0,
BOUNDARY_CLI => 4.0,
BOUNDARY_CODE_REGION | BOUNDARY_ROOT => 1.0,
_ => 0.0,
};
if shared_writes
.iter()
.any(|((c, _), syms)| c == &name && syms.len() >= 2)
{
score += 4.0; }
if entrypoints.get(&name).copied().unwrap_or(0) > 0 {
score += 4.0; }
if route_entities.get(&name).copied().unwrap_or(0) > 0 {
score += 3.0; }
if events.get(&name).copied().unwrap_or(0) > 0 {
score += 3.0; }
if let Some((internal, total)) = calls.get(&name) {
if *total > 0 {
score += 3.0 * (*internal as f64 / *total as f64); }
}
let dir_refs: Vec<&str> = dirs.iter().map(|d| d.as_str()).collect();
let co_pairs = pairs
.iter()
.filter(|p| {
crate::cochange::file_in_paths(&p.a, &dir_refs)
&& crate::cochange::file_in_paths(&p.b, &dir_refs)
})
.count();
score += 2.0 * co_pairs as f64; score = (score * 1000.0).round() / 1000.0;
e.attr("boundary_kind", json!(cluster.boundary_kind.clone()));
e.attr("layer", json!(cluster.layer.clone()));
e.attr("clustering_score", json!(score));
if let Some(parent) = parent_per_comp.get(&name) {
e.attr("parent", json!(parent));
}
let mut owned_claims: Vec<(String, Provenance, f64, Vec<String>)> = owns
.get(&name)
.cloned()
.unwrap_or_default();
if let Some(ios) = intent_owns.get(&name) {
for target in ios {
let target_l = target.to_ascii_lowercase();
let matched = graph
.entities_of_kind(kinds::DATA_STORE)
.into_iter()
.chain(graph.entities_of_kind(kinds::DATA_ENTITY))
.find(|e| e.name.to_ascii_lowercase() == target_l)
.map(|e| e.id.clone());
if let Some(mid) = matched {
owned_claims.push((mid, Provenance::Declared, 1.0, Vec::new()));
}
}
}
owned_claims.sort_by(|a, b| {
a.0.cmp(&b.0)
.then_with(|| prov_rank(b.1).cmp(&prov_rank(a.1)))
});
let owned_json: Vec<serde_json::Value> = owned_claims
.iter()
.map(|(t, p, c, ev)| {
json!({
"target": t,
"provenance": p.as_str(),
"confidence": c,
"evidence": ev,
})
})
.collect();
e.attr("owns", json!(owned_json));
let deps: Vec<serde_json::Value> = depends
.get(&name)
.map(|v| {
let mut sorted = v.clone();
sorted.sort_by(|a, b| b.3.cmp(&a.3).then_with(|| a.0.cmp(&b.0)));
sorted
.into_iter()
.map(|(t, p, c, n)| {
json!({"target": t, "provenance": p.as_str(), "confidence": c, "call_count": n})
})
.collect()
})
.unwrap_or_default();
e.attr("depends_on", json!(deps));
e.attr(
"retries",
json!(retries_per_comp.get(&name).cloned().unwrap_or_default()),
);
e.evidence = evidence_per_comp.get(&name).cloned().unwrap_or_default();
out.push(e);
}
clear_component_relationships(store)?;
let mut rels: Vec<(Relationship, String)> = Vec::new();
let sym_evidence_in_file = |fid: &str| -> Vec<String> {
let mut ev: Vec<String> = Vec::new();
for r in graph.out_pred(fid, scc_core::predicates::CONTAINS) {
if let Some(e) = graph.entities.get(&r.object) {
ev.extend(e.evidence.clone());
}
}
ev
};
let write_evidence_to = |store_id: &str| -> Vec<String> {
let store_target = if store_id.contains("/data/") {
graph
.entities
.get(store_id)
.and_then(|e| e.attributes.get("store"))
.and_then(|v| v.as_str())
.map(|s| entity_id(repo_id, kinds::DATA_STORE, s))
.unwrap_or_else(|| store_id.to_string())
} else {
store_id.to_string()
};
let mut ev: Vec<String> = Vec::new();
for r in graph.in_pred(&store_target, scc_core::predicates::WRITES) {
ev.extend(r.evidence.clone());
}
ev.sort();
ev.dedup();
ev
};
let call_evidence_between = |from_comp: &str, to_comp: &str| -> Vec<String> {
let mut ev: Vec<String> = Vec::new();
for (sym_id, comp) in &symbol_component {
if comp != from_comp {
continue;
}
for r in graph.out_pred(sym_id, scc_core::predicates::CALLS) {
if let Some(tc) = symbol_component.get(&r.object) {
if tc == to_comp {
ev.extend(r.evidence.clone());
}
}
}
}
ev.sort();
ev.dedup();
ev
};
for e in &out {
if let Some(files) = files_in_component.get(&e.name) {
for fid in files {
rels.push((
Relationship::new(
rel(&["component_contains", &e.id, fid]),
e.id.clone(),
scc_core::predicates::CONTAINS,
fid.clone(),
Provenance::Extracted,
)
.with_evidence(sym_evidence_in_file(fid)),
String::new(),
));
}
}
if let Some(owned) = e.attributes.get("owns").and_then(|v| v.as_array()) {
for o in owned {
let target = o.get("target").and_then(|v| v.as_str());
let prov = parse_prov(
o.get("provenance")
.and_then(|v| v.as_str())
.unwrap_or("INFERRED"),
);
let conf = o
.get("confidence")
.and_then(|v| v.as_f64())
.unwrap_or_else(|| prov.default_confidence());
let claim_evidence: Vec<String> = o
.get("evidence")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if let Some(os) = target {
let prov_tag = prov.as_str().to_ascii_lowercase();
let mut evidence = claim_evidence;
if evidence.is_empty() {
evidence = write_evidence_to(os);
}
rels.push((
Relationship::new(
rel(&["component_owns", &e.id, os, &prov_tag]),
e.id.clone(),
scc_core::predicates::OWNS,
os.to_string(),
prov,
)
.with_confidence(conf)
.with_evidence(evidence),
String::new(),
));
}
}
}
if let Some(deps) = e.attributes.get("depends_on").and_then(|v| v.as_array()) {
for d in deps {
if let Some(t) = d.get("target").and_then(|v| v.as_str()) {
let target_id = entity_id(repo_id, kinds::COMPONENT, t);
let prov = parse_prov(
d.get("provenance")
.and_then(|v| v.as_str())
.unwrap_or("INFERRED"),
);
rels.push((
Relationship::new(
rel(&["component_depends", &e.id, &target_id]),
e.id.clone(),
scc_core::predicates::DEPENDS_ON,
target_id,
prov,
)
.with_evidence(call_evidence_between(&e.name, t)),
String::new(),
));
}
}
}
}
for (r, src) in rels {
store.insert_relationship(&r, &src)?;
}
let state_authority = crate::state::compile_state_authority(graph, &symbol_component);
for c in out.iter_mut() {
let mut mine: Vec<String> = Vec::new();
for section in [
crate::state::S_RUNTIME,
crate::state::S_REACTIVE,
crate::state::S_CONFIGURATION,
crate::state::S_CACHES,
crate::state::S_DERIVED,
] {
if let Some(lines) = state_authority.get(section) {
let prefix = format!("{} ", c.name);
for l in lines {
if l.starts_with(&prefix) {
mine.push(l.clone());
}
}
}
}
c.attr("state_authority", json!(mine));
}
crate::clustering::compile_services(
store,
&out,
&clustering.component_weights,
&clustering.cross_unit,
&parent_per_comp,
)?;
Ok(out)
}
fn clear_component_relationships(store: &Store) -> Result<()> {
let rows = store.all_relationships()?;
let ids: Vec<String> = rows
.into_iter()
.filter(|r| r.id.starts_with(RELPREFIX))
.map(|r| r.id)
.collect();
for id in ids {
store.delete_relationship(&id)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn component_role_scopes_non_production_trees() {
let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert_eq!(component_role(&v(&["src/api", "src/db"])), "production");
assert_eq!(component_role(&v(&["fixtures/http-service-python"])), "fixture");
assert_eq!(component_role(&v(&["tests", "spec/e2e"])), "test");
assert_eq!(component_role(&v(&["benchmarks/external"])), "benchmark");
assert_eq!(component_role(&v(&["src/api", "tests/api"])), "mixed");
assert_eq!(component_role(&v(&["fixtures/a", "benchmarks/b"])), "mixed");
assert_eq!(component_role(&[]), "production");
}
#[test]
fn path_role_ignores_namespace_example_segments() {
assert_eq!(
path_role("java_service/src/main/java/com/example/greet/GreetingImpl.java"),
None
);
assert_eq!(path_role("examples/foo/main.py"), Some("example"));
assert_eq!(path_role("mycrate/examples/foo.rs"), Some("example"));
assert_eq!(path_role("fixtures/foo/app.py"), Some("fixture"));
assert_eq!(path_role("src/test/foo.py"), Some("test"));
}
#[test]
fn intent_ownership_stays_declared() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let repo = store.repo_id.clone();
let store_ent = scc_core::entity_id(&repo, kinds::DATA_STORE, "db");
store
.insert_entity(
&scc_core::Entity::new(store_ent.clone(), kinds::DATA_STORE, "db"),
&["main.py".into()],
)
.unwrap();
let file = scc_core::entity_id(&repo, kinds::FILE, "main.py");
store
.insert_entity(
&scc_core::Entity::new(file.clone(), kinds::FILE, "main.py"),
&["main.py".into()],
)
.unwrap();
let sym = scc_core::symbol_id(&repo, "main.py", "save");
store
.insert_entity(
&scc_core::Entity::new(sym.clone(), kinds::SYMBOL, "save"),
&["main.py".into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
"rel:contains",
file,
scc_core::predicates::CONTAINS,
sym.clone(),
Provenance::Extracted,
),
"main.py",
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
"rel:w",
sym.clone(),
scc_core::predicates::WRITES,
store_ent.clone(),
Provenance::Extracted,
)
.with_confidence(1.0),
"main.py",
)
.unwrap();
let intent = vec![(
"component".to_string(),
serde_json::json!({"name": "root", "owns": ["db"]}),
)];
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &intent, &[]).unwrap();
let root_comp = comps.iter().find(|c| c.name == "root").unwrap();
let claims = root_comp.attributes.get("owns").unwrap().as_array().unwrap();
assert_eq!(claims.len(), 2, "{claims:?}");
let declared = claims
.iter()
.find(|c| c.get("provenance").and_then(|v| v.as_str()) == Some("DECLARED"))
.expect("intent claim present");
assert_eq!(declared["target"].as_str().unwrap(), store_ent);
let extracted = claims
.iter()
.find(|c| c.get("provenance").and_then(|v| v.as_str()) == Some("EXTRACTED"))
.expect("write-edge claim present");
assert_eq!(extracted["target"].as_str().unwrap(), store_ent);
let rels = store.all_relationships().unwrap();
let owns: Vec<_> = rels
.iter()
.filter(|r| r.predicate == scc_core::predicates::OWNS)
.collect();
assert_eq!(owns.len(), 2, "{rels:?}");
assert!(
owns.iter().any(|r| r.provenance == Provenance::Declared),
"declared ownership relationship must exist: {owns:?}"
);
assert!(
!owns.iter().any(|r| r.provenance == Provenance::Resolved),
"no provenance promotion allowed: {owns:?}"
);
}
#[test]
fn path_assignment() {
let cands = vec![
ComponentCandidate::new("web", vec!["src/web".into()], BOUNDARY_PACKAGE),
ComponentCandidate::new("api", vec!["src/api".into()], BOUNDARY_DECLARED),
];
assert_eq!(component_for_path("src/api/routes.py", &cands), "api");
assert_eq!(component_for_path("src/web/app.ts", &cands), "web");
assert_eq!(component_for_path("src/shared/util.py", &cands), "src");
assert_eq!(component_for_path("README.md", &cands), "root");
}
fn insert_file_with_symbols(
store: &Store,
path: &str,
symbols: &[&str],
) -> (String, Vec<String>) {
let repo = store.repo_id.clone();
let file_id = scc_core::entity_id(&repo, kinds::FILE, path);
store
.insert_entity(
&scc_core::Entity::new(file_id.clone(), kinds::FILE, path),
&[path.into()],
)
.unwrap();
let mut sym_ids = Vec::new();
for s in symbols {
let sid = scc_core::symbol_id(&repo, path, s);
store
.insert_entity(
&scc_core::Entity::new(sid.clone(), kinds::SYMBOL, *s),
&[path.into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:contains:{}:{s}", path.replace('/', "_")),
file_id.clone(),
scc_core::predicates::CONTAINS,
sid.clone(),
Provenance::Extracted,
),
path,
)
.unwrap();
sym_ids.push(sid);
}
(file_id, sym_ids)
}
#[test]
fn boundary_kind_classification() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let repo = store.repo_id.clone();
for f in [
"web/app.py",
"packages/a/util.py",
"services/api/main.py",
"misc/util.py",
"README.md",
] {
store
.insert_entity(
&scc_core::Entity::new(
scc_core::entity_id(&repo, kinds::FILE, f),
kinds::FILE,
f,
),
&[f.into()],
)
.unwrap();
}
let mut pkg = scc_core::Entity::new(
scc_core::entity_id(&repo, kinds::PACKAGE, "pkg_a"),
kinds::PACKAGE,
"pkg_a",
);
pkg.attr("path", serde_json::json!("packages/a"));
store
.insert_entity(&pkg, &["packages/a/util.py".into()])
.unwrap();
let mut du = scc_core::Entity::new(
scc_core::entity_id(&repo, kinds::DEPLOYMENT_UNIT, "api"),
kinds::DEPLOYMENT_UNIT,
"api",
);
du.attr("build_context", serde_json::json!("services/api"));
store
.insert_entity(&du, &["services/api/main.py".into()])
.unwrap();
let intent = vec![(
"component".to_string(),
serde_json::json!({"name": "web", "paths": ["web"]}),
)];
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &intent, &[]).unwrap();
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
let kind_of = |n: &str| by_name[n].attributes["boundary_kind"].as_str().unwrap();
assert_eq!(kind_of("web"), BOUNDARY_DECLARED);
assert_eq!(kind_of("pkg_a"), BOUNDARY_PACKAGE);
assert_eq!(kind_of("api"), BOUNDARY_DEPLOYMENT);
assert_eq!(kind_of("misc"), BOUNDARY_CODE_REGION);
assert_eq!(kind_of("root"), BOUNDARY_ROOT);
assert!(
!by_name.contains_key("services"),
"empty dir shells are pruned by the clusterer: {comps:?}"
);
assert_eq!(by_name["api"].kind, kinds::COMPONENT);
assert!(by_name.contains_key("api"), "candidate name unchanged");
assert_eq!(by_name["api"].attributes["parent"], serde_json::json!("api"));
assert!(
!by_name["web"].attributes.contains_key("parent"),
"no parent outside a deployment unit"
);
for c in &comps {
assert!(c.attributes.contains_key("boundary_kind"), "{}", c.name);
assert!(c.attributes.contains_key("clustering_score"), "{}", c.name);
}
}
#[test]
fn cli_package_dirs_become_cli_boundary_components() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let _repo = store.repo_id.clone();
let (helm_file, helm_syms) =
insert_file_with_symbols(&store, "cmd/helm/helm.go", &["rootCmd", "serveCmd"]);
let mut flag_sym = store.get_entity(&helm_syms[0]).unwrap().unwrap();
flag_sym
.attributes
.insert("file".into(), serde_json::json!("cmd/helm/helm.go"));
flag_sym
.attributes
.insert("cli_flags".into(), serde_json::json!(["--port", "--env"]));
store.insert_entity(&flag_sym, &["cmd/helm/helm.go".into()]).unwrap();
let mut ep_sym = store.get_entity(&helm_syms[1]).unwrap().unwrap();
ep_sym
.attributes
.insert("file".into(), serde_json::json!("cmd/helm/helm.go"));
ep_sym.attributes
.insert("entrypoints".into(), serde_json::json!(["cli-subcommand"]));
store.insert_entity(&ep_sym, &["cmd/helm/helm.go".into()]).unwrap();
let (pkg_file, _) = insert_file_with_symbols(&store, "pkg/util.go", &["Util"]);
let _ = (helm_file, pkg_file);
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &[], &[]).unwrap();
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
let cli = by_name["cmd/helm"];
assert_eq!(cli.attributes["boundary_kind"], serde_json::json!("cli"));
assert_eq!(
cli.attributes["layer"],
serde_json::json!("component"),
"cli components are authoritative components, not code regions"
);
let impl_paths = cli.attributes["implementation"]["paths"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
assert_eq!(impl_paths, vec!["cmd/helm"]);
assert_eq!(
by_name["pkg"].attributes["boundary_kind"],
serde_json::json!("code-region")
);
assert_eq!(by_name["root"].attributes["boundary_kind"], serde_json::json!("root"));
}
#[test]
fn cli_evidence_in_root_dir_promotes_root_boundary() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let _repo = store.repo_id.clone();
let (_, syms) = insert_file_with_symbols(&store, "cli.py", &["main"]);
let mut sym = store.get_entity(&syms[0]).unwrap().unwrap();
sym.attributes.insert("file".into(), serde_json::json!("cli.py"));
sym.attributes
.insert("cli_flags".into(), serde_json::json!(["--verbose"]));
store.insert_entity(&sym, &["cli.py".into()]).unwrap();
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &[], &[]).unwrap();
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
assert_eq!(
by_name["root"].attributes["boundary_kind"],
serde_json::json!("cli")
);
}
#[test]
fn hierarchy_clusterer_builds_layer_stack() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let repo = store.repo_id.clone();
let mut intent: Vec<(String, serde_json::Value)> = Vec::new();
for name in ["a", "b", "c", "d", "e"] {
intent.push((
"component".to_string(),
serde_json::json!({"name": name, "paths": [name]}),
));
}
let (_fa, sa) = insert_file_with_symbols(&store, "a/x.py", &["a_main"]);
let (fb, sb) = insert_file_with_symbols(&store, "b/x.py", &["b_worker"]);
let (fc, sc) = insert_file_with_symbols(&store, "c/x.py", &["c_main"]);
let (_fd, sd) = insert_file_with_symbols(&store, "d/x.py", &["d_worker"]);
let (_fe, _se) = insert_file_with_symbols(&store, "e/x.py", &["e_standalone"]);
let _ = (sa[0].clone(), sb[0].clone(), sc[0].clone(), sd[0].clone());
for (i, (syms, store_name)) in [
(sa.clone(), "db"),
(sb.clone(), "db"),
(sc.clone(), "db2"),
(sd.clone(), "db2"),
]
.into_iter()
.enumerate()
{
let store_ent = scc_core::entity_id(&repo, kinds::DATA_STORE, store_name);
store
.insert_entity(
&scc_core::Entity::new(store_ent.clone(), kinds::DATA_STORE, store_name),
&["x.py".into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:w:{store_name}:{i}"),
syms[0].clone(),
scc_core::predicates::WRITES,
store_ent,
Provenance::Extracted,
),
"x.py",
)
.unwrap();
}
store
.insert_relationship(
&Relationship::new(
"rel:call_ab",
sa[0].clone(),
scc_core::predicates::CALLS,
sb[0].clone(),
Provenance::Extracted,
),
"a/x.py",
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
"rel:call_cd",
sc[0].clone(),
scc_core::predicates::CALLS,
sd[0].clone(),
Provenance::Extracted,
),
"c/x.py",
)
.unwrap();
for (i, sym) in [sa[0].clone(), sd[0].clone()].iter().enumerate() {
let route = scc_core::entity_id(&repo, kinds::ROUTE, &format!("get-/r{i}"));
store
.insert_entity(
scc_core::Entity::new(route.clone(), kinds::ROUTE, format!("get-/r{i}"))
.attr("method", serde_json::json!("GET"))
.attr("path", serde_json::json!(format!("/r{i}"))),
&["x.py".into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:h{i}"),
sym.clone(),
scc_core::predicates::HANDLES,
route,
Provenance::Extracted,
),
"x.py",
)
.unwrap();
}
for (i, sym) in [sb[0].clone(), sd[0].clone()].iter().enumerate() {
let mut e = store.get_entity(sym).unwrap().unwrap();
e.attributes
.insert("entrypoints".into(), serde_json::json!(["cli-subcommand"]));
store.insert_entity(&e, &["x.py".into()]).unwrap();
let _ = i;
}
for (i, sym) in [sc[0].clone(), sd[0].clone()].iter().enumerate() {
let topic = scc_core::entity_id(&repo, kinds::TOPIC, &format!("topic{i}"));
store
.insert_entity(
&scc_core::Entity::new(topic.clone(), kinds::TOPIC, format!("topic{i}")),
&["x.py".into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:p{i}"),
sym.clone(),
scc_core::predicates::PUBLISHES,
topic,
Provenance::Extracted,
),
"x.py",
)
.unwrap();
}
let cfg = scc_core::entity_id(&repo, kinds::CONFIGURATION, "MODE");
store
.insert_entity(
&scc_core::Entity::new(cfg.clone(), kinds::CONFIGURATION, "MODE"),
&["x.py".into()],
)
.unwrap();
for sym in [sa[0].clone(), sc[0].clone()] {
store
.insert_relationship(
&Relationship::new(
format!("rel:cfg:{}", sym),
cfg.clone(),
scc_core::predicates::CONFIGURED_BY,
sym,
Provenance::Extracted,
),
"x.py",
)
.unwrap();
}
store
.insert_relationship(
&Relationship::new(
"rel:imp_bc",
fb.clone(),
scc_core::predicates::IMPORTS,
fc.clone(),
Provenance::Extracted,
),
"b/x.py",
)
.unwrap();
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &intent, &[]).unwrap();
let names: std::collections::BTreeSet<&str> =
comps.iter().map(|c| c.name.as_str()).collect();
for n in ["a+b", "c+d", "e", "root"] {
assert!(names.contains(n), "clustering result keeps {n}: {names:?}");
}
assert!(!names.contains("a"), "merged pair has no a shell: {names:?}");
assert!(!names.contains("b"), "merged pair has no b shell: {names:?}");
assert!(!names.contains("c"), "merged pair has no c shell: {names:?}");
assert!(!names.contains("d"), "merged pair has no d shell: {names:?}");
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
let services = store.entities_by_kind(kinds::SERVICE).unwrap();
assert_eq!(services.len(), 0, "no service at sum 6: {services:?}");
let subsystems = store.entities_by_kind(kinds::SUBSYSTEM).unwrap();
assert_eq!(subsystems.len(), 0, "no subsystem containers anymore: {subsystems:?}");
assert_eq!(by_name["a+b"].attributes["layer"], serde_json::json!("component"));
assert_eq!(by_name["c+d"].attributes["layer"], serde_json::json!("component"));
assert_eq!(by_name["e"].attributes["layer"], serde_json::json!("component"));
assert_eq!(by_name["root"].attributes["layer"], serde_json::json!("code_region"));
let ab_paths = by_name["a+b"].attributes["implementation"]["paths"]
.as_array()
.unwrap();
assert_eq!(
ab_paths,
&vec![serde_json::json!("a"), serde_json::json!("b")],
"merged component paths: {ab_paths:?}"
);
let graph2 = RealityGraph::load(&store).unwrap();
let comps2 = compile_components(&graph2, &store, &intent, &[]).unwrap();
assert_eq!(comps2.len(), comps.len());
let names2: std::collections::BTreeSet<&str> =
comps2.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names2, names, "cluster names stable across recompiles");
for (a, b) in comps.iter().zip(comps2.iter()) {
assert_eq!(a.attributes.get("layer"), b.attributes.get("layer"), "{}", a.name);
assert_eq!(a.attributes.get("parent"), b.attributes.get("parent"), "{}", a.name);
}
let services2 = store.entities_by_kind(kinds::SERVICE).unwrap();
assert_eq!(services2.len(), 0, "no container accumulation");
}
#[test]
fn clustering_score_deterministic_and_ranked() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let repo = store.repo_id.clone();
let (_f1, api_syms) =
insert_file_with_symbols(&store, "api/routes.py", &["handle_a", "handle_b"]);
let (_f2, api_helpers) = insert_file_with_symbols(&store, "api/helpers.py", &["helper"]);
let (_f3, _web_syms) = insert_file_with_symbols(&store, "web/app.py", &["web_index"]);
let store_ent = scc_core::entity_id(&repo, kinds::DATA_STORE, "db");
store
.insert_entity(
&scc_core::Entity::new(store_ent.clone(), kinds::DATA_STORE, "db"),
&["api/routes.py".into()],
)
.unwrap();
let routes_file = scc_core::entity_id(&repo, kinds::FILE, "api/routes.py");
for (i, sym) in ["handle_a", "handle_b"].iter().enumerate() {
let route = scc_core::entity_id(&repo, kinds::ROUTE, &format!("GET /api/{i}"));
store
.insert_entity(
scc_core::Entity::new(route.clone(), kinds::ROUTE, format!("GET /api/{i}"))
.attr("method", serde_json::json!("GET"))
.attr("path", serde_json::json!(format!("/api/{i}"))),
&["api/routes.py".into()],
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:route_contains_{i}"),
routes_file.clone(),
scc_core::predicates::CONTAINS,
route.clone(),
Provenance::Extracted,
),
"api/routes.py",
)
.unwrap();
let sym_id = scc_core::symbol_id(&repo, "api/routes.py", sym);
store
.insert_relationship(
&Relationship::new(
format!("rel:handles_{i}"),
sym_id.clone(),
scc_core::predicates::HANDLES,
route.clone(),
Provenance::Extracted,
),
"api/routes.py",
)
.unwrap();
store
.insert_relationship(
&Relationship::new(
format!("rel:writes_{i}"),
sym_id,
scc_core::predicates::WRITES,
store_ent.clone(),
Provenance::Extracted,
),
"api/routes.py",
)
.unwrap();
}
store
.insert_relationship(
&Relationship::new(
"rel:call_internal",
api_syms[0].clone(),
scc_core::predicates::CALLS,
api_helpers[0].clone(),
Provenance::Extracted,
),
"api/routes.py",
)
.unwrap();
let intent = vec![(
"component".to_string(),
serde_json::json!({"name": "api", "paths": ["api"]}),
)];
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &intent, &[]).unwrap();
let graph2 = RealityGraph::load(&store).unwrap();
let comps2 = compile_components(&graph2, &store, &intent, &[]).unwrap();
let score = |c: &scc_core::Entity| c.attributes["clustering_score"].as_f64().unwrap();
for (a, b) in comps.iter().zip(comps2.iter()) {
assert_eq!(
a.attributes["clustering_score"],
b.attributes["clustering_score"],
"scores must be deterministic for {}",
a.name
);
}
let api = comps.iter().find(|c| c.name == "api").unwrap();
let web = comps.iter().find(|c| c.name == "web").unwrap();
assert_eq!(score(api), 14.0, "{:?}", api.attributes);
assert_eq!(score(web), 1.0, "bare directory: +1 only");
assert!(score(api) > score(web), "evidence-rich candidate outranks a bare dir");
assert_eq!(api.attributes["boundary_kind"], serde_json::json!("declared"));
assert_eq!(web.attributes["boundary_kind"], serde_json::json!("code-region"));
}
#[test]
fn manifest_parsing_is_deterministic() {
let ws = "[workspace]\nmembers = [\n \"crates/a\",\n \"crates/b\",\n]\nexclude = [\"crates/a\"]\n";
assert_eq!(
toml_string_array(ws, "members"),
vec!["crates/a", "crates/b"]
);
assert_eq!(toml_string_array(ws, "exclude"), vec!["crates/a"]);
let trick = "[workspace]\nexclude_members = [\"x\"]\nmembers = [\"crates/a\"]\n";
assert_eq!(toml_string_array(trick, "members"), vec!["crates/a"]);
let pkg = "[package]\nname = \"grep-cli\"\nversion = \"0.1.0\"\n";
assert_eq!(toml_package_name(pkg).as_deref(), Some("grep-cli"));
let deps = "[dependencies]\nname = \"x\"\n";
assert_eq!(toml_package_name(deps), None);
let gow = "go 1.22.0\n\nuse (\n\t./cmd/app\n\t./internal/lib\n\t// a comment\n)\n\nuse ./third\n";
assert_eq!(
gowork_use_dirs(gow),
vec!["cmd/app", "internal/lib", "third"]
);
assert_eq!(gowork_use_dirs("user = \"x\"\nuse ./only\n"), vec!["only"]);
assert!(gowork_use_dirs("go 1.22\n\nmodule = \"nouse\"\n").is_empty());
}
#[test]
fn cargo_workspace_members_compile_to_package_components() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(root.join("crates/alpha/src")).unwrap();
std::fs::create_dir_all(root.join("crates/beta/src")).unwrap();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n\n[package]\nname = \"top\"\n",
)
.unwrap();
std::fs::write(
root.join("crates/alpha/Cargo.toml"),
"[package]\nname = \"alpha\"\n",
)
.unwrap();
std::fs::write(
root.join("crates/beta/Cargo.toml"),
"[package]\nname = \"beta\"\n",
)
.unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let (fa, sa) =
insert_file_with_symbols(&store, "crates/alpha/src/lib.rs", &["alpha_run"]);
let (fb, sb) =
insert_file_with_symbols(&store, "crates/beta/src/lib.rs", &["beta_run"]);
let (_fr, _sr) = insert_file_with_symbols(&store, "README.md", &["readme"]);
let _ = (&fa, &fb, &sa, &sb);
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &[], &[]).unwrap();
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
assert!(
by_name.contains_key("crates/alpha/src"),
"per-crate component: {comps:?}"
);
assert!(
by_name.contains_key("crates/beta/src"),
"per-crate component: {comps:?}"
);
assert_eq!(
by_name["crates/alpha/src"].attributes["boundary_kind"].as_str(),
Some(BOUNDARY_PACKAGE)
);
assert_eq!(
by_name["crates/beta/src"].attributes["boundary_kind"].as_str(),
Some(BOUNDARY_PACKAGE)
);
assert_eq!(
by_name["crates/alpha/src"].attributes["implementation"]["paths"],
json!(["crates/alpha/src"])
);
assert_eq!(
by_name["crates/beta/src"].attributes["implementation"]["paths"],
json!(["crates/beta/src"])
);
let alpha_syms = by_name["crates/alpha/src"].attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(alpha_syms.iter().any(|s| s == "alpha_run"));
let beta_syms = by_name["crates/beta/src"].attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(beta_syms.iter().any(|s| s == "beta_run"));
if let Some(crates) = by_name.get("crates") {
let syms = crates.attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(syms.is_empty(), "'crates' must not swallow members: {syms:?}");
}
let cands = vec![
ComponentCandidate::new("crates/alpha/src", vec!["crates/alpha/src".into()], BOUNDARY_PACKAGE),
ComponentCandidate::new("crates/beta/src", vec!["crates/beta/src".into()], BOUNDARY_PACKAGE),
ComponentCandidate::new("crates", vec!["crates".into()], BOUNDARY_CODE_REGION),
];
assert_eq!(component_for_path("crates/alpha/src/lib.rs", &cands), "crates/alpha/src");
assert_eq!(component_for_path("crates/beta/src/lib.rs", &cands), "crates/beta/src");
}
#[test]
fn gowork_modules_compile_to_package_components() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
for d in ["cmd/app", "internal/lib", "third"] {
std::fs::create_dir_all(root.join(d)).unwrap();
}
std::fs::write(
root.join("go.work"),
"go 1.22.0\n\nuse (\n\t./cmd/app\n\t./internal/lib\n)\n\nuse ./third\n",
)
.unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
let (_fa, sa) = insert_file_with_symbols(&store, "cmd/app/main.go", &["app_main"]);
let (_fb, sb) = insert_file_with_symbols(&store, "internal/lib/lib.go", &["lib_fn"]);
let (_fc, sc) = insert_file_with_symbols(&store, "third/x.go", &["third_fn"]);
let _ = (&sa, &sb, &sc);
let graph = RealityGraph::load(&store).unwrap();
let comps = compile_components(&graph, &store, &[], &[]).unwrap();
let by_name: std::collections::BTreeMap<&str, &scc_core::Entity> =
comps.iter().map(|c| (c.name.as_str(), c)).collect();
for n in ["app", "lib", "third"] {
assert!(by_name.contains_key(n), "module component missing: {comps:?}");
assert_eq!(
by_name[n].attributes["boundary_kind"].as_str(),
Some(BOUNDARY_PACKAGE),
"{n}"
);
}
assert_eq!(
by_name["app"].attributes["implementation"]["paths"],
json!(["cmd/app"])
);
assert_eq!(
by_name["lib"].attributes["implementation"]["paths"],
json!(["internal/lib"])
);
let app_syms = by_name["app"].attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(app_syms.iter().any(|s| s == "app_main"));
let lib_syms = by_name["lib"].attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(lib_syms.iter().any(|s| s == "lib_fn"));
if let Some(cmd) = by_name.get("cmd") {
let syms = cmd.attributes["implementation"]["symbols"]
.as_array()
.unwrap();
assert!(syms.is_empty(), "'cmd' must not swallow modules: {syms:?}");
}
}
}