init: reconcile with existing nodes, filter orphaned edges

init now detects content changes in markdown files and updates
existing nodes (bumps version, appends to capnp log) instead of
only creating new ones. Link resolution uses the redirect table
so references to moved sections (e.g. from the reflections split)
create edges to the correct target.

On cache rebuild from capnp logs, filter out relations that
reference deleted/missing nodes so the relation count matches
the actual graph edge count.
This commit is contained in:
ProofOfConcept 2026-02-28 22:45:31 -05:00
parent 2d6c8d5199
commit 1a01cbf8f8

View file

@ -306,6 +306,12 @@ impl Store {
store.replay_relations(&rels_p)?;
}
// Drop edges referencing deleted/missing nodes
store.relations.retain(|r|
store.nodes.contains_key(&r.source_key) &&
store.nodes.contains_key(&r.target_key)
);
// Save cache
store.save()?;
Ok(store)
@ -526,6 +532,9 @@ impl Store {
let entries = fs::read_dir(dir)
.map_err(|e| format!("read dir {}: {}", dir.display(), e))?;
// Track which keys we see in markdown so we can detect removed sections
let mut seen_keys = std::collections::HashSet::new();
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
@ -541,6 +550,7 @@ impl Store {
let units = parse_units(&filename, &content);
let mut new_nodes = Vec::new();
let mut updated_nodes = Vec::new();
let mut new_relations = Vec::new();
// Determine node type from filename
@ -555,7 +565,23 @@ impl Store {
};
for unit in &units {
if !self.nodes.contains_key(&unit.key) {
seen_keys.insert(unit.key.clone());
if let Some(existing) = self.nodes.get(&unit.key) {
// Update if content changed
if existing.content != unit.content {
let mut node = existing.clone();
node.content = unit.content.clone();
node.version += 1;
if let Some(ref state) = unit.state {
node.state_tag = state.clone();
}
if let Some(ref src) = unit.source_ref {
node.source_ref = src.clone();
}
updated_nodes.push(node);
}
} else {
let mut node = Store::new_node(&unit.key, &unit.content);
node.node_type = node_type;
if let Some(ref state) = unit.state {
@ -578,7 +604,15 @@ impl Store {
count += new_nodes.len();
}
// Create relations from links
// Batch append updated nodes
if !updated_nodes.is_empty() {
self.append_nodes(&updated_nodes)?;
for node in &updated_nodes {
self.nodes.insert(node.key.clone(), node.clone());
}
}
// Create relations from links, using resolve_redirect for targets
for unit in &units {
let source_uuid = match self.nodes.get(&unit.key) {
Some(n) => n.uuid,
@ -586,7 +620,15 @@ impl Store {
};
for link in unit.marker_links.iter().chain(unit.md_links.iter()) {
let target_uuid = match self.nodes.get(link) {
// Try direct lookup, then redirect
let resolved_link = if self.nodes.contains_key(link) {
link.clone()
} else if let Some(redirect) = self.resolve_redirect(link) {
redirect
} else {
continue;
};
let target_uuid = match self.nodes.get(&resolved_link) {
Some(n) => n.uuid,
None => continue,
};
@ -598,14 +640,21 @@ impl Store {
let rel = Store::new_relation(
source_uuid, target_uuid,
RelationType::Link, 1.0,
&unit.key, link,
&unit.key, &resolved_link,
);
new_relations.push(rel);
}
}
for cause in &unit.causes {
let target_uuid = match self.nodes.get(cause) {
let resolved_cause = if self.nodes.contains_key(cause) {
cause.clone()
} else if let Some(redirect) = self.resolve_redirect(cause) {
redirect
} else {
continue;
};
let target_uuid = match self.nodes.get(&resolved_cause) {
Some(n) => n.uuid,
None => continue,
};
@ -616,7 +665,7 @@ impl Store {
let rel = Store::new_relation(
target_uuid, source_uuid,
RelationType::Causal, 1.0,
cause, &unit.key,
&resolved_cause, &unit.key,
);
new_relations.push(rel);
}