memstead_base/ops/coverage.rs
1//! Axis-coverage declarations: no read surface reports `clean` over
2//! state it did not examine. Where it cannot examine, it says so.
3//!
4//! WHY: eight findings in one sweep shared a single shape, a surface
5//! emitting an all-clear that asserted less than it read as. Strict
6//! health promoted a hand-remembered subset of conditions, `status`
7//! defaulted to a clean rollup when nothing was declared, the
8//! conformance linter could not fail against a mem's own schema, and
9//! `workspace dump` silently dropped mounts whose config did not
10//! parse. Each instance was fixed; this module is the rule that keeps
11//! the class shut. A reader who is told `clean` stops looking, so a
12//! clean verdict must carry the set of axes it answers for.
13//!
14//! THE RULE, STATED ONCE: every surface a caller can read is declared
15//! in a per-consumer registry. A surface that emits a clean/ok
16//! verdict declares, for EVERY axis in the workspace vocabulary,
17//! either that its verdict examined the axis or that the axis is
18//! excluded with a stated reason. A surface that emits no verdict
19//! declares why not. The declaration states intent, which is exactly
20//! what cannot be derived from the code, since the defect the rule
21//! closes is surfaces doing less than they claim. Scoped statements
22//! are instances of this rule, not siblings of it: the anchor
23//! surface saying "reconciliation could not be performed" and the
24//! verify rollup's blind-spots list are the per-run refinement of
25//! the same obligation the static declaration carries per surface.
26//!
27//! The vocabulary reuses [`HEALTH_INCLUDE_KEYS`] rather than
28//! inventing a parallel axis roster: those keys are already the one
29//! shared statement of what the engine can examine, and only the
30//! verdict subjects no health include covers are added here.
31//!
32//! ENFORCEMENT: [`validate_coverage`] is pure and total over data,
33//! so the gate can be demonstrated red against synthetic fixtures
34//! (a surface clean over an unexamined axis, an axis added without a
35//! declaration update) without reconstructing any historical tree.
36//! Each consumer crate holds a test that walks its own live surface
37//! roster (the clap command tree, the MCP tool router), hands the
38//! walk's output to the validator, and fails on any finding. Those
39//! tests ride the ordinary `cargo nextest` legs of `run-tests.sh`,
40//! the same path every other permanent guard runs on. One surface is
41//! deliberately outside the rule: `check` records a caller's verdict
42//! about the caller's own work, so its registry entry is
43//! [`CoverageDisposition::NoVerdict`], not an examined-axes claim.
44
45use crate::ops::health::HEALTH_INCLUDE_KEYS;
46
47/// Verdict subjects no health include key covers. `projection` is the
48/// fidelity axis `status` and `projection verify` answer for;
49/// `mounts` is the roster axis `workspace dump` and `overview` answer
50/// for (which mounts exist, which serve nothing, and why).
51pub const EXTRA_VERDICT_AXES: &[&str] = &["projection", "mounts"];
52
53/// The workspace axis vocabulary: everything a clean verdict can
54/// answer for. Composed, never copied, so it cannot drift from the
55/// health roster.
56pub fn verdict_axes() -> Vec<&'static str> {
57 let mut axes: Vec<&'static str> = HEALTH_INCLUDE_KEYS.to_vec();
58 axes.extend_from_slice(EXTRA_VERDICT_AXES);
59 axes
60}
61
62/// One surface's static coverage claim: which axes its verdict
63/// answers for, and why the rest are outside its scope. The two
64/// lists must jointly name every axis in the vocabulary; a blanket
65/// "everything else" clause is deliberately impossible, because it
66/// would swallow a newly introduced axis silently, and the one
67/// permanent property this module owes is that a new axis fails
68/// every declaration that has not met it.
69#[derive(Debug, Clone, Copy)]
70pub struct AxisCoverage {
71 /// Axes the surface's clean verdict actually examined.
72 pub examined: &'static [&'static str],
73 /// Axes the verdict does not answer for, each with the reason a
74 /// reader needs (typically: which surface answers for it instead).
75 pub excluded: &'static [(&'static str, &'static str)],
76}
77
78/// What a declared surface claims about verdicts.
79#[derive(Debug, Clone, Copy)]
80pub enum CoverageDisposition {
81 /// The surface can emit a clean/ok verdict and declares its axes.
82 Verdict(AxisCoverage),
83 /// The surface emits no clean/ok verdict; the reason says why the
84 /// rule does not bind it (it returns data, it reports what a
85 /// mutation did, or its verdict belongs to the caller).
86 NoVerdict(&'static str),
87}
88
89/// One registry row: a surface name exactly as the consumer's own
90/// mechanical walk produces it, plus its disposition.
91#[derive(Debug, Clone, Copy)]
92pub struct SurfaceCoverage {
93 pub surface: &'static str,
94 pub disposition: CoverageDisposition,
95}
96
97impl AxisCoverage {
98 /// The examined set as it is stamped into surface output.
99 pub fn examined_wire(&self) -> Vec<&'static str> {
100 self.examined.to_vec()
101 }
102
103 /// The exclusions as they are stamped into surface output:
104 /// `(axis, reason)` pairs, so a reader can see which axes the
105 /// verdict does not cover without reading the source.
106 pub fn excluded_wire(&self) -> Vec<(&'static str, &'static str)> {
107 self.excluded.to_vec()
108 }
109
110 /// The declaration as it is stamped into surface output: one
111 /// compact line naming both axis sets, the same form on JSON,
112 /// markdown, and frontmatter surfaces. Axis names only: the
113 /// per-axis exclusion reasons stay a registry fact the gate test
114 /// enforces, because stamping static prose into every response
115 /// would tax each call's token budget, and the reader's question
116 /// the stamp answers is WHICH axes the verdict covers.
117 pub fn wire_line(&self) -> String {
118 self.wire_line_promoting(&[])
119 }
120
121 /// The wire line with the named excluded axes promoted into the
122 /// examined set — for a report that rendered an opt-in axis this
123 /// pass (`--include anchors`) and therefore did examine it. An axis
124 /// not in the excluded list is ignored; the static declaration is
125 /// untouched.
126 pub fn wire_line_promoting(&self, promoted: &[&str]) -> String {
127 let mut examined: Vec<&str> = self.examined.to_vec();
128 let mut not_examined: Vec<&str> = Vec::new();
129 for (a, _) in self.excluded {
130 if promoted.contains(a) {
131 examined.push(a);
132 } else {
133 not_examined.push(a);
134 }
135 }
136 format!(
137 "examined={}; not_examined={}",
138 examined.join(","),
139 not_examined.join(",")
140 )
141 }
142}
143
144impl SurfaceCoverage {
145 /// The verdict declaration, when this row carries one; the
146 /// stamping sites use it so a surface can only stamp what its
147 /// registry row declares.
148 pub fn axis_coverage(&self) -> Option<&AxisCoverage> {
149 match &self.disposition {
150 CoverageDisposition::Verdict(c) => Some(c),
151 CoverageDisposition::NoVerdict(_) => None,
152 }
153 }
154}
155
156/// The health surface's coverage claim, shared by every consumer
157/// that renders a health report (the CLI command, the full MCP
158/// server's composer, and the lean server's own assembly): the axes
159/// whose findings the report treats as defects, so an empty defect
160/// statement reads as an all-clear exactly over them. Everything
161/// descriptive or advisory is excluded by name.
162pub const HEALTH_COVERAGE: AxisCoverage = AxisCoverage {
163 examined: &[
164 "dangling_links",
165 "missing_required_outgoing",
166 "constraints",
167 "signals",
168 "integrity",
169 "config",
170 "mounts",
171 ],
172 excluded: &[
173 (
174 "orphans",
175 "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
176 ),
177 (
178 "stubs",
179 "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
180 ),
181 (
182 "most_connected",
183 "descriptive ranking with no pass/fail semantics",
184 ),
185 (
186 "missing_fields",
187 "advisory count, never part of the defect verdict",
188 ),
189 (
190 "stale",
191 "advisory freshness, never part of the defect verdict",
192 ),
193 (
194 "tags",
195 "descriptive distribution with no pass/fail semantics",
196 ),
197 (
198 "labelling",
199 "advisory audit, never part of the defect verdict",
200 ),
201 (
202 "conformance",
203 "reported per entity beside the verdict, never folded into it",
204 ),
205 (
206 "anchors",
207 "drifted anchors stay advisory; the verify surfaces carry the drift statement",
208 ),
209 ("friction", "descriptive ledger counts"),
210 ("open_questions", "descriptive listing of open questions"),
211 (
212 "vital_signs",
213 "descriptive model-truth counts; the remodel skill holds the thresholds",
214 ),
215 (
216 "stale_derivations",
217 "advisory freshness of derived artifacts",
218 ),
219 (
220 "checks",
221 "check states are derived views; the verdicts in them belong to their recording callers",
222 ),
223 ("ledger", "descriptive view of the check ledger"),
224 (
225 "projection",
226 "projection fidelity is answered by status and projection verify",
227 ),
228 ],
229};
230
231/// The overview surface's coverage claim, shared by every consumer
232/// that renders the composed overview (the CLI command and both MCP
233/// servers), and stamped into the composed frontmatter by
234/// `compose_overview` itself so the declaration and the output cannot
235/// diverge.
236pub const OVERVIEW_COVERAGE: AxisCoverage = AxisCoverage {
237 examined: &["mounts", "config"],
238 excluded: &[
239 ("orphans", OVERVIEW_SCOPE),
240 ("stubs", OVERVIEW_SCOPE),
241 ("most_connected", OVERVIEW_SCOPE),
242 ("missing_fields", OVERVIEW_SCOPE),
243 ("stale", OVERVIEW_SCOPE),
244 (
245 "dangling_links",
246 "rendered on request as a listing; the verdict over them is health's",
247 ),
248 ("tags", OVERVIEW_SCOPE),
249 ("missing_required_outgoing", OVERVIEW_SCOPE),
250 ("constraints", OVERVIEW_SCOPE),
251 ("signals", OVERVIEW_SCOPE),
252 ("labelling", OVERVIEW_SCOPE),
253 ("conformance", OVERVIEW_SCOPE),
254 ("integrity", OVERVIEW_SCOPE),
255 ("anchors", OVERVIEW_SCOPE),
256 ("friction", OVERVIEW_SCOPE),
257 ("open_questions", OVERVIEW_SCOPE),
258 (
259 "vital_signs",
260 "descriptive model-truth counts; the remodel skill holds the thresholds",
261 ),
262 ("stale_derivations", OVERVIEW_SCOPE),
263 ("checks", OVERVIEW_SCOPE),
264 ("ledger", OVERVIEW_SCOPE),
265 ("projection", OVERVIEW_SCOPE),
266 ],
267};
268
269const OVERVIEW_SCOPE: &str = "overview is a descriptive composition; its only \
270 all-clear claim is that the roster it renders is complete and its mounts serve";
271
272/// Hold a registry against the axis vocabulary and a mechanically
273/// discovered surface roster. Returns one finding per defect; an
274/// empty result is the only clean outcome. Pure and total: callers
275/// in tests pass the live vocabulary and their own live walk,
276/// fixtures pass synthetic ones.
277///
278/// The findings, each mapped to the failure it refuses:
279/// - a discovered surface with no registry row (a surface landed
280/// without declaring),
281/// - a registry row no walk discovers (a stale declaration reading
282/// as coverage),
283/// - a duplicate row (two claims, no single truth),
284/// - an axis named by a declaration that the vocabulary does not
285/// carry (a stale axis reading as coverage),
286/// - an axis in the vocabulary that a verdict declaration neither
287/// examines nor excludes (a new axis met by silence: the clean
288/// verdict would cover it by omission),
289/// - an axis both examined and excluded (a contradiction),
290/// - an exclusion or no-verdict claim with an empty reason (a
291/// declaration that declares nothing).
292pub fn validate_coverage(
293 vocab: &[&str],
294 registry: &[SurfaceCoverage],
295 discovered: &[&str],
296) -> Vec<String> {
297 let mut findings = Vec::new();
298
299 for d in discovered {
300 if !registry.iter().any(|r| r.surface == *d) {
301 findings.push(format!(
302 "surface `{d}` is discoverable and has no coverage declaration: \
303 declare its verdict axes, or declare why it emits no verdict"
304 ));
305 }
306 }
307
308 let mut seen: Vec<&str> = Vec::new();
309 for row in registry {
310 if seen.contains(&row.surface) {
311 findings.push(format!(
312 "surface `{}` is declared more than once",
313 row.surface
314 ));
315 continue;
316 }
317 seen.push(row.surface);
318
319 if !discovered.contains(&row.surface) {
320 findings.push(format!(
321 "declared surface `{}` is not discoverable: a stale declaration \
322 reads as coverage, remove it or fix the walk",
323 row.surface
324 ));
325 }
326
327 match row.disposition {
328 CoverageDisposition::NoVerdict(reason) => {
329 if reason.trim().is_empty() {
330 findings.push(format!(
331 "surface `{}` declares no verdict without a reason",
332 row.surface
333 ));
334 }
335 }
336 CoverageDisposition::Verdict(cov) => {
337 for axis in cov.examined {
338 if !vocab.contains(axis) {
339 findings.push(format!(
340 "surface `{}` examines axis `{axis}`, which the \
341 vocabulary does not carry",
342 row.surface
343 ));
344 }
345 if cov.excluded.iter().any(|(a, _)| a == axis) {
346 findings.push(format!(
347 "surface `{}` both examines and excludes axis `{axis}`",
348 row.surface
349 ));
350 }
351 }
352 for (axis, reason) in cov.excluded {
353 if !vocab.contains(axis) {
354 findings.push(format!(
355 "surface `{}` excludes axis `{axis}`, which the \
356 vocabulary does not carry",
357 row.surface
358 ));
359 }
360 if reason.trim().is_empty() {
361 findings.push(format!(
362 "surface `{}` excludes axis `{axis}` without a reason",
363 row.surface
364 ));
365 }
366 }
367 for axis in vocab {
368 let examined = cov.examined.contains(axis);
369 let excluded = cov.excluded.iter().any(|(a, _)| a == axis);
370 if !examined && !excluded {
371 findings.push(format!(
372 "surface `{}` declares nothing for axis `{axis}`: \
373 its clean verdict would cover the axis by omission, \
374 examine it or exclude it with a reason",
375 row.surface
376 ));
377 }
378 }
379 }
380 }
381 }
382
383 findings
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 const VOCAB: &[&str] = &["anchors", "mounts"];
391
392 fn full() -> SurfaceCoverage {
393 SurfaceCoverage {
394 surface: "verify",
395 disposition: CoverageDisposition::Verdict(AxisCoverage {
396 examined: &["anchors"],
397 excluded: &[("mounts", "the roster surface answers for mounts")],
398 }),
399 }
400 }
401
402 fn ledger() -> SurfaceCoverage {
403 SurfaceCoverage {
404 surface: "check",
405 disposition: CoverageDisposition::NoVerdict(
406 "records the caller's verdict about the caller's own work",
407 ),
408 }
409 }
410
411 /// The complement: a registry that declares everything, over a
412 /// walk that finds exactly the declared surfaces, is clean, and
413 /// the only burden it carried was the declaration itself.
414 #[test]
415 fn complete_registry_is_clean() {
416 let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check"]);
417 assert!(findings.is_empty(), "{findings:?}");
418 }
419
420 /// The gate red, fixture one: a surface whose clean verdict
421 /// covers an axis by omission. This reproduces the sweep's
422 /// condition shape independently of whether the sweep happened,
423 /// since the fixture is synthetic.
424 #[test]
425 fn clean_over_an_unexamined_axis_fails() {
426 let silent = SurfaceCoverage {
427 surface: "verify",
428 disposition: CoverageDisposition::Verdict(AxisCoverage {
429 examined: &["anchors"],
430 excluded: &[],
431 }),
432 };
433 let findings = validate_coverage(VOCAB, &[silent, ledger()], &["verify", "check"]);
434 assert!(
435 findings.iter().any(|f| f.contains("`verify`")
436 && f.contains("`mounts`")
437 && f.contains("by omission")),
438 "{findings:?}"
439 );
440 }
441
442 /// The gate red, fixture two: an axis is introduced and an
443 /// existing declaration is not updated. This is the recurrence
444 /// case; a gate that passes here is a one-time sweep.
445 #[test]
446 fn axis_added_without_declaration_update_fails() {
447 let grown: &[&str] = &["anchors", "mounts", "fences"];
448 let findings = validate_coverage(grown, &[full(), ledger()], &["verify", "check"]);
449 assert!(
450 findings
451 .iter()
452 .any(|f| f.contains("`verify`") && f.contains("`fences`")),
453 "{findings:?}"
454 );
455 }
456
457 /// A surface that landed without any declaration fails.
458 #[test]
459 fn undeclared_surface_fails() {
460 let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check", "dump"]);
461 assert!(
462 findings
463 .iter()
464 .any(|f| f.contains("`dump`") && f.contains("no coverage declaration")),
465 "{findings:?}"
466 );
467 }
468
469 /// A declaration whose surface departed fails rather than skips.
470 #[test]
471 fn stale_surface_declaration_fails() {
472 let findings = validate_coverage(VOCAB, &[full(), ledger()], &["check"]);
473 assert!(
474 findings
475 .iter()
476 .any(|f| f.contains("`verify`") && f.contains("not discoverable")),
477 "{findings:?}"
478 );
479 }
480
481 /// An axis dropped from the vocabulary turns the declarations
482 /// naming it into findings, so a stale axis cannot read as
483 /// coverage.
484 #[test]
485 fn stale_axis_in_declaration_fails() {
486 let shrunk: &[&str] = &["anchors"];
487 let findings = validate_coverage(shrunk, &[full(), ledger()], &["verify", "check"]);
488 assert!(
489 findings
490 .iter()
491 .any(|f| f.contains("`mounts`") && f.contains("does not carry")),
492 "{findings:?}"
493 );
494 }
495
496 /// Excluding an axis without a reason fails: an unexplained
497 /// exclusion is a silent drop with paperwork.
498 #[test]
499 fn exclusion_without_reason_fails() {
500 let bare = SurfaceCoverage {
501 surface: "verify",
502 disposition: CoverageDisposition::Verdict(AxisCoverage {
503 examined: &["anchors"],
504 excluded: &[("mounts", " ")],
505 }),
506 };
507 let findings = validate_coverage(VOCAB, &[bare, ledger()], &["verify", "check"]);
508 assert!(
509 findings.iter().any(|f| f.contains("without a reason")),
510 "{findings:?}"
511 );
512 }
513
514 /// Examining and excluding the same axis is a contradiction, not
515 /// a double assurance.
516 #[test]
517 fn examined_and_excluded_fails() {
518 let both = SurfaceCoverage {
519 surface: "verify",
520 disposition: CoverageDisposition::Verdict(AxisCoverage {
521 examined: &["anchors", "mounts"],
522 excluded: &[("mounts", "also excluded")],
523 }),
524 };
525 let findings = validate_coverage(VOCAB, &[both, ledger()], &["verify", "check"]);
526 assert!(
527 findings
528 .iter()
529 .any(|f| f.contains("both examines and excludes")),
530 "{findings:?}"
531 );
532 }
533
534 /// Two rows for one surface fail: two claims, no single truth.
535 #[test]
536 fn duplicate_declaration_fails() {
537 let findings = validate_coverage(VOCAB, &[full(), full(), ledger()], &["verify", "check"]);
538 assert!(
539 findings
540 .iter()
541 .any(|f| f.contains("declared more than once")),
542 "{findings:?}"
543 );
544 }
545
546 /// The live vocabulary is the health roster plus the declared
547 /// extras, nothing more: composition, not a copy that can drift.
548 #[test]
549 fn vocabulary_composes_health_roster() {
550 let axes = verdict_axes();
551 for key in HEALTH_INCLUDE_KEYS {
552 assert!(axes.contains(key), "health include `{key}` missing");
553 }
554 for key in EXTRA_VERDICT_AXES {
555 assert!(axes.contains(key), "extra axis `{key}` missing");
556 }
557 assert_eq!(
558 axes.len(),
559 HEALTH_INCLUDE_KEYS.len() + EXTRA_VERDICT_AXES.len()
560 );
561 }
562}