use std::io::{self, IsTerminal};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use clap::Args;
use serde::{Deserialize, Serialize};
use mati_core::health::{gaps, onboarding};
use mati_core::store::{
Category, ConfidenceScore, FileRecord, Priority, QualityScore, Record, RecordLifecycle,
RecordSource, RecordVersion, StalenessScore, StalenessTier, Store,
};
use super::colors;
use super::proxy::StoreProxy;
#[derive(Args)]
pub struct StatsArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Deserialize)]
struct DailyAgg {
count: u64,
#[allow(dead_code)]
keys: Vec<String>,
}
const SNAPSHOT_KEY: &str = "analytics:knowledge_health";
const SNAPSHOT_MAX_AGE_SECS: u64 = 86_400;
#[derive(Serialize, Deserialize)]
struct HealthSnapshot {
files_with_purpose: u32,
total_files: u32,
purpose_coverage: f32,
gotchas_per_hotspot: f32,
decisions_documented: u32,
avg_confidence: f32,
knowledge_gaps: u32,
new_records_30d: u32,
multi_contributor_records: u32,
estimated_minutes: f32,
critical_files_covered: f32,
gotcha_coverage: f32,
decision_coverage: f32,
#[serde(default)]
critical_uncovered: u32,
#[serde(default)]
orphaned_decisions: u32,
#[serde(default)]
low_confidence: u32,
hits_7d: u64,
misses_7d: u64,
hit_rate_7d: f32,
bypasses_7d: u64,
computed_at: u64,
#[serde(default)]
write_seq: u64,
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub(crate) struct GotchaHealth {
pub(crate) active: u64,
pub(crate) confirmed: u64,
pub(crate) stale_or_worse: u64,
}
pub(crate) fn gotcha_is_confirmed(r: &Record) -> bool {
r.payload
.as_ref()
.and_then(|p| p.get("confirmed"))
.and_then(|v| v.as_bool())
!= Some(false)
}
pub(crate) fn gotcha_health(gotchas: &[Record]) -> GotchaHealth {
let confirmed = gotchas.iter().filter(|r| gotcha_is_confirmed(r)).count() as u64;
let stale_or_worse = gotchas
.iter()
.filter(|r| {
matches!(
r.staleness.tier,
StalenessTier::Stale | StalenessTier::Liability | StalenessTier::Tombstone
)
})
.count() as u64;
GotchaHealth {
active: gotchas.len() as u64,
confirmed,
stale_or_worse,
}
}
fn format_duration_ms(ms: u64) -> String {
if ms < 1_000 {
format!("{ms}ms")
} else if ms < 60_000 {
format!("{:.1}s", ms as f64 / 1_000.0)
} else {
format!("{:.1}m", ms as f64 / 60_000.0)
}
}
async fn enforcement_metrics_30d(
store: &StoreProxy,
now_ms: u64,
) -> Option<(
mati_core::store::enforcement::EnforcementEventCounts,
mati_core::store::enforcement::DerivedEnforcementMetrics,
)> {
let since_ms = now_ms.saturating_sub(30 * 86_400_000);
match store.scan_enforcement_events(0, u64::MAX).await {
Ok(mut events) => {
events.retain(|e| e.recorded_at_ms >= since_ms);
Some((
mati_core::store::enforcement::aggregate_event_counts(&events),
mati_core::store::enforcement::derive_enforcement_metrics(&events),
))
}
Err(e) => {
tracing::debug!("enforcement event scan failed: {e}");
None
}
}
}
async fn run_json(store: &StoreProxy, cwd: &std::path::Path) -> Result<()> {
let project = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let now = now_secs();
let mut gotchas = store.scan_prefix("gotcha:").await?;
gotchas.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
let health = gotcha_health(&gotchas);
let enforcement = match enforcement_metrics_30d(store, now * 1_000).await {
Some((counts, derived)) => serde_json::json!({
"available": true,
"total": counts.total,
"denials": counts.denials,
"allowed_after_receipt": counts.allowed_after_receipt,
"consulted": counts.receipts_minted,
"bypasses": counts.bypasses,
"gaps": counts.gaps,
"controls": {
"changed": counts.controls_changed,
"created": counts.controls_created,
"confirmed": counts.controls_confirmed,
"updated": counts.controls_updated,
"removed": counts.controls_removed,
},
"derived": {
"blocked_sessions": derived.blocked_sessions,
"attributed_denials": derived.attributed_denials,
"blocks_per_session": derived.blocks_per_session,
"median_time_to_consult_ms": derived.median_time_to_consult_ms,
"consult_pairs": derived.consult_pairs,
}
}),
None => serde_json::json!({ "available": false }),
};
let out = serde_json::json!({
"project": project,
"computed_at": now,
"window_days": 30,
"gotchas": {
"active": health.active,
"confirmed": health.confirmed,
"stale_or_worse": health.stale_or_worse,
},
"enforcement_30d": enforcement,
});
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
pub async fn run(args: StatsArgs) -> Result<()> {
let cwd = std::env::current_dir()?;
let store = StoreProxy::open(&cwd).await?;
if args.json {
let result = run_json(&store, &cwd).await;
store.close().await?;
return result;
}
let now = now_secs();
let current_seq = store.read_write_seq();
if let Ok(Some(cached)) = store.get(SNAPSHOT_KEY).await {
if let Some(snapshot) = cached.payload_as::<HealthSnapshot>() {
let age = now.saturating_sub(snapshot.computed_at);
if snapshot.write_seq == current_seq && age < SNAPSHOT_MAX_AGE_SECS {
display_cached_stats(&snapshot, age, &cwd);
store.close().await?;
return Ok(());
}
}
}
let use_color = io::stdout().is_terminal();
let (red, blue, green, yellow, gray, white, bold, reset) = if use_color {
(
colors::RED,
colors::BLUE,
colors::GREEN,
colors::YELLOW,
colors::GRAY,
colors::WHITE,
colors::BOLD,
colors::RESET,
)
} else {
("", "", "", "", "", "", "", "")
};
let (mut files, mut gotchas, mut decisions, mut notes, deps) = tokio::try_join!(
store.scan_prefix("file:"),
store.scan_prefix("gotcha:"),
store.scan_prefix("decision:"),
store.scan_prefix("dev_note:"),
store.scan_prefix("dep:"),
)?;
files.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
gotchas.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
decisions.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
notes.retain(|r| matches!(r.lifecycle, RecordLifecycle::Active));
let project = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!("\n{bold}{blue}◈ mati stats{reset} — project: {bold}{white}{project}{reset}\n");
println!(" {bold}{blue}Coverage{reset}");
let file_data: Vec<FileRecord> = files
.iter()
.filter_map(|r| r.payload_as::<FileRecord>())
.collect();
let files_with_purpose = file_data.iter().filter(|fr| !fr.purpose.is_empty()).count() as u32;
let total_files = files.len() as u32;
let purpose_pct = if total_files > 0 {
files_with_purpose as f32 / total_files as f32 * 100.0
} else {
0.0
};
let purpose_color = if purpose_pct >= 60.0 { green } else { yellow };
println!(
" Files with purpose {purpose_color}{files_with_purpose}{reset} / {white}{total_files}{reset} ({purpose_pct:.0}%)"
);
let hotspot_count = file_data.iter().filter(|fr| fr.is_hotspot).count();
let gotchas_per_hotspot = if hotspot_count > 0 {
gotchas.len() as f32 / hotspot_count as f32
} else {
0.0
};
let gph_color = if gotchas_per_hotspot >= 2.0 {
green
} else {
yellow
};
println!(
" Gotchas per hotspot {gph_color}{gotchas_per_hotspot:.1}{reset} (target >= 2.0)"
);
let gh = gotcha_health(&gotchas);
let stale_c = if gh.stale_or_worse == 0 {
green
} else {
yellow
};
println!(
" Stale gotchas {stale_c}{}{reset} {gray}(stale / liability / tombstone){reset}",
gh.stale_or_worse
);
let decisions_count = decisions.len() as u32;
let dec_color = if decisions_count > 0 { green } else { yellow };
println!(" Decisions documented {dec_color}{decisions_count}{reset}");
let knowledge_records: Vec<&Record> = gotchas.iter().chain(decisions.iter()).collect();
let avg_confidence = if knowledge_records.is_empty() {
0.0
} else {
let sum: f32 = knowledge_records.iter().map(|r| r.confidence.value).sum();
sum / knowledge_records.len() as f32
};
let conf_color = if avg_confidence >= 0.6 { green } else { yellow };
if knowledge_records.is_empty() {
println!(" Avg confidence {gray}— (no gotchas or decisions yet){reset}");
} else {
println!(
" Avg confidence {conf_color}{avg_confidence:.2}{reset} {gray}(gotchas + decisions, n={}){reset}",
knowledge_records.len()
);
}
let gap_list = gaps::analyze(
&files,
&gotchas,
&decisions,
&deps,
&std::collections::HashMap::new(),
);
let gap_count = gap_list.len() as u32;
let gap_color = if gap_count == 0 { green } else { yellow };
println!(" Knowledge gaps {gap_color}{gap_count}{reset}");
println!();
println!(" {bold}{blue}Knowledge velocity (30d){reset}");
let thirty_days_ago = now.saturating_sub(30 * 86400);
let all_records: Vec<&Record> = files
.iter()
.chain(gotchas.iter())
.chain(decisions.iter())
.chain(notes.iter())
.chain(deps.iter())
.collect();
let new_records_30d = all_records
.iter()
.filter(|r| r.created_at >= thirty_days_ago)
.count() as u32;
let vel_color = if new_records_30d > 0 { green } else { yellow };
println!(" New records added {vel_color}{new_records_30d}{reset}");
let multi_contributor = all_records
.iter()
.filter(|r| r.confidence.contributor_count >= 2)
.count() as u32;
let mc_color = if multi_contributor > 0 { green } else { yellow };
println!(" Confirmed by 2+ devs {mc_color}{multi_contributor}{reset}");
{
use std::collections::HashSet;
let prop_files: Vec<&FileRecord> = file_data
.iter()
.filter(|fr| {
fr.propagated_staleness
.as_ref()
.is_some_and(|p| p.source_count > 0)
})
.collect();
if !prop_files.is_empty() {
let sources: HashSet<&str> = prop_files
.iter()
.filter_map(|fr| {
fr.propagated_staleness
.as_ref()
.and_then(|p| p.primary_source.as_deref())
})
.collect();
println!(
" Propagation chains {yellow}{}{reset} files have inherited staleness from {white}{}{reset} source files",
prop_files.len(),
sources.len(),
);
}
}
println!();
println!(" {bold}{blue}Onboarding readiness{reset}");
let onboarding_score = onboarding::compute_from_records(&files, &decisions, &gotchas);
let min_color = if onboarding_score.estimated_minutes <= 10.0 {
green
} else {
yellow
};
println!(
" Estimated onboarding {min_color}{:.0} min{reset}",
onboarding_score.estimated_minutes
);
let critical_uncovered = file_data
.iter()
.filter(|fr| fr.is_hotspot && fr.purpose.is_empty())
.count();
let cu_color = if critical_uncovered == 0 {
green
} else {
yellow
};
println!(" Critical files uncov. {cu_color}{critical_uncovered}{reset}");
let orphaned_decisions = gap_list
.iter()
.filter(|g| g.gap_type == mati_core::store::GapType::OrphanedDecision)
.count();
let od_color = if orphaned_decisions == 0 {
green
} else {
yellow
};
println!(" Orphaned decisions {od_color}{orphaned_decisions}{reset}");
let low_confidence = all_records
.iter()
.filter(|r| r.confidence.value < 0.3)
.count();
let lc_color = if low_confidence == 0 { green } else { yellow };
println!(" Low-confidence (<0.3) {lc_color}{low_confidence}{reset}");
println!();
println!(" {bold}{blue}Compliance (7d){reset}");
let (hits_7d, misses_7d, bypasses_7d) = scan_compliance_7d(&store, now).await;
let total_lookups = hits_7d + misses_7d;
let hit_rate = if total_lookups > 0 {
hits_7d as f32 / total_lookups as f32 * 100.0
} else {
0.0
};
if total_lookups > 0 {
let hr_color = if hit_rate >= 80.0 { green } else { yellow };
println!(
" Hit rate {hr_color}{hit_rate:.0}%{reset} ({white}{hits_7d}{reset} hits / {white}{total_lookups}{reset} lookups)"
);
} else {
println!(" Hit rate {gray}\u{2014}{reset} (no hook data yet)");
}
let bp_color = if bypasses_7d == 0 { green } else { yellow };
if bypasses_7d > 0 || total_lookups > 0 {
println!(" Bypasses {bp_color}{bypasses_7d}{reset}");
} else {
println!(" Bypasses {gray}\u{2014}{reset}");
}
let fail_open = scan_fail_open_log(now);
if fail_open.count_7d > 0 {
let ago = format_ago(fail_open.last_ago_secs);
println!(
" Daemon unreachable {red}{}{reset} {gray}(last: {ago} ago){reset}",
fail_open.count_7d
);
}
println!();
if let Some((counts, derived)) = enforcement_metrics_30d(&store, now * 1_000).await {
if counts.total > 0 {
println!(" {bold}{blue}Enforcement (30d){reset}");
println!(" Total events {white}{}{reset}", counts.total);
let deny_color = if counts.denials > 0 { red } else { green };
println!(
" Denials (blocked) {deny_color}{}{reset}",
counts.denials
);
println!(
" Allowed after receipt {green}{}{reset}",
counts.allowed_after_receipt
);
println!(
" Consulted (receipts) {white}{}{reset}",
counts.receipts_minted
);
let bp_c = if counts.bypasses > 0 { red } else { green };
println!(
" Bypasses {bp_c}{}{reset}",
counts.bypasses
);
let gap_c = if counts.gaps > 0 { yellow } else { green };
println!(" Gaps {gap_c}{}{reset}", counts.gaps);
match derived.blocks_per_session {
Some(bps) => println!(
" Blocks / session {white}{bps:.1}{reset} {gray}({} sessions){reset}",
derived.blocked_sessions
),
None => println!(" Blocks / session {gray}\u{2014}{reset}"),
}
match derived.median_time_to_consult_ms {
Some(ms) => println!(
" Median time-to-consult {white}{}{reset} {gray}(n={}){reset}",
format_duration_ms(ms),
derived.consult_pairs
),
None => println!(" Median time-to-consult {gray}\u{2014}{reset}"),
}
if counts.controls_changed > 0 {
println!(
" Gotcha lifecycle {gray}created{reset} {white}{}{reset} {gray}· confirmed{reset} {white}{}{reset} {gray}· updated{reset} {white}{}{reset} {gray}· removed{reset} {white}{}{reset}",
counts.controls_created,
counts.controls_confirmed,
counts.controls_updated,
counts.controls_removed
);
}
println!();
}
}
let snapshot = HealthSnapshot {
files_with_purpose,
total_files,
purpose_coverage: if total_files > 0 {
files_with_purpose as f32 / total_files as f32
} else {
0.0
},
gotchas_per_hotspot,
decisions_documented: decisions_count,
avg_confidence,
knowledge_gaps: gap_count,
new_records_30d,
multi_contributor_records: multi_contributor,
estimated_minutes: onboarding_score.estimated_minutes,
critical_files_covered: onboarding_score.critical_files_covered,
gotcha_coverage: onboarding_score.gotcha_coverage,
decision_coverage: onboarding_score.decision_coverage,
critical_uncovered: critical_uncovered as u32,
orphaned_decisions: orphaned_decisions as u32,
low_confidence: low_confidence as u32,
hits_7d,
misses_7d,
hit_rate_7d: if total_lookups > 0 {
hits_7d as f32 / total_lookups as f32
} else {
0.0
},
bypasses_7d,
computed_at: now,
write_seq: current_seq,
};
match write_snapshot_record(&store, &snapshot, now).await {
Ok(()) => println!(" {gray}Snapshot written: {SNAPSHOT_KEY}{reset}"),
Err(e) => tracing::debug!("stats: snapshot write skipped: {e}"),
}
let unconfirmed: Vec<&mati_core::store::Record> =
gotchas.iter().filter(|r| !gotcha_is_confirmed(r)).collect();
if !unconfirmed.is_empty() {
let oldest_created = unconfirmed
.iter()
.map(|r| r.created_at)
.min()
.unwrap_or(now);
let oldest_days = (now.saturating_sub(oldest_created)) / 86400;
let confirmed_total = gotchas.len() - unconfirmed.len();
let confirmation_rate = if gotchas.is_empty() {
0
} else {
(confirmed_total as f32 / gotchas.len() as f32 * 100.0) as u32
};
println!();
println!(" {bold}{blue}Review backlog{reset}");
let age_color = if oldest_days > 14 { yellow } else { white };
println!(
" Pending {yellow}{}{reset}",
unconfirmed.len()
);
println!(
" Confirmation rate {white}{confirmation_rate}%{reset} {gray}({confirmed_total}/{} gotchas){reset}",
gotchas.len()
);
println!(" Oldest pending {age_color}{oldest_days}d{reset}");
}
println!();
store.close().await?;
Ok(())
}
async fn write_snapshot_record(
store: &StoreProxy,
snapshot: &HealthSnapshot,
now: u64,
) -> Result<()> {
let record = Record {
key: SNAPSHOT_KEY.to_string(),
value: String::new(),
payload: serde_json::to_value(snapshot).ok(),
category: Category::Analytics,
priority: Priority::Normal,
tags: vec![],
created_at: now,
updated_at: now,
ref_url: None,
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id: mati_core::store::stable_device_id(),
logical_clock: 1,
wall_clock: now,
},
quality: QualityScore::layer0_default(),
access_count: 0,
last_accessed: 0,
source: RecordSource::StaticAnalysis,
confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
gap_analysis_score: 0.0,
};
store.put(SNAPSHOT_KEY, &record).await
}
pub async fn seed_snapshot(
store: &Store,
files: &[Record],
gotchas: &[Record],
decisions: &[Record],
deps: &[Record],
now: u64,
) -> Result<()> {
use mati_core::health::onboarding;
use mati_core::store::FileRecord;
let file_data: Vec<FileRecord> = files
.iter()
.filter_map(|r| r.payload_as::<FileRecord>())
.collect();
let files_with_purpose = file_data.iter().filter(|fr| !fr.purpose.is_empty()).count() as u32;
let total_files = files.len() as u32;
let hotspot_count = file_data.iter().filter(|fr| fr.is_hotspot).count();
let gotchas_per_hotspot = if hotspot_count > 0 {
gotchas.len() as f32 / hotspot_count as f32
} else {
0.0
};
let decisions_count = decisions.len() as u32;
let all_knowledge: Vec<&Record> = gotchas.iter().chain(decisions.iter()).collect();
let avg_confidence = if all_knowledge.is_empty() {
0.0
} else {
let sum: f32 = all_knowledge.iter().map(|r| r.confidence.value).sum();
sum / all_knowledge.len() as f32
};
let gap_count = 0u32;
let thirty_days_ago = now.saturating_sub(30 * 86400);
let all_records: Vec<&Record> = files
.iter()
.chain(gotchas.iter())
.chain(decisions.iter())
.chain(deps.iter())
.collect();
let new_records_30d = all_records
.iter()
.filter(|r| r.created_at >= thirty_days_ago)
.count() as u32;
let multi_contributor = all_records
.iter()
.filter(|r| r.confidence.contributor_count >= 2)
.count() as u32;
let onboarding_score = onboarding::compute_from_records(files, decisions, gotchas);
let critical_uncovered = file_data
.iter()
.filter(|fr| fr.is_hotspot && fr.purpose.is_empty())
.count() as u32;
let orphaned_decisions = 0u32; let low_confidence = all_records
.iter()
.filter(|r| r.confidence.value < 0.3)
.count() as u32;
let write_seq = store.read_write_seq();
let snapshot = HealthSnapshot {
files_with_purpose,
total_files,
purpose_coverage: if total_files > 0 {
files_with_purpose as f32 / total_files as f32
} else {
0.0
},
gotchas_per_hotspot,
decisions_documented: decisions_count,
avg_confidence,
knowledge_gaps: gap_count,
new_records_30d,
multi_contributor_records: multi_contributor,
estimated_minutes: onboarding_score.estimated_minutes,
critical_files_covered: onboarding_score.critical_files_covered,
gotcha_coverage: onboarding_score.gotcha_coverage,
decision_coverage: onboarding_score.decision_coverage,
critical_uncovered,
orphaned_decisions,
low_confidence,
hits_7d: 0,
misses_7d: 0,
hit_rate_7d: 0.0,
bypasses_7d: 0,
computed_at: now,
write_seq,
};
write_snapshot_record_direct(store, &snapshot, now).await
}
async fn write_snapshot_record_direct(
store: &Store,
snapshot: &HealthSnapshot,
now: u64,
) -> Result<()> {
let record = Record {
key: SNAPSHOT_KEY.to_string(),
value: String::new(),
payload: serde_json::to_value(snapshot).ok(),
category: Category::Analytics,
priority: Priority::Normal,
tags: vec![],
created_at: now,
updated_at: now,
ref_url: None,
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id: mati_core::store::stable_device_id(),
logical_clock: 1,
wall_clock: now,
},
quality: QualityScore::layer0_default(),
access_count: 0,
last_accessed: 0,
source: RecordSource::StaticAnalysis,
confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
gap_analysis_score: 0.0,
};
store.put(SNAPSHOT_KEY, &record).await
}
fn display_cached_stats(s: &HealthSnapshot, age: u64, cwd: &std::path::Path) {
let use_color = io::stdout().is_terminal();
let (red, blue, green, yellow, gray, white, bold, reset) = if use_color {
(
colors::RED,
colors::BLUE,
colors::GREEN,
colors::YELLOW,
colors::GRAY,
colors::WHITE,
colors::BOLD,
colors::RESET,
)
} else {
("", "", "", "", "", "", "", "")
};
let project = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!(
"\n{bold}{blue}◈ mati stats{reset} — project: {bold}{white}{project}{reset} {gray}(cached {age}s ago){reset}\n"
);
println!(" {bold}{blue}Coverage{reset}");
let purpose_pct = if s.total_files > 0 {
s.files_with_purpose as f32 / s.total_files as f32 * 100.0
} else {
0.0
};
let purpose_color = if purpose_pct >= 60.0 { green } else { yellow };
println!(
" Files with purpose {purpose_color}{}{reset} / {white}{}{reset} ({purpose_pct:.0}%)",
s.files_with_purpose, s.total_files
);
let gph_color = if s.gotchas_per_hotspot >= 2.0 {
green
} else {
yellow
};
println!(
" Gotchas per hotspot {gph_color}{:.1}{reset} (target >= 2.0)",
s.gotchas_per_hotspot
);
let dec_color = if s.decisions_documented > 0 {
green
} else {
yellow
};
println!(
" Decisions documented {dec_color}{}{reset}",
s.decisions_documented
);
let conf_color = if s.avg_confidence >= 0.6 {
green
} else {
yellow
};
if s.avg_confidence == 0.0 && s.decisions_documented == 0 {
println!(" Avg confidence {gray}— (no gotchas or decisions yet){reset}");
} else {
println!(
" Avg confidence {conf_color}{:.2}{reset} {gray}(gotchas + decisions){reset}",
s.avg_confidence
);
}
let gap_color = if s.knowledge_gaps == 0 { green } else { yellow };
println!(
" Knowledge gaps {gap_color}{}{reset}",
s.knowledge_gaps
);
println!();
println!(" {bold}{blue}Knowledge velocity (30d){reset}");
let vel_color = if s.new_records_30d > 0 { green } else { yellow };
println!(
" New records added {vel_color}{}{reset}",
s.new_records_30d
);
let mc_color = if s.multi_contributor_records > 0 {
green
} else {
yellow
};
println!(
" Confirmed by 2+ devs {mc_color}{}{reset}",
s.multi_contributor_records
);
println!();
println!(" {bold}{blue}Onboarding readiness{reset}");
let min_color = if s.estimated_minutes <= 10.0 {
green
} else {
yellow
};
println!(
" Estimated onboarding {min_color}{:.0} min{reset}",
s.estimated_minutes
);
let cu_color = if s.critical_uncovered == 0 {
green
} else {
yellow
};
println!(
" Critical files uncov. {cu_color}{}{reset}",
s.critical_uncovered
);
let od_color = if s.orphaned_decisions == 0 {
green
} else {
yellow
};
println!(
" Orphaned decisions {od_color}{}{reset}",
s.orphaned_decisions
);
let lc_color = if s.low_confidence == 0 { green } else { yellow };
println!(
" Low-confidence (<0.3) {lc_color}{}{reset}",
s.low_confidence
);
println!();
println!(" {bold}{blue}Compliance (7d){reset}");
let total_lookups = s.hits_7d + s.misses_7d;
let hit_rate = if total_lookups > 0 {
s.hits_7d as f32 / total_lookups as f32 * 100.0
} else {
0.0
};
if total_lookups > 0 {
let hr_color = if hit_rate >= 80.0 { green } else { yellow };
println!(
" Hit rate {hr_color}{hit_rate:.0}%{reset} ({white}{}{reset} hits / {white}{total_lookups}{reset} lookups)",
s.hits_7d
);
} else {
println!(" Hit rate {gray}\u{2014}{reset} (no hook data yet)");
}
let bp_color = if s.bypasses_7d == 0 { green } else { yellow };
if s.bypasses_7d > 0 || total_lookups > 0 {
println!(
" Bypasses {bp_color}{}{reset}",
s.bypasses_7d
);
} else {
println!(" Bypasses {gray}\u{2014}{reset}");
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let fail_open = scan_fail_open_log(now);
if fail_open.count_7d > 0 {
let ago = format_ago(fail_open.last_ago_secs);
println!(
" Daemon unreachable {red}{}{reset} {gray}(last: {ago} ago){reset}",
fail_open.count_7d
);
}
println!();
}
async fn scan_compliance_7d(store: &StoreProxy, now: u64) -> (u64, u64, u64) {
let mut hits: u64 = 0;
let mut misses: u64 = 0;
let mut bypasses: u64 = 0;
for day_offset in 0..7 {
let day_ts = now.saturating_sub(day_offset * 86400);
let date = format_snapshot_date(day_ts);
let hit_key = format!("analytics:hit_{date}");
let miss_key = format!("analytics:miss_{date}");
let bypass_key = format!("compliance:miss_{date}");
let post_hit_key = format!("compliance:allow_after_receipt_{date}");
let codex_miss_key = format!("compliance:codex_shell_miss_{date}");
if let Ok(Some(record)) = store.get(&hit_key).await {
if let Some(agg) = record.payload_as::<DailyAgg>() {
hits += agg.count;
}
}
if let Ok(Some(record)) = store.get(&miss_key).await {
if let Some(agg) = record.payload_as::<DailyAgg>() {
misses += agg.count;
}
}
if let Ok(Some(record)) = store.get(&bypass_key).await {
if let Some(agg) = record.payload_as::<DailyAgg>() {
bypasses += agg.count;
}
}
if let Ok(Some(record)) = store.get(&post_hit_key).await {
if let Some(agg) = record.payload_as::<DailyAgg>() {
hits += agg.count;
}
}
if let Ok(Some(record)) = store.get(&codex_miss_key).await {
if let Some(agg) = record.payload_as::<DailyAgg>() {
misses += agg.count;
bypasses += agg.count;
}
}
}
(hits, misses, bypasses)
}
fn format_snapshot_date(ts: u64) -> String {
let days = ts / 86400;
let z = days as i64 + 719_468;
let era = z / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{:04}-{:02}-{:02}", y, m, d)
}
struct FailOpenStats {
count_7d: u64,
last_ago_secs: u64,
}
fn read_capped(path: &std::path::Path, max_bytes: u64) -> Option<String> {
use std::io::{Read, Seek, SeekFrom};
let mut f = std::fs::File::open(path).ok()?;
let len = f.metadata().ok()?.len();
let read_len = if len > max_bytes { max_bytes } else { len };
if read_len == 0 {
return Some(String::new());
}
let start = len.saturating_sub(read_len);
if start > 0 {
f.seek(SeekFrom::Start(start)).ok()?;
}
let mut buf = Vec::with_capacity(read_len as usize);
f.take(read_len).read_to_end(&mut buf).ok()?;
Some(String::from_utf8_lossy(&buf).into_owned())
}
const FAIL_OPEN_SCAN_MAX_READ_BYTES: u64 = 64 * 1024 * 1024;
fn scan_fail_open_log(now: u64) -> FailOpenStats {
let log_path = match dirs::home_dir() {
Some(h) => h.join(".mati").join("fail_open.log"),
None => {
return FailOpenStats {
count_7d: 0,
last_ago_secs: 0,
}
}
};
scan_fail_open_log_at(&log_path, now)
}
fn scan_fail_open_log_at(log_path: &std::path::Path, now: u64) -> FailOpenStats {
let content = match read_capped(log_path, FAIL_OPEN_SCAN_MAX_READ_BYTES) {
Some(c) => c,
None => {
return FailOpenStats {
count_7d: 0,
last_ago_secs: 0,
}
}
};
let cutoff = now.saturating_sub(7 * 86400);
let mut count: u64 = 0;
let mut latest_ts: u64 = 0;
for line in content.lines() {
if !line.contains("FAIL_OPEN") {
continue;
}
let ts_str = match line.split_whitespace().next() {
Some(s) => s,
None => continue,
};
let ts = parse_iso_timestamp(ts_str);
if ts == 0 {
continue;
}
if ts >= cutoff {
count += 1;
}
if ts > latest_ts {
latest_ts = ts;
}
}
let last_ago = if latest_ts > 0 {
now.saturating_sub(latest_ts)
} else {
0
};
FailOpenStats {
count_7d: count,
last_ago_secs: last_ago,
}
}
fn parse_iso_timestamp(s: &str) -> u64 {
if s.len() < 19 {
return 0;
}
let b = s.as_bytes();
let year = parse_u64(&s[0..4]);
let month = parse_u64(&s[5..7]);
let day = parse_u64(&s[8..10]);
let hour = parse_u64(&s[11..13]);
let min = parse_u64(&s[14..16]);
let sec = parse_u64(&s[17..19]);
if b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' {
return 0;
}
let days = civil_to_days(year, month, sec, day);
days * 86400 + hour * 3600 + min * 60 + sec
}
fn parse_u64(s: &str) -> u64 {
s.parse::<u64>().unwrap_or(0)
}
fn civil_to_days(y: u64, m: u64, _sec: u64, d: u64) -> u64 {
let y = y as i64;
let (y, m) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let doy = (153 * m + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
(era * 146_097 + doe as i64 - 719_468) as u64
}
fn format_ago(secs: u64) -> String {
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86400 {
format!("{}h", secs / 3600)
} else {
format!("{}d", secs / 86400)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_snapshot_date_epoch() {
assert_eq!(format_snapshot_date(0), "1970-01-01");
}
#[test]
fn format_snapshot_date_known() {
assert_eq!(format_snapshot_date(19737 * 86400), "2024-01-15");
}
#[test]
fn format_snapshot_date_leap_day() {
assert_eq!(format_snapshot_date(19782 * 86400), "2024-02-29");
}
#[test]
fn daily_agg_deserializes() {
let json = r#"{"count": 5, "keys": ["file:a.rs", "file:b.rs"]}"#;
let agg: DailyAgg = serde_json::from_str(json).unwrap();
assert_eq!(agg.count, 5);
assert_eq!(agg.keys.len(), 2);
}
fn sample_snapshot() -> HealthSnapshot {
HealthSnapshot {
files_with_purpose: 10,
total_files: 20,
purpose_coverage: 0.5,
gotchas_per_hotspot: 1.5,
decisions_documented: 3,
avg_confidence: 0.45,
knowledge_gaps: 7,
new_records_30d: 15,
multi_contributor_records: 2,
estimated_minutes: 16.5,
critical_files_covered: 0.6,
gotcha_coverage: 0.3,
decision_coverage: 1.0,
critical_uncovered: 4,
orphaned_decisions: 1,
low_confidence: 3,
hits_7d: 42,
misses_7d: 8,
hit_rate_7d: 0.84,
bypasses_7d: 1,
computed_at: 1_710_520_800,
write_seq: 42,
}
}
#[test]
fn health_snapshot_serializes() {
let snapshot = sample_snapshot();
let json = serde_json::to_string(&snapshot).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["files_with_purpose"], 10);
assert_eq!(parsed["total_files"], 20);
assert!((parsed["purpose_coverage"].as_f64().unwrap() - 0.5).abs() < 0.01);
assert_eq!(parsed["knowledge_gaps"], 7);
assert_eq!(parsed["hits_7d"], 42);
assert_eq!(parsed["bypasses_7d"], 1);
assert_eq!(parsed["critical_uncovered"], 4);
assert_eq!(parsed["orphaned_decisions"], 1);
assert_eq!(parsed["low_confidence"], 3);
}
#[test]
fn health_snapshot_roundtrips() {
let snapshot = sample_snapshot();
let json = serde_json::to_string(&snapshot).unwrap();
let deserialized: HealthSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.files_with_purpose, snapshot.files_with_purpose);
assert_eq!(deserialized.total_files, snapshot.total_files);
assert_eq!(
deserialized.decisions_documented,
snapshot.decisions_documented
);
assert_eq!(deserialized.knowledge_gaps, snapshot.knowledge_gaps);
assert_eq!(deserialized.new_records_30d, snapshot.new_records_30d);
assert_eq!(
deserialized.multi_contributor_records,
snapshot.multi_contributor_records
);
assert_eq!(deserialized.critical_uncovered, snapshot.critical_uncovered);
assert_eq!(deserialized.orphaned_decisions, snapshot.orphaned_decisions);
assert_eq!(deserialized.low_confidence, snapshot.low_confidence);
assert_eq!(deserialized.hits_7d, snapshot.hits_7d);
assert_eq!(deserialized.misses_7d, snapshot.misses_7d);
assert_eq!(deserialized.bypasses_7d, snapshot.bypasses_7d);
assert_eq!(deserialized.computed_at, snapshot.computed_at);
assert!((deserialized.avg_confidence - snapshot.avg_confidence).abs() < 0.001);
assert!((deserialized.estimated_minutes - snapshot.estimated_minutes).abs() < 0.01);
}
#[test]
fn health_snapshot_backward_compat_missing_new_fields() {
let old_json = r#"{
"files_with_purpose": 5,
"total_files": 10,
"purpose_coverage": 0.5,
"gotchas_per_hotspot": 2.0,
"decisions_documented": 1,
"avg_confidence": 0.7,
"knowledge_gaps": 2,
"new_records_30d": 8,
"multi_contributor_records": 0,
"estimated_minutes": 12.0,
"critical_files_covered": 0.8,
"gotcha_coverage": 0.5,
"decision_coverage": 1.0,
"hits_7d": 20,
"misses_7d": 5,
"hit_rate_7d": 0.8,
"bypasses_7d": 0,
"computed_at": 1710000000
}"#;
let snapshot: HealthSnapshot = serde_json::from_str(old_json).unwrap();
assert_eq!(snapshot.critical_uncovered, 0);
assert_eq!(snapshot.orphaned_decisions, 0);
assert_eq!(snapshot.low_confidence, 0);
assert_eq!(snapshot.files_with_purpose, 5);
assert_eq!(snapshot.total_files, 10);
assert_eq!(snapshot.hits_7d, 20);
}
#[test]
fn scan_fail_open_log_does_not_oom_on_pathological_file() {
use std::io::{Seek, SeekFrom, Write};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fail_open.log");
{
let mut f = std::fs::File::create(&path).unwrap();
f.seek(SeekFrom::Start(FAIL_OPEN_SCAN_MAX_READ_BYTES + 1024))
.unwrap();
f.write_all(
b"\n2026-04-29T12:00:00Z FAIL_OPEN hook=hook-decide file=src/x.rs reason=test\n",
)
.unwrap();
}
let pre_size = std::fs::metadata(&path).unwrap().len();
assert!(
pre_size > FAIL_OPEN_SCAN_MAX_READ_BYTES,
"test setup: file must exceed the read cap"
);
let now: u64 = 1_775_000_000; let stats = scan_fail_open_log_at(&path, now);
let _ = stats.count_7d;
let _ = stats.last_ago_secs;
}
#[test]
fn scan_fail_open_log_reads_full_file_under_cap() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fail_open.log");
let body = "\
2026-04-29T12:00:00Z FAIL_OPEN hook=hook-decide file=a.rs reason=x\n\
2026-04-29T12:00:01Z FAIL_OPEN hook=hook-decide file=b.rs reason=y\n\
2026-04-29T12:00:02Z FAIL_OPEN hook=hook-decide file=c.rs reason=z\n";
std::fs::write(&path, body).unwrap();
let latest = parse_iso_timestamp("2026-04-29T12:00:02Z");
assert!(latest > 0, "parser must accept ISO timestamps");
let now = latest + 100;
let stats = scan_fail_open_log_at(&path, now);
assert_eq!(stats.count_7d, 3, "all 3 events fall within 7-day window");
assert_eq!(stats.last_ago_secs, 100);
}
#[test]
fn scan_fail_open_log_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fail_open.log");
std::fs::write(&path, b"").unwrap();
let stats = scan_fail_open_log_at(&path, 1_775_000_000);
assert_eq!(stats.count_7d, 0);
assert_eq!(stats.last_ago_secs, 0);
}
#[test]
fn fail_open_log_round_trip_writer_reader() {
use std::time::{SystemTime, UNIX_EPOCH};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("fail_open.log");
let now_before = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("test wall clock must be post-epoch")
.as_secs();
super::super::hook_decide::log_fail_open_at(
&path,
"src/cli/stats.rs",
"round-trip writer/reader format check",
);
let stats = scan_fail_open_log_at(&path, now_before + 5);
assert_eq!(
stats.count_7d, 1,
"writer's ISO timestamp must parse — count_7d=0 means format mismatch \
between iso_utc_now() and parse_iso_timestamp() (silently breaks mati stats)"
);
assert!(
stats.last_ago_secs <= 10,
"last fail-open event should be recent; got last_ago_secs={}",
stats.last_ago_secs
);
}
}