fallow 2.69.0 → 2.70.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "2.69.0",
3
+ "version": "2.70.0",
4
4
  "description": "Codebase intelligence for TypeScript and JavaScript. Finds unused code, duplication, circular dependencies, complexity hotspots, and architecture drift. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 94 framework plugins.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -55,13 +55,13 @@
55
55
  "@tanstack/intent": "0.0.36"
56
56
  },
57
57
  "optionalDependencies": {
58
- "@fallow-cli/darwin-arm64": "2.69.0",
59
- "@fallow-cli/darwin-x64": "2.69.0",
60
- "@fallow-cli/linux-x64-gnu": "2.69.0",
61
- "@fallow-cli/linux-arm64-gnu": "2.69.0",
62
- "@fallow-cli/linux-x64-musl": "2.69.0",
63
- "@fallow-cli/linux-arm64-musl": "2.69.0",
64
- "@fallow-cli/win32-arm64-msvc": "2.69.0",
65
- "@fallow-cli/win32-x64-msvc": "2.69.0"
58
+ "@fallow-cli/darwin-arm64": "2.70.0",
59
+ "@fallow-cli/darwin-x64": "2.70.0",
60
+ "@fallow-cli/linux-x64-gnu": "2.70.0",
61
+ "@fallow-cli/linux-arm64-gnu": "2.70.0",
62
+ "@fallow-cli/linux-x64-musl": "2.70.0",
63
+ "@fallow-cli/linux-arm64-musl": "2.70.0",
64
+ "@fallow-cli/win32-arm64-msvc": "2.70.0",
65
+ "@fallow-cli/win32-x64-msvc": "2.70.0"
66
66
  }
67
67
  }
package/schema.json CHANGED
@@ -148,7 +148,8 @@
148
148
  "boundary-violation": "error",
149
149
  "coverage-gaps": "off",
150
150
  "feature-flags": "off",
151
- "stale-suppressions": "warn"
151
+ "stale-suppressions": "warn",
152
+ "unused-catalog-entries": "warn"
152
153
  }
153
154
  },
154
155
  "boundaries": {
@@ -873,6 +874,10 @@
873
874
  "stale-suppressions": {
874
875
  "$ref": "#/$defs/Severity",
875
876
  "default": "warn"
877
+ },
878
+ "unused-catalog-entries": {
879
+ "$ref": "#/$defs/Severity",
880
+ "default": "warn"
876
881
  }
877
882
  }
878
883
  },
@@ -1317,6 +1322,16 @@
1317
1322
  "type": "null"
1318
1323
  }
1319
1324
  ]
1325
+ },
1326
+ "unused-catalog-entries": {
1327
+ "anyOf": [
1328
+ {
1329
+ "$ref": "#/$defs/Severity"
1330
+ },
1331
+ {
1332
+ "type": "null"
1333
+ }
1334
+ ]
1320
1335
  }
1321
1336
  }
1322
1337
  },
@@ -15,6 +15,7 @@ Complete command and flag specifications for all fallow CLI commands.
15
15
  - [`health`: Function Complexity Analysis](#health-function-complexity-analysis)
16
16
  - [`audit`: Changed-File Quality Gate](#audit-changed-file-quality-gate)
17
17
  - [`flags`: Feature Flag Detection](#flags-feature-flag-detection)
18
+ - [`explain`: Rule Explanation](#explain-rule-explanation)
18
19
  - [`schema`: CLI Introspection](#schema-cli-introspection)
19
20
  - [`config-schema`: Config JSON Schema](#config-schema-config-json-schema)
20
21
  - [`plugin-schema`: Plugin JSON Schema](#plugin-schema-plugin-json-schema)
@@ -36,7 +37,7 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
36
37
 
37
38
  | Flag | Type | Default | Description |
38
39
  |------|------|---------|-------------|
39
- | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality` | `human` | Output format |
40
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
40
41
  | `--quiet` | bool | `false` | Suppress progress bars and timing on stderr |
41
42
  | `--changed-since` | string | — | Only analyze files changed since a git ref (e.g., `main`, `HEAD~3`) |
42
43
  | `--production` | bool | `false` | Exclude test/dev files, only start/build scripts (applies to every analysis) |
@@ -61,6 +62,7 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
61
62
  | `--unused-files` | Unused files |
62
63
  | `--unused-exports` | Unused exports |
63
64
  | `--unused-types` | Unused types |
65
+ | `--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). |
64
66
  | `--unused-deps` | Unused dependencies, devDependencies, optionalDependencies, type-only production deps, and test-only production deps |
65
67
  | `--unused-enum-members` | Unused enum members |
66
68
  | `--unused-class-members` | Unused class members |
@@ -128,11 +130,13 @@ fallow dead-code --format json --quiet --include-entry-exports
128
130
 
129
131
  Finds code duplication and clones across the project.
130
132
 
133
+ 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.
134
+
131
135
  ### Flags
132
136
 
133
137
  | Flag | Type | Default | Description |
134
138
  |------|------|---------|-------------|
135
- | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality` | `human` | Output format |
139
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
136
140
  | `--quiet` | bool | `false` | Suppress progress bars |
137
141
  | `--top` | number | — | Show only the N largest clone groups (sorted by line count descending). Summary stats reflect the full project. |
138
142
  | `--mode` | `strict\|mild\|weak\|semantic` | `mild` | Detection mode |
@@ -142,12 +146,14 @@ Finds code duplication and clones across the project.
142
146
  | `--skip-local` | bool | `false` | Only report cross-directory duplicates |
143
147
  | `--cross-language` | bool | `false` | Strip type annotations for TS↔JS matching |
144
148
  | `--ignore-imports` | bool | `false` | Exclude import declarations from clone detection |
149
+ | `--explain-skipped` | bool | `false` | Human/markdown only: show per-pattern counts for files skipped by default duplicates ignores |
145
150
  | `--trace` | `FILE:LINE` | — | Trace all clones at a specific location |
146
151
  | `--changed-since` | string | — | Only report duplication in files changed since a git ref |
147
152
  | `--baseline` | path | — | Compare against baseline |
148
153
  | `--save-baseline` | path | — | Save results as baseline |
149
154
  | `--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. |
150
155
  | `--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. |
156
+ | `--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. |
151
157
 
152
158
  ### Detection Modes
153
159
 
@@ -244,16 +250,16 @@ Creates a config file in the project root.
244
250
  | Flag | Type | Description |
245
251
  |------|------|-------------|
246
252
  | `--toml` | bool | Create `fallow.toml` instead of `.fallowrc.json` |
247
- | `--hooks` | bool | Scaffold a pre-commit git hook that runs `fallow dead-code --changed-since` on staged files |
248
- | `--branch` | string | Base branch for the pre-commit hook (default: auto-detected or `main`). Only used with `--hooks` |
253
+ | `--hooks` | bool | Scaffold a pre-commit git hook that runs `fallow audit --base <ref> --quiet`. Alias for `fallow hooks install --target git` |
254
+ | `--branch` | string | Fallback base branch for the pre-commit hook when no upstream is set (default: `main`). Only used with `--hooks` |
249
255
 
250
256
  ### Examples
251
257
 
252
258
  ```bash
253
259
  fallow init # creates .fallowrc.json with $schema
254
260
  fallow init --toml # creates fallow.toml
255
- fallow init --hooks # scaffold a pre-commit git hook (auto-detects base branch)
256
- fallow init --hooks --branch develop # hook using custom base branch
261
+ fallow hooks install --target git
262
+ fallow hooks install --target git --branch develop # fallback base branch when no upstream is set
257
263
  ```
258
264
 
259
265
  ---
@@ -296,38 +302,41 @@ fallow migrate --from knip.jsonc
296
302
 
297
303
  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.
298
304
 
305
+ 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).
306
+
299
307
  ### Flags
300
308
 
301
309
  | Flag | Type | Default | Description |
302
310
  |------|------|---------|-------------|
303
- | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|badge` | `human` | Output format |
311
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab\|badge` | `human` | Output format |
304
312
  | `--quiet` | bool | `false` | Suppress progress bars |
305
313
  | `--max-cyclomatic` | number | `20` | Fail if any function exceeds this cyclomatic complexity |
306
314
  | `--max-cognitive` | number | `15` | Fail if any function exceeds this cognitive complexity |
307
315
  | `--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. |
308
316
  | `--top` | number | — | Only show the top N most complex functions (and file scores/hotspots/targets) |
309
- | `--sort` | `cyclomatic\|cognitive\|lines` | `cyclomatic` | Sort order for complexity findings |
317
+ | `--sort` | `cyclomatic\|cognitive\|lines\|severity` | `cyclomatic` | Sort order for complexity findings |
310
318
  | `--complexity` | bool | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. |
311
319
  | `--file-scores` | bool | `false` | Show only per-file maintainability index (LOC, fan-in, fan-out, dead code ratio, complexity density). Runs the full analysis pipeline. When no section flags are set, all sections are shown by default. |
312
320
  | `--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. |
313
321
  | `--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. |
314
322
  | `--effort` | `low\|medium\|high` | — | Filter refactoring targets by effort level. Implies `--targets`. |
315
- | `--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. |
323
+ | `--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. |
316
324
  | `--min-score` | number | — | Fail if health score is below this threshold (exit code 1). Implies `--score`. CI quality gate. |
317
325
  | `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). |
318
326
  | `--min-commits` | number | `3` | Minimum number of commits for a file to be included in hotspot ranking. |
319
327
  | `--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 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. |
320
328
  | `--ownership-emails` | `raw\|handle\|hash` | `handle` | Privacy mode for author emails. `handle` shows the local-part only (default, with GitHub noreply unwrap). `hash` emits stable `xxh3:` pseudonyms. `raw` shows full addresses. Use `hash` in regulated environments. Implies `--ownership`. Configure default via `health.ownership.emailMode`. |
321
329
  | `--changed-since` | string | — | Only analyze files changed since a git ref |
322
- | `--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. |
323
- | `--baseline` | path | — | Compare against a saved baseline |
324
- | `--save-baseline` | path | — | Save current results as a baseline |
330
+ | `--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. |
331
+ | `--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. |
332
+ | `--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. |
333
+ | `--save-baseline` | path | — | Save current results as a baseline. Same `suppress-line` omission as `--baseline`. |
325
334
  | `--save-snapshot` | path (optional) | `.fallow/snapshots/<timestamp>.json` | Save vital signs snapshot for trend tracking. Forces file-scores + hotspot computation. |
326
335
  | `--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`. |
327
336
  | `--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`). |
328
- | `--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. |
329
- | `--coverage-root` | path | none | Rebase file paths in coverage data by stripping this prefix and prepending the project root. For CI/Docker environments where coverage was generated with different absolute paths. |
330
- | `--runtime-coverage` | path | none | Merge runtime runtime-coverage input into the health report (paid feature). Accepts a V8 coverage directory (`NODE_V8_COVERAGE=...`), a single V8 coverage JSON file, or an Istanbul `coverage-final.json`. Requires an active license; start one with `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`), stable content-hash IDs (`fallow:prod:<hash>`), 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. |
337
+ | `--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`. |
338
+ | `--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. |
339
+ | `--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`), stable content-hash IDs (`fallow:prod:<hash>`), 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. |
331
340
  | `--min-invocations-hot` | number | `100` | Invocation threshold for hot-path classification. Takes effect only when `--runtime-coverage` is set. |
332
341
  | `--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`. |
333
342
  | `--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%). |
@@ -415,7 +424,7 @@ fallow health --format json --quiet --trend
415
424
  ```json
416
425
  {
417
426
  "schema_version": 3,
418
- "version": "2.47.0",
427
+ "version": "2.69.0",
419
428
  "elapsed_ms": 32,
420
429
  "summary": {
421
430
  "files_analyzed": 482,
@@ -569,17 +578,23 @@ All `health` JSON output includes a `vital_signs` object with project-wide metri
569
578
  "dead_file_pct": 3.2,
570
579
  "dead_export_pct": 8.1,
571
580
  "avg_cyclomatic": 4.5,
581
+ "critical_complexity_pct": 1.2,
572
582
  "p90_cyclomatic": 12,
573
583
  "maintainability_avg": 88.5,
584
+ "maintainability_low_pct": 4.1,
574
585
  "hotspot_count": 7,
586
+ "hotspot_top_pct_count": 3,
575
587
  "circular_dep_count": 2,
588
+ "circular_deps_per_k_files": 4.1,
576
589
  "unused_dep_count": 3,
590
+ "unused_deps_per_k_files": 6.2,
577
591
  "unit_size_profile": {
578
592
  "low_risk": 82.1,
579
593
  "medium_risk": 11.4,
580
594
  "high_risk": 4.3,
581
595
  "very_high_risk": 2.2
582
596
  },
597
+ "functions_over_60_loc_per_k": 22.0,
583
598
  "unit_interfacing_profile": {
584
599
  "low_risk": 95.6,
585
600
  "medium_risk": 3.8,
@@ -592,13 +607,14 @@ All `health` JSON output includes a `vital_signs` object with project-wide metri
592
607
  }
593
608
  ```
594
609
 
595
- 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). 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.
610
+ 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.
596
611
 
597
612
  With `--score`, the JSON output includes a `health_score` object:
598
613
 
599
614
  ```json
600
615
  {
601
616
  "health_score": {
617
+ "formula_version": 2,
602
618
  "score": 76.9,
603
619
  "grade": "B",
604
620
  "penalties": {
@@ -607,7 +623,6 @@ With `--score`, the JSON output includes a `health_score` object:
607
623
  "complexity": 0.0,
608
624
  "p90_complexity": 0.0,
609
625
  "maintainability": 0.0,
610
- "hotspots": 0.0,
611
626
  "unused_deps": 10.0,
612
627
  "circular_deps": 4.0,
613
628
  "unit_size": 0.0,
@@ -618,7 +633,7 @@ With `--score`, the JSON output includes a `health_score` object:
618
633
  }
619
634
  ```
620
635
 
621
- Score is reproducible: `100 - sum(penalties) == score`. Penalty fields are absent when the pipeline didn't run. `--score` automatically runs duplication analysis. Grades: A (>= 85), B (70-84), C (55-69), D (40-54), F (< 40).
636
+ 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).
622
637
 
623
638
  ### Health Trend
624
639
 
@@ -661,7 +676,7 @@ With `--trend`, the JSON output includes a `health_trend` object comparing curre
661
676
  }
662
677
  ```
663
678
 
664
- 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: v5), the trend output warns that score deltas may reflect formula changes.
679
+ 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.
665
680
 
666
681
  ### Vital Signs Snapshots
667
682
 
@@ -669,17 +684,23 @@ Metrics tracked: `score`, `dead_file_pct`, `dead_export_pct`, `avg_cyclomatic`,
669
684
 
670
685
  ```json
671
686
  {
672
- "schema_version": 5,
687
+ "snapshot_schema_version": 8,
673
688
  "timestamp": "2025-12-01T10:30:00Z",
674
689
  "vital_signs": {
675
690
  "dead_file_pct": 3.2,
676
691
  "dead_export_pct": 8.1,
677
692
  "avg_cyclomatic": 4.5,
693
+ "critical_complexity_pct": 1.2,
678
694
  "p90_cyclomatic": 12,
679
695
  "maintainability_avg": 88.5,
696
+ "maintainability_low_pct": 4.1,
680
697
  "hotspot_count": 7,
698
+ "hotspot_top_pct_count": 3,
681
699
  "circular_dep_count": 2,
682
- "unused_dep_count": 3
700
+ "circular_deps_per_k_files": 4.1,
701
+ "unused_dep_count": 3,
702
+ "unused_deps_per_k_files": 6.2,
703
+ "functions_over_60_loc_per_k": 22.0
683
704
  },
684
705
  "counts": {
685
706
  "total_files": 482,
@@ -695,19 +716,20 @@ Metrics tracked: `score`, `dead_file_pct`, `dead_export_pct`, `avg_cyclomatic`,
695
716
  }
696
717
  ```
697
718
 
698
- The 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.
719
+ 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.
699
720
 
700
721
  ---
701
722
 
702
723
  ## `audit`: Changed-File Quality Gate
703
724
 
704
- 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.
725
+ 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.
705
726
 
706
727
  ### Flags
707
728
 
708
729
  | Flag | Type | Default | Description |
709
730
  |------|------|---------|-------------|
710
731
  | `--base` | string | auto-detect | Git ref to compare against (alias for `--changed-since`) |
732
+ | `--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. |
711
733
  | `--production` | bool | false | Exclude test/story/dev files (applies to dead-code, health, and dupes) |
712
734
  | `--production-dead-code` | bool | false | Per-analysis production mode for the dead-code sub-analysis only |
713
735
  | `--production-health` | bool | false | Per-analysis production mode for the health sub-analysis only |
@@ -721,6 +743,8 @@ Audits changed files for dead code, complexity, and duplication. Returns a verdi
721
743
  | `--health-baseline` | path | — | Baseline file (produced by `fallow health --save-baseline`). Pre-existing complexity findings are excluded from the verdict. |
722
744
  | `--dupes-baseline` | path | — | Baseline file (produced by `fallow dupes --save-baseline`). Pre-existing clone groups are excluded from the verdict. |
723
745
  | `--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. |
746
+ | `--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`. |
747
+ | `--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). |
724
748
  | `--fail-on-regression` | bool | false | Fail if issues increased beyond tolerance vs regression baseline |
725
749
  | `--tolerance` | string | `0` | Allowed increase before regression fails (`N` or `N%`) |
726
750
  | `--regression-baseline` | path | `.fallow/regression-baseline.json` | Path to the regression baseline file |
@@ -735,6 +759,19 @@ Audits changed files for dead code, complexity, and duplication. Returns a verdi
735
759
  | fail | 1 | Error-severity issues found |
736
760
  | error | 2 | Runtime error (invalid ref, not a git repo) |
737
761
 
762
+ 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.
763
+
764
+ ### JSON contract: which fields are severity-aware
765
+
766
+ | Field | Severity-aware? | What it counts |
767
+ |-------|-----------------|----------------|
768
+ | `verdict` | **yes** | Overall outcome honoring per-rule severity (`pass` / `warn` / `fail`) |
769
+ | `attribution.*_introduced` | no | Findings introduced by the changeset under `gate: new-only`, ignoring severity |
770
+ | `summary.*` | no | All findings in changed files, ignoring severity |
771
+ | Per-finding `introduced` | no | Whether each finding was introduced by the changeset |
772
+
773
+ 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.
774
+
738
775
  ### Examples
739
776
 
740
777
  ```bash
@@ -747,6 +784,9 @@ fallow audit --format json --quiet --base main
747
784
  # Audit last 3 commits
748
785
  fallow audit --format json --quiet --base HEAD~3
749
786
 
787
+ # Strict mode: fail on inherited findings too
788
+ fallow audit --format json --quiet --gate all
789
+
750
790
  # Production code only in a monorepo workspace
751
791
  fallow audit --format json --quiet --production --workspace @app/api
752
792
 
@@ -770,7 +810,7 @@ fallow audit \
770
810
  ```json
771
811
  {
772
812
  "schema_version": 3,
773
- "version": "2.47.0",
813
+ "version": "2.69.0",
774
814
  "command": "audit",
775
815
  "verdict": "fail",
776
816
  "changed_files_count": 12,
@@ -784,10 +824,19 @@ fallow audit \
784
824
  "max_cyclomatic": 28,
785
825
  "duplication_clone_groups": 0
786
826
  },
827
+ "attribution": {
828
+ "gate": "new-only",
829
+ "dead_code_introduced": 2,
830
+ "dead_code_inherited": 0,
831
+ "complexity_introduced": 1,
832
+ "complexity_inherited": 0,
833
+ "duplication_introduced": 0,
834
+ "duplication_inherited": 0
835
+ },
787
836
  "dead_code": {
788
837
  "schema_version": 3,
789
838
  "total_issues": 2,
790
- "unused_exports": [...]
839
+ "unused_exports": [{ "path": "src/api.ts", "export_name": "oldApi", "introduced": true, "actions": [...] }]
791
840
  },
792
841
  "complexity": {
793
842
  "findings": [...]
@@ -798,7 +847,9 @@ fallow audit \
798
847
  }
799
848
  ```
800
849
 
801
- The `verdict` field is always present and is the primary decision signal. 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).
850
+ 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).
851
+
852
+ 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.
802
853
 
803
854
  ---
804
855
 
@@ -810,7 +861,7 @@ Detects feature flag patterns in the codebase. Identifies environment variable f
810
861
 
811
862
  | Flag | Type | Default | Description |
812
863
  |------|------|---------|-------------|
813
- | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality` | `human` | Output format |
864
+ | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
814
865
  | `--quiet` | bool | `false` | Suppress progress bars |
815
866
  | `--top` | number | — | Show only the top N flags |
816
867
 
@@ -832,7 +883,7 @@ fallow flags --format json --quiet --workspace my-package
832
883
  ```json
833
884
  {
834
885
  "schema_version": 3,
835
- "version": "2.47.0",
886
+ "version": "2.69.0",
836
887
  "elapsed_ms": 116,
837
888
  "feature_flags": [],
838
889
  "total_flags": 0
@@ -841,6 +892,41 @@ fallow flags --format json --quiet --workspace my-package
841
892
 
842
893
  ---
843
894
 
895
+ ## `explain`: Rule Explanation
896
+
897
+ Print rule rationale, examples, fix guidance, and docs URL for one issue type without running analysis.
898
+
899
+ ### Usage
900
+
901
+ ```bash
902
+ fallow explain unused-export
903
+ fallow explain fallow/code-duplication --format json --quiet
904
+ ```
905
+
906
+ ### Arguments
907
+
908
+ | Argument | Description |
909
+ |----------|-------------|
910
+ | `<issue-type>` | Issue type token or rule id, for example `unused-export`, `unused-exports`, `fallow/unused-dependency`, `high-complexity`, or `code-duplication`. |
911
+
912
+ ### JSON Output Structure
913
+
914
+ ```json
915
+ {
916
+ "id": "fallow/unused-export",
917
+ "name": "Unused Exports",
918
+ "summary": "Export is never imported",
919
+ "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.",
920
+ "example": "export const formatPrice = ... exists in src/money.ts, but no module imports formatPrice.",
921
+ "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.",
922
+ "docs": "https://docs.fallow.tools/explanations/dead-code#unused-exports"
923
+ }
924
+ ```
925
+
926
+ MCP equivalent: `fallow_explain` with required `issue_type`.
927
+
928
+ ---
929
+
844
930
  ## `schema`: CLI Introspection
845
931
 
846
932
  Dumps the full CLI interface definition as machine-readable JSON.
@@ -871,9 +957,9 @@ fallow plugin-schema > plugin-schema.json
871
957
 
872
958
  ---
873
959
 
874
- ## `license`: Manage Paid-Feature License
960
+ ## `license`: Manage Continuous Runtime License
875
961
 
876
- Manage the local JWT used to unlock paid features (Phase 2 runtime coverage). 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).
962
+ 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).
877
963
 
878
964
  ```bash
879
965
  fallow license activate --trial --email you@company.com
@@ -915,7 +1001,7 @@ fallow license deactivate
915
1001
  |-----------------|-------|----------|
916
1002
  | `<= 7` | ExpiredWarning | Analysis runs; CLI prints a refresh hint |
917
1003
  | `> 7, <= 30` | ExpiredWatermark | Analysis runs; output gains a visible watermark until refreshed |
918
- | `> 30` | HardFail | Paid features blocked; run `fallow license refresh` or start a new trial |
1004
+ | `> 30` | HardFail | Continuous/cloud runtime monitoring is blocked; run `fallow license refresh` or start a new trial |
919
1005
 
920
1006
  ### Actionable error messages
921
1007
 
@@ -943,9 +1029,9 @@ Unknown codes fall back to the backend's `message` field, or the raw body.
943
1029
 
944
1030
  ## `coverage`: Production-Coverage Workflow
945
1031
 
946
- Helper subcommand for the runtime-coverage workflow. Three subcommands today:
1032
+ Helper subcommand for runtime coverage setup, focused analysis, and cloud inventory upload. Three subcommands today:
947
1033
 
948
- - `coverage setup` — resumable state machine that wires license activation, sidecar installation, framework-aware coverage recipe writing, and automatic handoff into `fallow health --runtime-coverage`.
1034
+ - `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`.
949
1035
  - `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.
950
1036
  - `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).
951
1037
 
@@ -953,37 +1039,53 @@ Helper subcommand for the runtime-coverage workflow. Three subcommands today:
953
1039
  fallow coverage setup # interactive
954
1040
  fallow coverage setup --yes # accept all prompts
955
1041
  fallow coverage setup --non-interactive # print instructions, do not prompt
1042
+ fallow coverage setup --yes --json # agent-readable JSON, no prompts/writes/installs/network
1043
+ fallow coverage setup --yes --json --explain # add _meta field docs, enums, warnings, docs URL
956
1044
 
957
1045
  fallow coverage analyze --runtime-coverage ./coverage --format json
958
1046
  fallow coverage analyze --cloud --repo owner/repo --format json
959
1047
 
960
1048
  fallow coverage upload-inventory # infers project-id, git-sha, API key
961
1049
  fallow coverage upload-inventory --dry-run # print what would be uploaded, exit 0
1050
+
1051
+ fallow coverage upload-source-maps --dir dist # upload build source maps from CI
1052
+ fallow coverage upload-source-maps --dry-run # print maps and fileName values, no network
962
1053
  ```
963
1054
 
1055
+ `--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.
1056
+
1057
+ ### `setup` flow
1058
+
1059
+ 1. **License check** — if missing or hard-fail, offers to start a trial.
1060
+ 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.
1061
+ 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.
1062
+ 4. **Handoff** — if `./coverage/coverage-final.json` or a V8 coverage directory already exists, setup runs `fallow health --runtime-coverage <path>` directly.
1063
+
964
1064
  ### `analyze` flags
965
1065
 
966
1066
  | Flag | Type | Default | Description |
967
1067
  |------|------|---------|-------------|
968
1068
  | `--runtime-coverage <PATH>` | path | none | Local V8 directory, V8 JSON file, or Istanbul coverage map. Mutually exclusive with cloud mode. |
969
1069
  | `--cloud`, `--runtime-coverage-cloud` | bool | false | Explicitly fetch cloud runtime facts from `/v1/coverage/:repo/runtime-context`. |
970
- | `--repo <OWNER/REPO>` | string | `$FALLOW_REPO` → `git remote get-url origin` | Repository identifier for cloud mode. Encoded as one URL path segment (`owner%2Frepo`). |
971
- | `--api-key <KEY>` | string | `$FALLOW_API_KEY` | Fallow cloud bearer token for cloud mode. The env var alone does not opt in to cloud mode. |
1070
+ | `--api-key <KEY>` | string | `$FALLOW_API_KEY` | Fallow cloud bearer token, used only after explicit cloud opt-in. |
972
1071
  | `--api-endpoint <URL>` | string | `$FALLOW_API_URL` or `https://api.fallow.cloud` | Override for staging / on-prem. |
1072
+ | `--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. |
973
1073
  | `--coverage-period <DAYS>` | integer | 30 | Cloud observation window, 1 through 90 days. |
974
- | `--project-id <ID>` | string | none | Optional monorepo/project disambiguator. |
975
- | `--environment <ENV>` | string | none | Optional cloud environment filter. |
976
- | `--commit-sha <SHA>` | string | none | Optional cloud commit filter. |
977
- | `--production` | bool | false | Analyze production code only before joining cloud facts. |
1074
+ | `--project-id <ID>` | string | none | Optional project discriminator for monorepos. |
1075
+ | `--environment <NAME>` | string | none | Optional environment filter. |
1076
+ | `--commit-sha <SHA>` | string | none | Optional advanced filter for a specific observed commit. |
1077
+ | `--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. |
1078
+ | `--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. |
1079
+ | `--importance` | bool | false | Show the first-class importance section in human output. JSON always includes `runtime_coverage.importance` whenever runtime coverage analysis runs. |
1080
+ | `--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. |
1081
+ | `--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`. |
1082
+ | `--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`. |
1083
+ | `--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%. |
1084
+ | `--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. |
978
1085
 
979
1086
  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.
980
1087
 
981
- ### `setup` flow
982
-
983
- 1. **License check** — if missing or hard-fail, offers to start a trial.
984
- 2. **Sidecar discovery** — resolves `FALLOW_COV_BIN`, project-local `node_modules/.bin/fallow-cov`, package-manager bin, `~/.fallow/bin/fallow-cov`, and `PATH`. When `FALLOW_COV_BIN` is set but points to a non-existent file, setup errors fast instead of falling through.
985
- 3. **Coverage recipe** — detects framework (Next.js, Nuxt, Astro, SvelteKit, Remix, NestJS, plain Node) and package manager (npm, pnpm, yarn, bun), then writes `docs/collect-coverage.md` with the correct commands.
986
- 4. **Handoff** — if `./coverage/coverage-final.json` or a V8 coverage directory already exists, setup runs `fallow health --runtime-coverage <path>` directly.
1088
+ 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.
987
1089
 
988
1090
  ### `upload-inventory` flags
989
1091
 
@@ -1004,9 +1106,26 @@ Only plain JS/TS/JSX/TSX sources are walked. Declaration files (`*.d.ts`, `*.d.m
1004
1106
  ### Environment
1005
1107
 
1006
1108
  - `FALLOW_COV_BIN` — explicit override for the sidecar binary (for `setup`). Wins over all other discovery paths. Must point to an existing file.
1007
- - `FALLOW_API_KEY` — fallow cloud bearer token (for `upload-inventory`). Overridden by `--api-key`.
1109
+ - `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.
1008
1110
  - `FALLOW_API_URL` — base URL for cloud calls. Overridden by `--api-endpoint`.
1009
1111
 
1112
+ ### `coverage upload-source-maps` flags
1113
+
1114
+ 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.
1115
+
1116
+ | Flag | Type | Default | Description |
1117
+ |------|------|---------|-------------|
1118
+ | `--dir <PATH>` | path | `dist` | Directory scanned recursively. |
1119
+ | `--include <GLOB>` | glob | `**/*.map` | Include glob relative to `--dir`. |
1120
+ | `--exclude <GLOB>` | glob | `**/node_modules/**` | Exclude glob, repeatable. |
1121
+ | `--repo <NAME>` | string | `package.json` `repository.url`, then `git remote get-url origin` | Repo name used in the source-map API path. |
1122
+ | `--git-sha <SHA>` | string | `$GITHUB_SHA` -> `$CI_COMMIT_SHA` -> `$COMMIT_SHA` -> `git rev-parse HEAD` | Commit SHA, 7-40 hex chars. |
1123
+ | `--endpoint <URL>` | string | `$FALLOW_API_URL` or `https://api.fallow.cloud` | Override for staging / on-prem. |
1124
+ | `--strip-path <BOOL>` | bool | `true` | Upload basename-only `fileName` values. Use `--strip-path=false` when runtime coverage reports paths like `assets/app.js`. |
1125
+ | `--dry-run` | bool | `false` | Print what would upload; no API key or network call. |
1126
+ | `--concurrency <N>` | integer | `4` | Parallel upload fanout. |
1127
+ | `--fail-fast` | bool | `false` | Stop on the first upload failure. |
1128
+
1010
1129
  ### Exit Codes
1011
1130
 
1012
1131
  | Code | Meaning |
@@ -1098,7 +1217,7 @@ Available on all commands:
1098
1217
  | `FALLOW_QUIET` | Set to `1` to suppress progress. CLI `--quiet` overrides. |
1099
1218
  | `FALLOW_BIN` | Path to fallow binary (used by the MCP server). |
1100
1219
  | `FALLOW_TIMEOUT_SECS` | MCP server subprocess timeout in seconds (default: `120`). Increase for very large codebases. |
1101
- | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote `extends` URLs in seconds (default: `5`). |
1220
+ | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
1102
1221
  | `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
1103
1222
  | `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
1104
1223
  | `FALLOW_CHANGED_SINCE` | GitLab CI: git ref for incremental analysis. Auto-detected in MR pipelines. |
@@ -1107,6 +1226,7 @@ Available on all commands:
1107
1226
  | `FALLOW_SCORE` | GitLab CI: set to `true` to compute health score in combined mode. Enables health delta header in MR comments. |
1108
1227
  | `FALLOW_TREND` | GitLab CI: set to `true` to compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. |
1109
1228
  | `FALLOW_EXTRA_ARGS` | GitLab CI: additional CLI flags passed through to fallow. |
1229
+ | `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. |
1110
1230
  | `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). |
1111
1231
 
1112
1232
  Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid passing flags on every invocation.
@@ -1123,6 +1243,29 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1123
1243
  | `compact` | Grep-friendly: `type:path:line:name` per line | Quick filtering |
1124
1244
  | `markdown` | Markdown tables | Documentation, PR comments |
1125
1245
  | `codeclimate` / `gitlab-codequality` | CodeClimate JSON array | GitLab Code Quality, CodeClimate-compatible tools |
1246
+ | `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 |
1247
+ | `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` |
1248
+
1249
+ ---
1250
+
1251
+ ## `ci`: Provider-Aware Review Automation
1252
+
1253
+ `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).
1254
+
1255
+ ### Flags
1256
+
1257
+ | Flag | Type | Description |
1258
+ |------|------|-------------|
1259
+ | `--provider` | `github\|gitlab` | Required. Selects the provider API. |
1260
+ | `--pr` | `<number>` | GitHub PR number. Required when `--provider github`. |
1261
+ | `--mr` | `<iid>` | GitLab MR internal id. Required when `--provider gitlab`. |
1262
+ | `--repo` | `owner/name` | GitHub repo. Defaults to `$GH_REPO` / `$GITHUB_REPOSITORY`. |
1263
+ | `--project-id` | `<id>` | GitLab project id (numeric or `group/project`). Defaults to `$CI_PROJECT_ID`. |
1264
+ | `--api-url` | `<url>` | Override the API base URL (GitHub Enterprise, self-hosted GitLab). |
1265
+ | `--envelope` | `<path>` | Path to the review envelope JSON written by `--format review-{github,gitlab}`. |
1266
+ | `--dry-run` | `bool` | Compute the new/stale plan without posting / resolving. |
1267
+
1268
+ 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.
1126
1269
 
1127
1270
  ---
1128
1271
 
@@ -1141,11 +1284,10 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1141
1284
  | `FALLOW_CHANGED_SINCE` | auto | Git ref for incremental analysis. Auto-detected in MR pipelines (`origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`) |
1142
1285
  | `FALLOW_COMMENT` | `false` | Post a summary comment on the MR with findings |
1143
1286
  | `FALLOW_REVIEW` | `false` | Post inline code review comments on MR diff lines where issues were found |
1144
- | `FALLOW_VERSION` | — | Fallow version to install. Empty reads the project's `package.json` `fallow` dependency, then falls back to `latest`; set explicitly to override the local pin |
1145
1287
  | `FALLOW_SCORE` | `false` | Compute health score (0-100 with letter grade) in combined mode. Enables the health delta header in MR comments |
1146
1288
  | `FALLOW_TREND` | `false` | Compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. Shows per-metric deltas |
1147
1289
  | `FALLOW_EXTRA_ARGS` | — | Additional CLI flags passed through to fallow |
1148
- | `GITLAB_TOKEN` | — | Project access token with `api` scope (required for `FALLOW_COMMENT` and `FALLOW_REVIEW`; `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API) |
1290
+ | `GITLAB_TOKEN` | — | Project access token with `api` scope (required for `FALLOW_COMMENT` and `FALLOW_REVIEW`). Alternatively, enable job token API access |
1149
1291
 
1150
1292
  **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`).
1151
1293
 
@@ -1160,7 +1302,7 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1160
1302
  ```json
1161
1303
  {
1162
1304
  "schema_version": 3,
1163
- "version": "2.47.0",
1305
+ "version": "2.69.0",
1164
1306
  "elapsed_ms": 45,
1165
1307
  "total_issues": 12,
1166
1308
  "entry_points": {
@@ -1185,9 +1327,9 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1185
1327
  "stale_suppressions": 0
1186
1328
  },
1187
1329
  "unused_files": [{ "path": "src/old.ts" }],
1188
- "unused_exports": [{ "path": "src/utils.ts", "name": "unusedFn", "line": 42, "actions": [{"type": "remove-export", "auto_fixable": true, "description": "Remove the `export` keyword from the declaration"}, {"type": "suppress-line", "auto_fixable": false, "description": "Suppress with an inline comment above the line", "comment": "// fallow-ignore-next-line unused-export"}] }],
1330
+ "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"}] }],
1189
1331
  "unused_types": [{ "path": "src/types.ts", "name": "OldType", "line": 10 }],
1190
- "unused_dependencies": [{ "name": "lodash", "line": 5 }],
1332
+ "unused_dependencies": [{ "name": "lodash", "line": 5, "used_in_workspaces": ["packages/web"] }],
1191
1333
  "unused_dev_dependencies": [{ "name": "jest", "line": 8 }],
1192
1334
  "unused_enum_members": [{ "path": "src/enums.ts", "enum_name": "Status", "member": "Archived", "line": 5 }],
1193
1335
  "unused_class_members": [{ "path": "src/service.ts", "class_name": "Service", "member": "oldMethod", "line": 20 }],
@@ -1203,13 +1345,15 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1203
1345
  }
1204
1346
  ```
1205
1347
 
1348
+ 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.
1349
+
1206
1350
  #### `actions` Array
1207
1351
 
1208
1352
  Every issue in `dead-code` JSON output includes an `actions` array with structured fix suggestions. Each action has:
1209
1353
 
1210
1354
  | Field | Type | Required | Description |
1211
1355
  |-------|------|----------|-------------|
1212
- | `type` | string | yes | Action type in kebab-case (14 types, e.g., `remove-export`, `remove-file`, `remove-dependency`, `suppress-line`, `add-to-config`) |
1356
+ | `type` | string | yes | Action type in kebab-case (for example `remove-export`, `remove-file`, `remove-dependency`, `move-dependency`, `suppress-line`, `add-to-config`) |
1213
1357
  | `auto_fixable` | bool | yes | `true` if `fallow fix` handles this action automatically |
1214
1358
  | `description` | string | yes | Human-readable description of the action |
1215
1359
  | `comment` | string | no | Suppression comment text (on `suppress-line` actions) |
@@ -1228,7 +1372,7 @@ Example:
1228
1372
  {
1229
1373
  "type": "remove-export",
1230
1374
  "auto_fixable": true,
1231
- "description": "Remove the `export` keyword from the declaration"
1375
+ "description": "Remove the unused export from the public API"
1232
1376
  },
1233
1377
  {
1234
1378
  "type": "suppress-line",
@@ -1263,6 +1407,25 @@ Dependency issues use `add-to-config` with `config_key` and `value`:
1263
1407
  }
1264
1408
  ```
1265
1409
 
1410
+ 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.
1411
+
1412
+ #### Health `actions` array (CRAP findings)
1413
+
1414
+ 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.
1415
+
1416
+ | Condition | Primary action |
1417
+ |-----------|----------------|
1418
+ | `cyclomatic >= maxCrap` (coverage cannot remediate, regardless of tier) | `refactor-function` |
1419
+ | `cyclomatic < maxCrap` and `coverage_tier=none` | `add-tests` ("start from scratch") |
1420
+ | `cyclomatic < maxCrap` and `coverage_tier=partial` or `high` | `increase-coverage` ("targeted branch coverage") |
1421
+ | Cyclomatic/cognitive triggered (no CRAP) | `refactor-function` |
1422
+
1423
+ The `coverage_tier` field is `"none"` (file not test-reachable / Istanbul 0%), `"partial"` (Istanbul `(0, 70)` / estimated 40%), or `"high"` (Istanbul `>= 70` / estimated 85%).
1424
+
1425
+ When CRAP-only with cyclomatic count within 5 of `maxCyclomatic` AND cognitive at or above `maxCognitive / 2`, a secondary `refactor-function` is appended. 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.
1426
+
1427
+ 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.
1428
+
1266
1429
  #### `baseline_deltas` Object
1267
1430
 
1268
1431
  When `--baseline` is used in combined output, the JSON includes a `baseline_deltas` object showing per-category changes since the baseline:
@@ -1284,7 +1447,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
1284
1447
  ```json
1285
1448
  {
1286
1449
  "schema_version": 3,
1287
- "version": "2.47.0",
1450
+ "version": "2.69.0",
1288
1451
  "elapsed_ms": 82,
1289
1452
  "total_clones": 15,
1290
1453
  "total_lines_duplicated": 230,
@@ -1328,7 +1491,7 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1328
1491
  {
1329
1492
  "check": {
1330
1493
  "schema_version": 3,
1331
- "version": "2.47.0",
1494
+ "version": "2.69.0",
1332
1495
  "elapsed_ms": 45,
1333
1496
  "total_issues": 12,
1334
1497
  "unused_files": [],
@@ -1350,7 +1513,7 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1350
1513
  },
1351
1514
  "dupes": {
1352
1515
  "schema_version": 3,
1353
- "version": "2.47.0",
1516
+ "version": "2.69.0",
1354
1517
  "elapsed_ms": 82,
1355
1518
  "total_clones": 15,
1356
1519
  "total_lines_duplicated": 230,
@@ -1359,7 +1522,7 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1359
1522
  },
1360
1523
  "health": {
1361
1524
  "schema_version": 3,
1362
- "version": "2.47.0",
1525
+ "version": "2.69.0",
1363
1526
  "elapsed_ms": 32,
1364
1527
  "summary": {},
1365
1528
  "findings": [],
@@ -1399,6 +1562,10 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1399
1562
  // Dependencies to ignore
1400
1563
  "ignoreDependencies": ["autoprefixer"],
1401
1564
 
1565
+ // Suppress unused-export findings when the symbol is referenced inside its
1566
+ // declaring file (knip parity). Boolean or { type, interface } object form.
1567
+ "ignoreExportsUsedInFile": true,
1568
+
1402
1569
  // Per-issue-type severity
1403
1570
  "rules": {
1404
1571
  "unused-files": "error",
@@ -1432,6 +1599,7 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1432
1599
  "minTokens": 50,
1433
1600
  "minLines": 5,
1434
1601
  "threshold": 0,
1602
+ "ignoreDefaults": true,
1435
1603
  "skipLocal": false,
1436
1604
  "ignorePatterns": ["**/*.generated.ts"]
1437
1605
  },
@@ -1453,8 +1621,8 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1453
1621
  // These files are treated as always-used and never flagged as unused.
1454
1622
  "dynamicallyLoaded": ["plugins/**/*.ts", "locales/**/*.json"],
1455
1623
 
1456
- // Inherit from base config (relative paths, npm: packages, or https:// URLs)
1457
- "extends": ["./base-config.json", "npm:@my-org/fallow-config", "https://example.com/shared-config.json"],
1624
+ // Inherit from base config (prefer local paths or trusted npm packages)
1625
+ "extends": ["./base-config.json", "npm:@my-org/fallow-config"],
1458
1626
 
1459
1627
  // Custom external plugins
1460
1628
  "plugins": ["tools/plugins/"],
@@ -1476,6 +1644,7 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1476
1644
  entry = ["src/index.ts", "scripts/*.ts"]
1477
1645
  ignorePatterns = ["**/*.generated.ts"]
1478
1646
  ignoreDependencies = ["autoprefixer"]
1647
+ ignoreExportsUsedInFile = true
1479
1648
  production = false
1480
1649
  publicPackages = ["@myorg/shared-lib", "@myorg/utils"]
1481
1650
  dynamicallyLoaded = ["plugins/**/*.ts", "locales/**/*.json"]
@@ -1489,6 +1658,7 @@ unused-types = "off"
1489
1658
  mode = "mild"
1490
1659
  minTokens = 50
1491
1660
  minLines = 5
1661
+ ignoreDefaults = true
1492
1662
 
1493
1663
  [[overrides]]
1494
1664
  files = ["*.test.ts"]
@@ -23,7 +23,7 @@ Always preview with `--dry-run` before applying. This is a destructive operation
23
23
 
24
24
  ## Don't Create Config Unless Needed
25
25
 
26
- Fallow works with zero configuration for most projects thanks to 90 auto-detecting framework plugins. Creating an unnecessary config file can mask issues or override detection behavior.
26
+ Fallow works with zero configuration for most projects thanks to 94 auto-detecting framework plugins. Creating an unnecessary config file can mask issues or override detection behavior.
27
27
 
28
28
  ```bash
29
29
  # WRONG: creating config for a standard Next.js project
@@ -429,6 +429,26 @@ Fallow marks `static/style.css` and `static/app.js` as reachable. Root-relative
429
429
 
430
430
  ---
431
431
 
432
+ ## GraphQL `#import` Documents Are Tracked
433
+
434
+ GraphQL `.graphql` and `.gql` files can keep nearby fragment documents reachable with relative `#import` comments. Fallow tracks `./` and `../` specifiers, including extensionless imports that resolve through `.graphql` and `.gql`; package-style specifiers are ignored.
435
+
436
+ ```graphql
437
+ # src/query.graphql
438
+
439
+ #import "./fragments/user-fields"
440
+
441
+ query CurrentUser {
442
+ currentUser {
443
+ ...UserFields
444
+ }
445
+ }
446
+ ```
447
+
448
+ Fallow marks `src/fragments/user-fields.graphql` or `src/fragments/user-fields.gql` as reachable when either file exists. A typo in the relative path is reported as an unresolved import instead of silently dropping the edge.
449
+
450
+ ---
451
+
432
452
  ## Library Packages: Use `publicPackages` Instead of Visibility Tags
433
453
 
434
454
  In monorepos, shared library packages have exported APIs consumed by external consumers not visible to fallow. Instead of annotating every export with `/** @public */` (or `@internal`, `@beta`, `@alpha`), use the `publicPackages` config to mark entire workspace packages as public libraries. Exports and exported enum/class members from these packages are excluded from unused API detection.
@@ -129,6 +129,31 @@ Parse the JSON to list specific files and exports that became unused.
129
129
 
130
130
  Computes a health score (0-100 with letter grade) in combined mode and enables the health delta header in PR comments.
131
131
 
132
+ ### GitHub Actions: Severity-Aware PR Quality Gate (Audit)
133
+
134
+ ```yaml
135
+ - uses: fallow-rs/fallow@v2
136
+ with:
137
+ command: audit
138
+ gate: new-only # default; fails only on findings introduced by this PR
139
+ fail-on-issues: true
140
+ ```
141
+
142
+ Runs `fallow audit` to combine dead-code + complexity + duplication scoped to changed files. The gate respects rule severity from `.fallowrc.json`, so `unused-exports: warn` projects do not fail when a PR touches a file with pre-existing warn-tier findings. Use `gate: all` to fail on every finding in changed files.
143
+
144
+ The action exposes `outputs.verdict` (`pass`/`warn`/`fail`) and `outputs.gate` for downstream conditionals; `outputs.issues` holds the introduced count under `gate: new-only` and the total count under `gate: all`.
145
+
146
+ ```yaml
147
+ - uses: fallow-rs/fallow@v2
148
+ id: fallow
149
+ with:
150
+ command: audit
151
+
152
+ - name: Block release on regression
153
+ if: steps.fallow.outputs.verdict == 'fail'
154
+ run: exit 1
155
+ ```
156
+
132
157
  ### GitHub Actions: Inline PR Annotations (No Advanced Security)
133
158
 
134
159
  The official action supports inline PR annotations via GitHub workflow commands. This does not require Advanced Security (unlike SARIF upload) and works on any GitHub plan.
@@ -398,6 +423,7 @@ Creates `.fallowrc.json` with mapped settings:
398
423
  - knip `rules`/`exclude`/`include` → fallow `rules` (error/warn/off)
399
424
  - knip `ignore` → fallow `ignorePatterns`
400
425
  - knip `ignoreDependencies` → fallow `ignoreDependencies`
426
+ - knip `ignoreExportsUsedInFile` → fallow `ignoreExportsUsedInFile` (boolean and `{ type, interface }` object form both supported; fallow groups type aliases and interfaces under one issue, so the two type-kind fields behave identically)
401
427
  - Unmappable fields generate warnings with suggestions
402
428
 
403
429
  ### Step 3: Compare results
@@ -595,7 +621,7 @@ Focus on findings that are BOTH dead code and duplicated:
595
621
 
596
622
  ## Custom Plugin Setup
597
623
 
598
- For frameworks not covered by the 90 built-in plugins.
624
+ For frameworks not covered by the 94 built-in plugins.
599
625
 
600
626
  ### Option 1: Inline framework config
601
627
 
@@ -693,13 +719,13 @@ Because Claude receives stderr as tool feedback on a blocked `PreToolUse` call,
693
719
  Install it automatically:
694
720
 
695
721
  ```bash
696
- fallow setup-hooks
722
+ fallow hooks install --target agent
697
723
  ```
698
724
 
699
725
  Remove it later with:
700
726
 
701
727
  ```bash
702
- fallow setup-hooks --uninstall
728
+ fallow hooks uninstall --target agent
703
729
  ```
704
730
 
705
731
  Manual files:
@@ -727,79 +753,14 @@ Manual files:
727
753
 
728
754
  ### `.claude/hooks/fallow-gate.sh`
729
755
 
730
- ```bash
731
- #!/usr/bin/env bash
732
- set -euo pipefail
733
-
734
- # Generated by fallow setup-hooks.
735
- # Requires bash and jq. On Windows run via git-bash or WSL.
736
- # Blocks Claude Code git commit and git push when fallow audit returns verdict fail.
737
- # Runtime errors fail open with a single stderr notice so skips stay visible.
738
-
739
- if ! command -v jq >/dev/null 2>&1; then
740
- echo "fallow-gate: jq not on PATH, skipping audit." >&2
741
- exit 0
742
- fi
756
+ Prefer `fallow hooks install --target agent` to install this file. The script is written and maintained by fallow itself; the canonical source is [`crates/cli/src/setup_hooks/fallow-gate.sh`](https://github.com/fallow-rs/fallow/blob/main/crates/cli/src/setup_hooks/fallow-gate.sh).
743
757
 
744
- INPUT="$(cat)"
745
- CMD="$(jq -r '.tool_input.command // empty' <<<"$INPUT")"
746
-
747
- if ! printf '%s\n' "$CMD" | grep -Eq '(^|[[:space:];|&()])git[[:space:]]+(commit|push)([[:space:]]|$)'; then
748
- exit 0
749
- fi
750
-
751
- if command -v fallow >/dev/null 2>&1; then
752
- RUNNER=(fallow)
753
- elif command -v npx >/dev/null 2>&1 && VER="$(npx --no-install fallow --version 2>/dev/null || true)" && [[ "$VER" == fallow* ]]; then
754
- RUNNER=(npx --no-install fallow)
755
- else
756
- echo "fallow-gate: fallow binary not found (tried PATH and npx --no-install), skipping audit." >&2
757
- exit 0
758
- fi
759
-
760
- TMP_JSON="$(mktemp)"
761
- TMP_ERR="$(mktemp)"
762
- cleanup() {
763
- rm -f "$TMP_JSON" "$TMP_ERR"
764
- }
765
- trap cleanup EXIT
766
-
767
- if "${RUNNER[@]}" audit --format json --quiet --explain >"$TMP_JSON" 2>"$TMP_ERR"; then
768
- STATUS=0
769
- else
770
- STATUS=$?
771
- fi
772
-
773
- VERDICT="$(jq -r '.verdict // empty' <"$TMP_JSON" 2>/dev/null || true)"
774
- IS_ERROR="$(jq -r '.error // false' <"$TMP_JSON" 2>/dev/null || echo false)"
775
-
776
- if [ "$VERDICT" = "fail" ]; then
777
- cat "$TMP_JSON" >&2
778
- exit 2
779
- fi
780
-
781
- if [ "$STATUS" -eq 2 ] || [ "$IS_ERROR" = "true" ]; then
782
- MSG="$(jq -r '.message // empty' <"$TMP_JSON" 2>/dev/null || true)"
783
- if [ -n "$MSG" ]; then
784
- echo "fallow-gate: fallow audit runtime error ($MSG), skipping." >&2
785
- else
786
- echo "fallow-gate: fallow audit runtime error, skipping." >&2
787
- fi
788
- exit 0
789
- fi
790
-
791
- if [ "$STATUS" -ne 0 ]; then
792
- ERR_LINE="$(sed -n '1p' "$TMP_ERR" 2>/dev/null || true)"
793
- if [ -n "$ERR_LINE" ]; then
794
- echo "fallow-gate: fallow audit exited $STATUS ($ERR_LINE), skipping." >&2
795
- else
796
- echo "fallow-gate: fallow audit exited $STATUS, skipping." >&2
797
- fi
798
- exit 0
799
- fi
800
-
801
- exit 0
802
- ```
758
+ Behavior you can rely on:
759
+ - Runs only when the intercepted command matches `git commit` or `git push`; otherwise exits 0.
760
+ - Resolves `fallow` from PATH first, then `npx --no-install fallow` as a fallback. Skips with a stderr notice if neither is available or if `jq` is missing.
761
+ - Enforces a version floor via `FALLOW_GATE_MIN_VERSION` (default `2.46.0`). Binaries below the floor are blocked with an upgrade hint. Set the env var to the empty string to disable the check.
762
+ - Runs `fallow audit --format json --quiet --explain` and, on verdict=`fail`, writes the full JSON envelope to stderr preceded by `fallow-gate: blocked by fallow <version> at <binary>` so the responsible binary is always identifiable.
763
+ - On runtime error (`{"error": true, ...}`) or unexpected non-zero exit, fails open with a one-line stderr notice; warn verdicts pass through silently.
803
764
 
804
765
  Codex fallback (add to repo root `AGENTS.md`):
805
766
 
@@ -812,13 +773,13 @@ Keep `fallow audit` in CI alongside this local gate. The hook only runs for Clau
812
773
  ### Remove the hook
813
774
 
814
775
  ```bash
815
- fallow setup-hooks --uninstall
776
+ fallow hooks uninstall --target agent
816
777
  ```
817
778
 
818
779
  Removes the fallow-gate handler from `.claude/settings.json` (preserving any other handlers in the same matcher group), deletes `.claude/hooks/fallow-gate.sh` if it still carries the `# Generated by fallow setup-hooks.` marker, and strips the managed block from `AGENTS.md`. Idempotent: a second run reports `unchanged` / `not present` and exits 0.
819
780
 
820
781
  Use `--force` to remove a hook script that the user has edited (the marker is no longer present). Use `--dry-run` to preview without touching files.
821
782
 
822
- ### Distinguish from `fallow init --hooks`
783
+ ### Distinguish from `fallow hooks install --target git`
823
784
 
824
- `fallow init --hooks` is a different command: it scaffolds a shell-level Git pre-commit hook under `.git/hooks/` that runs `fallow` on changed files. That is the *human* enforcement path. `fallow setup-hooks` is the *agent* enforcement path, targeting `.claude/` and `AGENTS.md`. Both can live in the same repo: git hooks catch human commits, the setup-hooks gate catches agent commits.
785
+ `fallow hooks install --target git` is a different target: it scaffolds a shell-level Git pre-commit hook under `.git/hooks/` that runs `fallow` on changed files. That is the *human* enforcement path. `fallow hooks install --target agent` is the *agent* enforcement path, targeting `.claude/` and `AGENTS.md`. Both can live in the same repo: git hooks catch human commits, the agent gate catches agent commits.