digest: replace method dispatch with fn pointer fields on DigestLevel

The gather() and find_args() methods dispatched on child_prefix via match,
duplicating the list of digest levels. Replace with fn pointer fields so
each DigestLevel const carries its own behavior directly — no enum-like
dispatch needed.

Also replaces child_prefix with journal_input bool for format_inputs.
This commit is contained in:
ProofOfConcept 2026-03-03 17:48:24 -05:00
parent b083cc433c
commit 849c6c4b98

View file

@ -24,8 +24,10 @@ struct DigestLevel {
period: &'static str, // "Date", "Week", "Month" period: &'static str, // "Date", "Week", "Month"
input_title: &'static str, input_title: &'static str,
instructions: &'static str, instructions: &'static str,
child_prefix: Option<&'static str>,
timeout: u64, timeout: u64,
journal_input: bool, // true for daily (journal entries), false for child digests
gather: fn(&Store, &str) -> Result<(String, Vec<(String, String)>), String>,
find_args: fn(&[String], &str) -> Vec<String>,
} }
const DAILY: DigestLevel = DigestLevel { const DAILY: DigestLevel = DigestLevel {
@ -67,8 +69,10 @@ Include the original timestamp as a reference.]
``` ```
If a concept doesn't have a matching key, note it with "NEW:" prefix."#, If a concept doesn't have a matching key, note it with "NEW:" prefix."#,
child_prefix: None,
timeout: 300, timeout: 300,
journal_input: true,
gather: gather_daily,
find_args: find_daily_args,
}; };
const WEEKLY: DigestLevel = DigestLevel { const WEEKLY: DigestLevel = DigestLevel {
@ -113,8 +117,10 @@ const WEEKLY: DigestLevel = DigestLevel {
## Looking ahead ## Looking ahead
[What's unfinished? What threads continue into next week?] [What's unfinished? What threads continue into next week?]
```"#, ```"#,
child_prefix: Some("daily"),
timeout: 300, timeout: 300,
journal_input: false,
gather: gather_weekly,
find_args: find_weekly_args,
}; };
const MONTHLY: DigestLevel = DigestLevel { const MONTHLY: DigestLevel = DigestLevel {
@ -173,8 +179,10 @@ Read all the weekly digests and synthesize the month's story.
## Looking ahead ## Looking ahead
[What threads carry into next month? What's unfinished?] [What threads carry into next month? What's unfinished?]
```"#, ```"#,
child_prefix: Some("weekly"),
timeout: 600, timeout: 600,
journal_input: false,
gather: gather_monthly,
find_args: find_monthly_args,
}; };
// --- Input gathering --- // --- Input gathering ---
@ -192,95 +200,77 @@ fn load_child_digests(prefix: &str, labels: &[String]) -> Result<Vec<(String, St
Ok(digests) Ok(digests)
} }
impl DigestLevel { fn find_daily_args(dates: &[String], today: &str) -> Vec<String> {
/// Find candidate args from journal dates for auto-detection. dates.iter()
/// Returns args suitable for passing to gather(). .filter(|d| d.as_str() != today)
fn find_args(&self, dates: &[String], today: &str) -> Vec<String> { .cloned()
match self.child_prefix { .collect()
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). fn find_weekly_args(dates: &[String], today: &str) -> Vec<String> {
/// For daily: arg is a date, gathers journal entries from store. let mut weeks: BTreeMap<String, String> = BTreeMap::new();
/// For weekly: arg is any date in the week, computes week label. for date in dates {
/// For monthly: arg is "YYYY-MM" (or empty for current month). if let Ok((wl, _)) = week_dates(date) {
fn gather(&self, store: &Store, arg: &str) -> Result<(String, Vec<(String, String)>), String> { weeks.entry(wl).or_insert_with(|| date.clone());
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))
}
} }
} }
weeks.into_values()
.filter(|date| {
week_dates(date).map_or(false, |(_, days)|
days.last().unwrap() < &today.to_string())
})
.collect()
}
fn find_monthly_args(dates: &[String], _today: &str) -> Vec<String> {
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()
}
fn gather_daily(store: &Store, date: &str) -> Result<(String, Vec<(String, 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));
Ok((date.to_string(), entries))
}
fn gather_weekly(_store: &Store, date: &str) -> Result<(String, Vec<(String, String)>), String> {
let (week_label, dates) = week_dates(date)?;
let inputs = load_child_digests("daily", &dates)?;
Ok((week_label, inputs))
}
fn gather_monthly(_store: &Store, arg: &str) -> Result<(String, Vec<(String, String)>), String> {
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("weekly", &child_labels)?;
Ok((label, inputs))
} }
// --- Unified generator --- // --- Unified generator ---
@ -317,7 +307,7 @@ fn generate_digest(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
let content = format_inputs(inputs, level.child_prefix.is_none()); let content = format_inputs(inputs, level.journal_input);
let covered = inputs.iter() let covered = inputs.iter()
.map(|(l, _)| l.as_str()) .map(|(l, _)| l.as_str())
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -354,17 +344,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 (label, inputs) = DAILY.gather(store, date)?; let (label, inputs) = (DAILY.gather)(store, date)?;
generate_digest(store, &DAILY, &label, &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 (label, inputs) = WEEKLY.gather(store, date)?; let (label, inputs) = (WEEKLY.gather)(store, date)?;
generate_digest(store, &WEEKLY, &label, &inputs) generate_digest(store, &WEEKLY, &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 (label, inputs) = MONTHLY.gather(store, month_arg)?; let (label, inputs) = (MONTHLY.gather)(store, month_arg)?;
generate_digest(store, &MONTHLY, &label, &inputs) generate_digest(store, &MONTHLY, &label, &inputs)
} }
@ -418,12 +408,12 @@ pub fn digest_auto(store: &mut Store) -> Result<(), String> {
let mut total = 0u32; let mut total = 0u32;
for level in LEVELS { for level in LEVELS {
let args = level.find_args(&dates, &today); let args = (level.find_args)(&dates, &today);
let mut generated = 0u32; let mut generated = 0u32;
let mut skipped = 0u32; let mut skipped = 0u32;
for arg in &args { for arg in &args {
let (label, inputs) = level.gather(store, arg)?; let (label, inputs) = (level.gather)(store, arg)?;
if epi.join(format!("{}-{}.md", level.name, label)).exists() { if epi.join(format!("{}-{}.md", level.name, label)).exists() {
skipped += 1; skipped += 1;
continue; continue;