use std::collections::BTreeMap;
use anyhow::Result;
use crate::context::CommandContext;
pub fn run() -> Result<()> {
let ctx = CommandContext::open(None)?;
let rows = ctx.session.store().stats_by_target_type_and_key()?;
if rows.is_empty() {
println!("no metadata stored");
return Ok(());
}
let (sqlite_count, git_ref_count) = ctx.session.store().stats_storage_counts()?;
println!("{sqlite_count} values in sqlite, {git_ref_count} values as git refs");
println!();
let (buckets, _) = ctx.session.store().stats_value_size_histogram()?;
let max_count = buckets.iter().map(|(_, c)| *c).max().unwrap_or(1).max(1);
let bar_width = 30usize;
println!("value sizes (inline):");
for (label, count) in &buckets {
let filled = ((*count as f64 / max_count as f64) * bar_width as f64).round() as usize;
let bar: String = "#".repeat(filled);
println!(" {label:>10} {bar:30} {count}");
}
println!();
let mut by_type: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
for (target_type, key, count) in &rows {
by_type
.entry(target_type.clone())
.or_default()
.entry(key.clone())
.or_insert(*count);
}
for (target_type, keys) in &by_type {
let grouped = group_keys_by_integer_pattern(keys);
let total: u64 = grouped.values().sum();
let tt = target_type.parse::<git_meta_lib::types::TargetType>()?;
let plural = tt.pluralize();
println!("{plural}: {total} keys");
let mut sorted: Vec<(&String, &u64)> = grouped.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
for (key, count) in sorted {
println!(" {key} {count}");
}
}
Ok(())
}
fn key_to_pattern(key: &str) -> String {
let mut result = String::new();
let mut chars = key.chars().peekable();
while let Some(c) = chars.next() {
if c.is_ascii_digit() {
result.push_str("[n]");
while chars.peek().is_some_and(char::is_ascii_digit) {
chars.next();
}
} else {
result.push(c);
}
}
result
}
fn group_keys_by_integer_pattern(keys: &BTreeMap<String, u64>) -> BTreeMap<String, u64> {
let mut pattern_keys: BTreeMap<String, Vec<&String>> = BTreeMap::new();
for key in keys.keys() {
let pattern = key_to_pattern(key);
pattern_keys.entry(pattern).or_default().push(key);
}
let mut result: BTreeMap<String, u64> = BTreeMap::new();
for (pattern, matching_keys) in &pattern_keys {
if matching_keys.len() > 1 {
let total: u64 = matching_keys.iter().map(|k| keys[*k]).sum();
result.insert(pattern.clone(), total);
} else {
let key = matching_keys[0];
result.insert(key.clone(), keys[key]);
}
}
result
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_key_to_pattern_no_digits() {
assert_eq!(key_to_pattern("agent:model"), "agent:model");
}
#[test]
fn test_key_to_pattern_with_digits() {
assert_eq!(key_to_pattern("agent:session-1"), "agent:session-[n]");
assert_eq!(key_to_pattern("agent:session-42"), "agent:session-[n]");
assert_eq!(key_to_pattern("step:1:result"), "step:[n]:result");
}
#[test]
fn test_key_to_pattern_multiple_digit_groups() {
assert_eq!(key_to_pattern("step:1:sub:2"), "step:[n]:sub:[n]");
}
#[test]
fn test_group_keys_collapses_integer_variants() {
let mut keys = BTreeMap::new();
keys.insert("agent:session-1".to_string(), 5u64);
keys.insert("agent:session-2".to_string(), 3u64);
keys.insert("agent:session-10".to_string(), 1u64);
keys.insert("agent:model".to_string(), 9u64);
let grouped = group_keys_by_integer_pattern(&keys);
assert_eq!(grouped.get("agent:session-[n]"), Some(&9u64)); assert_eq!(grouped.get("agent:model"), Some(&9u64));
assert!(!grouped.contains_key("agent:session-1"));
assert!(!grouped.contains_key("agent:session-2"));
}
#[test]
fn test_group_keys_keeps_single_int_key_as_is() {
let mut keys = BTreeMap::new();
keys.insert("agent:session-1".to_string(), 5u64);
keys.insert("agent:model".to_string(), 9u64);
let grouped = group_keys_by_integer_pattern(&keys);
assert_eq!(grouped.get("agent:session-1"), Some(&5u64));
assert_eq!(grouped.get("agent:model"), Some(&9u64));
assert!(!grouped.contains_key("agent:session-[n]"));
}
}