feat(action-items): table-backed action_items on Meeting; reconcile deletes (FR-LLM-3)

This commit is contained in:
iamdoubz committed 2026-07-11 22:32:14 -05:00
1 parent 9c74f84c5b
commit a0faaa94b4
1 file changed
+35
+35
View File
@@ -104,6 +104,12 @@ pub struct Meeting {
/// Note-template id (Phase 8, T8.1, FR-NOTE-5), if one was picked at
/// creation — resolved against `notes::templates::catalog()`.
pub template_id: Option<String>,
/// Confirmed/edited action items from the `action_items` table — the
/// source of truth the user manages directly (add/edit/delete via
/// `confirm_action_items`, FR-LLM-3). Falls back to the drafts parsed
/// into `summary.json` when the table has none yet, so a freshly
/// generated summary still shows its suggestions.
pub action_items: Vec<ActionItem>,
}
/// A calendar event with its attendees (T6.3/T6.4, FR-CAL-1/3) — the
@@ -892,6 +898,14 @@ impl Store for SqliteStore {
let summary = read_artifact(&folder.join("summary.json"))
.and_then(|s| serde_json::from_str::<SummaryFile>(&s).ok());
let tags = self.tags_for_meeting(id).await?;
// Table is the source of truth for confirmed/edited items; fall back
// to the summary's parsed drafts only when nothing's been saved yet.
let mut action_items = self.list_action_items(id).await?;
if action_items.is_empty() {
if let Some(s) = &summary {
action_items = s.action_items.clone();
}
}
Ok(Meeting {
id: row.get("id"),
@@ -911,6 +925,7 @@ impl Store for SqliteStore {
calendar_event_id: row.get("calendar_event_id"),
tags,
template_id: row.get("template_id"),
action_items,
})
}
@@ -1254,6 +1269,26 @@ impl Store for SqliteStore {
items: &[ActionItem],
) -> Result<Vec<ActionItem>, StoreError> {
let now = now_unix();
// Reconcile deletes: any row previously saved for this meeting that the
// caller no longer includes was removed in the UI (FR-LLM-3). Action
// items per meeting number in the low tens, so a per-row delete loop is
// fine. ponytail: O(n) delete scan, batch it only if n ever gets large.
let keep: std::collections::HashSet<&str> =
items.iter().filter_map(|i| i.id.as_deref()).collect();
let existing_ids: Vec<String> =
sqlx::query_scalar("SELECT id FROM action_items WHERE meeting_id = ?")
.bind(id)
.fetch_all(&self.pool)
.await?;
for eid in &existing_ids {
if !keep.contains(eid.as_str()) {
sqlx::query("DELETE FROM action_items WHERE id = ? AND meeting_id = ?")
.bind(eid)
.bind(id)
.execute(&self.pool)
.await?;
}
}
let mut saved = Vec::with_capacity(items.len());
for item in items {
let item_id = match &item.id {