use HashSet for orphan edge dedup, fix redundant type qualification

Replace O(n²) Vec::contains + sort/dedup with O(n) HashSet for orphan
node tracking in health_report(). Use imported HashMap type instead of
fully-qualified std::collections::HashMap.
This commit is contained in:
ProofOfConcept 2026-03-08 21:43:58 -04:00
parent 92f3ba5acf
commit 0a35a17fad

View file

@ -607,25 +607,20 @@ pub fn health_report(graph: &Graph, store: &Store) -> String {
// Orphan edges: relations referencing non-existent nodes // Orphan edges: relations referencing non-existent nodes
let mut orphan_edges = 0usize; let mut orphan_edges = 0usize;
let mut orphan_sources: Vec<String> = Vec::new(); let mut missing_nodes: HashSet<String> = HashSet::new();
let mut orphan_targets: Vec<String> = Vec::new();
for rel in &store.relations { for rel in &store.relations {
if rel.deleted { continue; } if rel.deleted { continue; }
let s_missing = !store.nodes.contains_key(&rel.source_key); let s_missing = !store.nodes.contains_key(&rel.source_key);
let t_missing = !store.nodes.contains_key(&rel.target_key); let t_missing = !store.nodes.contains_key(&rel.target_key);
if s_missing || t_missing { if s_missing || t_missing {
orphan_edges += 1; orphan_edges += 1;
if s_missing && !orphan_sources.contains(&rel.source_key) { if s_missing { missing_nodes.insert(rel.source_key.clone()); }
orphan_sources.push(rel.source_key.clone()); if t_missing { missing_nodes.insert(rel.target_key.clone()); }
}
if t_missing && !orphan_targets.contains(&rel.target_key) {
orphan_targets.push(rel.target_key.clone());
}
} }
} }
// NodeType breakdown // NodeType breakdown
let mut type_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); let mut type_counts: HashMap<&str, usize> = HashMap::new();
for node in store.nodes.values() { for node in store.nodes.values() {
let label = match node.node_type { let label = match node.node_type {
crate::store::NodeType::EpisodicSession => "episodic", crate::store::NodeType::EpisodicSession => "episodic",
@ -690,18 +685,16 @@ Types: semantic={semantic} episodic={episodic} daily={daily} weekly={weekly} mon
if orphan_edges == 0 { if orphan_edges == 0 {
report.push_str("\n\nBroken links: 0"); report.push_str("\n\nBroken links: 0");
} else { } else {
let mut all_missing: Vec<String> = orphan_sources;
all_missing.extend(orphan_targets);
all_missing.sort();
all_missing.dedup();
report.push_str(&format!( report.push_str(&format!(
"\n\nBroken links: {} edges reference {} missing nodes", "\n\nBroken links: {} edges reference {} missing nodes",
orphan_edges, all_missing.len())); orphan_edges, missing_nodes.len()));
for key in all_missing.iter().take(10) { let mut sorted: Vec<_> = missing_nodes.iter().collect();
sorted.sort();
for key in sorted.iter().take(10) {
report.push_str(&format!("\n - {}", key)); report.push_str(&format!("\n - {}", key));
} }
if all_missing.len() > 10 { if sorted.len() > 10 {
report.push_str(&format!("\n ... and {} more", all_missing.len() - 10)); report.push_str(&format!("\n ... and {} more", sorted.len() - 10));
} }
} }