- 1
//! `vak digest [--days N]` (docs/design/29-personal-os.md P3): the - 2
//! weekly usage report rendered as aligned plain text from - 3
//! `vak_core::digest`. Pure rendering; all math lives in the library. - 4
- 5
use std::path::PathBuf; - 6
- 7
use vak_core::{Core, digest::DigestReport}; - 8
- 9
const MIN_DAYS: u32 = 1; - 10
const MAX_DAYS: u32 = 90; - 11
const TOP_ROWS: usize = 5; - 12
- 13
pub fn clamp_days(days: u32) -> u32 { - 14
days.clamp(MIN_DAYS, MAX_DAYS) - 15
} - 16
- 17
pub fn run_digest(cwd: PathBuf, days: u32) -> i32 { - 18
let core = match Core::new(cwd) { - 19
Ok(c) => c, - 20
Err(e) => { - 21
eprintln!("error: {e}"); - 22
return 2; - 23
} - 24
}; - 25
let days = clamp_days(days); - 26
let report = vak_core::digest::digest(&core.sessions_home(), &core.shared_data_home(), days); - 27
print_digest(&report); - 28
0 - 29
} - 30
- 31
fn usd(v: f64) -> String { - 32
format!("${v:.2}") - 33
} - 34
- 35
fn print_digest(report: &DigestReport) { - 36
let days = report.days; - 37
if report.dispatches == 0 - 38
&& report.memory_notes_appended == 0 - 39
&& report.skill_proposals_opened == 0 - 40
{ - 41
println!("digest — no usage recorded in the last {days} day(s)"); - 42
return; - 43
} - 44
- 45
println!( - 46
"digest — last {days} day(s), since {}", - 47
report - 48
.since - 49
.map(|s| s.to_rfc3339()) - 50
.unwrap_or_else(|| "—".into()) - 51
); - 52
- 53
let unpriced_suffix = if report.unpriced_rows > 0 { - 54
format!(" · {} unpriced dispatch(es) excluded", report.unpriced_rows) - 55
} else { - 56
String::new() - 57
}; - 58
println!( - 59
" {:<14} {} total{}", - 60
"spend", - 61
usd(report.total_usd), - 62
unpriced_suffix - 63
); - 64
println!( - 65
" {:<14} {} dispatch(es) across {} session(s)", - 66
"dispatches", - 67
report.dispatches, - 68
report.distinct_sessions.len() - 69
); - 70
println!( - 71
" {:<14} in {} / out {} / cache-read {}", - 72
"tokens", report.input_tokens, report.output_tokens, report.cache_read_tokens - 73
); - 74
- 75
let mut models: Vec<(String, vak_core::digest::ModelRollup)> = report - 76
.by_model - 77
.iter() - 78
.map(|(k, v)| (k.clone(), v.clone())) - 79
.collect(); - 80
models.sort_by(|a, b| { - 81
b.1.usd - 82
.total_cmp(&a.1.usd) - 83
.then(b.1.rows.cmp(&a.1.rows)) - 84
.then(a.0.cmp(&b.0)) - 85
}); - 86
let model_parts: Vec<String> = models - 87
.iter() - 88
.take(TOP_ROWS) - 89
.map(|(name, r)| { - 90
format!( - 91
"{name} {} ({} row(s), in {}/out {})", - 92
usd(r.usd), - 93
r.rows, - 94
r.input_tokens, - 95
r.output_tokens - 96
) - 97
}) - 98
.collect(); - 99
print_row("top models", &model_parts); - 100
- 101
let mut providers: Vec<(String, vak_core::digest::ProviderRollup)> = report - 102
.by_provider - 103
.iter() - 104
.map(|(k, v)| (k.clone(), v.clone())) - 105
.collect(); - 106
providers.sort_by(|a, b| { - 107
b.1.usd - 108
.total_cmp(&a.1.usd) - 109
.then(b.1.rows.cmp(&a.1.rows)) - 110
.then(a.0.cmp(&b.0)) - 111
}); - 112
let provider_parts: Vec<String> = providers - 113
.iter() - 114
.take(TOP_ROWS) - 115
.map(|(name, r)| format!("{name} {} ({} row(s))", usd(r.usd), r.rows)) - 116
.collect(); - 117
print_row("providers", &provider_parts); - 118
- 119
for day in &report.per_day { - 120
let unpriced = if day.unpriced_rows > 0 { - 121
format!(" · {} unpriced", day.unpriced_rows) - 122
} else { - 123
String::new() - 124
}; - 125
println!( - 126
" {:<14} {} {}{}", - 127
"per day", - 128
day.day, - 129
usd(day.usd), - 130
unpriced - 131
); - 132
} - 133
- 134
println!( - 135
" {:<14} {} note(s) appended · {} proposal(s) opened", - 136
"learning", report.memory_notes_appended, report.skill_proposals_opened - 137
); - 138
} - 139
- 140
fn print_row(label: &str, parts: &[String]) { - 141
if !parts.is_empty() { - 142
println!(" {:<14} {}", label, parts.join(" · ")); - 143
} - 144
} - 145
- 146
#[cfg(test)] - 147
mod tests { - 148
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 149
use super::*; - 150
- 151
#[test] - 152
fn days_clamp_to_one_through_ninety() { - 153
assert_eq!(clamp_days(0), 1); - 154
assert_eq!(clamp_days(1), 1); - 155
assert_eq!(clamp_days(7), 7); - 156
assert_eq!(clamp_days(90), 90); - 157
assert_eq!(clamp_days(91), 90); - 158
assert_eq!(clamp_days(u32::MAX), 90); - 159
} - 160
- 161
#[test] - 162
fn empty_and_populated_reports_render_without_panicking() { - 163
let empty = DigestReport { - 164
days: 7, - 165
..Default::default() - 166
}; - 167
print_digest(&empty); - 168
- 169
let mut populated = DigestReport { - 170
days: 3, - 171
total_usd: 4.2, - 172
unpriced_rows: 1, - 173
input_tokens: 100, - 174
output_tokens: 50, - 175
dispatches: 2, - 176
..Default::default() - 177
}; - 178
populated.by_model.insert( - 179
"claude-x".into(), - 180
vak_core::digest::ModelRollup { - 181
rows: 2, - 182
usd: 4.2, - 183
input_tokens: 100, - 184
output_tokens: 50, - 185
cache_read_tokens: 10, - 186
}, - 187
); - 188
populated.by_provider.insert( - 189
"anthropic".into(), - 190
vak_core::digest::ProviderRollup { rows: 2, usd: 4.2 }, - 191
); - 192
populated.distinct_sessions = vec!["s1".into()]; - 193
populated.per_day.push(vak_core::digest::DayRollup { - 194
day: "2026-08-24".into(), - 195
usd: 4.2, - 196
unpriced_rows: 1, - 197
}); - 198
print_digest(&populated); - 199
} - 200
} - 201
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.