1use 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 rel_type: &'a str,
19 count: usize,
20}
21
22#[derive(Serialize)]
23struct TypeCount<'a> {
24 entity_type: &'a str,
27 count: usize,
28}
29
30#[derive(Serialize)]
36struct StatusPayload<'a> {
37 verdict_coverage: String,
40 rollup: Rollup,
41 mems: Vec<MemDurability>,
42 total_nodes: usize,
43 real_nodes: usize,
44 stub_nodes: usize,
45 total_edges: usize,
46 edge_types: Vec<EdgeTypeCount<'a>>,
47 type_distribution: Vec<TypeCount<'a>>,
48 projections: Vec<ProjectionStatus>,
49 #[serde(skip_serializing_if = "Vec::is_empty")]
55 quarantined: Vec<QuarantineLine>,
56}
57
58#[derive(Serialize)]
61struct QuarantineLine {
62 mem: String,
63 reason_code: String,
64 reason: String,
65}
66
67fn quarantine_roster(engine: &memstead_base::Engine) -> Vec<QuarantineLine> {
68 engine
69 .quarantined_mems()
70 .iter()
71 .map(|q| QuarantineLine {
72 mem: q.mount.mem.clone(),
73 reason_code: q.reason_code.clone(),
74 reason: q.reason_message.clone(),
75 })
76 .collect()
77}
78
79#[derive(Serialize)]
83struct MemDurability {
84 mem: String,
85 backend: &'static str,
86 durable: bool,
88 basis: &'static str,
91 #[serde(skip_serializing_if = "Option::is_none")]
94 unestablished: Option<&'static str>,
95}
96
97fn mem_durability(engine: &memstead_base::Engine) -> Vec<MemDurability> {
106 engine
107 .mounts()
108 .iter()
109 .map(|m| {
110 let head = engine.mem_head_sha(&m.mem).ok().flatten();
111 let basis = m.storage.durability_basis(head.as_deref());
112 MemDurability {
113 mem: m.mem.clone(),
114 backend: m.storage.backend_id(),
115 durable: m.storage.is_durable(),
116 basis: basis.as_wire(),
117 unestablished: match basis {
118 memstead_base::workspace::DurabilityBasis::Established => None,
119 memstead_base::workspace::DurabilityBasis::InferredFromMountKind => Some(
120 "writes land on disk and survive a restart; whether they reached \
121 version control is not something the engine can establish",
122 ),
123 },
124 }
125 })
126 .collect()
127}
128
129pub fn run(ctx: &CliContext) -> anyhow::Result<()> {
130 let root = ctx.workspace_shape().map(|(_, r)| r);
135
136 let (status, total, real, schema_counts, projections, rollup, mems, quarantined) =
137 match ctx.cli_engine()? {
138 #[cfg(feature = "mem-repo")]
139 CliEngine::MemRepo(engine) => {
140 let status = engine.status();
141 let store: &Store = engine.store();
142 let projections = root
143 .as_deref()
144 .map(|r| projection_status(&engine, r))
145 .unwrap_or_default();
146 let rollup = root
147 .as_deref()
148 .map(|r| projection_rollup(&engine, r))
149 .unwrap_or_default();
150 let mems = mem_durability(&engine);
151 let quarantined = quarantine_roster(&engine);
152 (
153 status,
154 store.len(),
155 store.all_entities().filter(|e| !e.stub).count(),
156 count_by_type(store),
157 projections,
158 rollup,
159 mems,
160 quarantined,
161 )
162 }
163 CliEngine::Filesystem(engine) => {
164 let status = engine.status();
165 let store: &Store = engine.store();
166 let projections = root
167 .as_deref()
168 .map(|r| projection_status(&engine, r))
169 .unwrap_or_default();
170 let rollup = root
171 .as_deref()
172 .map(|r| projection_rollup(&engine, r))
173 .unwrap_or_default();
174 let mems = mem_durability(&engine);
175 let quarantined = quarantine_roster(&engine);
176 (
177 status,
178 store.len(),
179 store.all_entities().filter(|e| !e.stub).count(),
180 count_by_type(store),
181 projections,
182 rollup,
183 mems,
184 quarantined,
185 )
186 }
187 };
188 let stubs = total - real;
189
190 let mut edge_pairs: Vec<_> = status.edge_types.iter().collect();
191 edge_pairs.sort_by(|a, b| b.1.cmp(a.1));
192
193 let mut schema_pairs: Vec<(String, usize)> = schema_counts.into_iter().collect();
194 schema_pairs.sort_by_key(|p| std::cmp::Reverse(p.1));
195
196 if ctx.json {
197 let payload = StatusPayload {
198 verdict_coverage: crate::coverage::STATUS
199 .axis_coverage()
200 .expect("status is a verdict surface")
201 .wire_line(),
202 rollup,
203 mems,
204 total_nodes: total,
205 real_nodes: real,
206 stub_nodes: stubs,
207 total_edges: status.edge_count,
208 edge_types: edge_pairs
209 .iter()
210 .map(|(t, c)| EdgeTypeCount {
211 rel_type: t,
212 count: **c,
213 })
214 .collect(),
215 type_distribution: schema_pairs
216 .iter()
217 .map(|(s, c)| TypeCount {
218 entity_type: s,
219 count: *c,
220 })
221 .collect(),
222 projections,
223 quarantined,
224 };
225 return print_json(&json!(payload));
226 }
227
228 let mut lines = Vec::new();
229
230 lines.push("# Status".to_string());
234 lines.push(String::new());
235 lines.push(format!(
239 "**Verdict:** {} — for {}",
240 rollup.verdict.as_wire(),
241 rollup.subject,
242 ));
243 if let Some(cov) = crate::coverage::STATUS.axis_coverage() {
246 lines.push(format!("**Verdict coverage:** {}", cov.wire_line()));
247 }
248
249 let unestablished: Vec<&MemDurability> =
253 mems.iter().filter(|m| m.unestablished.is_some()).collect();
254 if !unestablished.is_empty() {
255 lines.push(String::new());
256 lines.push("**Durability not established** for:".to_string());
257 for m in &unestablished {
258 lines.push(format!(
259 "- `{}` ({}) — {}",
260 m.mem,
261 m.backend,
262 m.unestablished.unwrap_or_default(),
263 ));
264 }
265 }
266 if !quarantined.is_empty() {
270 lines.push(String::new());
271 lines.push(format!(
272 "**Quarantined mems ({})** — attached but refused at load; each line carries \
273 the engine's reason and repair:",
274 quarantined.len()
275 ));
276 for q in &quarantined {
277 lines.push(format!("- `{}` [{}] {}", q.mem, q.reason_code, q.reason));
278 }
279 }
280 lines.push(String::new());
281 lines.push(rollup.headline.clone());
282 if !rollup.actions.is_empty() {
283 lines.push(String::new());
284 lines.push("## Do next".to_string());
285 lines.push(String::new());
286 for action in &rollup.actions {
287 lines.push(format!("- {action}"));
288 }
289 }
290 lines.push(String::new());
291
292 lines.push("# Graph status".to_string());
293 lines.push(String::new());
294 lines.push(format!("- Nodes: {total} ({real} real, {stubs} stubs)"));
295 lines.push(format!("- Edges: {}", status.edge_count));
296 if !edge_pairs.is_empty() {
297 let edges: Vec<String> = edge_pairs
298 .iter()
299 .map(|(t, c)| format!("{t} ({c})"))
300 .collect();
301 lines.push(format!("- Edge types: {}", edges.join(", ")));
302 }
303 if !schema_pairs.is_empty() {
304 let schemas: Vec<String> = schema_pairs
305 .iter()
306 .map(|(s, c)| format!("{s} ({c})"))
307 .collect();
308 lines.push(format!("- Types: {}", schemas.join(", ")));
309 }
310 if !projections.is_empty() {
311 lines.push(String::new());
312 lines.push("## Projections".to_string());
313 lines.push(String::new());
314 for p in &projections {
315 lines.push(format!(
316 "- `{}` → `{}` — operations: {}; advance: {} pending, {} disposed",
317 p.binding,
318 p.destination_mem,
319 p.operations.join(", "),
320 p.advance.pending,
321 p.advance.disposed,
322 ));
323 for (facet, state) in &p.state {
324 lines.push(format!(
325 " - {facet}: signal {}, synced {}, verified {}",
326 state.signal,
327 state.synced.as_deref().unwrap_or("none"),
328 state.verified.as_deref().unwrap_or("none"),
329 ));
330 }
331 }
332 }
333 print_markdown(&lines.join("\n"));
334 Ok(())
335}
336
337fn count_by_type(store: &Store) -> HashMap<String, usize> {
340 let mut counts: HashMap<String, usize> = HashMap::new();
341 for e in store.all_entities().filter(|e| !e.stub) {
342 *counts.entry(e.entity_type.clone()).or_default() += 1;
343 }
344 counts
345}