capnp_store: extract helpers, eliminate duplication
- modify_node(): get_mut→modify→version++→append pattern was duplicated across mark_used, mark_wrong, categorize — extract once - resolve_node_uuid(): resolve-or-redirect pattern was inlined in both link and causal edge creation — extract once - ingest_units() + classify_filename(): shared logic between scan_dir_for_init and import_file — import_file shrinks to 6 lines - Remove dead seen_keys HashSet (built but never read) - partial_cmp().unwrap() → total_cmp() in cap_degree -95 lines net.
This commit is contained in:
parent
ea0d631051
commit
0bce6aac3c
1 changed files with 77 additions and 172 deletions
|
|
@ -1071,9 +1071,6 @@ impl Store {
|
||||||
let entries = fs::read_dir(dir)
|
let entries = fs::read_dir(dir)
|
||||||
.map_err(|e| format!("read dir {}: {}", dir.display(), e))?;
|
.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() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
|
|
@ -1088,73 +1085,11 @@ impl Store {
|
||||||
.map_err(|e| format!("read {}: {}", path.display(), e))?;
|
.map_err(|e| format!("read {}: {}", path.display(), e))?;
|
||||||
|
|
||||||
let units = parse_units(&filename, &content);
|
let units = parse_units(&filename, &content);
|
||||||
let mut new_nodes = Vec::new();
|
let (new_count, _) = self.ingest_units(&units, &filename)?;
|
||||||
let mut updated_nodes = Vec::new();
|
count += new_count;
|
||||||
|
|
||||||
|
// Create relations from links
|
||||||
let mut new_relations = Vec::new();
|
let mut new_relations = Vec::new();
|
||||||
|
|
||||||
// Determine node type from filename
|
|
||||||
let node_type = if filename.starts_with("daily-") {
|
|
||||||
NodeType::EpisodicDaily
|
|
||||||
} else if filename.starts_with("weekly-") {
|
|
||||||
NodeType::EpisodicWeekly
|
|
||||||
} else if filename == "journal.md" {
|
|
||||||
NodeType::EpisodicSession
|
|
||||||
} else {
|
|
||||||
NodeType::Semantic
|
|
||||||
};
|
|
||||||
|
|
||||||
for (pos, unit) in units.iter().enumerate() {
|
|
||||||
seen_keys.insert(unit.key.clone());
|
|
||||||
|
|
||||||
if let Some(existing) = self.nodes.get(&unit.key) {
|
|
||||||
// Update if content or position changed
|
|
||||||
let pos_changed = existing.position != pos as u32;
|
|
||||||
if existing.content != unit.content || pos_changed {
|
|
||||||
let mut node = existing.clone();
|
|
||||||
node.content = unit.content.clone();
|
|
||||||
node.position = pos as u32;
|
|
||||||
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;
|
|
||||||
node.position = pos as u32;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
new_nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch append new nodes
|
|
||||||
if !new_nodes.is_empty() {
|
|
||||||
self.append_nodes(&new_nodes)?;
|
|
||||||
for node in &new_nodes {
|
|
||||||
self.uuid_to_key.insert(node.uuid, node.key.clone());
|
|
||||||
self.nodes.insert(node.key.clone(), node.clone());
|
|
||||||
}
|
|
||||||
count += new_nodes.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
for unit in &units {
|
||||||
let source_uuid = match self.nodes.get(&unit.key) {
|
let source_uuid = match self.nodes.get(&unit.key) {
|
||||||
Some(n) => n.uuid,
|
Some(n) => n.uuid,
|
||||||
|
|
@ -1162,54 +1097,28 @@ impl Store {
|
||||||
};
|
};
|
||||||
|
|
||||||
for link in unit.marker_links.iter().chain(unit.md_links.iter()) {
|
for link in unit.marker_links.iter().chain(unit.md_links.iter()) {
|
||||||
// Try direct lookup, then redirect
|
let Some((key, uuid)) = self.resolve_node_uuid(link) else { continue };
|
||||||
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,
|
|
||||||
};
|
|
||||||
// Check if relation already exists
|
|
||||||
let exists = self.relations.iter().any(|r|
|
let exists = self.relations.iter().any(|r|
|
||||||
(r.source == source_uuid && r.target == target_uuid) ||
|
(r.source == source_uuid && r.target == uuid) ||
|
||||||
(r.source == target_uuid && r.target == source_uuid));
|
(r.source == uuid && r.target == source_uuid));
|
||||||
if !exists {
|
if !exists {
|
||||||
let rel = Store::new_relation(
|
new_relations.push(Store::new_relation(
|
||||||
source_uuid, target_uuid,
|
source_uuid, uuid, RelationType::Link, 1.0,
|
||||||
RelationType::Link, 1.0,
|
&unit.key, &key,
|
||||||
&unit.key, &resolved_link,
|
));
|
||||||
);
|
|
||||||
new_relations.push(rel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for cause in &unit.causes {
|
for cause in &unit.causes {
|
||||||
let resolved_cause = if self.nodes.contains_key(cause) {
|
let Some((key, uuid)) = self.resolve_node_uuid(cause) else { continue };
|
||||||
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,
|
|
||||||
};
|
|
||||||
let exists = self.relations.iter().any(|r|
|
let exists = self.relations.iter().any(|r|
|
||||||
r.source == target_uuid && r.target == source_uuid
|
r.source == uuid && r.target == source_uuid
|
||||||
&& r.rel_type == RelationType::Causal);
|
&& r.rel_type == RelationType::Causal);
|
||||||
if !exists {
|
if !exists {
|
||||||
let rel = Store::new_relation(
|
new_relations.push(Store::new_relation(
|
||||||
target_uuid, source_uuid,
|
uuid, source_uuid, RelationType::Causal, 1.0,
|
||||||
RelationType::Causal, 1.0,
|
&key, &unit.key,
|
||||||
&resolved_cause, &unit.key,
|
));
|
||||||
);
|
|
||||||
new_relations.push(rel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1309,6 +1218,16 @@ impl Store {
|
||||||
.map(|(_, to)| to.to_string())
|
.map(|(_, to)| to.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a link target to (key, uuid), trying direct lookup then redirect.
|
||||||
|
fn resolve_node_uuid(&self, target: &str) -> Option<(String, [u8; 16])> {
|
||||||
|
if let Some(n) = self.nodes.get(target) {
|
||||||
|
return Some((target.to_string(), n.uuid));
|
||||||
|
}
|
||||||
|
let redirected = self.resolve_redirect(target)?;
|
||||||
|
let n = self.nodes.get(&redirected)?;
|
||||||
|
Some((redirected, n.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn log_retrieval(&mut self, query: &str, results: &[String]) {
|
pub fn log_retrieval(&mut self, query: &str, results: &[String]) {
|
||||||
self.retrieval_log.push(RetrievalEvent {
|
self.retrieval_log.push(RetrievalEvent {
|
||||||
query: query.to_string(),
|
query: query.to_string(),
|
||||||
|
|
@ -1339,41 +1258,36 @@ impl Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modify a node in-place, bump version, and persist to capnp log.
|
||||||
|
fn modify_node(&mut self, key: &str, f: impl FnOnce(&mut Node)) -> Result<(), String> {
|
||||||
|
let node = self.nodes.get_mut(key)
|
||||||
|
.ok_or_else(|| format!("No node '{}'", key))?;
|
||||||
|
f(node);
|
||||||
|
node.version += 1;
|
||||||
|
let node = node.clone();
|
||||||
|
self.append_nodes(&[node])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mark_used(&mut self, key: &str) {
|
pub fn mark_used(&mut self, key: &str) {
|
||||||
let updated = if let Some(node) = self.nodes.get_mut(key) {
|
let boost = self.params.use_boost as f32;
|
||||||
node.uses += 1;
|
let _ = self.modify_node(key, |n| {
|
||||||
node.weight = (node.weight + self.params.use_boost as f32).min(1.0);
|
n.uses += 1;
|
||||||
// Reset spaced repetition — used successfully, move up interval
|
n.weight = (n.weight + boost).min(1.0);
|
||||||
if node.spaced_repetition_interval < 30 {
|
if n.spaced_repetition_interval < 30 {
|
||||||
node.spaced_repetition_interval = match node.spaced_repetition_interval {
|
n.spaced_repetition_interval = match n.spaced_repetition_interval {
|
||||||
1 => 3, 3 => 7, 7 => 14, 14 => 30, _ => 30,
|
1 => 3, 3 => 7, 7 => 14, 14 => 30, _ => 30,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
node.last_replayed = now_epoch();
|
n.last_replayed = now_epoch();
|
||||||
node.version += 1;
|
});
|
||||||
Some(node.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(node) = updated {
|
|
||||||
let _ = self.append_nodes(&[node]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_wrong(&mut self, key: &str, _ctx: Option<&str>) {
|
pub fn mark_wrong(&mut self, key: &str, _ctx: Option<&str>) {
|
||||||
let updated = if let Some(node) = self.nodes.get_mut(key) {
|
let _ = self.modify_node(key, |n| {
|
||||||
node.wrongs += 1;
|
n.wrongs += 1;
|
||||||
node.weight = (node.weight - 0.1).max(0.0);
|
n.weight = (n.weight - 0.1).max(0.0);
|
||||||
// Reset spaced repetition interval — needs review
|
n.spaced_repetition_interval = 1;
|
||||||
node.spaced_repetition_interval = 1;
|
});
|
||||||
node.version += 1;
|
|
||||||
Some(node.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(node) = updated {
|
|
||||||
let _ = self.append_nodes(&[node]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_gap(&mut self, desc: &str) {
|
pub fn record_gap(&mut self, desc: &str) {
|
||||||
|
|
@ -1386,20 +1300,7 @@ impl Store {
|
||||||
pub fn categorize(&mut self, key: &str, cat_str: &str) -> Result<(), String> {
|
pub fn categorize(&mut self, key: &str, cat_str: &str) -> Result<(), String> {
|
||||||
let cat = Category::from_str(cat_str)
|
let cat = Category::from_str(cat_str)
|
||||||
.ok_or_else(|| format!("Unknown category '{}'. Use: core/tech/gen/obs/task", cat_str))?;
|
.ok_or_else(|| format!("Unknown category '{}'. Use: core/tech/gen/obs/task", cat_str))?;
|
||||||
let updated = if let Some(node) = self.nodes.get_mut(key) {
|
self.modify_node(key, |n| { n.category = cat; })
|
||||||
node.category = cat;
|
|
||||||
node.version += 1;
|
|
||||||
Some(node.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(node) = updated {
|
|
||||||
// Persist to capnp log so category survives cache rebuilds
|
|
||||||
self.append_nodes(&[node])?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!("No node '{}'", key))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decay(&mut self) -> (usize, usize) {
|
pub fn decay(&mut self) -> (usize, usize) {
|
||||||
|
|
@ -1556,7 +1457,7 @@ impl Store {
|
||||||
let excess = active.len() - max_degree;
|
let excess = active.len() - max_degree;
|
||||||
|
|
||||||
// Sort Auto by strength ascending
|
// Sort Auto by strength ascending
|
||||||
auto_indices.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
auto_indices.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||||
let auto_prune = excess.min(auto_indices.len());
|
let auto_prune = excess.min(auto_indices.len());
|
||||||
for &(i, _) in auto_indices.iter().take(auto_prune) {
|
for &(i, _) in auto_indices.iter().take(auto_prune) {
|
||||||
to_delete.insert(i);
|
to_delete.insert(i);
|
||||||
|
|
@ -1615,43 +1516,30 @@ impl Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a markdown file into the store, parsing it into nodes.
|
/// Process parsed memory units: diff against existing nodes, persist changes.
|
||||||
/// Returns (new_count, updated_count).
|
/// Returns (new_count, updated_count).
|
||||||
pub fn import_file(&mut self, path: &Path) -> Result<(usize, usize), String> {
|
fn ingest_units(&mut self, units: &[MemoryUnit], filename: &str) -> Result<(usize, usize), String> {
|
||||||
let filename = path.file_name().unwrap().to_string_lossy().to_string();
|
let node_type = classify_filename(filename);
|
||||||
let content = fs::read_to_string(path)
|
|
||||||
.map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
||||||
|
|
||||||
let units = parse_units(&filename, &content);
|
|
||||||
let mut new_nodes = Vec::new();
|
let mut new_nodes = Vec::new();
|
||||||
let mut updated_nodes = Vec::new();
|
let mut updated_nodes = Vec::new();
|
||||||
|
|
||||||
let node_type = if filename.starts_with("daily-") {
|
|
||||||
NodeType::EpisodicDaily
|
|
||||||
} else if filename.starts_with("weekly-") {
|
|
||||||
NodeType::EpisodicWeekly
|
|
||||||
} else if filename == "journal.md" {
|
|
||||||
NodeType::EpisodicSession
|
|
||||||
} else {
|
|
||||||
NodeType::Semantic
|
|
||||||
};
|
|
||||||
|
|
||||||
for (pos, unit) in units.iter().enumerate() {
|
for (pos, unit) in units.iter().enumerate() {
|
||||||
if let Some(existing) = self.nodes.get(&unit.key) {
|
if let Some(existing) = self.nodes.get(&unit.key) {
|
||||||
let pos_changed = existing.position != pos as u32;
|
if existing.content != unit.content || existing.position != pos as u32 {
|
||||||
if existing.content != unit.content || pos_changed {
|
|
||||||
let mut node = existing.clone();
|
let mut node = existing.clone();
|
||||||
node.content = unit.content.clone();
|
node.content = unit.content.clone();
|
||||||
node.position = pos as u32;
|
node.position = pos as u32;
|
||||||
node.version += 1;
|
node.version += 1;
|
||||||
println!(" U {}", unit.key);
|
if let Some(ref s) = unit.state { node.state_tag = s.clone(); }
|
||||||
|
if let Some(ref s) = unit.source_ref { node.source_ref = s.clone(); }
|
||||||
updated_nodes.push(node);
|
updated_nodes.push(node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut node = Store::new_node(&unit.key, &unit.content);
|
let mut node = Store::new_node(&unit.key, &unit.content);
|
||||||
node.node_type = node_type;
|
node.node_type = node_type;
|
||||||
node.position = pos as u32;
|
node.position = pos as u32;
|
||||||
println!(" + {}", unit.key);
|
if let Some(ref s) = unit.state { node.state_tag = s.clone(); }
|
||||||
|
if let Some(ref s) = unit.source_ref { node.source_ref = s.clone(); }
|
||||||
new_nodes.push(node);
|
new_nodes.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1673,6 +1561,16 @@ impl Store {
|
||||||
Ok((new_nodes.len(), updated_nodes.len()))
|
Ok((new_nodes.len(), updated_nodes.len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import a markdown file into the store, parsing it into nodes.
|
||||||
|
/// Returns (new_count, updated_count).
|
||||||
|
pub fn import_file(&mut self, path: &Path) -> Result<(usize, usize), String> {
|
||||||
|
let filename = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.map_err(|e| format!("read {}: {}", path.display(), e))?;
|
||||||
|
let units = parse_units(&filename, &content);
|
||||||
|
self.ingest_units(&units, &filename)
|
||||||
|
}
|
||||||
|
|
||||||
/// Gather all sections for a file key, sorted by position.
|
/// Gather all sections for a file key, sorted by position.
|
||||||
/// Returns None if no nodes found.
|
/// Returns None if no nodes found.
|
||||||
pub fn file_sections(&self, file_key: &str) -> Option<Vec<&Node>> {
|
pub fn file_sections(&self, file_key: &str) -> Option<Vec<&Node>> {
|
||||||
|
|
@ -1777,6 +1675,13 @@ impl Store {
|
||||||
|
|
||||||
// Markdown parsing — same as old system but returns structured units
|
// Markdown parsing — same as old system but returns structured units
|
||||||
|
|
||||||
|
fn classify_filename(filename: &str) -> NodeType {
|
||||||
|
if filename.starts_with("daily-") { NodeType::EpisodicDaily }
|
||||||
|
else if filename.starts_with("weekly-") { NodeType::EpisodicWeekly }
|
||||||
|
else if filename == "journal.md" { NodeType::EpisodicSession }
|
||||||
|
else { NodeType::Semantic }
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MemoryUnit {
|
pub struct MemoryUnit {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue