memstead_cli/commands/
status.rs1use std::collections::HashMap;
2
3use memstead_base::Store;
4use memstead_base::ingest::status::{
5 ProjectionStatus, Rollup, projection_rollup, projection_status,
6};
7use serde::Serialize;
8use serde_json::json;
9
10use crate::output::{print_json, print_markdown};
11use crate::setup::{CliContext, CliEngine};
12
13#[derive(Serialize)]
14struct EdgeTypeCount<'a> {
15 #[serde(rename = "type")]
16 rel_type: &'a str,
17 count: usize,
18}
19
20#[derive(Serialize)]
21struct TypeCount<'a> {
22 #[serde(rename = "type")]
23 entity_type: &'a str,
24 count: usize,
25}
26
27#[derive(Serialize)]
33struct StatusPayload<'a> {
34 rollup: Rollup,
35 total_nodes: usize,
36 real_nodes: usize,
37 stub_nodes: usize,
38 total_edges: usize,
39 edge_types: Vec<EdgeTypeCount<'a>>,
40 type_distribution: Vec<TypeCount<'a>>,
41 projections: Vec<ProjectionStatus>,
42}
43
44pub fn run(ctx: &CliContext) -> anyhow::Result<()> {
45 let root = ctx.workspace_shape().map(|(_, r)| r);
50
51 let (status, total, real, schema_counts, projections, rollup) = match ctx.cli_engine()? {
52 #[cfg(feature = "mem-repo")]
53 CliEngine::MemRepo(engine) => {
54 let status = engine.status();
55 let store: &Store = engine.store();
56 let projections = root
57 .as_deref()
58 .map(|r| projection_status(&engine, r))
59 .unwrap_or_default();
60 let rollup = root
61 .as_deref()
62 .map(|r| projection_rollup(&engine, r))
63 .unwrap_or_default();
64 (
65 status,
66 store.len(),
67 store.all_entities().filter(|e| !e.stub).count(),
68 count_by_type(store),
69 projections,
70 rollup,
71 )
72 }
73 CliEngine::Filesystem(engine) => {
74 let status = engine.status();
75 let store: &Store = engine.store();
76 let projections = root
77 .as_deref()
78 .map(|r| projection_status(&engine, r))
79 .unwrap_or_default();
80 let rollup = root
81 .as_deref()
82 .map(|r| projection_rollup(&engine, r))
83 .unwrap_or_default();
84 (
85 status,
86 store.len(),
87 store.all_entities().filter(|e| !e.stub).count(),
88 count_by_type(store),
89 projections,
90 rollup,
91 )
92 }
93 };
94 let stubs = total - real;
95
96 let mut edge_pairs: Vec<_> = status.edge_types.iter().collect();
97 edge_pairs.sort_by(|a, b| b.1.cmp(a.1));
98
99 let mut schema_pairs: Vec<(String, usize)> = schema_counts.into_iter().collect();
100 schema_pairs.sort_by_key(|p| std::cmp::Reverse(p.1));
101
102 if ctx.json {
103 let payload = StatusPayload {
104 rollup,
105 total_nodes: total,
106 real_nodes: real,
107 stub_nodes: stubs,
108 total_edges: status.edge_count,
109 edge_types: edge_pairs
110 .iter()
111 .map(|(t, c)| EdgeTypeCount {
112 rel_type: t,
113 count: **c,
114 })
115 .collect(),
116 type_distribution: schema_pairs
117 .iter()
118 .map(|(s, c)| TypeCount {
119 entity_type: s,
120 count: *c,
121 })
122 .collect(),
123 projections,
124 };
125 return print_json(&json!(payload));
126 }
127
128 let mut lines = Vec::new();
129
130 lines.push("# Status".to_string());
134 lines.push(String::new());
135 lines.push(format!("**Verdict:** {}", rollup.verdict.as_wire()));
136 lines.push(String::new());
137 lines.push(rollup.headline.clone());
138 if !rollup.actions.is_empty() {
139 lines.push(String::new());
140 lines.push("## Do next".to_string());
141 lines.push(String::new());
142 for action in &rollup.actions {
143 lines.push(format!("- {action}"));
144 }
145 }
146 lines.push(String::new());
147
148 lines.push("# Graph status".to_string());
149 lines.push(String::new());
150 lines.push(format!("- Nodes: {total} ({real} real, {stubs} stubs)"));
151 lines.push(format!("- Edges: {}", status.edge_count));
152 if !edge_pairs.is_empty() {
153 let edges: Vec<String> = edge_pairs
154 .iter()
155 .map(|(t, c)| format!("{t} ({c})"))
156 .collect();
157 lines.push(format!("- Edge types: {}", edges.join(", ")));
158 }
159 if !schema_pairs.is_empty() {
160 let schemas: Vec<String> = schema_pairs
161 .iter()
162 .map(|(s, c)| format!("{s} ({c})"))
163 .collect();
164 lines.push(format!("- Types: {}", schemas.join(", ")));
165 }
166 if !projections.is_empty() {
167 lines.push(String::new());
168 lines.push("## Projections".to_string());
169 lines.push(String::new());
170 for p in &projections {
171 lines.push(format!(
172 "- `{}` → `{}` — operations: {}; advance: {} pending, {} disposed",
173 p.binding,
174 p.destination_mem,
175 p.operations.join(", "),
176 p.advance.pending,
177 p.advance.disposed,
178 ));
179 for (facet, state) in &p.state {
180 lines.push(format!(
181 " - {facet}: signal {}, synced {}, verified {}",
182 state.signal,
183 state.synced.as_deref().unwrap_or("none"),
184 state.verified.as_deref().unwrap_or("none"),
185 ));
186 }
187 }
188 }
189 print_markdown(&lines.join("\n"));
190 Ok(())
191}
192
193fn count_by_type(store: &Store) -> HashMap<String, usize> {
196 let mut counts: HashMap<String, usize> = HashMap::new();
197 for e in store.all_entities().filter(|e| !e.stub) {
198 *counts.entry(e.entity_type.clone()).or_default() += 1;
199 }
200 counts
201}