use std::path::PathBuf;
use super::{LineageEdge, ParseFailure};
pub fn dedupe_lineage_edges(edges: &[LineageEdge]) -> (Vec<LineageEdge>, Vec<ParseFailure>) {
use std::collections::{HashMap, HashSet};
type DedupKey<'a> = (&'a str, &'a str, Option<&'a str>, Option<&'a str>);
fn key_of(edge: &LineageEdge) -> DedupKey<'_> {
(
edge.child.as_str(),
edge.parent.as_str(),
edge.child_canonical_path.as_deref(),
edge.parent_canonical_path.as_deref(),
)
}
let mut counts: HashMap<DedupKey<'_>, usize> = HashMap::new();
for edge in edges {
*counts.entry(key_of(edge)).or_insert(0) += 1;
}
let mut emitted: HashSet<DedupKey<'_>> = HashSet::new();
let mut deduped: Vec<LineageEdge> = Vec::with_capacity(edges.len());
let mut failures: Vec<ParseFailure> = Vec::new();
for edge in edges {
let key = key_of(edge);
let count = counts.get(&key).copied().unwrap_or(0);
if emitted.insert(key) {
deduped.push(edge.clone());
if count > 1 {
failures.push(ParseFailure {
file: edge.file.clone(),
error: format!(
"duplicate #[descended_from({})] declarations on `{}` \
(first at line {}); structural lies surface as \
diagnostics rather than being silently collapsed \
(ADR-004 implicit-to-explicit elevation)",
edge.parent, edge.child, edge.line
),
});
}
}
}
(deduped, failures)
}
pub fn detect_lineage_failures(edges: &[LineageEdge], max_depth: usize) -> Vec<ParseFailure> {
use std::collections::HashMap;
let mut failures: Vec<ParseFailure> = Vec::new();
let mut adjacency: HashMap<&str, Vec<(&str, usize)>> = HashMap::new();
for (idx, edge) in edges.iter().enumerate() {
adjacency
.entry(edge.child.as_str())
.or_default()
.push((edge.parent.as_str(), idx));
}
let mut color: HashMap<&str, u8> = HashMap::new();
let mut reported_cycles: std::collections::HashSet<Vec<String>> =
std::collections::HashSet::new();
let mut roots_in_order: Vec<&str> = Vec::new();
let mut seen_roots: std::collections::HashSet<&str> = std::collections::HashSet::new();
for edge in edges {
let c = edge.child.as_str();
if seen_roots.insert(c) {
roots_in_order.push(c);
}
}
for &root in &roots_in_order {
if color.contains_key(root) {
continue;
}
let mut stack: Vec<(&str, usize)> = Vec::new();
let mut path: Vec<&str> = Vec::new();
color.insert(root, 1);
stack.push((root, 0));
path.push(root);
while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
if path.len() > max_depth {
let leaf = *path.last().unwrap_or(&node);
let anchor = adjacency
.get(leaf)
.and_then(|v| v.first())
.and_then(|(_, edge_idx)| edges.get(*edge_idx))
.map_or_else(PathBuf::new, |e| e.file.clone());
failures.push(ParseFailure {
file: anchor,
error: format!(
"#[descended_from] chain exceeds maximum depth ({max_depth}) at \
`{leaf}`; chain: {}",
path.join(" -> ")
),
});
color.insert(node, 2);
stack.pop();
path.pop();
continue;
}
let children = adjacency.get(node).map_or(&[][..], Vec::as_slice);
if *idx >= children.len() {
color.insert(node, 2);
stack.pop();
path.pop();
continue;
}
let (child, edge_idx) = children[*idx];
*idx += 1;
match color.get(child).copied().unwrap_or(0) {
0 => {
color.insert(child, 1);
path.push(child);
stack.push((child, 0));
}
1 => {
let cycle_start = path.iter().position(|n| *n == child).unwrap_or(0);
let bare_refs: Vec<&str> = path[cycle_start..].to_vec();
let mut cycle_chain: Vec<String> =
bare_refs.iter().map(|s| (*s).to_string()).collect();
cycle_chain.push(child.to_string());
let canonical = canonicalise_cycle(&bare_refs);
if reported_cycles.insert(canonical) {
let edge = edges.get(edge_idx);
let file = edge.map_or_else(PathBuf::new, |e| e.file.clone());
let line = edge.map_or(0, |e| e.line);
failures.push(ParseFailure {
file,
error: format!(
"#[descended_from] forms a cycle (closing edge at line \
{line}): {}",
cycle_chain.join(" -> ")
),
});
}
}
_ => {
}
}
}
}
failures
}
pub fn canonicalise_cycle(bare: &[&str]) -> Vec<String> {
if bare.is_empty() {
return Vec::new();
}
let n = bare.len();
let mut best_start = 0;
for start in 1..n {
for i in 0..n {
let a = bare[(start + i) % n];
let b = bare[(best_start + i) % n];
if a < b {
best_start = start;
break;
} else if a > b {
break;
}
}
}
(0..n)
.map(|i| bare[(best_start + i) % n].to_string())
.collect()
}