diff --git a/src/digest.rs b/src/digest.rs index c1ab3ce..9245f3d 100644 --- a/src/digest.rs +++ b/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, String> { let dir = memory_subdir("episodic")?; @@ -209,6 +192,97 @@ fn load_child_digests(prefix: &str, labels: &[String]) -> Result Vec { + 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 = 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 { // --- 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 = 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 = 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::>() + .into_iter() + .collect(); + + let mut total = 0u32; + + 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; } + if inputs.is_empty() { continue; } + println!("[auto] Missing {} digest for {}", level.name, label); + generate_digest(store, level, &label, &inputs)?; + generated += 1; } + + println!("[auto] {}: {} generated, {} existed", level.name, generated, skipped); + total += generated; } - let mut daily_done: Vec = Vec::new(); - let mut stats = [0u32; 6]; // [daily_gen, daily_skip, weekly_gen, weekly_skip, monthly_gen, monthly_skip] - - for date in &dates { - if date == &today { continue; } - if epi.join(format!("daily-{}.md", date)).exists() { - stats[1] += 1; - daily_done.push(date.clone()); - 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> = BTreeMap::new(); - for date in &daily_done { - if let Ok((wl, _)) = week_dates(date) { - weeks.entry(wl).or_default().push(date.clone()); - } - } - - let mut weekly_done: Vec = 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())); - } - } - - 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 {