use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use aristo_core::index::{Status, VerifyLevel};
use crate::commands::show::read_index;
use crate::nudge::intents::{authored_intents, AuthoredIntent};
use crate::nudge::state::{NudgeState, STATE_FILENAME};
use crate::{CliResult, Workspace};
struct SessionView {
kind: String,
open: usize,
total: usize,
}
#[derive(Default)]
struct BarView {
tier: Option<String>,
unreviewed: usize,
verified_clean: usize,
verifiable: usize,
stale: usize,
canon: usize,
session: Option<SessionView>,
}
#[aristo::intent(
"The statusline is read-only and TOLERANT: on any failure — no workspace, \
an unreadable index, or nudges globally off — it prints nothing and exits \
0. The status bar re-renders on every turn, so a statusline that errored \
or wrote files would corrupt the bar or thrash the workspace on every \
keystroke. Silence is the correct degraded state. It also stays CHEAP: \
index + nudge-state + the session pointer + a stat of the annotated \
files + a local sign-in check, never a source-tree walk.",
verify = "test",
id = "statusline_is_read_only_and_tolerant"
)]
pub(crate) fn run() -> CliResult<()> {
let start = cwd_from_stdin();
let Ok(ws) = Workspace::find(start.as_deref()) else {
return Ok(()); };
if ws.load_config().nudges.aggressiveness.is_off() {
return Ok(()); }
let mut view = BarView::default();
let state = NudgeState::load(&ws.aristo_dir().join(STATE_FILENAME));
view.tier = state.baseline.as_ref().map(|b| b.tier.clone());
view.session = active_session_view(&ws);
if view.session.is_none() {
if let Ok(index) = read_index(&ws.index_path()) {
let intents = authored_intents(&index);
view.unreviewed = state.unreviewed_count(
intents
.iter()
.map(|i| (i.id.as_str(), i.text_hash.as_str(), i.body_hash.as_str())),
);
let index_mtime = file_mtime(&ws.index_path());
for i in &intents {
if matches!(i.verify, VerifyLevel::Bool(false)) {
continue; }
view.verifiable += 1;
if i.status.is_terminal_clean() {
view.verified_clean += 1;
}
if is_stale(i, &ws, index_mtime) {
view.stale += 1;
}
}
}
}
if aristo_core::auth::resolve_full().is_ok() {
view.canon = crate::commands::canon::suggestions::pending_total(&ws);
}
if let Some(line) = statusline(&view) {
let color = std::env::var_os("NO_COLOR").is_none();
println!("{}", colorize(&line, &view, color));
}
Ok(())
}
fn statusline(view: &BarView) -> Option<String> {
if let Some(s) = &view.session {
let mut parts = vec![
format!("⏸ {} {}/{}", s.kind, s.open, s.total),
"stamp/verify paused".to_string(),
];
if let Some(t) = &view.tier {
parts.push(t.clone());
}
if view.canon > 0 {
parts.push(format!("{} canon", view.canon));
}
return Some(format!("aristo {}", parts.join(" · ")));
}
let complete = view.unreviewed == 0
&& view.stale == 0
&& view.verifiable > 0
&& view.verified_clean == view.verifiable;
if complete {
if let Some(t) = &view.tier {
return Some(format!("aristo {t} ✓"));
}
}
let mut parts: Vec<String> = Vec::new();
if let Some(t) = &view.tier {
parts.push(t.clone());
}
if view.unreviewed > 0 {
parts.push(format!("{} review", view.unreviewed));
}
if view.verifiable > 0 {
parts.push(format!(
"{}/{} verified",
view.verified_clean, view.verifiable
));
}
if view.stale > 0 {
parts.push(format!("⚠ {} stale", view.stale));
}
if view.canon > 0 {
parts.push(format!("{} canon", view.canon));
}
if parts.is_empty() {
None
} else {
Some(format!("aristo {}", parts.join(" · ")))
}
}
fn colorize(line: &str, view: &BarView, color: bool) -> String {
if !color {
return line.to_string();
}
let mut s = line.to_string();
if view.stale > 0 {
let tok = format!("⚠ {} stale", view.stale);
s = s.replace(&tok, &format!("\x1b[33m{tok}\x1b[0m"));
}
s = s.replace(" ✓", " \x1b[32m✓\x1b[0m");
if view.session.is_none() && view.verifiable > 0 && view.verified_clean == view.verifiable {
let tok = format!("{}/{} verified", view.verified_clean, view.verifiable);
s = s.replace(&tok, &format!("\x1b[32m{tok}\x1b[0m"));
}
if view.session.is_some() {
s = s.replace('⏸', "\x1b[1m⏸");
s = s.replace(" · stamp/verify paused", "\x1b[0m · stamp/verify paused");
}
s
}
fn active_session_view(ws: &Workspace) -> Option<SessionView> {
let id = crate::session::storage::read_active_pointer(ws)
.ok()
.flatten()?;
let session = crate::session::storage::read_active_session(ws, &id)
.ok()
.flatten()?;
let c = session.bucket_counts();
let total = c.open + c.accepted + c.rejected + c.pending;
Some(SessionView {
kind: session.kind.clone(),
open: c.open,
total,
})
}
fn is_stale(i: &AuthoredIntent, ws: &Workspace, index_mtime: Option<SystemTime>) -> bool {
let src_mtime = file_mtime(&ws.root.join(&i.file));
intent_is_stale(i.status, src_mtime, index_mtime)
}
#[aristo::intent(
"The bar's staleness count is cheap and conservative: an intent is stale \
if it is recorded broken (Status::Stale or Counterexample) OR it is \
currently terminal-clean but its source file's mtime is newer than the \
index's (edited since the last stamp, so its proof may be clobbered). The \
mtime test is a FILE-level heuristic — it over-counts versus a per-\
function body-hash recompute, which is the right bias for a 're-verify' \
warning and avoids the per-render source parse the bar forbids. When \
either mtime is unreadable it does NOT warn (tolerant: omission over a \
false alarm). Unknown/Inconclusive are not stale, only unverified.",
verify = "test",
id = "statusline_staleness_is_cheap_and_conservative"
)]
fn intent_is_stale(
status: Status,
src_mtime: Option<SystemTime>,
index_mtime: Option<SystemTime>,
) -> bool {
match status {
Status::Stale | Status::Counterexample => true,
s if s.is_terminal_clean() => {
matches!((src_mtime, index_mtime), (Some(src), Some(idx)) if src > idx)
}
_ => false,
}
}
fn file_mtime(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}
fn cwd_from_stdin() -> Option<PathBuf> {
let mut buf = String::new();
if std::io::stdin().read_to_string(&mut buf).is_err() || buf.trim().is_empty() {
return None;
}
let json: serde_json::Value = serde_json::from_str(&buf).ok()?;
let dir = json
.get("workspace")
.and_then(|w| w.get("current_dir"))
.and_then(|v| v.as_str())
.or_else(|| json.get("cwd").and_then(|v| v.as_str()))?;
Some(PathBuf::from(dir))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn view() -> BarView {
BarView::default()
}
#[test]
fn empty_when_nothing_to_surface() {
assert_eq!(statusline(&view()), None);
}
#[test]
fn clean_and_complete_collapses_to_tier_check() {
let v = BarView {
tier: Some("Adept".into()),
verifiable: 14,
verified_clean: 14,
..view()
};
assert_eq!(statusline(&v).as_deref(), Some("aristo Adept ✓"));
}
#[test]
fn typical_shows_review_and_coverage() {
let v = BarView {
tier: Some("Apprentice".into()),
unreviewed: 3,
verifiable: 14,
verified_clean: 12,
..view()
};
assert_eq!(
statusline(&v).as_deref(),
Some("aristo Apprentice · 3 review · 12/14 verified")
);
}
#[test]
fn drift_shows_amber_stale_token() {
let v = BarView {
tier: Some("Apprentice".into()),
unreviewed: 3,
verifiable: 14,
verified_clean: 12,
stale: 3,
..view()
};
assert_eq!(
statusline(&v).as_deref(),
Some("aristo Apprentice · 3 review · 12/14 verified · ⚠ 3 stale")
);
}
#[test]
fn canon_shows_only_when_present_signed_in() {
let v = BarView {
tier: Some("Apprentice".into()),
verifiable: 14,
verified_clean: 12,
canon: 4,
..view()
};
assert_eq!(
statusline(&v).as_deref(),
Some("aristo Apprentice · 12/14 verified · 4 canon")
);
let v0 = BarView { canon: 0, ..v };
assert_eq!(
statusline(&v0).as_deref(),
Some("aristo Apprentice · 12/14 verified")
);
}
#[test]
fn active_session_takes_over_and_suppresses_counts() {
let v = BarView {
tier: Some("Apprentice".into()),
unreviewed: 3,
verifiable: 14,
verified_clean: 12,
stale: 5,
session: Some(SessionView {
kind: "intent-review".into(),
open: 2,
total: 7,
}),
..view()
};
assert_eq!(
statusline(&v).as_deref(),
Some("aristo ⏸ intent-review 2/7 · stamp/verify paused · Apprentice")
);
}
#[test]
fn no_tier_still_shows_work() {
let v = BarView {
unreviewed: 2,
..view()
};
assert_eq!(statusline(&v).as_deref(), Some("aristo 2 review"));
}
#[test]
fn color_is_decoration_and_degrades_to_plain() {
let v = BarView {
tier: Some("Apprentice".into()),
verifiable: 14,
verified_clean: 12,
stale: 3,
..view()
};
let plain = statusline(&v).unwrap();
assert_eq!(colorize(&plain, &v, false), plain);
let colored = colorize(&plain, &v, true);
assert!(colored.contains("⚠ 3 stale"));
assert!(colored.contains("\x1b[33m"));
assert!(colored.contains("\x1b[0m"));
}
#[test]
fn stale_logic_recorded_and_drift() {
let t0 = SystemTime::UNIX_EPOCH;
let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
assert!(intent_is_stale(Status::Stale, None, None));
assert!(intent_is_stale(Status::Counterexample, Some(t0), Some(t1)));
assert!(intent_is_stale(Status::Verified, Some(t1), Some(t0)));
assert!(intent_is_stale(Status::Tested, Some(t1), Some(t0)));
assert!(!intent_is_stale(Status::Neural, Some(t0), Some(t1)));
assert!(!intent_is_stale(Status::Verified, None, Some(t1)));
assert!(!intent_is_stale(Status::Unknown, Some(t1), Some(t0)));
}
}