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: "migrate a dependency",
78 command: "fallow trace --dependency <name> --sites",
79 note: Some(
80 "Counts each imported name, follows one hop through project wrappers, and counts each use that it cannot resolve.",
81 ),
82 probe: &["trace", "--dependency", "lodash", "--sites"],
83 },
84 TaskRow {
85 task: "commit or open a PR",
86 command: "fallow audit --base <ref>",
87 note: None,
88 probe: &["audit", "--base", "main"],
89 },
90 TaskRow {
91 task: "read a diff before approving it",
92 command: "fallow review --base <ref> --brief",
93 note: Some(
94 "orientation, never gates: deterministic and always exit 0, unlike the audit row",
95 ),
96 probe: &["review", "--base", "main", "--brief"],
97 },
98 TaskRow {
99 task: "prioritize refactoring",
100 command: "fallow health --hotspots --targets",
101 note: None,
102 probe: &["health", "--hotspots", "--targets"],
103 },
104 TaskRow {
105 task: "ask who owns code",
106 command: "fallow health --ownership",
107 note: None,
108 probe: &["health", "--ownership"],
109 },
110 TaskRow {
111 task: "check untested-but-reachable code",
112 command: "fallow health --coverage-gaps",
113 note: None,
114 probe: &["health", "--coverage-gaps"],
115 },
116 TaskRow {
117 task: "consolidate duplication",
118 command: "fallow dupes --trace dup:<fingerprint>",
119 note: None,
120 probe: &["dupes", "--trace", "dup:abc123"],
121 },
122 TaskRow {
123 task: "find feature flags",
124 command: "fallow flags",
125 note: None,
126 probe: &["flags"],
127 },
128 TaskRow {
129 task: "check which architecture rules apply to a file before changing it",
130 command: "fallow guard <files>",
131 note: None,
132 probe: &["guard", "src/index.ts"],
133 },
134 TaskRow {
135 task: "check import cycles, boundaries and policy rules after changing code",
136 command: "fallow architecture",
137 note: None,
138 probe: &["architecture"],
139 },
140 TaskRow {
141 task: "surface security candidates",
142 command: "fallow security",
143 note: None,
144 probe: &["security"],
145 },
146 TaskRow {
147 task: "understand a finding",
148 command: "fallow explain <issue-type>",
149 note: None,
150 probe: &["explain", "unused-export"],
151 },
152 TaskRow {
153 task: "scope a monorepo",
154 command: "--workspace <glob> / --changed-workspaces <ref>",
155 note: Some("global flags, prefix any command"),
156 probe: &[],
159 },
160];
161
162impl TaskRow {
163 #[must_use]
167 pub fn to_json(&self) -> serde_json::Value {
168 serde_json::json!({
169 "task": self.task,
170 "command": self.command,
171 "note": self.note,
172 })
173 }
174}
175
176pub const MUTATING_COMMANDS: &[&str] = &[
179 "agent",
180 "fix",
181 "init",
182 "hooks",
183 "migrate",
184 "setup-hooks",
185 "watch",
186];
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn leading_command_token(row: &TaskRow) -> &'static str {
195 let after_fallow = row.command.strip_prefix("fallow ").unwrap_or(row.command);
196 after_fallow.split_whitespace().next().unwrap_or("")
197 }
198
199 #[test]
200 fn matrix_is_non_empty() {
201 assert!(!TASK_MATRIX.is_empty());
202 }
203
204 #[test]
206 fn matrix_excludes_mutating_commands() {
207 for row in TASK_MATRIX {
208 let first_token = leading_command_token(row);
209 assert!(
210 !MUTATING_COMMANDS.contains(&first_token),
211 "task matrix row '{}' names mutating command '{first_token}'",
212 row.task
213 );
214 }
215 }
216
217 #[test]
218 fn to_json_omits_probe_and_keeps_note_key() {
219 let row = TaskRow {
220 task: "t",
221 command: "fallow flags",
222 note: None,
223 probe: &["flags"],
224 };
225 let value = row.to_json();
226 assert_eq!(value["task"], "t");
227 assert_eq!(value["command"], "fallow flags");
228 assert!(value["note"].is_null());
229 assert!(value.get("probe").is_none());
230 }
231
232 #[test]
233 fn tasks_are_unique() {
234 let mut tasks: Vec<&str> = TASK_MATRIX.iter().map(|row| row.task).collect();
235 let total = tasks.len();
236 tasks.sort_unstable();
237 tasks.dedup();
238 assert_eq!(tasks.len(), total, "duplicate task in TASK_MATRIX");
239 }
240
241 #[test]
242 fn leading_token_skips_the_fallow_prefix() {
243 let row = TaskRow {
244 task: "t",
245 command: "fallow audit --base main",
246 note: None,
247 probe: &[],
248 };
249 assert_eq!(leading_command_token(&row), "audit");
250 let fragment = TaskRow {
251 task: "t",
252 command: "--workspace <glob>",
253 note: None,
254 probe: &[],
255 };
256 assert_eq!(leading_command_token(&fragment), "--workspace");
257 }
258}