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