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}