Skip to main content

fallow_cli/
cli_args.rs

1//! Clap definition of the `fallow` command line.
2
3use std::path::PathBuf;
4
5use clap::{Parser, Subcommand};
6
7use crate::cli_agent::AgentCli;
8use crate::cli_format::Format;
9use crate::cli_hooks::HooksCli;
10use crate::cli_impact::{ImpactCli, ImpactSortCli};
11use crate::dispatch::resolve_negatable_flag;
12use crate::dupes::{DupesMode, DupesOverrides};
13use crate::health::SortBy;
14use crate::runtime_support::{AnalysisKind, GroupBy};
15use crate::{
16    audit_decision_surface, doctor, flags, mutation_cli, security, setup_hooks, similar_code_cli,
17    viz,
18};
19
20const TOP_LEVEL_HELP_TEMPLATE: &str =
21    "{about-with-newline}\n{usage-heading} {usage}{after-help}\n\nOptions:\n{options}";
22
23// Macros instead of consts so `concat!` can assemble the short (`-h`) and
24// long (`--help`) after-help surfaces from the same source fragments.
25macro_rules! top_level_task_cheat_sheet {
26    () => {
27        "\
28When the agent is about to...
29  delete an \"unused\" export or file        fallow dead-code --trace <file>:<export>
30  prove exact TypeScript symbol consumers  fallow dead-code --type-aware --symbol-impact <file>:<export-or-class.method>
31  find how one module reaches another      fallow trace --path <from> <to>
32  delete an \"unused\" dependency            fallow dead-code --trace-dependency <name>
33  migrate a dependency                     fallow trace --dependency <name> --sites
34  commit or open a PR                      fallow audit --base <ref>
35  read a diff before approving it          fallow review --base <ref> --brief
36  prioritize refactoring                   fallow health --hotspots --targets
37  ask who owns code                        fallow health --ownership
38  check untested-but-reachable code        fallow health --coverage-gaps
39  consolidate duplication                  fallow dupes --trace dup:<fingerprint>
40  find feature flags                       fallow flags
41  check architecture rules before editing  fallow guard <files>
42  check cycles and boundaries after edits  fallow architecture
43  surface security candidates              fallow security
44  inspect a target before editing          fallow inspect --file <path>
45  understand a finding                     fallow explain <issue-type>
46  scope a monorepo                         --workspace <glob> / --changed-workspaces <ref>"
47    };
48}
49
50macro_rules! top_level_core_command_groups {
51    () => {
52        "\
53Analysis:
54  dead-code      Analyze unused code and dependency hygiene
55  guard          Show which architecture rules apply to files before editing
56  architecture   Check import cycles, boundaries and policy rules after editing
57  dupes          Find copy-paste and structural code duplication
58  health         Analyze complexity, maintainability, hotspots, and coverage gaps
59  flags          Detect feature flag usage patterns
60  security       Surface local security candidates for agent verification (opt-in)
61  similar-code   Find semantic implementation overlap for verification (opt-in, local)
62  mutation       Measure how well the tests detect code changes (opt-in, runs tests)
63  audit          Review changed files for dead code, complexity, duplication, and styling
64
65Workflow:
66  watch          Re-run analysis as files change
67  fix            Auto-fix safe unused-code findings"
68    };
69}
70
71macro_rules! top_level_extended_command_groups {
72    () => {
73        "\
74Project inspection:
75  list              List discovered files, entry points, plugins, boundaries, workspaces, and entry weight
76  inspect           Inspect one file or exported symbol as a bundled evidence query
77  trace             Trace a symbol's call chain (best-effort, syntactic)
78  trace-error       Resolve a runtime stack trace's frames to project definitions
79  decision-surface  Surface the structural decisions a change embeds (advisory)
80  workspaces        Show monorepo workspace discovery diagnostics
81  explain           Explain one issue type without running analysis
82  suppressions      List active fallow-ignore suppression markers
83  impact            Show what fallow has done for you (opt-in, local-only)
84  viz               Generate an interactive HTML map of the codebase
85
86Setup and configuration:
87  doctor            Diagnose project readiness without changing anything
88  init              Create a fallow config, optionally with a Git hook
89  agent             Wire fallow into Claude Code, Codex, or Cursor in one pass
90  audit-cache       Maintain reusable audit base-snapshot caches
91  baselines         Prune committed baselines to the findings that remain
92  recommend         Recommend a project-tailored config for an agent to author
93  migrate           Migrate knip, jscpd, or stylelint config to fallow
94  config            Show the resolved config and loaded config file
95  config-schema     Print the fallow config JSON Schema
96  plugin-schema     Print the external plugin JSON Schema
97  plugin-check      Dry-run external plugins and report what they seed
98  rule-pack         Manage declarative rule packs (policy-as-code)
99  rule-pack-schema  Print the rule pack JSON Schema
100  type-aware        Inspect the optional TypeScript semantic companion
101
102Automation and CI:
103  ci             Build PR/MR feedback envelopes
104  ci-template    Print or vendor CI integration templates
105  report         Re-render saved JSON as GitHub or CodeClimate output
106  hooks          Install or remove fallow-managed Git and agent hooks
107  setup-hooks    Deprecated: use `agent install` or `hooks install --target agent`
108
109Runtime coverage:
110  coverage       Set up or analyze runtime coverage data
111  license        Manage the paid-feature license
112  telemetry      Manage opt-in product telemetry
113
114Reference:
115  schema         Dump the CLI interface as machine-readable JSON
116  help           Print this message or the help of a command"
117    };
118}
119
120const TOP_LEVEL_AFTER_HELP: &str = concat!(
121    top_level_task_cheat_sheet!(),
122    "\n\n",
123    top_level_core_command_groups!(),
124    "\n\nRun fallow --help for the complete command list."
125);
126
127pub const TOP_LEVEL_AFTER_LONG_HELP: &str = concat!(
128    top_level_task_cheat_sheet!(),
129    "\n\n",
130    top_level_core_command_groups!(),
131    "\n\n",
132    top_level_extended_command_groups!(),
133    "\n\n",
134    "When no command is given, fallow runs dead-code + dupes + health together.\n",
135    "Use --only/--skip to select specific analyses."
136);
137
138#[derive(Parser)]
139#[command(
140    name = "fallow",
141    about = "Codebase analyzer for TypeScript/JavaScript: unused code, circular dependencies, code duplication, complexity hotspots, and architecture boundary violations",
142    version,
143    disable_version_flag = true,
144    help_template = TOP_LEVEL_HELP_TEMPLATE,
145    after_help = TOP_LEVEL_AFTER_HELP,
146    after_long_help = TOP_LEVEL_AFTER_LONG_HELP
147)]
148pub struct Cli {
149    #[command(subcommand)]
150    pub command: Option<Command>,
151
152    /// Scope reported findings to this file or directory (default: whole project).
153    /// The full project graph is still built; only reported items are narrowed.
154    #[arg(value_name = "PATH")]
155    pub path: Option<PathBuf>,
156
157    /// Print version.
158    /// Accepts `-v`, `-V`, and `--version`; TS/JS tooling (node, npm, pnpm,
159    /// yarn, bun, tsc) uses `-v`, while `-V` matches knip/oxlint/biome.
160    #[arg(
161        short = 'v',
162        visible_short_alias = 'V',
163        long = "version",
164        action = clap::ArgAction::Version
165    )]
166    pub version: Option<bool>,
167
168    /// Project root directory
169    #[arg(short, long, global = true)]
170    pub root: Option<PathBuf>,
171
172    /// Path to config file (.fallowrc.json, .fallowrc.jsonc, fallow.toml, or .fallow.toml)
173    #[arg(short, long, global = true)]
174    pub config: Option<PathBuf>,
175
176    /// Allow trusted config files to extend HTTPS URLs
177    #[arg(hide_short_help = true, long, global = true)]
178    pub allow_remote_extends: bool,
179
180    /// Output format (alias: --output)
181    #[arg(
182        short,
183        long,
184        visible_alias = "output",
185        global = true,
186        default_value = "human"
187    )]
188    pub format: Format,
189
190    /// Indent JSON output for manual inspection. Requires the final output format to be JSON.
191    #[arg(hide_short_help = true, long, global = true)]
192    pub pretty: bool,
193
194    /// Suppress progress output
195    #[arg(short, long, global = true)]
196    pub quiet: bool,
197
198    /// Disable incremental caching
199    #[arg(hide_short_help = true, long, global = true)]
200    pub no_cache: bool,
201
202    /// Number of parser threads
203    #[arg(hide_short_help = true, long, global = true)]
204    pub threads: Option<usize>,
205
206    /// Only report issues in files changed since this git ref (e.g., main, HEAD~5)
207    ///
208    /// For this run, the ref replaces the per-package refs of
209    /// `workspaces.changedSince` in the config.
210    #[arg(long, visible_alias = "base", global = true)]
211    pub changed_since: Option<String>,
212
213    /// Ignore the per-package refs of `workspaces.changedSince` for this run
214    ///
215    /// Every workspace package is then analyzed in full scope, for example to
216    /// save or gate a whole-project baseline. A global `--changed-since`,
217    /// `--workspace` or `--diff-file` scope still applies.
218    #[arg(hide_short_help = true, long, global = true)]
219    pub no_package_baselines: bool,
220
221    /// Unified diff for line-level scoping.
222    /// Use `-` to read from stdin. Project-level findings still bypass this
223    /// filter. When both this and `--changed-since` are set, the diff filter
224    /// wins for finding scope while `--changed-since` still drives file discovery.
225    #[arg(
226        hide_short_help = true,
227        long = "diff-file",
228        value_name = "PATH",
229        global = true
230    )]
231    pub diff_file: Option<PathBuf>,
232
233    /// Read the unified diff from stdin.
234    /// Equivalent to `--diff-file -`.
235    #[arg(hide_short_help = true, long = "diff-stdin", global = true)]
236    pub diff_stdin: bool,
237
238    /// Import change history from a `fallow-churn/v1` JSON file instead of `git
239    /// log`, powering hotspots, ownership, and bus-factor on projects with no
240    /// git repository (Yandex Arc, Mercurial, Perforce). A small wrapper
241    /// translates your VCS log into the contract. Resolved relative to `--root`.
242    /// Affects `health --hotspots` / `--ownership` / `--targets` only; `audit`,
243    /// `impact`, and `--changed-since` still require git.
244    #[arg(
245        hide_short_help = true,
246        long = "churn-file",
247        value_name = "PATH",
248        global = true
249    )]
250    pub churn_file: Option<PathBuf>,
251
252    /// Skip source files larger than this many megabytes (default 5) instead of
253    /// parsing them, guarding against the out-of-memory blowup a single
254    /// multi-MB generated/vendored/bundled file causes on large repos. Use `0`
255    /// for no limit. Declaration files (`.d.ts`) are always analyzed. Skipped
256    /// files are reported and excluded from every analysis. Also settable via
257    /// `FALLOW_MAX_FILE_SIZE`.
258    #[arg(
259        hide_short_help = true,
260        long = "max-file-size",
261        value_name = "MB",
262        global = true
263    )]
264    pub max_file_size: Option<u32>,
265
266    /// Compare against a previously saved baseline file. Used by bare
267    /// `fallow`, `dead-code`, `architecture`, `dupes` and `health`; other
268    /// subcommands reject it. On bare `fallow` it holds the dead-code baseline
269    /// only: pass health and duplication baselines with `--health-baseline`
270    /// and `--dupes-baseline`
271    #[arg(hide_short_help = true, long, global = true)]
272    pub baseline: Option<PathBuf>,
273
274    /// How `--baseline` matches health findings: per file and category
275    /// (`count`, the default) or per function identity (`identity`, strict,
276    /// and only against a baseline that was saved with `--baseline-mode
277    /// identity`; such a baseline still reads in count mode).
278    ///
279    /// A finding's identity is its file path plus its function name, so
280    /// renaming or moving a function that is still in the baseline reports it
281    /// as new. Re-save the baseline after that kind of refactor. Functions
282    /// that share a name in one file, and unnamed functions, share one
283    /// identity and can mask each other.
284    ///
285    /// Defaults to `count` when omitted. Saving without the flag refuses to
286    /// overwrite a baseline that carries identities; pass `--baseline-mode
287    /// count` explicitly to downgrade such a baseline on purpose.
288    #[arg(
289        hide_short_help = true,
290        long = "baseline-mode",
291        value_enum,
292        global = true
293    )]
294    pub baseline_mode: Option<BaselineModeArg>,
295
296    /// Correlate this run with a previous telemetry analysis run.
297    ///
298    /// Used only for opt-in telemetry follow-up measurement. The value is not
299    /// interpreted as a path, repository, package, or user identifier. Hidden
300    /// from `--help`; agents receive the correlation token from JSON output.
301    #[arg(long, global = true, value_name = "RUN_ID", hide = true)]
302    pub parent_run: Option<String>,
303
304    /// Save the current results as a baseline file. Used by bare `fallow`,
305    /// `dead-code`, `architecture`, `dupes` and `health`; other subcommands
306    /// reject it. On bare `fallow` it saves the dead-code baseline only: save
307    /// the others with `fallow health --save-baseline` and `fallow dupes
308    /// --save-baseline`. The path must resolve inside the project root, its Git
309    /// work tree, the CI workspace or a temp directory
310    #[arg(hide_short_help = true, long, global = true)]
311    pub save_baseline: Option<PathBuf>,
312
313    /// Production mode: exclude test/story/dev files, only start/build scripts,
314    /// report type-only dependencies
315    #[arg(long, global = true)]
316    pub production: bool,
317
318    /// Force production mode OFF for every analysis, overriding a project
319    /// config's `production: true` (and `FALLOW_PRODUCTION`). Conflicts with
320    /// `--production`.
321    #[arg(
322        hide_short_help = true,
323        long = "no-production",
324        global = true,
325        conflicts_with = "production"
326    )]
327    pub no_production: bool,
328
329    /// Run dead-code analysis in production mode when using bare combined mode.
330    #[arg(hide_short_help = true, long = "production-dead-code")]
331    pub production_dead_code: bool,
332
333    /// Run health analysis in production mode when using bare combined mode.
334    #[arg(hide_short_help = true, long = "production-health")]
335    pub production_health: bool,
336
337    /// Run duplication analysis in production mode when using bare combined mode.
338    #[arg(hide_short_help = true, long = "production-dupes")]
339    pub production_dupes: bool,
340
341    /// Scope output to selected workspaces.
342    /// Accepts exact names, glob patterns, and `!`-prefixed negations.
343    /// Values can be comma-separated or repeated.
344    #[arg(short, long, global = true, value_delimiter = ',')]
345    pub workspace: Option<Vec<String>>,
346
347    /// Scope output to workspaces touched since the given git ref.
348    /// Git is required. Mutually exclusive with `--workspace`.
349    #[arg(long, global = true, value_name = "REF")]
350    pub changed_workspaces: Option<String>,
351
352    /// Group output by owner or by directory.
353    #[arg(hide_short_help = true, long, global = true)]
354    pub group_by: Option<GroupBy>,
355
356    /// Keep only the matching groups of a `--group-by` health run.
357    /// Accepts exact group keys, glob patterns, and `!`-prefixed negations.
358    /// Values can be comma-separated or repeated. Project-level sections are
359    /// not filtered. Supported by `fallow health` only.
360    #[arg(
361        hide_short_help = true,
362        long,
363        global = true,
364        value_delimiter = ',',
365        value_name = "KEY",
366        requires = "group_by"
367    )]
368    pub group: Option<Vec<String>>,
369
370    /// Show pipeline performance timing breakdown
371    #[arg(hide_short_help = true, long, global = true)]
372    pub performance: bool,
373
374    /// Include metric definitions and rule descriptions in output.
375    #[arg(hide_short_help = true, long, global = true)]
376    pub explain: bool,
377
378    /// Show per-pattern counts for skipped files: default duplicate ignores on dupes
379    /// and audit, built-in discovery ignores on check, dead-code, audit and the default run.
380    #[arg(hide_short_help = true, long, global = true)]
381    pub explain_skipped: bool,
382
383    /// Show only category counts without individual items
384    #[arg(hide_short_help = true, long, global = true)]
385    pub summary: bool,
386
387    /// CI mode: equivalent to --format sarif --fail-on-issues --quiet
388    #[arg(long, global = true)]
389    pub ci: bool,
390
391    /// Exit with code 1 if issues are found
392    #[arg(hide_short_help = true, long, global = true)]
393    pub fail_on_issues: bool,
394
395    /// Write SARIF output to a file (in addition to the primary --format output).
396    /// Used by bare `fallow`, `dead-code`, `architecture` and `security`. The
397    /// path must resolve inside the project root, its Git work tree, the CI
398    /// workspace or a temp directory
399    #[arg(hide_short_help = true, long, global = true, value_name = "PATH")]
400    pub sarif_file: Option<PathBuf>,
401
402    /// Write the report to a file instead of stdout, for any --format (no ANSI
403    /// codes). Useful on large projects where the terminal scrollback truncates
404    /// the top. Progress and the confirmation stay on stderr. The path must
405    /// resolve inside the project root, its Git work tree, the CI workspace or
406    /// a temp directory.
407    #[arg(short = 'o', long, global = true, value_name = "PATH")]
408    pub output_file: Option<PathBuf>,
409
410    /// Prefix prepended to every path in the CI-facing formats
411    /// (`github-annotations`, `github-summary`, `codeclimate`,
412    /// `pr-comment-github`, `pr-comment-gitlab`, `review-github`,
413    /// `review-gitlab`). CI platforms address files by
414    /// repository-root-relative path, so when the analyzed project lives in a
415    /// subdirectory (e.g. `packages/app/`), paths need that offset. fallow
416    /// detects the offset via the git toplevel automatically; this flag
417    /// overrides the detection. Pass an empty string to disable rebasing and
418    /// emit paths relative to `--root`.
419    #[arg(
420        hide_short_help = true,
421        long = "report-path-prefix",
422        visible_alias = "annotations-path-prefix",
423        global = true,
424        value_name = "PREFIX"
425    )]
426    pub report_path_prefix: Option<String>,
427
428    /// Fail if issue count increased beyond tolerance compared to a regression baseline.
429    ///
430    /// With `list --entry-weight`, fail when the eager source bytes of an
431    /// entry grew beyond the tolerance. Without this flag that comparison is
432    /// report-only.
433    #[arg(hide_short_help = true, long, global = true)]
434    pub fail_on_regression: bool,
435
436    /// Exit with code 1 if a loaded --baseline has entries that match nothing in this run.
437    ///
438    /// Stricter than the default advisory warning: any stale entry fails, not
439    /// just a quarter of the baseline. Applies in every output format. The
440    /// verdict reaches a machine consumer as `gate_outcomes["stale-baseline"]`,
441    /// which is published with or without this flag; the flag decides only
442    /// whether that entry is `enforced`, and the exit code plus one stderr
443    /// line. Inert when no baseline is loaded, on runs narrowed to part of
444    /// the project (including production mode), which cannot judge a
445    /// whole-project baseline, and under `health --report-only`, which never
446    /// fails a run; such a run prints why it stood down instead of passing
447    /// quietly. `fallow audit` always analyzes only changed files, so the gate
448    /// never fires there.
449    #[arg(hide_short_help = true, long, global = true)]
450    pub fail_on_stale_baseline: bool,
451
452    /// Exit with code 1 if a loaded baseline has a key that the same file at the base ref does not have.
453    ///
454    /// Makes a committed baseline shrink-only. A change that adds a finding and
455    /// re-saves the baseline in the same commit passes `--baseline` and
456    /// `--fail-on-stale-baseline`; this gate fails it and lists each new key
457    /// per category on stderr. A renamed file or a moved line gives a new key,
458    /// so it counts as growth. The base ref is `--baseline-base`, else the
459    /// `fallow audit` base: `--changed-since` / `--base`, then
460    /// `FALLOW_AUDIT_BASE`, then the merge-base with the upstream or the remote
461    /// default branch. A baseline that the base ref does not have is a new
462    /// baseline and passes with a note. A base ref that git cannot resolve
463    /// (for example in a shallow clone) exits 2. Applies to `dead-code`,
464    /// `architecture`, `dupes`, `health`, `audit` and the bare run, and reaches a machine
465    /// consumer as `gate_outcomes["baseline-growth"]`.
466    #[arg(hide_short_help = true, long, global = true)]
467    pub fail_on_baseline_growth: bool,
468
469    /// The git ref that --fail-on-baseline-growth compares the baseline with.
470    ///
471    /// Defaults to the `fallow audit` base resolution. Needs
472    /// --fail-on-baseline-growth.
473    #[arg(hide_short_help = true, long, global = true, value_name = "REF")]
474    pub baseline_base: Option<String>,
475
476    /// Exit with code 1 if fallow could not parse a source file cleanly.
477    ///
478    /// Arms the `parse-error` gate: a file with a `source-parse-degraded`
479    /// entry in `workspace_diagnostics[]` fails the run, and the gate entry in
480    /// `gate_outcomes["parse-error"]` names each such file. Off by default,
481    /// because the parser also rejects valid syntax that is newer than the
482    /// parser. Applies to `dead-code`, `architecture`, `health`, `audit` and
483    /// the bare run, in every output format. The `failOnParseError` config key
484    /// arms the same gate. `health --report-only` never fails a run.
485    #[arg(hide_short_help = true, long, global = true)]
486    pub fail_on_parse_error: bool,
487
488    /// Allowed issue count increase before a regression is flagged.
489    ///
490    /// With `list --entry-weight`, the value is the allowed growth of eager
491    /// source bytes per entry: a byte count such as `1024`, or a percentage
492    /// such as `5%`.
493    #[arg(
494        hide_short_help = true,
495        long,
496        global = true,
497        value_name = "TOLERANCE",
498        default_value = "0"
499    )]
500    pub tolerance: String,
501
502    /// Path to the regression baseline file.
503    #[arg(hide_short_help = true, long, global = true, value_name = "PATH")]
504    pub regression_baseline: Option<PathBuf>,
505
506    /// Save the current issue counts as a regression baseline. Omit PATH to
507    /// update regression.baseline in the discovered fallow config, or create
508    /// .fallowrc.json when none exists. Provide PATH to write a standalone file;
509    /// PATH must resolve inside the project root, its Git work tree, the CI workspace or a temp directory.
510    #[expect(
511        clippy::option_option,
512        reason = "clap pattern: None=not passed, Some(None)=flag only (write to config), Some(Some(path))=write to file"
513    )]
514    #[arg(hide_short_help = true, long, global = true, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
515    pub save_regression_baseline: Option<Option<String>>,
516
517    /// Run only specific analyses when no subcommand is given.
518    #[arg(long, value_delimiter = ',')]
519    pub only: Vec<AnalysisKind>,
520
521    /// Skip specific analyses when no subcommand is given.
522    #[arg(long, value_delimiter = ',')]
523    pub skip: Vec<AnalysisKind>,
524
525    /// Override duplication detection mode in combined mode.
526    #[arg(hide_short_help = true, long = "dupes-mode", global = true)]
527    pub dupes_mode: Option<DupesMode>,
528
529    /// Enable function-scoped near-miss clone detection in combined mode.
530    #[arg(hide_short_help = true, long = "dupes-near", global = true)]
531    pub dupes_near: bool,
532
533    /// Override duplication threshold in combined mode.
534    #[arg(hide_short_help = true, long = "dupes-threshold", global = true)]
535    pub dupes_threshold: Option<f64>,
536
537    /// Override the minimum token count for clones in combined mode.
538    #[arg(hide_short_help = true, long = "dupes-min-tokens", global = true)]
539    pub dupes_min_tokens: Option<usize>,
540
541    /// Override the minimum line count for clones in combined mode.
542    #[arg(hide_short_help = true, long = "dupes-min-lines", global = true)]
543    pub dupes_min_lines: Option<usize>,
544
545    /// Override the minimum clone occurrences in combined mode (must be >= 2).
546    #[arg(hide_short_help = true, long = "dupes-min-occurrences", global = true, value_parser = parse_min_occurrences)]
547    pub dupes_min_occurrences: Option<usize>,
548
549    /// Only report cross-directory duplicates in combined mode.
550    #[arg(hide_short_help = true, long = "dupes-skip-local", global = true)]
551    pub dupes_skip_local: bool,
552
553    /// Enable cross-language duplicate detection in combined mode.
554    #[arg(hide_short_help = true, long = "dupes-cross-language", global = true)]
555    pub dupes_cross_language: bool,
556
557    /// Exclude module wiring from duplicate detection in combined mode
558    /// (default). Pass `--dupes-no-ignore-imports` to count it again.
559    #[arg(hide_short_help = true, long = "dupes-ignore-imports", global = true)]
560    pub dupes_ignore_imports: bool,
561
562    /// Count module wiring as clone candidates in combined mode (opt out of the
563    /// default exclusion).
564    #[arg(
565        hide_short_help = true,
566        long = "dupes-no-ignore-imports",
567        global = true,
568        conflicts_with = "dupes_ignore_imports"
569    )]
570    pub dupes_no_ignore_imports: bool,
571
572    /// Omit clone instances whose path is a symlink, or lies under a
573    /// symlinked directory, in combined mode.
574    #[arg(hide_short_help = true, long = "dupes-ignore-symlinks", global = true)]
575    pub dupes_ignore_symlinks: bool,
576
577    /// Report symlinked clone instances in combined mode (opt out of a config
578    /// `duplicates.ignoreSymlinks: true`).
579    #[arg(
580        hide_short_help = true,
581        long = "dupes-no-ignore-symlinks",
582        global = true,
583        conflicts_with = "dupes_ignore_symlinks"
584    )]
585    pub dupes_no_ignore_symlinks: bool,
586
587    /// Compute health score in combined mode.
588    #[arg(hide_short_help = true, long)]
589    pub score: bool,
590
591    /// Compare current health metrics against the most recent saved snapshot.
592    #[arg(hide_short_help = true, long)]
593    pub trend: bool,
594
595    /// Compare current health metrics against this snapshot file in combined
596    /// mode. Implies --trend and --score.
597    #[arg(hide_short_help = true, long, value_name = "PATH")]
598    pub trend_from: Option<PathBuf>,
599
600    /// Save a vital signs snapshot for trend tracking in combined mode.
601    /// Provide a path or omit for the default `.fallow/snapshots/` location.
602    /// A given path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory.
603    #[expect(
604        clippy::option_option,
605        reason = "clap pattern: None=not passed, Some(None)=default path, Some(Some(path))=custom path"
606    )]
607    #[arg(hide_short_help = true, long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
608    pub save_snapshot: Option<Option<String>>,
609
610    /// Path to Istanbul or raw V8 coverage data for exact CRAP scores in combined mode.
611    /// Also settable via `FALLOW_COVERAGE` or `health.coverage`.
612    #[arg(hide_short_help = true, long, value_name = "PATH")]
613    pub coverage: Option<PathBuf>,
614
615    /// Absolute prefix to strip from Istanbul file paths in combined mode.
616    /// Also settable via `FALLOW_COVERAGE_ROOT` or `health.coverageRoot`.
617    #[arg(hide_short_help = true, long = "coverage-root", value_name = "PATH")]
618    pub coverage_root: Option<PathBuf>,
619
620    /// Compare duplication clone groups against a saved baseline in combined
621    /// mode (produced by `fallow dupes --save-baseline`).
622    #[arg(hide_short_help = true, long = "dupes-baseline", value_name = "PATH")]
623    pub dupes_baseline: Option<PathBuf>,
624
625    /// Compare health findings against a saved baseline in combined mode
626    /// (produced by `fallow health --save-baseline`).
627    #[arg(hide_short_help = true, long = "health-baseline", value_name = "PATH")]
628    pub health_baseline: Option<PathBuf>,
629
630    /// Report unused exports in entry files instead of auto-marking them as used.
631    #[arg(hide_short_help = true, long, global = true)]
632    pub include_entry_exports: bool,
633
634    /// Also report the unused exports, types, class members and enum members
635    /// of unused files. By default the report hides them, because deleting the
636    /// file removes them, and counts them in `cascade_hidden`.
637    #[arg(hide_short_help = true, long, global = true)]
638    pub show_cascade: bool,
639
640    /// Opt in to TypeScript semantic analysis for project-wide symbol evidence.
641    /// This does not emit compiler diagnostics or typed lint findings.
642    #[arg(hide_short_help = true, long, global = true)]
643    pub type_aware: bool,
644
645    /// Disable TypeScript semantic analysis even when `typeAware.enabled` or
646    /// `FALLOW_TYPE_AWARE` opts in, keeping this run fully syntactic.
647    #[arg(
648        hide_short_help = true,
649        long,
650        global = true,
651        conflicts_with = "type_aware"
652    )]
653    pub no_type_aware: bool,
654
655    /// TypeScript project config to use for type-aware analysis (repeatable).
656    #[arg(hide_short_help = true, long, global = true, value_name = "PATH", action = clap::ArgAction::Append)]
657    pub type_aware_project: Vec<PathBuf>,
658
659    /// Decide whether incomplete type-aware analysis is advisory or gating.
660    #[arg(hide_short_help = true, long, global = true, value_enum)]
661    pub type_aware_require: Option<TypeAwareRequireArg>,
662}
663
664impl Cli {
665    /// Tri-state CLI override for type-aware analysis: `Some(true)` for
666    /// `--type-aware`, `Some(false)` for `--no-type-aware`, `None` when
667    /// neither flag was passed (environment and config decide).
668    pub const fn type_aware_override(&self) -> Option<bool> {
669        if self.no_type_aware {
670            Some(false)
671        } else if self.type_aware {
672            Some(true)
673        } else {
674            None
675        }
676    }
677
678    /// The global `--dupes-*` overrides of the `duplicates` config.
679    pub const fn dupes_overrides(&self) -> DupesOverrides {
680        DupesOverrides {
681            mode: self.dupes_mode,
682            near: self.dupes_near,
683            threshold: self.dupes_threshold,
684            min_tokens: self.dupes_min_tokens,
685            min_lines: self.dupes_min_lines,
686            min_occurrences: self.dupes_min_occurrences,
687            skip_local: self.dupes_skip_local,
688            ignore_symlinks: resolve_negatable_flag(
689                self.dupes_ignore_symlinks,
690                self.dupes_no_ignore_symlinks,
691            ),
692            cross_language: self.dupes_cross_language,
693            ignore_imports: resolve_negatable_flag(
694                self.dupes_ignore_imports,
695                self.dupes_no_ignore_imports,
696            ),
697        }
698    }
699}
700
701#[derive(Clone, Copy, Subcommand)]
702pub enum TypeAwareCli {
703    /// Report companion availability and version compatibility without analysis.
704    Status,
705}
706
707#[derive(Subcommand)]
708pub enum Command {
709    /// Analyze project for unused code and dependency hygiene
710    #[command(name = "dead-code", alias = "check")]
711    Check {
712        /// Only report unused files
713        #[arg(long)]
714        unused_files: bool,
715
716        /// Only report unused exports
717        #[arg(long)]
718        unused_exports: bool,
719
720        /// Only report unused dependencies
721        #[arg(long)]
722        unused_deps: bool,
723
724        /// Only report unused type exports
725        #[arg(long)]
726        unused_types: bool,
727
728        /// Opt in to private type leak API hygiene findings and only report that issue type
729        #[arg(long)]
730        private_type_leaks: bool,
731
732        /// Opt in to `@deprecated` exports that are still in use and only report that issue type
733        #[arg(long)]
734        deprecated_exports_in_use: bool,
735
736        /// Only report unused enum members
737        #[arg(long)]
738        unused_enum_members: bool,
739
740        /// Only report unused class members
741        #[arg(long)]
742        unused_class_members: bool,
743
744        /// Only report unused store members
745        #[arg(long)]
746        unused_store_members: bool,
747
748        /// Only report unprovided injects
749        #[arg(long)]
750        unprovided_injects: bool,
751
752        /// Only report unrendered components
753        #[arg(long)]
754        unrendered_components: bool,
755
756        /// Only report unused component props
757        #[arg(long)]
758        unused_component_props: bool,
759
760        /// Review optional props omitted by known reachable callers (enables this rule)
761        #[arg(long)]
762        absent_component_props: bool,
763
764        /// Only report unused component emits
765        #[arg(long)]
766        unused_component_emits: bool,
767
768        /// Only report unused component inputs
769        #[arg(long)]
770        unused_component_inputs: bool,
771
772        /// Only report unused component outputs
773        #[arg(long)]
774        unused_component_outputs: bool,
775
776        /// Only report unused Svelte dispatched events
777        #[arg(long)]
778        unused_svelte_events: bool,
779
780        /// Only report Effect layers composed only into unused layers
781        #[arg(long)]
782        unused_effect_layers: bool,
783
784        /// Only report Effect tagged errors that no code produces
785        #[arg(long)]
786        unused_tagged_errors: bool,
787
788        /// Only report catchTag handlers for a tag that no code produces
789        #[arg(long)]
790        unproduced_catch_tags: bool,
791
792        /// Only report unused server actions
793        #[arg(long)]
794        unused_server_actions: bool,
795
796        /// Only report unused SvelteKit load() data keys
797        #[arg(long)]
798        unused_load_data_keys: bool,
799
800        /// Only report unresolved imports
801        #[arg(long)]
802        unresolved_imports: bool,
803
804        /// Only report unlisted dependencies
805        #[arg(long)]
806        unlisted_deps: bool,
807
808        /// Only report duplicate exports
809        #[arg(long)]
810        duplicate_exports: bool,
811
812        /// Only report circular dependencies.
813        /// Deprecated alias: use `fallow architecture --cycles`.
814        #[arg(long)]
815        circular_deps: bool,
816
817        /// Only report re-export cycles.
818        /// Deprecated alias: use `fallow architecture --cycles`.
819        #[arg(long)]
820        re_export_cycles: bool,
821
822        /// Only report dependency cycles between workspace packages.
823        /// Deprecated alias: use `fallow architecture --cycles`.
824        #[arg(long)]
825        package_cycles: bool,
826
827        /// Only report boundary violations.
828        /// Deprecated alias: use `fallow architecture --boundaries`.
829        #[arg(long)]
830        boundary_violations: bool,
831
832        /// Only report rule-pack policy violations.
833        /// Deprecated alias: use `fallow architecture --policy`.
834        #[arg(long)]
835        policy_violations: bool,
836
837        /// Only report stale suppressions
838        #[arg(long)]
839        stale_suppressions: bool,
840
841        /// Only report unused pnpm catalog entries
842        #[arg(long)]
843        unused_catalog_entries: bool,
844
845        /// Only report empty pnpm catalog groups
846        #[arg(long)]
847        empty_catalog_groups: bool,
848
849        /// Only report unresolved pnpm catalog references
850        #[arg(long)]
851        unresolved_catalog_references: bool,
852
853        /// Only report unused package-manager dependency overrides
854        #[arg(long)]
855        unused_dependency_overrides: bool,
856
857        /// Only report misconfigured package-manager dependency overrides
858        #[arg(long)]
859        misconfigured_dependency_overrides: bool,
860
861        /// Also run duplication analysis and cross-reference with dead code
862        #[arg(long)]
863        include_dupes: bool,
864
865        /// Trace why an export is used/unused (format: `FILE:EXPORT_NAME`)
866        #[arg(long, value_name = "FILE:EXPORT")]
867        trace: Option<String>,
868
869        /// Trace all edges for a file (imports, exports, importers)
870        #[arg(long, value_name = "PATH")]
871        trace_file: Option<String>,
872
873        /// Trace where a dependency is used
874        #[arg(long, value_name = "PACKAGE")]
875        trace_dependency: Option<String>,
876
877        /// Compute the impact closure for a file (the transitive
878        /// affected-but-not-in-diff set + coordination gap). Walks reverse-deps
879        /// and re-export chains; powers the `inspect_target` MCP tool.
880        #[arg(long, value_name = "PATH")]
881        impact_closure: Option<String>,
882
883        /// Compute exact-symbol consumers, affected files, and targeted tests.
884        #[arg(long, value_name = "FILE:EXPORT")]
885        symbol_impact: Option<String>,
886
887        /// Show only the top N items per category. Human output only: the JSON,
888        /// SARIF, and CodeClimate envelopes drive exit codes and CI baselines,
889        /// so they always carry every finding and a consumer slices the arrays
890        /// itself.
891        #[arg(long)]
892        top: Option<usize>,
893
894        /// Only report issues in the specified file(s). Accepts multiple values.
895        /// The full project graph is still built, but only issues in matching files
896        /// are reported. Useful for lint-staged pre-commit hooks.
897        #[arg(long, value_name = "PATH")]
898        file: Vec<std::path::PathBuf>,
899
900        /// Only report the findings with these `finding_id` values. Repeat the flag
901        /// or pass a comma-separated list. Ids stay the same under every filter.
902        /// The JSON output adds `finding_id_query`: a missing id means "resolved"
903        /// only when `conclusive` is true.
904        #[arg(long = "finding-id", value_name = "ID", value_delimiter = ',')]
905        finding_id: Vec<String>,
906
907        /// Scope reported findings to this file or directory (default: whole project).
908        /// The full project graph is still built; only reported items are narrowed.
909        #[arg(value_name = "PATH")]
910        path: Option<std::path::PathBuf>,
911    },
912
913    /// Check import cycles, boundaries and policy rules after editing
914    ///
915    /// Run `fallow guard <files>` before an edit to see the rules that apply to
916    /// those files.
917    Architecture {
918        /// Only report import cycles: circular dependencies, re-export cycles
919        /// and package cycles
920        #[arg(long)]
921        cycles: bool,
922
923        /// Only report boundary violations, boundary coverage and forbidden calls
924        #[arg(long)]
925        boundaries: bool,
926
927        /// Only report rule-pack policy violations
928        #[arg(long)]
929        policy: bool,
930
931        /// Only report issues in the specified file(s). Accepts multiple values.
932        /// The full project graph is still built, but only issues in matching files
933        /// are reported.
934        #[arg(long, value_name = "PATH")]
935        file: Vec<std::path::PathBuf>,
936
937        /// Only report the findings with these `finding_id` values. Repeat the flag
938        /// or pass a comma-separated list.
939        #[arg(long = "finding-id", value_name = "ID", value_delimiter = ',')]
940        finding_id: Vec<String>,
941
942        /// Scope reported findings to this file or directory (default: whole project).
943        /// The full project graph is still built; only reported items are narrowed.
944        #[arg(value_name = "PATH")]
945        path: Option<std::path::PathBuf>,
946    },
947
948    /// Watch for changes and re-run analysis
949    Watch {
950        /// Don't clear the screen between re-analyses
951        #[arg(long)]
952        no_clear: bool,
953    },
954
955    /// Inspect the optional TypeScript semantic companion.
956    TypeAware {
957        #[command(subcommand)]
958        subcommand: TypeAwareCli,
959    },
960
961    /// Diagnose project readiness without analysis or mutation.
962    ///
963    /// Checks the root, config resolution, workspace discovery, external
964    /// plugins, and the optional type-aware companion. Uses only local reads;
965    /// it performs no cache writes, telemetry, network requests, or third-party
966    /// execution. Supports human and JSON output.
967    #[command(override_help = doctor::HELP)]
968    Doctor,
969
970    /// Find semantically similar functions with a pinned local model (opt-in).
971    ///
972    /// Results are unverified candidates. The model score is not a probability,
973    /// gate, vulnerability verdict, or safe-refactor decision. Model setup is
974    /// explicit, and project source remains local and offline during analysis.
975    SimilarCode {
976        #[command(subcommand)]
977        subcommand: Option<similar_code_cli::SimilarCodeSubcommand>,
978        /// Minimum cosine similarity retained as an unverified candidate.
979        #[arg(long, value_name = "0..1")]
980        threshold: Option<f64>,
981        /// Minimum source lines per extracted function.
982        #[arg(long, value_name = "N")]
983        min_lines: Option<usize>,
984        /// Cap displayed candidates after bounded full-corpus comparison.
985        #[arg(long, value_name = "N")]
986        top: Option<usize>,
987        /// Report pairs touching one of these project-relative files.
988        #[arg(long, value_name = "PATH")]
989        file: Vec<PathBuf>,
990
991        /// Scope reported findings to this file or directory (default: whole project).
992        /// The full project graph is still built; only reported items are narrowed.
993        #[arg(value_name = "PATH")]
994        path: Option<PathBuf>,
995    },
996
997    /// Measure how well the tests detect changes in the code (opt-in).
998    ///
999    /// Changes the code in small ways (mutants) and runs the project tests
1000    /// against each mutant. A mutant that no test fails on survives. The
1001    /// default scope is the changed code against the base ref, as `audit`.
1002    /// `--plan` and `inspect` run no code. Fallow does not write to project
1003    /// files. The report lists any path that the tests changed.
1004    #[command(disable_help_subcommand = true)]
1005    Mutation {
1006        #[command(subcommand)]
1007        subcommand: Option<mutation_cli::MutationSubcommand>,
1008        #[command(flatten)]
1009        args: mutation_cli::MutationArgs,
1010    },
1011
1012    /// Inspect one file or exported symbol as a bundled evidence query
1013    Inspect {
1014        /// File to inspect.
1015        #[arg(
1016            long,
1017            value_name = "PATH",
1018            conflicts_with = "symbol",
1019            required_unless_present = "symbol"
1020        )]
1021        file: Option<String>,
1022
1023        /// Exported symbol to inspect, formatted as FILE:EXPORT.
1024        #[arg(long, value_name = "FILE:EXPORT", conflicts_with = "file")]
1025        symbol: Option<String>,
1026
1027        /// OPT-IN: also attach the best-effort symbol-level call chain
1028        /// (`fallow trace`) as the `symbol_chain` evidence section. Only
1029        /// meaningful for a `--symbol` target. Default off (best-effort,
1030        /// syntactic, OFF the ranked path).
1031        #[arg(long)]
1032        symbol_chain: bool,
1033
1034        /// OPT-IN: attach target-level git churn evidence from the health
1035        /// hotspot subsystem. Default off to avoid git-history latency.
1036        #[arg(long)]
1037        churn: bool,
1038    },
1039
1040    /// Trace a symbol's call chain, or the shortest import path between two
1041    /// modules (best-effort, syntactic; OFF the ranked path)
1042    ///
1043    /// Walks callers UP (modules that import the symbol) and callees DOWN
1044    /// (import-symbol edges + intra-module call sites) via the module graph,
1045    /// bounded by `--depth`. Symbol-level chains are labeled best-effort per
1046    /// ADR-001: resolved-vs-unresolved callees are reported honestly, never
1047    /// silently dropped. The result is its OWN surface, NOT folded into the
1048    /// ranked brief and NEVER an input to the focus map / ranking.
1049    ///
1050    /// `--path <FROM> <TO>` answers a different question: the shortest chain of
1051    /// imports by which one module reaches another. Type-only hops are reported
1052    /// rather than skipped, and an unreachable pair reports `reachable: false`
1053    /// instead of an error.
1054    Trace {
1055        /// Target symbol, formatted as FILE:SYMBOL (e.g. src/utils.ts:formatDate).
1056        /// Omitted when `--path` or `--dependency` is used.
1057        #[arg(
1058            value_name = "FILE:SYMBOL",
1059            required_unless_present_any = ["path", "dependency"]
1060        )]
1061        symbol: Option<String>,
1062
1063        /// How the code uses each imported name of a package: file and call
1064        /// counts, one hop through project wrappers, and a count of each use
1065        /// that the trace cannot resolve
1066        ///
1067        /// `--sites`, `--specifier`, `--limit` and `--cursor` page the usage
1068        /// sites. `--callers --depth N` adds the files that import the users.
1069        #[arg(
1070            long,
1071            value_name = "PACKAGE",
1072            conflicts_with_all = ["symbol", "path", "callees"]
1073        )]
1074        dependency: Option<String>,
1075
1076        /// With `--dependency`, report only these imported names
1077        /// (repeatable or comma-separated). Turns on the site page.
1078        #[arg(
1079            long,
1080            value_name = "NAME",
1081            value_delimiter = ',',
1082            requires = "dependency"
1083        )]
1084        specifier: Vec<String>,
1085
1086        /// With `--dependency`, list the usage sites with file, line and column
1087        #[arg(long, requires = "dependency")]
1088        sites: bool,
1089
1090        /// With `--dependency`, the largest number of sites on a page
1091        /// (1 to 500, default 50). Turns on the site page.
1092        #[arg(
1093            long,
1094            value_name = "N",
1095            requires = "dependency",
1096            value_parser = clap::value_parser!(u16).range(1..=500)
1097        )]
1098        limit: Option<u16>,
1099
1100        /// With `--dependency`, the `next_cursor` of the previous page. Turns
1101        /// on the site page.
1102        #[arg(long, value_name = "TOKEN", requires = "dependency")]
1103        cursor: Option<String>,
1104
1105        /// Shortest import path between two modules, as two file paths
1106        /// (e.g. `--path src/app.ts src/db.ts`). Mutually exclusive with the
1107        /// symbol target and the call-chain flags.
1108        #[arg(
1109            long,
1110            num_args = 2,
1111            value_names = ["FROM", "TO"],
1112            conflicts_with_all = ["symbol", "callers", "callees", "depth"]
1113        )]
1114        path: Vec<String>,
1115
1116        /// With `--path`, follow only static value imports, so the route
1117        /// explains why TO loads before FROM runs
1118        ///
1119        /// `import()`, lazy globs, worker loads and `import type` do not
1120        /// qualify.
1121        #[arg(long, requires = "path")]
1122        eager_only: bool,
1123
1124        /// Walk UP to callers (modules that import the symbol). When neither
1125        /// `--callers` nor `--callees` is set, both directions are walked.
1126        #[arg(long)]
1127        callers: bool,
1128
1129        /// Walk DOWN to callees (the symbol's module's import-symbol edges plus
1130        /// unresolved call sites). When neither flag is set, both are walked.
1131        #[arg(long)]
1132        callees: bool,
1133
1134        /// Chain depth bound for both directions (default 2). Symbol-level is
1135        /// best-effort, so a shallow bound keeps the trace legible.
1136        #[arg(long, value_name = "N")]
1137        depth: Option<u32>,
1138    },
1139
1140    /// Resolve a runtime stack trace's frames to the definitions they name
1141    /// (best-effort, syntactic; OFF the ranked path)
1142    ///
1143    /// Reads a V8 / Node or SpiderMonkey / JavaScriptCore stack trace from a
1144    /// file or from stdin, classifies every frame as in-project,
1145    /// node-modules, or out-of-corpus, and asks the module graph which
1146    /// definitions the in-project frames name.
1147    ///
1148    /// The result refuses to overclaim. A frame matching several definitions
1149    /// reports `ambiguous` and lists all of them rather than picking one; a
1150    /// frame matching nothing reports `not_found` rather than disappearing; a
1151    /// frame the graph was never asked about reports `not_attempted`. Every
1152    /// frame read stays in the array in input order and `counts` publishes the
1153    /// per-outcome totals, so a caller can see what went unanswered.
1154    ///
1155    /// No source maps are read. A frame inside generated build output is
1156    /// reported as such, because a stale map rebinds silently to the wrong
1157    /// line.
1158    #[command(name = "trace-error")]
1159    TraceError {
1160        /// Stack trace file, or `-` to read stdin. Defaults to stdin. A
1161        /// relative path is resolved against the project root, matching
1162        /// `--diff-file`.
1163        #[arg(value_name = "FILE")]
1164        trace_file: Option<String>,
1165    },
1166
1167    /// Auto-fix issues: remove unused exports, dependencies, and enum
1168    /// members; add duplicate-export rules to a fallow config file.
1169    ///
1170    /// When no fallow config exists outside a monorepo subpackage, a
1171    /// fresh `.fallowrc.json` is created from the same scaffolding
1172    /// `fallow init` would emit (framework detection, `$schema`,
1173    /// `entry`, etc.) and the duplicate-export rules are layered on
1174    /// top. Inside a monorepo subpackage the create-fallback refuses
1175    /// and points at the workspace root. Pass `--no-create-config` to
1176    /// opt out of the create-fallback (recommended for pre-commit
1177    /// hooks, CI bots, and `fallow watch`).
1178    ///
1179    /// Use `--dry-run` to preview source-file edits and config-file
1180    /// diffs without writing.
1181    Fix {
1182        /// Dry run, show what would be changed without modifying files
1183        #[arg(long)]
1184        dry_run: bool,
1185
1186        /// Skip confirmation prompt (required in non-TTY environments like CI or AI agents)
1187        #[arg(long, alias = "force")]
1188        yes: bool,
1189
1190        /// Refuse to create a new fallow config file when none exists.
1191        /// Use this from pre-commit hooks, CI bots, and `fallow watch`
1192        /// where silently materialising a new top-level config file would
1193        /// surprise the user. The duplicate-export config-add path is
1194        /// skipped with an explanatory message; source-file edits proceed
1195        /// normally.
1196        #[arg(long)]
1197        no_create_config: bool,
1198
1199        /// Scope reported findings to this file or directory (default: whole project).
1200        /// The full project graph is still built; only reported items are narrowed.
1201        /// Only fixes touching scoped files are planned and applied.
1202        #[arg(value_name = "PATH")]
1203        path: Option<PathBuf>,
1204    },
1205
1206    /// Initialize a .fallowrc.json configuration file, AGENTS.md guide, or git
1207    /// pre-commit hook. Use `.fallowrc.jsonc` for editor-native JSON-with-comments
1208    /// support; both extensions are auto-discovered.
1209    ///
1210    /// `--hooks` scaffolds a shell-level Git pre-commit hook under
1211    /// `.git/hooks/` that runs fallow on changed files. The clearer hook
1212    /// namespace is `fallow hooks install --target git`; `init --hooks`
1213    /// remains as a convenience during project initialization.
1214    Init {
1215        /// Generate TOML instead of JSONC
1216        #[arg(long)]
1217        toml: bool,
1218
1219        /// Scaffold a starter AGENTS.md guidance file for coding agents
1220        #[arg(long, conflicts_with_all = ["toml", "hooks", "branch"])]
1221        agents: bool,
1222
1223        /// Scaffold a shell-level pre-commit git hook in `.git/hooks/` that
1224        /// runs fallow on changed files. Alias for
1225        /// `fallow hooks install --target git`.
1226        #[arg(long)]
1227        hooks: bool,
1228
1229        /// Fallback base branch/ref for the pre-commit hook when no upstream is set
1230        #[arg(long, requires = "hooks")]
1231        branch: Option<String>,
1232
1233        /// Record that this project deliberately stays unconfigured: persists a
1234        /// decline so the first-contact setup hint and the `setup` next-step
1235        /// stop appearing here. Writes no config file; idempotent
1236        #[arg(long, conflicts_with_all = ["toml", "agents", "hooks", "branch"])]
1237        decline: bool,
1238    },
1239
1240    /// Install or remove fallow-managed Git and agent hooks.
1241    ///
1242    /// Use `fallow hooks install --target git` for a shell-level Git
1243    /// pre-commit hook. Use `fallow hooks install --target agent` for a
1244    /// Claude Code / Codex gate that blocks agent `git commit` / `git push`
1245    /// commands until `fallow audit` passes.
1246    Hooks {
1247        #[command(subcommand)]
1248        subcommand: HooksCli,
1249    },
1250
1251    /// Wire fallow into the coding-agent harnesses used by this project in
1252    /// one pass (AGENTS.md task map, skill, MCP server, commit/push gate), or
1253    /// show and remove what was installed. `fallow init --agents` and
1254    /// `fallow hooks install --target agent` remain the single-piece
1255    /// commands underneath.
1256    Agent {
1257        #[command(subcommand)]
1258        subcommand: AgentCli,
1259    },
1260
1261    /// CI helpers for PR/MR feedback envelopes.
1262    Ci {
1263        #[command(subcommand)]
1264        subcommand: CiCli,
1265    },
1266
1267    /// Print the JSON Schema for fallow configuration files
1268    ConfigSchema,
1269
1270    /// Print the JSON Schema for external plugin files
1271    PluginSchema,
1272
1273    /// Dry-run external plugins: report what each activated and seeded
1274    PluginCheck,
1275
1276    /// Print the JSON Schema for rule pack files
1277    RulePackSchema,
1278
1279    /// Manage declarative rule packs (policy-as-code)
1280    RulePack {
1281        #[command(subcommand)]
1282        subcommand: RulePackCli,
1283    },
1284
1285    /// Show which architecture rules apply to files before changing them.
1286    Guard {
1287        /// Files to report on (root-relative or absolute; may not exist yet)
1288        #[arg(required = true, num_args = 1..)]
1289        files: Vec<String>,
1290    },
1291
1292    /// Show the resolved config and which config file was loaded
1293    ///
1294    /// Walks up from the project root looking for `.fallowrc.json`,
1295    /// `.fallowrc.jsonc`, `fallow.toml`, or `.fallow.toml`, resolves `extends`, and prints
1296    /// the final config as JSON. Use `--path` to print only the config
1297    /// file path (useful in shell scripts). The default view always exits 0:
1298    /// it prints the loaded config, or, on a zero-config project, the effective
1299    /// defaults (fully supported). `--path` exits 3 when no config file exists,
1300    /// since there is no path to report.
1301    ///
1302    /// Precedence is first-match-wins per directory, in the order
1303    /// `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`,
1304    /// walking up to the workspace root. `.fallowrc.json` accepts JSONC
1305    /// (comments and trailing commas); `.fallowrc.jsonc` is identical in
1306    /// behavior, the extension only signals to editors that comments are
1307    /// expected. If two config files coexist in one directory, fallow loads the
1308    /// higher-precedence one and warns on stderr naming the file it ignored.
1309    Config {
1310        /// Print only the config file path (one line, no JSON)
1311        #[arg(long)]
1312        path: bool,
1313    },
1314
1315    /// Recommend a project-tailored config for an agent to author.
1316    ///
1317    /// Read-only. Inspects the project (frameworks, workspace layout, tooling)
1318    /// and emits what fallow detected, a safe proposed config, and a list of
1319    /// decisions split into auto (decided from detection), default (a disclosed
1320    /// overridable default), and taste (a genuinely subjective choice surfaced
1321    /// to the user as an open question). Honors `--root` and `--format`.
1322    Recommend,
1323
1324    /// List discovered entry points, files, plugins, boundaries, workspaces, and the
1325    /// startup import weight.
1326    List {
1327        /// Show entry points
1328        #[arg(long)]
1329        entry_points: bool,
1330
1331        /// Show all discovered files
1332        #[arg(long)]
1333        files: bool,
1334
1335        /// Show active plugins
1336        #[arg(long)]
1337        plugins: bool,
1338
1339        /// Show architecture boundary zones, rules, and per-zone file counts
1340        #[arg(long)]
1341        boundaries: bool,
1342
1343        /// Show monorepo workspaces and any workspace-discovery diagnostics
1344        /// (malformed package.json, unreachable glob matches, missing
1345        /// tsconfig references).
1346        #[arg(long)]
1347        workspaces: bool,
1348
1349        /// Show the startup import weight of each runtime entry point, in
1350        /// source bytes (not bundle size)
1351        ///
1352        /// For each runtime entry: the modules and source bytes that load
1353        /// before the entry runs, the modules behind `import()` and behind
1354        /// workers or forks, the packages on the startup path, and the single
1355        /// imports that keep the most bytes eager. Source bytes include types
1356        /// and comments. An import without the `type` keyword counts as eager,
1357        /// even when it brings in only types that TypeScript removes.
1358        #[arg(long)]
1359        entry_weight: bool,
1360
1361        /// Scope reported findings to this file or directory (default: whole project).
1362        /// The full project graph is still built; only reported items are narrowed.
1363        #[arg(value_name = "PATH")]
1364        path: Option<PathBuf>,
1365    },
1366
1367    /// Show monorepo workspaces and any workspace-discovery diagnostics.
1368    ///
1369    /// Equivalent to `fallow list --workspaces`. Use this dedicated form
1370    /// when introspecting only the workspace topology (other `list`
1371    /// sections stay hidden).
1372    Workspaces,
1373
1374    /// Find code duplication / clones across the project
1375    Dupes {
1376        /// Detection mode: strict, mild, weak, or semantic
1377        /// (defaults to the value in `.fallowrc.jsonc`, or `mild` if unset).
1378        #[arg(long)]
1379        mode: Option<DupesMode>,
1380
1381        /// Enable function-scoped near-miss clone detection.
1382        #[arg(long)]
1383        near: bool,
1384
1385        /// Minimum token count for a clone
1386        /// (defaults to the value in `.fallowrc.jsonc`, or `50` if unset).
1387        #[arg(long)]
1388        min_tokens: Option<usize>,
1389
1390        /// Minimum line count for a clone
1391        /// (defaults to the value in `.fallowrc.jsonc`, or `5` if unset).
1392        #[arg(long)]
1393        min_lines: Option<usize>,
1394
1395        /// Minimum number of occurrences before a clone group is reported.
1396        /// Raise to focus on widespread copy-paste worth refactoring and skip
1397        /// pair-only clones.
1398        /// (defaults to the value in `.fallowrc.jsonc`, or `2` if unset).
1399        #[arg(long, value_parser = parse_min_occurrences)]
1400        min_occurrences: Option<usize>,
1401
1402        /// Fail if duplication exceeds this percentage (0 = no limit)
1403        /// (defaults to the value in `.fallowrc.jsonc`, or `0` if unset).
1404        #[arg(long)]
1405        threshold: Option<f64>,
1406
1407        /// Only report cross-directory duplicates
1408        #[arg(long)]
1409        skip_local: bool,
1410
1411        /// Enable cross-language detection (strip TS type annotations for TS↔JS matching)
1412        #[arg(long)]
1413        cross_language: bool,
1414
1415        /// Exclude module wiring from clone detection (default; covers imports,
1416        /// re-exports, and top-level static require bindings). Pass
1417        /// `--no-ignore-imports` to count it again.
1418        #[arg(long)]
1419        ignore_imports: bool,
1420
1421        /// Count module wiring as clone candidates (opt out of the default
1422        /// exclusion).
1423        #[arg(long, conflicts_with = "ignore_imports")]
1424        no_ignore_imports: bool,
1425
1426        /// Omit clone instances whose path is a symlink, or lies under a
1427        /// symlinked directory. A clone group with fewer than two remaining
1428        /// instances is not reported. Without this flag, JSON output marks
1429        /// these instances with `is_symlink: true`.
1430        #[arg(long)]
1431        ignore_symlinks: bool,
1432
1433        /// Report symlinked clone instances (opt out of a config
1434        /// `duplicates.ignoreSymlinks: true`).
1435        #[arg(long, conflicts_with = "ignore_symlinks")]
1436        no_ignore_symlinks: bool,
1437
1438        /// Show only the N highest-ranked clone groups. Ranking combines clone
1439        /// size, occurrence count, and capped directory or line spread.
1440        /// `clone_families[]` is rebuilt from the groups that survive, so it
1441        /// narrows too. `stats` keeps describing the whole corpus;
1442        /// `clone_groups_shown` / `clone_groups_omitted` and
1443        /// `clone_families_shown` / `clone_families_omitted` report the split.
1444        /// Refused with exit code 2 alongside `--group-by`: grouped output
1445        /// reports per-bucket stats over every clone group in the bucket, which
1446        /// a global top-N truncation would silently contradict.
1447        #[arg(long)]
1448        top: Option<usize>,
1449
1450        /// Omit the verbatim source text from each clone instance in
1451        /// `--format json`. The file and line/column range still address the
1452        /// same code, and this is most of the payload on a duplicated codebase.
1453        #[arg(long)]
1454        no_fragments: bool,
1455
1456        /// Trace all clones at a specific location (format: `FILE:LINE`)
1457        #[arg(long, value_name = "FILE:LINE")]
1458        trace: Option<String>,
1459
1460        /// Scope reported findings to this file or directory (default: whole project).
1461        /// The full project graph is still built; only reported items are narrowed.
1462        #[arg(value_name = "PATH")]
1463        path: Option<PathBuf>,
1464    },
1465
1466    /// Analyze function complexity (cyclomatic + cognitive)
1467    ///
1468    /// By default, shows all existing sections: health score, complexity findings,
1469    /// file scores, hotspots, and refactoring targets. When any section flag is
1470    /// specified, only those sections are shown.
1471    Health {
1472        /// Maximum cyclomatic complexity threshold (overrides config)
1473        #[arg(long)]
1474        max_cyclomatic: Option<u16>,
1475
1476        /// Maximum cognitive complexity threshold (overrides config)
1477        #[arg(long)]
1478        max_cognitive: Option<u16>,
1479
1480        /// Maximum CRAP score threshold (overrides config, default 30.0).
1481        /// Functions meeting or exceeding this score are reported alongside
1482        /// complexity findings. Pair with `--coverage` for accurate scoring.
1483        #[arg(long)]
1484        max_crap: Option<f64>,
1485
1486        /// Show only the N most complex functions
1487        #[arg(long)]
1488        top: Option<usize>,
1489
1490        /// Sort by: cyclomatic (default), cognitive, lines, or severity
1491        #[arg(long, default_value = "cyclomatic")]
1492        sort: SortBy,
1493
1494        /// Show only complexity findings (functions exceeding thresholds).
1495        /// By default all sections are shown; use this to select only complexity.
1496        #[arg(long)]
1497        complexity: bool,
1498
1499        /// Include the per-decision-point complexity breakdown (`contributions[]`)
1500        /// on each complexity finding in `--format json` output. Each entry names
1501        /// the construct (if, else-if, loop, boolean operator, ...) and its
1502        /// cyclomatic/cognitive weight, so a consumer can explain WHY a function
1503        /// scored high. Used by the VS Code inline editor breakdown. Off by
1504        /// default to keep CI/default output lean.
1505        #[arg(long)]
1506        complexity_breakdown: bool,
1507
1508        /// Show only per-file health scores (fan-in, fan-out, dead code ratio, maintainability index).
1509        /// Requires full analysis pipeline (graph + dead code detection).
1510        /// Sorted by risk-aware triage concern: lower MI and higher CRAP risk first.
1511        /// --sort and --baseline apply to complexity findings only, not file scores.
1512        #[arg(long)]
1513        file_scores: bool,
1514
1515        /// Show only static test coverage gaps: runtime files and exports with no
1516        /// dependency path from any discovered test root. Requires full analysis pipeline.
1517        #[arg(long)]
1518        coverage_gaps: bool,
1519
1520        /// Show only hotspots: files that are both complex and frequently changing.
1521        /// Combines git churn history with complexity data. Requires a git repository.
1522        #[arg(long)]
1523        hotspots: bool,
1524
1525        /// Attach ownership signals to hotspot entries: bus factor, contributor
1526        /// count, declared CODEOWNERS owner, and ownership drift. Implies
1527        /// `--hotspots`. Requires a git repository.
1528        #[arg(long)]
1529        ownership: bool,
1530
1531        /// Privacy mode for author emails emitted with `--ownership`.
1532        /// Defaults to `handle` (local-part only). Use `raw` for OSS repos
1533        /// where authors are public, or `anonymized` to emit non-reversible
1534        /// pseudonyms in regulated environments. Implies `--ownership`.
1535        #[arg(long, value_name = "MODE", value_enum)]
1536        ownership_emails: Option<EmailModeArg>,
1537
1538        /// Show only refactoring targets: ranked recommendations based on complexity,
1539        /// coupling, churn, and dead code signals. Requires full analysis pipeline.
1540        #[arg(long)]
1541        targets: bool,
1542
1543        /// Show advisory project-local public-signature type coupling. Requires
1544        /// type-aware analysis and does not change the health score.
1545        #[arg(long)]
1546        type_coupling: bool,
1547
1548        /// Add structural CSS analytics: specificity hotspots, !important density,
1549        /// over-complex selectors, deep nesting, and conservative cleanup
1550        /// candidates. Standard CSS is parsed structurally; preprocessor sources
1551        /// are scanned only where fallow can avoid expanding Sass/Less semantics.
1552        #[arg(long)]
1553        css: bool,
1554
1555        /// Filter refactoring targets by effort level (low, medium, high).
1556        /// Implies --targets.
1557        #[arg(long, value_enum)]
1558        effort: Option<EffortFilter>,
1559
1560        /// Show only the project health score (0–100) with letter grade (A/B/C/D/F).
1561        /// The score is included by default when no section flags are set.
1562        #[arg(long)]
1563        score: bool,
1564
1565        /// Fail if the health score is below this threshold (0-100).
1566        /// Implies --score. The authoritative CI quality gate: when set,
1567        /// complexity findings become informational and the exit code is
1568        /// driven solely by the score (so --min-score 0 always exits 0).
1569        /// Composes with --min-severity (fails if either gate trips). Plain
1570        /// `fallow health` (no gate flag) stays advisory and exits 1 on any
1571        /// finding; for a gate on newly-introduced complexity use
1572        /// `fallow audit --gate new-only`.
1573        #[arg(long, value_name = "N")]
1574        min_score: Option<f64>,
1575
1576        /// Only exit with error for findings at or above this severity.
1577        /// Use --min-severity critical to ignore moderate/high findings in CI.
1578        /// Composes with --min-score (the run fails if either gate trips).
1579        /// The complexity-* rules apply first: a finding whose rule is warn
1580        /// never fails the run, whatever its severity.
1581        #[arg(long, value_name = "LEVEL", value_enum)]
1582        min_severity: Option<HealthSeverityCli>,
1583
1584        /// Print the score and findings but never fail CI (always exit 0).
1585        /// Advisory mode for surfacing health in logs without blocking.
1586        /// Mutually exclusive with --min-score and --min-severity.
1587        #[arg(long)]
1588        report_only: bool,
1589
1590        /// Git history window for hotspot analysis (default: 6m).
1591        /// Accepts durations (6m, 90d, 1y, 2w) or ISO dates (2025-06-01).
1592        #[arg(long, value_name = "DURATION")]
1593        since: Option<String>,
1594
1595        /// Minimum number of commits for a file to be included in hotspot ranking (default: 3)
1596        #[arg(long, value_name = "N")]
1597        min_commits: Option<u32>,
1598
1599        /// Save a vital signs snapshot for trend tracking.
1600        /// Defaults to `.fallow/snapshots/{timestamp}.json` if no path is given.
1601        /// A given path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory.
1602        /// Forces file-scores, hotspot, and score computation for complete metrics.
1603        #[expect(
1604            clippy::option_option,
1605            reason = "clap pattern: None=not passed, Some(None)=flag only, Some(Some(path))=with value"
1606        )]
1607        #[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
1608        save_snapshot: Option<Option<String>>,
1609
1610        /// Compare current metrics against the most recent saved snapshot.
1611        /// Reads from `.fallow/snapshots/` and shows per-metric deltas with
1612        /// directional indicators. Implies --score.
1613        #[arg(long)]
1614        trend: bool,
1615
1616        /// Compare current metrics against this snapshot file instead of the
1617        /// newest file in `.fallow/snapshots/`. Use it to restore a baseline
1618        /// from external storage in CI. With --group-by, groups are compared
1619        /// by key when the snapshot holds the same grouping. Implies --trend.
1620        #[arg(long, value_name = "PATH")]
1621        trend_from: Option<PathBuf>,
1622
1623        /// Path to coverage data for exact per-function CRAP scores. Accepts an
1624        /// Istanbul coverage map JSON file (coverage-final.json, from
1625        /// `jest --coverage`, `vitest run --coverage`, c8 or nyc), a directory
1626        /// containing coverage-final.json, a raw V8 coverage directory
1627        /// (`NODE_V8_COVERAGE=<dir> node --test`), or a single V8 coverage JSON
1628        /// file. Transpiled V8 scripts (tsx, bundles) map back to their source
1629        /// files through the source map that Node records in the dump. A
1630        /// script that differs from the file on disk and has no source map
1631        /// keeps the estimate. Use --coverage-root when
1632        /// the data was generated in a different environment (CI runner,
1633        /// Docker). Affects CRAP scores only, not --coverage-gaps. Also
1634        /// configurable via FALLOW_COVERAGE env var.
1635        #[arg(long, value_name = "PATH")]
1636        coverage: Option<PathBuf>,
1637
1638        /// Absolute prefix to strip from file paths in coverage data before
1639        /// prepending the project root. Use when coverage was generated in a
1640        /// different environment (CI runner, Docker). Example: if coverage paths
1641        /// start with /home/runner/work/myapp and the project root is ./,
1642        /// pass --coverage-root /home/runner/work/myapp.
1643        #[arg(long, value_name = "PATH")]
1644        coverage_root: Option<PathBuf>,
1645
1646        /// File or directory containing runtime coverage input. Accepts a
1647        /// V8 coverage directory, a single V8 JSON file, or a single
1648        /// Istanbul coverage map JSON file (commonly coverage-final.json).
1649        /// A single local capture is free. Continuous or multi-capture
1650        /// monitoring needs a license (see `fallow license`).
1651        #[arg(long, value_name = "PATH")]
1652        runtime_coverage: Option<PathBuf>,
1653
1654        /// Saved `fallow mutation --format json` report (for example
1655        /// `.fallow/mutation/last-report.json` or the `-o` file of a run).
1656        /// Adds the mutation score of each complexity finding next to its
1657        /// CRAP score. The score never changes CRAP or the health score. A
1658        /// function whose file changed since the run gets no score. Reads
1659        /// the file only; it runs no test.
1660        #[arg(long, value_name = "FILE")]
1661        mutation_report: Option<PathBuf>,
1662
1663        /// Threshold for hot-path classification
1664        #[arg(long, default_value_t = 100)]
1665        min_invocations_hot: u64,
1666
1667        /// Minimum total trace volume before the sidecar allows high-confidence
1668        /// `safe_to_delete` / `review_required` verdicts. Below this the
1669        /// sidecar caps confidence at `medium` to protect against overconfident
1670        /// verdicts on new or low-traffic services. Omit to use the sidecar's
1671        /// spec default (5000).
1672        #[arg(long, value_name = "N")]
1673        min_observation_volume: Option<u32>,
1674
1675        /// Fraction of total trace count below which an invoked function is
1676        /// classified as `low_traffic` rather than `active`. Expressed as a
1677        /// decimal (e.g. `0.001` for 0.1%). Omit to use the sidecar's spec
1678        /// default (0.001).
1679        #[arg(long, value_name = "RATIO")]
1680        low_traffic_threshold: Option<f64>,
1681
1682        /// Scope reported findings to this file or directory (default: whole project).
1683        /// The full project graph is still built; only reported items are narrowed.
1684        #[arg(value_name = "PATH")]
1685        path: Option<PathBuf>,
1686    },
1687
1688    /// Detect feature flag patterns in the codebase
1689    ///
1690    /// Identifies environment variable flags (process.env.FEATURE_*),
1691    /// SDK calls from common providers, and config object patterns (opt-in).
1692    /// Reports flag locations, detection confidence, and cross-reference
1693    /// with dead code findings.
1694    Flags {
1695        /// Show only the top N flags
1696        #[arg(long)]
1697        top: Option<usize>,
1698
1699        /// Add a retirement report: one row per flag, with the reasons the
1700        /// flag can be retired. Advisory only; nothing is removed.
1701        #[arg(long)]
1702        retirement: bool,
1703
1704        /// Keep only retirement rows with this reason (repeatable)
1705        #[arg(long = "reason", value_name = "CODE", requires = "retirement")]
1706        reasons: Vec<flags::RetirementReasonArg>,
1707
1708        /// Order of the retirement rows
1709        #[arg(
1710            long,
1711            value_name = "KEY",
1712            requires = "retirement",
1713            default_value = "age"
1714        )]
1715        sort: flags::RetirementSortArg,
1716
1717        /// How to measure flag age: blame (lower bound), pickaxe (first
1718        /// commit with the name, slower) or off
1719        #[arg(
1720            long,
1721            value_name = "MODE",
1722            requires = "retirement",
1723            default_value = "blame"
1724        )]
1725        flag_age: flags::FlagAgeArg,
1726
1727        /// Keep only retirement rows at least this many days old
1728        #[arg(long, value_name = "DAYS", requires = "retirement")]
1729        min_age: Option<u64>,
1730
1731        /// Vendor flag export (JSON, read offline) that adds the
1732        /// fully-rolled-out, archived-in-vendor, missing-in-vendor and
1733        /// vendor-only reasons
1734        #[arg(long, value_name = "FILE", requires = "retirement")]
1735        flag_state: Option<std::path::PathBuf>,
1736
1737        /// Exit with code 1 when a flag in scope is older than this many
1738        /// days. Opt-in; needs a flag age
1739        #[arg(long, value_name = "DAYS", requires = "retirement")]
1740        max_flag_age: Option<u64>,
1741    },
1742
1743    /// List active fallow-ignore suppression markers (read-only inventory)
1744    ///
1745    /// Shows every `fallow-ignore-next-line` and `fallow-ignore-file` marker
1746    /// present in analyzed files, grouped per file with line, kind, level,
1747    /// and reason, plus project totals and a stale cross-reference against
1748    /// this run's stale-suppression findings. A governance surface, not a
1749    /// detector: always exits 0. Honors `--root`, `--format {human,json}`,
1750    /// `--workspace`, `--changed-workspaces`, `--changed-since`, and
1751    /// `--quiet`.
1752    Suppressions {
1753        /// Only list suppressions in the specified files. Accepts multiple values.
1754        #[arg(long, value_name = "PATH")]
1755        file: Vec<std::path::PathBuf>,
1756    },
1757
1758    /// Explain one fallow issue type without running an analysis.
1759    ///
1760    /// Prints the rule rationale, a worked example, fix guidance, and the
1761    /// relevant docs URL. Accepts values like `unused-export`,
1762    /// `fallow/unused-export`, `unused exports`, and `code duplication`.
1763    Explain {
1764        /// Issue type, issue label, or rule id to explain
1765        #[arg(required = true, num_args = 1.., value_name = "ISSUE_TYPE")]
1766        issue_type: Vec<String>,
1767    },
1768
1769    /// Audit changed files for dead code, complexity, duplication, and styling.
1770    ///
1771    /// Purpose-built for reviewing AI-generated code and PR quality gates.
1772    /// Combines dead-code + complexity + duplication + styling scoped to
1773    /// changed files and returns a verdict (pass/warn/fail).
1774    ///
1775    /// `fallow audit` answers "will CI block this?": it gates (exit 1 on a
1776    /// fail verdict). The `review` alias plus `--brief` answer "where do I
1777    /// look?": the same analysis rendered as a deterministic orientation brief
1778    /// that ALWAYS exits 0, so a reviewer or agent can read it regardless of
1779    /// the verdict. `--format` is orthogonal to `--brief`.
1780    /// When `--changed-since`/`--base` is unset, the base is the git merge-base
1781    /// against the branch's upstream or the remote default (`origin/HEAD`,
1782    /// `origin/main`, `origin/master`); set `FALLOW_AUDIT_BASE` to pin it.
1783    /// By default, only findings introduced by the changeset affect the verdict;
1784    /// inherited findings are reported with new-vs-inherited attribution and
1785    /// individual JSON findings include `introduced: true/false`. Use
1786    /// `--gate all` or `[audit] gate = "all"` to fail on every finding in
1787    /// changed files without running the extra base-snapshot attribution pass.
1788    ///
1789    /// The global --baseline / --save-baseline flags are rejected on audit.
1790    /// Use --dead-code-baseline, --health-baseline, and --dupes-baseline
1791    /// (or their config equivalents) because each sub-analysis uses a
1792    /// different baseline format.
1793    #[command(visible_alias = "review")]
1794    Audit {
1795        /// Run dead-code analysis in production mode for this audit.
1796        #[arg(long = "production-dead-code")]
1797        production_dead_code: bool,
1798
1799        /// Run health analysis in production mode for this audit.
1800        #[arg(long = "production-health")]
1801        production_health: bool,
1802
1803        /// Run duplication analysis in production mode for this audit.
1804        #[arg(long = "production-dupes")]
1805        production_dupes: bool,
1806
1807        /// Compare dead-code issues against a saved baseline
1808        /// (produced by `fallow dead-code --save-baseline`).
1809        #[arg(long)]
1810        dead_code_baseline: Option<PathBuf>,
1811
1812        /// Compare health findings against a saved baseline
1813        /// (produced by `fallow health --save-baseline`).
1814        #[arg(long)]
1815        health_baseline: Option<PathBuf>,
1816
1817        /// Compare duplication clone groups against a saved baseline
1818        /// (produced by `fallow dupes --save-baseline`).
1819        #[arg(long)]
1820        dupes_baseline: Option<PathBuf>,
1821
1822        /// Maximum CRAP score threshold (overrides config, default 30.0).
1823        /// Functions meeting or exceeding this score cause audit to fail.
1824        /// Pair with `--coverage` for accurate scoring.
1825        #[arg(long)]
1826        max_crap: Option<f64>,
1827
1828        /// Path to Istanbul coverage data (coverage-final.json) or raw V8
1829        /// coverage (a `NODE_V8_COVERAGE` directory or one V8 JSON file) for
1830        /// accurate per-function CRAP scores in the health sub-analysis. Also
1831        /// configurable via FALLOW_COVERAGE or health.coverage.
1832        #[arg(long, value_name = "PATH")]
1833        coverage: Option<PathBuf>,
1834
1835        /// Absolute prefix to strip from coverage data paths before CRAP matching.
1836        /// Use when coverage was generated under a different checkout root in CI or Docker.
1837        /// Also configurable via FALLOW_COVERAGE_ROOT or health.coverageRoot.
1838        #[arg(long, value_name = "PATH")]
1839        coverage_root: Option<PathBuf>,
1840
1841        /// Run only these sub-analyses (comma-separated: dead-code,
1842        /// architecture, dupes, health), with the same values as bare
1843        /// `fallow --only`. Replaces the `audit.deadCode`, `audit.health` and
1844        /// `audit.dupes` selection. The review brief (`--brief`, `fallow
1845        /// review`) keeps every sub-analysis.
1846        #[arg(long, value_delimiter = ',', conflicts_with = "skip")]
1847        only: Vec<AnalysisKind>,
1848
1849        /// Skip these sub-analyses (comma-separated: dead-code, architecture,
1850        /// dupes, health), with the same values as bare `fallow --skip`, on
1851        /// top of the `audit.*` selection.
1852        #[arg(long, value_delimiter = ',')]
1853        skip: Vec<AnalysisKind>,
1854
1855        /// Disable styling analytics in audit.
1856        #[arg(long = "no-css")]
1857        no_css: bool,
1858
1859        /// Enable deep CSS analysis for audit explicitly: project-wide styling
1860        /// reachability, narrowed back to changed anchors. Deep CSS is on by
1861        /// default; use this to override `audit.cssDeep = false`.
1862        #[arg(long)]
1863        css_deep: bool,
1864
1865        /// Disable deep CSS analysis while keeping local styling analytics on.
1866        #[arg(long = "no-css-deep")]
1867        no_css_deep: bool,
1868
1869        /// Which findings affect the audit verdict.
1870        ///
1871        /// new-only (default): fail only on findings introduced by the current
1872        /// changeset. all: fail on every finding in changed files and skip
1873        /// base-snapshot attribution.
1874        #[arg(long, value_enum)]
1875        gate: Option<AuditGateArg>,
1876
1877        /// Runtime coverage input. Accepts a V8 directory, a single V8 JSON
1878        /// file, or an Istanbul coverage map JSON. Runs the `fallow-cov`
1879        /// sidecar inside the audit, so the `hot-path-touched` verdict shows
1880        /// next to the dead-code and complexity findings without a second
1881        /// `fallow health` run in CI. The verdict is informational and does
1882        /// not change the exit code. A single local capture is free.
1883        /// Continuous or multi-capture monitoring needs a license (see
1884        /// `fallow license`).
1885        #[arg(long, value_name = "PATH")]
1886        runtime_coverage: Option<PathBuf>,
1887
1888        /// Threshold for hot-path classification, forwarded to the sidecar
1889        /// when `--runtime-coverage` is set.
1890        #[arg(long, default_value_t = 100)]
1891        min_invocations_hot: u64,
1892
1893        /// Internal marker identifying a gate run (e.g. `pre-commit`), set by
1894        /// the generated git hook so Fallow Impact can record a containment
1895        /// event when the gate blocks then clears. Hidden; never changes the
1896        /// verdict, exit code, or output.
1897        #[arg(long, value_name = "MARKER", hide = true)]
1898        gate_marker: Option<String>,
1899
1900        /// Render the deterministic review brief instead of the gating audit
1901        /// report. The brief answers "where do I look?" rather than "will CI
1902        /// block this?", runs the same analysis, and ALWAYS exits 0 (the
1903        /// verdict is carried informationally). Implied by `fallow review`.
1904        /// Orthogonal to `--format`.
1905        #[arg(long)]
1906        brief: bool,
1907
1908        /// Cap on the number of consequential structural decisions surfaced in
1909        /// the review brief's decision surface (the working-memory limit).
1910        /// Default 4; clamped to the 3-5 band (4 plus or minus 1). Only
1911        /// consulted on the brief path.
1912        #[arg(
1913            long,
1914            value_name = "N",
1915            default_value_t = audit_decision_surface::DEFAULT_DECISION_CAP
1916        )]
1917        max_decisions: usize,
1918
1919        /// Emit the agent-contract WALKTHROUGH GUIDE: the current digest
1920        /// (brief + decision surface), the review direction, the JSON schema the
1921        /// agent must return, and a deterministic graph-snapshot hash pinned into
1922        /// the digest. The digest is built from the graph only (PR prose is never
1923        /// folded in, so it is injection-resistant). Implies the brief; always
1924        /// exits 0. A thin agent skill calls this to fetch the current guide,
1925        /// produces judgment JSON, then reopens with `--walkthrough-file`.
1926        #[arg(long, conflicts_with_all = ["walkthrough_file", "walkthrough"])]
1927        walkthrough_guide: bool,
1928
1929        /// Ingest an agent's judgment JSON and POST-VALIDATE it against the
1930        /// LIVE graph. Rejects any judgment whose `signal_id` fallow did not emit
1931        /// (anti-hallucination); refuses the whole payload as stale when the
1932        /// echoed graph-snapshot hash no longer matches (the tree moved); rejects
1933        /// an `action` outside `block`, `address`, `consider`, `fyi`
1934        /// (`invalid-action`). The verifier is the graph, not a second model.
1935        /// Implies the brief; always exits 0. The agent's free-text framing and
1936        /// action label are fenced as non-deterministic and never gate or
1937        /// auto-post.
1938        #[arg(long, value_name = "PATH")]
1939        walkthrough_file: Option<PathBuf>,
1940
1941        /// Render the existing walkthrough guide as a staged HUMAN terminal tour
1942        /// (Stage 1 load-bearing / Stage 2 mechanical), or markdown with
1943        /// `--format markdown`. Implies the brief; always exits 0.
1944        /// `--format json --walkthrough` emits the same agent-contract JSON as
1945        /// `--walkthrough-guide`.
1946        #[arg(long, conflicts_with_all = ["walkthrough_guide", "walkthrough_file"])]
1947        walkthrough: bool,
1948
1949        /// Record one or more changed files as VIEWED in the local walkthrough
1950        /// viewed-state ledger (`.fallow/walkthrough-state.json`), then render the
1951        /// tour. Files already viewed (and still current) collapse into the
1952        /// Cleared panel. Repeatable. Stale marks (the tree moved) are ignored on
1953        /// render but never deleted. Only consulted on the `--walkthrough` path.
1954        #[arg(long, value_name = "PATH")]
1955        mark_viewed: Vec<PathBuf>,
1956
1957        /// Expand the Cleared panel in the human/markdown walkthrough tour: list
1958        /// each de-prioritized and already-viewed file instead of the collapsed
1959        /// one-line summary. Only consulted on the `--walkthrough` path.
1960        #[arg(long)]
1961        show_cleared: bool,
1962
1963        /// Expand the de-prioritized units in the review brief's weighted
1964        /// focus map ("show me what you de-prioritized"). The `deprioritized`
1965        /// escape-hatch list is ALWAYS present in `--format json` regardless; this
1966        /// flag only re-expands the collapse-by-default human focus render. Only
1967        /// consulted on the `--walkthrough` path.
1968        #[arg(long)]
1969        show_deprioritized: bool,
1970
1971        /// Scope reported findings to this file or directory (default: whole project).
1972        /// The full project graph is still built; only reported items are narrowed.
1973        #[arg(value_name = "PATH")]
1974        path: Option<PathBuf>,
1975    },
1976
1977    /// Maintain reusable audit base-snapshot caches.
1978    AuditCache {
1979        #[command(subcommand)]
1980        subcommand: AuditCacheCli,
1981    },
1982
1983    /// Maintain the committed baseline files that the config points at.
1984    Baselines {
1985        #[command(subcommand)]
1986        subcommand: BaselinesCli,
1987    },
1988
1989    /// Surface the consequential structural DECISIONS a change embeds (the apex
1990    /// of the review brief), each framed as a judgment question with the routed
1991    /// expert to ask.
1992    ///
1993    /// The product's decision surface: a ranked, capped (4 plus or minus 1),
1994    /// signal_id-anchored set of the SOLID-3 decisions (coupling/boundary,
1995    /// exports-aware public-API/contract, dependency). Runs the same changed-code
1996    /// analysis as `fallow review` but emits ONLY the decisions, separable and
1997    /// cheap. Coupling and public-API decisions are suppressible with
1998    /// `// fallow-ignore`; a dependency decision anchors on `package.json` and
1999    /// has no suppress action. Never gates: exits 0 after a successful run, and
2000    /// exits 2 on invalid input (for example the global `--baseline` or
2001    /// `--save-baseline` flag) like every other command. Use `--base` /
2002    /// `--changed-since` to pick the comparison point, exactly like `fallow audit`.
2003    DecisionSurface {
2004        /// Cap on the number of surfaced decisions (the working-memory limit).
2005        /// Default 4; clamped to the 3-5 band (4 plus or minus 1).
2006        #[arg(
2007            long,
2008            value_name = "N",
2009            default_value_t = audit_decision_surface::DEFAULT_DECISION_CAP
2010        )]
2011        max_decisions: usize,
2012    },
2013
2014    /// Show what fallow has done for you: how many issues it is surfacing, the
2015    /// trend since the last recorded run, and how many commits it contained at
2016    /// the pre-commit gate.
2017    ///
2018    /// Local-only and opt-in: enable per project with `fallow impact enable`, or
2019    /// turn it on everywhere with `fallow impact default on`, then let your
2020    /// `fallow audit` / pre-commit gate runs build history. History is stored in
2021    /// your user config dir (never written into the repo) and forced off in CI.
2022    /// Impact never uploads anything and never affects exit codes.
2023    Impact {
2024        #[command(subcommand)]
2025        subcommand: Option<ImpactCli>,
2026        /// Aggregate every tracked project into one cross-repo roll-up
2027        /// ("what has fallow done for me across all my repos"). Reads the
2028        /// user config dir; ignores `--root`. Cannot combine with a subcommand.
2029        #[arg(long)]
2030        all: bool,
2031        /// Row ordering for `--all` (default: most recently recorded first).
2032        #[arg(long, value_enum, default_value_t = ImpactSortCli::Recent)]
2033        sort: ImpactSortCli,
2034        /// Cap the number of `--all` rows printed (grand totals still reflect
2035        /// every tracked project).
2036        #[arg(long)]
2037        limit: Option<usize>,
2038    },
2039
2040    /// Surface local security candidates for downstream agent verification (opt-in).
2041    ///
2042    /// Ships three complementary surfaces. (1) The graph-structural
2043    /// `client-server-leak` rule: a `"use client"` file that transitively imports
2044    /// a module reading a non-public env secret through `process.env` or
2045    /// `import.meta.env`. (2) The data-driven
2046    /// `tainted-sink` catalogue: syntactic sink sites matched against a CWE
2047    /// catalogue (`security_matchers.toml`) spanning categories such as
2048    /// dangerous-html, template-escape-bypass, command-injection, code-injection,
2049    /// dynamic-regex, redos-regex, resource-amplification, dynamic-module-load,
2050    /// sql-injection, ssrf, path-traversal, header-injection, open-redirect,
2051    /// cleartext-transport, electron-unsafe-webpreferences,
2052    /// world-writable-permission, insecure-temp-file,
2053    /// mysql-multiple-statements, mass-assignment, weak-crypto,
2054    /// deprecated-cipher, insecure-randomness,
2055    /// unsafe-buffer-alloc, unsafe-deserialization, prototype-pollution,
2056    /// zip-slip, nosql-injection, ssti, xxe, xpath-injection, and
2057    /// webview-injection. (3) `hardcoded-secret`,
2058    /// an include-required
2059    /// category for provider-prefix literals and high-entropy literals assigned
2060    /// to secret-shaped identifiers. It never runs from raw entropy alone. All
2061    /// findings are CANDIDATES for verification, NOT verified vulnerabilities.
2062    /// This command is the only
2063    /// surface for security findings; they never appear under bare `fallow` or
2064    /// the `audit` gate. Build-config and test files are excluded, and public
2065    /// env prefixes such as `NEXT_PUBLIC_` and `VITE_` are treated as public.
2066    /// Honors
2067    /// `--root`, `--format {human,json,sarif}`, `--changed-since`, `--file`, `--gate`, `--diff-file`,
2068    /// `--diff-stdin`, `--workspace`, `--changed-workspaces`, `--ci`,
2069    /// `--fail-on-issues`, `--sarif-file`, `--summary`, `--explain`, and `--surface`.
2070    Security {
2071        #[command(subcommand)]
2072        subcommand: Option<SecuritySubcommand>,
2073        /// Runtime coverage input. Accepts a V8 directory, a single V8 JSON
2074        /// file, or an Istanbul coverage map JSON. When set, `fallow security`
2075        /// adds production runtime state to tainted-sink candidates and uses
2076        /// that state as an extra ranking signal. A single local capture is
2077        /// free. Continuous or multi-capture monitoring needs a license (see
2078        /// `fallow license`).
2079        #[arg(long, value_name = "PATH")]
2080        runtime_coverage: Option<PathBuf>,
2081        /// Threshold for hot-path classification, forwarded to the sidecar
2082        /// when `--runtime-coverage` is set.
2083        #[arg(long, default_value_t = 100)]
2084        min_invocations_hot: u64,
2085        /// Only report security candidates in or reachable from the specified files.
2086        /// The full project graph is still built, but output is scoped to matching
2087        /// finding anchors or trace hops. Accepts multiple values.
2088        #[arg(long, value_name = "PATH")]
2089        file: Vec<std::path::PathBuf>,
2090        /// Opt-in regression gate: fail (exit 8) only when the change introduces a
2091        /// NEW security-sink candidate in the changed lines, not on the whole
2092        /// candidate backlog. Requires a diff source: `--changed-since <ref>`,
2093        /// `--diff-file <path>`, or `--diff-stdin`. There is deliberately no `all`
2094        /// mode (gating on the full backlog is the anti-feature this gate avoids).
2095        #[arg(long, value_name = "MODE")]
2096        gate: Option<security::SecurityGateArg>,
2097        /// Include the agent-facing attack-surface inventory in JSON output.
2098        #[arg(long)]
2099        surface: bool,
2100
2101        /// Scope reported findings to this file or directory (default: whole project).
2102        /// The full project graph is still built; only reported items are narrowed.
2103        #[arg(value_name = "PATH")]
2104        path: Option<PathBuf>,
2105    },
2106
2107    /// Render a saved `--format json` results file in another format without
2108    /// re-running analysis (analyze once, then render every CI surface from
2109    /// the same file). Supports GitHub annotations/summary, CodeClimate,
2110    /// SARIF, markdown, and GitHub/GitLab PR-comment and review formats.
2111    Report {
2112        /// Path to a fallow JSON results file produced by `--format json`
2113        /// (dead-code, architecture, dupes, health, audit, security, or bare
2114        /// combined).
2115        #[arg(long, value_name = "PATH")]
2116        from: PathBuf,
2117    },
2118    /// Dump fallow's capability manifest (CLI commands and flags, issue types, MCP tools, framework plugins, env vars) as machine-readable JSON for agent introspection. Always JSON, regardless of --format
2119    Schema,
2120
2121    /// Print or vendor CI integration templates.
2122    ///
2123    /// Use `fallow ci-template gitlab` to print the GitLab CI template, or
2124    /// `fallow ci-template gitlab --vendor` to write the template plus the
2125    /// bash helper files that enable MR comments without downloading from
2126    /// raw.githubusercontent.com at pipeline runtime.
2127    CiTemplate {
2128        #[command(subcommand)]
2129        subcommand: CiTemplateCli,
2130    },
2131
2132    /// Migrate configuration from knip, jscpd, or stylelint to fallow
2133    Migrate {
2134        /// Generate `fallow.toml` instead of JSONC
2135        #[arg(long, conflicts_with = "jsonc")]
2136        toml: bool,
2137
2138        /// Write JSONC content to `.fallowrc.jsonc` instead of `.fallowrc.json`. The
2139        /// generated content is the same JSONC (with `//` comments) either way; the
2140        /// `.jsonc` extension lets editors auto-detect JSON-with-comments syntax
2141        /// highlighting and silences linters that flag comments in `.json`. Without
2142        /// `--jsonc` or `--toml`, fallow auto-mirrors the source extension: a
2143        /// `knip.jsonc` migration writes `.fallowrc.jsonc`, a `knip.json` migration
2144        /// writes `.fallowrc.json`.
2145        #[arg(long)]
2146        jsonc: bool,
2147
2148        /// Only preview the generated config without writing
2149        #[arg(long)]
2150        dry_run: bool,
2151
2152        /// Path to source config file (auto-detect if not specified)
2153        #[arg(long, value_name = "PATH")]
2154        from: Option<PathBuf>,
2155    },
2156
2157    /// Manage the license for continuous/cloud runtime monitoring.
2158    ///
2159    /// Verification is offline against an Ed25519 public key compiled into
2160    /// the binary. The license file lives at `~/.fallow/license.jwt` (or
2161    /// `$FALLOW_LICENSE_PATH`); `$FALLOW_LICENSE` env var takes precedence
2162    /// and is the recommended path for shared CI runners.
2163    License {
2164        #[command(subcommand)]
2165        subcommand: LicenseCli,
2166    },
2167
2168    /// Manage opt-in product telemetry.
2169    ///
2170    /// Telemetry is off by default. It never collects repository names, paths,
2171    /// package names, source code, config values, raw errors, or raw agent
2172    /// detection evidence. Use `fallow telemetry inspect --example` to see the
2173    /// documented payload shape, or prefix a real command with
2174    /// `FALLOW_TELEMETRY=inspect` to print the exact payload without sending.
2175    Telemetry {
2176        #[command(subcommand)]
2177        subcommand: TelemetryCli,
2178    },
2179
2180    /// Runtime coverage workflow.
2181    ///
2182    /// `setup` is the resumable first-run flow: license check, sidecar
2183    /// install, coverage recipe, then analysis.
2184    Coverage {
2185        #[command(subcommand)]
2186        subcommand: CoverageCli,
2187    },
2188
2189    /// Install or remove a Claude Code PreToolUse hook that gates
2190    /// `git commit` / `git push` on `fallow audit`, so the agent cleans
2191    /// findings before the command runs.
2192    ///
2193    /// Deprecated: use `fallow agent install` (one pass for every harness)
2194    /// or `fallow hooks install --target agent` (the gate alone). This
2195    /// command keeps working throughout fallow 3 and is removed in the next
2196    /// major. It writes into `.claude/settings.json` +
2197    /// `.claude/hooks/fallow-gate.sh`, or for Codex into `.codex/hooks.json` +
2198    /// `.codex/hooks/fallow-gate.sh` plus an `AGENTS.md` routing block. For a
2199    /// shell-level Git pre-commit hook in
2200    /// `.git/hooks/`, see `fallow hooks install --target git` instead.
2201    SetupHooks {
2202        /// Target a specific agent surface (default: auto-detect).
2203        #[arg(long, value_enum)]
2204        agent: Option<setup_hooks::HookAgentArg>,
2205
2206        /// Print what would be written or removed without touching the filesystem.
2207        #[arg(long)]
2208        dry_run: bool,
2209
2210        /// Overwrite a user-edited hook script, invalid settings.json, or
2211        /// remove a user-edited script during uninstall.
2212        #[arg(long)]
2213        force: bool,
2214
2215        /// Write to the user's home directory instead of the project root.
2216        #[arg(long)]
2217        user: bool,
2218
2219        /// Append `.claude/` to the project's `.gitignore`.
2220        #[arg(long)]
2221        gitignore_claude: bool,
2222
2223        /// Remove the fallow-gate handler, hook script, and AGENTS.md
2224        /// managed block instead of installing them. Idempotent: reports
2225        /// "unchanged" when nothing to remove.
2226        #[arg(long)]
2227        uninstall: bool,
2228    },
2229
2230    /// Generate an interactive HTML map of the codebase
2231    Viz {
2232        /// Output file path (default: fallow-viz.html in project root)
2233        #[arg(long = "out", value_name = "PATH")]
2234        output: Option<PathBuf>,
2235
2236        /// Don't open the output file in the browser
2237        #[arg(long)]
2238        no_open: bool,
2239
2240        /// Visualization output format
2241        #[arg(long = "viz-format", default_value = "html")]
2242        viz_format: viz::VizFormat,
2243    },
2244}
2245
2246#[derive(Subcommand)]
2247pub enum SecuritySubcommand {
2248    /// Render verifier-retained survivor candidates from fallow output plus verifier verdicts.
2249    Survivors {
2250        /// Raw `fallow security --format json` candidate output.
2251        #[arg(long, value_name = "PATH")]
2252        candidates: PathBuf,
2253        /// Verifier verdict JSON file.
2254        #[arg(long, value_name = "PATH")]
2255        verdicts: PathBuf,
2256        /// Fail when any candidate has no matching verdict.
2257        #[arg(long)]
2258        require_verdict_for_each_candidate: bool,
2259    },
2260    /// Group unresolved security callees into actionable blind-spot output.
2261    #[command(name = "blind-spots")]
2262    BlindSpots {
2263        /// Scope diagnostics to selected files.
2264        #[arg(long, value_name = "PATH")]
2265        file: Vec<PathBuf>,
2266    },
2267}
2268
2269#[derive(clap::Subcommand)]
2270pub enum BaselinesCli {
2271    /// Remove the baseline entries that match no current finding.
2272    ///
2273    /// Reads the files that `audit.deadCodeBaseline`, `audit.healthBaseline`
2274    /// and `audit.dupesBaseline` name, runs one whole-project analysis, and
2275    /// removes each entry that `--baseline` would no longer match. Prune never
2276    /// adds an entry: a new finding stays outside the baseline, and the normal
2277    /// gates report it. To record new findings on purpose, use
2278    /// `fallow <dead-code|health|dupes> --save-baseline`.
2279    ///
2280    /// Exit codes: 0 when the files are pruned or have nothing to prune, 1 with
2281    /// `--check` when an entry can be pruned, and 2 when a file cannot be read
2282    /// or pruned. A file with an older key form is skipped with the command
2283    /// that saves it again.
2284    Prune {
2285        /// Report the entries that a prune would remove, and write nothing.
2286        #[arg(long)]
2287        check: bool,
2288
2289        /// The dead-code baseline to prune. Overrides `audit.deadCodeBaseline`.
2290        #[arg(long, value_name = "PATH")]
2291        dead_code_baseline: Option<PathBuf>,
2292
2293        /// The health baseline to prune. Overrides `audit.healthBaseline`.
2294        #[arg(long, value_name = "PATH")]
2295        health_baseline: Option<PathBuf>,
2296
2297        /// The duplication baseline to prune. Overrides `audit.dupesBaseline`.
2298        #[arg(long, value_name = "PATH")]
2299        dupes_baseline: Option<PathBuf>,
2300
2301        /// The coverage input for CRAP scores, as for `fallow health
2302        /// --coverage`. Use the input that the health baseline was saved with.
2303        #[arg(long, value_name = "PATH")]
2304        coverage: Option<PathBuf>,
2305
2306        /// Rebase the file paths of the coverage input onto this root.
2307        #[arg(long, value_name = "PATH")]
2308        coverage_root: Option<PathBuf>,
2309    },
2310}
2311
2312#[derive(clap::Subcommand)]
2313pub enum AuditCacheCli {
2314    /// Remove reusable audit caches owned by an explicit project root.
2315    ///
2316    /// Deletes this project's cache entries unconditionally, warm or not. To
2317    /// apply the age-based GC policy across every cache entry instead, use
2318    /// `fallow audit-cache prune`.
2319    Remove {
2320        /// Print what would be removed without touching the filesystem.
2321        #[arg(long)]
2322        dry_run: bool,
2323
2324        /// Confirm removal in non-interactive environments.
2325        #[arg(long, alias = "force")]
2326        yes: bool,
2327    },
2328
2329    /// Apply the audit cache GC policy now and report every entry.
2330    ///
2331    /// Runs the same reclaim policy every `fallow audit` run already applies
2332    /// silently: orphaned-sidecar cleanup, age-based reclaim under the
2333    /// resolved threshold, and cross-repo reclaim of abandoned entries whose
2334    /// recorded owner root no longer exists. It also bounds the base-snapshot
2335    /// files in the project cache directory (`pass: "snapshot"`): files older
2336    /// than the threshold go, and only the newest 32 stay. Entries owned by
2337    /// other live projects are never touched. Use `--dry-run` to preview every decision
2338    /// the policy would take without touching the filesystem. `--root` is
2339    /// optional and defaults to the current directory. Reported sizes come
2340    /// from a full recursive walk of each cache entry, which can take a few
2341    /// seconds on large caches. To delete one project's caches
2342    /// unconditionally, use `fallow audit-cache remove --root <path> --yes`.
2343    Prune {
2344        /// Preview decisions without touching the filesystem.
2345        #[arg(long)]
2346        dry_run: bool,
2347
2348        /// Age threshold in days for this invocation. Overrides
2349        /// FALLOW_AUDIT_CACHE_MAX_AGE_DAYS and the `audit.cacheMaxAgeDays`
2350        /// config field (default 30). `0` disables age-based reclaim but
2351        /// still reclaims orphaned sidecars and entries whose recorded owner
2352        /// root is gone; unconditional deletion of one project's caches is
2353        /// `fallow audit-cache remove --root <path> --yes`.
2354        #[arg(long, value_name = "N")]
2355        max_age_days: Option<u32>,
2356    },
2357}
2358
2359#[derive(clap::Subcommand)]
2360pub enum LicenseCli {
2361    /// Activate a license JWT.
2362    ///
2363    /// JWT input precedence: positional arg > `--from-file` > stdin (`-`).
2364    /// All paths normalize whitespace before crypto verification.
2365    Activate {
2366        /// JWT as a positional argument.
2367        #[arg(value_name = "JWT")]
2368        jwt: Option<String>,
2369
2370        /// Path to a file containing the JWT.
2371        #[arg(long, value_name = "PATH")]
2372        from_file: Option<PathBuf>,
2373
2374        /// Read JWT from stdin.
2375        #[arg(long, conflicts_with_all = ["jwt", "from_file"])]
2376        stdin: bool,
2377
2378        /// Start a 30-day email-gated trial in one step.
2379        ///
2380        /// The trial endpoint is rate-limited to 5 requests per hour per IP.
2381        /// In CI or behind a shared NAT, start the trial from a developer
2382        /// machine and set FALLOW_LICENSE (or FALLOW_LICENSE_PATH) on the
2383        /// runner instead of re-running `activate --trial` per job.
2384        #[arg(long, requires = "email")]
2385        trial: bool,
2386
2387        /// Email address for the trial flow.
2388        #[arg(long, value_name = "ADDR")]
2389        email: Option<String>,
2390    },
2391    /// Show the active license tier, seats, features, and days remaining.
2392    Status,
2393    /// Fetch a fresh JWT from `api.fallow.cloud` (network-only).
2394    ///
2395    /// The stored license JWT is the primary identity proof. When it is
2396    /// missing, or the cloud reports it as too stale to exchange, the request
2397    /// is retried with a full-access API key.
2398    Refresh {
2399        /// Fallow Cloud API key (bearer token) used when the stored license
2400        /// JWT cannot be refreshed.
2401        ///
2402        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2403        /// <https://fallow.cloud/settings#api-keys>.
2404        ///
2405        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2406        /// secret on the command line may be visible to other processes via
2407        /// `ps` and can leak into shell history or process audit logs.
2408        #[arg(long, value_name = "KEY")]
2409        api_key: Option<String>,
2410    },
2411    /// Remove the local license file.
2412    Deactivate,
2413}
2414
2415#[derive(Clone, Copy, clap::Subcommand)]
2416pub enum TelemetryCli {
2417    /// Show effective telemetry state, precedence, and controls.
2418    Status,
2419    /// Enable opt-in telemetry in the user-level fallow config.
2420    Enable,
2421    /// Disable telemetry in the user-level fallow config.
2422    Disable,
2423    /// Explain inspect mode or print example payloads.
2424    Inspect {
2425        /// Print documented example payloads and field purposes.
2426        #[arg(long)]
2427        example: bool,
2428    },
2429}
2430
2431#[derive(clap::Subcommand)]
2432pub enum CiTemplateCli {
2433    /// Print or vendor the GitLab CI template and MR integration helpers.
2434    Gitlab {
2435        /// Write ci/ and action/ helper files under DIR instead of printing the template.
2436        ///
2437        /// Passing --vendor without a DIR writes into the current directory.
2438        #[arg(long, value_name = "DIR", num_args = 0..=1, default_missing_value = ".")]
2439        vendor: Option<PathBuf>,
2440
2441        /// Overwrite existing files that differ from the bundled template.
2442        #[arg(long)]
2443        force: bool,
2444    },
2445}
2446
2447#[derive(clap::Subcommand)]
2448pub enum CoverageCli {
2449    /// Resumable first-run setup: license + sidecar + recipe + analysis.
2450    Setup {
2451        /// Accept all prompts automatically.
2452        #[arg(short = 'y', long)]
2453        yes: bool,
2454
2455        /// Print instructions instead of prompting.
2456        #[arg(long)]
2457        non_interactive: bool,
2458
2459        /// Emit deterministic setup instructions as JSON. Implies --non-interactive.
2460        #[arg(long)]
2461        json: bool,
2462    },
2463    /// Analyze runtime coverage from a local artifact or explicit cloud source.
2464    ///
2465    /// Cloud mode is opt-in only. `FALLOW_API_KEY` by itself never selects
2466    /// cloud mode; pass `--cloud` / `--runtime-coverage-cloud`, or set
2467    /// `FALLOW_RUNTIME_COVERAGE_SOURCE=cloud`.
2468    Analyze {
2469        /// File or directory containing local runtime coverage input. A
2470        /// single local capture is free. Continuous or multi-capture
2471        /// monitoring needs a license (see `fallow license`).
2472        #[arg(long, value_name = "PATH", conflicts_with = "cloud")]
2473        runtime_coverage: Option<PathBuf>,
2474
2475        /// Fetch latest runtime facts from Fallow Cloud for the selected repo.
2476        #[arg(long, visible_alias = "runtime-coverage-cloud")]
2477        cloud: bool,
2478
2479        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2480        #[arg(long, value_name = "KEY")]
2481        api_key: Option<String>,
2482
2483        /// Override the Fallow Cloud base URL.
2484        #[arg(long, value_name = "URL")]
2485        api_endpoint: Option<String>,
2486
2487        /// Repository identifier, for example `owner/repo`.
2488        ///
2489        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2490        /// `git remote get-url origin`. Slashes are percent-encoded as one
2491        /// URL segment when calling the cloud runtime-context endpoint.
2492        #[arg(long, value_name = "OWNER/REPO")]
2493        repo: Option<String>,
2494
2495        /// Optional monorepo/project disambiguator.
2496        #[arg(long, value_name = "ID")]
2497        project_id: Option<String>,
2498
2499        /// Runtime observation window to request from cloud (1..=90 days).
2500        #[arg(long, value_name = "DAYS", default_value_t = 30)]
2501        coverage_period: u16,
2502
2503        /// Optional runtime environment filter.
2504        #[arg(long, value_name = "ENV")]
2505        environment: Option<String>,
2506
2507        /// Optional commit SHA filter for cloud runtime facts.
2508        #[arg(long, value_name = "SHA")]
2509        commit_sha: Option<String>,
2510
2511        /// Analyze production code only.
2512        #[arg(long)]
2513        production: bool,
2514
2515        /// Threshold for hot-path classification.
2516        #[arg(long, default_value_t = 100)]
2517        min_invocations_hot: u64,
2518
2519        /// Minimum total trace volume before high-confidence verdicts.
2520        #[arg(long, value_name = "N")]
2521        min_observation_volume: Option<u32>,
2522
2523        /// Fraction of total trace count below which an invoked function is low traffic.
2524        #[arg(long, value_name = "RATIO")]
2525        low_traffic_threshold: Option<f64>,
2526
2527        /// Show only the top N runtime findings and hot paths.
2528        #[arg(long)]
2529        top: Option<usize>,
2530
2531        /// Show the first-class blast-radius section in human output.
2532        #[arg(long)]
2533        blast_radius: bool,
2534
2535        /// Show the first-class importance section in human output.
2536        #[arg(long)]
2537        importance: bool,
2538
2539        /// List cloud runtime functions with no local counterpart on stderr.
2540        #[arg(long)]
2541        debug_unmatched: bool,
2542    },
2543    /// Upload a static function inventory to Fallow Cloud. Needs a fallow
2544    /// cloud API key. Unlocks the `untracked` filter on the dashboard by
2545    /// pairing runtime coverage data with the AST view of "every function
2546    /// that exists". See <https://fallow.tools/docs/analysis/runtime-coverage/>.
2547    ///
2548    /// This command makes network calls to Fallow Cloud. `fallow dead-code`
2549    /// stays offline.
2550    ///
2551    /// Exit codes: 0 ok · 7 network · 10 validation · 11 payload too large
2552    /// · 12 auth rejected · 13 server error.
2553    UploadInventory {
2554        /// Fallow Cloud API key (bearer token).
2555        ///
2556        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2557        /// <https://fallow.cloud/settings#api-keys>.
2558        ///
2559        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2560        /// secret on the command line may be visible to other processes via
2561        /// `ps` and can leak into shell history or process audit logs.
2562        #[arg(long, value_name = "KEY")]
2563        api_key: Option<String>,
2564
2565        /// Override the Fallow Cloud base URL.
2566        ///
2567        /// Useful for staging and on-premise deployments. Also respects
2568        /// $FALLOW_API_URL when this flag is not set.
2569        #[arg(long, value_name = "URL")]
2570        api_endpoint: Option<String>,
2571
2572        /// Project identifier, for example `fallow-cloud-api` or `owner/repo`.
2573        ///
2574        /// Defaults to $GITHUB_REPOSITORY, then $CI_PROJECT_PATH, then the
2575        /// parsed origin URL from `git remote get-url origin`.
2576        #[arg(long, value_name = "PROJECT_ID")]
2577        project_id: Option<String>,
2578
2579        /// Explicit git SHA for this inventory.
2580        ///
2581        /// Default: `git rev-parse HEAD`. The inventory is keyed on this
2582        /// value; the cloud back-fills hourly buckets with a matching SHA.
2583        #[arg(long, value_name = "SHA")]
2584        git_sha: Option<String>,
2585
2586        /// Proceed even when the working tree has uncommitted changes.
2587        ///
2588        /// Warning: the inventory is generated from the working copy, so it
2589        /// may not match the uploaded git SHA. Commit or stash first if you
2590        /// want a SHA-exact upload.
2591        #[arg(long)]
2592        allow_dirty: bool,
2593
2594        /// Additional glob patterns to exclude from the walk.
2595        ///
2596        /// Applied after the existing fallow ignore rules. Repeatable.
2597        #[arg(long, value_name = "GLOB", num_args = 0..)]
2598        exclude_paths: Vec<String>,
2599
2600        /// Prefix prepended to every emitted filePath so the static
2601        /// inventory joins with the runtime beacon for your deployment.
2602        /// Required for containerized deployments where the deployed
2603        /// WORKDIR rebases paths at runtime. Default: none (paths emit
2604        /// repo-relative, matching local runs and non-container CI).
2605        ///
2606        /// Common values: `/app` (typical Dockerfile), `/workspace`
2607        /// (Buildpacks / Cloud Run), `/usr/src/app` (older Node images),
2608        /// `/var/task` (Lambda), `/home/runner/work/<repo>/<repo>`
2609        /// (GitHub Actions default checkout).
2610        ///
2611        /// Must start with `/` and use POSIX separators.
2612        #[arg(long, value_name = "PREFIX")]
2613        path_prefix: Option<String>,
2614
2615        /// Print what would be uploaded and exit. No network call.
2616        #[arg(long)]
2617        dry_run: bool,
2618
2619        /// Also upload importer edges (which files import each function) so the
2620        /// cloud can show change-time blast radius. Opt-in: this builds the
2621        /// import graph by running the full static analysis, whereas the default
2622        /// upload is a fast per-file walk. The graph is cached, so a CI step that
2623        /// already ran analysis pays little extra.
2624        #[arg(long)]
2625        with_callers: bool,
2626
2627        /// Treat transient upload failures as warnings instead of errors
2628        /// (exit 0). Validation and auth errors still fail hard; this only
2629        /// downgrades transport and server errors.
2630        #[arg(long)]
2631        ignore_upload_errors: bool,
2632    },
2633    /// Upload JavaScript source maps to Fallow Cloud for bundled runtime coverage.
2634    ///
2635    /// Scans a build output directory for `.map` files and uploads them under
2636    /// the selected repo + git SHA. The production beacon reports bundled
2637    /// paths; the cloud resolver uses these maps to remap runtime coverage back
2638    /// to original source files.
2639    ///
2640    /// Each upload also carries the map's path relative to the repo root, so the
2641    /// source-evidence viewer can resolve a monorepo sub-package map's relative
2642    /// `sources[]` (e.g. `../../src/X`) to the package-prefixed source path
2643    /// (e.g. `dashboard/src/X`). Run from the repo root so this prefix is
2644    /// correct.
2645    UploadSourceMaps {
2646        /// Directory to scan recursively for source maps.
2647        #[arg(long, value_name = "PATH", default_value = "dist")]
2648        dir: PathBuf,
2649
2650        /// Glob pattern, relative to --dir, selecting maps to upload.
2651        #[arg(long, value_name = "GLOB", default_value = "**/*.map")]
2652        include: String,
2653
2654        /// Glob pattern, relative to --dir, selecting files to skip.
2655        ///
2656        /// Repeatable. Defaults to `**/node_modules/**`.
2657        #[arg(long, value_name = "GLOB", default_value = "**/node_modules/**")]
2658        exclude: Vec<String>,
2659
2660        /// Repo name used in the API path.
2661        ///
2662        /// Defaults to package.json repository.url, then `git remote get-url origin`.
2663        #[arg(long, value_name = "NAME")]
2664        repo: Option<String>,
2665
2666        /// Commit SHA to key uploads under.
2667        ///
2668        /// Defaults to $GITHUB_SHA, $CI_COMMIT_SHA, $COMMIT_SHA, then
2669        /// `git rev-parse HEAD`.
2670        #[arg(long, value_name = "SHA")]
2671        git_sha: Option<String>,
2672
2673        /// Override the Fallow Cloud base URL.
2674        #[arg(long, value_name = "URL")]
2675        endpoint: Option<String>,
2676
2677        /// Send only the basename as fileName by default.
2678        ///
2679        /// Use `--strip-path=false` when your runtime coverage reports bundle
2680        /// paths relative to the build directory, such as `assets/app.js`.
2681        /// When two maps share a basename, each of them is sent under its path
2682        /// in the build directory, so that no map replaces another.
2683        #[arg(long, value_name = "BOOL", default_value_t = true, action = clap::ArgAction::Set)]
2684        strip_path: bool,
2685
2686        /// Print what would be uploaded and exit. No network call.
2687        #[arg(long)]
2688        dry_run: bool,
2689
2690        /// Parallel upload fanout.
2691        #[arg(long, value_name = "N", default_value_t = 4)]
2692        concurrency: usize,
2693
2694        /// Stop on first upload error.
2695        #[arg(long)]
2696        fail_fast: bool,
2697    },
2698    /// Upload static dead-code findings to Fallow Cloud for the source-evidence viewer.
2699    ///
2700    /// Runs fallow's static analysis and uploads the `unused_export` and
2701    /// `dead_file` verdicts under the selected repo + git SHA. The cloud
2702    /// overlays them on the source view alongside the runtime coverage overlay.
2703    /// Findings are replace-by-SHA: each run sends the complete set for the SHA.
2704    UploadStaticFindings {
2705        /// Fallow Cloud API key (bearer token).
2706        ///
2707        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2708        /// <https://fallow.cloud/settings#api-keys>. This must be a live API
2709        /// key, not a publishable ingest key.
2710        ///
2711        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2712        /// secret on the command line may be visible to other processes via
2713        /// `ps` and can leak into shell history or process audit logs.
2714        #[arg(long, value_name = "KEY")]
2715        api_key: Option<String>,
2716
2717        /// Override the Fallow Cloud base URL.
2718        ///
2719        /// Useful for staging and on-premise deployments. Also respects
2720        /// $FALLOW_API_URL when this flag is not set.
2721        #[arg(long, value_name = "URL")]
2722        api_endpoint: Option<String>,
2723
2724        /// Project identifier, for example `fallow-cloud-api` or `owner/repo`.
2725        ///
2726        /// Defaults to $GITHUB_REPOSITORY, then $CI_PROJECT_PATH, then the
2727        /// parsed origin URL from `git remote get-url origin`.
2728        #[arg(long, value_name = "PROJECT_ID")]
2729        project_id: Option<String>,
2730
2731        /// Explicit git SHA for these findings.
2732        ///
2733        /// Default: `git rev-parse HEAD`. Findings are keyed on this value and
2734        /// fully replace any prior set uploaded for the same SHA.
2735        #[arg(long, value_name = "SHA")]
2736        git_sha: Option<String>,
2737
2738        /// Proceed even when the working tree has uncommitted changes.
2739        ///
2740        /// Warning: findings are generated from the working copy, so they may
2741        /// not match the uploaded git SHA. Commit or stash first if you want a
2742        /// SHA-exact upload.
2743        #[arg(long)]
2744        allow_dirty: bool,
2745
2746        /// Print what would be uploaded and exit. No network call.
2747        #[arg(long)]
2748        dry_run: bool,
2749
2750        /// Treat transient upload failures as warnings instead of errors
2751        /// (exit 0). Validation and auth errors still fail hard; this only
2752        /// downgrades transport and server errors.
2753        #[arg(long)]
2754        ignore_upload_errors: bool,
2755    },
2756    /// Read the production facts of changed code from Fallow Cloud.
2757    ///
2758    /// Sends the changed files or functions to the cloud review-packet
2759    /// endpoint and prints the answer as JSON: per function the production
2760    /// calls, the tracking state and the evidence window. It runs no local
2761    /// analysis and pulls no full runtime context, so the answer stays small.
2762    /// Without --file and --function, the files changed against the base are
2763    /// sent. The base resolves like `fallow audit`: `--base` (alias of
2764    /// `--changed-since`), then $FALLOW_AUDIT_BASE, then the merge-base with
2765    /// the upstream or the remote default branch.
2766    ReviewPacket {
2767        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2768        #[arg(long, value_name = "KEY")]
2769        api_key: Option<String>,
2770
2771        /// Override the Fallow Cloud base URL.
2772        #[arg(long, value_name = "URL")]
2773        api_endpoint: Option<String>,
2774
2775        /// Repository identifier, for example `owner/repo`.
2776        ///
2777        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2778        /// `git remote get-url origin`.
2779        #[arg(long, value_name = "OWNER/REPO")]
2780        repo: Option<String>,
2781
2782        /// Optional monorepo/project disambiguator.
2783        #[arg(long, value_name = "ID")]
2784        project_id: Option<String>,
2785
2786        /// Runtime observation window (1..=90 days). The cloud default is 30.
2787        #[arg(long, value_name = "DAYS")]
2788        coverage_period: Option<u16>,
2789
2790        /// Optional commit SHA of the deployment to read.
2791        #[arg(long, value_name = "SHA")]
2792        commit_sha: Option<String>,
2793
2794        /// Repo-relative file to include. Repeatable.
2795        #[arg(long = "file", value_name = "PATH")]
2796        files: Vec<String>,
2797
2798        /// Function to include, as FILE:NAME or FILE:NAME:LINE. Repeatable.
2799        #[arg(long = "function", value_name = "FILE:NAME[:LINE]")]
2800        functions: Vec<String>,
2801    },
2802    /// Read how production behavior changed between two deployments.
2803    ///
2804    /// Prints the cloud deployment change report as JSON: per function the
2805    /// change kind (new_called, new_not_called, heated_up, cooled_down,
2806    /// stopped, unchanged) between the deployment and its base. Pass the
2807    /// base deployment commit with `--base <sha>`; without it, the cloud
2808    /// picks the previous deployment with production runtime.
2809    DeploymentChanges {
2810        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2811        #[arg(long, value_name = "KEY")]
2812        api_key: Option<String>,
2813
2814        /// Override the Fallow Cloud base URL.
2815        #[arg(long, value_name = "URL")]
2816        api_endpoint: Option<String>,
2817
2818        /// Repository identifier, for example `owner/repo`.
2819        ///
2820        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2821        /// `git remote get-url origin`.
2822        #[arg(long, value_name = "OWNER/REPO")]
2823        repo: Option<String>,
2824
2825        /// Commit SHA of the deployment. Default: `git rev-parse HEAD`.
2826        #[arg(long, value_name = "SHA")]
2827        sha: Option<String>,
2828
2829        /// Show only one change kind.
2830        #[arg(long, value_name = "KIND", value_parser = [
2831            "stopped", "new_not_called", "heated_up", "cooled_down", "new_called", "unchanged",
2832        ])]
2833        change: Option<String>,
2834
2835        /// Page size (1..=200).
2836        #[arg(long, value_name = "N")]
2837        limit: Option<u16>,
2838
2839        /// Cursor from `meta.cursor` of the previous page.
2840        #[arg(long, value_name = "CURSOR")]
2841        cursor: Option<String>,
2842    },
2843}
2844
2845#[derive(Subcommand)]
2846pub enum CiCli {
2847    /// Compute the provider action for a rendered sticky PR summary comment.
2848    PlanPrComment {
2849        /// Path to the rendered PR comment Markdown body.
2850        #[arg(long)]
2851        body: PathBuf,
2852
2853        /// Sticky marker id used in the rendered body.
2854        #[arg(long)]
2855        marker_id: String,
2856
2857        /// Treat the rendered body as a clean no-findings result.
2858        #[arg(long)]
2859        clean: bool,
2860
2861        /// Existing provider comment id, when a matching sticky comment exists.
2862        #[arg(long)]
2863        existing_comment_id: Option<String>,
2864
2865        /// Path to the existing provider comment body. Enables unchanged-skip planning.
2866        #[arg(long)]
2867        existing_body: Option<PathBuf>,
2868    },
2869
2870    /// Post, update, or skip a rendered sticky PR summary comment.
2871    PostPrComment {
2872        /// Provider whose PR comment is being posted.
2873        #[arg(long, value_enum)]
2874        provider: CiProviderArg,
2875
2876        /// Pull request number (GitHub).
2877        #[arg(long)]
2878        pr: Option<String>,
2879
2880        /// Merge request IID (GitLab).
2881        #[arg(long)]
2882        mr: Option<String>,
2883
2884        /// Path to the rendered PR comment Markdown body.
2885        #[arg(long)]
2886        body: PathBuf,
2887
2888        /// Path to the typed PR comment envelope JSON, when available.
2889        #[arg(long)]
2890        envelope: Option<PathBuf>,
2891
2892        /// Sticky marker id used in the rendered body.
2893        #[arg(long)]
2894        marker_id: String,
2895
2896        /// Treat the rendered body as a clean no-findings result.
2897        #[arg(long)]
2898        clean: bool,
2899
2900        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
2901        #[arg(long)]
2902        repo: Option<String>,
2903
2904        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
2905        #[arg(long = "project-id")]
2906        project_id: Option<String>,
2907
2908        /// Provider API base URL. Defaults to github.com.
2909        #[arg(long = "api-url")]
2910        api_url: Option<String>,
2911
2912        /// Compute the post plan without creating or updating the provider comment.
2913        #[arg(long)]
2914        dry_run: bool,
2915    },
2916
2917    /// Post a rendered review envelope as a provider review or summary comment.
2918    PostReview {
2919        /// Provider whose review envelope is being posted.
2920        #[arg(long, value_enum)]
2921        provider: CiProviderArg,
2922
2923        /// Pull request number (GitHub).
2924        #[arg(long)]
2925        pr: Option<String>,
2926
2927        /// Merge request IID (GitLab).
2928        #[arg(long)]
2929        mr: Option<String>,
2930
2931        /// Path to a review-github or review-gitlab JSON envelope.
2932        #[arg(long)]
2933        envelope: PathBuf,
2934
2935        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
2936        #[arg(long)]
2937        repo: Option<String>,
2938
2939        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
2940        #[arg(long = "project-id")]
2941        project_id: Option<String>,
2942
2943        /// Provider API base URL. Defaults to github.com or CI_API_V4_URL/gitlab.com.
2944        #[arg(long = "api-url")]
2945        api_url: Option<String>,
2946
2947        /// Compute the post plan without creating provider comments.
2948        #[arg(long)]
2949        dry_run: bool,
2950    },
2951
2952    /// Post a GitHub Check Run from a typed PR decision surface.
2953    PostCheckRun {
2954        /// Provider whose check run is being posted. Only GitHub is supported.
2955        #[arg(long, value_enum)]
2956        provider: CiProviderArg,
2957
2958        /// Path to a fallow-pr-decision JSON sidecar.
2959        #[arg(long)]
2960        decision: PathBuf,
2961
2962        /// GitHub repository in owner/name form.
2963        #[arg(long)]
2964        repo: String,
2965
2966        /// Head SHA the check run should attach to.
2967        #[arg(long = "head-sha")]
2968        head_sha: String,
2969
2970        /// Provider API base URL. Defaults to github.com.
2971        #[arg(long = "api-url")]
2972        api_url: Option<String>,
2973
2974        /// Post one check run per decision gate instead of one aggregate check.
2975        #[arg(long = "split-gates")]
2976        split_gates: bool,
2977
2978        /// Print the check run payload without posting it.
2979        #[arg(long)]
2980        dry_run: bool,
2981    },
2982
2983    /// Validate a rendered review envelope and compute a stable reconcile plan.
2984    ReconcileReview {
2985        /// Provider whose review envelope is being reconciled.
2986        #[arg(long, value_enum)]
2987        provider: CiProviderArg,
2988
2989        /// Pull request number (GitHub).
2990        #[arg(long)]
2991        pr: Option<String>,
2992
2993        /// Merge request IID (GitLab).
2994        #[arg(long)]
2995        mr: Option<String>,
2996
2997        /// Path to a review-github or review-gitlab JSON envelope.
2998        #[arg(long)]
2999        envelope: PathBuf,
3000
3001        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
3002        #[arg(long)]
3003        repo: Option<String>,
3004
3005        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
3006        #[arg(long = "project-id")]
3007        project_id: Option<String>,
3008
3009        /// Provider API base URL. Defaults to github.com or CI_API_V4_URL/gitlab.com.
3010        #[arg(long = "api-url")]
3011        api_url: Option<String>,
3012
3013        /// Compute the reconcile plan without posting resolution notes or resolving threads.
3014        #[arg(long)]
3015        dry_run: bool,
3016    },
3017}
3018
3019#[derive(Subcommand)]
3020pub enum RulePackCli {
3021    /// Scaffold a new rule pack file and wire it into the config
3022    Init {
3023        /// Pack name (default: the template name, or "team-policy")
3024        name: Option<String>,
3025
3026        /// Template: starter, ai-safe-repo, side-effect-free-domain, clean-architecture, next-app-router
3027        #[arg(long, default_value = "starter")]
3028        template: String,
3029
3030        /// Directory for the pack file, relative to the project root
3031        #[arg(long, default_value = "rule-packs")]
3032        dir: String,
3033
3034        /// Only write the pack file; do not modify the config
3035        #[arg(long)]
3036        no_config: bool,
3037    },
3038
3039    /// List configured rule packs and their rules
3040    List,
3041
3042    /// Evaluate a pack (or all configured packs) against this project and print matches
3043    Test {
3044        /// Path to a pack file to test in isolation (default: all configured packs)
3045        pack: Option<PathBuf>,
3046    },
3047
3048    /// Print the JSON Schema for rule pack files
3049    Schema,
3050}
3051
3052/// CLI mirror of [`fallow_engine::baseline::HealthBaselineMode`].
3053#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
3054pub enum BaselineModeArg {
3055    /// Match a saved health baseline per file and finding category.
3056    #[default]
3057    Count,
3058    /// Match a saved health baseline per function identity and finding
3059    /// category, so a hotspot that replaces another hotspot is reported.
3060    Identity,
3061}
3062
3063impl From<BaselineModeArg> for fallow_engine::baseline::HealthBaselineMode {
3064    fn from(value: BaselineModeArg) -> Self {
3065        match value {
3066            BaselineModeArg::Count => Self::Count,
3067            BaselineModeArg::Identity => Self::Identity,
3068        }
3069    }
3070}
3071
3072#[derive(Clone, Copy, Debug, clap::ValueEnum)]
3073pub enum CiProviderArg {
3074    Github,
3075    Gitlab,
3076}
3077
3078/// CLI mirror of [`fallow_config::TypeAwareRequire`].
3079#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3080pub enum TypeAwareRequireArg {
3081    /// Keep conservative findings and report semantic gaps.
3082    BestEffort,
3083    /// Fail the quality gate when a requested semantic query is incomplete.
3084    Complete,
3085}
3086
3087impl From<TypeAwareRequireArg> for fallow_config::TypeAwareRequire {
3088    fn from(value: TypeAwareRequireArg) -> Self {
3089        match value {
3090            TypeAwareRequireArg::BestEffort => Self::BestEffort,
3091            TypeAwareRequireArg::Complete => Self::Complete,
3092        }
3093    }
3094}
3095
3096/// Filter refactoring targets by effort level.
3097#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3098pub enum EffortFilter {
3099    Low,
3100    Medium,
3101    High,
3102}
3103
3104impl EffortFilter {
3105    /// Convert to the corresponding `EffortEstimate` for comparison.
3106    pub(crate) const fn to_estimate(self) -> fallow_output::EffortEstimate {
3107        match self {
3108            Self::Low => fallow_output::EffortEstimate::Low,
3109            Self::Medium => fallow_output::EffortEstimate::Medium,
3110            Self::High => fallow_output::EffortEstimate::High,
3111        }
3112    }
3113}
3114
3115/// CLI parser for the health severity gate.
3116#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3117pub enum HealthSeverityCli {
3118    Moderate,
3119    High,
3120    Critical,
3121}
3122
3123impl HealthSeverityCli {
3124    /// Convert to the typed health output severity.
3125    pub(crate) const fn to_health_severity(self) -> fallow_output::FindingSeverity {
3126        match self {
3127            Self::Moderate => fallow_output::FindingSeverity::Moderate,
3128            Self::High => fallow_output::FindingSeverity::High,
3129            Self::Critical => fallow_output::FindingSeverity::Critical,
3130        }
3131    }
3132}
3133
3134/// Privacy mode for author emails emitted by `--ownership`.
3135///
3136/// CLI mirror of [`fallow_config::EmailMode`]. Kept as a separate enum so
3137/// the help text controls rendering and we don't leak config-internal
3138/// schema details into clap.
3139#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3140pub enum EmailModeArg {
3141    /// Show full email addresses as recorded in git history.
3142    Raw,
3143    /// Show local-part only (default). Unwraps GitHub-style noreply prefixes.
3144    Handle,
3145    /// Show stable non-cryptographic pseudonyms (`xxh3:<hex>`).
3146    Anonymized,
3147    /// Legacy spelling for anonymized output.
3148    #[value(hide = true)]
3149    Hash,
3150}
3151
3152impl EmailModeArg {
3153    /// Convert to the equivalent config-level mode.
3154    pub(crate) const fn to_config(self) -> fallow_config::EmailMode {
3155        match self {
3156            Self::Raw => fallow_config::EmailMode::Raw,
3157            Self::Handle => fallow_config::EmailMode::Handle,
3158            Self::Anonymized => fallow_config::EmailMode::Anonymized,
3159            Self::Hash => fallow_config::EmailMode::Hash,
3160        }
3161    }
3162}
3163
3164/// CLI mirror of [`fallow_config::AuditGate`].
3165#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3166pub enum AuditGateArg {
3167    /// Only findings introduced by the current changeset affect the verdict.
3168    NewOnly,
3169    /// All findings in changed files affect the verdict.
3170    All,
3171}
3172
3173impl From<AuditGateArg> for fallow_config::AuditGate {
3174    fn from(value: AuditGateArg) -> Self {
3175        match value {
3176            AuditGateArg::NewOnly => Self::NewOnly,
3177            AuditGateArg::All => Self::All,
3178        }
3179    }
3180}
3181
3182/// Parse `--min-occurrences` and reject values below 2. A single occurrence
3183/// is not a duplicate; silently clamping would diverge from the config-file
3184/// validator, which also rejects `< 2`.
3185fn parse_min_occurrences(s: &str) -> Result<usize, String> {
3186    let value: usize = s
3187        .parse()
3188        .map_err(|_| format!("`{s}` is not a non-negative integer"))?;
3189    if value < 2 {
3190        return Err(format!(
3191            "must be at least 2 (got {value}); a single occurrence isn't a duplicate"
3192        ));
3193    }
3194    Ok(value)
3195}