use crate::core::types;
use std::path::Path;
pub(crate) fn cmd_graph_resource_dependency_topological_depth(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let mut depths: Vec<(String, usize)> = config
.resources
.keys()
.map(|name| {
let d = topo_depth(&config, name, &mut std::collections::HashMap::new());
(name.clone(), d)
})
.collect();
depths.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let max = depths.first().map(|(_, d)| *d).unwrap_or(0);
if json {
let items: Vec<String> = depths
.iter()
.map(|(n, d)| format!("{{\"resource\":\"{n}\",\"depth\":{d}}}"))
.collect();
println!(
"{{\"max_depth\":{},\"resources\":[{}]}}",
max,
items.join(",")
);
} else if depths.is_empty() {
println!("No resources found.");
} else {
println!("Topological depth (max: {max}):");
for (n, d) in &depths {
println!(" {n} — depth {d}");
}
}
Ok(())
}
fn topo_depth(
config: &types::ForjarConfig,
name: &str,
cache: &mut std::collections::HashMap<String, usize>,
) -> usize {
if let Some(&d) = cache.get(name) {
return d;
}
let res = match config.resources.get(name) {
Some(r) => r,
None => return 0,
};
if res.depends_on.is_empty() {
cache.insert(name.to_string(), 0);
return 0;
}
let max_dep = res
.depends_on
.iter()
.map(|dep| topo_depth(config, dep, cache))
.max()
.unwrap_or(0);
let depth = max_dep + 1;
cache.insert(name.to_string(), depth);
depth
}
pub(crate) fn cmd_graph_resource_dependency_weak_links(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let mut in_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for res in config.resources.values() {
for dep in &res.depends_on {
*in_counts.entry(dep.as_str()).or_insert(0) += 1;
}
}
let mut weak_links: Vec<(String, String, usize)> = Vec::new();
for (name, res) in &config.resources {
for dep in &res.depends_on {
let dependents = in_counts.get(dep.as_str()).copied().unwrap_or(0);
if dependents > 1 {
weak_links.push((name.clone(), dep.clone(), dependents));
}
}
}
weak_links.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.cmp(&b.0)));
if json {
let items: Vec<String> = weak_links
.iter()
.map(|(from, to, d)| {
format!("{{\"from\":\"{from}\",\"to\":\"{to}\",\"dependents\":{d}}}")
})
.collect();
println!("{{\"weak_links\":[{}]}}", items.join(","));
} else if weak_links.is_empty() {
println!("No weak links found (no shared dependencies).");
} else {
println!("Weak links (shared dependencies, cascading risk):");
for (from, to, d) in &weak_links {
println!(" {from} → {to} ({d} dependents)");
}
}
Ok(())
}
pub(crate) fn cmd_graph_resource_dependency_minimum_cut(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let names: Vec<&str> = config.resources.keys().map(|s| s.as_str()).collect();
let idx: std::collections::HashMap<&str, usize> =
names.iter().enumerate().map(|(i, n)| (*n, i)).collect();
let n = names.len();
let (adj, edge_count) = build_undirected_adj(&config, &idx, n);
let bridges = find_bridge_edges(&config, &idx, &adj, n);
if json {
let items: Vec<String> = bridges
.iter()
.map(|(a, b)| format!("{{\"from\":\"{a}\",\"to\":\"{b}\"}}"))
.collect();
println!(
"{{\"minimum_cut_edges\":[{}],\"total_edges\":{}}}",
items.join(","),
edge_count
);
} else if bridges.is_empty() {
println!("No bridge edges found — graph has no single-edge cut points.");
} else {
println!("Minimum cut edges (bridges):");
for (a, b) in &bridges {
println!(" {a} → {b}");
}
}
Ok(())
}
fn build_undirected_adj(
config: &types::ForjarConfig,
idx: &std::collections::HashMap<&str, usize>,
n: usize,
) -> (Vec<Vec<bool>>, usize) {
let mut adj = vec![vec![false; n]; n];
let mut edge_count = 0usize;
for (name, res) in &config.resources {
let u = match idx.get(name.as_str()) {
Some(&u) => u,
None => continue,
};
for dep in &res.depends_on {
let v = match idx.get(dep.as_str()) {
Some(&v) => v,
None => continue,
};
if !adj[u][v] {
edge_count += 1;
}
adj[u][v] = true;
adj[v][u] = true;
}
}
(adj, edge_count)
}
fn find_bridge_edges(
config: &types::ForjarConfig,
idx: &std::collections::HashMap<&str, usize>,
adj: &[Vec<bool>],
n: usize,
) -> Vec<(String, String)> {
let baseline = count_components(adj, n);
let mut bridges = Vec::new();
for (name, res) in &config.resources {
let u = match idx.get(name.as_str()) {
Some(&u) => u,
None => continue,
};
for dep in &res.depends_on {
let v = match idx.get(dep.as_str()) {
Some(&v) => v,
None => continue,
};
let mut adj_copy = adj.to_vec();
adj_copy[u][v] = false;
adj_copy[v][u] = false;
if count_components(&adj_copy, n) > baseline {
bridges.push((name.clone(), dep.clone()));
}
}
}
bridges
}
fn count_components(adj: &[Vec<bool>], n: usize) -> usize {
let mut visited = vec![false; n];
let mut components = 0;
for i in 0..n {
if !visited[i] {
components += 1;
let mut stack = vec![i];
while let Some(node) = stack.pop() {
if visited[node] {
continue;
}
visited[node] = true;
for (j, &is_adj) in adj[node].iter().enumerate() {
if is_adj && !visited[j] {
stack.push(j);
}
}
}
}
}
components
}
pub(crate) fn cmd_graph_resource_dependency_dominator_tree(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let names: Vec<String> = config.resources.keys().cloned().collect();
let idx: std::collections::HashMap<&str, usize> = names
.iter()
.enumerate()
.map(|(i, n)| (n.as_str(), i))
.collect();
let n = names.len();
let children = build_children_adj(&config, &idx, n);
let roots = find_roots(&config, &names, n);
let dom_list = compute_dominator_counts(&names, &children, &roots, n);
if json {
let items: Vec<String> = dom_list
.iter()
.map(|(name, d)| format!("{{\"resource\":\"{name}\",\"dominates\":{d}}}"))
.collect();
println!("{{\"dominator_tree\":[{}]}}", items.join(","));
} else if dom_list.is_empty() {
println!("No dominators found (no single points of failure).");
} else {
println!("Dominator tree (single points of failure):");
for (name, d) in &dom_list {
println!(" {name} — dominates {d} resources");
}
}
Ok(())
}
fn build_children_adj(
config: &types::ForjarConfig,
idx: &std::collections::HashMap<&str, usize>,
n: usize,
) -> Vec<Vec<usize>> {
let mut children: Vec<Vec<usize>> = vec![vec![]; n];
for (name, res) in &config.resources {
let to = match idx.get(name.as_str()) {
Some(&t) => t,
None => continue,
};
for dep in &res.depends_on {
let from = match idx.get(dep.as_str()) {
Some(&f) => f,
None => continue,
};
children[from].push(to);
}
}
children
}
fn find_roots(config: &types::ForjarConfig, names: &[String], n: usize) -> Vec<usize> {
(0..n)
.filter(|&i| config.resources[&names[i]].depends_on.is_empty())
.collect()
}
fn compute_dominator_counts(
names: &[String],
children: &[Vec<usize>],
roots: &[usize],
n: usize,
) -> Vec<(String, usize)> {
let mut dominates: Vec<usize> = vec![0; n];
for &root in roots {
let reachable = reachable_from(root, children, n);
for (node, &is_reachable) in reachable.iter().enumerate() {
if node == root || !is_reachable {
continue;
}
let still_reachable = roots
.iter()
.any(|&r| r != root && reachable_without(r, root, children, n, node));
if !still_reachable {
dominates[root] += 1;
}
}
}
let mut dom_list: Vec<(String, usize)> = names
.iter()
.enumerate()
.map(|(i, name)| (name.clone(), dominates[i]))
.filter(|(_, d)| *d > 0)
.collect();
dom_list.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
dom_list
}
fn reachable_from(start: usize, children: &[Vec<usize>], n: usize) -> Vec<bool> {
let mut visited = vec![false; n];
let mut stack = vec![start];
while let Some(node) = stack.pop() {
if visited[node] {
continue;
}
visited[node] = true;
for &child in &children[node] {
stack.push(child);
}
}
visited
}
fn reachable_without(
start: usize,
exclude: usize,
children: &[Vec<usize>],
n: usize,
target: usize,
) -> bool {
let mut visited = vec![false; n];
let mut stack = vec![start];
while let Some(node) = stack.pop() {
if node == exclude || visited[node] {
continue;
}
visited[node] = true;
if node == target {
return true;
}
for &child in &children[node] {
stack.push(child);
}
}
false
}