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];
145
146pub const COMMANDS_WITHOUT_ANALYSIS_ENVELOPE: &[(&str, &str)] = &[
148 (
149 "guard",
150 "per-file rule lookup before an edit; it reports rules, not findings",
151 ),
152 ("watch", "interactive loop that prints human output only"),
153 (
154 "fix",
155 "applies fixes; `report --from` detects its kind-less envelope by its fields",
156 ),
157 (
158 "list",
159 "project inspection; writes `list-boundaries` or `list-workspaces`",
160 ),
161 ("inspect", "evidence bundle for one target"),
162 ("trace", "call-chain and import-path trace for one symbol"),
163 ("trace-error", "stack-trace frame resolution"),
164 (
165 "decision-surface",
166 "advisory review signals of a change, not findings",
167 ),
168 ("workspaces", "workspace discovery diagnostics"),
169 ("explain", "static issue-type documentation"),
170 ("suppressions", "inventory of suppression markers"),
171 ("impact", "local impact digest of fallow itself"),
172 ("viz", "writes an HTML map"),
173 ("doctor", "project readiness checks"),
174 ("init", "writes a config file"),
175 ("agent", "wires fallow into agent tools"),
176 ("audit-cache", "maintains audit base-snapshot caches"),
177 (
178 "baselines",
179 "maintains committed baseline files; writes a `baselines-prune` envelope",
180 ),
181 ("recommend", "config recommendation for an agent"),
182 ("migrate", "config migration"),
183 ("config", "resolved config"),
184 ("config-schema", "JSON Schema of the config"),
185 ("plugin-schema", "JSON Schema of external plugins"),
186 ("plugin-check", "external plugin dry run"),
187 ("rule-pack", "rule-pack management"),
188 ("rule-pack-schema", "JSON Schema of rule packs"),
189 ("type-aware", "semantic companion status"),
190 ("ci", "builds PR and MR feedback from a saved envelope"),
191 ("ci-template", "CI template output"),
192 ("report", "renders a saved envelope"),
193 ("hooks", "Git and agent hook management"),
194 ("setup-hooks", "deprecated alias of hook installation"),
195 ("coverage", "runtime coverage setup and analysis"),
196 ("license", "license management"),
197 ("telemetry", "telemetry settings"),
198 ("schema", "capability manifest"),
199];
200
201#[must_use]
203pub fn command_envelope(command: &str) -> Option<&'static CommandEnvelope> {
204 COMMAND_ENVELOPES.iter().find(|row| row.command == command)
205}
206
207#[cfg(test)]
208mod tests {
209 use std::collections::BTreeSet;
210
211 use super::*;
212
213 #[test]
214 fn rows_name_each_command_and_kind_once() {
215 let commands: BTreeSet<&str> = COMMAND_ENVELOPES.iter().map(|row| row.command).collect();
216 assert_eq!(commands.len(), COMMAND_ENVELOPES.len());
217 let kinds: Vec<&str> = COMMAND_ENVELOPES
218 .iter()
219 .flat_map(|row| std::iter::once(row.kind).chain(row.grouped_kind))
220 .collect();
221 let unique: BTreeSet<&str> = kinds.iter().copied().collect();
222 assert_eq!(unique.len(), kinds.len(), "a kind is in two rows");
223 for (command, _) in COMMANDS_WITHOUT_ANALYSIS_ENVELOPE {
224 assert!(
225 command_envelope(command).is_none(),
226 "{command} is in both lists"
227 );
228 }
229 }
230
231 #[test]
232 fn every_row_has_an_mcp_tool_or_a_reason() {
233 for row in COMMAND_ENVELOPES {
234 assert_eq!(
235 row.mcp_tool.is_some(),
236 row.mcp_omission.is_none(),
237 "{}: set exactly one of mcp_tool and mcp_omission",
238 row.command
239 );
240 if let Some(tool) = row.mcp_tool {
241 let info = crate::mcp_manifest::MCP_TOOLS
242 .iter()
243 .find(|info| info.name == tool)
244 .unwrap_or_else(|| panic!("{}: no MCP tool `{tool}`", row.command));
245 let expected = format!("fallow {} ", row.command);
246 assert!(
247 info.cli_command
248 .is_some_and(|cli| cli.starts_with(expected.as_str())),
249 "{}: MCP tool `{tool}` names another CLI analogue: {:?}",
250 row.command,
251 info.cli_command
252 );
253 }
254 }
255 }
256}