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: "find how one module reaches another",
64 command: "fallow trace --path <from> <to>",
65 note: Some(
66 "Reports `reachable: false` instead of failing when no import path exists; type-only hops are reported, not skipped.",
67 ),
68 probe: &["trace", "--path", "src/app.ts", "src/db.ts"],
69 },
70 TaskRow {
71 task: "delete an \"unused\" dependency",
72 command: "fallow dead-code --trace-dependency <name>",
73 note: None,
74 probe: &["dead-code", "--trace-dependency", "lodash"],
75 },
76 TaskRow {
77 task: "commit or open a PR",
78 command: "fallow audit --base <ref>",
79 note: None,
80 probe: &["audit", "--base", "main"],
81 },
82 TaskRow {
83 task: "read a diff before approving it",
84 command: "fallow review --base <ref> --brief",
85 note: Some(
86 "orientation, never gates: deterministic and always exit 0, unlike the audit row",
87 ),
88 probe: &["review", "--base", "main", "--brief"],
89 },
90 TaskRow {
91 task: "prioritize refactoring",
92 command: "fallow health --hotspots --targets",
93 note: None,
94 probe: &["health", "--hotspots", "--targets"],
95 },
96 TaskRow {
97 task: "ask who owns code",
98 command: "fallow health --ownership",
99 note: None,
100 probe: &["health", "--ownership"],
101 },
102 TaskRow {
103 task: "check untested-but-reachable code",
104 command: "fallow health --coverage-gaps",
105 note: None,
106 probe: &["health", "--coverage-gaps"],
107 },
108 TaskRow {
109 task: "consolidate duplication",
110 command: "fallow dupes --trace dup:<fingerprint>",
111 note: None,
112 probe: &["dupes", "--trace", "dup:abc123"],
113 },
114 TaskRow {
115 task: "find feature flags",
116 command: "fallow flags",
117 note: None,
118 probe: &["flags"],
119 },
120 TaskRow {
121 task: "check which architecture rules apply to a file before changing it",
122 command: "fallow guard <files>",
123 note: None,
124 probe: &["guard", "src/index.ts"],
125 },
126 TaskRow {
127 task: "surface security candidates",
128 command: "fallow security",
129 note: None,
130 probe: &["security"],
131 },
132 TaskRow {
133 task: "understand a finding",
134 command: "fallow explain <issue-type>",
135 note: None,
136 probe: &["explain", "unused-export"],
137 },
138 TaskRow {
139 task: "scope a monorepo",
140 command: "--workspace <glob> / --changed-workspaces <ref>",
141 note: Some("global flags, prefix any command"),
142 probe: &[],
145 },
146];
147
148impl TaskRow {
149 #[must_use]
153 pub fn to_json(&self) -> serde_json::Value {
154 serde_json::json!({
155 "task": self.task,
156 "command": self.command,
157 "note": self.note,
158 })
159 }
160}
161
162pub const MUTATING_COMMANDS: &[&str] = &[
165 "agent",
166 "fix",
167 "init",
168 "hooks",
169 "migrate",
170 "setup-hooks",
171 "watch",
172];
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn leading_command_token(row: &TaskRow) -> &'static str {
181 let after_fallow = row.command.strip_prefix("fallow ").unwrap_or(row.command);
182 after_fallow.split_whitespace().next().unwrap_or("")
183 }
184
185 #[test]
186 fn matrix_is_non_empty() {
187 assert!(!TASK_MATRIX.is_empty());
188 }
189
190 #[test]
192 fn matrix_excludes_mutating_commands() {
193 for row in TASK_MATRIX {
194 let first_token = leading_command_token(row);
195 assert!(
196 !MUTATING_COMMANDS.contains(&first_token),
197 "task matrix row '{}' names mutating command '{first_token}'",
198 row.task
199 );
200 }
201 }
202
203 #[test]
204 fn to_json_omits_probe_and_keeps_note_key() {
205 let row = TaskRow {
206 task: "t",
207 command: "fallow flags",
208 note: None,
209 probe: &["flags"],
210 };
211 let value = row.to_json();
212 assert_eq!(value["task"], "t");
213 assert_eq!(value["command"], "fallow flags");
214 assert!(value["note"].is_null());
215 assert!(value.get("probe").is_none());
216 }
217
218 #[test]
219 fn tasks_are_unique() {
220 let mut tasks: Vec<&str> = TASK_MATRIX.iter().map(|row| row.task).collect();
221 let total = tasks.len();
222 tasks.sort_unstable();
223 tasks.dedup();
224 assert_eq!(tasks.len(), total, "duplicate task in TASK_MATRIX");
225 }
226
227 #[test]
228 fn leading_token_skips_the_fallow_prefix() {
229 let row = TaskRow {
230 task: "t",
231 command: "fallow audit --base main",
232 note: None,
233 probe: &[],
234 };
235 assert_eq!(leading_command_token(&row), "audit");
236 let fragment = TaskRow {
237 task: "t",
238 command: "--workspace <glob>",
239 note: None,
240 probe: &[],
241 };
242 assert_eq!(leading_command_token(&fragment), "--workspace");
243 }
244}