1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
use std::collections::HashMap;
use memstead_base::Store;
use memstead_base::ingest::status::{
ProjectionStatus, Rollup, projection_rollup, projection_status,
};
use serde::Serialize;
use serde_json::json;
use crate::output::{print_json, print_markdown};
use crate::setup::{CliContext, CliEngine};
#[derive(Serialize)]
struct EdgeTypeCount<'a> {
/// Spelled `rel_type`, like every other relation surface. The
/// sibling `TypeCount` below keeps `type` because ITS field is an
/// entity type — a different concept that owns the word.
rel_type: &'a str,
count: usize,
}
#[derive(Serialize)]
struct TypeCount<'a> {
#[serde(rename = "type")]
entity_type: &'a str,
count: usize,
}
/// The `memstead status` JSON payload. The graph-count fields are
/// byte-compatible with the former `stats` command's payload; `projections` is
/// the additive per-binding array. `rollup` is the dashboard lead — one verdict
/// plus the top-three concrete actions derived from the durable findings store
/// and freshness; the graph counts and `projections` are the drill-down.
#[derive(Serialize)]
struct StatusPayload<'a> {
/// The coverage rule (memstead_base::ops::coverage): the axes the
/// rollup verdict answers for, from the CLI's registry row.
verdict_coverage: String,
rollup: Rollup,
mems: Vec<MemDurability>,
total_nodes: usize,
real_nodes: usize,
stub_nodes: usize,
total_edges: usize,
edge_types: Vec<EdgeTypeCount<'a>>,
type_distribution: Vec<TypeCount<'a>>,
projections: Vec<ProjectionStatus>,
/// Boot-honesty roster, present whenever non-empty and never behind
/// an opt-in — the same rule `health` follows. On a filesystem
/// workspace `status` is the roster surface the `mem list` refusal
/// points at, so a quarantine this payload omitted was simply
/// invisible: the graph counts read as a small healthy workspace.
#[serde(skip_serializing_if = "Vec::is_empty")]
quarantined: Vec<QuarantineLine>,
}
/// One quarantined mem: attached, refused at load, and the engine holds
/// the typed reason and the repair — surfaced, never restated.
#[derive(Serialize)]
struct QuarantineLine {
mem: String,
reason_code: String,
reason: String,
}
fn quarantine_roster(engine: &memstead_base::Engine) -> Vec<QuarantineLine> {
engine
.quarantined_mems()
.iter()
.map(|q| QuarantineLine {
mem: q.mount.mem.clone(),
reason_code: q.reason_code.clone(),
reason: q.reason_message.clone(),
})
.collect()
}
/// One mem's durability line: what the engine can say about whether that
/// mem's writes are recorded anywhere, and what it cannot (04/04, criterion
/// 6).
#[derive(Serialize)]
struct MemDurability {
mem: String,
backend: &'static str,
/// The engine's narrow answer: writes survive a process restart.
durable: bool,
/// Whether that answer was established from a real commit or read off the
/// mount kind.
basis: &'static str,
/// Present exactly when the engine cannot establish that the mem's writes
/// reached version control. Never a claim that they did not.
#[serde(skip_serializing_if = "Option::is_none")]
unestablished: Option<&'static str>,
}
/// What the engine can and cannot say about each mem's durability.
///
/// `status` used to touch no backend at all, so a folder mem's writes could
/// be sitting outside any version control and nothing said so. It still does
/// not shell out to git: a folder mem's root may not be in a repository, and
/// a missing repository is not a defect. The reportable fact is that the
/// engine cannot ESTABLISH durability there, which is true either way
/// (04/04, criterion 6). It never claims debt it did not observe.
fn mem_durability(engine: &memstead_base::Engine) -> Vec<MemDurability> {
engine
.mounts()
.iter()
.map(|m| {
let head = engine.mem_head_sha(&m.mem).ok().flatten();
let basis = m.storage.durability_basis(head.as_deref());
MemDurability {
mem: m.mem.clone(),
backend: m.storage.backend_id(),
durable: m.storage.is_durable(),
basis: basis.as_wire(),
unestablished: match basis {
memstead_base::workspace::DurabilityBasis::Established => None,
memstead_base::workspace::DurabilityBasis::InferredFromMountKind => Some(
"writes land on disk and survive a restart; whether they reached \
version control is not something the engine can establish",
),
},
}
})
.collect()
}
pub fn run(ctx: &CliContext) -> anyhow::Result<()> {
// The workspace root (for the projection store / advance store reads). The
// engine build below fails before this matters when we are outside a
// workspace, so a `None` here only ever means "in a workspace that declares
// no projections" once we get past `cli_engine()?`.
let root = ctx.workspace_shape().map(|(_, r)| r);
let (status, total, real, schema_counts, projections, rollup, mems, quarantined) =
match ctx.cli_engine()? {
#[cfg(feature = "mem-repo")]
CliEngine::MemRepo(engine) => {
let status = engine.status();
let store: &Store = engine.store();
let projections = root
.as_deref()
.map(|r| projection_status(&engine, r))
.unwrap_or_default();
let rollup = root
.as_deref()
.map(|r| projection_rollup(&engine, r))
.unwrap_or_default();
let mems = mem_durability(&engine);
let quarantined = quarantine_roster(&engine);
(
status,
store.len(),
store.all_entities().filter(|e| !e.stub).count(),
count_by_type(store),
projections,
rollup,
mems,
quarantined,
)
}
CliEngine::Filesystem(engine) => {
let status = engine.status();
let store: &Store = engine.store();
let projections = root
.as_deref()
.map(|r| projection_status(&engine, r))
.unwrap_or_default();
let rollup = root
.as_deref()
.map(|r| projection_rollup(&engine, r))
.unwrap_or_default();
let mems = mem_durability(&engine);
let quarantined = quarantine_roster(&engine);
(
status,
store.len(),
store.all_entities().filter(|e| !e.stub).count(),
count_by_type(store),
projections,
rollup,
mems,
quarantined,
)
}
};
let stubs = total - real;
let mut edge_pairs: Vec<_> = status.edge_types.iter().collect();
edge_pairs.sort_by(|a, b| b.1.cmp(a.1));
let mut schema_pairs: Vec<(String, usize)> = schema_counts.into_iter().collect();
schema_pairs.sort_by_key(|p| std::cmp::Reverse(p.1));
if ctx.json {
let payload = StatusPayload {
verdict_coverage: crate::coverage::STATUS
.axis_coverage()
.expect("status is a verdict surface")
.wire_line(),
rollup,
mems,
total_nodes: total,
real_nodes: real,
stub_nodes: stubs,
total_edges: status.edge_count,
edge_types: edge_pairs
.iter()
.map(|(t, c)| EdgeTypeCount {
rel_type: t,
count: **c,
})
.collect(),
type_distribution: schema_pairs
.iter()
.map(|(s, c)| TypeCount {
entity_type: s,
count: *c,
})
.collect(),
projections,
quarantined,
};
return print_json(&json!(payload));
}
let mut lines = Vec::new();
// Lead with the dashboard rollup: one verdict + the top-three concrete
// actions. The graph counts and per-binding projection detail below are the
// drill-down.
lines.push("# Status".to_string());
lines.push(String::new());
// The subject rides with the verdict, never apart from it: a bare
// "clean" is read as a claim about the workspace, and this one answers
// for projection bindings only (04/04, criterion 5).
lines.push(format!(
"**Verdict:** {} — for {}",
rollup.verdict.as_wire(),
rollup.subject,
));
// The coverage rule: the axes the verdict answers for, in the
// output itself (memstead_base::ops::coverage).
if let Some(cov) = crate::coverage::STATUS.axis_coverage() {
lines.push(format!("**Verdict coverage:** {}", cov.wire_line()));
}
// What the engine could not establish, named rather than left to the
// reader's assumption. A mem whose durability IS established says so and
// adds no caveat (04/04, criterion 6 and its complement).
let unestablished: Vec<&MemDurability> =
mems.iter().filter(|m| m.unestablished.is_some()).collect();
if !unestablished.is_empty() {
lines.push(String::new());
lines.push("**Durability not established** for:".to_string());
for m in &unestablished {
lines.push(format!(
"- `{}` ({}) — {}",
m.mem,
m.backend,
m.unestablished.unwrap_or_default(),
));
}
}
// The quarantine roster, present whenever non-empty — same rule as
// `health`. Without it a quarantined mem on a filesystem workspace
// vanished from the very surface the `mem list` refusal points at.
if !quarantined.is_empty() {
lines.push(String::new());
lines.push(format!(
"**Quarantined mems ({})** — attached but refused at load; each line carries \
the engine's reason and repair:",
quarantined.len()
));
for q in &quarantined {
lines.push(format!("- `{}` [{}] {}", q.mem, q.reason_code, q.reason));
}
}
lines.push(String::new());
lines.push(rollup.headline.clone());
if !rollup.actions.is_empty() {
lines.push(String::new());
lines.push("## Do next".to_string());
lines.push(String::new());
for action in &rollup.actions {
lines.push(format!("- {action}"));
}
}
lines.push(String::new());
lines.push("# Graph status".to_string());
lines.push(String::new());
lines.push(format!("- Nodes: {total} ({real} real, {stubs} stubs)"));
lines.push(format!("- Edges: {}", status.edge_count));
if !edge_pairs.is_empty() {
let edges: Vec<String> = edge_pairs
.iter()
.map(|(t, c)| format!("{t} ({c})"))
.collect();
lines.push(format!("- Edge types: {}", edges.join(", ")));
}
if !schema_pairs.is_empty() {
let schemas: Vec<String> = schema_pairs
.iter()
.map(|(s, c)| format!("{s} ({c})"))
.collect();
lines.push(format!("- Types: {}", schemas.join(", ")));
}
if !projections.is_empty() {
lines.push(String::new());
lines.push("## Projections".to_string());
lines.push(String::new());
for p in &projections {
lines.push(format!(
"- `{}` → `{}` — operations: {}; advance: {} pending, {} disposed",
p.binding,
p.destination_mem,
p.operations.join(", "),
p.advance.pending,
p.advance.disposed,
));
for (facet, state) in &p.state {
lines.push(format!(
" - {facet}: signal {}, synced {}, verified {}",
state.signal,
state.synced.as_deref().unwrap_or("none"),
state.verified.as_deref().unwrap_or("none"),
));
}
}
}
print_markdown(&lines.join("\n"));
Ok(())
}
/// Count real (non-stub) entities by `entity_type`. Both engine
/// flavours expose a `&Store`, so this helper is engine-agnostic.
fn count_by_type(store: &Store) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
for e in store.all_entities().filter(|e| !e.stub) {
*counts.entry(e.entity_type.clone()).or_default() += 1;
}
counts
}