#[allow(unused_imports)]
use crate::core::{codegen, executor, migrate, parser, planner, resolver, secrets, state, types};
use std::collections::{HashMap, HashSet};
use std::path::Path;
pub(crate) fn cmd_graph_resource_impact_score(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let scores = compute_impact_scores(&config);
if json {
let items: Vec<String> = scores
.iter()
.map(|(r, s)| format!("{{\"resource\":\"{r}\",\"impact_score\":{s}}}"))
.collect();
println!("{{\"resource_impact_scores\":[{}]}}", items.join(","));
} else if scores.is_empty() {
println!("No resources.");
} else {
println!("Resource impact scores (dependents + depth):");
for (r, s) in &scores {
println!(" {r} — score {s}");
}
}
Ok(())
}
pub(super) fn compute_impact_scores(config: &types::ForjarConfig) -> Vec<(String, usize)> {
let mut fanin: HashMap<&str, usize> = HashMap::new();
for resource in config.resources.values() {
for dep in &resource.depends_on {
*fanin.entry(dep.as_str()).or_default() += 1;
}
}
let mut results: Vec<(String, usize)> = config
.resources
.keys()
.map(|name| {
let dependents = fanin.get(name.as_str()).copied().unwrap_or(0);
let depth = config.resources[name].depends_on.len();
(name.clone(), dependents * 2 + depth)
})
.collect();
results.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
results
}
pub(crate) fn cmd_graph_resource_stability_score(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let scores = compute_stability_scores(&config);
if json {
let items: Vec<String> = scores
.iter()
.map(|(r, s)| format!("{{\"resource\":\"{r}\",\"stability_score\":{s}}}"))
.collect();
println!("{{\"resource_stability_scores\":[{}]}}", items.join(","));
} else if scores.is_empty() {
println!("No resources.");
} else {
println!("Resource stability scores (higher = more stable):");
for (r, s) in &scores {
println!(" {r} — score {s}");
}
}
Ok(())
}
pub(super) fn compute_stability_scores(config: &types::ForjarConfig) -> Vec<(String, usize)> {
let mut results: Vec<(String, usize)> = config
.resources
.keys()
.map(|name| {
let r = &config.resources[name];
let mut score = 10usize;
score = score.saturating_sub(r.depends_on.len());
if r.state.is_some() {
score += 2;
}
if r.content.is_some() {
score += 1;
}
(name.clone(), score)
})
.collect();
results.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
results
}
pub(crate) fn cmd_graph_resource_dependency_fanout(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let fanouts = compute_fanouts(&config);
if json {
let items: Vec<String> = fanouts
.iter()
.map(|(r, f)| format!("{{\"resource\":\"{r}\",\"fanout\":{f}}}"))
.collect();
println!("{{\"resource_dependency_fanout\":[{}]}}", items.join(","));
} else if fanouts.is_empty() {
println!("No resources.");
} else {
println!("Resource dependency fan-out:");
for (r, f) in &fanouts {
println!(" {r} — {f} dependents");
}
}
Ok(())
}
pub(super) fn compute_fanouts(config: &types::ForjarConfig) -> Vec<(String, usize)> {
let mut fanin: HashMap<&str, usize> = HashMap::new();
for resource in config.resources.values() {
for dep in &resource.depends_on {
*fanin.entry(dep.as_str()).or_default() += 1;
}
}
let mut results: Vec<(String, usize)> = config
.resources
.keys()
.map(|name| {
let count = fanin.get(name.as_str()).copied().unwrap_or(0);
(name.clone(), count)
})
.collect();
results.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
results
}
pub(crate) fn cmd_graph_resource_dependency_weight(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let weights = compute_dependency_weights(&config);
if json {
let items: Vec<String> = weights
.iter()
.map(|(a, b, w)| format!("{{\"from\":\"{a}\",\"to\":\"{b}\",\"weight\":{w}}}"))
.collect();
println!("{{\"dependency_weights\":[{}]}}", items.join(","));
} else if weights.is_empty() {
println!("No dependency edges.");
} else {
println!("Dependency edge weights:");
for (a, b, w) in &weights {
println!(" {a} → {b} — weight {w}");
}
}
Ok(())
}
pub(super) fn compute_dependency_weights(
config: &types::ForjarConfig,
) -> Vec<(String, String, usize)> {
let mut edges = Vec::new();
for (name, resource) in &config.resources {
for dep in &resource.depends_on {
let mut weight = 1usize;
if let Some(dep_resource) = config.resources.get(dep) {
let ma: HashSet<&str> = resource.machine.iter().collect();
let mb: HashSet<&str> = dep_resource.machine.iter().collect();
if !ma.is_disjoint(&mb) {
weight += 1;
}
}
edges.push((name.clone(), dep.clone(), weight));
}
}
edges.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.cmp(&b.0)));
edges
}
pub(crate) fn cmd_graph_resource_dependency_bottleneck(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let bottlenecks = find_dependency_bottlenecks(&config);
if json {
let items: Vec<String> = bottlenecks
.iter()
.map(|(n, fi, fo)| format!("{{\"resource\":\"{n}\",\"fan_in\":{fi},\"fan_out\":{fo}}}"))
.collect();
println!("{{\"bottlenecks\":[{}]}}", items.join(","));
} else if bottlenecks.is_empty() {
println!("No dependency bottlenecks found.");
} else {
println!("Dependency bottlenecks (fan-in + fan-out):");
for (n, fi, fo) in &bottlenecks {
println!(" {n} — fan-in {fi}, fan-out {fo}");
}
}
Ok(())
}
pub(super) fn find_dependency_bottlenecks(
config: &types::ForjarConfig,
) -> Vec<(String, usize, usize)> {
let mut fan_in: HashMap<String, usize> = HashMap::new();
let mut fan_out: HashMap<String, usize> = HashMap::new();
for (name, resource) in &config.resources {
fan_out.insert(name.clone(), resource.depends_on.len());
for dep in &resource.depends_on {
*fan_in.entry(dep.clone()).or_default() += 1;
}
}
let mut results: Vec<(String, usize, usize)> = config
.resources
.keys()
.map(|n| {
(
n.clone(),
*fan_in.get(n).unwrap_or(&0),
*fan_out.get(n).unwrap_or(&0),
)
})
.filter(|(_, fi, fo)| *fi > 0 || *fo > 0)
.collect();
results.sort_by(|a, b| (b.1 + b.2).cmp(&(a.1 + a.2)).then(a.0.cmp(&b.0)));
results
}
pub(crate) fn cmd_graph_resource_type_clustering(file: &Path, json: bool) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| format!("Read error: {e}"))?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| format!("Parse error: {e}"))?;
let clusters = build_type_clusters(&config);
if json {
let items: Vec<String> = clusters
.iter()
.map(|(t, rs)| {
let names: Vec<String> = rs.iter().map(|r| format!("\"{r}\"")).collect();
format!("{{\"type\":\"{}\",\"resources\":[{}]}}", t, names.join(","))
})
.collect();
println!("{{\"type_clusters\":[{}]}}", items.join(","));
} else if clusters.is_empty() {
println!("No resources to cluster.");
} else {
println!("Resource type clusters:");
for (t, rs) in &clusters {
println!(" {} — {} resources: {}", t, rs.len(), rs.join(", "));
}
}
Ok(())
}
pub(super) fn build_type_clusters(config: &types::ForjarConfig) -> Vec<(String, Vec<String>)> {
let mut clusters: HashMap<String, Vec<String>> = HashMap::new();
for (name, resource) in &config.resources {
let type_str = format!("{:?}", resource.resource_type);
clusters.entry(type_str).or_default().push(name.clone());
}
for v in clusters.values_mut() {
v.sort();
}
let mut result: Vec<(String, Vec<String>)> = clusters.into_iter().collect();
result.sort_by(|a, b| b.1.len().cmp(&a.1.len()).then(a.0.cmp(&b.0)));
result
}
pub(crate) fn cmd_graph_resource_dependency_cycle_risk(
file: &std::path::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 risks = find_cycle_risks(&config);
if json {
let items: Vec<String> = risks
.iter()
.map(|(a, b, depth)| {
format!("{{\"from\":\"{a}\",\"to\":\"{b}\",\"mutual_depth\":{depth}}}")
})
.collect();
println!("{{\"cycle_risks\":[{}]}}", items.join(","));
} else if risks.is_empty() {
println!("No dependency cycle risks found.");
} else {
println!("Dependency cycle risks:");
for (a, b, depth) in &risks {
println!(" {a} ↔ {b} — mutual depth {depth}");
}
}
Ok(())
}
pub(super) fn find_cycle_risks(config: &types::ForjarConfig) -> Vec<(String, String, usize)> {
let mut risks = Vec::new();
for (name, resource) in &config.resources {
for dep in &resource.depends_on {
let dep_res = match config.resources.get(dep) {
Some(r) => r,
None => continue,
};
if !dep_res.depends_on.contains(name) {
continue;
}
let pair = if name < dep {
(name.clone(), dep.clone())
} else {
(dep.clone(), name.clone())
};
let already_found = risks
.iter()
.any(|(a, b, _): &(String, String, usize)| a == &pair.0 && b == &pair.1);
if !already_found {
risks.push((pair.0, pair.1, 1));
}
}
}
risks.sort_by(|a, b| a.0.cmp(&b.0));
risks
}
pub(super) use super::graph_scoring_b::*;