admin: convert fsck and dedup reads to use index

- fsck: use for_each_relation for dangling edge detection
  (pruning deferred - needs delete_edge operation)
- dedup: use for_each_relation for edge counting

Remaining Vec uses in dedup mutation section need new index ops:
- redirect_edge: change source/target UUID
- delete_edge_by_uuid: tombstone by UUID

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-13 21:22:52 -04:00
parent 5832e57970
commit 58b0947625

View file

@ -75,42 +75,35 @@ pub async fn cmd_fsck() -> Result<()> {
}
}
// Check edge endpoints
// Check edge endpoints using index
use crate::hippocampus::store::StoreView;
let mut dangling = 0;
for rel in &store.relations {
if rel.deleted { continue; }
if !store.contains_key(&rel.source_key).unwrap_or(false) {
eprintln!("DANGLING: edge source '{}'", rel.source_key);
let mut orphan_edges: Vec<(String, String)> = Vec::new();
store.for_each_relation(|source, target, _, _| {
let s_missing = !store.contains_key(source).unwrap_or(false);
let t_missing = !store.contains_key(target).unwrap_or(false);
if s_missing {
eprintln!("DANGLING: edge source '{}'", source);
dangling += 1;
}
if !store.contains_key(&rel.target_key).unwrap_or(false) {
eprintln!("DANGLING: edge target '{}'", rel.target_key);
if t_missing {
eprintln!("DANGLING: edge target '{}'", target);
dangling += 1;
}
}
if s_missing || t_missing {
orphan_edges.push((source.to_string(), target.to_string()));
}
});
// Prune orphan edges
let mut to_tombstone = Vec::new();
for rel in &store.relations {
if rel.deleted { continue; }
if !store.contains_key(&rel.source_key).unwrap_or(false)
|| !store.contains_key(&rel.target_key).unwrap_or(false) {
let mut tombstone = rel.clone();
tombstone.deleted = true;
tombstone.version += 1;
to_tombstone.push(tombstone);
if !orphan_edges.is_empty() {
let count = orphan_edges.len();
for (source, target) in &orphan_edges {
// set_link_strength with 0 would delete, but we don't have that
// For now just report - full cleanup requires more work
eprintln!("Would prune: {}{}", source, target);
}
}
if !to_tombstone.is_empty() {
let count = to_tombstone.len();
store.append_relations(&to_tombstone)?;
for t in &to_tombstone {
if let Some(r) = store.relations.iter_mut().find(|r| r.uuid == t.uuid) {
r.deleted = true;
r.version = t.version;
}
}
eprintln!("Pruned {} orphan edges", count);
eprintln!("Found {} orphan edges (prune not yet implemented for index)", count);
}
let g = store.build_graph();
@ -131,12 +124,19 @@ pub async fn cmd_dedup(apply: bool) -> Result<()> {
return Ok(());
}
// Count edges per UUID
// Count edges per key (we'll map to UUID later)
use crate::hippocampus::store::StoreView;
let mut edges_by_key: HashMap<String, usize> = HashMap::new();
store.for_each_relation(|source, target, _, _| {
*edges_by_key.entry(source.to_string()).or_default() += 1;
*edges_by_key.entry(target.to_string()).or_default() += 1;
});
// Convert to edges_by_uuid for compatibility
let mut edges_by_uuid: HashMap<[u8; 16], usize> = HashMap::new();
for rel in &store.relations {
if rel.deleted { continue; }
*edges_by_uuid.entry(rel.source).or_default() += 1;
*edges_by_uuid.entry(rel.target).or_default() += 1;
for (key, count) in &edges_by_key {
if let Ok(Some(node)) = store.get_node(key) {
edges_by_uuid.insert(node.uuid, *count);
}
}
let mut identical_groups = Vec::new();