From 804578b9774cd9c711d5ac8a8ffcc41437e5d096 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 8 Mar 2026 20:14:37 -0400 Subject: [PATCH] query by NodeType instead of key prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace key prefix matching (journal#j-, daily-, weekly-, monthly-) with NodeType filters (EpisodicSession, EpisodicDaily, EpisodicWeekly, EpisodicMonthly) for all queries: journal-tail, digest gathering, digest auto-detection, experience mining dedup, and find_journal_node. Add EpisodicMonthly to NodeType enum and capnp schema. Key naming conventions (journal#j-TIMESTAMP-slug, daily-DATE, etc.) are retained for key generation — the fix is about how we find nodes, not how we name them. Co-Authored-By: ProofOfConcept --- schema/memory.capnp | 1 + src/digest.rs | 34 +++++++++++++++------------------- src/enrich.rs | 16 +++------------- src/main.rs | 30 ++++++++++++++++++------------ src/query.rs | 1 + src/store/mod.rs | 4 ++-- src/store/parse.rs | 1 + src/store/types.rs | 3 ++- 8 files changed, 43 insertions(+), 47 deletions(-) diff --git a/schema/memory.capnp b/schema/memory.capnp index e6cdffd..4111769 100644 --- a/schema/memory.capnp +++ b/schema/memory.capnp @@ -45,6 +45,7 @@ enum NodeType { episodicDaily @1; episodicWeekly @2; semantic @3; + episodicMonthly @4; } enum Provenance { diff --git a/src/digest.rs b/src/digest.rs index 7761257..161d09b 100644 --- a/src/digest.rs +++ b/src/digest.rs @@ -145,15 +145,13 @@ fn gather(level: &DigestLevel, store: &Store, arg: &str) -> Result<(String, Vec< .collect(); load_child_digests(store, child_name, &child_labels) } else { - // Leaf level: scan store for journal entries matching label - let date_re = Regex::new(&format!( - r"^journal#j-{}", regex::escape(&label) - )).unwrap(); + // Leaf level: scan store for episodic entries matching date let mut entries: Vec<_> = store.nodes.values() - .filter(|n| date_re.is_match(&n.key)) + .filter(|n| n.node_type == store::NodeType::EpisodicSession + && n.timestamp > 0 + && store::format_date(n.timestamp) == label) .map(|n| { - let ts = n.key.strip_prefix("journal#j-").unwrap_or(&n.key); - (ts.to_string(), n.content.clone()) + (store::format_datetime(n.timestamp), n.content.clone()) }) .collect(); entries.sort_by(|a, b| a.0.cmp(&b.0)); @@ -252,14 +250,10 @@ pub fn generate(store: &mut Store, level_name: &str, arg: &str) -> Result<(), St pub fn digest_auto(store: &mut Store) -> Result<(), String> { let today = Local::now().format("%Y-%m-%d").to_string(); - // Collect all dates with journal entries - let date_re = Regex::new(r"^\d{4}-\d{2}-\d{2}").unwrap(); - let dates: Vec = store.nodes.keys() - .filter_map(|key| { - key.strip_prefix("journal#j-") - .filter(|rest| rest.len() >= 10 && date_re.is_match(rest)) - .map(|rest| rest[..10].to_string()) - }) + // Collect all dates with episodic entries + let dates: Vec = store.nodes.values() + .filter(|n| n.node_type == store::NodeType::EpisodicSession && n.timestamp > 0) + .map(|n| store::format_date(n.timestamp)) .collect::>() .into_iter() .collect(); @@ -408,10 +402,12 @@ fn parse_digest_node_links(key: &str, content: &str) -> Vec { pub fn parse_all_digest_links(store: &Store) -> Vec { let mut all_links = Vec::new(); - let mut digest_keys: Vec<&String> = store.nodes.keys() - .filter(|k| k.starts_with("daily-") - || k.starts_with("weekly-") - || k.starts_with("monthly-")) + let mut digest_keys: Vec<&String> = store.nodes.iter() + .filter(|(_, n)| matches!(n.node_type, + store::NodeType::EpisodicDaily + | store::NodeType::EpisodicWeekly + | store::NodeType::EpisodicMonthly)) + .map(|(k, _)| k) .collect(); digest_keys.sort(); diff --git a/src/enrich.rs b/src/enrich.rs index d63ae42..59ddea4 100644 --- a/src/enrich.rs +++ b/src/enrich.rs @@ -336,21 +336,11 @@ pub fn experience_mine( .map(|n| n.content.clone()) .unwrap_or_default(); - // Get recent journal entries to avoid duplication - let key_date_re = Regex::new(r"^journal#j-(\d{4}-\d{2}-\d{2}[t-]\d{2}-\d{2})").unwrap(); - let date_re = Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap(); + // Get recent episodic entries to avoid duplication let mut journal: Vec<_> = store.nodes.values() - .filter(|node| node.key.starts_with("journal#j-")) + .filter(|node| matches!(node.node_type, store::NodeType::EpisodicSession)) .collect(); - journal.sort_by(|a, b| { - let ak = key_date_re.captures(&a.key).map(|c| c[1].to_string()) - .or_else(|| date_re.captures(&a.content).map(|c| c[1].to_string())) - .unwrap_or_default(); - let bk = key_date_re.captures(&b.key).map(|c| c[1].to_string()) - .or_else(|| date_re.captures(&b.content).map(|c| c[1].to_string())) - .unwrap_or_default(); - ak.cmp(&bk) - }); + journal.sort_by_key(|n| n.timestamp); let recent: String = journal.iter().rev().take(10) .map(|n| format!("---\n{}\n", n.content)) .collect(); diff --git a/src/main.rs b/src/main.rs index 4a1f88f..b829609 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1122,7 +1122,8 @@ fn cmd_trace(args: &[String]) -> Result<(), String> { episodic_session.push(entry), store::NodeType::EpisodicDaily => episodic_daily.push(entry), - store::NodeType::EpisodicWeekly => + store::NodeType::EpisodicWeekly + | store::NodeType::EpisodicMonthly => episodic_weekly.push(entry), store::NodeType::Semantic => semantic.push(entry), @@ -1855,12 +1856,12 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> { // Original journal-tail behavior journal_tail_entries(&store, n, full) } else { - let prefix = match level { - 1 => "daily-", - 2 => "weekly-", - _ => "monthly-", + let node_type = match level { + 1 => store::NodeType::EpisodicDaily, + 2 => store::NodeType::EpisodicWeekly, + _ => store::NodeType::EpisodicMonthly, }; - journal_tail_digests(&store, prefix, n, full) + journal_tail_digests(&store, node_type, n, full) } } @@ -1916,17 +1917,22 @@ fn journal_tail_entries(store: &store::Store, n: usize, full: bool) -> Result<() Ok(()) } -fn journal_tail_digests(store: &store::Store, prefix: &str, n: usize, full: bool) -> Result<(), String> { +fn journal_tail_digests(store: &store::Store, node_type: store::NodeType, n: usize, full: bool) -> Result<(), String> { let mut digests: Vec<_> = store.nodes.values() - .filter(|node| node.key.starts_with(prefix)) + .filter(|node| node.node_type == node_type) .collect(); - // Sort by key — the date/week label sorts lexicographically - digests.sort_by(|a, b| a.key.cmp(&b.key)); + // Sort by timestamp, fall back to key for lexicographic ordering + digests.sort_by(|a, b| { + if a.timestamp > 0 && b.timestamp > 0 { + a.timestamp.cmp(&b.timestamp) + } else { + a.key.cmp(&b.key) + } + }); let skip = if digests.len() > n { digests.len() - n } else { 0 }; for node in digests.iter().skip(skip) { - let label = node.key.strip_prefix(prefix) - .unwrap_or(&node.key); + let label = &node.key; let title = extract_title(&node.content); if full { println!("--- [{}] {} ---\n{}\n", label, title, node.content); diff --git a/src/query.rs b/src/query.rs index 4053663..4db36e6 100644 --- a/src/query.rs +++ b/src/query.rs @@ -196,6 +196,7 @@ fn node_type_label(nt: NodeType) -> &'static str { NodeType::EpisodicSession => "episodic_session", NodeType::EpisodicDaily => "episodic_daily", NodeType::EpisodicWeekly => "episodic_weekly", + NodeType::EpisodicMonthly => "episodic_monthly", NodeType::Semantic => "semantic", } } diff --git a/src/store/mod.rs b/src/store/mod.rs index 80a8e61..a49a2f6 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -293,7 +293,7 @@ impl Store { Some(output.trim_end().to_string()) } - /// Find the journal node that best matches the given entry text. + /// Find the episodic node that best matches the given entry text. pub fn find_journal_node(&self, entry_text: &str) -> Option { if entry_text.is_empty() { return None; @@ -308,7 +308,7 @@ impl Store { let mut best_score = 0; for (key, node) in &self.nodes { - if !key.starts_with("journal#") { + if node.node_type != NodeType::EpisodicSession { continue; } let content_lower = node.content.to_lowercase(); diff --git a/src/store/parse.rs b/src/store/parse.rs index 1f0820e..d3310ea 100644 --- a/src/store/parse.rs +++ b/src/store/parse.rs @@ -27,6 +27,7 @@ pub fn classify_filename(filename: &str) -> NodeType { let bare = filename.strip_suffix(".md").unwrap_or(filename); if bare.starts_with("daily-") { NodeType::EpisodicDaily } else if bare.starts_with("weekly-") { NodeType::EpisodicWeekly } + else if bare.starts_with("monthly-") { NodeType::EpisodicMonthly } else if bare == "journal" { NodeType::EpisodicSession } else { NodeType::Semantic } } diff --git a/src/store/types.rs b/src/store/types.rs index 5a73ac6..7071ef8 100644 --- a/src/store/types.rs +++ b/src/store/types.rs @@ -226,6 +226,7 @@ pub enum NodeType { EpisodicDaily, EpisodicWeekly, Semantic, + EpisodicMonthly, } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] @@ -345,7 +346,7 @@ pub enum RelationType { } capnp_enum!(NodeType, memory_capnp::NodeType, - [EpisodicSession, EpisodicDaily, EpisodicWeekly, Semantic]); + [EpisodicSession, EpisodicDaily, EpisodicWeekly, Semantic, EpisodicMonthly]); capnp_enum!(Provenance, memory_capnp::Provenance, [Manual, Journal, Agent, Dream, Derived,