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, which it reports beside the verdict without folding
64/// them in, and which lie outside its scope altogether. The three
65/// lists must jointly name every axis in the vocabulary; a blanket
66/// "everything else" clause is deliberately impossible, because it
67/// would swallow a newly introduced axis silently, and the one
68/// permanent property this module owes is that a new axis fails
69/// every declaration that has not met it.
70///
71/// The three buckets, as they read on the wire:
72/// - `examined`: the verdict answers for the axis; a finding there
73///   makes the verdict fail.
74/// - `advisory`: the surface renders the axis (always, or on request)
75///   beside the verdict and never folds it in; a reader sees the
76///   figures, the verdict says nothing about them. Filing these under
77///   "not examined" read, to every independent reader so far, as "not
78///   looked at", and a gate reading the word would refuse a report
79///   that had done the work.
80/// - `not_examined`: the surface does not look at the axis at all;
81///   the reason names the surface that answers for it.
82#[derive(Debug, Clone, Copy)]
83pub struct AxisCoverage {
84    /// Axes the surface's clean verdict actually examined.
85    pub examined: &'static [&'static str],
86    /// Axes the surface reports beside its verdict without folding
87    /// them in, each with the reason a reader needs.
88    pub advisory: &'static [(&'static str, &'static str)],
89    /// Axes the surface does not examine, each with the reason a
90    /// reader needs (typically: which surface answers for it instead).
91    pub not_examined: &'static [(&'static str, &'static str)],
92}
93
94/// What a declared surface claims about verdicts.
95#[derive(Debug, Clone, Copy)]
96pub enum CoverageDisposition {
97    /// The surface can emit a clean/ok verdict and declares its axes.
98    Verdict(AxisCoverage),
99    /// The surface emits no clean/ok verdict; the reason says why the
100    /// rule does not bind it (it returns data, it reports what a
101    /// mutation did, or its verdict belongs to the caller).
102    NoVerdict(&'static str),
103}
104
105/// One registry row: a surface name exactly as the consumer's own
106/// mechanical walk produces it, plus its disposition.
107#[derive(Debug, Clone, Copy)]
108pub struct SurfaceCoverage {
109    pub surface: &'static str,
110    pub disposition: CoverageDisposition,
111}
112
113impl AxisCoverage {
114    /// The examined set as it is stamped into surface output.
115    pub fn examined_wire(&self) -> Vec<&'static str> {
116        self.examined.to_vec()
117    }
118
119    /// The advisory axes as they are stamped into surface output:
120    /// `(axis, reason)` pairs, so a reader can see which axes the
121    /// surface reports beside its verdict without reading the source.
122    pub fn advisory_wire(&self) -> Vec<(&'static str, &'static str)> {
123        self.advisory.to_vec()
124    }
125
126    /// The unexamined axes as they are stamped into surface output:
127    /// `(axis, reason)` pairs, so a reader can see which axes the
128    /// verdict does not cover without reading the source.
129    pub fn not_examined_wire(&self) -> Vec<(&'static str, &'static str)> {
130        self.not_examined.to_vec()
131    }
132
133    /// The declaration as it is stamped into surface output: one
134    /// compact line naming both axis sets, the same form on JSON,
135    /// markdown, and frontmatter surfaces. Axis names only: the
136    /// per-axis exclusion reasons stay a registry fact the gate test
137    /// enforces, because stamping static prose into every response
138    /// would tax each call's token budget, and the reader's question
139    /// the stamp answers is WHICH axes the verdict covers.
140    pub fn wire_line(&self) -> String {
141        self.wire_line_promoting(&[])
142    }
143
144    /// The wire line with the named advisory or unexamined axes
145    /// promoted into the examined set — for a report that rendered an
146    /// opt-in axis this pass (`--include anchors`) and therefore did
147    /// examine it. An axis in neither list is ignored; the static
148    /// declaration is untouched.
149    pub fn wire_line_promoting(&self, promoted: &[&str]) -> String {
150        let mut examined: Vec<&str> = self.examined.to_vec();
151        let mut advisory: Vec<&str> = Vec::new();
152        let mut not_examined: Vec<&str> = Vec::new();
153        for (a, _) in self.advisory {
154            if promoted.contains(a) {
155                examined.push(a);
156            } else {
157                advisory.push(a);
158            }
159        }
160        for (a, _) in self.not_examined {
161            if promoted.contains(a) {
162                examined.push(a);
163            } else {
164                not_examined.push(a);
165            }
166        }
167        format!(
168            "examined={}; advisory={}; not_examined={}",
169            examined.join(","),
170            advisory.join(","),
171            not_examined.join(",")
172        )
173    }
174}
175
176impl SurfaceCoverage {
177    /// The verdict declaration, when this row carries one; the
178    /// stamping sites use it so a surface can only stamp what its
179    /// registry row declares.
180    pub fn axis_coverage(&self) -> Option<&AxisCoverage> {
181        match &self.disposition {
182            CoverageDisposition::Verdict(c) => Some(c),
183            CoverageDisposition::NoVerdict(_) => None,
184        }
185    }
186}
187
188/// The health surface's coverage claim, shared by every consumer
189/// that renders a health report (the CLI command, the full MCP
190/// server's composer, and the lean server's own assembly): the axes
191/// whose findings the report treats as defects, so an empty defect
192/// statement reads as an all-clear exactly over them. Everything
193/// descriptive or advisory is excluded by name.
194pub const HEALTH_COVERAGE: AxisCoverage = AxisCoverage {
195    examined: &[
196        "dangling_links",
197        "missing_required_outgoing",
198        "constraints",
199        "signals",
200        "integrity",
201        "config",
202        "mounts",
203    ],
204    // Every axis health renders (always, or on `--include`) without
205    // folding it into the defect verdict. `anchors` is promoted into
206    // the examined set for the pass that rendered it.
207    advisory: &[
208        (
209            "orphans",
210            "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
211        ),
212        (
213            "stubs",
214            "descriptive list; the defect verdict polices orphan stubs through the integrity findings",
215        ),
216        (
217            "most_connected",
218            "descriptive ranking with no pass/fail semantics",
219        ),
220        (
221            "missing_fields",
222            "advisory count, never part of the defect verdict",
223        ),
224        (
225            "stale",
226            "advisory freshness, never part of the defect verdict",
227        ),
228        (
229            "tags",
230            "descriptive distribution with no pass/fail semantics",
231        ),
232        (
233            "labelling",
234            "advisory audit, never part of the defect verdict",
235        ),
236        (
237            "conformance",
238            "reported per entity beside the verdict, never folded into it",
239        ),
240        (
241            "anchors",
242            "drifted anchors stay advisory; the verify surfaces carry the drift statement",
243        ),
244        ("friction", "descriptive ledger counts"),
245        ("open_questions", "descriptive listing of open questions"),
246        (
247            "vital_signs",
248            "descriptive model-truth counts; the remodel skill holds the thresholds",
249        ),
250        (
251            "stale_derivations",
252            "advisory freshness of derived artifacts",
253        ),
254        (
255            "checks",
256            "check states are derived views; the verdicts in them belong to their recording callers",
257        ),
258        ("ledger", "descriptive view of the check ledger"),
259    ],
260    not_examined: &[(
261        "projection",
262        "projection fidelity is answered by status and projection verify",
263    )],
264};
265
266/// The overview surface's coverage claim, shared by every consumer
267/// that renders the composed overview (the CLI command and both MCP
268/// servers), and stamped into the composed frontmatter by
269/// `compose_overview` itself so the declaration and the output cannot
270/// diverge.
271pub const OVERVIEW_COVERAGE: AxisCoverage = AxisCoverage {
272    examined: &["mounts", "config"],
273    advisory: &[(
274        "dangling_links",
275        "rendered on request as a listing; the verdict over them is health's",
276    )],
277    not_examined: &[
278        ("orphans", OVERVIEW_SCOPE),
279        ("stubs", OVERVIEW_SCOPE),
280        ("most_connected", OVERVIEW_SCOPE),
281        ("missing_fields", OVERVIEW_SCOPE),
282        ("stale", OVERVIEW_SCOPE),
283        ("tags", OVERVIEW_SCOPE),
284        ("missing_required_outgoing", OVERVIEW_SCOPE),
285        ("constraints", OVERVIEW_SCOPE),
286        ("signals", OVERVIEW_SCOPE),
287        ("labelling", OVERVIEW_SCOPE),
288        ("conformance", OVERVIEW_SCOPE),
289        ("integrity", OVERVIEW_SCOPE),
290        ("anchors", OVERVIEW_SCOPE),
291        ("friction", OVERVIEW_SCOPE),
292        ("open_questions", OVERVIEW_SCOPE),
293        ("vital_signs", OVERVIEW_SCOPE),
294        ("stale_derivations", OVERVIEW_SCOPE),
295        ("checks", OVERVIEW_SCOPE),
296        ("ledger", OVERVIEW_SCOPE),
297        ("projection", OVERVIEW_SCOPE),
298    ],
299};
300
301const OVERVIEW_SCOPE: &str = "overview is a descriptive composition; its only \
302     all-clear claim is that the roster it renders is complete and its mounts serve";
303
304/// Hold a registry against the axis vocabulary and a mechanically
305/// discovered surface roster. Returns one finding per defect; an
306/// empty result is the only clean outcome. Pure and total: callers
307/// in tests pass the live vocabulary and their own live walk,
308/// fixtures pass synthetic ones.
309///
310/// The findings, each mapped to the failure it refuses:
311/// - a discovered surface with no registry row (a surface landed
312///   without declaring),
313/// - a registry row no walk discovers (a stale declaration reading
314///   as coverage),
315/// - a duplicate row (two claims, no single truth),
316/// - an axis named by a declaration that the vocabulary does not
317///   carry (a stale axis reading as coverage),
318/// - an axis in the vocabulary that a verdict declaration neither
319///   examines, reports as advisory, nor excludes (a new axis met by
320///   silence: the clean verdict would cover it by omission),
321/// - an axis in more than one bucket (a contradiction),
322/// - an advisory, exclusion or no-verdict claim with an empty reason
323///   (a declaration that declares nothing).
324pub fn validate_coverage(
325    vocab: &[&str],
326    registry: &[SurfaceCoverage],
327    discovered: &[&str],
328) -> Vec<String> {
329    let mut findings = Vec::new();
330
331    for d in discovered {
332        if !registry.iter().any(|r| r.surface == *d) {
333            findings.push(format!(
334                "surface `{d}` is discoverable and has no coverage declaration: \
335                 declare its verdict axes, or declare why it emits no verdict"
336            ));
337        }
338    }
339
340    let mut seen: Vec<&str> = Vec::new();
341    for row in registry {
342        if seen.contains(&row.surface) {
343            findings.push(format!(
344                "surface `{}` is declared more than once",
345                row.surface
346            ));
347            continue;
348        }
349        seen.push(row.surface);
350
351        if !discovered.contains(&row.surface) {
352            findings.push(format!(
353                "declared surface `{}` is not discoverable: a stale declaration \
354                 reads as coverage, remove it or fix the walk",
355                row.surface
356            ));
357        }
358
359        match row.disposition {
360            CoverageDisposition::NoVerdict(reason) => {
361                if reason.trim().is_empty() {
362                    findings.push(format!(
363                        "surface `{}` declares no verdict without a reason",
364                        row.surface
365                    ));
366                }
367            }
368            CoverageDisposition::Verdict(cov) => {
369                let in_advisory = |axis: &str| cov.advisory.iter().any(|(a, _)| *a == axis);
370                let in_not_examined = |axis: &str| cov.not_examined.iter().any(|(a, _)| *a == axis);
371                for axis in cov.examined {
372                    if !vocab.contains(axis) {
373                        findings.push(format!(
374                            "surface `{}` examines axis `{axis}`, which the \
375                             vocabulary does not carry",
376                            row.surface
377                        ));
378                    }
379                    if in_advisory(axis) || in_not_examined(axis) {
380                        findings.push(format!(
381                            "surface `{}` both examines and excludes axis `{axis}`",
382                            row.surface
383                        ));
384                    }
385                }
386                for (bucket, rows) in [
387                    ("advisory", cov.advisory),
388                    ("not_examined", cov.not_examined),
389                ] {
390                    for (axis, reason) in rows {
391                        if !vocab.contains(axis) {
392                            findings.push(format!(
393                                "surface `{}` files axis `{axis}` as {bucket}, which the \
394                                 vocabulary does not carry",
395                                row.surface
396                            ));
397                        }
398                        if reason.trim().is_empty() {
399                            findings.push(format!(
400                                "surface `{}` files axis `{axis}` as {bucket} without a reason",
401                                row.surface
402                            ));
403                        }
404                    }
405                }
406                for (axis, _) in cov.advisory {
407                    if in_not_examined(axis) {
408                        findings.push(format!(
409                            "surface `{}` files axis `{axis}` as both advisory and not_examined",
410                            row.surface
411                        ));
412                    }
413                }
414                for axis in vocab {
415                    let examined = cov.examined.contains(axis);
416                    if !examined && !in_advisory(axis) && !in_not_examined(axis) {
417                        findings.push(format!(
418                            "surface `{}` declares nothing for axis `{axis}`: \
419                             its clean verdict would cover the axis by omission, \
420                             examine it, report it as advisory, or exclude it with a reason",
421                            row.surface
422                        ));
423                    }
424                }
425            }
426        }
427    }
428
429    findings
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    const VOCAB: &[&str] = &["anchors", "mounts"];
437
438    fn full() -> SurfaceCoverage {
439        SurfaceCoverage {
440            surface: "verify",
441            disposition: CoverageDisposition::Verdict(AxisCoverage {
442                examined: &["anchors"],
443                advisory: &[],
444                not_examined: &[("mounts", "the roster surface answers for mounts")],
445            }),
446        }
447    }
448
449    fn ledger() -> SurfaceCoverage {
450        SurfaceCoverage {
451            surface: "check",
452            disposition: CoverageDisposition::NoVerdict(
453                "records the caller's verdict about the caller's own work",
454            ),
455        }
456    }
457
458    /// The complement: a registry that declares everything, over a
459    /// walk that finds exactly the declared surfaces, is clean, and
460    /// the only burden it carried was the declaration itself.
461    #[test]
462    fn complete_registry_is_clean() {
463        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check"]);
464        assert!(findings.is_empty(), "{findings:?}");
465    }
466
467    /// The gate red, fixture one: a surface whose clean verdict
468    /// covers an axis by omission. This reproduces the sweep's
469    /// condition shape independently of whether the sweep happened,
470    /// since the fixture is synthetic.
471    #[test]
472    fn clean_over_an_unexamined_axis_fails() {
473        let silent = SurfaceCoverage {
474            surface: "verify",
475            disposition: CoverageDisposition::Verdict(AxisCoverage {
476                examined: &["anchors"],
477                advisory: &[],
478                not_examined: &[],
479            }),
480        };
481        let findings = validate_coverage(VOCAB, &[silent, ledger()], &["verify", "check"]);
482        assert!(
483            findings.iter().any(|f| f.contains("`verify`")
484                && f.contains("`mounts`")
485                && f.contains("by omission")),
486            "{findings:?}"
487        );
488    }
489
490    /// The gate red, fixture two: an axis is introduced and an
491    /// existing declaration is not updated. This is the recurrence
492    /// case; a gate that passes here is a one-time sweep.
493    #[test]
494    fn axis_added_without_declaration_update_fails() {
495        let grown: &[&str] = &["anchors", "mounts", "fences"];
496        let findings = validate_coverage(grown, &[full(), ledger()], &["verify", "check"]);
497        assert!(
498            findings
499                .iter()
500                .any(|f| f.contains("`verify`") && f.contains("`fences`")),
501            "{findings:?}"
502        );
503    }
504
505    /// A surface that landed without any declaration fails.
506    #[test]
507    fn undeclared_surface_fails() {
508        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["verify", "check", "dump"]);
509        assert!(
510            findings
511                .iter()
512                .any(|f| f.contains("`dump`") && f.contains("no coverage declaration")),
513            "{findings:?}"
514        );
515    }
516
517    /// A declaration whose surface departed fails rather than skips.
518    #[test]
519    fn stale_surface_declaration_fails() {
520        let findings = validate_coverage(VOCAB, &[full(), ledger()], &["check"]);
521        assert!(
522            findings
523                .iter()
524                .any(|f| f.contains("`verify`") && f.contains("not discoverable")),
525            "{findings:?}"
526        );
527    }
528
529    /// An axis dropped from the vocabulary turns the declarations
530    /// naming it into findings, so a stale axis cannot read as
531    /// coverage.
532    #[test]
533    fn stale_axis_in_declaration_fails() {
534        let shrunk: &[&str] = &["anchors"];
535        let findings = validate_coverage(shrunk, &[full(), ledger()], &["verify", "check"]);
536        assert!(
537            findings
538                .iter()
539                .any(|f| f.contains("`mounts`") && f.contains("does not carry")),
540            "{findings:?}"
541        );
542    }
543
544    /// Excluding an axis without a reason fails: an unexplained
545    /// exclusion is a silent drop with paperwork.
546    #[test]
547    fn exclusion_without_reason_fails() {
548        let bare = SurfaceCoverage {
549            surface: "verify",
550            disposition: CoverageDisposition::Verdict(AxisCoverage {
551                examined: &["anchors"],
552                advisory: &[],
553                not_examined: &[("mounts", "  ")],
554            }),
555        };
556        let findings = validate_coverage(VOCAB, &[bare, ledger()], &["verify", "check"]);
557        assert!(
558            findings.iter().any(|f| f.contains("without a reason")),
559            "{findings:?}"
560        );
561    }
562
563    /// Examining and excluding the same axis is a contradiction, not
564    /// a double assurance.
565    #[test]
566    fn examined_and_excluded_fails() {
567        let both = SurfaceCoverage {
568            surface: "verify",
569            disposition: CoverageDisposition::Verdict(AxisCoverage {
570                examined: &["anchors", "mounts"],
571                advisory: &[],
572                not_examined: &[("mounts", "also excluded")],
573            }),
574        };
575        let findings = validate_coverage(VOCAB, &[both, ledger()], &["verify", "check"]);
576        assert!(
577            findings
578                .iter()
579                .any(|f| f.contains("both examines and excludes")),
580            "{findings:?}"
581        );
582    }
583
584    /// Two rows for one surface fail: two claims, no single truth.
585    #[test]
586    fn duplicate_declaration_fails() {
587        let findings = validate_coverage(VOCAB, &[full(), full(), ledger()], &["verify", "check"]);
588        assert!(
589            findings
590                .iter()
591                .any(|f| f.contains("declared more than once")),
592            "{findings:?}"
593        );
594    }
595
596    /// An axis filed as both advisory and not examined is a
597    /// contradiction, not a double assurance.
598    #[test]
599    fn advisory_and_not_examined_fails() {
600        let both = SurfaceCoverage {
601            surface: "verify",
602            disposition: CoverageDisposition::Verdict(AxisCoverage {
603                examined: &["anchors"],
604                advisory: &[("mounts", "rendered beside the verdict")],
605                not_examined: &[("mounts", "the roster surface answers for mounts")],
606            }),
607        };
608        let findings = validate_coverage(VOCAB, &[both, ledger()], &["verify", "check"]);
609        assert!(
610            findings
611                .iter()
612                .any(|f| f.contains("both advisory and not_examined")),
613            "{findings:?}"
614        );
615    }
616
617    /// The wire line names all three buckets, and a promoted axis
618    /// leaves its bucket for the examined set.
619    #[test]
620    fn wire_line_names_three_buckets_and_promotes() {
621        let cov = AxisCoverage {
622            examined: &["mounts"],
623            advisory: &[("anchors", "rendered on request")],
624            not_examined: &[("projection", "another surface answers")],
625        };
626        assert_eq!(
627            cov.wire_line(),
628            "examined=mounts; advisory=anchors; not_examined=projection"
629        );
630        assert_eq!(
631            cov.wire_line_promoting(&["anchors"]),
632            "examined=mounts,anchors; advisory=; not_examined=projection"
633        );
634    }
635
636    /// The health declaration files every axis it can render as
637    /// examined or advisory and only the axis it never renders as not
638    /// examined: the bucket names say what the report did.
639    #[test]
640    fn health_declaration_buckets_follow_what_the_report_renders() {
641        let cov = HEALTH_COVERAGE;
642        for key in HEALTH_INCLUDE_KEYS {
643            let examined = cov.examined.contains(key);
644            let advisory = cov.advisory.iter().any(|(a, _)| a == key);
645            assert!(
646                examined || advisory,
647                "health include `{key}` renders on request, so it is examined or advisory"
648            );
649        }
650        assert_eq!(
651            cov.not_examined.iter().map(|(a, _)| *a).collect::<Vec<_>>(),
652            vec!["projection"]
653        );
654    }
655
656    /// The live vocabulary is the health roster plus the declared
657    /// extras, nothing more: composition, not a copy that can drift.
658    #[test]
659    fn vocabulary_composes_health_roster() {
660        let axes = verdict_axes();
661        for key in HEALTH_INCLUDE_KEYS {
662            assert!(axes.contains(key), "health include `{key}` missing");
663        }
664        for key in EXTRA_VERDICT_AXES {
665            assert!(axes.contains(key), "extra axis `{key}` missing");
666        }
667        assert_eq!(
668            axes.len(),
669            HEALTH_INCLUDE_KEYS.len() + EXTRA_VERDICT_AXES.len()
670        );
671    }
672}