1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TaskRow {
31 pub task: &'static str,
33 pub command: &'static str,
35 pub note: Option<&'static str>,
37 pub probe: &'static [&'static str],
40}
41
42pub const TASK_MATRIX: &[TaskRow] = &[
45 TaskRow {
46 task: "delete an \"unused\" export or file",
47 command: "fallow dead-code --trace <file>:<export>",
48 note: None,
49 probe: &["dead-code", "--trace", "src/index.ts:foo"],
50 },
51 TaskRow {
52 task: "prove a TypeScript symbol's exact consumers before refactoring",
53 command: "fallow dead-code --type-aware --symbol-impact <file>:<export-or-class.method>",
54 note: None,
55 probe: &[
56 "dead-code",
57 "--type-aware",
58 "--symbol-impact",
59 "src/index.ts:foo",
60 ],
61 },
62 TaskRow {
63 task: "delete an \"unused\" dependency",
64 command: "fallow dead-code --trace-dependency <name>",
65 note: None,
66 probe: &["dead-code", "--trace-dependency", "lodash"],
67 },
68 TaskRow {
69 task: "commit or open a PR",
70 command: "fallow audit --base <ref>",
71 note: None,
72 probe: &["audit", "--base", "main"],
73 },
74 TaskRow {
75 task: "prioritize refactoring",
76 command: "fallow health --hotspots --targets",
77 note: None,
78 probe: &["health", "--hotspots", "--targets"],
79 },
80 TaskRow {
81 task: "ask who owns code",
82 command: "fallow health --ownership",
83 note: None,
84 probe: &["health", "--ownership"],
85 },
86 TaskRow {
87 task: "check untested-but-reachable code",
88 command: "fallow health --coverage-gaps",
89 note: None,
90 probe: &["health", "--coverage-gaps"],
91 },
92 TaskRow {
93 task: "consolidate duplication",
94 command: "fallow dupes --trace dup:<fingerprint>",
95 note: None,
96 probe: &["dupes", "--trace", "dup:abc123"],
97 },
98 TaskRow {
99 task: "find feature flags",
100 command: "fallow flags",
101 note: None,
102 probe: &["flags"],
103 },
104 TaskRow {
105 task: "check which architecture rules apply to a file before changing it",
106 command: "fallow guard <files>",
107 note: None,
108 probe: &["guard", "src/index.ts"],
109 },
110 TaskRow {
111 task: "surface security candidates",
112 command: "fallow security",
113 note: None,
114 probe: &["security"],
115 },
116 TaskRow {
117 task: "understand a finding",
118 command: "fallow explain <issue-type>",
119 note: None,
120 probe: &["explain", "unused-export"],
121 },
122 TaskRow {
123 task: "scope a monorepo",
124 command: "--workspace <glob> / --changed-workspaces <ref>",
125 note: Some("global flags, prefix any command"),
126 probe: &[],
129 },
130];
131
132impl TaskRow {
133 #[must_use]
137 pub fn to_json(&self) -> serde_json::Value {
138 serde_json::json!({
139 "task": self.task,
140 "command": self.command,
141 "note": self.note,
142 })
143 }
144}
145
146pub const MUTATING_COMMANDS: &[&str] = &[
149 "agent",
150 "fix",
151 "init",
152 "hooks",
153 "migrate",
154 "setup-hooks",
155 "watch",
156];
157
158#[must_use]
161pub fn leading_command_token(row: &TaskRow) -> &'static str {
162 let after_fallow = row.command.strip_prefix("fallow ").unwrap_or(row.command);
163 after_fallow.split_whitespace().next().unwrap_or("")
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn matrix_is_non_empty() {
172 assert!(!TASK_MATRIX.is_empty());
173 }
174
175 #[test]
177 fn matrix_excludes_mutating_commands() {
178 for row in TASK_MATRIX {
179 let first_token = leading_command_token(row);
180 assert!(
181 !MUTATING_COMMANDS.contains(&first_token),
182 "task matrix row '{}' names mutating command '{first_token}'",
183 row.task
184 );
185 }
186 }
187
188 #[test]
189 fn to_json_omits_probe_and_keeps_note_key() {
190 let row = TaskRow {
191 task: "t",
192 command: "fallow flags",
193 note: None,
194 probe: &["flags"],
195 };
196 let value = row.to_json();
197 assert_eq!(value["task"], "t");
198 assert_eq!(value["command"], "fallow flags");
199 assert!(value["note"].is_null());
200 assert!(value.get("probe").is_none());
201 }
202
203 #[test]
204 fn tasks_are_unique() {
205 let mut tasks: Vec<&str> = TASK_MATRIX.iter().map(|row| row.task).collect();
206 let total = tasks.len();
207 tasks.sort_unstable();
208 tasks.dedup();
209 assert_eq!(tasks.len(), total, "duplicate task in TASK_MATRIX");
210 }
211
212 #[test]
213 fn leading_token_skips_the_fallow_prefix() {
214 let row = TaskRow {
215 task: "t",
216 command: "fallow audit --base main",
217 note: None,
218 probe: &[],
219 };
220 assert_eq!(leading_command_token(&row), "audit");
221 let fragment = TaskRow {
222 task: "t",
223 command: "--workspace <glob>",
224 note: None,
225 probe: &[],
226 };
227 assert_eq!(leading_command_token(&fragment), "--workspace");
228 }
229}