use std::fs;
use std::time::SystemTime;
use aristo_core::walk::{walk_for_freshness_with, WalkOptions};
use crate::Workspace;
#[derive(Debug, Clone)]
pub(crate) struct FreshnessReport {
pub stale_count: usize,
#[allow(dead_code)]
pub index_missing: bool,
}
impl FreshnessReport {
pub(crate) fn is_stale(&self) -> bool {
self.stale_count > 0
}
}
#[aristo::intent(
"Recomputed from scratch on every invocation — no caching, no \
incremental tracking between calls. Correctness over speed: an \
advisory check shouldn't introduce its own cache-staleness mode.",
verify = "neural",
id = "freshness_check_compares_source_mtimes_to_index"
)]
pub(crate) fn freshness_check(ws: &Workspace) -> FreshnessReport {
let index_path = ws.index_path();
let Ok(index_meta) = fs::metadata(&index_path) else {
return FreshnessReport {
stale_count: 0,
index_missing: true,
};
};
let index_mtime = index_meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let opts = WalkOptions::from_index_config(&ws.load_config().index).unwrap_or_default();
let sources = walk_for_freshness_with(&ws.root, &opts).unwrap_or_default();
let stale_count = sources
.into_iter()
.filter(|p| {
fs::metadata(p)
.and_then(|m| m.modified())
.map(|src_mtime| src_mtime > index_mtime)
.unwrap_or(false)
})
.count();
FreshnessReport {
stale_count,
index_missing: false,
}
}
pub(crate) fn emit_advisory_if_stale(report: &FreshnessReport) {
if !report.is_stale() {
return;
}
eprintln!(
"warning: .aristo/index.toml may be stale relative to source ({} files newer than indexed).",
report.stale_count
);
eprintln!(" Run `aristo stamp` to refresh.");
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
use tempfile::TempDir;
fn make_ws() -> (TempDir, Workspace) {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("aristo.toml"),
"[verify]\ndefault_method = \"test\"\n",
)
.unwrap();
fs::create_dir_all(tmp.path().join(".aristo")).unwrap();
fs::write(
tmp.path().join(".aristo/index.toml"),
"[__meta__]\nschema_version = 1\n",
)
.unwrap();
let ws = Workspace::find(Some(tmp.path())).unwrap();
(tmp, ws)
}
#[test]
fn fresh_index_reports_zero_stale() {
let (_tmp, ws) = make_ws();
let report = freshness_check(&ws);
assert_eq!(report.stale_count, 0);
assert!(!report.is_stale());
assert!(!report.index_missing);
}
#[test]
fn source_newer_than_index_reports_stale() {
let (tmp, ws) = make_ws();
sleep(Duration::from_millis(50));
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(tmp.path().join("src/lib.rs"), "fn x() {}").unwrap();
let report = freshness_check(&ws);
assert_eq!(report.stale_count, 1);
assert!(report.is_stale());
}
#[test]
fn missing_index_reports_index_missing_not_stale() {
let (tmp, ws) = make_ws();
fs::remove_file(tmp.path().join(".aristo/index.toml")).unwrap();
let report = freshness_check(&ws);
assert!(report.index_missing);
assert_eq!(report.stale_count, 0);
}
#[test]
fn multiple_stale_sources_count_correctly() {
let (tmp, ws) = make_ws();
sleep(Duration::from_millis(50));
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(tmp.path().join("src/a.rs"), "fn a() {}").unwrap();
fs::write(tmp.path().join("src/b.rs"), "fn b() {}").unwrap();
fs::write(tmp.path().join("src/c.rs"), "fn c() {}").unwrap();
let report = freshness_check(&ws);
assert_eq!(report.stale_count, 3);
}
}