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