Skip to main content

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        let not_examined: Vec<&str> = self.excluded.iter().map(|(a, _)| *a).collect();
119        format!(
120            "examined={}; not_examined={}",
121            self.examined.join(","),
122            not_examined.join(",")
123        )
124    }
125}
126
127impl SurfaceCoverage {
128    /// The verdict declaration, when this row carries one; the
129    /// stamping sites use it so a surface can only stamp what its
130    /// registry row declares.
131    pub fn axis_coverage(&self) -> Option<&AxisCoverage> {
132        match &self.disposition {
133            CoverageDisposition::Verdict(c) => Some(c),
134            CoverageDisposition::NoVerdict(_) => None,
135        }
136    }
137}
138
139/// The health surface's coverage claim, shared by every consumer
140/// that renders a health report (the CLI command, the full MCP
141/// server's composer, and the lean server's own assembly): the axes
142/// whose findings the report treats as defects, so an empty defect
143/// statement reads as an all-clear exactly over them. Everything
144/// descriptive or advisory is excluded by name.
145pub const HEALTH_COVERAGE: AxisCoverage = AxisCoverage {
146    examined: &[
147        "dangling_links",
148        "missing_required_outgoing",
149        "constraints",
150        "signals",
151        "integrity",
152        "config",
153        "mounts",
154    ],
155    excluded: &[
156        (
157            "orphans",
158            "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
159        ),
160        (
161            "stubs",
162            "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
163        ),
164        (
165            "most_connected",
166            "descriptive ranking with no pass/fail semantics",
167        ),
168        (
169            "missing_fields",
170            "advisory count, never part of the defect verdict",
171        ),
172        (
173            "stale",
174            "advisory freshness, never part of the defect verdict",
175        ),
176        (
177            "tags",
178            "descriptive distribution with no pass/fail semantics",
179        ),
180        (
181            "labelling",
182            "advisory audit, never part of the defect verdict",
183        ),
184        (
185            "conformance",
186            "reported per entity beside the verdict, never folded into it",
187        ),
188        (
189            "anchors",
190            "drifted anchors stay advisory; the verify surfaces carry the drift statement",
191        ),
192        ("friction", "descriptive ledger counts"),
193        ("open_questions", "descriptive listing of open questions"),
194        (
195            "stale_derivations",
196            "advisory freshness of derived artifacts",
197        ),
198        (
199            "checks",
200            "check states are derived views; the verdicts in them belong to their recording callers",
201        ),
202        ("ledger", "descriptive view of the check ledger"),
203        (
204            "projection",
205            "projection fidelity is answered by status and projection verify",
206        ),
207    ],
208};
209
210/// The overview surface's coverage claim, shared by every consumer
211/// that renders the composed overview (the CLI command and both MCP
212/// servers), and stamped into the composed frontmatter by
213/// `compose_overview` itself so the declaration and the output cannot
214/// diverge.
215pub const OVERVIEW_COVERAGE: AxisCoverage = AxisCoverage {
216    examined: &["mounts", "config"],
217    excluded: &[
218        ("orphans", OVERVIEW_SCOPE),
219        ("stubs", OVERVIEW_SCOPE),
220        ("most_connected", OVERVIEW_SCOPE),
221        ("missing_fields", OVERVIEW_SCOPE),
222        ("stale", OVERVIEW_SCOPE),
223        (
224            "dangling_links",
225            "rendered on request as a listing; the verdict over them is health's",
226        ),
227        ("tags", OVERVIEW_SCOPE),
228        ("missing_required_outgoing", OVERVIEW_SCOPE),
229        ("constraints", OVERVIEW_SCOPE),
230        ("signals", OVERVIEW_SCOPE),
231        ("labelling", OVERVIEW_SCOPE),
232        ("conformance", OVERVIEW_SCOPE),
233        ("integrity", OVERVIEW_SCOPE),
234        ("anchors", OVERVIEW_SCOPE),
235        ("friction", OVERVIEW_SCOPE),
236        ("open_questions", OVERVIEW_SCOPE),
237        ("stale_derivations", OVERVIEW_SCOPE),
238        ("checks", OVERVIEW_SCOPE),
239        ("ledger", OVERVIEW_SCOPE),
240        ("projection", OVERVIEW_SCOPE),
241    ],
242};
243
244const OVERVIEW_SCOPE: &str = "overview is a descriptive composition; its only \
245     all-clear claim is that the roster it renders is complete and its mounts serve";
246
247/// Hold a registry against the axis vocabulary and a mechanically
248/// discovered surface roster. Returns one finding per defect; an
249/// empty result is the only clean outcome. Pure and total: callers
250/// in tests pass the live vocabulary and their own live walk,
251/// fixtures pass synthetic ones.
252///
253/// The findings, each mapped to the failure it refuses:
254/// - a discovered surface with no registry row (a surface landed
255///   without declaring),
256/// - a registry row no walk discovers (a stale declaration reading
257///   as coverage),
258/// - a duplicate row (two claims, no single truth),
259/// - an axis named by a declaration that the vocabulary does not
260///   carry (a stale axis reading as coverage),
261/// - an axis in the vocabulary that a verdict declaration neither
262///   examines nor excludes (a new axis met by silence: the clean
263///   verdict would cover it by omission),
264/// - an axis both examined and excluded (a contradiction),
265/// - an exclusion or no-verdict claim with an empty reason (a
266///   declaration that declares nothing).
267pub fn validate_coverage(
268    vocab: &[&str],
269    registry: &[SurfaceCoverage],
270    discovered: &[&str],
271) -> Vec<String> {
272    let mut findings = Vec::new();
273
274    for d in discovered {
275        if !registry.iter().any(|r| r.surface == *d) {
276            findings.push(format!(
277                "surface `{d}` is discoverable and has no coverage declaration: \
278                 declare its verdict axes, or declare why it emits no verdict"
279            ));
280        }
281    }
282
283    let mut seen: Vec<&str> = Vec::new();
284    for row in registry {
285        if seen.contains(&row.surface) {
286            findings.push(format!(
287                "surface `{}` is declared more than once",
288                row.surface
289            ));
290            continue;
291        }
292        seen.push(row.surface);
293
294        if !discovered.contains(&row.surface) {
295            findings.push(format!(
296                "declared surface `{}` is not discoverable: a stale declaration \
297                 reads as coverage, remove it or fix the walk",
298                row.surface
299            ));
300        }
301
302        match row.disposition {
303            CoverageDisposition::NoVerdict(reason) => {
304                if reason.trim().is_empty() {
305                    findings.push(format!(
306                        "surface `{}` declares no verdict without a reason",
307                        row.surface
308                    ));
309                }
310            }
311            CoverageDisposition::Verdict(cov) => {
312                for axis in cov.examined {
313                    if !vocab.contains(axis) {
314                        findings.push(format!(
315                            "surface `{}` examines axis `{axis}`, which the \
316                             vocabulary does not carry",
317                            row.surface
318                        ));
319                    }
320                    if cov.excluded.iter().any(|(a, _)| a == axis) {
321                        findings.push(format!(
322                            "surface `{}` both examines and excludes axis `{axis}`",
323                            row.surface
324                        ));
325                    }
326                }
327                for (axis, reason) in cov.excluded {
328                    if !vocab.contains(axis) {
329                        findings.push(format!(
330                            "surface `{}` excludes axis `{axis}`, which the \
331                             vocabulary does not carry",
332                            row.surface
333                        ));
334                    }
335                    if reason.trim().is_empty() {
336                        findings.push(format!(
337                            "surface `{}` excludes axis `{axis}` without a reason",
338                            row.surface
339                        ));
340                    }
341                }
342                for axis in vocab {
343                    let examined = cov.examined.contains(axis);
344                    let excluded = cov.excluded.iter().any(|(a, _)| a == axis);
345                    if !examined && !excluded {
346                        findings.push(format!(
347                            "surface `{}` declares nothing for axis `{axis}`: \
348                             its clean verdict would cover the axis by omission, \
349                             examine it or exclude it with a reason",
350                            row.surface
351                        ));
352                    }
353                }
354            }
355        }
356    }
357
358    findings
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    const VOCAB: &[&str] = &["anchors", "mounts"];
366
367    fn full() -> SurfaceCoverage {
368        SurfaceCoverage {
369            surface: "verify",
370            disposition: CoverageDisposition::Verdict(AxisCoverage {
371                examined: &["anchors"],
372                excluded: &[("mounts", "the roster surface answers for mounts")],
373            }),
374        }
375    }
376
377    fn ledger() -> SurfaceCoverage {
378        SurfaceCoverage {
379            surface: "check",
380            disposition: CoverageDisposition::NoVerdict(
381                "records the caller's verdict about the caller's own work",
382            ),
383        }
384    }
385
386    /// The complement: a registry that declares everything, over a
387    /// walk that finds exactly the declared surfaces, is clean, and
388    /// the only burden it carried was the declaration itself.
389    #[test]
390    fn complete_registry_is_clean() {
391        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check"]);
392        assert!(findings.is_empty(), "{findings:?}");
393    }
394
395    /// The gate red, fixture one: a surface whose clean verdict
396    /// covers an axis by omission. This reproduces the sweep's
397    /// condition shape independently of whether the sweep happened,
398    /// since the fixture is synthetic.
399    #[test]
400    fn clean_over_an_unexamined_axis_fails() {
401        let silent = SurfaceCoverage {
402            surface: "verify",
403            disposition: CoverageDisposition::Verdict(AxisCoverage {
404                examined: &["anchors"],
405                excluded: &[],
406            }),
407        };
408        let findings = validate_coverage(VOCAB, &[silent, ledger()], &["verify", "check"]);
409        assert!(
410            findings.iter().any(|f| f.contains("`verify`")
411                && f.contains("`mounts`")
412                && f.contains("by omission")),
413            "{findings:?}"
414        );
415    }
416
417    /// The gate red, fixture two: an axis is introduced and an
418    /// existing declaration is not updated. This is the recurrence
419    /// case; a gate that passes here is a one-time sweep.
420    #[test]
421    fn axis_added_without_declaration_update_fails() {
422        let grown: &[&str] = &["anchors", "mounts", "fences"];
423        let findings = validate_coverage(grown, &[full(), ledger()], &["verify", "check"]);
424        assert!(
425            findings
426                .iter()
427                .any(|f| f.contains("`verify`") && f.contains("`fences`")),
428            "{findings:?}"
429        );
430    }
431
432    /// A surface that landed without any declaration fails.
433    #[test]
434    fn undeclared_surface_fails() {
435        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check", "dump"]);
436        assert!(
437            findings
438                .iter()
439                .any(|f| f.contains("`dump`") && f.contains("no coverage declaration")),
440            "{findings:?}"
441        );
442    }
443
444    /// A declaration whose surface departed fails rather than skips.
445    #[test]
446    fn stale_surface_declaration_fails() {
447        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["check"]);
448        assert!(
449            findings
450                .iter()
451                .any(|f| f.contains("`verify`") && f.contains("not discoverable")),
452            "{findings:?}"
453        );
454    }
455
456    /// An axis dropped from the vocabulary turns the declarations
457    /// naming it into findings, so a stale axis cannot read as
458    /// coverage.
459    #[test]
460    fn stale_axis_in_declaration_fails() {
461        let shrunk: &[&str] = &["anchors"];
462        let findings = validate_coverage(shrunk, &[full(), ledger()], &["verify", "check"]);
463        assert!(
464            findings
465                .iter()
466                .any(|f| f.contains("`mounts`") && f.contains("does not carry")),
467            "{findings:?}"
468        );
469    }
470
471    /// Excluding an axis without a reason fails: an unexplained
472    /// exclusion is a silent drop with paperwork.
473    #[test]
474    fn exclusion_without_reason_fails() {
475        let bare = SurfaceCoverage {
476            surface: "verify",
477            disposition: CoverageDisposition::Verdict(AxisCoverage {
478                examined: &["anchors"],
479                excluded: &[("mounts", "  ")],
480            }),
481        };
482        let findings = validate_coverage(VOCAB, &[bare, ledger()], &["verify", "check"]);
483        assert!(
484            findings.iter().any(|f| f.contains("without a reason")),
485            "{findings:?}"
486        );
487    }
488
489    /// Examining and excluding the same axis is a contradiction, not
490    /// a double assurance.
491    #[test]
492    fn examined_and_excluded_fails() {
493        let both = SurfaceCoverage {
494            surface: "verify",
495            disposition: CoverageDisposition::Verdict(AxisCoverage {
496                examined: &["anchors", "mounts"],
497                excluded: &[("mounts", "also excluded")],
498            }),
499        };
500        let findings = validate_coverage(VOCAB, &[both, ledger()], &["verify", "check"]);
501        assert!(
502            findings
503                .iter()
504                .any(|f| f.contains("both examines and excludes")),
505            "{findings:?}"
506        );
507    }
508
509    /// Two rows for one surface fail: two claims, no single truth.
510    #[test]
511    fn duplicate_declaration_fails() {
512        let findings = validate_coverage(VOCAB, &[full(), full(), ledger()], &["verify", "check"]);
513        assert!(
514            findings
515                .iter()
516                .any(|f| f.contains("declared more than once")),
517            "{findings:?}"
518        );
519    }
520
521    /// The live vocabulary is the health roster plus the declared
522    /// extras, nothing more: composition, not a copy that can drift.
523    #[test]
524    fn vocabulary_composes_health_roster() {
525        let axes = verdict_axes();
526        for key in HEALTH_INCLUDE_KEYS {
527            assert!(axes.contains(key), "health include `{key}` missing");
528        }
529        for key in EXTRA_VERDICT_AXES {
530            assert!(axes.contains(key), "extra axis `{key}` missing");
531        }
532        assert_eq!(
533            axes.len(),
534            HEALTH_INCLUDE_KEYS.len() + EXTRA_VERDICT_AXES.len()
535        );
536    }
537}