sara-tasks 2.7.3

Sara — folder-aware task manager
use anyhow::Result;
use rusqlite::Connection;
use serde_json::json;

use crate::commands::doctor::types::{Check, DETAIL_LIMIT, Labels, Status};
use crate::domain::AnchorState;
use crate::infrastructure::db;
use crate::infrastructure::util::fingerprint;

pub(in crate::commands::doctor) fn stale_anchor_check(
    conn: &Connection,
    labels: &Labels,
) -> Result<Vec<Check>> {
    let anchors = db::all_active_item_file_fingerprints(conn)?;
    let mut stale: Vec<(String, String, AnchorState)> = anchors
        .into_iter()
        .filter_map(|(uuid, file, fp)| {
            let state = fingerprint::anchor_state(&file, fp.as_deref());
            state.is_stale().then(|| (labels.of(&uuid), file, state))
        })
        .collect();
    stale.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
    let memories: std::collections::BTreeSet<&str> = stale.iter().map(|s| s.0.as_str()).collect();

    let mut c = Check::new(
        "stale_anchors",
        Status::Info,
        memories.len(),
        "sara relearn <label> <text> (re-validate) or sara forget <label>",
    );
    c.summary = if stale.is_empty() {
        "no anchored file has drifted or vanished since its memory was learned".to_string()
    } else {
        format!(
            "{} memory(ies) anchored to {} file(s) that drifted or vanished since learn",
            memories.len(),
            stale.len()
        )
    };
    c.details = json!(
        stale
            .iter()
            .take(DETAIL_LIMIT)
            .map(|(label, file, state)| match state {
                AnchorState::Drifted(d) => json!({
                    "label": label,
                    "file": file,
                    "reason": "drifted",
                    "drift": (d * 100.0).round() / 100.0,
                }),
                _ => json!({ "label": label, "file": file, "reason": "missing" }),
            })
            .collect::<Vec<_>>()
    );
    Ok(vec![c])
}