Skip to main content

memstead_cli/commands/
stats.rs

1use 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
113/// Count real (non-stub) entities by `entity_type`. Both engine
114/// flavours expose a `&Store`, so this helper is engine-agnostic.
115fn 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}