1use std::collections::BTreeMap;
2
3use memstead_base::Store;
4use memstead_base::ingest::status::{ProjectionStatus, Rollup, projection_overview};
5use serde::Serialize;
6use serde_json::json;
7
8use crate::output::{print_json, print_markdown};
9use crate::setup::{CliContext, CliEngine};
10
11#[derive(Serialize)]
12struct EdgeTypeCount<'a> {
13 rel_type: &'a str,
17 count: usize,
18}
19
20#[derive(Serialize)]
21struct TypeCount<'a> {
22 entity_type: &'a str,
25 count: usize,
26}
27
28#[derive(Serialize)]
34struct StatusPayload<'a> {
35 verdict_coverage: String,
38 rollup: Rollup,
39 mems: Vec<MemDurability>,
40 total_nodes: usize,
41 real_nodes: usize,
42 stub_nodes: usize,
43 total_edges: usize,
44 edge_types: Vec<EdgeTypeCount<'a>>,
45 type_distribution: Vec<TypeCount<'a>>,
46 projections: Vec<ProjectionStatus>,
47 #[serde(skip_serializing_if = "Vec::is_empty")]
53 quarantined: Vec<QuarantineLine>,
54}
55
56#[derive(Serialize)]
59struct QuarantineLine {
60 mem: String,
61 reason_code: String,
62 reason: String,
63}
64
65fn quarantine_roster(engine: &memstead_base::Engine) -> Vec<QuarantineLine> {
66 engine
67 .quarantined_mems()
68 .iter()
69 .map(|q| QuarantineLine {
70 mem: q.mount.mem.clone(),
71 reason_code: q.reason_code.clone(),
72 reason: q.reason_message.clone(),
73 })
74 .collect()
75}
76
77#[derive(Serialize)]
81struct MemDurability {
82 mem: String,
83 backend: &'static str,
84 durable: bool,
86 basis: &'static str,
89 #[serde(skip_serializing_if = "Option::is_none")]
92 unestablished: Option<&'static str>,
93}
94
95fn mem_durability(engine: &memstead_base::Engine) -> Vec<MemDurability> {
104 engine
105 .mounts()
106 .iter()
107 .map(|m| {
108 let head = engine.mem_head_sha(&m.mem).ok().flatten();
109 let basis = m.storage.durability_basis(head.as_deref());
110 MemDurability {
111 mem: m.mem.clone(),
112 backend: m.storage.backend_id(),
113 durable: m.storage.is_durable(),
114 basis: basis.as_wire(),
115 unestablished: match basis {
116 memstead_base::workspace::DurabilityBasis::Established => None,
117 memstead_base::workspace::DurabilityBasis::InferredFromMountKind => Some(
118 "writes land on disk and survive a restart; whether they reached \
119 version control is not something the engine can establish",
120 ),
121 },
122 }
123 })
124 .collect()
125}
126
127pub fn run(ctx: &CliContext) -> anyhow::Result<()> {
128 let root = ctx.workspace_shape().map(|(_, r)| r);
133
134 let (status, total, real, schema_counts, projections, rollup, mems, quarantined) =
135 match ctx.cli_engine()? {
136 #[cfg(feature = "mem-repo")]
137 CliEngine::MemRepo(engine) => {
138 let status = engine.status();
139 let store: &Store = engine.store();
140 let (projections, rollup) = root
142 .as_deref()
143 .map(|r| {
144 let o = projection_overview(&engine, r);
145 (o.bindings, o.rollup)
146 })
147 .unwrap_or_default();
148 let mems = mem_durability(&engine);
149 let quarantined = quarantine_roster(&engine);
150 (
151 status,
152 store.len(),
153 store.all_entities().filter(|e| !e.stub).count(),
154 count_by_type(store),
155 projections,
156 rollup,
157 mems,
158 quarantined,
159 )
160 }
161 CliEngine::Filesystem(engine) => {
162 let status = engine.status();
163 let store: &Store = engine.store();
164 let (projections, rollup) = root
166 .as_deref()
167 .map(|r| {
168 let o = projection_overview(&engine, r);
169 (o.bindings, o.rollup)
170 })
171 .unwrap_or_default();
172 let mems = mem_durability(&engine);
173 let quarantined = quarantine_roster(&engine);
174 (
175 status,
176 store.len(),
177 store.all_entities().filter(|e| !e.stub).count(),
178 count_by_type(store),
179 projections,
180 rollup,
181 mems,
182 quarantined,
183 )
184 }
185 };
186 let stubs = total - real;
187
188 let edge_pairs: Vec<(&String, &usize)> = status.edge_types.iter().collect();
191 let schema_pairs: Vec<(String, usize)> = schema_counts.into_iter().collect();
192
193 if ctx.json {
194 let payload = StatusPayload {
195 verdict_coverage: crate::coverage::STATUS
196 .axis_coverage()
197 .expect("status is a verdict surface")
198 .wire_line(),
199 rollup,
200 mems,
201 total_nodes: total,
202 real_nodes: real,
203 stub_nodes: stubs,
204 total_edges: status.edge_count,
205 edge_types: edge_pairs
206 .iter()
207 .map(|(t, c)| EdgeTypeCount {
208 rel_type: t,
209 count: **c,
210 })
211 .collect(),
212 type_distribution: schema_pairs
213 .iter()
214 .map(|(s, c)| TypeCount {
215 entity_type: s,
216 count: *c,
217 })
218 .collect(),
219 projections,
220 quarantined,
221 };
222 return print_json(&json!(payload));
223 }
224
225 let mut lines = Vec::new();
226
227 lines.push("# Status".to_string());
231 lines.push(String::new());
232 lines.push(format!(
236 "**Verdict:** {} — for {}",
237 rollup.verdict.as_wire(),
238 rollup.subject,
239 ));
240 if let Some(cov) = crate::coverage::STATUS.axis_coverage() {
243 lines.push(format!("**Verdict coverage:** {}", cov.wire_line()));
244 }
245
246 let unestablished: Vec<&MemDurability> =
250 mems.iter().filter(|m| m.unestablished.is_some()).collect();
251 if !unestablished.is_empty() {
252 lines.push(String::new());
253 lines.push("**Durability not established** for:".to_string());
254 for m in &unestablished {
255 lines.push(format!(
256 "- `{}` ({}) — {}",
257 m.mem,
258 m.backend,
259 m.unestablished.unwrap_or_default(),
260 ));
261 }
262 }
263 if !quarantined.is_empty() {
267 lines.push(String::new());
268 lines.push(format!(
269 "**Quarantined mems ({})** — attached but refused at load; each line carries \
270 the engine's reason and repair:",
271 quarantined.len()
272 ));
273 for q in &quarantined {
274 lines.push(format!("- `{}` [{}] {}", q.mem, q.reason_code, q.reason));
275 }
276 }
277 lines.push(String::new());
278 lines.push(rollup.headline.clone());
279 if !rollup.actions.is_empty() {
280 lines.push(String::new());
281 lines.push("## Do next".to_string());
282 lines.push(String::new());
283 for action in &rollup.actions {
284 lines.push(format!("- {action}"));
285 }
286 }
287 lines.push(String::new());
288
289 lines.push("# Graph status".to_string());
290 lines.push(String::new());
291 lines.push(format!("- Nodes: {total} ({real} real, {stubs} stubs)"));
292 lines.push(format!("- Edges: {}", status.edge_count));
293 if !edge_pairs.is_empty() {
294 let edges: Vec<String> = edge_pairs
295 .iter()
296 .map(|(t, c)| format!("{t} ({c})"))
297 .collect();
298 lines.push(format!("- Edge types: {}", edges.join(", ")));
299 }
300 if !schema_pairs.is_empty() {
301 let schemas: Vec<String> = schema_pairs
302 .iter()
303 .map(|(s, c)| format!("{s} ({c})"))
304 .collect();
305 lines.push(format!("- Types: {}", schemas.join(", ")));
306 }
307 if !projections.is_empty() {
308 lines.push(String::new());
309 lines.push("## Projections".to_string());
310 lines.push(String::new());
311 for p in &projections {
312 lines.push(format!(
313 "- `{}` → `{}` — operations: {}; advance: {} pending, {} disposed",
314 p.binding,
315 p.destination_mem,
316 p.operations.join(", "),
317 p.advance.pending,
318 p.advance.disposed,
319 ));
320 for (facet, state) in &p.state {
321 lines.push(format!(
322 " - {facet}: signal {}, synced {}, verified {}",
323 state.signal,
324 state.synced.as_deref().unwrap_or("none"),
325 state.verified.as_deref().unwrap_or("none"),
326 ));
327 }
328 }
329 }
330 print_markdown(&lines.join("\n"));
331 Ok(())
332}
333
334fn count_by_type(store: &Store) -> BTreeMap<String, usize> {
337 let mut counts: BTreeMap<String, usize> = BTreeMap::new();
338 for e in store.all_entities().filter(|e| !e.stub) {
339 *counts.entry(e.entity_type.clone()).or_default() += 1;
340 }
341 counts
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
352 fn edge_and_type_lists_render_in_name_order() {
353 fn name_ordered(_: &BTreeMap<String, usize>) {}
354 let mut edge_types = BTreeMap::new();
355 for (k, c) in [("USES", 3), ("DEPENDS_ON", 9), ("MENTIONS", 1)] {
356 edge_types.insert(k.to_string(), c);
357 }
358 let status = memstead_base::ops::Status {
359 entity_count: 0,
360 edge_count: 13,
361 edge_types,
362 community_count: 0,
363 mem_count: 0,
364 types_in_use: vec![],
365 };
366 name_ordered(&status.edge_types);
367 let rendered: Vec<EdgeTypeCount> = status
368 .edge_types
369 .iter()
370 .map(|(t, c)| EdgeTypeCount {
371 rel_type: t,
372 count: *c,
373 })
374 .collect();
375 let names: Vec<&str> = rendered.iter().map(|e| e.rel_type).collect();
376 assert_eq!(names, vec!["DEPENDS_ON", "MENTIONS", "USES"]);
377 name_ordered(&count_by_type(&Store::new()));
379 }
380}