Skip to main content

fallow_types/
task_matrix.rs

1//! Single source of truth for the agent-discoverability task-to-command matrix
2//! (R2/R3). One const slice drives every render surface: the `fallow schema`
3//! manifest (`task_matrix`), the `init --agents` AGENTS.md template, the
4//! `hooks install --target agent` managed block, the root `--help` cheat
5//! sheet, and the `fallow://task-matrix` MCP resource. The
6//! `scripts/generate-agent-docs.mjs` generator renders the same table into
7//! SKILL.md from the schema-serialized form, so the Markdown surfaces stay
8//! consistent without duplicating the rows.
9//!
10//! This module carries data only. The Markdown renderer and the clap probe
11//! drift test live in `crates/cli`, which owns the command tree; the MCP
12//! server projects the rows without `probe`.
13//!
14//! Read-only-evidence principle (R1): the matrix carries NO mutating commands
15//! (`fix`, `init`, `hooks`, `migrate`, `setup-hooks`, `watch`). Unit tests in
16//! this crate and in `crates/cli` pin that contract, mirroring the
17//! `next_steps[]` builder in the CLI report layer.
18
19/// One task-to-command row for the agent-discoverability cheat sheet (R2/R3).
20///
21/// `command` MAY contain `<placeholder>` or glob tokens because it renders
22/// into docs and help text, unlike the runnable-only `next_steps[]` contract.
23/// `probe` is the runnable clap token sequence (placeholders and values
24/// replaced with concrete dummies) that the CLI schema drift test parses
25/// through `Cli::try_parse_from`, so a row can never name a flag or subcommand
26/// that does not exist. A row whose command is a bare flag fragment (no
27/// leading subcommand) carries an empty `probe`; the drift test skips it and a
28/// dedicated test asserts the flags exist on the live global arg set instead.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TaskRow {
31    /// The agent intent, phrased as "when the agent is about to ...".
32    pub task: &'static str,
33    /// The command to run, render-ready (may contain `<placeholder>` tokens).
34    pub command: &'static str,
35    /// Optional clarifying note appended in parentheses in the rendered table.
36    pub note: Option<&'static str>,
37    /// Runnable clap token sequence the CLI drift test parses, or empty for a
38    /// flag-fragment row that is covered by the global-flag existence test.
39    pub probe: &'static [&'static str],
40}
41
42/// The canonical task-to-command matrix. Verified against the live clap
43/// command tree; the CLI schema drift test re-checks every non-empty `probe`.
44pub 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        // Flag-fragment row: no leading subcommand. Covered by
127        // `task_matrix_workspace_flags_are_global` in the CLI schema tests.
128        probe: &[],
129    },
130];
131
132impl TaskRow {
133    /// The `fallow schema` `task_matrix` row: `task`, `command`, and `note`
134    /// (`null` when absent, honoring the manifest's no-absent-key convention).
135    /// `probe` is a test-only concern and never serializes.
136    #[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
146/// Mutating command tokens the matrix must never reference (R1 read-only
147/// principle). Shared with the CLI schema exclusion test.
148pub const MUTATING_COMMANDS: &[&str] = &[
149    "agent",
150    "fix",
151    "init",
152    "hooks",
153    "migrate",
154    "setup-hooks",
155    "watch",
156];
157
158/// The first command token after the `fallow` prefix, or the empty string for
159/// a bare flag-fragment row.
160#[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    /// Read-only-evidence contract (R1): no row may name a mutating command.
176    #[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}