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        /// How a worsened complexity finding affects the audit verdict.
1878        ///
1879        /// A worsened finding was already in the base snapshot, and a measured
1880        /// value is higher now. Any growth of cyclomatic, cognitive or CRAP
1881        /// counts. The line count must grow by at least 5 lines. It includes
1882        /// comments and blank lines in the function.
1883        /// warn (default): the verdict is at least warn, and the run does not
1884        /// fail. error: a worsened finding counts like an introduced finding.
1885        /// off: only growth of an exceeded metric stays worsened (warn).
1886        /// Overrides `audit.worsened` in config. Has no effect with
1887        /// `--gate all`, which counts every finding.
1888        #[arg(long, value_enum)]
1889        worsened: Option<AuditWorsenedArg>,
1890
1891        /// Runtime coverage input. Accepts a V8 directory, a single V8 JSON
1892        /// file, or an Istanbul coverage map JSON. Runs the `fallow-cov`
1893        /// sidecar inside the audit, so the `hot-path-touched` verdict shows
1894        /// next to the dead-code and complexity findings without a second
1895        /// `fallow health` run in CI. The verdict is informational and does
1896        /// not change the exit code. A single local capture is free.
1897        /// Continuous or multi-capture monitoring needs a license (see
1898        /// `fallow license`).
1899        #[arg(long, value_name = "PATH")]
1900        runtime_coverage: Option<PathBuf>,
1901
1902        /// Threshold for hot-path classification, forwarded to the sidecar
1903        /// when `--runtime-coverage` is set.
1904        #[arg(long, default_value_t = 100)]
1905        min_invocations_hot: u64,
1906
1907        /// Internal marker identifying a gate run (e.g. `pre-commit`), set by
1908        /// the generated git hook so Fallow Impact can record a containment
1909        /// event when the gate blocks then clears. Hidden; never changes the
1910        /// verdict, exit code, or output.
1911        #[arg(long, value_name = "MARKER", hide = true)]
1912        gate_marker: Option<String>,
1913
1914        /// Render the deterministic review brief instead of the gating audit
1915        /// report. The brief answers "where do I look?" rather than "will CI
1916        /// block this?", runs the same analysis, and ALWAYS exits 0 (the
1917        /// verdict is carried informationally). Implied by `fallow review`.
1918        /// Orthogonal to `--format`.
1919        #[arg(long)]
1920        brief: bool,
1921
1922        /// Cap on the number of consequential structural decisions surfaced in
1923        /// the review brief's decision surface (the working-memory limit).
1924        /// Default 4; clamped to the 3-5 band (4 plus or minus 1). Only
1925        /// consulted on the brief path.
1926        #[arg(
1927            long,
1928            value_name = "N",
1929            default_value_t = audit_decision_surface::DEFAULT_DECISION_CAP
1930        )]
1931        max_decisions: usize,
1932
1933        /// Emit the agent-contract WALKTHROUGH GUIDE: the current digest
1934        /// (brief + decision surface), the review direction, the JSON schema the
1935        /// agent must return, and a deterministic graph-snapshot hash pinned into
1936        /// the digest. The digest is built from the graph only (PR prose is never
1937        /// folded in, so it is injection-resistant). Implies the brief; always
1938        /// exits 0. A thin agent skill calls this to fetch the current guide,
1939        /// produces judgment JSON, then reopens with `--walkthrough-file`.
1940        #[arg(long, conflicts_with_all = ["walkthrough_file", "walkthrough"])]
1941        walkthrough_guide: bool,
1942
1943        /// Ingest an agent's judgment JSON and POST-VALIDATE it against the
1944        /// LIVE graph. Rejects any judgment whose `signal_id` fallow did not emit
1945        /// (anti-hallucination); refuses the whole payload as stale when the
1946        /// echoed graph-snapshot hash no longer matches (the tree moved); rejects
1947        /// an `action` outside `block`, `address`, `consider`, `fyi`
1948        /// (`invalid-action`). The verifier is the graph, not a second model.
1949        /// Implies the brief; always exits 0. The agent's free-text framing and
1950        /// action label are fenced as non-deterministic and never gate or
1951        /// auto-post.
1952        #[arg(long, value_name = "PATH")]
1953        walkthrough_file: Option<PathBuf>,
1954
1955        /// Render the existing walkthrough guide as a staged HUMAN terminal tour
1956        /// (Stage 1 load-bearing / Stage 2 mechanical), or markdown with
1957        /// `--format markdown`. Implies the brief; always exits 0.
1958        /// `--format json --walkthrough` emits the same agent-contract JSON as
1959        /// `--walkthrough-guide`.
1960        #[arg(long, conflicts_with_all = ["walkthrough_guide", "walkthrough_file"])]
1961        walkthrough: bool,
1962
1963        /// Record one or more changed files as VIEWED in the local walkthrough
1964        /// viewed-state ledger (`.fallow/walkthrough-state.json`), then render the
1965        /// tour. Files already viewed (and still current) collapse into the
1966        /// Cleared panel. Repeatable. Stale marks (the tree moved) are ignored on
1967        /// render but never deleted. Only consulted on the `--walkthrough` path.
1968        #[arg(long, value_name = "PATH")]
1969        mark_viewed: Vec<PathBuf>,
1970
1971        /// Expand the Cleared panel in the human/markdown walkthrough tour: list
1972        /// each de-prioritized and already-viewed file instead of the collapsed
1973        /// one-line summary. Only consulted on the `--walkthrough` path.
1974        #[arg(long)]
1975        show_cleared: bool,
1976
1977        /// Expand the de-prioritized units in the review brief's weighted
1978        /// focus map ("show me what you de-prioritized"). The `deprioritized`
1979        /// escape-hatch list is ALWAYS present in `--format json` regardless; this
1980        /// flag only re-expands the collapse-by-default human focus render. Only
1981        /// consulted on the `--walkthrough` path.
1982        #[arg(long)]
1983        show_deprioritized: bool,
1984
1985        /// Scope reported findings to this file or directory (default: whole project).
1986        /// The full project graph is still built; only reported items are narrowed.
1987        #[arg(value_name = "PATH")]
1988        path: Option<PathBuf>,
1989    },
1990
1991    /// Maintain reusable audit base-snapshot caches.
1992    AuditCache {
1993        #[command(subcommand)]
1994        subcommand: AuditCacheCli,
1995    },
1996
1997    /// Maintain the committed baseline files that the config points at.
1998    Baselines {
1999        #[command(subcommand)]
2000        subcommand: BaselinesCli,
2001    },
2002
2003    /// Surface the consequential structural DECISIONS a change embeds (the apex
2004    /// of the review brief), each framed as a judgment question with the routed
2005    /// expert to ask.
2006    ///
2007    /// The product's decision surface: a ranked, capped (4 plus or minus 1),
2008    /// signal_id-anchored set of the SOLID-3 decisions (coupling/boundary,
2009    /// exports-aware public-API/contract, dependency). Runs the same changed-code
2010    /// analysis as `fallow review` but emits ONLY the decisions, separable and
2011    /// cheap. Coupling and public-API decisions are suppressible with
2012    /// `// fallow-ignore`; a dependency decision anchors on `package.json` and
2013    /// has no suppress action. Never gates: exits 0 after a successful run, and
2014    /// exits 2 on invalid input (for example the global `--baseline` or
2015    /// `--save-baseline` flag) like every other command. Use `--base` /
2016    /// `--changed-since` to pick the comparison point, exactly like `fallow audit`.
2017    DecisionSurface {
2018        /// Cap on the number of surfaced decisions (the working-memory limit).
2019        /// Default 4; clamped to the 3-5 band (4 plus or minus 1).
2020        #[arg(
2021            long,
2022            value_name = "N",
2023            default_value_t = audit_decision_surface::DEFAULT_DECISION_CAP
2024        )]
2025        max_decisions: usize,
2026    },
2027
2028    /// Show what fallow has done for you: how many issues it is surfacing, the
2029    /// trend since the last recorded run, and how many commits it contained at
2030    /// the pre-commit gate.
2031    ///
2032    /// Local-only and opt-in: enable per project with `fallow impact enable`, or
2033    /// turn it on everywhere with `fallow impact default on`, then let your
2034    /// `fallow audit` / pre-commit gate runs build history. History is stored in
2035    /// your user config dir (never written into the repo) and forced off in CI.
2036    /// Impact never uploads anything and never affects exit codes.
2037    Impact {
2038        #[command(subcommand)]
2039        subcommand: Option<ImpactCli>,
2040        /// Aggregate every tracked project into one cross-repo roll-up
2041        /// ("what has fallow done for me across all my repos"). Reads the
2042        /// user config dir; ignores `--root`. Cannot combine with a subcommand.
2043        #[arg(long)]
2044        all: bool,
2045        /// Row ordering for `--all` (default: most recently recorded first).
2046        #[arg(long, value_enum, default_value_t = ImpactSortCli::Recent)]
2047        sort: ImpactSortCli,
2048        /// Cap the number of `--all` rows printed (grand totals still reflect
2049        /// every tracked project).
2050        #[arg(long)]
2051        limit: Option<usize>,
2052    },
2053
2054    /// Surface local security candidates for downstream agent verification (opt-in).
2055    ///
2056    /// Ships three complementary surfaces. (1) The graph-structural
2057    /// `client-server-leak` rule: a `"use client"` file that transitively imports
2058    /// a module reading a non-public env secret through `process.env` or
2059    /// `import.meta.env`. (2) The data-driven
2060    /// `tainted-sink` catalogue: syntactic sink sites matched against a CWE
2061    /// catalogue (`security_matchers.toml`) spanning categories such as
2062    /// dangerous-html, template-escape-bypass, command-injection, code-injection,
2063    /// dynamic-regex, redos-regex, resource-amplification, dynamic-module-load,
2064    /// sql-injection, ssrf, path-traversal, header-injection, open-redirect,
2065    /// cleartext-transport, electron-unsafe-webpreferences,
2066    /// world-writable-permission, insecure-temp-file,
2067    /// mysql-multiple-statements, mass-assignment, weak-crypto,
2068    /// deprecated-cipher, insecure-randomness,
2069    /// unsafe-buffer-alloc, unsafe-deserialization, prototype-pollution,
2070    /// zip-slip, nosql-injection, ssti, xxe, xpath-injection, and
2071    /// webview-injection. (3) `hardcoded-secret`,
2072    /// an include-required
2073    /// category for provider-prefix literals and high-entropy literals assigned
2074    /// to secret-shaped identifiers. It never runs from raw entropy alone. All
2075    /// findings are CANDIDATES for verification, NOT verified vulnerabilities.
2076    /// This command is the only
2077    /// surface for security findings; they never appear under bare `fallow` or
2078    /// the `audit` gate. Build-config and test files are excluded, and public
2079    /// env prefixes such as `NEXT_PUBLIC_` and `VITE_` are treated as public.
2080    /// Honors
2081    /// `--root`, `--format {human,json,sarif}`, `--changed-since`, `--file`, `--gate`, `--diff-file`,
2082    /// `--diff-stdin`, `--workspace`, `--changed-workspaces`, `--ci`,
2083    /// `--fail-on-issues`, `--sarif-file`, `--summary`, `--explain`, and `--surface`.
2084    Security {
2085        #[command(subcommand)]
2086        subcommand: Option<SecuritySubcommand>,
2087        /// Runtime coverage input. Accepts a V8 directory, a single V8 JSON
2088        /// file, or an Istanbul coverage map JSON. When set, `fallow security`
2089        /// adds production runtime state to tainted-sink candidates and uses
2090        /// that state as an extra ranking signal. A single local capture is
2091        /// free. Continuous or multi-capture monitoring needs a license (see
2092        /// `fallow license`).
2093        #[arg(long, value_name = "PATH")]
2094        runtime_coverage: Option<PathBuf>,
2095        /// Threshold for hot-path classification, forwarded to the sidecar
2096        /// when `--runtime-coverage` is set.
2097        #[arg(long, default_value_t = 100)]
2098        min_invocations_hot: u64,
2099        /// Only report security candidates in or reachable from the specified files.
2100        /// The full project graph is still built, but output is scoped to matching
2101        /// finding anchors or trace hops. Accepts multiple values.
2102        #[arg(long, value_name = "PATH")]
2103        file: Vec<std::path::PathBuf>,
2104        /// Opt-in regression gate: fail (exit 8) only when the change introduces a
2105        /// NEW security-sink candidate in the changed lines, not on the whole
2106        /// candidate backlog. Requires a diff source: `--changed-since <ref>`,
2107        /// `--diff-file <path>`, or `--diff-stdin`. There is deliberately no `all`
2108        /// mode (gating on the full backlog is the anti-feature this gate avoids).
2109        #[arg(long, value_name = "MODE")]
2110        gate: Option<security::SecurityGateArg>,
2111        /// Include the agent-facing attack-surface inventory in JSON output.
2112        #[arg(long)]
2113        surface: bool,
2114
2115        /// Scope reported findings to this file or directory (default: whole project).
2116        /// The full project graph is still built; only reported items are narrowed.
2117        #[arg(value_name = "PATH")]
2118        path: Option<PathBuf>,
2119    },
2120
2121    /// Render a saved `--format json` results file in another format without
2122    /// re-running analysis (analyze once, then render every CI surface from
2123    /// the same file). Supports GitHub annotations/summary, CodeClimate,
2124    /// SARIF, markdown, and GitHub/GitLab PR-comment and review formats.
2125    Report {
2126        /// Path to a fallow JSON results file produced by `--format json`
2127        /// (dead-code, architecture, dupes, health, audit, security, or bare
2128        /// combined).
2129        #[arg(long, value_name = "PATH")]
2130        from: PathBuf,
2131    },
2132    /// 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
2133    Schema,
2134
2135    /// Print or vendor CI integration templates.
2136    ///
2137    /// Use `fallow ci-template gitlab` to print the GitLab CI template, or
2138    /// `fallow ci-template gitlab --vendor` to write the template plus the
2139    /// bash helper files that enable MR comments without downloading from
2140    /// raw.githubusercontent.com at pipeline runtime.
2141    CiTemplate {
2142        #[command(subcommand)]
2143        subcommand: CiTemplateCli,
2144    },
2145
2146    /// Migrate configuration from knip, jscpd, or stylelint to fallow
2147    Migrate {
2148        /// Generate `fallow.toml` instead of JSONC
2149        #[arg(long, conflicts_with = "jsonc")]
2150        toml: bool,
2151
2152        /// Write JSONC content to `.fallowrc.jsonc` instead of `.fallowrc.json`. The
2153        /// generated content is the same JSONC (with `//` comments) either way; the
2154        /// `.jsonc` extension lets editors auto-detect JSON-with-comments syntax
2155        /// highlighting and silences linters that flag comments in `.json`. Without
2156        /// `--jsonc` or `--toml`, fallow auto-mirrors the source extension: a
2157        /// `knip.jsonc` migration writes `.fallowrc.jsonc`, a `knip.json` migration
2158        /// writes `.fallowrc.json`.
2159        #[arg(long)]
2160        jsonc: bool,
2161
2162        /// Only preview the generated config without writing
2163        #[arg(long)]
2164        dry_run: bool,
2165
2166        /// Path to source config file (auto-detect if not specified)
2167        #[arg(long, value_name = "PATH")]
2168        from: Option<PathBuf>,
2169    },
2170
2171    /// Manage the license for continuous/cloud runtime monitoring.
2172    ///
2173    /// Verification is offline against an Ed25519 public key compiled into
2174    /// the binary. The license file lives at `~/.fallow/license.jwt` (or
2175    /// `$FALLOW_LICENSE_PATH`); `$FALLOW_LICENSE` env var takes precedence
2176    /// and is the recommended path for shared CI runners.
2177    License {
2178        #[command(subcommand)]
2179        subcommand: LicenseCli,
2180    },
2181
2182    /// Manage opt-in product telemetry.
2183    ///
2184    /// Telemetry is off by default. It never collects repository names, paths,
2185    /// package names, source code, config values, raw errors, or raw agent
2186    /// detection evidence. Use `fallow telemetry inspect --example` to see the
2187    /// documented payload shape, or prefix a real command with
2188    /// `FALLOW_TELEMETRY=inspect` to print the exact payload without sending.
2189    Telemetry {
2190        #[command(subcommand)]
2191        subcommand: TelemetryCli,
2192    },
2193
2194    /// Runtime coverage workflow.
2195    ///
2196    /// `setup` is the resumable first-run flow: license check, sidecar
2197    /// install, coverage recipe, then analysis.
2198    Coverage {
2199        #[command(subcommand)]
2200        subcommand: CoverageCli,
2201    },
2202
2203    /// Install or remove a Claude Code PreToolUse hook that gates
2204    /// `git commit` / `git push` on `fallow audit`, so the agent cleans
2205    /// findings before the command runs.
2206    ///
2207    /// Deprecated: use `fallow agent install` (one pass for every harness)
2208    /// or `fallow hooks install --target agent` (the gate alone). This
2209    /// command keeps working throughout fallow 3 and is removed in the next
2210    /// major. It writes into `.claude/settings.json` +
2211    /// `.claude/hooks/fallow-gate.sh`, or for Codex into `.codex/hooks.json` +
2212    /// `.codex/hooks/fallow-gate.sh` plus an `AGENTS.md` routing block. For a
2213    /// shell-level Git pre-commit hook in
2214    /// `.git/hooks/`, see `fallow hooks install --target git` instead.
2215    SetupHooks {
2216        /// Target a specific agent surface (default: auto-detect).
2217        #[arg(long, value_enum)]
2218        agent: Option<setup_hooks::HookAgentArg>,
2219
2220        /// Print what would be written or removed without touching the filesystem.
2221        #[arg(long)]
2222        dry_run: bool,
2223
2224        /// Overwrite a user-edited hook script, invalid settings.json, or
2225        /// remove a user-edited script during uninstall.
2226        #[arg(long)]
2227        force: bool,
2228
2229        /// Write to the user's home directory instead of the project root.
2230        #[arg(long)]
2231        user: bool,
2232
2233        /// Append `.claude/` to the project's `.gitignore`.
2234        #[arg(long)]
2235        gitignore_claude: bool,
2236
2237        /// Remove the fallow-gate handler, hook script, and AGENTS.md
2238        /// managed block instead of installing them. Idempotent: reports
2239        /// "unchanged" when nothing to remove.
2240        #[arg(long)]
2241        uninstall: bool,
2242    },
2243
2244    /// Generate an interactive HTML map of the codebase
2245    Viz {
2246        /// Output file path (default: fallow-viz.html in project root)
2247        #[arg(long = "out", value_name = "PATH")]
2248        output: Option<PathBuf>,
2249
2250        /// Don't open the output file in the browser
2251        #[arg(long)]
2252        no_open: bool,
2253
2254        /// Visualization output format
2255        #[arg(long = "viz-format", default_value = "html")]
2256        viz_format: viz::VizFormat,
2257    },
2258}
2259
2260#[derive(Subcommand)]
2261pub enum SecuritySubcommand {
2262    /// Render verifier-retained survivor candidates from fallow output plus verifier verdicts.
2263    Survivors {
2264        /// Raw `fallow security --format json` candidate output.
2265        #[arg(long, value_name = "PATH")]
2266        candidates: PathBuf,
2267        /// Verifier verdict JSON file.
2268        #[arg(long, value_name = "PATH")]
2269        verdicts: PathBuf,
2270        /// Fail when any candidate has no matching verdict.
2271        #[arg(long)]
2272        require_verdict_for_each_candidate: bool,
2273    },
2274    /// Group unresolved security callees into actionable blind-spot output.
2275    #[command(name = "blind-spots")]
2276    BlindSpots {
2277        /// Scope diagnostics to selected files.
2278        #[arg(long, value_name = "PATH")]
2279        file: Vec<PathBuf>,
2280    },
2281}
2282
2283#[derive(clap::Subcommand)]
2284pub enum BaselinesCli {
2285    /// Remove the baseline entries that match no current finding.
2286    ///
2287    /// Reads the files that `audit.deadCodeBaseline`, `audit.healthBaseline`
2288    /// and `audit.dupesBaseline` name, runs one whole-project analysis, and
2289    /// removes each entry that `--baseline` would no longer match. Prune never
2290    /// adds an entry: a new finding stays outside the baseline, and the normal
2291    /// gates report it. To record new findings on purpose, use
2292    /// `fallow <dead-code|health|dupes> --save-baseline`.
2293    ///
2294    /// Exit codes: 0 when the files are pruned or have nothing to prune, 1 with
2295    /// `--check` when an entry can be pruned, and 2 when a file cannot be read
2296    /// or pruned. A file with an older key form is skipped with the command
2297    /// that saves it again.
2298    Prune {
2299        /// Report the entries that a prune would remove, and write nothing.
2300        #[arg(long)]
2301        check: bool,
2302
2303        /// The dead-code baseline to prune. Overrides `audit.deadCodeBaseline`.
2304        #[arg(long, value_name = "PATH")]
2305        dead_code_baseline: Option<PathBuf>,
2306
2307        /// The health baseline to prune. Overrides `audit.healthBaseline`.
2308        #[arg(long, value_name = "PATH")]
2309        health_baseline: Option<PathBuf>,
2310
2311        /// The duplication baseline to prune. Overrides `audit.dupesBaseline`.
2312        #[arg(long, value_name = "PATH")]
2313        dupes_baseline: Option<PathBuf>,
2314
2315        /// The coverage input for CRAP scores, as for `fallow health
2316        /// --coverage`. Use the input that the health baseline was saved with.
2317        #[arg(long, value_name = "PATH")]
2318        coverage: Option<PathBuf>,
2319
2320        /// Rebase the file paths of the coverage input onto this root.
2321        #[arg(long, value_name = "PATH")]
2322        coverage_root: Option<PathBuf>,
2323    },
2324}
2325
2326#[derive(clap::Subcommand)]
2327pub enum AuditCacheCli {
2328    /// Remove reusable audit caches owned by an explicit project root.
2329    ///
2330    /// Deletes this project's cache entries unconditionally, warm or not. To
2331    /// apply the age-based GC policy across every cache entry instead, use
2332    /// `fallow audit-cache prune`.
2333    Remove {
2334        /// Print what would be removed without touching the filesystem.
2335        #[arg(long)]
2336        dry_run: bool,
2337
2338        /// Confirm removal in non-interactive environments.
2339        #[arg(long, alias = "force")]
2340        yes: bool,
2341    },
2342
2343    /// Apply the audit cache GC policy now and report every entry.
2344    ///
2345    /// Runs the same reclaim policy every `fallow audit` run already applies
2346    /// silently: orphaned-sidecar cleanup, age-based reclaim under the
2347    /// resolved threshold, and cross-repo reclaim of abandoned entries whose
2348    /// recorded owner root no longer exists. It also bounds the base-snapshot
2349    /// files in the project cache directory (`pass: "snapshot"`): files older
2350    /// than the threshold go, and only the newest 32 stay. Entries owned by
2351    /// other live projects are never touched. Use `--dry-run` to preview every decision
2352    /// the policy would take without touching the filesystem. `--root` is
2353    /// optional and defaults to the current directory. Reported sizes come
2354    /// from a full recursive walk of each cache entry, which can take a few
2355    /// seconds on large caches. To delete one project's caches
2356    /// unconditionally, use `fallow audit-cache remove --root <path> --yes`.
2357    Prune {
2358        /// Preview decisions without touching the filesystem.
2359        #[arg(long)]
2360        dry_run: bool,
2361
2362        /// Age threshold in days for this invocation. Overrides
2363        /// FALLOW_AUDIT_CACHE_MAX_AGE_DAYS and the `audit.cacheMaxAgeDays`
2364        /// config field (default 30). `0` disables age-based reclaim but
2365        /// still reclaims orphaned sidecars and entries whose recorded owner
2366        /// root is gone; unconditional deletion of one project's caches is
2367        /// `fallow audit-cache remove --root <path> --yes`.
2368        #[arg(long, value_name = "N")]
2369        max_age_days: Option<u32>,
2370    },
2371}
2372
2373#[derive(clap::Subcommand)]
2374pub enum LicenseCli {
2375    /// Activate a license JWT.
2376    ///
2377    /// JWT input precedence: positional arg > `--from-file` > stdin (`-`).
2378    /// All paths normalize whitespace before crypto verification.
2379    Activate {
2380        /// JWT as a positional argument.
2381        #[arg(value_name = "JWT")]
2382        jwt: Option<String>,
2383
2384        /// Path to a file containing the JWT.
2385        #[arg(long, value_name = "PATH")]
2386        from_file: Option<PathBuf>,
2387
2388        /// Read JWT from stdin.
2389        #[arg(long, conflicts_with_all = ["jwt", "from_file"])]
2390        stdin: bool,
2391
2392        /// Start a 30-day email-gated trial in one step.
2393        ///
2394        /// The trial endpoint is rate-limited to 5 requests per hour per IP.
2395        /// In CI or behind a shared NAT, start the trial from a developer
2396        /// machine and set FALLOW_LICENSE (or FALLOW_LICENSE_PATH) on the
2397        /// runner instead of re-running `activate --trial` per job.
2398        #[arg(long, requires = "email")]
2399        trial: bool,
2400
2401        /// Email address for the trial flow.
2402        #[arg(long, value_name = "ADDR")]
2403        email: Option<String>,
2404    },
2405    /// Show the active license tier, seats, features, and days remaining.
2406    Status,
2407    /// Fetch a fresh JWT from `api.fallow.cloud` (network-only).
2408    ///
2409    /// The stored license JWT is the primary identity proof. When it is
2410    /// missing, or the cloud reports it as too stale to exchange, the request
2411    /// is retried with a full-access API key.
2412    Refresh {
2413        /// Fallow Cloud API key (bearer token) used when the stored license
2414        /// JWT cannot be refreshed.
2415        ///
2416        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2417        /// <https://fallow.cloud/settings#api-keys>.
2418        ///
2419        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2420        /// secret on the command line may be visible to other processes via
2421        /// `ps` and can leak into shell history or process audit logs.
2422        #[arg(long, value_name = "KEY")]
2423        api_key: Option<String>,
2424    },
2425    /// Remove the local license file.
2426    Deactivate,
2427}
2428
2429#[derive(Clone, Copy, clap::Subcommand)]
2430pub enum TelemetryCli {
2431    /// Show effective telemetry state, precedence, and controls.
2432    Status,
2433    /// Enable opt-in telemetry in the user-level fallow config.
2434    Enable,
2435    /// Disable telemetry in the user-level fallow config.
2436    Disable,
2437    /// Explain inspect mode or print example payloads.
2438    Inspect {
2439        /// Print documented example payloads and field purposes.
2440        #[arg(long)]
2441        example: bool,
2442    },
2443}
2444
2445#[derive(clap::Subcommand)]
2446pub enum CiTemplateCli {
2447    /// Print or vendor the GitLab CI template and MR integration helpers.
2448    Gitlab {
2449        /// Write ci/ and action/ helper files under DIR instead of printing the template.
2450        ///
2451        /// Passing --vendor without a DIR writes into the current directory.
2452        #[arg(long, value_name = "DIR", num_args = 0..=1, default_missing_value = ".")]
2453        vendor: Option<PathBuf>,
2454
2455        /// Overwrite existing files that differ from the bundled template.
2456        #[arg(long)]
2457        force: bool,
2458    },
2459}
2460
2461#[derive(clap::Subcommand)]
2462pub enum CoverageCli {
2463    /// Resumable first-run setup: license + sidecar + recipe + analysis.
2464    Setup {
2465        /// Accept all prompts automatically.
2466        #[arg(short = 'y', long)]
2467        yes: bool,
2468
2469        /// Print instructions instead of prompting.
2470        #[arg(long)]
2471        non_interactive: bool,
2472
2473        /// Emit deterministic setup instructions as JSON. Implies --non-interactive.
2474        #[arg(long)]
2475        json: bool,
2476    },
2477    /// Analyze runtime coverage from a local artifact or explicit cloud source.
2478    ///
2479    /// Cloud mode is opt-in only. `FALLOW_API_KEY` by itself never selects
2480    /// cloud mode; pass `--cloud` / `--runtime-coverage-cloud`, or set
2481    /// `FALLOW_RUNTIME_COVERAGE_SOURCE=cloud`.
2482    Analyze {
2483        /// File or directory containing local runtime coverage input. A
2484        /// single local capture is free. Continuous or multi-capture
2485        /// monitoring needs a license (see `fallow license`).
2486        #[arg(long, value_name = "PATH", conflicts_with = "cloud")]
2487        runtime_coverage: Option<PathBuf>,
2488
2489        /// Fetch latest runtime facts from Fallow Cloud for the selected repo.
2490        #[arg(long, visible_alias = "runtime-coverage-cloud")]
2491        cloud: bool,
2492
2493        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2494        #[arg(long, value_name = "KEY")]
2495        api_key: Option<String>,
2496
2497        /// Override the Fallow Cloud base URL.
2498        #[arg(long, value_name = "URL")]
2499        api_endpoint: Option<String>,
2500
2501        /// Repository identifier, for example `owner/repo`.
2502        ///
2503        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2504        /// `git remote get-url origin`. Slashes are percent-encoded as one
2505        /// URL segment when calling the cloud runtime-context endpoint.
2506        #[arg(long, value_name = "OWNER/REPO")]
2507        repo: Option<String>,
2508
2509        /// Optional monorepo/project disambiguator.
2510        #[arg(long, value_name = "ID")]
2511        project_id: Option<String>,
2512
2513        /// Runtime observation window to request from cloud (1..=90 days).
2514        #[arg(long, value_name = "DAYS", default_value_t = 30)]
2515        coverage_period: u16,
2516
2517        /// Optional runtime environment filter.
2518        #[arg(long, value_name = "ENV")]
2519        environment: Option<String>,
2520
2521        /// Optional commit SHA filter for cloud runtime facts.
2522        #[arg(long, value_name = "SHA")]
2523        commit_sha: Option<String>,
2524
2525        /// Analyze production code only.
2526        #[arg(long)]
2527        production: bool,
2528
2529        /// Threshold for hot-path classification.
2530        #[arg(long, default_value_t = 100)]
2531        min_invocations_hot: u64,
2532
2533        /// Minimum total trace volume before high-confidence verdicts.
2534        #[arg(long, value_name = "N")]
2535        min_observation_volume: Option<u32>,
2536
2537        /// Fraction of total trace count below which an invoked function is low traffic.
2538        #[arg(long, value_name = "RATIO")]
2539        low_traffic_threshold: Option<f64>,
2540
2541        /// Show only the top N runtime findings and hot paths.
2542        #[arg(long)]
2543        top: Option<usize>,
2544
2545        /// Show the first-class blast-radius section in human output.
2546        #[arg(long)]
2547        blast_radius: bool,
2548
2549        /// Show the first-class importance section in human output.
2550        #[arg(long)]
2551        importance: bool,
2552
2553        /// List cloud runtime functions with no local counterpart on stderr.
2554        #[arg(long)]
2555        debug_unmatched: bool,
2556    },
2557    /// Upload a static function inventory to Fallow Cloud. Needs a fallow
2558    /// cloud API key. Unlocks the `untracked` filter on the dashboard by
2559    /// pairing runtime coverage data with the AST view of "every function
2560    /// that exists". See <https://fallow.tools/docs/analysis/runtime-coverage/>.
2561    ///
2562    /// This command makes network calls to Fallow Cloud. `fallow dead-code`
2563    /// stays offline.
2564    ///
2565    /// Exit codes: 0 ok · 7 network · 10 validation · 11 payload too large
2566    /// · 12 auth rejected · 13 server error.
2567    UploadInventory {
2568        /// Fallow Cloud API key (bearer token).
2569        ///
2570        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2571        /// <https://fallow.cloud/settings#api-keys>.
2572        ///
2573        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2574        /// secret on the command line may be visible to other processes via
2575        /// `ps` and can leak into shell history or process audit logs.
2576        #[arg(long, value_name = "KEY")]
2577        api_key: Option<String>,
2578
2579        /// Override the Fallow Cloud base URL.
2580        ///
2581        /// Useful for staging and on-premise deployments. Also respects
2582        /// $FALLOW_API_URL when this flag is not set.
2583        #[arg(long, value_name = "URL")]
2584        api_endpoint: Option<String>,
2585
2586        /// Project identifier, for example `fallow-cloud-api` or `owner/repo`.
2587        ///
2588        /// Defaults to $GITHUB_REPOSITORY, then $CI_PROJECT_PATH, then the
2589        /// parsed origin URL from `git remote get-url origin`.
2590        #[arg(long, value_name = "PROJECT_ID")]
2591        project_id: Option<String>,
2592
2593        /// Explicit git SHA for this inventory.
2594        ///
2595        /// Default: `git rev-parse HEAD`. The inventory is keyed on this
2596        /// value; the cloud back-fills hourly buckets with a matching SHA.
2597        #[arg(long, value_name = "SHA")]
2598        git_sha: Option<String>,
2599
2600        /// Proceed even when the working tree has uncommitted changes.
2601        ///
2602        /// Warning: the inventory is generated from the working copy, so it
2603        /// may not match the uploaded git SHA. Commit or stash first if you
2604        /// want a SHA-exact upload.
2605        #[arg(long)]
2606        allow_dirty: bool,
2607
2608        /// Additional glob patterns to exclude from the walk.
2609        ///
2610        /// Applied after the existing fallow ignore rules. Repeatable.
2611        #[arg(long, value_name = "GLOB", num_args = 0..)]
2612        exclude_paths: Vec<String>,
2613
2614        /// Prefix prepended to every emitted filePath so the static
2615        /// inventory joins with the runtime beacon for your deployment.
2616        /// Required for containerized deployments where the deployed
2617        /// WORKDIR rebases paths at runtime. Default: none (paths emit
2618        /// repo-relative, matching local runs and non-container CI).
2619        ///
2620        /// Common values: `/app` (typical Dockerfile), `/workspace`
2621        /// (Buildpacks / Cloud Run), `/usr/src/app` (older Node images),
2622        /// `/var/task` (Lambda), `/home/runner/work/<repo>/<repo>`
2623        /// (GitHub Actions default checkout).
2624        ///
2625        /// Must start with `/` and use POSIX separators.
2626        #[arg(long, value_name = "PREFIX")]
2627        path_prefix: Option<String>,
2628
2629        /// Print what would be uploaded and exit. No network call.
2630        #[arg(long)]
2631        dry_run: bool,
2632
2633        /// Also upload importer edges (which files import each function) so the
2634        /// cloud can show change-time blast radius. Opt-in: this builds the
2635        /// import graph by running the full static analysis, whereas the default
2636        /// upload is a fast per-file walk. The graph is cached, so a CI step that
2637        /// already ran analysis pays little extra.
2638        #[arg(long)]
2639        with_callers: bool,
2640
2641        /// Treat transient upload failures as warnings instead of errors
2642        /// (exit 0). Validation and auth errors still fail hard; this only
2643        /// downgrades transport and server errors.
2644        #[arg(long)]
2645        ignore_upload_errors: bool,
2646    },
2647    /// Upload JavaScript source maps to Fallow Cloud for bundled runtime coverage.
2648    ///
2649    /// Scans a build output directory for `.map` files and uploads them under
2650    /// the selected repo + git SHA. The production beacon reports bundled
2651    /// paths; the cloud resolver uses these maps to remap runtime coverage back
2652    /// to original source files.
2653    ///
2654    /// Each upload also carries the map's path relative to the repo root, so the
2655    /// source-evidence viewer can resolve a monorepo sub-package map's relative
2656    /// `sources[]` (e.g. `../../src/X`) to the package-prefixed source path
2657    /// (e.g. `dashboard/src/X`). Run from the repo root so this prefix is
2658    /// correct.
2659    UploadSourceMaps {
2660        /// Directory to scan recursively for source maps.
2661        #[arg(long, value_name = "PATH", default_value = "dist")]
2662        dir: PathBuf,
2663
2664        /// Glob pattern, relative to --dir, selecting maps to upload.
2665        #[arg(long, value_name = "GLOB", default_value = "**/*.map")]
2666        include: String,
2667
2668        /// Glob pattern, relative to --dir, selecting files to skip.
2669        ///
2670        /// Repeatable. Defaults to `**/node_modules/**`.
2671        #[arg(long, value_name = "GLOB", default_value = "**/node_modules/**")]
2672        exclude: Vec<String>,
2673
2674        /// Repo name used in the API path.
2675        ///
2676        /// Defaults to package.json repository.url, then `git remote get-url origin`.
2677        #[arg(long, value_name = "NAME")]
2678        repo: Option<String>,
2679
2680        /// Commit SHA to key uploads under.
2681        ///
2682        /// Defaults to $GITHUB_SHA, $CI_COMMIT_SHA, $COMMIT_SHA, then
2683        /// `git rev-parse HEAD`.
2684        #[arg(long, value_name = "SHA")]
2685        git_sha: Option<String>,
2686
2687        /// Override the Fallow Cloud base URL.
2688        #[arg(long, value_name = "URL")]
2689        endpoint: Option<String>,
2690
2691        /// Send only the basename as fileName by default.
2692        ///
2693        /// Use `--strip-path=false` when your runtime coverage reports bundle
2694        /// paths relative to the build directory, such as `assets/app.js`.
2695        /// When two maps share a basename, each of them is sent under its path
2696        /// in the build directory, so that no map replaces another.
2697        #[arg(long, value_name = "BOOL", default_value_t = true, action = clap::ArgAction::Set)]
2698        strip_path: bool,
2699
2700        /// Print what would be uploaded and exit. No network call.
2701        #[arg(long)]
2702        dry_run: bool,
2703
2704        /// Parallel upload fanout.
2705        #[arg(long, value_name = "N", default_value_t = 4)]
2706        concurrency: usize,
2707
2708        /// Stop on first upload error.
2709        #[arg(long)]
2710        fail_fast: bool,
2711    },
2712    /// Upload static dead-code findings to Fallow Cloud for the source-evidence viewer.
2713    ///
2714    /// Runs fallow's static analysis and uploads the `unused_export` and
2715    /// `dead_file` verdicts under the selected repo + git SHA. The cloud
2716    /// overlays them on the source view alongside the runtime coverage overlay.
2717    /// Findings are replace-by-SHA: each run sends the complete set for the SHA.
2718    UploadStaticFindings {
2719        /// Fallow Cloud API key (bearer token).
2720        ///
2721        /// Precedence: this flag > $FALLOW_API_KEY. Generate at
2722        /// <https://fallow.cloud/settings#api-keys>. This must be a live API
2723        /// key, not a publishable ingest key.
2724        ///
2725        /// Security: prefer $FALLOW_API_KEY on shared CI runners. Passing a
2726        /// secret on the command line may be visible to other processes via
2727        /// `ps` and can leak into shell history or process audit logs.
2728        #[arg(long, value_name = "KEY")]
2729        api_key: Option<String>,
2730
2731        /// Override the Fallow Cloud base URL.
2732        ///
2733        /// Useful for staging and on-premise deployments. Also respects
2734        /// $FALLOW_API_URL when this flag is not set.
2735        #[arg(long, value_name = "URL")]
2736        api_endpoint: Option<String>,
2737
2738        /// Project identifier, for example `fallow-cloud-api` or `owner/repo`.
2739        ///
2740        /// Defaults to $GITHUB_REPOSITORY, then $CI_PROJECT_PATH, then the
2741        /// parsed origin URL from `git remote get-url origin`.
2742        #[arg(long, value_name = "PROJECT_ID")]
2743        project_id: Option<String>,
2744
2745        /// Explicit git SHA for these findings.
2746        ///
2747        /// Default: `git rev-parse HEAD`. Findings are keyed on this value and
2748        /// fully replace any prior set uploaded for the same SHA.
2749        #[arg(long, value_name = "SHA")]
2750        git_sha: Option<String>,
2751
2752        /// Proceed even when the working tree has uncommitted changes.
2753        ///
2754        /// Warning: findings are generated from the working copy, so they may
2755        /// not match the uploaded git SHA. Commit or stash first if you want a
2756        /// SHA-exact upload.
2757        #[arg(long)]
2758        allow_dirty: bool,
2759
2760        /// Print what would be uploaded and exit. No network call.
2761        #[arg(long)]
2762        dry_run: bool,
2763
2764        /// Treat transient upload failures as warnings instead of errors
2765        /// (exit 0). Validation and auth errors still fail hard; this only
2766        /// downgrades transport and server errors.
2767        #[arg(long)]
2768        ignore_upload_errors: bool,
2769    },
2770    /// Read the production facts of changed code from Fallow Cloud.
2771    ///
2772    /// Sends the changed files or functions to the cloud review-packet
2773    /// endpoint and prints the answer as JSON: per function the production
2774    /// calls, the tracking state and the evidence window. It runs no local
2775    /// analysis and pulls no full runtime context, so the answer stays small.
2776    /// Without --file and --function, the files changed against the base are
2777    /// sent. The base resolves like `fallow audit`: `--base` (alias of
2778    /// `--changed-since`), then $FALLOW_AUDIT_BASE, then the merge-base with
2779    /// the upstream or the remote default branch.
2780    ReviewPacket {
2781        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2782        #[arg(long, value_name = "KEY")]
2783        api_key: Option<String>,
2784
2785        /// Override the Fallow Cloud base URL.
2786        #[arg(long, value_name = "URL")]
2787        api_endpoint: Option<String>,
2788
2789        /// Repository identifier, for example `owner/repo`.
2790        ///
2791        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2792        /// `git remote get-url origin`.
2793        #[arg(long, value_name = "OWNER/REPO")]
2794        repo: Option<String>,
2795
2796        /// Optional monorepo/project disambiguator.
2797        #[arg(long, value_name = "ID")]
2798        project_id: Option<String>,
2799
2800        /// Runtime observation window (1..=90 days). The cloud default is 30.
2801        #[arg(long, value_name = "DAYS")]
2802        coverage_period: Option<u16>,
2803
2804        /// Optional commit SHA of the deployment to read.
2805        #[arg(long, value_name = "SHA")]
2806        commit_sha: Option<String>,
2807
2808        /// Repo-relative file to include. Repeatable.
2809        #[arg(long = "file", value_name = "PATH")]
2810        files: Vec<String>,
2811
2812        /// Function to include, as FILE:NAME or FILE:NAME:LINE. Repeatable.
2813        #[arg(long = "function", value_name = "FILE:NAME[:LINE]")]
2814        functions: Vec<String>,
2815    },
2816    /// Read how production behavior changed between two deployments.
2817    ///
2818    /// Prints the cloud deployment change report as JSON: per function the
2819    /// change kind (new_called, new_not_called, heated_up, cooled_down,
2820    /// stopped, unchanged) between the deployment and its base. Pass the
2821    /// base deployment commit with `--base <sha>`; without it, the cloud
2822    /// picks the previous deployment with production runtime.
2823    DeploymentChanges {
2824        /// Fallow Cloud API key. Precedence: this flag > $FALLOW_API_KEY.
2825        #[arg(long, value_name = "KEY")]
2826        api_key: Option<String>,
2827
2828        /// Override the Fallow Cloud base URL.
2829        #[arg(long, value_name = "URL")]
2830        api_endpoint: Option<String>,
2831
2832        /// Repository identifier, for example `owner/repo`.
2833        ///
2834        /// Defaults to $FALLOW_REPO, then the parsed origin URL from
2835        /// `git remote get-url origin`.
2836        #[arg(long, value_name = "OWNER/REPO")]
2837        repo: Option<String>,
2838
2839        /// Commit SHA of the deployment. Default: `git rev-parse HEAD`.
2840        #[arg(long, value_name = "SHA")]
2841        sha: Option<String>,
2842
2843        /// Show only one change kind.
2844        #[arg(long, value_name = "KIND", value_parser = [
2845            "stopped", "new_not_called", "heated_up", "cooled_down", "new_called", "unchanged",
2846        ])]
2847        change: Option<String>,
2848
2849        /// Page size (1..=200).
2850        #[arg(long, value_name = "N")]
2851        limit: Option<u16>,
2852
2853        /// Cursor from `meta.cursor` of the previous page.
2854        #[arg(long, value_name = "CURSOR")]
2855        cursor: Option<String>,
2856    },
2857}
2858
2859#[derive(Subcommand)]
2860pub enum CiCli {
2861    /// Compute the provider action for a rendered sticky PR summary comment.
2862    PlanPrComment {
2863        /// Path to the rendered PR comment Markdown body.
2864        #[arg(long)]
2865        body: PathBuf,
2866
2867        /// Sticky marker id used in the rendered body.
2868        #[arg(long)]
2869        marker_id: String,
2870
2871        /// Treat the rendered body as a clean no-findings result.
2872        #[arg(long)]
2873        clean: bool,
2874
2875        /// Existing provider comment id, when a matching sticky comment exists.
2876        #[arg(long)]
2877        existing_comment_id: Option<String>,
2878
2879        /// Path to the existing provider comment body. Enables unchanged-skip planning.
2880        #[arg(long)]
2881        existing_body: Option<PathBuf>,
2882    },
2883
2884    /// Post, update, or skip a rendered sticky PR summary comment.
2885    PostPrComment {
2886        /// Provider whose PR comment is being posted.
2887        #[arg(long, value_enum)]
2888        provider: CiProviderArg,
2889
2890        /// Pull request number (GitHub).
2891        #[arg(long)]
2892        pr: Option<String>,
2893
2894        /// Merge request IID (GitLab).
2895        #[arg(long)]
2896        mr: Option<String>,
2897
2898        /// Path to the rendered PR comment Markdown body.
2899        #[arg(long)]
2900        body: PathBuf,
2901
2902        /// Path to the typed PR comment envelope JSON, when available.
2903        #[arg(long)]
2904        envelope: Option<PathBuf>,
2905
2906        /// Sticky marker id used in the rendered body.
2907        #[arg(long)]
2908        marker_id: String,
2909
2910        /// Treat the rendered body as a clean no-findings result.
2911        #[arg(long)]
2912        clean: bool,
2913
2914        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
2915        #[arg(long)]
2916        repo: Option<String>,
2917
2918        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
2919        #[arg(long = "project-id")]
2920        project_id: Option<String>,
2921
2922        /// Provider API base URL. Defaults to github.com.
2923        #[arg(long = "api-url")]
2924        api_url: Option<String>,
2925
2926        /// Compute the post plan without creating or updating the provider comment.
2927        #[arg(long)]
2928        dry_run: bool,
2929    },
2930
2931    /// Post a rendered review envelope as a provider review or summary comment.
2932    PostReview {
2933        /// Provider whose review envelope is being posted.
2934        #[arg(long, value_enum)]
2935        provider: CiProviderArg,
2936
2937        /// Pull request number (GitHub).
2938        #[arg(long)]
2939        pr: Option<String>,
2940
2941        /// Merge request IID (GitLab).
2942        #[arg(long)]
2943        mr: Option<String>,
2944
2945        /// Path to a review-github or review-gitlab JSON envelope.
2946        #[arg(long)]
2947        envelope: PathBuf,
2948
2949        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
2950        #[arg(long)]
2951        repo: Option<String>,
2952
2953        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
2954        #[arg(long = "project-id")]
2955        project_id: Option<String>,
2956
2957        /// Provider API base URL. Defaults to github.com or CI_API_V4_URL/gitlab.com.
2958        #[arg(long = "api-url")]
2959        api_url: Option<String>,
2960
2961        /// Compute the post plan without creating provider comments.
2962        #[arg(long)]
2963        dry_run: bool,
2964    },
2965
2966    /// Post a GitHub Check Run from a typed PR decision surface.
2967    PostCheckRun {
2968        /// Provider whose check run is being posted. Only GitHub is supported.
2969        #[arg(long, value_enum)]
2970        provider: CiProviderArg,
2971
2972        /// Path to a fallow-pr-decision JSON sidecar.
2973        #[arg(long)]
2974        decision: PathBuf,
2975
2976        /// GitHub repository in owner/name form.
2977        #[arg(long)]
2978        repo: String,
2979
2980        /// Head SHA the check run should attach to.
2981        #[arg(long = "head-sha")]
2982        head_sha: String,
2983
2984        /// Provider API base URL. Defaults to github.com.
2985        #[arg(long = "api-url")]
2986        api_url: Option<String>,
2987
2988        /// Post one check run per decision gate instead of one aggregate check.
2989        #[arg(long = "split-gates")]
2990        split_gates: bool,
2991
2992        /// Print the check run payload without posting it.
2993        #[arg(long)]
2994        dry_run: bool,
2995    },
2996
2997    /// Validate a rendered review envelope and compute a stable reconcile plan.
2998    ReconcileReview {
2999        /// Provider whose review envelope is being reconciled.
3000        #[arg(long, value_enum)]
3001        provider: CiProviderArg,
3002
3003        /// Pull request number (GitHub).
3004        #[arg(long)]
3005        pr: Option<String>,
3006
3007        /// Merge request IID (GitLab).
3008        #[arg(long)]
3009        mr: Option<String>,
3010
3011        /// Path to a review-github or review-gitlab JSON envelope.
3012        #[arg(long)]
3013        envelope: PathBuf,
3014
3015        /// GitHub repository in owner/name form. Defaults to GH_REPO or GITHUB_REPOSITORY.
3016        #[arg(long)]
3017        repo: Option<String>,
3018
3019        /// GitLab project id or path. Defaults to CI_PROJECT_ID.
3020        #[arg(long = "project-id")]
3021        project_id: Option<String>,
3022
3023        /// Provider API base URL. Defaults to github.com or CI_API_V4_URL/gitlab.com.
3024        #[arg(long = "api-url")]
3025        api_url: Option<String>,
3026
3027        /// Compute the reconcile plan without posting resolution notes or resolving threads.
3028        #[arg(long)]
3029        dry_run: bool,
3030    },
3031}
3032
3033#[derive(Subcommand)]
3034pub enum RulePackCli {
3035    /// Scaffold a new rule pack file and wire it into the config
3036    Init {
3037        /// Pack name (default: the template name, or "team-policy")
3038        name: Option<String>,
3039
3040        /// Template: starter, ai-safe-repo, side-effect-free-domain, clean-architecture, next-app-router
3041        #[arg(long, default_value = "starter")]
3042        template: String,
3043
3044        /// Directory for the pack file, relative to the project root
3045        #[arg(long, default_value = "rule-packs")]
3046        dir: String,
3047
3048        /// Only write the pack file; do not modify the config
3049        #[arg(long)]
3050        no_config: bool,
3051    },
3052
3053    /// List configured rule packs and their rules
3054    List,
3055
3056    /// Evaluate a pack (or all configured packs) against this project and print matches
3057    Test {
3058        /// Path to a pack file to test in isolation (default: all configured packs)
3059        pack: Option<PathBuf>,
3060    },
3061
3062    /// Print the JSON Schema for rule pack files
3063    Schema,
3064}
3065
3066/// CLI mirror of [`fallow_engine::baseline::HealthBaselineMode`].
3067#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
3068pub enum BaselineModeArg {
3069    /// Match a saved health baseline per file and finding category.
3070    #[default]
3071    Count,
3072    /// Match a saved health baseline per function identity and finding
3073    /// category, so a hotspot that replaces another hotspot is reported.
3074    Identity,
3075}
3076
3077impl From<BaselineModeArg> for fallow_engine::baseline::HealthBaselineMode {
3078    fn from(value: BaselineModeArg) -> Self {
3079        match value {
3080            BaselineModeArg::Count => Self::Count,
3081            BaselineModeArg::Identity => Self::Identity,
3082        }
3083    }
3084}
3085
3086#[derive(Clone, Copy, Debug, clap::ValueEnum)]
3087pub enum CiProviderArg {
3088    Github,
3089    Gitlab,
3090}
3091
3092/// CLI mirror of [`fallow_config::TypeAwareRequire`].
3093#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3094pub enum TypeAwareRequireArg {
3095    /// Keep conservative findings and report semantic gaps.
3096    BestEffort,
3097    /// Fail the quality gate when a requested semantic query is incomplete.
3098    Complete,
3099}
3100
3101impl From<TypeAwareRequireArg> for fallow_config::TypeAwareRequire {
3102    fn from(value: TypeAwareRequireArg) -> Self {
3103        match value {
3104            TypeAwareRequireArg::BestEffort => Self::BestEffort,
3105            TypeAwareRequireArg::Complete => Self::Complete,
3106        }
3107    }
3108}
3109
3110/// Filter refactoring targets by effort level.
3111#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3112pub enum EffortFilter {
3113    Low,
3114    Medium,
3115    High,
3116}
3117
3118impl EffortFilter {
3119    /// Convert to the corresponding `EffortEstimate` for comparison.
3120    pub(crate) const fn to_estimate(self) -> fallow_output::EffortEstimate {
3121        match self {
3122            Self::Low => fallow_output::EffortEstimate::Low,
3123            Self::Medium => fallow_output::EffortEstimate::Medium,
3124            Self::High => fallow_output::EffortEstimate::High,
3125        }
3126    }
3127}
3128
3129/// CLI parser for the health severity gate.
3130#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3131pub enum HealthSeverityCli {
3132    Moderate,
3133    High,
3134    Critical,
3135}
3136
3137impl HealthSeverityCli {
3138    /// Convert to the typed health output severity.
3139    pub(crate) const fn to_health_severity(self) -> fallow_output::FindingSeverity {
3140        match self {
3141            Self::Moderate => fallow_output::FindingSeverity::Moderate,
3142            Self::High => fallow_output::FindingSeverity::High,
3143            Self::Critical => fallow_output::FindingSeverity::Critical,
3144        }
3145    }
3146}
3147
3148/// Privacy mode for author emails emitted by `--ownership`.
3149///
3150/// CLI mirror of [`fallow_config::EmailMode`]. Kept as a separate enum so
3151/// the help text controls rendering and we don't leak config-internal
3152/// schema details into clap.
3153#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3154pub enum EmailModeArg {
3155    /// Show full email addresses as recorded in git history.
3156    Raw,
3157    /// Show local-part only (default). Unwraps GitHub-style noreply prefixes.
3158    Handle,
3159    /// Show stable non-cryptographic pseudonyms (`xxh3:<hex>`).
3160    Anonymized,
3161    /// Legacy spelling for anonymized output.
3162    #[value(hide = true)]
3163    Hash,
3164}
3165
3166impl EmailModeArg {
3167    /// Convert to the equivalent config-level mode.
3168    pub(crate) const fn to_config(self) -> fallow_config::EmailMode {
3169        match self {
3170            Self::Raw => fallow_config::EmailMode::Raw,
3171            Self::Handle => fallow_config::EmailMode::Handle,
3172            Self::Anonymized => fallow_config::EmailMode::Anonymized,
3173            Self::Hash => fallow_config::EmailMode::Hash,
3174        }
3175    }
3176}
3177
3178/// CLI mirror of [`fallow_config::AuditGate`].
3179#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3180pub enum AuditGateArg {
3181    /// Only findings introduced by the current changeset affect the verdict.
3182    NewOnly,
3183    /// All findings in changed files affect the verdict.
3184    All,
3185}
3186
3187/// CLI value of `--worsened`, mirroring `fallow_config::AuditWorsened`.
3188#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
3189pub enum AuditWorsenedArg {
3190    /// A worsened finding makes the verdict at least warn.
3191    Warn,
3192    /// A worsened finding counts like an introduced finding.
3193    Error,
3194    /// Only growth of an exceeded metric is worsened (warn).
3195    Off,
3196}
3197
3198impl From<AuditWorsenedArg> for fallow_config::AuditWorsened {
3199    fn from(value: AuditWorsenedArg) -> Self {
3200        match value {
3201            AuditWorsenedArg::Warn => Self::Warn,
3202            AuditWorsenedArg::Error => Self::Error,
3203            AuditWorsenedArg::Off => Self::Off,
3204        }
3205    }
3206}
3207
3208impl From<AuditGateArg> for fallow_config::AuditGate {
3209    fn from(value: AuditGateArg) -> Self {
3210        match value {
3211            AuditGateArg::NewOnly => Self::NewOnly,
3212            AuditGateArg::All => Self::All,
3213        }
3214    }
3215}
3216
3217/// Parse `--min-occurrences` and reject values below 2. A single occurrence
3218/// is not a duplicate; silently clamping would diverge from the config-file
3219/// validator, which also rejects `< 2`.
3220fn parse_min_occurrences(s: &str) -> Result<usize, String> {
3221    let value: usize = s
3222        .parse()
3223        .map_err(|_| format!("`{s}` is not a non-negative integer"))?;
3224    if value < 2 {
3225        return Err(format!(
3226            "must be at least 2 (got {value}); a single occurrence isn't a duplicate"
3227        ));
3228    }
3229    Ok(value)
3230}