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 ---
|
// --- 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.
|
/// Load child digest files from the episodic directory.
|
||||||
fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, String)>, String> {
|
fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, String)>, String> {
|
||||||
let dir = memory_subdir("episodic")?;
|
let dir = memory_subdir("episodic")?;
|
||||||
|
|
@ -209,6 +192,97 @@ fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, St
|
||||||
Ok(digests)
|
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 ---
|
// --- Unified generator ---
|
||||||
|
|
||||||
fn format_inputs(inputs: &[(String, String)], daily: bool) -> String {
|
fn format_inputs(inputs: &[(String, String)], daily: bool) -> String {
|
||||||
|
|
@ -280,28 +354,17 @@ fn generate_digest(
|
||||||
// --- Public API ---
|
// --- Public API ---
|
||||||
|
|
||||||
pub fn generate_daily(store: &mut Store, date: &str) -> Result<(), String> {
|
pub fn generate_daily(store: &mut Store, date: &str) -> Result<(), String> {
|
||||||
let inputs = daily_inputs(store, date);
|
let (label, inputs) = DAILY.gather(store, date)?;
|
||||||
generate_digest(store, &DAILY, date, &inputs)
|
generate_digest(store, &DAILY, &label, &inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_weekly(store: &mut Store, date: &str) -> Result<(), String> {
|
pub fn generate_weekly(store: &mut Store, date: &str) -> Result<(), String> {
|
||||||
let (week_label, dates) = week_dates(date)?;
|
let (label, inputs) = WEEKLY.gather(store, date)?;
|
||||||
let inputs = load_child_digests("daily", &dates)?;
|
generate_digest(store, &WEEKLY, &label, &inputs)
|
||||||
generate_digest(store, &WEEKLY, &week_label, &inputs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_monthly(store: &mut Store, month_arg: &str) -> Result<(), String> {
|
pub fn generate_monthly(store: &mut Store, month_arg: &str) -> Result<(), String> {
|
||||||
let (year, month) = if month_arg.is_empty() {
|
let (label, inputs) = MONTHLY.gather(store, month_arg)?;
|
||||||
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)?;
|
|
||||||
generate_digest(store, &MONTHLY, &label, &inputs)
|
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 ---
|
// --- Auto-detect and generate missing digests ---
|
||||||
|
|
||||||
|
const LEVELS: &[&DigestLevel] = &[&DAILY, &WEEKLY, &MONTHLY];
|
||||||
|
|
||||||
pub fn digest_auto(store: &mut Store) -> Result<(), String> {
|
pub fn digest_auto(store: &mut Store) -> Result<(), String> {
|
||||||
let now = Local::now();
|
let today = Local::now().format("%Y-%m-%d").to_string();
|
||||||
let today = now.format("%Y-%m-%d").to_string();
|
|
||||||
let epi = memory_subdir("episodic")?;
|
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 date_re = Regex::new(r"^\d{4}-\d{2}-\d{2}").unwrap();
|
||||||
let mut dates: BTreeSet<String> = BTreeSet::new();
|
let dates: Vec<String> = store.nodes.keys()
|
||||||
for key in store.nodes.keys() {
|
.filter_map(|key| {
|
||||||
if let Some(rest) = key.strip_prefix("journal.md#j-") {
|
key.strip_prefix("journal.md#j-")
|
||||||
if rest.len() >= 10 && date_re.is_match(rest) {
|
.filter(|rest| rest.len() >= 10 && date_re.is_match(rest))
|
||||||
dates.insert(rest[..10].to_string());
|
.map(|rest| rest[..10].to_string())
|
||||||
}
|
})
|
||||||
}
|
.collect::<BTreeSet<_>>()
|
||||||
}
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut daily_done: Vec<String> = Vec::new();
|
let mut total = 0u32;
|
||||||
let mut stats = [0u32; 6]; // [daily_gen, daily_skip, weekly_gen, weekly_skip, monthly_gen, monthly_skip]
|
|
||||||
|
|
||||||
for date in &dates {
|
for level in LEVELS {
|
||||||
if date == &today { continue; }
|
let args = level.find_args(&dates, &today);
|
||||||
if epi.join(format!("daily-{}.md", date)).exists() {
|
let mut generated = 0u32;
|
||||||
stats[1] += 1;
|
let mut skipped = 0u32;
|
||||||
daily_done.push(date.clone());
|
|
||||||
|
for arg in &args {
|
||||||
|
let (label, inputs) = level.gather(store, arg)?;
|
||||||
|
if epi.join(format!("{}-{}.md", level.name, label)).exists() {
|
||||||
|
skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
println!("[auto] Missing daily digest for {}", date);
|
if inputs.is_empty() { continue; }
|
||||||
generate_daily(store, date)?;
|
println!("[auto] Missing {} digest for {}", level.name, label);
|
||||||
stats[0] += 1;
|
generate_digest(store, level, &label, &inputs)?;
|
||||||
daily_done.push(date.clone());
|
generated += 1;
|
||||||
}
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut weekly_done: Vec<String> = Vec::new();
|
println!("[auto] {}: {} generated, {} existed", level.name, generated, skipped);
|
||||||
for (week_label, example_dates) in &weeks {
|
total += generated;
|
||||||
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 {
|
if total == 0 {
|
||||||
println!("[auto] All digests up to date.");
|
println!("[auto] All digests up to date.");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue