Skip to main content

sui_spec/
cli_coverage.rs

1//! `cli_coverage` — typed self-description of sui's nix-replacement
2//! progress.
3//!
4//! Every subcommand sui exposes is declared as a Lisp form:
5//!
6//!   (defsui-command
7//!     :name             "store sign"
8//!     :nix-equivalent   "nix store sign"
9//!     :maturity         Working
10//!     :substrate        ("store_layout" "hash")
11//!     :notes            "Materializes ed25519-keyed signatures over NAR hashes")
12//!
13//! The substrate enforces a catalog ↔ source invariant: every
14//! command pattern in `sui/src/main.rs` must have a catalog entry,
15//! and every catalog entry must point at code that exists.  Adding
16//! a stub command **requires** landing its catalog entry in the
17//! same commit, so the operator-facing coverage matrix stays
18//! truthful.
19//!
20//! Operators query "how close is sui to a full nix replacement?"
21//! via `sui-spec-inventory --coverage`, which walks the catalog,
22//! groups by maturity, and emits a Nord-styled coverage gauge.
23//! The same data drives substrate-wide tickets — each `Missing`
24//! and each `Stub` is a queued substrate task.
25
26use serde::{Deserialize, Serialize};
27use tatara_lisp::DeriveTataraDomain;
28
29use crate::SpecError;
30
31/// One sui subcommand's coverage entry vs the equivalent nix
32/// surface.
33#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
34#[tatara(keyword = "defsui-command")]
35pub struct SuiCommand {
36    /// Stable command path — `"store sign"`, `"flake show"`,
37    /// `"rebuild-shadow"`.  Used as the catalog key + the
38    /// inventory subject.
39    pub name: String,
40    /// Equivalent canonical nix invocation.  Sometimes empty
41    /// when sui adds a primitive nix doesn't have
42    /// (e.g. `sui rebuild-shadow`).
43    #[serde(rename = "nixEquivalent")]
44    pub nix_equivalent: String,
45    /// Coverage maturity gate.
46    pub maturity: SuiCommandMaturity,
47    /// Substrate primitives the command consumes.
48    /// Cross-references `catalog::SubstrateDomain`.
49    #[serde(default)]
50    pub substrate: Vec<String>,
51    /// One-line operator-facing description.
52    pub notes: String,
53}
54
55/// Maturity gate for a sui subcommand — where it stands on the
56/// path to full nix replacement.
57#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum SuiCommandMaturity {
59    /// End-to-end working — operator can replace the nix
60    /// invocation today without behavior loss.
61    Working,
62    /// Partial — accepts the args, produces correct output for
63    /// the common path, but at least one known feature gap.
64    Partial,
65    /// Stub — argparser accepts the invocation but returns a
66    /// `NotImplemented` typed error.
67    Stub,
68    /// Missing — no argparser binding yet.  Sui doesn't accept
69    /// the command at all.
70    Missing,
71    /// Sui-native primitive — no nix equivalent.  Counted
72    /// separately so it doesn't dilute the replacement metric.
73    SuiNative,
74}
75
76impl SuiCommandMaturity {
77    /// Stable display name.
78    #[must_use]
79    pub fn name(self) -> &'static str {
80        match self {
81            Self::Working => "Working",
82            Self::Partial => "Partial",
83            Self::Stub => "Stub",
84            Self::Missing => "Missing",
85            Self::SuiNative => "SuiNative",
86        }
87    }
88
89    /// `true` if the command counts toward the replacement metric
90    /// (`Working` only — `Partial` doesn't count because of the
91    /// known gap; `SuiNative` doesn't count because there's no
92    /// nix equivalent).
93    #[must_use]
94    pub fn counts_as_replacing_nix(self) -> bool {
95        matches!(self, Self::Working)
96    }
97
98    /// `true` if the command is a queued substrate task
99    /// (`Partial` / `Stub` / `Missing`).
100    #[must_use]
101    pub fn is_queued_task(self) -> bool {
102        matches!(self, Self::Partial | Self::Stub | Self::Missing)
103    }
104}
105
106pub const CANONICAL_CLI_COVERAGE_LISP: &str =
107    include_str!("../specs/cli_coverage.lisp");
108
109/// Load the full canonical CLI coverage catalog.
110///
111/// # Errors
112///
113/// Fails if the Lisp source can't be parsed under the schema.
114pub fn load_canonical() -> Result<Vec<SuiCommand>, SpecError> {
115    crate::loader::load_all::<SuiCommand>(CANONICAL_CLI_COVERAGE_LISP)
116}
117
118/// Coverage histogram — how many commands sit in each maturity
119/// gate.  Operators query this for the headline number.
120///
121/// # Errors
122///
123/// Returns the same errors as [`load_canonical`].
124pub fn maturity_histogram()
125    -> Result<Vec<(SuiCommandMaturity, usize)>, SpecError>
126{
127    use std::collections::BTreeMap;
128    let cat = load_canonical()?;
129    let mut counts: BTreeMap<String, usize> = BTreeMap::new();
130    for c in &cat {
131        *counts.entry(c.maturity.name().to_string()).or_default() += 1;
132    }
133    // Stable order: Working → Partial → Stub → Missing → SuiNative
134    let order = [
135        SuiCommandMaturity::Working,
136        SuiCommandMaturity::Partial,
137        SuiCommandMaturity::Stub,
138        SuiCommandMaturity::Missing,
139        SuiCommandMaturity::SuiNative,
140    ];
141    Ok(order
142        .into_iter()
143        .map(|m| (m, *counts.get(m.name()).unwrap_or(&0)))
144        .collect())
145}
146
147/// Headline coverage number: `Working / (everything that isn't SuiNative)`.
148///
149/// # Errors
150///
151/// Returns the same errors as [`load_canonical`].
152pub fn replacement_percentage() -> Result<f64, SpecError> {
153    let cat = load_canonical()?;
154    let total_nix: usize = cat.iter()
155        .filter(|c| c.maturity != SuiCommandMaturity::SuiNative)
156        .count();
157    let working: usize = cat.iter()
158        .filter(|c| c.maturity == SuiCommandMaturity::Working)
159        .count();
160    if total_nix == 0 {
161        return Ok(0.0);
162    }
163    Ok(working as f64 / total_nix as f64)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn canonical_catalog_parses() {
172        let cat = load_canonical().expect("catalog must parse");
173        assert!(!cat.is_empty(), "catalog must have ≥1 entry");
174    }
175
176    #[test]
177    fn every_command_has_unique_name() {
178        let cat = load_canonical().unwrap();
179        let mut seen = std::collections::HashSet::new();
180        for c in &cat {
181            assert!(seen.insert(c.name.clone()),
182                "duplicate sui command name `{}`", c.name);
183        }
184    }
185
186    #[test]
187    fn histogram_sums_to_total() {
188        let cat = load_canonical().unwrap();
189        let hist = maturity_histogram().unwrap();
190        let total: usize = hist.iter().map(|(_, n)| n).sum();
191        assert_eq!(total, cat.len());
192    }
193
194    #[test]
195    fn replacement_percentage_is_in_range() {
196        let pct = replacement_percentage().unwrap();
197        assert!((0.0..=1.0).contains(&pct));
198    }
199
200    #[test]
201    fn every_substrate_ref_points_at_a_real_domain() {
202        let cat = load_canonical().unwrap();
203        let domains = crate::catalog::load_canonical().unwrap();
204        let names: std::collections::HashSet<String> = domains
205            .iter()
206            .map(|d| d.name.clone())
207            .collect();
208        for c in &cat {
209            for s in &c.substrate {
210                assert!(
211                    names.contains(s),
212                    "command `{}` references substrate `{}` which has no catalog entry",
213                    c.name, s,
214                );
215            }
216        }
217    }
218}