use std::collections::BTreeMap;
use aristo_core::badge::{compute_tier, TierComputation};
use aristo_core::index::{
AssumeEntry, IndexEntry, IndexFile, IntentEntry, Status, VerifyLevel, VerifyMethod,
};
use aristo_core::walk::{count_fns_per_module_with, WalkOptions};
use crate::commands::index::workspace_or_error;
use crate::commands::show::{read_index, status_label};
use crate::preflight::{emit_advisory_if_stale, freshness_check};
use crate::{CliError, CliResult};
pub(crate) fn run() -> CliResult<()> {
let ws = workspace_or_error()?;
let report = freshness_check(&ws);
emit_advisory_if_stale(&report);
let index = read_index(&ws.index_path())?;
println!();
println!("Aristo SDK v{}", env!("CARGO_PKG_VERSION"));
println!(" Default verify: {}", default_verify_for_display(&ws));
let counts = Counts::from(&index);
println!();
println!("Annotations:");
println!(" Total: {}", counts.total);
println!(
" By kind: intent={} assume={}",
counts.intent, counts.assume
);
println!(
" By verify level: neural={} test={} full={} true={} false={}",
counts.verify_neural,
counts.verify_test,
counts.verify_full,
counts.verify_true,
counts.verify_false,
);
print!(" By status: ");
let status_pairs = counts.status_breakdown();
let pieces: Vec<String> = status_pairs
.iter()
.map(|(label, n)| format!("{label}={n}"))
.collect();
println!("{}", pieces.join(" "));
println!();
println!("Verification rate (verified / total per pipeline):");
println!(
" neural: {} / {} ({})",
counts.verified_neural,
counts.verify_neural,
format_rate(counts.verified_neural, counts.verify_neural),
);
println!(
" test: {} / {} ({})",
counts.verified_test,
counts.verify_test,
format_rate(counts.verified_test, counts.verify_test),
);
println!(
" full: {} / {} ({})",
counts.verified_full,
counts.verify_full,
format_rate(counts.verified_full, counts.verify_full),
);
let computation = compute_project_tier(&ws, &index)?;
println!();
println!("Tier:");
println!(
" Score: {:.3} (visible)",
computation.visible_score
);
println!(" Tier: {}", computation.tier.label());
if computation.arete_gate_met {
println!(" Areté gate: ✓ met (paid-formal-proof + server-bound)");
}
println!();
println!("Index health:");
println!(" schema_version: {} (current)", index.meta.schema_version);
let backlog_counts = collect_backlog_counts(&ws)?;
if !backlog_counts.is_empty() {
println!();
println!("Review backlog:");
for (kind, count) in &backlog_counts {
let item_word = if *count == 1 { "item" } else { "items" };
println!(" {kind}: {count} {item_word}");
}
}
if let Some(active) = active_session_summary(&ws)? {
println!();
println!("Active review session:");
println!(" {active}");
}
print_canon_health(&ws);
println!();
println!(
"For per-annotation diagnostics, run `aristo stamp` (or `aristo list --filter status=<state>`)."
);
Ok(())
}
#[aristo::intent(
"`aristo status` includes a canon-health block sourced from \
local state only — credentials presence (no token print), the \
[canon] config flag, last_fetched + canon_version + \
effective_scopes from `.aristo/canon-matches.toml`'s meta, and \
pending/accepted/rejected counts across all annotations. The \
block must NOT make a network call: status is the offline \
daily-loop summary; coupling it to canon API state would \
break the offline-friendly invariant.",
verify = "neural",
id = "status_canon_health_is_offline_only"
)]
fn print_canon_health(ws: &crate::Workspace) {
use aristo_core::canon::auth;
use aristo_core::canon::cache::CanonMatchesFile;
println!();
println!("Canon binding:");
let config = ws.load_config();
if !config.canon.enabled {
println!(" Status: disabled (`[canon] enabled = false` in aristo.toml)");
return;
}
let auth_state = match auth::resolve() {
Ok(_) => "authenticated (token present)".to_string(),
Err(aristo_core::canon::AuthError::NoToken) => "no token (free-tier mode)".to_string(),
Err(aristo_core::canon::AuthError::Malformed(_)) => {
"malformed credentials file".to_string()
}
Err(aristo_core::canon::AuthError::Invalid) => {
"invalid token (server rejected)".to_string()
}
};
println!(" Auth: {auth_state}");
let cache_path = ws.canon_matches_path();
let cache = CanonMatchesFile::read(&cache_path).unwrap_or_default();
let last_fetched = cache.meta.last_fetched.as_deref().unwrap_or("never");
let canon_version = cache.meta.canon_version.as_deref().unwrap_or("—");
println!(" Last fetched: {last_fetched}");
println!(" Catalog version: {canon_version}");
let mut pending = 0usize;
let mut accepted = 0usize;
let mut rejected = 0usize;
for entry in cache.entries.values() {
pending += entry.pending_matches.len();
accepted += entry.accepted_matches.len();
rejected += entry.rejected_matches.len();
}
println!(" Pending: {pending}");
println!(" Accepted (bound): {accepted}");
println!(" Rejected: {rejected}");
let _ = config; }
fn collect_backlog_counts(ws: &crate::Workspace) -> CliResult<Vec<(String, usize)>> {
let dir = ws.sessions_backlog_dir();
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let mut out: Vec<(String, usize)> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Some(kind) = name.strip_suffix(".toml") else {
continue;
};
if let Ok(count) = crate::session::backlog::count(ws, kind) {
if count > 0 {
out.push((kind.to_string(), count));
}
}
}
out.sort();
Ok(out)
}
fn active_session_summary(ws: &crate::Workspace) -> CliResult<Option<String>> {
let Some(id) = crate::session::storage::read_active_pointer(ws)? else {
return Ok(None);
};
let Some(session) = crate::session::storage::read_active_session(ws, &id)? else {
return Ok(None);
};
let c = session.bucket_counts();
Ok(Some(format!(
"id={} kind={} subject={} (open={} accepted={} rejected={} pending={})",
session.id, session.kind, session.subject, c.open, c.accepted, c.rejected, c.pending
)))
}
fn format_rate(verified: usize, total: usize) -> String {
if total == 0 {
"n/a".to_string()
} else {
let pct = (verified as f64 / total as f64) * 100.0;
format!("{pct:.1}%")
}
}
#[aristo::intent(
"Status's tier computation routes through `aristo_core::badge::compute_tier` \
with the same `count_fns_per_module_with(WalkOptions::none())` denominator \
that `aristo badge` uses. Drift between the two would produce a project \
where the badge SVG and `aristo status` report different tiers — a \
contradiction the user can't reconcile. Sharing the call site (not the \
formula) is the load-bearing invariant.",
verify = "test",
id = "status_tier_call_matches_badge_command_call"
)]
fn compute_project_tier(ws: &crate::Workspace, index: &IndexFile) -> CliResult<TierComputation> {
let fn_counts =
count_fns_per_module_with(&ws.root, &WalkOptions::none()).map_err(|e| CliError::Other {
message: format!("failed to walk source for tier computation: {e}"),
exit_code: 1,
})?;
let default_method = ws.load_config().verify.default_method;
Ok(compute_tier(index, &fn_counts, default_method))
}
fn default_verify_for_display(ws: &crate::Workspace) -> String {
let path = ws.config_path();
let Ok(text) = std::fs::read_to_string(&path) else {
return "(aristo.toml unreadable)".to_string();
};
let Ok(cfg): Result<aristo_core::config::ConfigFile, _> = toml::from_str(&text) else {
return "(aristo.toml unparseable)".to_string();
};
match cfg.verify.default_method {
Some(VerifyMethod::Neural) => "\"neural\"".to_string(),
Some(VerifyMethod::Test) => "\"test\"".to_string(),
Some(VerifyMethod::Full) => "\"full\"".to_string(),
None => "(per-tier default)".to_string(),
}
}
#[derive(Debug, Default)]
struct Counts {
total: usize,
intent: usize,
assume: usize,
verify_neural: usize,
verify_test: usize,
verify_full: usize,
verify_true: usize,
verify_false: usize,
verified_neural: usize,
verified_test: usize,
verified_full: usize,
by_status: BTreeMap<&'static str, usize>,
}
impl Counts {
fn from(index: &IndexFile) -> Self {
let mut c = Counts::default();
for entry in index.entries.values() {
c.total += 1;
match entry {
IndexEntry::Intent(e) => {
c.intent += 1;
c.tally_verify(e);
c.tally_status_intent(e);
}
IndexEntry::Assume(e) => {
c.assume += 1;
c.tally_status_assume(e);
}
}
}
c
}
fn tally_verify(&mut self, e: &IntentEntry) {
match e.verify {
VerifyLevel::Method(VerifyMethod::Neural) => {
self.verify_neural += 1;
if e.status == Status::Neural {
self.verified_neural += 1;
}
}
VerifyLevel::Method(VerifyMethod::Test) => {
self.verify_test += 1;
if e.status == Status::Tested {
self.verified_test += 1;
}
}
VerifyLevel::Method(VerifyMethod::Full) => {
self.verify_full += 1;
if e.status == Status::Verified {
self.verified_full += 1;
}
}
VerifyLevel::Bool(true) => self.verify_true += 1,
VerifyLevel::Bool(false) => self.verify_false += 1,
}
}
fn tally_status_intent(&mut self, e: &IntentEntry) {
*self.by_status.entry(status_label(e.status)).or_insert(0) += 1;
}
fn tally_status_assume(&mut self, e: &AssumeEntry) {
*self.by_status.entry(status_label(e.status)).or_insert(0) += 1;
}
#[cfg(test)]
fn verification_rate_triple(&self) -> ((usize, usize), (usize, usize), (usize, usize)) {
(
(self.verified_neural, self.verify_neural),
(self.verified_test, self.verify_test),
(self.verified_full, self.verify_full),
)
}
fn status_breakdown(&self) -> Vec<(&'static str, usize)> {
let order = [
"verified",
"tested",
"neural",
"stale",
"unknown",
"pending-deepen",
"orphan",
"forged",
];
let mut out = Vec::new();
for label in order {
let n = *self.by_status.get(label).unwrap_or(&0);
if n > 0 {
out.push((label, n));
}
}
if out.is_empty() {
out.push(("unknown", 0));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use aristo_core::index::{
AnnotationId, BindingState, CoveredRegion, IndexEntry, IndexFile, IntentEntry, Meta,
Sha256, Status, VerifyLevel, VerifyMethod,
};
use std::collections::BTreeMap;
fn sha(c: char) -> Sha256 {
Sha256::parse(&format!("sha256:{}", c.to_string().repeat(64))).unwrap()
}
fn intent_with(verify: VerifyLevel, status: Status) -> IndexEntry {
IndexEntry::Intent(IntentEntry {
text: "x".into(),
verify,
status,
text_hash: sha('a'),
body_hash: sha('b'),
file: "src/lib.rs".into(),
site: "fn x (line 1)".into(),
covered_region: CoveredRegion::Function,
binding: BindingState::Local,
parent: None,
last_critiqued_at_text_hash: None,
last_critique_finding_count: None,
})
}
fn build(entries: &[(&str, IndexEntry)]) -> IndexFile {
let mut map = BTreeMap::new();
for (id, e) in entries {
map.insert(AnnotationId::parse(id).unwrap(), e.clone());
}
IndexFile {
meta: Meta {
schema_version: 1,
generated_by: None,
generated_at: None,
source_root: None,
},
entries: map,
}
}
#[test]
fn verification_rate_neural_pipeline_counts_only_status_neural() {
let idx = build(&[
(
"a",
intent_with(VerifyLevel::Method(VerifyMethod::Neural), Status::Neural),
),
(
"b",
intent_with(VerifyLevel::Method(VerifyMethod::Neural), Status::Unknown),
),
(
"c",
intent_with(VerifyLevel::Method(VerifyMethod::Neural), Status::Stale),
),
]);
let c = Counts::from(&idx);
let ((vn, n), _, _) = c.verification_rate_triple();
assert_eq!((vn, n), (1, 3), "1 of 3 neural intents in clean state");
}
#[test]
fn verification_rate_test_pipeline_counts_only_status_tested() {
let idx = build(&[
(
"a",
intent_with(VerifyLevel::Method(VerifyMethod::Test), Status::Tested),
),
(
"b",
intent_with(VerifyLevel::Method(VerifyMethod::Test), Status::Tested),
),
(
"c",
intent_with(VerifyLevel::Method(VerifyMethod::Test), Status::Stale),
),
(
"d",
intent_with(VerifyLevel::Method(VerifyMethod::Test), Status::Neural),
),
]);
let c = Counts::from(&idx);
let (_, (vt, t), _) = c.verification_rate_triple();
assert_eq!((vt, t), (2, 4));
}
#[test]
fn verification_rate_full_pipeline_counts_only_status_verified() {
let idx = build(&[
(
"a",
intent_with(VerifyLevel::Method(VerifyMethod::Full), Status::Verified),
),
(
"b",
intent_with(VerifyLevel::Method(VerifyMethod::Full), Status::Tested),
),
]);
let c = Counts::from(&idx);
let (_, _, (vf, f)) = c.verification_rate_triple();
assert_eq!((vf, f), (1, 2));
}
#[test]
fn format_rate_empty_denominator_is_not_a_number() {
assert_eq!(format_rate(0, 0), "n/a");
}
#[test]
fn format_rate_normal_case_shows_one_decimal_place() {
assert_eq!(format_rate(1, 3), "33.3%");
assert_eq!(format_rate(3, 4), "75.0%");
assert_eq!(format_rate(0, 5), "0.0%");
}
}