memstead_cli/commands/
stats.rs1use std::collections::HashMap;
2
3use memstead_base::Store;
4use serde::Serialize;
5use serde_json::json;
6
7use crate::output::{print_json, print_markdown};
8use crate::setup::{CliContext, CliEngine};
9
10#[derive(Serialize)]
11struct EdgeTypeCount<'a> {
12 #[serde(rename = "type")]
13 rel_type: &'a str,
14 count: usize,
15}
16
17#[derive(Serialize)]
18struct TypeCount<'a> {
19 #[serde(rename = "type")]
20 entity_type: &'a str,
21 count: usize,
22}
23
24#[derive(Serialize)]
25struct StatsPayload<'a> {
26 total_nodes: usize,
27 real_nodes: usize,
28 stub_nodes: usize,
29 total_edges: usize,
30 edge_types: Vec<EdgeTypeCount<'a>>,
31 type_distribution: Vec<TypeCount<'a>>,
32}
33
34pub fn run(ctx: &CliContext) -> anyhow::Result<()> {
35 let (stats, total, real, schema_counts) = match ctx.cli_engine()? {
36 #[cfg(feature = "mem-repo")]
37 CliEngine::MemRepo(engine) => {
38 let stats = engine.stats();
39 let store: &Store = engine.store();
40 (
41 stats,
42 store.len(),
43 store.all_entities().filter(|e| !e.stub).count(),
44 count_by_type(store),
45 )
46 }
47 CliEngine::Filesystem(engine) => {
48 let stats = engine.stats();
49 let store: &Store = engine.store();
50 (
51 stats,
52 store.len(),
53 store.all_entities().filter(|e| !e.stub).count(),
54 count_by_type(store),
55 )
56 }
57 };
58 let stubs = total - real;
59
60 let mut edge_pairs: Vec<_> = stats.edge_types.iter().collect();
61 edge_pairs.sort_by(|a, b| b.1.cmp(a.1));
62
63 let mut schema_pairs: Vec<(String, usize)> = schema_counts.into_iter().collect();
64 schema_pairs.sort_by_key(|p| std::cmp::Reverse(p.1));
65
66 if ctx.json {
67 let payload = StatsPayload {
68 total_nodes: total,
69 real_nodes: real,
70 stub_nodes: stubs,
71 total_edges: stats.edge_count,
72 edge_types: edge_pairs
73 .iter()
74 .map(|(t, c)| EdgeTypeCount {
75 rel_type: t,
76 count: **c,
77 })
78 .collect(),
79 type_distribution: schema_pairs
80 .iter()
81 .map(|(s, c)| TypeCount {
82 entity_type: s,
83 count: *c,
84 })
85 .collect(),
86 };
87 return print_json(&json!(payload));
88 }
89
90 let mut lines = Vec::new();
91 lines.push("# Graph stats".to_string());
92 lines.push(String::new());
93 lines.push(format!("- Nodes: {total} ({real} real, {stubs} stubs)"));
94 lines.push(format!("- Edges: {}", stats.edge_count));
95 if !edge_pairs.is_empty() {
96 let edges: Vec<String> = edge_pairs
97 .iter()
98 .map(|(t, c)| format!("{t} ({c})"))
99 .collect();
100 lines.push(format!("- Edge types: {}", edges.join(", ")));
101 }
102 if !schema_pairs.is_empty() {
103 let schemas: Vec<String> = schema_pairs
104 .iter()
105 .map(|(s, c)| format!("{s} ({c})"))
106 .collect();
107 lines.push(format!("- Types: {}", schemas.join(", ")));
108 }
109 print_markdown(&lines.join("\n"));
110 Ok(())
111}
112
113fn count_by_type(store: &Store) -> HashMap<String, usize> {
116 let mut counts: HashMap<String, usize> = HashMap::new();
117 for e in store.all_entities().filter(|e| !e.stub) {
118 *counts.entry(e.entity_type.clone()).or_default() += 1;
119 }
120 counts
121}