digest: add gather/find_args methods, collapse digest_auto to loop
DigestLevel gains two methods: - gather(): returns (label, inputs) for a given arg — daily reads journal entries, weekly/monthly compute child labels and load files - find_args(): returns candidate args from journal dates for auto- detection, handling per-level completeness checks Public generate_daily/weekly/monthly become two-liners: gather + generate. digest_auto collapses from three near-identical phases into a single loop over LEVELS. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
796c72fb25
commit
b083cc433c
1 changed files with 129 additions and 113 deletions
234
src/digest.rs
234
src/digest.rs
|
|
@ -179,23 +179,6 @@ Read all the weekly digests and synthesize the month's story.
|
|||
|
||||
// --- Input gathering ---
|
||||
|
||||
/// Collect journal entries for a given date from the store.
|
||||
fn daily_inputs(store: &Store, date: &str) -> Vec<(String, String)> {
|
||||
let date_re = Regex::new(&format!(
|
||||
r"^journal\.md#j-{}", regex::escape(date)
|
||||
)).unwrap();
|
||||
|
||||
let mut entries: Vec<_> = store.nodes.values()
|
||||
.filter(|n| date_re.is_match(&n.key))
|
||||
.map(|n| {
|
||||
let label = n.key.strip_prefix("journal.md#j-").unwrap_or(&n.key);
|
||||
(label.to_string(), n.content.clone())
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
entries
|
||||
}
|
||||
|
||||
/// Load child digest files from the episodic directory.
|
||||
fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, String)>, String> {
|
||||
let dir = memory_subdir("episodic")?;
|
||||
|
|
@ -209,6 +192,97 @@ fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, St
|
|||
Ok(digests)
|
||||
}
|
||||
|
||||
impl DigestLevel {
|
||||
/// Find candidate args from journal dates for auto-detection.
|
||||
/// Returns args suitable for passing to gather().
|
||||
fn find_args(&self, dates: &[String], today: &str) -> Vec<String> {
|
||||
match self.child_prefix {
|
||||
None => {
|
||||
// Daily: each date is a candidate, skip today
|
||||
dates.iter()
|
||||
.filter(|d| d.as_str() != today)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
Some("daily") => {
|
||||
// Weekly: group dates by week, return one date per complete week
|
||||
let mut weeks: BTreeMap<String, String> = BTreeMap::new();
|
||||
for date in dates {
|
||||
if let Ok((wl, _)) = week_dates(date) {
|
||||
weeks.entry(wl).or_insert_with(|| date.clone());
|
||||
}
|
||||
}
|
||||
weeks.into_values()
|
||||
.filter(|date| {
|
||||
week_dates(date).map_or(false, |(_, days)|
|
||||
days.last().unwrap() < &today.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Some(_) => {
|
||||
// Monthly: group dates by month, return labels for past months
|
||||
let now = Local::now();
|
||||
let cur = (now.year(), now.month());
|
||||
let mut months: BTreeSet<(i32, u32)> = BTreeSet::new();
|
||||
for date in dates {
|
||||
if let Ok(nd) = NaiveDate::parse_from_str(date, "%Y-%m-%d") {
|
||||
months.insert((nd.year(), nd.month()));
|
||||
}
|
||||
}
|
||||
months.into_iter()
|
||||
.filter(|ym| *ym < cur)
|
||||
.map(|(y, m)| format!("{}-{:02}", y, m))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gather inputs for this digest level. Returns (label, inputs).
|
||||
/// For daily: arg is a date, gathers journal entries from store.
|
||||
/// For weekly: arg is any date in the week, computes week label.
|
||||
/// For monthly: arg is "YYYY-MM" (or empty for current month).
|
||||
fn gather(&self, store: &Store, arg: &str) -> Result<(String, Vec<(String, String)>), String> {
|
||||
match self.child_prefix {
|
||||
None => {
|
||||
// Daily: gather journal entries for this date
|
||||
let date_re = Regex::new(&format!(
|
||||
r"^journal\.md#j-{}", regex::escape(arg)
|
||||
)).unwrap();
|
||||
let mut entries: Vec<_> = store.nodes.values()
|
||||
.filter(|n| date_re.is_match(&n.key))
|
||||
.map(|n| {
|
||||
let label = n.key.strip_prefix("journal.md#j-").unwrap_or(&n.key);
|
||||
(label.to_string(), n.content.clone())
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok((arg.to_string(), entries))
|
||||
}
|
||||
Some("daily") => {
|
||||
// Weekly: compute week from date, load daily digests
|
||||
let (week_label, dates) = week_dates(arg)?;
|
||||
let inputs = load_child_digests("daily", &dates)?;
|
||||
Ok((week_label, inputs))
|
||||
}
|
||||
Some(prefix) => {
|
||||
// Monthly: parse month arg, load weekly digests
|
||||
let (year, month) = if arg.is_empty() {
|
||||
let now = Local::now();
|
||||
(now.year(), now.month())
|
||||
} else {
|
||||
let d = NaiveDate::parse_from_str(&format!("{}-01", arg), "%Y-%m-%d")
|
||||
.map_err(|e| format!("bad month '{}': {} (expected YYYY-MM)", arg, e))?;
|
||||
(d.year(), d.month())
|
||||
};
|
||||
let label = format!("{}-{:02}", year, month);
|
||||
let child_labels = weeks_in_month(year, month);
|
||||
let inputs = load_child_digests(prefix, &child_labels)?;
|
||||
Ok((label, inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Unified generator ---
|
||||
|
||||
fn format_inputs(inputs: &[(String, String)], daily: bool) -> String {
|
||||
|
|
@ -280,28 +354,17 @@ fn generate_digest(
|
|||
// --- Public API ---
|
||||
|
||||
pub fn generate_daily(store: &mut Store, date: &str) -> Result<(), String> {
|
||||
let inputs = daily_inputs(store, date);
|
||||
generate_digest(store, &DAILY, date, &inputs)
|
||||
let (label, inputs) = DAILY.gather(store, date)?;
|
||||
generate_digest(store, &DAILY, &label, &inputs)
|
||||
}
|
||||
|
||||
pub fn generate_weekly(store: &mut Store, date: &str) -> Result<(), String> {
|
||||
let (week_label, dates) = week_dates(date)?;
|
||||
let inputs = load_child_digests("daily", &dates)?;
|
||||
generate_digest(store, &WEEKLY, &week_label, &inputs)
|
||||
let (label, inputs) = WEEKLY.gather(store, date)?;
|
||||
generate_digest(store, &WEEKLY, &label, &inputs)
|
||||
}
|
||||
|
||||
pub fn generate_monthly(store: &mut Store, month_arg: &str) -> Result<(), String> {
|
||||
let (year, month) = if month_arg.is_empty() {
|
||||
let now = Local::now();
|
||||
(now.year(), now.month())
|
||||
} else {
|
||||
let d = NaiveDate::parse_from_str(&format!("{}-01", month_arg), "%Y-%m-%d")
|
||||
.map_err(|e| format!("bad month '{}': {} (expected YYYY-MM)", month_arg, e))?;
|
||||
(d.year(), d.month())
|
||||
};
|
||||
let label = format!("{}-{:02}", year, month);
|
||||
let week_labels = weeks_in_month(year, month);
|
||||
let inputs = load_child_digests("weekly", &week_labels)?;
|
||||
let (label, inputs) = MONTHLY.gather(store, month_arg)?;
|
||||
generate_digest(store, &MONTHLY, &label, &inputs)
|
||||
}
|
||||
|
||||
|
|
@ -334,94 +397,47 @@ fn weeks_in_month(year: i32, month: u32) -> Vec<String> {
|
|||
|
||||
// --- Auto-detect and generate missing digests ---
|
||||
|
||||
const LEVELS: &[&DigestLevel] = &[&DAILY, &WEEKLY, &MONTHLY];
|
||||
|
||||
pub fn digest_auto(store: &mut Store) -> Result<(), String> {
|
||||
let now = Local::now();
|
||||
let today = now.format("%Y-%m-%d").to_string();
|
||||
let today = Local::now().format("%Y-%m-%d").to_string();
|
||||
let epi = memory_subdir("episodic")?;
|
||||
|
||||
// Phase 1: daily — find dates with journal entries but no digest
|
||||
// Collect all dates with journal entries
|
||||
let date_re = Regex::new(r"^\d{4}-\d{2}-\d{2}").unwrap();
|
||||
let mut dates: BTreeSet<String> = BTreeSet::new();
|
||||
for key in store.nodes.keys() {
|
||||
if let Some(rest) = key.strip_prefix("journal.md#j-") {
|
||||
if rest.len() >= 10 && date_re.is_match(rest) {
|
||||
dates.insert(rest[..10].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let dates: Vec<String> = store.nodes.keys()
|
||||
.filter_map(|key| {
|
||||
key.strip_prefix("journal.md#j-")
|
||||
.filter(|rest| rest.len() >= 10 && date_re.is_match(rest))
|
||||
.map(|rest| rest[..10].to_string())
|
||||
})
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut daily_done: Vec<String> = Vec::new();
|
||||
let mut stats = [0u32; 6]; // [daily_gen, daily_skip, weekly_gen, weekly_skip, monthly_gen, monthly_skip]
|
||||
let mut total = 0u32;
|
||||
|
||||
for date in &dates {
|
||||
if date == &today { continue; }
|
||||
if epi.join(format!("daily-{}.md", date)).exists() {
|
||||
stats[1] += 1;
|
||||
daily_done.push(date.clone());
|
||||
for level in LEVELS {
|
||||
let args = level.find_args(&dates, &today);
|
||||
let mut generated = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
|
||||
for arg in &args {
|
||||
let (label, inputs) = level.gather(store, arg)?;
|
||||
if epi.join(format!("{}-{}.md", level.name, label)).exists() {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
println!("[auto] Missing daily digest for {}", date);
|
||||
generate_daily(store, date)?;
|
||||
stats[0] += 1;
|
||||
daily_done.push(date.clone());
|
||||
}
|
||||
println!("[auto] Daily: {} generated, {} existed", stats[0], stats[1]);
|
||||
|
||||
// Phase 2: weekly — group dates into weeks, generate if week is complete
|
||||
let mut weeks: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
for date in &daily_done {
|
||||
if let Ok((wl, _)) = week_dates(date) {
|
||||
weeks.entry(wl).or_default().push(date.clone());
|
||||
}
|
||||
if inputs.is_empty() { continue; }
|
||||
println!("[auto] Missing {} digest for {}", level.name, label);
|
||||
generate_digest(store, level, &label, &inputs)?;
|
||||
generated += 1;
|
||||
}
|
||||
|
||||
let mut weekly_done: Vec<String> = Vec::new();
|
||||
for (week_label, example_dates) in &weeks {
|
||||
if let Ok((_, days)) = week_dates(example_dates.first().unwrap()) {
|
||||
if days.last().unwrap() >= &today { continue; }
|
||||
}
|
||||
if epi.join(format!("weekly-{}.md", week_label)).exists() {
|
||||
stats[3] += 1;
|
||||
weekly_done.push(week_label.clone());
|
||||
continue;
|
||||
}
|
||||
if !example_dates.iter().any(|d| epi.join(format!("daily-{}.md", d)).exists()) {
|
||||
continue;
|
||||
}
|
||||
println!("[auto] Missing weekly digest for {}", week_label);
|
||||
generate_weekly(store, example_dates.first().unwrap())?;
|
||||
stats[2] += 1;
|
||||
weekly_done.push(week_label.clone());
|
||||
}
|
||||
println!("[auto] Weekly: {} generated, {} existed", stats[2], stats[3]);
|
||||
|
||||
// Phase 3: monthly — group dates into months, generate if month is past
|
||||
let cur_month = (now.year(), now.month());
|
||||
let mut months: BTreeSet<(i32, u32)> = BTreeSet::new();
|
||||
for date in &daily_done {
|
||||
if let Ok(nd) = NaiveDate::parse_from_str(date, "%Y-%m-%d") {
|
||||
months.insert((nd.year(), nd.month()));
|
||||
}
|
||||
println!("[auto] {}: {} generated, {} existed", level.name, generated, skipped);
|
||||
total += generated;
|
||||
}
|
||||
|
||||
for (y, m) in &months {
|
||||
if (*y, *m) >= cur_month { continue; }
|
||||
let label = format!("{}-{:02}", y, m);
|
||||
if epi.join(format!("monthly-{}.md", label)).exists() {
|
||||
stats[5] += 1;
|
||||
continue;
|
||||
}
|
||||
let wl = weeks_in_month(*y, *m);
|
||||
if !wl.iter().any(|w| epi.join(format!("weekly-{}.md", w)).exists()) {
|
||||
continue;
|
||||
}
|
||||
println!("[auto] Missing monthly digest for {}", label);
|
||||
generate_monthly(store, &label)?;
|
||||
stats[4] += 1;
|
||||
}
|
||||
println!("[auto] Monthly: {} generated, {} existed", stats[4], stats[5]);
|
||||
|
||||
let total = stats[0] + stats[2] + stats[4];
|
||||
if total == 0 {
|
||||
println!("[auto] All digests up to date.");
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue