journal-tail: add --level= for digest hierarchy
Support viewing daily, weekly, and monthly digests through the same journal-tail interface: poc-memory journal-tail --level=daily 3 poc-memory journal-tail --level=weekly --full poc-memory journal-tail --level=2 1 Levels: 0/journal (default), 1/daily, 2/weekly, 3/monthly. Accepts both names and integer indices. Refactored title extraction into shared extract_title() and split the journal vs digest display paths for clarity.
This commit is contained in:
parent
9e52fd5b95
commit
a77609c025
1 changed files with 73 additions and 36 deletions
109
src/main.rs
109
src/main.rs
|
|
@ -195,7 +195,9 @@ Commands:
|
|||
import FILE [FILE...] Import markdown file(s) into the store
|
||||
export [FILE|--all] Export store nodes to markdown file(s)
|
||||
journal-write TEXT Write a journal entry to the store
|
||||
journal-tail [N] [--full] Show last N journal entries (default 20, --full for content)
|
||||
journal-tail [N] [--level=L] [--full]
|
||||
Show last N entries (default 20, --full for content)
|
||||
--level: 0/journal, 1/daily, 2/weekly, 3/monthly
|
||||
query 'EXPR | stages' Query the memory graph
|
||||
Stages: sort F [asc], limit N, select F,F, count
|
||||
Ex: \"degree > 15 | sort degree | limit 10\"
|
||||
|
|
@ -1696,9 +1698,19 @@ fn cmd_journal_write(args: &[String]) -> Result<(), String> {
|
|||
fn cmd_journal_tail(args: &[String]) -> Result<(), String> {
|
||||
let mut n: usize = 20;
|
||||
let mut full = false;
|
||||
let mut level: u8 = 0; // 0=journal, 1=daily, 2=weekly, 3=monthly
|
||||
|
||||
for arg in args {
|
||||
if arg == "--full" || arg == "-f" {
|
||||
full = true;
|
||||
} else if let Some(val) = arg.strip_prefix("--level=") {
|
||||
level = match val {
|
||||
"0" | "journal" => 0,
|
||||
"1" | "daily" => 1,
|
||||
"2" | "weekly" => 2,
|
||||
"3" | "monthly" => 3,
|
||||
_ => return Err(format!("unknown level '{}': use 0-3 or journal/daily/weekly/monthly", val)),
|
||||
};
|
||||
} else if let Ok(num) = arg.parse::<usize>() {
|
||||
n = num;
|
||||
}
|
||||
|
|
@ -1706,8 +1718,20 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> {
|
|||
|
||||
let store = store::Store::load()?;
|
||||
|
||||
// Collect journal nodes (EpisodicSession), sorted by created_at.
|
||||
// Legacy nodes (created_at == 0) fall back to key/content parsing.
|
||||
if level == 0 {
|
||||
// Original journal-tail behavior
|
||||
journal_tail_entries(&store, n, full)
|
||||
} else {
|
||||
let prefix = match level {
|
||||
1 => "daily-",
|
||||
2 => "weekly-",
|
||||
_ => "monthly-",
|
||||
};
|
||||
journal_tail_digests(&store, prefix, n, full)
|
||||
}
|
||||
}
|
||||
|
||||
fn journal_tail_entries(store: &store::Store, n: usize, full: bool) -> Result<(), String> {
|
||||
let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap();
|
||||
let key_date_re = regex::Regex::new(r"j-(\d{4}-\d{2}-\d{2}[t-]\d{2}-\d{2})").unwrap();
|
||||
|
||||
|
|
@ -1720,13 +1744,10 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> {
|
|||
}
|
||||
};
|
||||
|
||||
// Returns (sort_key, display_string) for a journal node.
|
||||
// Prefer created_at (stable, rename-safe); fall back to key/content.
|
||||
let extract_sort = |node: &store::Node| -> (i64, String) {
|
||||
if node.created_at > 0 {
|
||||
return (node.created_at, store::format_datetime(node.created_at));
|
||||
}
|
||||
// Legacy: parse from key or content
|
||||
if let Some(caps) = key_date_re.captures(&node.key) {
|
||||
return (0, normalize_date(&caps[1]));
|
||||
}
|
||||
|
|
@ -1749,48 +1770,64 @@ fn cmd_journal_tail(args: &[String]) -> Result<(), String> {
|
|||
}
|
||||
});
|
||||
|
||||
// Show last N — each entry: [timestamp] ## Title
|
||||
let skip = if journal.len() > n { journal.len() - n } else { 0 };
|
||||
for node in journal.iter().skip(skip) {
|
||||
let (_, ts) = extract_sort(node);
|
||||
// Find a meaningful title: first ## header, or first non-date non-empty line
|
||||
let mut title = String::new();
|
||||
for line in node.content.lines() {
|
||||
let stripped = line.trim();
|
||||
if stripped.is_empty() { continue; }
|
||||
// Skip date-only lines like "## 2026-03-01T01:22"
|
||||
if date_re.is_match(stripped) && stripped.len() < 25 { continue; }
|
||||
if stripped.starts_with("## ") {
|
||||
title = stripped[3..].to_string();
|
||||
break;
|
||||
} else if stripped.starts_with("# ") {
|
||||
title = stripped[2..].to_string();
|
||||
break;
|
||||
} else {
|
||||
// Use first content line, truncated
|
||||
title = if stripped.len() > 70 {
|
||||
let mut end = 67;
|
||||
while !stripped.is_char_boundary(end) { end -= 1; }
|
||||
format!("{}...", &stripped[..end])
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if title.is_empty() {
|
||||
title = node.key.clone();
|
||||
}
|
||||
let title = extract_title(&node.content);
|
||||
if full {
|
||||
println!("--- [{}] {} ---\n{}\n", ts, title, node.content);
|
||||
} else {
|
||||
println!("[{}] {}", ts, title);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn journal_tail_digests(store: &store::Store, prefix: &str, n: usize, full: bool) -> Result<(), String> {
|
||||
let mut digests: Vec<_> = store.nodes.values()
|
||||
.filter(|node| node.key.starts_with(prefix))
|
||||
.collect();
|
||||
// Sort by key — the date/week label sorts lexicographically
|
||||
digests.sort_by(|a, b| 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)
|
||||
.and_then(|s| s.strip_suffix(".md"))
|
||||
.unwrap_or(&node.key);
|
||||
let title = extract_title(&node.content);
|
||||
if full {
|
||||
println!("--- [{}] {} ---\n{}\n", label, title, node.content);
|
||||
} else {
|
||||
println!("[{}] {}", label, title);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_title(content: &str) -> String {
|
||||
let date_re = regex::Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})").unwrap();
|
||||
for line in content.lines() {
|
||||
let stripped = line.trim();
|
||||
if stripped.is_empty() { continue; }
|
||||
if date_re.is_match(stripped) && stripped.len() < 25 { continue; }
|
||||
if stripped.starts_with("## ") {
|
||||
return stripped[3..].to_string();
|
||||
} else if stripped.starts_with("# ") {
|
||||
return stripped[2..].to_string();
|
||||
} else {
|
||||
return if stripped.len() > 70 {
|
||||
let mut end = 67;
|
||||
while !stripped.is_char_boundary(end) { end -= 1; }
|
||||
format!("{}...", &stripped[..end])
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
}
|
||||
}
|
||||
String::from("(untitled)")
|
||||
}
|
||||
|
||||
fn cmd_interference(args: &[String]) -> Result<(), String> {
|
||||
let mut threshold = 0.4f32;
|
||||
let mut i = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue