opencodekit 0.23.0 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/index.js +354 -825
  2. package/dist/template/.opencode/AGENTS.md +15 -0
  3. package/dist/template/.opencode/command/init.md +198 -34
  4. package/dist/template/.opencode/context/fallow.md +137 -0
  5. package/dist/template/.opencode/dcp-prompts/overrides/compress-range.md +89 -0
  6. package/dist/template/.opencode/opencode.json +110 -315
  7. package/dist/template/.opencode/plugin/README.md +10 -0
  8. package/dist/template/.opencode/plugin/memory/compile.ts +171 -186
  9. package/dist/template/.opencode/plugin/memory/index-generator.ts +118 -133
  10. package/dist/template/.opencode/plugin/memory/lint.ts +253 -275
  11. package/dist/template/.opencode/plugin/memory/tools.ts +224 -268
  12. package/dist/template/.opencode/plugin/memory/validate.ts +154 -164
  13. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-preview.ts +13 -30
  14. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-shared.ts +25 -0
  15. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search.ts +17 -34
  16. package/dist/template/.opencode/plugin/session-summary.ts +542 -0
  17. package/dist/template/.opencode/plugin/srcwalk.ts +775 -661
  18. package/dist/template/.opencode/skill/condition-based-waiting/example.ts +15 -2
  19. package/dist/template/.opencode/skill/fallow/SKILL.md +409 -0
  20. package/dist/template/.opencode/skill/fallow/references/cli-reference.md +1905 -0
  21. package/dist/template/.opencode/skill/fallow/references/gotchas.md +644 -0
  22. package/dist/template/.opencode/skill/fallow/references/patterns.md +791 -0
  23. package/package.json +2 -2
@@ -0,0 +1,1905 @@
1
+ # Fallow CLI Reference
2
+
3
+ Complete command and flag specifications for all fallow CLI commands.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [`dead-code`: Dead Code Analysis](#dead-code-dead-code-analysis)
10
+ - [`dupes`: Duplication Detection](#dupes-duplication-detection)
11
+ - [`fix`: Auto-Remove Unused Code](#fix-auto-remove-unused-code)
12
+ - [`list`: Project Introspection](#list-project-introspection)
13
+ - [`init`: Config Generation](#init-config-generation)
14
+ - [`migrate`: Config Migration](#migrate-config-migration)
15
+ - [`health`: Function Complexity Analysis](#health-function-complexity-analysis)
16
+ - [`audit`: Changed-File Quality Gate](#audit-changed-file-quality-gate)
17
+ - [`flags`: Feature Flag Detection](#flags-feature-flag-detection)
18
+ - [`security`: Security Candidate Detection](#security-security-candidate-detection)
19
+ - [`explain`: Rule Explanation](#explain-rule-explanation)
20
+ - [`schema`: CLI Introspection](#schema-cli-introspection)
21
+ - [`config-schema`: Config JSON Schema](#config-schema-config-json-schema)
22
+ - [`plugin-schema`: Plugin JSON Schema](#plugin-schema-plugin-json-schema)
23
+ - [`config`: Show Resolved Config](#config-show-resolved-config)
24
+ - [Global Flags](#global-flags)
25
+ - [Environment Variables](#environment-variables)
26
+ - [Output Formats](#output-formats)
27
+ - [JSON Output Structure](#json-output-structure)
28
+ - [Configuration File Format](#configuration-file-format)
29
+ - [Inline Suppression Comments](#inline-suppression-comments)
30
+
31
+ ---
32
+
33
+ ## `dead-code`: Dead Code Analysis
34
+
35
+ Analyzes the project for unused files, exports, dependencies, types, members, and more. Running `fallow` with no subcommand runs all analyses (dead code + duplication + complexity). Use `fallow dead-code` for dead code only.
36
+
37
+ ### Flags
38
+
39
+ | Flag | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
42
+ | `--quiet` | bool | `false` | Suppress progress bars and timing on stderr |
43
+ | `--legacy-envelope` | bool | `false` | Remove the top-level `kind` field from typed JSON roots for one migration cycle |
44
+ | `--changed-since` | string | — | Only analyze files changed since a git ref (e.g., `main`, `HEAD~3`) |
45
+ | `--production` | bool | `false` | Exclude test/dev files, only start/build scripts (applies to every analysis) |
46
+ | `--production-dead-code` | bool | `false` | Per-analysis production mode for dead-code. Bare combined runs and `fallow audit` only. |
47
+ | `--production-health` | bool | `false` | Per-analysis production mode for health. Bare combined runs and `fallow audit` only. |
48
+ | `--production-dupes` | bool | `false` | Per-analysis production mode for duplication. Bare combined runs and `fallow audit` only. |
49
+ | `--baseline` | path | — | Compare against a saved baseline |
50
+ | `--save-baseline` | path | — | Save current results as a baseline |
51
+ | `--workspace` | string | — | Scope to one or more workspaces. Comma-separated values, globs (`apps/*`, `@scope/*`), and `!`-prefixed negation (`!apps/legacy`) supported. Matched against package name AND workspace path relative to repo root. |
52
+ | `--changed-workspaces` | string (git ref) | — | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF` (e.g. `origin/main`). Auto-derives the workspace set from `git diff`. Mutually exclusive with `--workspace`. Missing ref is a hard error (exit 2), not silent full-scope fallback. |
53
+ | `--include-dupes` | bool | `false` | Cross-reference with duplication findings |
54
+ | `--dupes-min-occurrences` | number | `config` | Override the minimum clone occurrences in combined mode (must be >= 2). Falls back to the config value when unset. Mirrors the standalone `dupes --min-occurrences`. |
55
+ | `--file` | path (multiple) | — | Scope output to specific files. Only issues in the specified files are reported. Project-wide dependency issues are suppressed. Warns on non-existent paths. Useful for lint-staged |
56
+ | `--include-entry-exports` | bool | `false` | Report unused exports in entry files (package.json `main`/`exports`, framework pages). Catches typos like `meatdata` vs `metadata`. Global flag, also accepted on combined mode (`fallow --include-entry-exports`) and `fallow audit`. Also configurable as `includeEntryExports: true` in fallow config |
57
+ | `--trace` | `FILE:EXPORT` | — | Trace export usage chain |
58
+ | `--trace-file` | path | — | Show all edges for a file |
59
+ | `--trace-dependency` | string | — | Trace where a dependency is used |
60
+
61
+ ### Issue Type Filters
62
+
63
+ | Flag | Issue Type |
64
+ |------|------------|
65
+ | `--unused-files` | Unused files |
66
+ | `--unused-exports` | Unused exports |
67
+ | `--unused-types` | Unused types |
68
+ | `--private-type-leaks` | Opt-in API hygiene check (default `off`) for exported signatures that reference same-file private types. Storybook `*.stories.*` story files and framework routing convention files (Next.js App + Pages Router, Gatsby, Remix v2, TanStack Router, Expo Router) are skipped to avoid noise. Enable via this flag or `private-type-leaks: "warn"` / `"error"` in [`rules`](#rules-configuration). |
69
+ | `--unused-deps` | Unused dependencies, devDependencies, optionalDependencies, type-only production deps, and test-only production deps |
70
+ | `--unused-enum-members` | Unused enum members |
71
+ | `--unused-class-members` | Unused class members |
72
+ | `--unresolved-imports` | Unresolved imports |
73
+ | `--unlisted-deps` | Unlisted dependencies |
74
+ | `--duplicate-exports` | Duplicate exports |
75
+ | `--circular-deps` | Circular dependencies |
76
+ | `--re-export-cycles` | Re-export cycles (`kind: multi-node` for barrel files re-exporting from each other in a loop, `kind: self-loop` for a barrel re-exporting from itself). File-scoped finding; chain propagation through the loop is a no-op so imports may silently come up empty. Distinct from `--circular-deps` (runtime cycles). |
77
+ | `--boundary-violations` | Boundary violations (imports crossing architecture zone boundaries) |
78
+ | `--stale-suppressions` | Stale suppression comments or `@expected-unused` JSDoc tags |
79
+ | `--unused-catalog-entries` | Unused pnpm catalog entries |
80
+ | `--empty-catalog-groups` | Empty named pnpm catalog groups |
81
+ | `--unresolved-catalog-references` | Package references to missing pnpm catalog entries |
82
+ | `--unused-dependency-overrides` | Unused pnpm dependency overrides |
83
+ | `--misconfigured-dependency-overrides` | Malformed pnpm dependency overrides |
84
+
85
+ ### Examples
86
+
87
+ ```bash
88
+ # Full analysis with JSON output
89
+ fallow dead-code --format json --quiet
90
+
91
+ # Only unused exports
92
+ fallow dead-code --format json --quiet --unused-exports
93
+
94
+ # PR check: only changed files
95
+ fallow dead-code --format json --quiet --changed-since main --fail-on-issues
96
+
97
+ # CI mode with SARIF upload
98
+ fallow dead-code --ci
99
+
100
+ # Production-only analysis
101
+ fallow dead-code --format json --quiet --production
102
+
103
+ # Single workspace package
104
+ fallow dead-code --format json --quiet --workspace my-package
105
+
106
+ # Multiple workspaces: comma-separated
107
+ fallow dead-code --format json --quiet --workspace web,admin
108
+
109
+ # Glob (matches package name OR relative path)
110
+ fallow dead-code --format json --quiet --workspace 'apps/*'
111
+
112
+ # Exclude a workspace from the set
113
+ fallow dead-code --format json --quiet --workspace 'apps/*,!apps/legacy'
114
+
115
+ # Monorepo CI: auto-scope to workspaces containing any file changed since origin/main
116
+ fallow dead-code --format json --quiet --changed-workspaces origin/main
117
+
118
+ # Debug: trace an export
119
+ fallow dead-code --format json --quiet --trace src/utils.ts:myFunction
120
+
121
+ # Incremental adoption with baseline
122
+ fallow dead-code --format json --quiet --save-baseline fallow-baselines/dead-code.json
123
+ fallow dead-code --format json --quiet --baseline fallow-baselines/dead-code.json --fail-on-issues
124
+
125
+ # Regression detection: save baseline on main, compare on PRs
126
+ fallow dead-code --format json --quiet --save-regression-baseline
127
+ fallow dead-code --format json --quiet --fail-on-regression --tolerance 2%
128
+
129
+ # Scope to specific files (e.g., lint-staged)
130
+ fallow dead-code --format json --quiet --file src/utils.ts --file src/helpers.ts
131
+
132
+ # Catch typos in entry file exports
133
+ fallow dead-code --format json --quiet --include-entry-exports
134
+ ```
135
+
136
+ ---
137
+
138
+ ## `dupes`: Duplication Detection
139
+
140
+ Finds code duplication and clones across the project.
141
+
142
+ By default, `fallow dupes` skips generated framework output matching `**/.next/**`, `**/.nuxt/**`, `**/.svelte-kit/**`, `**/.turbo/**`, `**/.parcel-cache/**`, `**/.vite/**`, `**/.cache/**`, `**/out/**`, and `**/storybook-static/**`. These defaults merge with `duplicates.ignore`. Set `duplicates.ignoreDefaults = false` to opt out and use only your configured ignore list. If the reported duplication percentage drops after upgrading, this generated-output filtering is the expected reason.
143
+
144
+ ### Flags
145
+
146
+ | Flag | Type | Default | Description |
147
+ |------|------|---------|-------------|
148
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
149
+ | `--quiet` | bool | `false` | Suppress progress bars |
150
+ | `--top` | number | — | Show only the N most-duplicated clone groups (sorted by instance count desc, tiebreak: line count desc, then path/line). Summary stats reflect the full project. |
151
+ | `--mode` | `strict\|mild\|weak\|semantic` | `mild` | Detection mode |
152
+ | `--min-tokens` | number | `50` | Minimum token count for a clone |
153
+ | `--min-lines` | number | `5` | Minimum line count for a clone |
154
+ | `--min-occurrences` | number | `2` | Minimum number of occurrences before a clone group is reported (must be ≥ 2). Raise to skip pair-only clones and focus on widespread copy-paste worth refactoring. `fallow init` writes `minOccurrences: 3` into new projects. |
155
+ | `--threshold` | number | `0` | Fail if duplication exceeds this percentage |
156
+ | `--skip-local` | bool | `false` | Only report cross-directory duplicates |
157
+ | `--cross-language` | bool | `false` | Strip type annotations for TS↔JS matching |
158
+ | `--ignore-imports` | bool | `false` | Exclude import declarations from clone detection |
159
+ | `--explain-skipped` | bool | `false` | Human/markdown only: show per-pattern counts for files skipped by default duplicates ignores |
160
+ | `--trace` | `FILE:LINE` \| `dup:<fp>` | — | Deep-dive clones. `FILE:LINE` traces all clones at a location; `dup:<id>` traces a clone group by the stable fingerprint shown in the listing and on `clone_groups[].fingerprint` in JSON. Fingerprints are usually `dup:<8hex>` and widen only on rare report collisions. Trace output adds an extract-function suggestion, estimated savings, and a best-effort proposed name per group |
161
+ | `--changed-since` | string | — | Only report duplication in files changed since a git ref |
162
+ | `--baseline` | path | — | Compare against baseline |
163
+ | `--save-baseline` | path | — | Save results as baseline |
164
+ | `--workspace` | string | — | Scope to one or more workspaces. Comma-separated values, globs (`apps/*`, `@scope/*`), and `!`-prefixed negation (`!apps/legacy`) supported. Matched against package name AND workspace path relative to repo root. |
165
+ | `--changed-workspaces` | string (git ref) | — | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF`. Mutually exclusive with `--workspace`. Missing ref is a hard error. |
166
+ | `--group-by` | `owner\|directory\|package\|section` | — | Partition the report into per-group sections. Each clone group is attributed to its **largest owner** (most instances; alphabetical tiebreak): a group split 2 src / 1 lib appears under `src`. JSON adds `grouped_by` plus a `groups` array; each bucket carries dedup-aware `stats`, `clone_groups` (every group tagged with `primary_owner` and per-instance `owner`), and `clone_families`. SARIF results carry `properties.group`, CodeClimate issues a top-level `group` field. Compact and markdown fall back to ungrouped with a stderr note. |
167
+
168
+ ### Detection Modes
169
+
170
+ | Mode | Behavior |
171
+ |------|----------|
172
+ | `strict` | Exact token match (no normalization) |
173
+ | `mild` | Syntax normalized (whitespace, semicolons) |
174
+ | `weak` | Different literal values treated as equivalent |
175
+ | `semantic` | Renamed variables also treated as equivalent |
176
+
177
+ ### Examples
178
+
179
+ ```bash
180
+ # Default duplication scan
181
+ fallow dupes --format json --quiet
182
+
183
+ # Semantic mode (detects renames)
184
+ fallow dupes --format json --quiet --mode semantic
185
+
186
+ # Cross-directory only, fail at 5%
187
+ fallow dupes --format json --quiet --skip-local --threshold 5
188
+
189
+ # Trace clones at a specific location
190
+ fallow dupes --format json --quiet --trace src/utils.ts:42
191
+
192
+ # Deep-dive a clone group by its dup:<id> fingerprint (from the listing or JSON)
193
+ fallow dupes --format json --quiet --trace dup:7f3a2c1e
194
+
195
+ # Only check duplication in changed files
196
+ fallow dupes --format json --quiet --changed-since main
197
+
198
+ # Incremental CI
199
+ fallow dupes --format json --quiet --save-baseline fallow-baselines/dupes.json
200
+ fallow dupes --format json --quiet --baseline fallow-baselines/dupes.json --threshold 5
201
+ ```
202
+
203
+ ---
204
+
205
+ ## `fix`: Auto-Remove Unused Code
206
+
207
+ Auto-removes unused exports, dependencies, enum members, and pnpm catalog entries.
208
+
209
+ ### Flags
210
+
211
+ | Flag | Type | Default | Description |
212
+ |------|------|---------|-------------|
213
+ | `--dry-run` | bool | `false` | Show what would be removed without modifying files. For `add-to-config` actions, prints a unified-diff preview of the proposed config write; JSON mode includes the diff under a `proposed_diff` field on the fix entry. |
214
+ | `--yes` | bool | `false` | Skip confirmation prompt (**required** in non-TTY) |
215
+ | `--force` | bool | `false` | Alias for `--yes` |
216
+ | `--no-create-config` | bool | `false` | Refuse to create a new `.fallowrc.json` when none exists. The duplicate-export config-add path is skipped with `skip_reason: "no_create_config"`; source-file edits proceed normally. Use in pre-commit hooks, CI bots, and `fallow watch` where silently materialising a new top-level file would surprise the user. |
217
+ | `--format` | `human\|json` | `human` | Output format |
218
+ | `--quiet` | bool | `false` | Suppress progress bars |
219
+
220
+ ### What gets fixed
221
+
222
+ - Unused exports (removes the `export` keyword; whole-enum block when every member is unused)
223
+ - Unused dependencies (removed from `package.json`)
224
+ - Unused enum members (removed from the declaration)
225
+ - Unused pnpm catalog entries (removed from `pnpm-workspace.yaml` by line-aware deletion). Object-form entries are removed as one block. By default, fallow also removes a contiguous YAML comment block immediately above the entry when it clearly belongs to that entry; configure this with `fix.catalog.deletePrecedingComments` (`"auto"`, `"always"`, or `"never"`). Two escape hatches keep curated comments safe regardless of policy: a `# fallow-keep` marker on any line in the block preserves it, and the `auto` policy additionally preserves section-banner blocks whose body starts with three or more `=`, `-`, `*`, `_`, `~`, `+`, or `#` characters (e.g. `# === React 18 production pins ===`). Other comments and stylistic choices are preserved. When the last entry of a catalog group is removed, the header is rewritten to `catalog: {}` / `<name>: {}` so pnpm doesn't reject the resulting null value. Entries with non-empty `hardcoded_consumers` are skipped to avoid breaking `pnpm install`; the skip is surfaced in the JSON fix output as `{"type": "remove_catalog_entry", "applied": false, "skipped": true, "skip_reason": "hardcoded_consumers", "consumers": [...]}`. The JSON action carries both `line` (first deleted line, the leading comment when policy absorbs one) and `entry_line` (the catalog entry's original 1-based line); use `entry_line` as a stable anchor across policy changes. After a successful catalog edit the CLI emits a one-line `Run pnpm install to refresh pnpm-lock.yaml` reminder, and the human stderr summary appends `(+M catalog comment lines)` to the fixed-issue count when comment lines were absorbed. The JSON envelope carries a top-level `"skipped"` count alongside `"total_fixed"` for partial-fix gating.
226
+ - Duplicate exports (appends an `ignoreExports` rule to your fallow config file). When no fallow config file exists, `.fallowrc.json` is created using the same scaffolding `fallow init` would emit (framework detection, `$schema`, `entry`, `ignorePatterns`, etc.) and the rules are layered on top. Inside a monorepo subpackage (`pnpm-workspace.yaml`, `package.json#workspaces`, `turbo.json`, `lerna.json`, or `rush.json` above the invocation directory) the create-fallback refuses to fire and emits `skip_reason: "monorepo_subpackage"` with a relative `workspace_root` path pointing at the workspace root. The applied entry carries `created_files: [".fallowrc.json"]` so consumers can detect file-creation side effects programmatically.
227
+
228
+ ### On-disk drift protection
229
+
230
+ `fallow fix` captures every parsed source file's xxh3 content hash during the in-process analysis and recomputes it at fix time. Files whose hash drifted between analysis and write (parallel editor save, CI rebase, concurrent tool) are skipped with `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}` in the JSON output and `Skipping <path>: file content changed since fallow check ran. Re-run fallow fix to refresh the analysis first.` on stderr (gated on non-quiet). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope's top-level `skipped_content_changed: number` is always present and disjoint from `skipped` (which still tallies catalog / YAML guard skips only). Per-file writes are batched: each rewrite is staged to a sibling temp file, and the orchestrator promotes the batch only after every stage succeeds. A stage failure leaves every target file at its original content. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); `package.json` and `pnpm-workspace.yaml` are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net.
231
+
232
+ ### Low-confidence export removals
233
+
234
+ Issue #602: `fallow fix` withholds unused-export removals when the consumer may be invisible to static analysis, because stripping a real export breaks `tsc` and the build. Two cases are skipped:
235
+
236
+ - **Off-graph consumer directories.** The file is under any of `__mocks__`, `__fixtures__`, `fixtures`, `e2e`, `e2e-tests`, `cypress`, `playwright`, `examples`, `evals`, `golden` (matched on any path segment). Catches Vitest mock aliases, off-workspace e2e suites, and fixture / golden harnesses. Plain `test` / `tests` / `__tests__` are deliberately NOT on the list, so genuinely-dead test helpers still auto-remove.
237
+ - **Files with an unresolved import.** The file itself imports something fallow could not resolve, so its local usage graph is incomplete.
238
+
239
+ JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"}` (or `"low_confidence_unresolved_imports"`) plus a top-level counter `skipped_low_confidence_exports: number` (always present), disjoint from `skipped`. Unlike the drift and encoding skips this is INTENTIONAL and does NOT change the exit code; the export stays reported by `fallow check` for manual review. High-confidence exports in normal source files are removed unchanged. The AI agent should report kept exports to the user and let them decide whether the export is truly unused before removing it by hand.
240
+
241
+ ### File encoding contract
242
+
243
+ `fallow fix` is UTF-8 only. Two encoding shapes that previously caused silent corruption are handled explicitly (issue #475):
244
+
245
+ - **UTF-8 BOM round-trip.** Files with a leading UTF-8 byte-order mark (`EF BB BF`, common on Windows-authored TypeScript) are read with the BOM stripped before line-offset computation and parsing, so reported line numbers do not shift by the BOM codepoint, and the BOM is re-prepended on write so the file's encoding is preserved on round-trip. fallow neither adds nor removes a BOM; if your input has one, the output has one.
246
+
247
+ - **Mixed CRLF / LF rejection.** Files containing both `\r\n` and bare-LF line endings (common after cross-platform edits without `core.autocrlf`) are skipped instead of silently rewritten to the wrong offsets. The stderr message names the remediation: `Skipping <path>: file has mixed CRLF/LF line endings. Normalize with dos2unix or set git config core.autocrlf input, then re-run fallow fix.`. JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "mixed_line_endings"}` plus a top-level counter `skipped_mixed_line_endings: number` (always present) disjoint from `skipped_content_changed`. Any non-zero mixed-EOL count exits the run with code 2.
248
+
249
+ **The skip is NOT self-healing**. Re-running `fallow fix` produces the same skip; the AI agent or user must run `dos2unix <path>` (or set `git config core.autocrlf input` and re-checkout) before fallow can act on the file. When the same file carries findings for multiple fixers (e.g. an unused export AND an unused enum member), the skip is reported once per file, not once per fixer.
250
+
251
+ ### Examples
252
+
253
+ ```bash
254
+ # Preview changes
255
+ fallow fix --dry-run --format json --quiet
256
+
257
+ # Apply changes (--yes required in agent/CI environments)
258
+ fallow fix --yes --format json --quiet
259
+ ```
260
+
261
+ ---
262
+
263
+ ## `list`: Project Introspection
264
+
265
+ Inspect discovered files, entry points, detected frameworks, and architecture boundary zones.
266
+
267
+ ### Flags
268
+
269
+ | Flag | Type | Description |
270
+ |------|------|-------------|
271
+ | `--files` | bool | List all discovered files |
272
+ | `--entry-points` | bool | List detected entry points |
273
+ | `--plugins` | bool | List active framework plugins |
274
+ | `--boundaries` | bool | Show architecture boundary zones, rules, per-zone file counts, and `logical_groups[]` for `autoDiscover` parents |
275
+ | `--workspaces` | bool | Show discovered monorepo workspaces plus any workspace-discovery diagnostics (malformed `package.json`, unreachable glob matches, missing tsconfig references). Available as the `fallow workspaces` alias too. |
276
+ | `--format` | `human\|json` | Output format |
277
+ | `--quiet` | bool | Suppress progress bars |
278
+
279
+ ### Examples
280
+
281
+ ```bash
282
+ fallow list --files --format json --quiet
283
+ fallow list --entry-points --format json --quiet
284
+ fallow list --plugins --format json --quiet
285
+ fallow list --boundaries --format json --quiet
286
+ fallow list --workspaces --format json --quiet
287
+ fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
288
+ ```
289
+
290
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow check --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
291
+
292
+ The `--boundaries` JSON output carries `boundaries.logical_groups[]` alongside the existing `zones[]` / `rules[]` arrays. Each logical-group entry surfaces a user-authored `autoDiscover` parent zone (which expansion otherwise flattens into per-child zones like `features/auth` / `features/billing`): `name`, `children`, `auto_discover` (verbatim user strings), `status` (`ok` / `empty` / `invalid_path`), `source_zone_index`, summed `file_count`, optional `authored_rule` (the pre-expansion `{ allow, allowTypeOnly }` keyed on the parent), optional `fallback_zone` cross-reference when the parent also kept its own `patterns` (Bulletproof case), optional `merged_from` (parent zone indices when the user declared the same parent name twice; surfaces the duplicate in JSON instead of only in `tracing::warn!`), optional `original_zone_root` (echo of the parent's `root` subtree scope for monorepo patchers), and optional `child_source_indices` (parallel to `children`, attributing each child to a specific `auto_discover` entry when multiple paths were authored). The full shape is documented in `docs/output-schema.json` under `ListBoundariesOutput`.
293
+
294
+ ---
295
+
296
+ ## `init`: Config Generation
297
+
298
+ Creates a config file in the project root.
299
+
300
+ ### Flags
301
+
302
+ | Flag | Type | Description |
303
+ |------|------|-------------|
304
+ | `--toml` | bool | Create `fallow.toml` instead of `.fallowrc.json` |
305
+ | `--hooks` | bool | Scaffold a pre-commit git hook that runs `fallow audit --base <ref> --quiet`. Alias for `fallow hooks install --target git` |
306
+ | `--branch` | string | Fallback base branch for the pre-commit hook when no upstream is set (default: `main`). Only used with `--hooks` |
307
+
308
+ ### Examples
309
+
310
+ ```bash
311
+ fallow init # creates .fallowrc.json with $schema
312
+ fallow init --toml # creates fallow.toml
313
+ fallow hooks install --target git
314
+ fallow hooks install --target git --branch develop # fallback base branch when no upstream is set
315
+ ```
316
+
317
+ ---
318
+
319
+ ## `migrate`: Config Migration
320
+
321
+ Migrates configuration from knip and/or jscpd to fallow. Auto-detects config files.
322
+
323
+ ### Flags
324
+
325
+ | Flag | Type | Description |
326
+ |------|------|-------------|
327
+ | `--toml` | bool | Output as `fallow.toml` (mutually exclusive with `--jsonc`) |
328
+ | `--jsonc` | bool | Write to `.fallowrc.jsonc` instead of `.fallowrc.json`. Same JSONC content either way; the `.jsonc` extension lets editors auto-detect JSON-with-comments syntax highlighting |
329
+ | `--dry-run` | bool | Preview without writing |
330
+ | `--from` | path | Specify source config file path |
331
+
332
+ Without `--jsonc` or `--toml`, fallow auto-mirrors the source extension: a `knip.jsonc` migration writes `.fallowrc.jsonc`, a `knip.json` migration writes `.fallowrc.json`.
333
+
334
+ ### Detected Source Configs
335
+
336
+ - `knip.json`, `knip.jsonc`, `.knip.json`, `.knip.jsonc`
337
+ - `package.json` embedded `knip` field
338
+ - `.jscpd.json`
339
+ - `package.json` embedded `jscpd` field
340
+
341
+ ### Examples
342
+
343
+ ```bash
344
+ fallow migrate --dry-run # preview
345
+ fallow migrate # auto-detect; mirrors source extension
346
+ fallow migrate --jsonc # force .fallowrc.jsonc output
347
+ fallow migrate --toml # output as fallow.toml
348
+ fallow migrate --from knip.jsonc
349
+ ```
350
+
351
+ ---
352
+
353
+ ## `health`: Function Complexity & File Health Analysis
354
+
355
+ Analyzes function complexity across the project using cyclomatic and cognitive complexity metrics. By default all sections are included (health score, complexity findings, file scores, hotspots, and refactoring targets). Use `--complexity`, `--file-scores`, `--hotspots`, `--targets`, or `--score` to show only specific sections.
356
+
357
+ Angular templates contribute synthetic `<template>` complexity findings whenever they use `@if`/`@for`/`@switch`/`@case`/`@defer (when ...)`/`@let` blocks, legacy structural directives (`*ngIf`, `*ngFor`), bound attributes (`[x]`, `(x)`, `bind-x`, `on-x`), or `{{ }}` interpolations. Both standalone external `.html` files referenced via `templateUrl` AND inline `@Component({ template: \`...\` })` literals are scanned. Inline-template findings anchor at the host `.ts` file's `@Component` decorator line and emit a `suppress-line` action with `// fallow-ignore-next-line complexity` (place the comment directly above the `@Component` decorator). External-template findings emit a `suppress-file` action with `<!-- fallow-ignore-file complexity -->` (place at the top of the `.html` file; HTML cannot express line-level comments). Tagged template literals containing `${...}` interpolations and `template:` properties bound to a variable are skipped (out of scope for the first cut).
358
+
359
+ ### Flags
360
+
361
+ | Flag | Type | Default | Description |
362
+ |------|------|---------|-------------|
363
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab\|badge` | `human` | Output format |
364
+ | `--quiet` | bool | `false` | Suppress progress bars |
365
+ | `--max-cyclomatic` | number | `20` | Fail if any function exceeds this cyclomatic complexity |
366
+ | `--max-cognitive` | number | `15` | Fail if any function exceeds this cognitive complexity |
367
+ | `--max-crap` | number | `30.0` | Fail if any function has CRAP score >= threshold. CRAP combines complexity with coverage (`CC^2 * (1 - cov/100)^3 + CC`). Pair with `--coverage` for accurate per-function CRAP; without Istanbul data fallow estimates coverage from the module graph. |
368
+ | `--top` | number | — | Only show the top N most complex functions (and file scores/hotspots/targets) |
369
+ | `--sort` | `cyclomatic\|cognitive\|lines\|severity` | `cyclomatic` | Sort order for complexity findings |
370
+ | `--complexity` | bool | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. |
371
+ | `--file-scores` | bool | `false` | Show only per-file health scores (maintainability index, LOC, fan-in, fan-out, dead code ratio, complexity density, CRAP risk). Runs the full analysis pipeline. Sorted by risk-aware triage concern: lower maintainability index and higher CRAP risk first. When no section flags are set, all sections are shown by default. |
372
+ | `--hotspots` | bool | `false` | Show only hotspots: files that are both complex and frequently changing. Combines git churn history with complexity data. Requires a git repository. When no section flags are set, all sections are shown by default. |
373
+ | `--targets` | bool | `false` | Show only refactoring targets: ranked recommendations based on complexity, coupling, churn, and dead code signals. Categories: churn+complexity, circular dep, high impact, dead code, complexity, coupling. When no section flags are set, all sections are shown by default. |
374
+ | `--effort` | `low\|medium\|high` | — | Filter refactoring targets by effort level. Implies `--targets`. |
375
+ | `--score` | bool | `false` | Show only the project health score (0-100) with letter grade (A/B/C/D/F). The score is included by default when no section flags are set. JSON includes `health_score` object with `score`, `grade`, and `penalties` breakdown. As of v2.55.0, plain `--score` skips the churn-backed hotspot penalty so it does not run a `git log` shell-out per invocation; pass `--hotspots` (or `--targets` with `--score`) to include the hotspot penalty. Snapshot (`--save-snapshot`) and trend (`--trend`) flows still trigger hotspot vital signs so saved data stays complete. |
376
+ | `--min-score` | number | — | Fail (exit 1) only when the health score is below this threshold. Implies `--score`. Authoritative CI quality gate: when set, complexity findings are demoted to informational and the exit code is driven solely by the score, so `--min-score 0` always exits 0. Composes with `--min-severity`. |
377
+ | `--min-severity` | `moderate\|high\|critical` | — | Only exit with an error for findings at or above this severity. Composes with `--min-score` (the run fails if either gate trips). |
378
+ | `--report-only` | bool | `false` | Print the score and findings but never fail CI (always exit 0). Advisory mode. Mutually exclusive with `--min-score` and `--min-severity`. |
379
+ | `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). |
380
+ | `--min-commits` | number | `3` | Minimum number of commits for a file to be included in hotspot ranking. |
381
+ | `--ownership` | bool | `false` | Attach ownership signals to hotspot entries: bus factor (Avelino truck factor), contributor count, top contributor with stale-days, recent contributors (top-3), `suggested_reviewers`, declared CODEOWNERS owner, `ownership_state`, ownership drift, unowned-hotspot detection. Human output gains a project-level summary line. JSON adds `low-bus-factor`, `unowned-hotspot`, `ownership-drift` action types. Test files get a `[test]` tag. Implies `--hotspots`. Requires git. |
382
+ | `--ownership-emails` | `raw\|handle\|anonymized\|hash` | `handle` | Privacy mode for author emails. `handle` shows the local-part only (default, with GitHub noreply unwrap and deterministic same-handle disambiguation). `anonymized` emits stable `xxh3:` pseudonyms; `hash` remains accepted as the legacy spelling. `raw` shows full addresses. Use `anonymized` in regulated environments. Implies `--ownership`. Configure default via `health.ownership.emailMode`. |
383
+ | `--changed-since` | string | — | Only analyze files changed since a git ref |
384
+ | `--workspace` | string | — | Scope to one or more workspaces. Comma-separated values, globs (`apps/*`, `@scope/*`), and `!`-prefixed negation (`!apps/legacy`) supported. Matched against package name AND workspace path relative to repo root. Vital signs, health score, hotspots, file scores, findings, and `summary.files_analyzed` are all recomputed against the scoped subset. |
385
+ | `--group-by` | `owner\|directory\|package\|section` | — | Partition the report into per-group sections. JSON adds `grouped_by` plus a `groups` array; each group contains its own `vital_signs`, `health_score`, `findings`, `file_scores`, `hotspots`, `large_functions`, and `targets` recomputed against the group's files. The top-level metrics stay project-wide so consumers that ignore grouping still see the project headline. Human output adds a per-group score / files / hot / p90 summary block (sorted worst-first when `--score`). SARIF results carry `properties.group` and CodeClimate issues carry a top-level `group` field so GitHub Code Scanning / GitLab Code Quality can partition per team / package. Compact, markdown, and badge fall back to ungrouped output with a stderr note. |
386
+ | `--baseline` | path | — | Compare against a saved baseline. When set, the JSON `actions` array on each finding omits `suppress-line` (the baseline already suppresses) and the report root carries an `actions_meta: { suppression_hints_omitted: true, reason: "baseline-active" }` breadcrumb. |
387
+ | `--save-baseline` | path | — | Save current results as a baseline. Same `suppress-line` omission as `--baseline`. |
388
+ | `--save-snapshot` | path (optional) | `.fallow/snapshots/<timestamp>.json` | Save vital signs snapshot for trend tracking. Forces file-scores + hotspot computation. |
389
+ | `--trend` | bool | `false` | Compare current metrics against the most recent saved snapshot. Reads from `.fallow/snapshots/` and shows per-metric deltas with directional indicators (improving/declining/stable). Implies `--score`. |
390
+ | `--coverage-gaps` | bool | `false` | Show runtime files and exports that no test dependency path reaches. Opt-in (default off). Configure severity via the `coverage-gaps` rule (`error`/`warn`/`off`). |
391
+ | `--coverage` | path | none | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores. Uses `CC^2 * (1-cov/100)^3 + CC` instead of static binary model. Relative paths resolve against `--root`. |
392
+ | `--coverage-root` | path | none | Absolute prefix to strip from file paths in coverage data before prepending the project root. For CI/Docker environments where coverage was generated with different absolute paths. |
393
+ | `--runtime-coverage` | path | none | Merge runtime-coverage input into the health report. Accepts a V8 coverage directory (`NODE_V8_COVERAGE=...`), a single V8 coverage JSON file, or an Istanbul `coverage-final.json`. One local capture is free and does not require a license; continuous/cloud or multi-capture runtime monitoring requires an active license or trial (`fallow license activate --trial --email <addr>`). JSON output gains a `runtime_coverage` object with a top-level report verdict, per-finding `verdict` (`safe_to_delete` / `review_required` / `low_traffic` / `coverage_unavailable` / `active`), a per-finding suppression `id` (`fallow:prod:<hash>`, hashes the current line), an optional cross-surface `stable_id` join key (`fallow:fn:<hash>`, hashes file + name + start line; one value per function across findings / hot-paths / blast-radius / importance and across V8/Istanbul/oxc producers), an optional content-digest `source_hash` (line-move-immune, so baselines survive a pure line shift), an evidence block, and percentile-ranked hot paths. On protocol-0.3+ sidecars the `summary` also carries an optional `capture_quality` block (`window_seconds`, `instances_observed`, `lazy_parse_warning`, `untracked_ratio_percent`) that flags short-window captures where lazy-parsed scripts may not appear. |
394
+ | `--min-invocations-hot` | number | `100` | Invocation threshold for hot-path classification. Takes effect only when `--runtime-coverage` is set. |
395
+ | `--min-observation-volume` | number | `5000` | Minimum total trace volume before the sidecar may emit high-confidence `safe_to_delete` / `review_required` verdicts. Below this, confidence is capped at `medium`. |
396
+ | `--low-traffic-threshold` | number | `0.001` | Fraction of total trace count below which an invoked function is classified `low_traffic` rather than `active`. Expressed as a decimal (0.001 = 0.1%). |
397
+
398
+ ### Exit Codes
399
+
400
+ The gate flag in play determines what drives the exit code. Plain `fallow health` (no gate flag) stays advisory but still fails on any finding (back-compat).
401
+
402
+ | Invocation | Exit 0 when | Exit 1 when |
403
+ |------------|-------------|-------------|
404
+ | `fallow health` (no gate flag) | no function exceeds a threshold | any function exceeds a threshold |
405
+ | `--min-score N` | score >= N (findings informational) | score < N |
406
+ | `--min-severity LEVEL` | no finding at or above LEVEL | any finding at or above LEVEL |
407
+ | `--min-score N --min-severity LEVEL` | score >= N AND no finding >= LEVEL | score < N OR a finding >= LEVEL |
408
+ | `--report-only` | always | never |
409
+
410
+ `--report-only` with `--min-score` / `--min-severity` exits 2 (mutually exclusive). The `--runtime-coverage` and coverage-gap gates stay independent and are not demoted by `--min-score`. For gating only newly-introduced complexity, use `fallow audit --gate new-only`.
411
+
412
+ ### Examples
413
+
414
+ ```bash
415
+ # Full complexity analysis with JSON output
416
+ fallow health --format json --quiet
417
+
418
+ # Project health score with letter grade
419
+ fallow health --format json --quiet --score
420
+
421
+ # CI gate: fail if score below 70
422
+ fallow health --format json --quiet --min-score 70
423
+
424
+ # Top 10 most complex functions
425
+ fallow health --format json --quiet --top 10
426
+
427
+ # Sort by cognitive complexity
428
+ fallow health --format json --quiet --sort cognitive
429
+
430
+ # Custom thresholds
431
+ fallow health --format json --quiet --max-cyclomatic 15 --max-cognitive 10
432
+
433
+ # Per-file health scores
434
+ fallow health --format json --quiet --file-scores
435
+
436
+ # Top 20 files by triage concern
437
+ fallow health --format json --quiet --file-scores --top 20
438
+
439
+ # Only analyze files changed since main
440
+ fallow health --format json --quiet --changed-since main
441
+
442
+ # Single workspace package
443
+ fallow health --format json --quiet --workspace my-package
444
+
445
+ # Incremental adoption with baseline
446
+ fallow health --format json --quiet --save-baseline fallow-baselines/health.json
447
+ fallow health --format json --quiet --baseline fallow-baselines/health.json
448
+
449
+ # CI: fail if any function is too complex
450
+ fallow health --max-cyclomatic 25 --max-cognitive 20 --quiet
451
+
452
+ # Hotspot analysis (complex + frequently changing files)
453
+ fallow health --format json --quiet --hotspots
454
+
455
+ # Hotspots from the last year
456
+ fallow health --format json --quiet --hotspots --since 1y
457
+
458
+ # Hotspots with at least 5 commits
459
+ fallow health --format json --quiet --hotspots --min-commits 5
460
+
461
+ # Top 10 hotspots from the last 90 days
462
+ fallow health --format json --quiet --hotspots --since 90d --top 10
463
+
464
+ # Ranked refactoring recommendations
465
+ fallow health --format json --quiet --targets
466
+
467
+ # Top 5 refactoring targets
468
+ fallow health --format json --quiet --targets --top 5
469
+
470
+ # Only low-effort refactoring targets (quick wins)
471
+ fallow health --format json --quiet --effort low
472
+
473
+ # Save a vital signs snapshot for trend tracking
474
+ fallow health --format json --quiet --save-snapshot
475
+
476
+ # Save snapshot to a custom path
477
+ fallow health --format json --quiet --save-snapshot .fallow/baseline-snapshot.json
478
+
479
+ # Compare current metrics against the most recent snapshot
480
+ fallow health --format json --quiet --trend
481
+ ```
482
+
483
+ ### JSON Output Structure
484
+
485
+ ```json
486
+ {
487
+ "kind": "health",
488
+ "schema_version": 7,
489
+ "version": "2.88.2",
490
+ "elapsed_ms": 32,
491
+ "summary": {
492
+ "files_analyzed": 482,
493
+ "functions_analyzed": 3200,
494
+ "functions_above_threshold": 3,
495
+ "max_cyclomatic_threshold": 20,
496
+ "max_cognitive_threshold": 15
497
+ },
498
+ "findings": [
499
+ {
500
+ "path": "src/parser.ts",
501
+ "name": "parseExpression",
502
+ "line": 42,
503
+ "col": 0,
504
+ "cyclomatic": 28,
505
+ "cognitive": 22,
506
+ "line_count": 95,
507
+ "exceeded": "both"
508
+ }
509
+ ]
510
+ }
511
+ ```
512
+
513
+ When the unit size very-high-risk percentage is >= 3%, the JSON output includes a `large_functions` array listing functions exceeding 60 lines of code:
514
+
515
+ ```json
516
+ {
517
+ "large_functions": [
518
+ {
519
+ "path": "src/parser.ts",
520
+ "name": "parseExpression",
521
+ "line": 42,
522
+ "line_count": 95
523
+ }
524
+ ]
525
+ }
526
+ ```
527
+
528
+ This drill-down shows which specific functions are driving the unit size penalty in the health score, making it actionable without a separate analysis pass.
529
+
530
+ With `--file-scores`, the JSON output also includes `file_scores` array and `summary.files_scored` / `summary.average_maintainability`:
531
+
532
+ ```json
533
+ {
534
+ "summary": {
535
+ "files_scored": 482,
536
+ "average_maintainability": 88.5,
537
+ "coverage_model": "static_estimated",
538
+ "coverage_source_consistency": "uniform"
539
+ },
540
+ "file_scores": [
541
+ {
542
+ "path": "src/parser.ts",
543
+ "fan_in": 8,
544
+ "fan_out": 4,
545
+ "dead_code_ratio": 0.25,
546
+ "complexity_density": 0.22,
547
+ "maintainability_index": 75.1,
548
+ "total_cyclomatic": 42,
549
+ "total_cognitive": 35,
550
+ "function_count": 12,
551
+ "lines": 190,
552
+ "crap_max": 42.0,
553
+ "crap_above_threshold": 2
554
+ }
555
+ ]
556
+ }
557
+ ```
558
+
559
+ The `file_scores` array is sorted by risk-aware triage concern: the larger of low-MI concern and CRAP risk. This keeps files with very high untested complexity near the top even when their Maintainability Index is not the lowest.
560
+
561
+ The `crap_max` field is the highest CRAP (Change Risk Anti-Patterns) score among functions in the file, using the canonical formula `CC^2 * (1 - cov/100)^3 + CC`. The default model (`static_estimated`) estimates per-function coverage from export references: directly test-referenced = 85%, indirectly test-reachable = 40%, untested = 0%. Provide `--coverage <path>` with Istanbul-format `coverage-final.json` for exact scores (`istanbul` model). The `crap_above_threshold` field counts functions with CRAP >= 30. When `--file-scores` is active, `summary.coverage_model` indicates the model used (`"static_estimated"` or `"istanbul"`). When CRAP findings carry `coverage_source`, `summary.coverage_source_consistency` is `uniform` or `mixed`; grouped health JSON mirrors this as `groups[].coverage_source_consistency`.
562
+
563
+ Maintainability index formula: `100 - (complexity_density × 30) - (dead_code_ratio × 20) - min(ln(fan_out+1) × 4, 15)`, clamped to 0–100. Higher is better. Type-only exports are excluded from dead_code_ratio. Zero-function files (barrels) are excluded by default.
564
+
565
+ With `--hotspots`, the JSON output includes a `hotspots` array and `hotspot_summary`:
566
+
567
+ ```json
568
+ {
569
+ "hotspot_summary": {
570
+ "since": "6m",
571
+ "min_commits": 3,
572
+ "files_analyzed": 482,
573
+ "files_excluded": 312,
574
+ "shallow_clone": false
575
+ },
576
+ "hotspots": [
577
+ {
578
+ "path": "src/parser.ts",
579
+ "score": 92,
580
+ "commits": 28,
581
+ "weighted_commits": 34.5,
582
+ "lines_added": 410,
583
+ "lines_deleted": 180,
584
+ "complexity_density": 0.22,
585
+ "fan_in": 8,
586
+ "trend": "Accelerating"
587
+ }
588
+ ]
589
+ }
590
+ ```
591
+
592
+ Hotspot score formula: `normalized_churn × normalized_complexity × 100`, scaled 0–100. Higher means more urgent to refactor. The `trend` field indicates recent change velocity: `Accelerating` (increasing churn), `Stable` (constant), or `Cooling` (decreasing). Files below `--min-commits` are excluded. The `shallow_clone` field warns when git history is truncated (shallow clone), which may undercount commits.
593
+
594
+ With `--targets`, the JSON output includes a `targets` array with ranked refactoring recommendations:
595
+
596
+ ```json
597
+ {
598
+ "targets": [
599
+ {
600
+ "path": "src/parser.ts",
601
+ "priority": 82.5,
602
+ "efficiency": 27.5,
603
+ "recommendation": "Split high-impact file — 25 dependents amplify every change",
604
+ "category": "split_high_impact",
605
+ "effort": "high",
606
+ "confidence": "medium",
607
+ "factors": [
608
+ {
609
+ "metric": "complexity_density",
610
+ "value": 0.75,
611
+ "threshold": 0.3,
612
+ "detail": "density 0.75 exceeds 0.3"
613
+ },
614
+ {
615
+ "metric": "fan_in",
616
+ "value": 25.0,
617
+ "threshold": 10.0,
618
+ "detail": "25 files depend on this"
619
+ }
620
+ ]
621
+ }
622
+ ],
623
+ "target_thresholds": {
624
+ "fan_in_p95": 12.0,
625
+ "fan_in_p75": 5.0,
626
+ "fan_out_p95": 15.0,
627
+ "fan_out_p90": 8
628
+ }
629
+ }
630
+ ```
631
+
632
+ Targets are sorted by `efficiency` (priority / effort_numeric) descending, surfacing quick wins first. The `target_thresholds` object exposes the adaptive percentile-based thresholds used for scoring. Priority formula: `min(complexity_density, 1) x 30 + hotspot_boost x 25 + dead_code_ratio x 20 + fan_in_norm x 15 + fan_out_norm x 10`, clamped to 0-100. Fan-in and fan-out normalization uses the project's p95 values (with floors). Categories: `urgent_churn_complexity`, `break_circular_dependency`, `split_high_impact`, `remove_dead_code`, `extract_complex_functions`, `extract_dependencies`, `add_test_coverage`. Each target includes `efficiency`, `effort` (low/medium/high), `confidence` (high/medium/low, data source reliability), and contributing `factors`.
633
+
634
+ The `add_test_coverage` category fires when a file has 2+ functions with CRAP scores >= 30 and complexity density > 0.3. The `crap_max` metric appears in contributing factors for these targets.
635
+
636
+ ### Vital Signs
637
+
638
+ All `health` JSON output includes a `vital_signs` object with project-wide metrics:
639
+
640
+ ```json
641
+ {
642
+ "vital_signs": {
643
+ "dead_file_pct": 3.2,
644
+ "dead_export_pct": 8.1,
645
+ "avg_cyclomatic": 4.5,
646
+ "critical_complexity_pct": 1.2,
647
+ "p90_cyclomatic": 12,
648
+ "maintainability_avg": 88.5,
649
+ "maintainability_low_pct": 4.1,
650
+ "hotspot_count": 7,
651
+ "hotspot_top_pct_count": 3,
652
+ "circular_dep_count": 2,
653
+ "circular_deps_per_k_files": 4.1,
654
+ "unused_dep_count": 3,
655
+ "unused_deps_per_k_files": 6.2,
656
+ "unit_size_profile": {
657
+ "low_risk": 82.1,
658
+ "medium_risk": 11.4,
659
+ "high_risk": 4.3,
660
+ "very_high_risk": 2.2
661
+ },
662
+ "functions_over_60_loc_per_k": 22.0,
663
+ "unit_interfacing_profile": {
664
+ "low_risk": 95.6,
665
+ "medium_risk": 3.8,
666
+ "high_risk": 0.5,
667
+ "very_high_risk": 0.1
668
+ },
669
+ "p95_fan_in": 8,
670
+ "coupling_high_pct": 2.3
671
+ }
672
+ }
673
+ ```
674
+
675
+ Fields are `null` when the corresponding data source is not available (e.g., `hotspot_count` is null without `--hotspots` or when git is not available). Health score formula v2 also uses scale-invariant density/tail fields: `critical_complexity_pct`, `hotspot_top_pct_count`, `maintainability_low_pct`, `unused_deps_per_k_files`, `circular_deps_per_k_files`, and `functions_over_60_loc_per_k`. The `unit_size_profile` and `unit_interfacing_profile` are risk distribution histograms (low risk / medium risk / high risk / very high risk as percentages). `p95_fan_in` is the 95th percentile of incoming dependencies. `coupling_high_pct` is the percentage of files above the effective coupling threshold.
676
+
677
+ With `--score`, the JSON output includes a `health_score` object:
678
+
679
+ ```json
680
+ {
681
+ "health_score": {
682
+ "formula_version": 2,
683
+ "score": 76.9,
684
+ "grade": "B",
685
+ "penalties": {
686
+ "dead_files": 3.1,
687
+ "dead_exports": 6.0,
688
+ "complexity": 0.0,
689
+ "p90_complexity": 0.0,
690
+ "maintainability": 0.0,
691
+ "unused_deps": 10.0,
692
+ "circular_deps": 4.0,
693
+ "unit_size": 0.0,
694
+ "coupling": 0.0,
695
+ "duplication": 4.0
696
+ }
697
+ }
698
+ }
699
+ ```
700
+
701
+ Score is reproducible: `100 - sum(penalties) == score`. `formula_version` identifies the scoring formula; version 2 uses scale-invariant density and tail metrics for monorepo-safe scoring. Penalty fields are absent when the pipeline didn't run. `--score` automatically runs duplication analysis; add `--hotspots` (or combine `--score --targets`) when the score should include the churn-backed hotspot penalty. Grades: A (>= 85), B (70-84), C (55-69), D (40-54), F (< 40).
702
+
703
+ ### Health Trend
704
+
705
+ With `--trend`, the JSON output includes a `health_trend` object comparing current metrics against the most recent saved snapshot:
706
+
707
+ ```json
708
+ {
709
+ "health_trend": {
710
+ "compared_to": {
711
+ "timestamp": "2026-03-25T14:30:00Z",
712
+ "git_sha": "a1b2c3d",
713
+ "score": 74.2,
714
+ "grade": "B"
715
+ },
716
+ "metrics": [
717
+ {
718
+ "name": "score",
719
+ "label": "Health Score",
720
+ "previous": 74.2,
721
+ "current": 76.9,
722
+ "delta": 2.7,
723
+ "direction": "improving",
724
+ "unit": ""
725
+ },
726
+ {
727
+ "name": "dead_file_pct",
728
+ "label": "Dead Files",
729
+ "previous": 5.1,
730
+ "current": 4.2,
731
+ "delta": -0.9,
732
+ "direction": "improving",
733
+ "unit": "%",
734
+ "previous_count": { "value": 13, "total": 255 },
735
+ "current_count": { "value": 11, "total": 262 }
736
+ }
737
+ ],
738
+ "snapshots_loaded": 3,
739
+ "overall_direction": "improving"
740
+ }
741
+ }
742
+ ```
743
+
744
+ Metrics tracked: `score`, `dead_file_pct`, `dead_export_pct`, `avg_cyclomatic`, `maintainability_avg`, `unused_dep_count`, `circular_dep_count`, `hotspot_count`, `unit_size_very_high_pct`, `p95_fan_in`, `duplication_pct`. Each metric includes `direction` (`improving`, `declining`, `stable`). Percentage metrics include `previous_count`/`current_count` with raw numerator/denominator. `--trend` requires at least one saved snapshot in `.fallow/snapshots/`. When comparing against a snapshot from an older schema version (current: v8), the trend output warns that score deltas may reflect formula changes.
745
+
746
+ ### Vital Signs Snapshots
747
+
748
+ `--save-snapshot` persists a `VitalSignsSnapshot` JSON file for trend tracking across runs. Snapshots automatically include the health score and grade. The snapshot contains more detail than the inline `vital_signs` object:
749
+
750
+ ```json
751
+ {
752
+ "snapshot_schema_version": 8,
753
+ "timestamp": "2025-12-01T10:30:00Z",
754
+ "vital_signs": {
755
+ "dead_file_pct": 3.2,
756
+ "dead_export_pct": 8.1,
757
+ "avg_cyclomatic": 4.5,
758
+ "critical_complexity_pct": 1.2,
759
+ "p90_cyclomatic": 12,
760
+ "maintainability_avg": 88.5,
761
+ "maintainability_low_pct": 4.1,
762
+ "hotspot_count": 7,
763
+ "hotspot_top_pct_count": 3,
764
+ "circular_dep_count": 2,
765
+ "circular_deps_per_k_files": 4.1,
766
+ "unused_dep_count": 3,
767
+ "unused_deps_per_k_files": 6.2,
768
+ "functions_over_60_loc_per_k": 22.0
769
+ },
770
+ "counts": {
771
+ "total_files": 482,
772
+ "dead_files": 15,
773
+ "total_exports": 1200,
774
+ "dead_exports": 97,
775
+ "total_dependencies": 42,
776
+ "unused_dependencies": 3
777
+ },
778
+ "git_sha": "abc1234",
779
+ "git_branch": "main",
780
+ "shallow_clone": false
781
+ }
782
+ ```
783
+
784
+ The snapshot `snapshot_schema_version` is independent of the report `schema_version`. Default path: `.fallow/snapshots/<timestamp>.json`. The `--save-snapshot` flag forces file-scores and hotspot computation to populate all vital signs fields.
785
+
786
+ ---
787
+
788
+ ## `audit`: Changed-File Quality Gate
789
+
790
+ Audits changed files for dead code, complexity, and duplication. Returns a verdict (pass/warn/fail). Purpose-built for PR quality gates and reviewing AI-generated code. Auto-detects the base branch if `--base` is not set. Defaults to `--gate new-only`, which fails only on findings introduced by the current changeset and reports inherited findings as context.
791
+
792
+ ### Flags
793
+
794
+ | Flag | Type | Default | Description |
795
+ |------|------|---------|-------------|
796
+ | `--base` | string | auto-detect | Git ref to compare against (alias for `--changed-since`) |
797
+ | `--gate` | `new-only\|all` | `new-only` | Which findings affect the verdict. `new-only` gates only introduced findings; `all` gates every finding in changed files and skips the extra base-snapshot attribution pass. |
798
+ | `--production` | bool | false | Exclude test/story/dev files (applies to dead-code, health, and dupes) |
799
+ | `--production-dead-code` | bool | false | Per-analysis production mode for the dead-code sub-analysis only |
800
+ | `--production-health` | bool | false | Per-analysis production mode for the health sub-analysis only |
801
+ | `--production-dupes` | bool | false | Per-analysis production mode for the duplication sub-analysis only |
802
+ | `-w, --workspace` | string | — | Scope to one or more workspaces. Comma-separated, globs, `!` negation. |
803
+ | `--explain` | bool | false | JSON: include metric definitions in `_meta`. Human: print a `Description:` line under each section header. |
804
+ | `--ci` | bool | false | Equivalent to `--format sarif --fail-on-issues --quiet` |
805
+ | `--fail-on-issues` | bool | false | Exit with code 1 if issues are found |
806
+ | `--sarif-file` | path | — | Write SARIF output to a file alongside primary format |
807
+ | `--dead-code-baseline` | path | — | Baseline file (produced by `fallow dead-code --save-baseline`). Pre-existing dead-code issues are excluded from the verdict. |
808
+ | `--health-baseline` | path | — | Baseline file (produced by `fallow health --save-baseline`). Pre-existing complexity findings are excluded from the verdict. |
809
+ | `--dupes-baseline` | path | — | Baseline file (produced by `fallow dupes --save-baseline`). Pre-existing clone groups are excluded from the verdict. |
810
+ | `--max-crap` | number | `30.0` | Forwarded to the health sub-analysis. Functions meeting or exceeding this CRAP score cause audit to fail. Same formula as `health --max-crap`. Pair with coverage data for accurate per-function CRAP. |
811
+ | `--coverage` | path | none | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores in the health sub-analysis. Same format and semantics as `health --coverage`. Also configurable via `FALLOW_COVERAGE`. Relative paths resolve against `--root`. |
812
+ | `--coverage-root` | path | none | Absolute prefix to strip from file paths in coverage data before prepending the project root. Use when coverage was generated under a different checkout root in CI / Docker (e.g., `/home/runner/work/myapp` on GitHub Actions). |
813
+ | `--fail-on-regression` | bool | false | Fail if issues increased beyond tolerance vs regression baseline |
814
+ | `--tolerance` | string | `0` | Allowed increase before regression fails (`N` or `N%`) |
815
+ | `--regression-baseline` | path | `.fallow/regression-baseline.json` | Path to the regression baseline file |
816
+ | `--save-regression-baseline` | path | — | Save current issue counts as a regression baseline |
817
+
818
+ ### Verdicts
819
+
820
+ | Verdict | Exit code | When |
821
+ |---------|-----------|------|
822
+ | pass | 0 | No issues in changed files |
823
+ | warn | 0 | Issues found, all warn-severity |
824
+ | fail | 1 | Error-severity issues found |
825
+ | error | 2 | Runtime error (invalid ref, not a git repo) |
826
+
827
+ With `--gate new-only`, inherited error-severity findings can be present in the JSON output while the verdict remains `pass`; check the `attribution` object and per-finding `introduced` booleans.
828
+
829
+ ### JSON contract: which fields are severity-aware
830
+
831
+ | Field | Severity-aware? | What it counts |
832
+ |-------|-----------------|----------------|
833
+ | `verdict` | **yes** | Overall outcome honoring per-rule severity (`pass` / `warn` / `fail`) |
834
+ | `attribution.*_introduced` | no | Findings introduced by the changeset under `gate: new-only`, ignoring severity |
835
+ | `summary.*` | no | All findings in changed files, ignoring severity |
836
+ | Per-finding `introduced` | no | Whether each finding was introduced by the changeset |
837
+
838
+ For CI gating and any "did this PR pass?" question, read `verdict` (or exit code). Counting introduced findings ignores severity and breaks projects with `unused-exports: warn`. For agent triage, read `verdict` first, then `attribution` for new-vs-inherited counts, then the per-category finding arrays for actionable details.
839
+
840
+ ### Examples
841
+
842
+ ```bash
843
+ # Auto-detect base branch
844
+ fallow audit --format json --quiet
845
+
846
+ # Explicit base ref
847
+ fallow audit --format json --quiet --base main
848
+
849
+ # Audit last 3 commits
850
+ fallow audit --format json --quiet --base HEAD~3
851
+
852
+ # Strict mode: fail on inherited findings too
853
+ fallow audit --format json --quiet --gate all
854
+
855
+ # Production code only in a monorepo workspace
856
+ fallow audit --format json --quiet --production --workspace @app/api
857
+
858
+ # Production-only health, full-tree dead-code and dupes
859
+ fallow audit --format json --quiet --production-health --workspace @app/api
860
+
861
+ # CI mode (SARIF + fail on issues + quiet)
862
+ fallow audit --ci
863
+
864
+ # Per-analysis baselines: only fail on genuinely new issues
865
+ fallow audit \
866
+ --dead-code-baseline fallow-baselines/dead-code.json \
867
+ --health-baseline fallow-baselines/health.json \
868
+ --dupes-baseline fallow-baselines/dupes.json
869
+ # Or set these under `audit.*Baseline` in .fallowrc.json so `fallow audit` picks them up with no flags.
870
+ # The global --baseline / --save-baseline flags are REJECTED on audit (exit 2) because each sub-analysis uses a different baseline format.
871
+ ```
872
+
873
+ ### JSON Output Structure
874
+
875
+ ```json
876
+ {
877
+ "kind": "audit",
878
+ "schema_version": 7,
879
+ "version": "2.88.2",
880
+ "command": "audit",
881
+ "verdict": "fail",
882
+ "changed_files_count": 12,
883
+ "base_ref": "main",
884
+ "head_sha": "d4a2f91",
885
+ "elapsed_ms": 2140,
886
+ "summary": {
887
+ "dead_code_issues": 2,
888
+ "dead_code_has_errors": true,
889
+ "complexity_findings": 1,
890
+ "max_cyclomatic": 28,
891
+ "duplication_clone_groups": 0
892
+ },
893
+ "attribution": {
894
+ "gate": "new-only",
895
+ "dead_code_introduced": 2,
896
+ "dead_code_inherited": 0,
897
+ "complexity_introduced": 1,
898
+ "complexity_inherited": 0,
899
+ "duplication_introduced": 0,
900
+ "duplication_inherited": 0
901
+ },
902
+ "dead_code": {
903
+ "schema_version": 3,
904
+ "total_issues": 2,
905
+ "unused_exports": [{ "path": "src/api.ts", "export_name": "oldApi", "introduced": true, "actions": [...] }]
906
+ },
907
+ "complexity": {
908
+ "findings": [...]
909
+ },
910
+ "duplication": {
911
+ "clone_groups": []
912
+ }
913
+ }
914
+ ```
915
+
916
+ The `verdict` field is always present and is the primary decision signal. With the default `new-only` gate, the `attribution` object counts introduced vs inherited findings and audit sub-results annotate individual findings with `introduced: true/false`. With `gate=all`, audit skips that extra base-snapshot attribution pass, so introduced/inherited counts stay `0` and per-finding `introduced` fields are omitted. Dead code, complexity, and duplication sections follow their respective schemas from the individual commands. Thresholds for complexity are inherited from `fallow health` config (defaults: cyclomatic 20, cognitive 15).
917
+
918
+ Audit creates a temporary git worktree to compare against the base ref. When the current checkout has `node_modules`, audit links it into the base worktree so tsconfig `extends` chains into installed packages and path aliases resolve like the working tree. The worktree is removed on normal exit. If the process is force-killed, run `git worktree prune` to clean up stale `.git/worktrees/fallow-audit-base-*` entries.
919
+
920
+ ---
921
+
922
+ ## `flags`: Feature Flag Detection
923
+
924
+ Detects feature flag patterns in the codebase. Identifies environment variable flags (`process.env.FEATURE_*`), SDK calls from common providers (LaunchDarkly, Statsig, Unleash, GrowthBook, Split, PostHog, Vercel Flags, ConfigCat, Flagsmith, Optimizely, Eppo), and config object patterns (opt-in). Reports flag locations, detection confidence, and cross-references with dead code findings.
925
+
926
+ ### Flags
927
+
928
+ | Flag | Type | Default | Description |
929
+ |------|------|---------|-------------|
930
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
931
+ | `--quiet` | bool | `false` | Suppress progress bars |
932
+ | `--top` | number | — | Show only the top N flags |
933
+
934
+ ### Examples
935
+
936
+ ```bash
937
+ # Detect all feature flags with JSON output
938
+ fallow flags --format json --quiet
939
+
940
+ # Top 10 flags
941
+ fallow flags --format json --quiet --top 10
942
+
943
+ # Single workspace package
944
+ fallow flags --format json --quiet --workspace my-package
945
+ ```
946
+
947
+ ### JSON Output Structure
948
+
949
+ ```json
950
+ {
951
+ "schema_version": 7,
952
+ "version": "2.88.2",
953
+ "elapsed_ms": 116,
954
+ "feature_flags": [],
955
+ "total_flags": 0
956
+ }
957
+ ```
958
+
959
+ ---
960
+
961
+ ## `security`: Security Candidate Detection
962
+
963
+ Surfaces local security candidates for agent or human verification. The first rule, `client-server-leak`, starts at `"use client"` files and reports a candidate when that client boundary directly reads, or statically imports a path to a module that reads, a non-public `process.env` value.
964
+
965
+ Findings are not confirmed vulnerabilities. Use the structural trace to verify whether the value can actually reach client-bundled code. Public env conventions (`NODE_ENV`, `NEXT_PUBLIC_*`, `VITE_*`, `NUXT_PUBLIC_*`, `REACT_APP_*`, `PUBLIC_*`, `GATSBY_*`, `EXPO_PUBLIC_*`, `STORYBOOK_*`) are excluded.
966
+
967
+ The second rule family is a data-driven `tainted-sink` catalogue: syntactic dangerous-sink candidates across 9 CWE categories. A candidate fires only when the relevant argument is non-literal, so a fully-literal value (`el.innerHTML = "<b>x</b>"`, `child_process.exec("ls")`) never fires; fallow prefers false-negatives over false-positives.
968
+
969
+ | Category | CWE | Sink |
970
+ |----------|-----|------|
971
+ | `dangerous-html` | 79 | `innerHTML` / `outerHTML` / `insertAdjacentHTML` / `dangerouslySetInnerHTML` |
972
+ | `command-injection` | 78 | `child_process` `exec` / `execSync` / `spawn` / `spawnSync` (provenance-gated to `node:child_process`) |
973
+ | `code-injection` | 94 | `eval` / `vm.runInNewContext` |
974
+ | `sql-injection` | 89 | string concat or interpolated template into `.query()` / `.execute()`, and `sql.raw(...)`. Parameterized `` sql`${x}` `` and the object form `.execute({ sql, args })` are NOT flagged |
975
+ | `ssrf` | 918 | `fetch` / `axios` / `http(s).request` |
976
+ | `path-traversal` | 22 | `fs.*` / `path.join` / `path.resolve` |
977
+ | `open-redirect` | 601 | `res.redirect` |
978
+ | `weak-crypto` | 327 | runtime-selectable hash / cipher algorithm |
979
+ | `unsafe-deserialization` | 502 | `js-yaml` `load` / `node-serialize` |
980
+
981
+ Build-config and test files are excluded from candidate generation. Both rule families default to `off` and are surfaced only by `fallow security`, never under bare `fallow` or the `audit` gate. Scope which catalogue categories run with `security.categories` include / exclude lists in config.
982
+
983
+ ### Flags
984
+
985
+ | Flag | Type | Default | Description |
986
+ |------|------|---------|-------------|
987
+ | `--format` | `human\|json\|sarif` | `human` | Output format |
988
+ | `--quiet` | bool | `false` | Suppress progress output |
989
+ | `--summary` | bool | `false` | Show a compact human summary |
990
+ | `--ci` | bool | `false` | Equivalent to `--format sarif --fail-on-issues --quiet` |
991
+ | `--fail-on-issues` | bool | `false` | Exit 1 when candidates are found |
992
+ | `--sarif-file` | path | none | Write SARIF in addition to the primary output |
993
+ | `--changed-since` | git ref | none | Scope to candidates whose client anchor or trace hops touch changed files |
994
+ | `--diff-file` | path | none | Scope candidates to added hunks on the client anchor or import trace. Secret-source hops use file-level retention because member-access spans are not yet stored. Use `-` for stdin |
995
+ | `--workspace` | string | none | Scope to selected workspace packages |
996
+ | `--changed-workspaces` | git ref | none | Scope to workspaces changed since a git ref |
997
+
998
+ ### Examples
999
+
1000
+ ```bash
1001
+ fallow security --format json --quiet
1002
+ fallow security --ci --sarif-file fallow-security.sarif
1003
+ git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
1004
+ ```
1005
+
1006
+ ### JSON Output Structure
1007
+
1008
+ ```json
1009
+ {
1010
+ "kind": "security",
1011
+ "schema_version": "1",
1012
+ "security_findings": [],
1013
+ "unresolved_edge_files": 0,
1014
+ "unresolved_callee_sites": 0
1015
+ }
1016
+ ```
1017
+
1018
+ Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, and `actions`. `tainted-sink` findings additionally carry `category` (the catalogue id, e.g. `"dangerous-html"`) and `cwe`; `client-server-leak` findings omit both. `unresolved_edge_files` (client-server-leak) and `unresolved_callee_sites` (tainted-sink) are in-band blind-spot counters: a zero finding count with a non-zero counter is not a clean bill. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak` (client-server-leak) or `// fallow-ignore-file security-sink` (any tainted-sink category).
1019
+
1020
+ ---
1021
+
1022
+ ## `explain`: Rule Explanation
1023
+
1024
+ Print rule rationale, examples, fix guidance, and docs URL for one issue type without running analysis.
1025
+
1026
+ ### Usage
1027
+
1028
+ ```bash
1029
+ fallow explain unused-export
1030
+ fallow explain fallow/code-duplication --format json --quiet
1031
+ ```
1032
+
1033
+ ### Arguments
1034
+
1035
+ | Argument | Description |
1036
+ |----------|-------------|
1037
+ | `<issue-type>` | Issue type token or rule id, for example `unused-export`, `unused-exports`, `fallow/unused-dependency`, `high-complexity`, or `code-duplication`. |
1038
+
1039
+ ### JSON Output Structure
1040
+
1041
+ ```json
1042
+ {
1043
+ "id": "fallow/unused-export",
1044
+ "name": "Unused Exports",
1045
+ "summary": "Export is never imported",
1046
+ "rationale": "Named exports that are never imported by any other module in the project. Includes both direct exports and re-exports through barrel files. The export may still be used locally within the same file.",
1047
+ "example": "export const formatPrice = ... exists in src/money.ts, but no module imports formatPrice.",
1048
+ "how_to_fix": "Remove the export or make it file-local. If it is public API, import it from an entry point or add an intentional suppression with context.",
1049
+ "docs": "https://docs.fallow.tools/explanations/dead-code#unused-exports"
1050
+ }
1051
+ ```
1052
+
1053
+ MCP equivalent: `fallow_explain` with required `issue_type`.
1054
+
1055
+ ---
1056
+
1057
+ ## `schema`: CLI Introspection
1058
+
1059
+ Dumps the full CLI interface definition as machine-readable JSON.
1060
+
1061
+ ```bash
1062
+ fallow schema
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ ## `config-schema`: Config JSON Schema
1068
+
1069
+ Prints the JSON Schema for fallow configuration files.
1070
+
1071
+ ```bash
1072
+ fallow config-schema > schema.json
1073
+ ```
1074
+
1075
+ ---
1076
+
1077
+ ## `plugin-schema`: Plugin JSON Schema
1078
+
1079
+ Prints the JSON Schema for external plugin definition files.
1080
+
1081
+ ```bash
1082
+ fallow plugin-schema > plugin-schema.json
1083
+ ```
1084
+
1085
+ ---
1086
+
1087
+ ## `license`: Manage Continuous Runtime License
1088
+
1089
+ Manage the local JWT used to unlock continuous/cloud runtime monitoring. Single-capture local runtime analysis does not require a license. Verification is fully offline against an Ed25519 public key compiled into the binary. Only `--trial` and `refresh` hit the network (`api.fallow.cloud`, 5s connect / 10s total timeout).
1090
+
1091
+ ```bash
1092
+ fallow license activate --trial --email you@company.com
1093
+ fallow license activate eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
1094
+ fallow license activate --from-file ./license.jwt
1095
+ cat ./license.jwt | fallow license activate --stdin
1096
+ fallow license status
1097
+ fallow license refresh
1098
+ fallow license deactivate
1099
+ ```
1100
+
1101
+ ### Subcommands
1102
+
1103
+ | Subcommand | Purpose |
1104
+ |------------|---------|
1105
+ | `activate` | Install a JWT or start a 30-day trial. JWT input precedence: positional arg > `--from-file` > `--stdin`. |
1106
+ | `status` | Print tier, seats, features, days-until-expiry, and (when `refresh_after` has passed) a proactive refresh hint. |
1107
+ | `refresh` | Fetch a fresh JWT using the currently stored one as identity proof. Exit 7 on network failure. |
1108
+ | `deactivate` | Remove the local license file. |
1109
+
1110
+ ### `activate` flags
1111
+
1112
+ | Flag | Type | Description |
1113
+ |------|------|-------------|
1114
+ | `--trial` | bool | Start a 30-day email-gated trial. Requires `--email`. **Rate-limited to 5 requests per hour per IP** — in CI or behind a shared NAT, start the trial locally and set `FALLOW_LICENSE` on the runner. |
1115
+ | `--email <ADDR>` | string | Email for the trial flow. On success, `trialEndsAt` is printed to stdout so you can see the trial window without decoding the JWT. |
1116
+ | `--from-file <PATH>` | path | Read a JWT from a file. |
1117
+ | `--stdin` | bool | Read a JWT from stdin. Conflicts with `--from-file` and positional JWT. |
1118
+
1119
+ ### Storage precedence
1120
+
1121
+ 1. `FALLOW_LICENSE` (env var holding the full JWT string)
1122
+ 2. `FALLOW_LICENSE_PATH` (env var pointing at a file)
1123
+ 3. `~/.fallow/license.jwt` (default; written `chmod 0600` on Unix)
1124
+
1125
+ ### Grace ladder
1126
+
1127
+ | Days past `exp` | State | Behavior |
1128
+ |-----------------|-------|----------|
1129
+ | `<= 7` | ExpiredWarning | Analysis runs; CLI prints a refresh hint |
1130
+ | `> 7, <= 30` | ExpiredWatermark | Analysis runs; output gains a visible watermark until refreshed |
1131
+ | `> 30` | HardFail | Continuous/cloud runtime monitoring is blocked; run `fallow license refresh` or start a new trial |
1132
+
1133
+ ### Actionable error messages
1134
+
1135
+ On HTTP error from `api.fallow.cloud`, fallow parses the `{error, message, code}` envelope and maps known codes to targeted hints:
1136
+
1137
+ | Operation + code | CLI message |
1138
+ |------------------|-------------|
1139
+ | `refresh` + `token_stale` | `your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr>` |
1140
+ | `refresh` + `invalid_token` | `your stored license token is missing required claims. Reactivate with: fallow license activate --trial --email <addr>` |
1141
+ | `refresh` or `trial` + `unauthorized` | `authentication failed. Reactivate with: fallow license activate --trial --email <addr>` |
1142
+ | `trial` + `rate_limit_exceeded` | `trial creation is rate-limited to 5 per hour per IP. Wait an hour or retry from a different network (in CI, start the trial locally and set FALLOW_LICENSE on the runner).` |
1143
+
1144
+ Unknown codes fall back to the backend's `message` field, or the raw body.
1145
+
1146
+ ### Clock skew
1147
+
1148
+ License verification rejects JWTs whose `iat` claim is more than 24 hours in the future relative to the local system clock. The same check catches both a forward-signed JWT and a local clock behind reality. Rejection exits non-zero so paid features fail closed.
1149
+
1150
+ | Env var | Default | Effect |
1151
+ |---------|---------|--------|
1152
+ | `FALLOW_LICENSE_SKEW_TOLERANCE_SECONDS` | `86400` (24h) | Overrides the tolerance window applied to the `iat` claim. Lenient parsing: unset / empty / unparsable / negative all fall back to the default. |
1153
+
1154
+ Common non-user causes: CI containers without NTP, machines with a dead BIOS battery, drifted laptop clocks after long sleep.
1155
+
1156
+ ### Exit Codes
1157
+
1158
+ | Code | Meaning |
1159
+ |------|---------|
1160
+ | `0` | Valid license (or trial/refresh succeeded) |
1161
+ | `2` | Bad invocation (missing email for `--trial`, unreadable file) |
1162
+ | `3` | License missing, hard-fail expired, malformed JWT, or clock skew exceeds tolerance |
1163
+ | `7` | Network failure or non-success HTTP status from `api.fallow.cloud` |
1164
+
1165
+ ---
1166
+
1167
+ ## `telemetry`: Opt-in Product Telemetry
1168
+
1169
+ Manage opt-in, off-by-default product telemetry that helps prioritize agent, CI, MCP, and editor workflows. Fallow never collects repository names, file paths, package or dependency names, source code, config values, environment variable names or values, raw command lines, or raw errors. Hashing those values is not used as a workaround.
1170
+
1171
+ ```bash
1172
+ fallow telemetry status # effective state, source, and config path
1173
+ fallow telemetry enable # opt in (user action only; agents must not run this)
1174
+ fallow telemetry disable # opt out
1175
+ fallow telemetry inspect --example # print an example payload + field purposes
1176
+ ```
1177
+
1178
+ Inspect the exact payload a real command would send, without sending it:
1179
+
1180
+ ```bash
1181
+ FALLOW_TELEMETRY=inspect fallow audit --format json --quiet
1182
+ ```
1183
+
1184
+ The inspected payload prints to stderr; stdout (including `--format json`) is untouched.
1185
+
1186
+ ### Behavior
1187
+
1188
+ - **Off by default.** Precedence: `DO_NOT_TRACK` / `FALLOW_TELEMETRY_DISABLED` (kill switches) > `FALLOW_TELEMETRY_DEBUG` (forces inspect) > `FALLOW_TELEMETRY` env > CI (off unless `FALLOW_TELEMETRY` is set) > user config (`fallow telemetry enable/disable`) > off.
1189
+ - **CI is off** unless `FALLOW_TELEMETRY` is explicitly set in that CI environment; a local `enable` never turns on org CI telemetry.
1190
+ - **Transport:** when enabled, one small JSON event is POSTed to `https://api.fallow.cloud/v1/telemetry/events` (override with `FALLOW_API_URL`), no auth token, no cookies, on a background thread so it does not delay your command. Delivery is best-effort; errors never change output or exit code.
1191
+ - **Agent source:** wrappers may set `FALLOW_AGENT_SOURCE=<allowlisted-value>` so an enabled run is attributed correctly. Allowlist: `codex`, `claude_code`, `cursor`, `copilot`, `opencode`, `aider`, `roo`, `windsurf`, `gemini` (aliases `gemini_cli`/`antigravity`), `cline`, `continue`, `zed`, `goose`, `other_known`, `unknown`, `none`. Setting it never enables telemetry and uploads no codebase content.
1192
+
1193
+ ---
1194
+
1195
+ ## `coverage`: Production-Coverage Workflow
1196
+
1197
+ Helper subcommand for runtime coverage setup, focused analysis, and cloud inventory upload. Three subcommands today:
1198
+
1199
+ - `coverage setup` — resumable state machine that wires sidecar installation, framework-aware coverage recipe writing, optional license activation for continuous monitoring, and automatic handoff into `fallow health --runtime-coverage`.
1200
+ - `coverage analyze` — focused runtime coverage analysis. Local mode reads `--runtime-coverage <path>`; cloud mode requires explicit `--cloud`, `--runtime-coverage-cloud`, or `FALLOW_RUNTIME_COVERAGE_SOURCE=cloud` and never triggers from `FALLOW_API_KEY` alone.
1201
+ - `coverage upload-inventory` — push a static function inventory to fallow cloud so the dashboard can surface `untracked` functions (those in the codebase but never called at runtime).
1202
+
1203
+ ```bash
1204
+ fallow coverage setup # interactive
1205
+ fallow coverage setup --yes # accept all prompts
1206
+ fallow coverage setup --non-interactive # print instructions, do not prompt
1207
+ fallow coverage setup --yes --json # agent-readable JSON, no prompts/writes/installs/network
1208
+ fallow coverage setup --yes --json --explain # add _meta field docs, enums, warnings, docs URL
1209
+
1210
+ fallow coverage analyze --runtime-coverage ./coverage --format json
1211
+ fallow coverage analyze --cloud --repo owner/repo --format json
1212
+
1213
+ fallow coverage upload-inventory # infers project-id, git-sha, API key
1214
+ fallow coverage upload-inventory --dry-run # print what would be uploaded, exit 0
1215
+
1216
+ fallow coverage upload-source-maps --dir dist # upload build source maps from CI
1217
+ fallow coverage upload-source-maps --dry-run # print maps and fileName values, no network
1218
+ ```
1219
+
1220
+ `--json` is the agent-driven entry point: implies `--non-interactive`, never writes files, never installs the sidecar, never makes network calls, and produces a stable JSON payload with these top-level keys: `schema_version` (string `"1"`), `framework_detected`, `package_manager`, `runtime_targets`, `members`, `config_written`, `commands`, `files_to_edit`, `snippets`, `dockerfile_snippet`, `next_steps`, `warnings`. Add `--explain` to inject an opt-in `_meta` block with field definitions, enum values, warning semantics, and the docs URL; `schema_version` stays `"1"`. `framework_detected` uses canonical ids (`nextjs`, `nestjs`, `nuxt`, `sveltekit`, `astro`, `remix`, `vite`, `plain_node`, `unknown`). When both a Node-server framework (Elysia, Hono, Fastify, Express, Koa, `@trpc/server`) and Vite appear in the same `package.json`, the Node-server framework wins. Workspace projects emit one `members[]` entry per runtime-bearing workspace (each with its own `framework_detected`, `package_manager`, `runtime_targets`, `files_to_edit`, `snippets`, `dockerfile_snippet`, `warnings`); top-level fields mirror the first emitted runtime member, and `runtime_targets` at top level is the union (`[]`, `["node"]`, `["browser"]`, or `["node", "browser"]`) across all members. Single-app projects emit a `members[]` array of length 1 (path `"."`) so consumers can treat it uniformly. Library-only workspaces (no `start`/`preview`/`dev` script and no Node-server dependency) are skipped, as are aggregator roots whose only `dev` / `preview` script delegates to a tool other than vite (e.g., `turbo dev`, `nx run-many`); when no runtime members are found, the payload reports `framework_detected: "unknown"`, `runtime_targets: []`, `members: []`, and a `warnings` entry of `"No runtime workspace members were detected; emitted install commands only."`. A Vite browser app is recognized when `vite` is a dependency AND either a `dev`/`preview` script invokes `vite` (or `vite-preview` / `vite-plus` / `vp`) OR the workspace contains an `index.html` or `src/main.{ts,tsx,js,jsx,mts,mjs}` entry.
1221
+
1222
+ ### `setup` flow
1223
+
1224
+ 1. **License check** — if missing or hard-fail, offers to start a trial.
1225
+ 2. **Sidecar discovery** — resolves `FALLOW_COV_BIN`, `FALLOW_COV_BINARY_PATH`, platform-package binaries in npm/bun/pnpm layouts, project-local `node_modules/.bin/fallow-cov`, package-manager bin, `~/.fallow/bin/fallow-cov`, and `PATH`. When an explicit env path is set but points to a non-existent file, setup errors fast instead of falling through.
1226
+ 3. **Coverage recipe** — detects framework (Next.js, Nuxt, Astro, SvelteKit, Remix, NestJS, Vite browser apps, plain Node) and package manager (npm, pnpm, yarn, bun), then writes `docs/collect-coverage.md` with the correct commands.
1227
+ 4. **Handoff** — if `./coverage/coverage-final.json` or a V8 coverage directory already exists, setup runs `fallow health --runtime-coverage <path>` directly.
1228
+
1229
+ ### `analyze` flags
1230
+
1231
+ | Flag | Type | Default | Description |
1232
+ |------|------|---------|-------------|
1233
+ | `--runtime-coverage <PATH>` | path | none | Local V8 directory, V8 JSON file, or Istanbul coverage map. Mutually exclusive with cloud mode. |
1234
+ | `--cloud`, `--runtime-coverage-cloud` | bool | false | Explicitly fetch cloud runtime facts from `/v1/coverage/:repo/runtime-context`. |
1235
+ | `--api-key <KEY>` | string | `$FALLOW_API_KEY` | Fallow cloud bearer token, used only after explicit cloud opt-in. |
1236
+ | `--api-endpoint <URL>` | string | `$FALLOW_API_URL` or `https://api.fallow.cloud` | Override for staging / on-prem. |
1237
+ | `--repo <OWNER/REPO>` | string | `$FALLOW_REPO`, then parsed git origin | Repository whose latest cloud runtime facts should be pulled. Slashes are percent-encoded as one route segment. |
1238
+ | `--coverage-period <DAYS>` | integer | 30 | Cloud observation window, 1 through 90 days. |
1239
+ | `--project-id <ID>` | string | none | Optional project discriminator for monorepos. |
1240
+ | `--environment <NAME>` | string | none | Optional environment filter. |
1241
+ | `--commit-sha <SHA>` | string | none | Optional advanced filter for a specific observed commit. |
1242
+ | `--top <N>` | integer | unset | Show only the top N runtime findings, hot paths, blast-radius entries, and importance entries. Truncation happens before rendering, so it propagates to JSON, human, and cloud-merge output equally. |
1243
+ | `--blast-radius` | bool | false | Show the first-class blast-radius section in human output. JSON always includes `runtime_coverage.blast_radius` whenever runtime coverage analysis runs. |
1244
+ | `--importance` | bool | false | Show the first-class importance section in human output. JSON always includes `runtime_coverage.importance` whenever runtime coverage analysis runs. |
1245
+ | `--production` | bool | false | Run analyze in production mode, matching `fallow health --production`. Filters out test files and dev-only code paths before merging runtime data. |
1246
+ | `--min-invocations-hot <N>` | integer | 100 | Hot-path classification threshold. Functions invoked at least N times during the captured window are classified as hot. Mirrors the same flag on `fallow health --runtime-coverage`. |
1247
+ | `--min-observation-volume <N>` | integer | 5000 | Minimum total trace volume before the sidecar emits high-confidence `safe_to_delete` / `review_required` verdicts. Below this, confidence is capped at `medium`. |
1248
+ | `--low-traffic-threshold <RATIO>` | decimal | 0.001 | Fraction of total trace count below which an invoked function is classified `low_traffic` rather than `active`. `0.001` = 0.1%. |
1249
+ | `--explain` | bool | false | With `--format json`, attach a top-level `_meta` block with field definitions, enum values (`data_source`, `test_coverage`, `v8_tracking`, `action_type`, etc.), warning-code documentation, and the docs URL. |
1250
+
1251
+ Cloud analysis emits the same `runtime_coverage` JSON block as local mode. Its summary includes `data_source: "cloud"`, `last_received_at`, and `capture_quality` derived from the pulled runtime window. Cloud functions that cannot be matched to the local AST/static index are omitted from findings and reported through a `cloud_functions_unmatched` warning.
1252
+
1253
+ Each finding's `actions[].type` uses the canonical kebab-case vocabulary: `delete-cold-code` is emitted on `verdict=safe_to_delete`, `review-runtime` on `verdict=review_required`. The sidecar may emit additional protocol-specific identifiers, so consumers should treat unknown values as forward-compat extensions rather than schema violations.
1254
+
1255
+ ### `upload-inventory` flags
1256
+
1257
+ | Flag | Type | Default | Description |
1258
+ |------|------|---------|-------------|
1259
+ | `--api-key <KEY>` | string | `$FALLOW_API_KEY` | Fallow cloud bearer token. Generate at `https://fallow.cloud/settings#api-keys`. **Prefer `$FALLOW_API_KEY` on shared CI runners**: `--api-key` on the command line may be visible to other processes via `ps`. |
1260
+ | `--api-endpoint <URL>` | string | `$FALLOW_API_URL` or `https://api.fallow.cloud` | Override for staging / on-prem. |
1261
+ | `--project-id <OWNER/REPO>` | string | `$GITHUB_REPOSITORY` → `$CI_PROJECT_PATH` → `git remote get-url origin` | Project identifier. |
1262
+ | `--git-sha <SHA>` | string | `git rev-parse HEAD` | Commit SHA this inventory is keyed to. Max 64 chars; `[A-Za-z0-9._-]` only. |
1263
+ | `--allow-dirty` | bool | `false` | Silence the warning when the working tree has uncommitted changes. |
1264
+ | `--exclude-paths <GLOB>` | glob | none | Additional globs to skip (repeatable), applied after the configured fallow ignore rules. |
1265
+ | `--path-prefix <PREFIX>` | string | none | Prefix prepended to every emitted `filePath` so inventory matches runtime paths. Required for containerized deployments (runtime reports `/app/src/*` while the walker emits `src/*`). Common values: `/app`, `/workspace`, `/usr/src/app`, `/var/task`, `/home/runner/work/<repo>/<repo>`. Must start with `/`. |
1266
+ | `--dry-run` | bool | `false` | Print what would be uploaded and exit. No network call. |
1267
+ | `--ignore-upload-errors` | bool | `false` | Treat upload failures as warnings (exit 0). Validation errors still fail hard. |
1268
+
1269
+ Only plain JS/TS/JSX/TSX sources are walked. Declaration files (`*.d.ts`, `*.d.mts`, `*.d.cts`, `*.d.tsx`) and bodyless function signatures (TS overloads, `abstract` methods, `declare function`) are intentionally skipped; they have no runtime footprint. Function names match `oxc-coverage-instrument` byte-for-byte so the join with runtime coverage succeeds.
1270
+
1271
+ ### Environment
1272
+
1273
+ - `FALLOW_COV_BIN` — explicit override for the sidecar binary (for `setup`). Wins over all other discovery paths. Must point to an existing file.
1274
+ - `FALLOW_API_KEY` — fallow cloud bearer token (for `upload-inventory` and `upload-source-maps`). Overridden by `--api-key` for `upload-inventory`; `upload-source-maps` reads only the env var so secrets stay out of argv.
1275
+ - `FALLOW_API_URL` — base URL for cloud calls. Overridden by `--api-endpoint`.
1276
+ - `FALLOW_CA_BUNDLE` - PEM certificate bundle for cloud calls. Relative paths resolve from the process cwd. The bundle replaces default WebPKI roots, so private-CA runners should pass a complete bundle that includes public roots plus the private CA.
1277
+
1278
+ ### `coverage upload-source-maps` flags
1279
+
1280
+ Coverage CI helper for bundled/minified runtime coverage. It scans a build directory for `.map` files and uploads them to `/v1/coverage/:repo/source-maps` keyed by the commit SHA the beacon reports.
1281
+
1282
+ Uploads retry network failures, HTTP 429, and HTTP 502/503/504 up to three attempts. HTTP 429 honors `Retry-After` delta seconds and HTTP-date values, capped at 60 seconds. Setup or transport failures that prevent every map from uploading exit 7; mixed per-map failures still exit 1.
1283
+
1284
+ | Flag | Type | Default | Description |
1285
+ |------|------|---------|-------------|
1286
+ | `--dir <PATH>` | path | `dist` | Directory scanned recursively. |
1287
+ | `--include <GLOB>` | glob | `**/*.map` | Include glob relative to `--dir`. |
1288
+ | `--exclude <GLOB>` | glob | `**/node_modules/**` | Exclude glob, repeatable. |
1289
+ | `--repo <NAME>` | string | `package.json` `repository.url`, then `git remote get-url origin` parsed to `owner/repo` | Repo identifier used in the source-map API path. Must match the beacon's `projectId` (and `upload-inventory`'s `--project-id`); pass `--repo <bare-name>` explicitly if the beacon reports a bare name. |
1290
+ | `--git-sha <SHA>` | string | `$GITHUB_SHA` -> `$CI_COMMIT_SHA` -> `$COMMIT_SHA` -> `git rev-parse HEAD` | Commit SHA, 7-40 hex chars. |
1291
+ | `--endpoint <URL>` | string | `$FALLOW_API_URL` or `https://api.fallow.cloud` | Override for staging / on-prem. |
1292
+ | `--strip-path <BOOL>` | bool | `true` | Upload basename-only `fileName` values. Use `--strip-path=false` when runtime coverage reports paths like `assets/app.js`. |
1293
+ | `--dry-run` | bool | `false` | Print what would upload; no API key or network call. |
1294
+ | `--concurrency <N>` | integer | `4` | Parallel upload fanout. |
1295
+ | `--fail-fast` | bool | `false` | Stop on the first upload failure. |
1296
+
1297
+ ### Exit Codes
1298
+
1299
+ | Code | Meaning |
1300
+ |------|---------|
1301
+ | `0` | Setup complete / upload succeeded / dry-run printed |
1302
+ | `2` | Bad invocation, unable to resolve sidecar via env override (`setup`) |
1303
+ | `4` | Sidecar install failed (`setup`) |
1304
+ | `5` | Coverage input could not be pre-processed (`setup`) |
1305
+ | `7` | Network failure (trial activation for `setup`; upload DNS/TLS/connect for `upload-inventory`) |
1306
+ | `10` | Validation error: missing API key, unresolvable project-id, zero functions (`upload-inventory`) |
1307
+ | `11` | Payload too large: inventory exceeds the 200,000-function server cap (`upload-inventory`) |
1308
+ | `12` | Auth rejected: 401 / 403 from the server (`upload-inventory`) |
1309
+ | `13` | Server error: 5xx or other non-2xx status (`upload-inventory`) |
1310
+
1311
+ ---
1312
+
1313
+ ## `config`: Show Resolved Config
1314
+
1315
+ Prints the loaded config file path and the resolved config (with `extends` merged) as JSON. Useful for verifying which config fallow picked up, especially in monorepos.
1316
+
1317
+ ```bash
1318
+ fallow config # path on first line, JSON below
1319
+ fallow config --path # only the path (scriptable)
1320
+ ```
1321
+
1322
+ ### Flags
1323
+
1324
+ | Flag | Type | Description |
1325
+ |------|------|-------------|
1326
+ | `--path` | bool | Print only the config file path, no JSON |
1327
+
1328
+ ### Exit Codes
1329
+
1330
+ | Code | Meaning |
1331
+ |------|---------|
1332
+ | `0` | Config file found and loaded |
1333
+ | `2` | Error (parse failure, explicit `--config` path missing) |
1334
+ | `3` | No config file found; defaults are in effect |
1335
+
1336
+ Honors the global `--config <path>` flag: if passed, that path is loaded directly instead of walking the directory tree.
1337
+
1338
+ The `loaded config: <path>` line is also emitted to stderr automatically at the start of every human-format CLI run (suppressed by `--quiet` and non-human formats).
1339
+
1340
+ ---
1341
+
1342
+ ## Global Flags
1343
+
1344
+ Available on all commands:
1345
+
1346
+ | Flag | Type | Description |
1347
+ |------|------|-------------|
1348
+ | `-r, --root` | path | Project root directory |
1349
+ | `-c, --config` | path | Config file path |
1350
+ | `-f, --format` (alias: `--output`) | string | Output format |
1351
+ | `-q, --quiet` | bool | Suppress progress output |
1352
+ | `--no-cache` | bool | Disable incremental caching |
1353
+ | `--threads` | number | Number of parser threads |
1354
+ | `--changed-since` (alias: `--base`) | string | Git-aware incremental analysis |
1355
+ | `--baseline` | path | Compare to baseline |
1356
+ | `--save-baseline` | path | Save results as baseline |
1357
+ | `--fail-on-regression` | bool | Fail if issue count increased beyond tolerance vs a regression baseline |
1358
+ | `--tolerance` | string | Allowed increase: `"2%"` (percentage) or `"5"` (absolute). Default: `"0"` |
1359
+ | `--regression-baseline` | path | Path to regression baseline file (default: `.fallow/regression-baseline.json`) |
1360
+ | `--save-regression-baseline` | path | Save current issue counts as a regression baseline |
1361
+ | `--production` | bool | Exclude test/dev files, only start/build scripts (applies to every analysis) |
1362
+ | `--production-dead-code` / `--production-health` / `--production-dupes` | bool | Per-analysis production mode for bare combined runs and `fallow audit`. Per-analysis env vars `FALLOW_PRODUCTION_DEAD_CODE`/`HEALTH`/`DUPES` mirror these flags. Per-analysis env beats global `FALLOW_PRODUCTION`. |
1363
+ | `--performance` | bool | Show pipeline timing breakdown |
1364
+ | `-w, --workspace` | string | Scope to one or more workspaces (comma-separated, globs, `!` negation) |
1365
+ | `--changed-workspaces` | string (git ref) | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF`. Mutually exclusive with `--workspace`. Missing ref is a hard error. |
1366
+ | `--explain` | bool | JSON: include metric definitions in `_meta`. Human: print a `Description:` line under each section header. Always on for MCP. |
1367
+ | `--legacy-envelope` | bool | Emit the previous typed JSON root envelope without top-level `kind` |
1368
+ | `--only` | string | Run only specific analyses (e.g., `--only dead-code,dupes`). Values: `dead-code` (alias: `check`), `dupes`, `health` |
1369
+ | `--skip` | string | Skip specific analyses (e.g., `--skip health`). Values: `dead-code` (alias: `check`), `dupes`, `health` |
1370
+ | `--ci` | bool | CI mode: `--format sarif --fail-on-issues --quiet` |
1371
+ | `--fail-on-issues` | bool | Exit 1 if any issues found (promotes `warn` to `error`) |
1372
+ | `--sarif-file` | path | Write SARIF output to a file instead of stdout |
1373
+ | `--summary` | bool | Show only category counts without individual items. Useful for dashboards and quick overviews |
1374
+ | `--group-by` | `owner\|directory\|package\|section` | Group output by CODEOWNERS ownership (`owner`), first path component (`directory`), workspace package (`package`, aliases: `workspace`, `pkg`), or GitLab CODEOWNERS `[Section]` headers (`section`, alias: `gl-section`). All output formats partition issues into labeled groups. `section` mode attaches an `owners` array to each group in JSON output |
1375
+ | `--score` | bool | Compute health score (0-100 with letter grade) in combined mode. Enables the health delta header in PR comments. JSON includes `health_score` object with `score`, `grade`, and `penalties` breakdown |
1376
+ | `--trend` | bool | Compare current health metrics against saved snapshot. Implies `--score`. Shows per-metric deltas with directional indicators. Requires at least one saved snapshot in `.fallow/snapshots/` |
1377
+ | `--save-snapshot` | path (optional) | Save vital signs snapshot for trend tracking. Default path: `.fallow/snapshots/<timestamp>.json`. Forces file-scores + hotspot computation |
1378
+
1379
+ ---
1380
+
1381
+ ## Environment Variables
1382
+
1383
+ | Variable | Description |
1384
+ |----------|-------------|
1385
+ | `FALLOW_FORMAT` | Default output format. CLI `--format` overrides. |
1386
+ | `FALLOW_QUIET` | Set to `1` to suppress progress. CLI `--quiet` overrides. |
1387
+ | `FALLOW_BIN` | Path to fallow binary (used by the MCP server). |
1388
+ | `FALLOW_TIMEOUT_SECS` | MCP server subprocess timeout in seconds (default: `120`). Increase for very large codebases. |
1389
+ | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
1390
+ | `FALLOW_CACHE_MAX_SIZE` | Maximum on-disk extraction cache (`.fallow/cache.bin`) size in megabytes (default: `256`). Triggers LRU eviction when crossed. Wins over `cache.maxSizeMb` config field. Intended for CI runners with disk quotas. `--no-cache` short-circuits this knob. |
1391
+ | `FALLOW_AUDIT_CACHE_MAX_AGE_DAYS` | Max age (in days since last reuse or fresh create) of a persistent reusable `fallow audit` base-snapshot worktree cache. Older entries are reclaimed at the top of the next `fallow audit` invocation (default: `30`). Wins over `audit.cacheMaxAgeDays` config field. `0` disables the GC; invalid values silently fall back to config / default. |
1392
+ | `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
1393
+ | `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
1394
+ | `FALLOW_CHANGED_SINCE` | GitLab CI: git ref for incremental analysis. Auto-detected in MR pipelines. |
1395
+ | `FALLOW_COMMENT` | GitLab CI: set to `true` to post MR summary comments. |
1396
+ | `FALLOW_REVIEW` | GitLab CI: set to `true` to post inline code review comments on MR diffs. |
1397
+ | `FALLOW_REVIEW_GUIDANCE` | Add collapsed "What to do" guidance blocks to `review-github` / `review-gitlab` inline comments. |
1398
+ | `FALLOW_SUMMARY_SCOPE` | Sticky PR/MR summary scope for `pr-comment-github` / `pr-comment-gitlab`: `all` (default) keeps project-level findings outside the diff; `diff` applies the diff filter to those findings too. Inline review comments are unaffected. |
1399
+ | `FALLOW_SCORE` | GitLab CI: set to `true` to compute health score in combined mode. Enables health delta header in MR comments. |
1400
+ | `FALLOW_TREND` | GitLab CI: set to `true` to compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. |
1401
+ | `FALLOW_EXTRA_ARGS` | GitLab CI: additional CLI flags passed through to fallow. |
1402
+ | `FALLOW_VERSION` | GitLab CI: fallow version to install. Empty (default) reads the project's `package.json` `fallow` dependency, then falls back to `latest`; set explicitly to override the local pin. |
1403
+ | `FALLOW_SKIP_BINARY_VERIFY` | Skip Ed25519 + SHA-256 verification of platform binaries on first invocation of `fallow`, `fallow-lsp`, or `fallow-mcp` (and during the GitHub Action installer). Set to `1`, `true`, or `yes` ONLY when deliberately replacing the published binary (source builds, airgapped mirrors, signed-repack registries). The skip is recorded in `fallow --version` output as `verified: skipped (FALLOW_SKIP_BINARY_VERIFY is set)` so it stays visible in CI logs and vendor audits. Never set in regular CI; use the published binary or the documented out-of-band verification recipe in [`SECURITY.md`](https://github.com/fallow-rs/fallow/blob/main/SECURITY.md) instead. |
1404
+ | `FALLOW_VERIFY_CACHE_DIR` | Override where the lazy-verify sentinel file is written. Cascade is platform-pkg-dir, then this override, then `$XDG_CACHE_HOME/fallow/sentinels/` (Linux/macOS) or `%LOCALAPPDATA%\fallow\sentinels\` (Windows). Useful when the platform pkg dir is read-only (yarn PnP, Docker layered images, pnpm verify-store). |
1405
+ | `FALLOW_VERIFY_LOG` | Set to `1`, `true`, or `yes` to emit one structured stderr line per verify outcome (`fallow-verify outcome=ok cache=hit sentinel=...`). Off by default so MCP stdout/stderr stay clean; enable for CI diagnostic logs. |
1406
+ | `FALLOW_TELEMETRY` | Opt-in product telemetry mode, off by default: `off`/`on`/`inspect` (plus `0`/`1`/`true`/`false`/`disabled`/`enabled`/`debug`/`log`). `inspect` prints the exact payload to stderr without sending. Wins over the user telemetry config. |
1407
+ | `FALLOW_TELEMETRY_DISABLED` | Admin/fleet telemetry kill switch (top precedence, with `DO_NOT_TRACK`). Truthy (`1`/`true`/`yes`/`on`) hard-disables telemetry and refuses `fallow telemetry enable`. |
1408
+ | `FALLOW_TELEMETRY_DEBUG` | Forces inspect mode; outranks `FALLOW_TELEMETRY`. |
1409
+ | `DO_NOT_TRACK` | Honored as a top-precedence telemetry kill switch (consoledonottrack.com convention). |
1410
+ | `FALLOW_AGENT_SOURCE` | Declare the calling agent for telemetry classification (only used when telemetry is enabled; never enables it): `codex`, `claude_code`, `cursor`, `copilot`, `opencode`, `aider`, `roo`, `windsurf`, `gemini` (aliases `gemini_cli`/`antigravity`), `cline`, `continue`, `zed`, `goose`, `other_known`, `unknown`, `none`. Unrecognized values are ignored. |
1411
+ | `GITLAB_TOKEN` | GitLab CI: project access token with `api` scope (for MR comments/reviews; `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API). |
1412
+
1413
+ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid passing flags on every invocation.
1414
+
1415
+ ---
1416
+
1417
+ ## Output Formats
1418
+
1419
+ | Format | Description | Use Case |
1420
+ |--------|-------------|----------|
1421
+ | `human` | Colored terminal output | Interactive use |
1422
+ | `json` | Machine-readable JSON | Agent integration, CI pipelines |
1423
+ | `sarif` | Static Analysis Results Interchange Format | GitHub Code Scanning, SARIF-compatible tools |
1424
+ | `compact` | Grep-friendly: `type:path:line:name` per line | Quick filtering |
1425
+ | `markdown` | Markdown tables | Documentation, PR comments |
1426
+ | `codeclimate` / `gitlab-codequality` | CodeClimate JSON array | GitLab Code Quality, CodeClimate-compatible tools |
1427
+ | `pr-comment-github` / `pr-comment-gitlab` | Sticky PR/MR comment markdown with HTML-comment marker for upsert | Posted by the action / template `comment.sh` scripts |
1428
+ | `review-github` / `review-gitlab` | JSON envelope for `POST /pulls/.../reviews` (GH) or `POST /merge_requests/.../discussions` (GL) | Posted by the action / template `review.sh` scripts; reconciled by `fallow ci reconcile-review` |
1429
+
1430
+ ---
1431
+
1432
+ ## `ci`: Provider-Aware Review Automation
1433
+
1434
+ `fallow ci reconcile-review` reads a typed review envelope (`--format review-github` / `review-gitlab`), looks up existing fingerprints on the PR/MR, and resolves stale review threads when their finding is no longer present in the new envelope. Posts an idempotent "Resolved in `<sha>`" follow-up comment per stale finding (skipped if a marker for the same fingerprint at the current SHA already exists).
1435
+
1436
+ Provider mutations are fail-fast. If a preflight check, permission error, or provider mutation fails, JSON output keeps `apply_errors` and can add `apply_hint`, `failed_fingerprints`, and `unapplied_fingerprints` so agents and CI wrappers can report what was not fully applied.
1437
+
1438
+ ### Flags
1439
+
1440
+ | Flag | Type | Description |
1441
+ |------|------|-------------|
1442
+ | `--provider` | `github\|gitlab` | Required. Selects the provider API. |
1443
+ | `--pr` | `<number>` | GitHub PR number. Required when `--provider github`. |
1444
+ | `--mr` | `<iid>` | GitLab MR internal id. Required when `--provider gitlab`. |
1445
+ | `--repo` | `owner/name` | GitHub repo. Defaults to `$GH_REPO` / `$GITHUB_REPOSITORY`. |
1446
+ | `--project-id` | `<id>` | GitLab project id (numeric or `group/project`). Defaults to `$CI_PROJECT_ID`. |
1447
+ | `--api-url` | `<url>` | Override the API base URL (GitHub Enterprise, self-hosted GitLab). |
1448
+ | `--envelope` | `<path>` | Path to the review envelope JSON written by `--format review-{github,gitlab}`. |
1449
+ | `--dry-run` | `bool` | Compute the new/stale plan without posting / resolving. |
1450
+
1451
+ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_API_RETRIES` (default 3) caps attempts; `FALLOW_API_RETRY_DELAY` (default 2) sets the floor delay; server-supplied `Retry-After` overrides the floor on 429 responses.
1452
+
1453
+ ---
1454
+
1455
+ ## CI Integration
1456
+
1457
+ - **GitHub Actions**: `uses: fallow-rs/fallow@v2` — supports SARIF upload to Code Scanning, inline PR annotations (`annotations: true`), PR comments, all commands. Annotations use workflow commands (no Advanced Security required); limit with `max-annotations` (default 50). Set `score: true` to compute health score and enable the health delta header in PR comments
1458
+ - **GitLab CI**: include `ci/gitlab-ci.yml` template and extend `.fallow` — generates Code Quality reports via `--format codeclimate` / `--format gitlab-codequality` (inline MR annotations), rich MR comments, code review comments, all commands. Use `fallow ci-template gitlab --vendor` when runners cannot reach `raw.githubusercontent.com`; commit the generated `ci/` and `action/` files and use GitLab's local include syntax. Variables use `FALLOW_` prefix (e.g., `FALLOW_COMMAND`, `FALLOW_FAIL_ON_ISSUES`). Set `FALLOW_SCORE: "true"` to compute health score; `FALLOW_TREND: "true"` to compare against saved snapshots
1459
+ - **Any CI**: `npx fallow --ci` — equivalent to `--format sarif --fail-on-issues --quiet`
1460
+
1461
+ ### GitLab CI Variables
1462
+
1463
+ | Variable | Default | Description |
1464
+ |----------|---------|-------------|
1465
+ | `FALLOW_COMMAND` | `dead-code` | Command to run (`dead-code`, `dupes`, `health`, or default combined) |
1466
+ | `FALLOW_FAIL_ON_ISSUES` | `false` | Exit 1 if issues found |
1467
+ | `FALLOW_CHANGED_SINCE` | auto | Git ref for incremental analysis. Auto-detected in MR pipelines (`origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`) |
1468
+ | `FALLOW_COMMENT` | `false` | Post a summary comment on the MR with findings |
1469
+ | `FALLOW_REVIEW` | `false` | Post inline code review comments on MR diff lines where issues were found |
1470
+ | `FALLOW_REVIEW_GUIDANCE` | `false` | Add collapsed "What to do" guidance blocks to inline review comments |
1471
+ | `FALLOW_SUMMARY_SCOPE` | `all` | Sticky summary scope: `all` keeps project-level findings outside the diff; `diff` applies the diff filter to those findings too |
1472
+ | `FALLOW_SCORE` | `false` | Compute health score (0-100 with letter grade) in combined mode. Enables the health delta header in MR comments |
1473
+ | `FALLOW_TREND` | `false` | Compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. Shows per-metric deltas |
1474
+ | `FALLOW_EXTRA_ARGS` | — | Additional CLI flags passed through to fallow |
1475
+ | `GITLAB_TOKEN` | — | Project access token with `api` scope (required for `FALLOW_COMMENT` and `FALLOW_REVIEW`). Alternatively, enable job token API access |
1476
+
1477
+ **Package manager detection**: The GitLab template auto-detects the project's package manager (npm, pnpm, or yarn) from lockfiles. MR comments and review comments show the correct install/run commands for the detected manager (e.g., `pnpm add -D` vs `npm install --save-dev`).
1478
+
1479
+ **Auto `--changed-since` in MR pipelines**: When running in a merge request pipeline, the template automatically sets `--changed-since origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME` unless `FALLOW_CHANGED_SINCE` is explicitly set. This scopes analysis to files changed in the MR without manual configuration.
1480
+
1481
+ ---
1482
+
1483
+ ## JSON Output Structure
1484
+
1485
+ ### `dead-code` output
1486
+
1487
+ ```json
1488
+ {
1489
+ "kind": "dead-code",
1490
+ "schema_version": 7,
1491
+ "version": "2.88.2",
1492
+ "elapsed_ms": 45,
1493
+ "total_issues": 12,
1494
+ "entry_points": {
1495
+ "total": 5,
1496
+ "sources": { "package_json_scripts": 2, "next_js": 3 }
1497
+ },
1498
+ "summary": {
1499
+ "total_issues": 12,
1500
+ "unused_files": 1,
1501
+ "unused_exports": 1,
1502
+ "unused_types": 1,
1503
+ "unused_dependencies": 1,
1504
+ "unused_enum_members": 0,
1505
+ "unused_class_members": 0,
1506
+ "unresolved_imports": 0,
1507
+ "unlisted_dependencies": 0,
1508
+ "duplicate_exports": 0,
1509
+ "type_only_dependencies": 0,
1510
+ "test_only_dependencies": 0,
1511
+ "circular_dependencies": 0,
1512
+ "re_export_cycles": 0,
1513
+ "boundary_violations": 0,
1514
+ "stale_suppressions": 0
1515
+ },
1516
+ "unused_files": [{ "path": "src/old.ts" }],
1517
+ "unused_exports": [{ "path": "src/utils.ts", "name": "unusedFn", "line": 42, "actions": [{"type": "remove-export", "auto_fixable": true, "description": "Remove the unused export from the public API"}, {"type": "suppress-line", "auto_fixable": false, "description": "Suppress with an inline comment above the line", "comment": "// fallow-ignore-next-line unused-export"}] }],
1518
+ "unused_types": [{ "path": "src/types.ts", "name": "OldType", "line": 10 }],
1519
+ "unused_dependencies": [{ "name": "lodash", "line": 5, "used_in_workspaces": ["packages/web"] }],
1520
+ "unused_dev_dependencies": [{ "name": "jest", "line": 8 }],
1521
+ "unused_enum_members": [{ "path": "src/enums.ts", "enum_name": "Status", "member": "Archived", "line": 5 }],
1522
+ "unused_class_members": [{ "path": "src/service.ts", "class_name": "Service", "member": "oldMethod", "line": 20 }],
1523
+ "unresolved_imports": [{ "path": "src/index.ts", "specifier": "./missing", "line": 3 }],
1524
+ "unlisted_dependencies": [{ "name": "chalk", "imported_from": [{ "path": "src/cli.ts", "line": 1, "col": 0 }] }],
1525
+ "duplicate_exports": [{ "name": "Config", "locations": ["src/config.ts:5", "src/types.ts:12"] }],
1526
+ "circular_dependencies": [{ "cycle": ["src/a.ts", "src/b.ts", "src/a.ts"], "line": 3, "col": 0, "is_cross_package": false }],
1527
+ "re_export_cycles": [{ "files": ["src/api/index.ts", "src/api/internal/index.ts"], "kind": "multi-node", "actions": [{ "type": "fix", "kind": "refactor-re-export-cycle", "auto_fixable": false, "description": "Remove one `export * from` (or `export { ... } from`) statement on any one member to break the cycle" }, { "type": "suppress-file", "kind": "suppress-file", "auto_fixable": false, "comment": "// fallow-ignore-file re-export-cycle" }] }],
1528
+ "boundary_violations": [{ "from_path": "src/ui/Button.ts", "to_path": "src/data/db.ts", "from_zone": "ui", "to_zone": "data", "import_specifier": "../data/db", "line": 5, "col": 0 }],
1529
+ "unused_optional_dependencies": [{ "name": "fsevents" }],
1530
+ "type_only_dependencies": [{ "name": "zod", "used_in": ["src/schema.ts"], "line": 12 }],
1531
+ "test_only_dependencies": [{ "name": "msw", "path": "package.json", "line": 15 }],
1532
+ "stale_suppressions": [{ "path": "src/utils.ts", "line": 5, "col": 0, "origin": { "type": "inline_comment", "issue_type": "unused-export", "is_file_level": false } }]
1533
+ }
1534
+ ```
1535
+
1536
+ For dependency findings, `used_in_workspaces` means the package is imported by another workspace even though the declaring workspace does not import it. Move the dependency to the consuming workspace instead of auto-removing it.
1537
+
1538
+ #### `actions` Array
1539
+
1540
+ Every issue in `dead-code` JSON output includes an `actions` array with structured fix suggestions. Each action has:
1541
+
1542
+ | Field | Type | Required | Description |
1543
+ |-------|------|----------|-------------|
1544
+ | `type` | string | yes | Action type in kebab-case (for example `remove-export`, `remove-file`, `remove-dependency`, `move-dependency`, `suppress-line`, `add-to-config`) |
1545
+ | `auto_fixable` | bool | yes | `true` if `fallow fix` handles this action automatically. Evaluated PER FINDING, not per action type: the same `type` may carry `true` on one finding and `false` on another when per-instance guards in the applier discriminate. Filter on this bool of each individual action, not on `type` alone. |
1546
+ | `description` | string | yes | Human-readable description of the action |
1547
+ | `comment` | string | no | Suppression comment text (on `suppress-line` actions) |
1548
+ | `note` | string | no | Additional context on non-auto-fixable items |
1549
+ | `config_key` | string | no | Config field to update (on `add-to-config` actions) |
1550
+ | `value` | string \| array | no | Value to add to the config field (on `add-to-config` actions). Scalar for `ignoreDependencies`-style keys (e.g. `"lodash"`); array of `{ file, exports }` rule objects for `ignoreExports`. |
1551
+ | `value_schema` | string | no | URL pointing at the JSON Schema fragment that describes `value` (on `add-to-config` actions). Agents that want to validate `value` before writing it into a user's config can fetch and apply the linked schema. |
1552
+
1553
+ Example:
1554
+
1555
+ ```json
1556
+ {
1557
+ "path": "src/utils.ts",
1558
+ "name": "helperFn",
1559
+ "line": 10,
1560
+ "actions": [
1561
+ {
1562
+ "type": "remove-export",
1563
+ "auto_fixable": true,
1564
+ "description": "Remove the unused export from the public API"
1565
+ },
1566
+ {
1567
+ "type": "suppress-line",
1568
+ "auto_fixable": false,
1569
+ "description": "Suppress with an inline comment above the line",
1570
+ "comment": "// fallow-ignore-next-line unused-export"
1571
+ }
1572
+ ]
1573
+ }
1574
+ ```
1575
+
1576
+ Dependency issues use `add-to-config` with `config_key` and `value`:
1577
+
1578
+ ```json
1579
+ {
1580
+ "name": "autoprefixer",
1581
+ "line": 5,
1582
+ "actions": [
1583
+ {
1584
+ "type": "remove-dependency",
1585
+ "auto_fixable": true,
1586
+ "description": "Remove from package.json dependencies"
1587
+ },
1588
+ {
1589
+ "type": "add-to-config",
1590
+ "auto_fixable": false,
1591
+ "description": "Add to ignoreDependencies in fallow config",
1592
+ "config_key": "ignoreDependencies",
1593
+ "value": "autoprefixer",
1594
+ "value_schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json#/properties/ignoreDependencies/items"
1595
+ }
1596
+ ]
1597
+ }
1598
+ ```
1599
+
1600
+ When a dependency action is `move-dependency`, `auto_fixable` is `false`; the package is imported from another workspace and needs a package.json ownership move rather than removal.
1601
+
1602
+ Per-instance `auto_fixable` flips today (the same action `type` flipping between findings):
1603
+
1604
+ - `remove-catalog-entry` (unused-catalog-entries): `true` only when `hardcoded_consumers` is empty; `false` otherwise (the applier skips the entry to avoid breaking `pnpm install`).
1605
+ - `remove-dependency` vs `move-dependency` (dependency findings): primary action flips between `remove-dependency` (`true`) and `move-dependency` (`false`) on `used_in_workspaces`.
1606
+ - `add-to-config` for `ignoreExports` (duplicate-exports): `true` when `fallow fix` can safely apply the action, which today means EITHER a fallow config file already exists OR no config exists and the working directory is NOT inside a monorepo subpackage. In the second case the applier creates `.fallowrc.json` using `fallow init`'s framework-aware scaffolding and layers the new rules on top. `false` inside a monorepo subpackage with no workspace-root config (the applier refuses to fragment per-package configs). Pass `--no-create-config` to `fallow fix` from pre-commit hooks, CI bots, and `fallow watch` to opt out of the create-fallback; the action then surfaces with `auto_fixable: false`.
1607
+ - `update-catalog-reference` (unresolved-catalog-references): always `false` today; non-singleton on the wire so a future applier can promote it without a schema change.
1608
+ - All `suppress-line` and `suppress-file` actions are uniformly `false`.
1609
+
1610
+ #### Health `actions` array (CRAP findings)
1611
+
1612
+ Health findings (`fallow health` JSON output) include an `actions` array. Primary action selection is formula-aware: the rule first checks whether full coverage CAN bring CRAP under threshold (CRAP bottoms out at `cyclomatic` at 100% coverage, so `cyclomatic < maxCrap` means coverage is a viable remediation), then uses `coverage_tier` to choose the description.
1613
+
1614
+ | Condition | Primary action |
1615
+ |-----------|----------------|
1616
+ | `cyclomatic >= maxCrap` (coverage cannot remediate, regardless of tier) | `refactor-function` |
1617
+ | `cyclomatic < maxCrap` and `coverage_tier=none` | `add-tests` ("start from scratch") |
1618
+ | `cyclomatic < maxCrap` and `coverage_tier=partial` or `high` | `increase-coverage` ("targeted branch coverage") |
1619
+ | Cyclomatic/cognitive triggered (no CRAP) | `refactor-function` |
1620
+
1621
+ The `coverage_tier` field is `"none"` (file not test-reachable / Istanbul 0%), `"partial"` (Istanbul `(0, 70)` / estimated 40%), or `"high"` (Istanbul `>= 70` / estimated 85%).
1622
+
1623
+ Each CRAP finding also carries a `coverage_source` discriminator: `"istanbul"` (direct fnMap match for this function), `"estimated"` (graph-based estimate evaluated against the finding's own file), or `"estimated_component_inherited"` (graph-based estimate inherited from an Angular component `.ts` reached via the inverse `templateUrl` edge). The report summary carries `coverage_source_consistency` (`"uniform"` or `"mixed"`) whenever emitted CRAP findings have source data; grouped health JSON also includes `groups[].coverage_source_consistency`. Synthetic `<template>` findings on Angular `.html` templates use the `estimated_component_inherited` source and include an `inherited_from` field with the project-relative path to the owning `.component.ts`. When the inherit path applies, the primary `increase-coverage` action targets that `.ts` file (description names the component path explicitly and includes a `target_path` field) so AI agents add component tests rather than scaffolding tests against a structurally untestable `.html` path. The human `fallow health` output renders `(inherited from <project-relative-path>.component.ts)` after the CRAP score on those rows (project-relative since fallow 2.78.0; was the bare basename before). This is the JIT-test fallback (Angular's runtime renders templates via `ɵɵconditional` / `ɵɵrepeaterCreate` calls; Istanbul never has `fnMap` entries keyed at `.html` paths). AOT-compiled coverage with source-map back-mapping is planned as a phase 2 follow-up; when it lands, `coverage_source` will gain a `"measured_aot_source_map"` variant.
1624
+
1625
+ When CRAP-only with cyclomatic count within `health.crapRefactorBand` of `maxCyclomatic` AND cognitive at or above `maxCognitive / 2`, a secondary `refactor-function` is appended. The default band is `5`; set it to `0` to only add the secondary refactor after cyclomatic reaches `maxCyclomatic`. The cognitive floor suppresses false positives on flat type-tag dispatchers and JSX render maps (high CC, near-zero cog). A single finding can carry multiple action types: e.g. a finding that exceeds both cyclomatic and CRAP at `coverage_tier=partial` gets `increase-coverage` AND `refactor-function`. Treat the first non-`suppress-line` action as primary.
1626
+
1627
+ The `suppress-line` action is auto-omitted when `--baseline`/`--save-baseline` is set, OR when `health.suggestInlineSuppression: false` in config. The report root carries an `actions_meta: { suppression_hints_omitted: true, reason: "baseline-active" | "config-disabled" }` breadcrumb in that case.
1628
+
1629
+ #### `baseline_deltas` Object
1630
+
1631
+ When `--baseline` is used in combined output, the JSON includes a `baseline_deltas` object showing per-category changes since the baseline:
1632
+
1633
+ ```json
1634
+ {
1635
+ "baseline_deltas": {
1636
+ "total_delta": -3,
1637
+ "per_category": {
1638
+ "unused_files": { "current": 5, "baseline": 7, "delta": -2 },
1639
+ "unused_exports": { "current": 10, "baseline": 11, "delta": -1 }
1640
+ }
1641
+ }
1642
+ }
1643
+ ```
1644
+
1645
+ ### `dupes` output
1646
+
1647
+ ```json
1648
+ {
1649
+ "kind": "dupes",
1650
+ "schema_version": 7,
1651
+ "version": "2.88.2",
1652
+ "elapsed_ms": 82,
1653
+ "total_clones": 15,
1654
+ "total_lines_duplicated": 230,
1655
+ "duplication_percentage": 4.2,
1656
+ "clone_groups": [
1657
+ {
1658
+ "instances": [
1659
+ { "path": "src/a.ts", "start_line": 10, "end_line": 25 },
1660
+ { "path": "src/b.ts", "start_line": 40, "end_line": 55 }
1661
+ ],
1662
+ "tokens": 120,
1663
+ "lines": 16,
1664
+ "family": { "suggestion": "extract_function", "shared_files": ["src/a.ts", "src/b.ts"] }
1665
+ }
1666
+ ],
1667
+ "mirrored_directories": [
1668
+ { "dir_a": "src/components", "dir_b": "src/legacy/components", "shared_clones": 4 }
1669
+ ]
1670
+ }
1671
+ ```
1672
+
1673
+ The `mirrored_directories` array identifies directory pairs that share many clone groups, suggesting structural duplication (e.g., a copy-pasted module that was never cleaned up).
1674
+
1675
+ ### `fix` output (dry-run)
1676
+
1677
+ ```json
1678
+ {
1679
+ "changes": [
1680
+ { "path": "src/utils.ts", "action": "remove_export", "name": "unusedFn", "line": 42 },
1681
+ { "path": "package.json", "action": "remove_dependency", "name": "lodash" }
1682
+ ],
1683
+ "total_changes": 2
1684
+ }
1685
+ ```
1686
+
1687
+ ### Combined output (`fallow` with no subcommand)
1688
+
1689
+ When running `fallow` with no subcommand (all analyses), the JSON output combines results from all enabled analyses:
1690
+
1691
+ ```json
1692
+ {
1693
+ "kind": "combined",
1694
+ "schema_version": 7,
1695
+ "version": "2.88.2",
1696
+ "elapsed_ms": 159,
1697
+ "check": {
1698
+ "schema_version": 7,
1699
+ "version": "2.88.2",
1700
+ "elapsed_ms": 45,
1701
+ "total_issues": 12,
1702
+ "unused_files": [],
1703
+ "unused_exports": [],
1704
+ "unused_types": [],
1705
+ "unused_dependencies": [],
1706
+ "unused_dev_dependencies": [],
1707
+ "unused_enum_members": [],
1708
+ "unused_class_members": [],
1709
+ "unresolved_imports": [],
1710
+ "unlisted_dependencies": [],
1711
+ "duplicate_exports": [],
1712
+ "circular_dependencies": [],
1713
+ "re_export_cycles": [],
1714
+ "boundary_violations": [],
1715
+ "unused_optional_dependencies": [],
1716
+ "type_only_dependencies": [],
1717
+ "test_only_dependencies": [],
1718
+ "stale_suppressions": []
1719
+ },
1720
+ "dupes": {
1721
+ "total_clones": 15,
1722
+ "total_lines_duplicated": 230,
1723
+ "duplication_percentage": 4.2,
1724
+ "clone_groups": []
1725
+ },
1726
+ "health": {
1727
+ "summary": {},
1728
+ "findings": [],
1729
+ "vital_signs": {}
1730
+ }
1731
+ }
1732
+ ```
1733
+
1734
+ Use `--only` or `--skip` to control which analyses are included in the combined output.
1735
+
1736
+ With `--score`, the combined output's `health` section includes a `health_score` object (same schema as `health --score`). With `--trend`, it includes a `health_trend` object comparing against the most recent saved snapshot. With `--save-snapshot`, a vital signs snapshot is persisted for future trend comparisons.
1737
+
1738
+ ### Error output (exit code 2)
1739
+
1740
+ ```json
1741
+ {"error": true, "message": "invalid config: unknown field 'detect'", "exit_code": 2}
1742
+ ```
1743
+
1744
+ ---
1745
+
1746
+ ## Configuration File Format
1747
+
1748
+ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` are parsed as JSON-with-comments; the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting.
1749
+
1750
+ ### JSON Format (`.fallowrc.json` / `.fallowrc.jsonc`)
1751
+
1752
+ ```jsonc
1753
+ {
1754
+ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
1755
+
1756
+ // Entry points (glob patterns)
1757
+ "entry": ["src/index.ts", "scripts/*.ts"],
1758
+
1759
+ // Files to ignore (glob patterns)
1760
+ "ignorePatterns": ["**/*.generated.ts", "**/*.d.ts"],
1761
+
1762
+ // Dependencies to ignore
1763
+ "ignoreDependencies": ["autoprefixer"],
1764
+
1765
+ // Suppress unused-export findings when the symbol is referenced inside its
1766
+ // declaring file (knip parity). Boolean or { type, interface } object form.
1767
+ "ignoreExportsUsedInFile": true,
1768
+
1769
+ // Per-issue-type severity
1770
+ "rules": {
1771
+ "unused-files": "error",
1772
+ "unused-exports": "warn",
1773
+ "unused-types": "off",
1774
+ "unused-dependencies": "error",
1775
+ "unused-dev-dependencies": "warn",
1776
+ "unused-enum-members": "error",
1777
+ "unused-class-members": "warn",
1778
+ "unresolved-imports": "error",
1779
+ "unlisted-dependencies": "error",
1780
+ "duplicate-exports": "warn",
1781
+ "circular-dependencies": "warn",
1782
+ "boundary-violation": "error",
1783
+ "type-only-dependencies": "error",
1784
+ "test-only-dependencies": "warn",
1785
+ "stale-suppressions": "warn"
1786
+ },
1787
+
1788
+ // Per-path rule overrides
1789
+ "overrides": [
1790
+ {
1791
+ "files": ["*.test.ts", "*.spec.ts"],
1792
+ "rules": { "unused-exports": "off" }
1793
+ }
1794
+ ],
1795
+
1796
+ // Duplication settings
1797
+ "duplicates": {
1798
+ "mode": "mild",
1799
+ "minTokens": 50,
1800
+ "minLines": 5,
1801
+ "threshold": 0,
1802
+ "ignoreDefaults": true,
1803
+ "skipLocal": false,
1804
+ "ignorePatterns": ["**/*.generated.ts"]
1805
+ },
1806
+
1807
+ // Architecture boundaries (preset, custom zones/rules, or auto-discovered feature zones)
1808
+ // Presets: "layered", "hexagonal", "feature-sliced", "bulletproof"
1809
+ // Rules accept an optional `allowTypeOnly: [zones]` list that admits type-only imports
1810
+ // (`import type`, inline `{ type Foo }`, namespace type imports, and `export type` re-exports)
1811
+ // to the listed zones even when not present in `allow`. Mixed-specifier imports still fire.
1812
+ "boundaries": {
1813
+ "preset": "bulletproof"
1814
+ // Or:
1815
+ // "zones": [
1816
+ // { "name": "app", "patterns": ["src/app/**"] },
1817
+ // { "name": "features", "patterns": ["src/features/**"], "autoDiscover": ["src/features"] },
1818
+ // { "name": "shared", "patterns": ["src/shared/**"] }
1819
+ // ],
1820
+ // "rules": [
1821
+ // { "from": "app", "allow": ["features", "shared"] },
1822
+ // { "from": "features", "allow": ["shared"], "allowTypeOnly": ["features"] }
1823
+ // ]
1824
+ },
1825
+
1826
+ // Resolve framework convention auto-imports (Nuxt components) as graph edges.
1827
+ // Edges for `<Card001 />`-style template tags are always synthesized; setting
1828
+ // this to true also drops the Nuxt component entry patterns so an
1829
+ // unreferenced component is reported as unused-file. Kept conservative: a
1830
+ // `components:` key in nuxt.config keeps the entry patterns. Default false.
1831
+ "autoImports": false,
1832
+
1833
+ // Production mode
1834
+ "production": false,
1835
+
1836
+ // Workspace packages that are public libraries.
1837
+ // Exported API surface from these packages is not flagged as unused.
1838
+ "publicPackages": ["@myorg/shared-lib", "@myorg/utils"],
1839
+
1840
+ // Glob patterns for files that are dynamically loaded at runtime.
1841
+ // These files are treated as always-used and never flagged as unused.
1842
+ "dynamicallyLoaded": ["plugins/**/*.ts", "locales/**/*.json"],
1843
+
1844
+ // Inherit from base config (prefer local paths or trusted npm packages)
1845
+ "extends": ["./base-config.json", "npm:@my-org/fallow-config"],
1846
+
1847
+ // Custom external plugins
1848
+ "plugins": ["tools/plugins/"],
1849
+
1850
+ // Inline framework definitions
1851
+ "framework": [
1852
+ {
1853
+ "name": "my-framework",
1854
+ "enablers": ["my-framework"],
1855
+ "entryPoints": ["src/routes/**/*.ts"]
1856
+ }
1857
+ ]
1858
+ }
1859
+ ```
1860
+
1861
+ ### TOML Format (`fallow.toml`)
1862
+
1863
+ ```toml
1864
+ entry = ["src/index.ts", "scripts/*.ts"]
1865
+ ignorePatterns = ["**/*.generated.ts"]
1866
+ ignoreDependencies = ["autoprefixer"]
1867
+ ignoreExportsUsedInFile = true
1868
+ production = false
1869
+ publicPackages = ["@myorg/shared-lib", "@myorg/utils"]
1870
+ dynamicallyLoaded = ["plugins/**/*.ts", "locales/**/*.json"]
1871
+
1872
+ [rules]
1873
+ unused-files = "error"
1874
+ unused-exports = "warn"
1875
+ unused-types = "off"
1876
+
1877
+ [duplicates]
1878
+ mode = "mild"
1879
+ minTokens = 50
1880
+ minLines = 5
1881
+ ignoreDefaults = true
1882
+
1883
+ [[overrides]]
1884
+ files = ["*.test.ts"]
1885
+ [overrides.rules]
1886
+ unused-exports = "off"
1887
+
1888
+ [boundaries]
1889
+ preset = "bulletproof"
1890
+ ```
1891
+
1892
+ ---
1893
+
1894
+ ## Inline Suppression Comments
1895
+
1896
+ | Comment | Effect |
1897
+ |---------|--------|
1898
+ | `// fallow-ignore-next-line` | Suppress any issue on the next line |
1899
+ | `// fallow-ignore-next-line unused-export` | Suppress specific issue type |
1900
+ | `// fallow-ignore-file` | Suppress all issues in a file |
1901
+ | `// fallow-ignore-file unused-export` | Suppress specific issue type file-wide |
1902
+
1903
+ ### Valid Issue Type Tokens
1904
+
1905
+ `unused-file`, `unused-export`, `unused-type`, `unused-dependency`, `unused-dev-dependency`, `unused-enum-member`, `unused-class-member`, `unresolved-import`, `unlisted-dependency`, `duplicate-export`, `circular-dependency`, `re-export-cycle`, `boundary-violation`, `unused-optional-dependency`, `type-only-dependency`, `test-only-dependency`, `code-duplication`