1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ReportFrom {
18 Renders,
20 Refused(&'static str),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CiIntegration {
27 Routed,
30 Omitted(&'static str),
32}
33
34#[derive(Debug, Clone, Copy)]
36pub struct CommandEnvelope {
37 pub command: &'static str,
39 pub kind: &'static str,
41 pub grouped_kind: Option<&'static str>,
43 pub report_from: ReportFrom,
45 pub mcp_tool: Option<&'static str>,
47 pub mcp_omission: Option<&'static str>,
49 pub verdict: bool,
52 pub ci: CiIntegration,
54}
55
56pub const COMMAND_ENVELOPES: &[CommandEnvelope] = &[
58 CommandEnvelope {
59 command: "dead-code",
60 kind: "dead-code",
61 grouped_kind: Some("dead-code-grouped"),
62 report_from: ReportFrom::Renders,
63 mcp_tool: Some("analyze"),
64 mcp_omission: None,
65 verdict: true,
66 ci: CiIntegration::Routed,
67 },
68 CommandEnvelope {
69 command: "architecture",
70 kind: "architecture",
71 grouped_kind: Some("architecture-grouped"),
72 report_from: ReportFrom::Renders,
73 mcp_tool: Some("check_architecture"),
74 mcp_omission: None,
75 verdict: true,
76 ci: CiIntegration::Routed,
77 },
78 CommandEnvelope {
79 command: "dupes",
80 kind: "dupes",
81 grouped_kind: None,
82 report_from: ReportFrom::Renders,
83 mcp_tool: Some("find_dupes"),
84 mcp_omission: None,
85 verdict: true,
86 ci: CiIntegration::Routed,
87 },
88 CommandEnvelope {
89 command: "health",
90 kind: "health",
91 grouped_kind: None,
92 report_from: ReportFrom::Renders,
93 mcp_tool: Some("check_health"),
94 mcp_omission: None,
95 verdict: true,
96 ci: CiIntegration::Routed,
97 },
98 CommandEnvelope {
99 command: "security",
100 kind: "security",
101 grouped_kind: None,
102 report_from: ReportFrom::Renders,
103 mcp_tool: Some("security_candidates"),
104 mcp_omission: None,
105 verdict: true,
106 ci: CiIntegration::Routed,
107 },
108 CommandEnvelope {
109 command: "audit",
110 kind: "audit",
111 grouped_kind: None,
112 report_from: ReportFrom::Renders,
113 mcp_tool: Some("audit"),
114 mcp_omission: None,
115 verdict: true,
116 ci: CiIntegration::Routed,
117 },
118 CommandEnvelope {
119 command: "flags",
120 kind: "feature-flags",
121 grouped_kind: None,
122 report_from: ReportFrom::Refused(
123 "feature flags are an inventory with no CI annotation or review surface",
124 ),
125 mcp_tool: Some("feature_flags"),
126 mcp_omission: None,
127 verdict: false,
128 ci: CiIntegration::Omitted("feature flags are an inventory with no CI gate or annotation"),
129 },
130 CommandEnvelope {
131 command: "similar-code",
132 kind: "similar-code",
133 grouped_kind: None,
134 report_from: ReportFrom::Refused(
135 "similar-code candidates are unverified and never feed a CI surface",
136 ),
137 mcp_tool: Some("find_similar_code"),
138 mcp_omission: None,
139 verdict: false,
140 ci: CiIntegration::Omitted(
141 "similar-code candidates are unverified and never feed a CI surface",
142 ),
143 },
144 CommandEnvelope {
145 command: "mutation",
146 kind: "mutation",
147 grouped_kind: None,
148 report_from: ReportFrom::Refused(
149 "mutation results come from one opt-in test run and feed no CI surface in version 1",
150 ),
151 mcp_tool: Some("mutation_plan"),
152 mcp_omission: None,
153 verdict: false,
154 ci: CiIntegration::Omitted(
155 "mutation testing runs the project tests; only an explicit local command starts a run",
156 ),
157 },
158];
159
160pub const COMMANDS_WITHOUT_ANALYSIS_ENVELOPE: &[(&str, &str)] = &[
162 (
163 "guard",
164 "per-file rule lookup before an edit; it reports rules, not findings",
165 ),
166 ("watch", "interactive loop that prints human output only"),
167 (
168 "fix",
169 "applies fixes; `report --from` detects its kind-less envelope by its fields",
170 ),
171 (
172 "list",
173 "project inspection; writes `list-boundaries` or `list-workspaces`",
174 ),
175 ("inspect", "evidence bundle for one target"),
176 ("trace", "call-chain and import-path trace for one symbol"),
177 ("trace-error", "stack-trace frame resolution"),
178 (
179 "decision-surface",
180 "advisory review signals of a change, not findings",
181 ),
182 ("workspaces", "workspace discovery diagnostics"),
183 ("explain", "static issue-type documentation"),
184 ("suppressions", "inventory of suppression markers"),
185 ("impact", "local impact digest of fallow itself"),
186 ("viz", "writes an HTML map"),
187 ("doctor", "project readiness checks"),
188 ("init", "writes a config file"),
189 ("agent", "wires fallow into agent tools"),
190 ("audit-cache", "maintains audit base-snapshot caches"),
191 (
192 "baselines",
193 "maintains committed baseline files; writes a `baselines-prune` envelope",
194 ),
195 ("recommend", "config recommendation for an agent"),
196 ("migrate", "config migration"),
197 ("config", "resolved config"),
198 ("config-schema", "JSON Schema of the config"),
199 ("plugin-schema", "JSON Schema of external plugins"),
200 ("plugin-check", "external plugin dry run"),
201 ("rule-pack", "rule-pack management"),
202 ("rule-pack-schema", "JSON Schema of rule packs"),
203 ("type-aware", "semantic companion status"),
204 ("ci", "builds PR and MR feedback from a saved envelope"),
205 ("ci-template", "CI template output"),
206 ("report", "renders a saved envelope"),
207 ("hooks", "Git and agent hook management"),
208 ("setup-hooks", "deprecated alias of hook installation"),
209 ("coverage", "runtime coverage setup and analysis"),
210 ("license", "license management"),
211 ("telemetry", "telemetry settings"),
212 ("schema", "capability manifest"),
213];
214
215#[must_use]
217pub fn command_envelope(command: &str) -> Option<&'static CommandEnvelope> {
218 COMMAND_ENVELOPES.iter().find(|row| row.command == command)
219}
220
221#[cfg(test)]
222mod tests {
223 use std::collections::BTreeSet;
224
225 use super::*;
226
227 #[test]
228 fn rows_name_each_command_and_kind_once() {
229 let commands: BTreeSet<&str> = COMMAND_ENVELOPES.iter().map(|row| row.command).collect();
230 assert_eq!(commands.len(), COMMAND_ENVELOPES.len());
231 let kinds: Vec<&str> = COMMAND_ENVELOPES
232 .iter()
233 .flat_map(|row| std::iter::once(row.kind).chain(row.grouped_kind))
234 .collect();
235 let unique: BTreeSet<&str> = kinds.iter().copied().collect();
236 assert_eq!(unique.len(), kinds.len(), "a kind is in two rows");
237 for (command, _) in COMMANDS_WITHOUT_ANALYSIS_ENVELOPE {
238 assert!(
239 command_envelope(command).is_none(),
240 "{command} is in both lists"
241 );
242 }
243 }
244
245 #[test]
246 fn every_row_has_an_mcp_tool_or_a_reason() {
247 for row in COMMAND_ENVELOPES {
248 assert_eq!(
249 row.mcp_tool.is_some(),
250 row.mcp_omission.is_none(),
251 "{}: set exactly one of mcp_tool and mcp_omission",
252 row.command
253 );
254 if let Some(tool) = row.mcp_tool {
255 let info = crate::mcp_manifest::MCP_TOOLS
256 .iter()
257 .find(|info| info.name == tool)
258 .unwrap_or_else(|| panic!("{}: no MCP tool `{tool}`", row.command));
259 let expected = format!("fallow {} ", row.command);
260 assert!(
261 info.cli_command
262 .is_some_and(|cli| cli.starts_with(expected.as_str())),
263 "{}: MCP tool `{tool}` names another CLI analogue: {:?}",
264 row.command,
265 info.cli_command
266 );
267 }
268 }
269 }
270}