fallow 2.84.0 → 2.86.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.
@@ -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
+ - [`security`: Security Candidate Detection](#security-security-candidate-detection)
18
19
  - [`explain`: Rule Explanation](#explain-rule-explanation)
19
20
  - [`schema`: CLI Introspection](#schema-cli-introspection)
20
21
  - [`config-schema`: Config JSON Schema](#config-schema-config-json-schema)
@@ -39,6 +40,7 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
39
40
  |------|------|---------|-------------|
40
41
  | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
41
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 |
42
44
  | `--changed-since` | string | — | Only analyze files changed since a git ref (e.g., `main`, `HEAD~3`) |
43
45
  | `--production` | bool | `false` | Exclude test/dev files, only start/build scripts (applies to every analysis) |
44
46
  | `--production-dead-code` | bool | `false` | Per-analysis production mode for dead-code. Bare combined runs and `fallow audit` only. |
@@ -70,6 +72,7 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
70
72
  | `--unlisted-deps` | Unlisted dependencies |
71
73
  | `--duplicate-exports` | Duplicate exports |
72
74
  | `--circular-deps` | Circular dependencies |
75
+ | `--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). |
73
76
  | `--boundary-violations` | Boundary violations (imports crossing architecture zone boundaries) |
74
77
  | `--stale-suppressions` | Stale suppression comments or `@expected-unused` JSDoc tags |
75
78
  | `--unused-catalog-entries` | Unused pnpm catalog entries |
@@ -153,7 +156,7 @@ By default, `fallow dupes` skips generated framework output matching `**/.next/*
153
156
  | `--cross-language` | bool | `false` | Strip type annotations for TS↔JS matching |
154
157
  | `--ignore-imports` | bool | `false` | Exclude import declarations from clone detection |
155
158
  | `--explain-skipped` | bool | `false` | Human/markdown only: show per-pattern counts for files skipped by default duplicates ignores |
156
- | `--trace` | `FILE:LINE` | — | Trace all clones at a specific location |
159
+ | `--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 |
157
160
  | `--changed-since` | string | — | Only report duplication in files changed since a git ref |
158
161
  | `--baseline` | path | — | Compare against baseline |
159
162
  | `--save-baseline` | path | — | Save results as baseline |
@@ -185,6 +188,9 @@ fallow dupes --format json --quiet --skip-local --threshold 5
185
188
  # Trace clones at a specific location
186
189
  fallow dupes --format json --quiet --trace src/utils.ts:42
187
190
 
191
+ # Deep-dive a clone group by its dup:<id> fingerprint (from the listing or JSON)
192
+ fallow dupes --format json --quiet --trace dup:7f3a2c1e
193
+
188
194
  # Only check duplication in changed files
189
195
  fallow dupes --format json --quiet --changed-since main
190
196
 
@@ -218,6 +224,29 @@ Auto-removes unused exports, dependencies, enum members, and pnpm catalog entrie
218
224
  - 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.
219
225
  - 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.
220
226
 
227
+ ### On-disk drift protection
228
+
229
+ `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.
230
+
231
+ ### Low-confidence export removals
232
+
233
+ 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:
234
+
235
+ - **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.
236
+ - **Files with an unresolved import.** The file itself imports something fallow could not resolve, so its local usage graph is incomplete.
237
+
238
+ 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.
239
+
240
+ ### File encoding contract
241
+
242
+ `fallow fix` is UTF-8 only. Two encoding shapes that previously caused silent corruption are handled explicitly (issue #475):
243
+
244
+ - **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.
245
+
246
+ - **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.
247
+
248
+ **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.
249
+
221
250
  ### Examples
222
251
 
223
252
  ```bash
@@ -242,6 +271,7 @@ Inspect discovered files, entry points, detected frameworks, and architecture bo
242
271
  | `--entry-points` | bool | List detected entry points |
243
272
  | `--plugins` | bool | List active framework plugins |
244
273
  | `--boundaries` | bool | Show architecture boundary zones, rules, per-zone file counts, and `logical_groups[]` for `autoDiscover` parents |
274
+ | `--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. |
245
275
  | `--format` | `human\|json` | Output format |
246
276
  | `--quiet` | bool | Suppress progress bars |
247
277
 
@@ -252,8 +282,12 @@ fallow list --files --format json --quiet
252
282
  fallow list --entry-points --format json --quiet
253
283
  fallow list --plugins --format json --quiet
254
284
  fallow list --boundaries --format json --quiet
285
+ fallow list --workspaces --format json --quiet
286
+ fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
255
287
  ```
256
288
 
289
+ 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.
290
+
257
291
  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`.
258
292
 
259
293
  ---
@@ -333,15 +367,17 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
333
367
  | `--top` | number | — | Only show the top N most complex functions (and file scores/hotspots/targets) |
334
368
  | `--sort` | `cyclomatic\|cognitive\|lines\|severity` | `cyclomatic` | Sort order for complexity findings |
335
369
  | `--complexity` | bool | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. |
336
- | `--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. |
370
+ | `--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. |
337
371
  | `--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. |
338
372
  | `--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. |
339
373
  | `--effort` | `low\|medium\|high` | — | Filter refactoring targets by effort level. Implies `--targets`. |
340
374
  | `--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. |
341
- | `--min-score` | number | — | Fail if health score is below this threshold (exit code 1). Implies `--score`. CI quality gate. |
375
+ | `--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`. |
376
+ | `--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). |
377
+ | `--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`. |
342
378
  | `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). |
343
379
  | `--min-commits` | number | `3` | Minimum number of commits for a file to be included in hotspot ranking. |
344
- | `--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. |
380
+ | `--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. |
345
381
  | `--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`. |
346
382
  | `--changed-since` | string | — | Only analyze files changed since a git ref |
347
383
  | `--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. |
@@ -353,17 +389,24 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
353
389
  | `--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`). |
354
390
  | `--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`. |
355
391
  | `--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. |
356
- | `--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. |
392
+ | `--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. |
357
393
  | `--min-invocations-hot` | number | `100` | Invocation threshold for hot-path classification. Takes effect only when `--runtime-coverage` is set. |
358
394
  | `--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`. |
359
395
  | `--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%). |
360
396
 
361
397
  ### Exit Codes
362
398
 
363
- | Code | Meaning |
364
- |------|---------|
365
- | 0 | No functions exceed thresholds (and score above `--min-score` if set) |
366
- | 1 | Functions exceed thresholds, or score below `--min-score` |
399
+ 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).
400
+
401
+ | Invocation | Exit 0 when | Exit 1 when |
402
+ |------------|-------------|-------------|
403
+ | `fallow health` (no gate flag) | no function exceeds a threshold | any function exceeds a threshold |
404
+ | `--min-score N` | score >= N (findings informational) | score < N |
405
+ | `--min-severity LEVEL` | no finding at or above LEVEL | any finding at or above LEVEL |
406
+ | `--min-score N --min-severity LEVEL` | score >= N AND no finding >= LEVEL | score < N OR a finding >= LEVEL |
407
+ | `--report-only` | always | never |
408
+
409
+ `--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`.
367
410
 
368
411
  ### Examples
369
412
 
@@ -386,10 +429,10 @@ fallow health --format json --quiet --sort cognitive
386
429
  # Custom thresholds
387
430
  fallow health --format json --quiet --max-cyclomatic 15 --max-cognitive 10
388
431
 
389
- # Per-file maintainability index
432
+ # Per-file health scores
390
433
  fallow health --format json --quiet --file-scores
391
434
 
392
- # Worst 20 files by maintainability
435
+ # Top 20 files by triage concern
393
436
  fallow health --format json --quiet --file-scores --top 20
394
437
 
395
438
  # Only analyze files changed since main
@@ -440,8 +483,9 @@ fallow health --format json --quiet --trend
440
483
 
441
484
  ```json
442
485
  {
443
- "schema_version": 3,
444
- "version": "2.75.0",
486
+ "kind": "health",
487
+ "schema_version": 7,
488
+ "version": "2.86.0",
445
489
  "elapsed_ms": 32,
446
490
  "summary": {
447
491
  "files_analyzed": 482,
@@ -489,7 +533,8 @@ With `--file-scores`, the JSON output also includes `file_scores` array and `sum
489
533
  "summary": {
490
534
  "files_scored": 482,
491
535
  "average_maintainability": 88.5,
492
- "coverage_model": "static_estimated"
536
+ "coverage_model": "static_estimated",
537
+ "coverage_source_consistency": "uniform"
493
538
  },
494
539
  "file_scores": [
495
540
  {
@@ -510,7 +555,9 @@ With `--file-scores`, the JSON output also includes `file_scores` array and `sum
510
555
  }
511
556
  ```
512
557
 
513
- 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"`).
558
+ 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.
559
+
560
+ 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`.
514
561
 
515
562
  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.
516
563
 
@@ -752,7 +799,7 @@ Audits changed files for dead code, complexity, and duplication. Returns a verdi
752
799
  | `--production-health` | bool | false | Per-analysis production mode for the health sub-analysis only |
753
800
  | `--production-dupes` | bool | false | Per-analysis production mode for the duplication sub-analysis only |
754
801
  | `-w, --workspace` | string | — | Scope to one or more workspaces. Comma-separated, globs, `!` negation. |
755
- | `--explain` | bool | false | Include metric definitions in JSON output |
802
+ | `--explain` | bool | false | JSON: include metric definitions in `_meta`. Human: print a `Description:` line under each section header. |
756
803
  | `--ci` | bool | false | Equivalent to `--format sarif --fail-on-issues --quiet` |
757
804
  | `--fail-on-issues` | bool | false | Exit with code 1 if issues are found |
758
805
  | `--sarif-file` | path | — | Write SARIF output to a file alongside primary format |
@@ -826,8 +873,9 @@ fallow audit \
826
873
 
827
874
  ```json
828
875
  {
829
- "schema_version": 3,
830
- "version": "2.75.0",
876
+ "kind": "audit",
877
+ "schema_version": 7,
878
+ "version": "2.86.0",
831
879
  "command": "audit",
832
880
  "verdict": "fail",
833
881
  "changed_files_count": 12,
@@ -872,7 +920,7 @@ Audit creates a temporary git worktree to compare against the base ref. When the
872
920
 
873
921
  ## `flags`: Feature Flag Detection
874
922
 
875
- Detects feature flag patterns in the codebase. Identifies environment variable flags (`process.env.FEATURE_*`), SDK calls (LaunchDarkly, Statsig, Unleash, GrowthBook), and config object patterns (opt-in). Reports flag locations, detection confidence, and cross-references with dead code findings.
923
+ 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.
876
924
 
877
925
  ### Flags
878
926
 
@@ -899,8 +947,8 @@ fallow flags --format json --quiet --workspace my-package
899
947
 
900
948
  ```json
901
949
  {
902
- "schema_version": 3,
903
- "version": "2.75.0",
950
+ "schema_version": 7,
951
+ "version": "2.86.0",
904
952
  "elapsed_ms": 116,
905
953
  "feature_flags": [],
906
954
  "total_flags": 0
@@ -909,6 +957,50 @@ fallow flags --format json --quiet --workspace my-package
909
957
 
910
958
  ---
911
959
 
960
+ ## `security`: Security Candidate Detection
961
+
962
+ 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.
963
+
964
+ 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.
965
+
966
+ ### Flags
967
+
968
+ | Flag | Type | Default | Description |
969
+ |------|------|---------|-------------|
970
+ | `--format` | `human\|json\|sarif` | `human` | Output format |
971
+ | `--quiet` | bool | `false` | Suppress progress output |
972
+ | `--summary` | bool | `false` | Show a compact human summary |
973
+ | `--ci` | bool | `false` | Equivalent to `--format sarif --fail-on-issues --quiet` |
974
+ | `--fail-on-issues` | bool | `false` | Exit 1 when candidates are found |
975
+ | `--sarif-file` | path | none | Write SARIF in addition to the primary output |
976
+ | `--changed-since` | git ref | none | Scope to candidates whose client anchor or trace hops touch changed files |
977
+ | `--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 |
978
+ | `--workspace` | string | none | Scope to selected workspace packages |
979
+ | `--changed-workspaces` | git ref | none | Scope to workspaces changed since a git ref |
980
+
981
+ ### Examples
982
+
983
+ ```bash
984
+ fallow security --format json --quiet
985
+ fallow security --ci --sarif-file fallow-security.sarif
986
+ git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
987
+ ```
988
+
989
+ ### JSON Output Structure
990
+
991
+ ```json
992
+ {
993
+ "kind": "security",
994
+ "schema_version": "1",
995
+ "security_findings": [],
996
+ "unresolved_edge_files": 0
997
+ }
998
+ ```
999
+
1000
+ Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, and `actions`. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak`.
1001
+
1002
+ ---
1003
+
912
1004
  ## `explain`: Rule Explanation
913
1005
 
914
1006
  Print rule rationale, examples, fix guidance, and docs URL for one issue type without running analysis.
@@ -1033,17 +1125,55 @@ On HTTP error from `api.fallow.cloud`, fallow parses the `{error, message, code}
1033
1125
 
1034
1126
  Unknown codes fall back to the backend's `message` field, or the raw body.
1035
1127
 
1128
+ ### Clock skew
1129
+
1130
+ 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.
1131
+
1132
+ | Env var | Default | Effect |
1133
+ |---------|---------|--------|
1134
+ | `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. |
1135
+
1136
+ Common non-user causes: CI containers without NTP, machines with a dead BIOS battery, drifted laptop clocks after long sleep.
1137
+
1036
1138
  ### Exit Codes
1037
1139
 
1038
1140
  | Code | Meaning |
1039
1141
  |------|---------|
1040
1142
  | `0` | Valid license (or trial/refresh succeeded) |
1041
1143
  | `2` | Bad invocation (missing email for `--trial`, unreadable file) |
1042
- | `3` | License missing, hard-fail expired, or malformed JWT |
1144
+ | `3` | License missing, hard-fail expired, malformed JWT, or clock skew exceeds tolerance |
1043
1145
  | `7` | Network failure or non-success HTTP status from `api.fallow.cloud` |
1044
1146
 
1045
1147
  ---
1046
1148
 
1149
+ ## `telemetry`: Opt-in Product Telemetry
1150
+
1151
+ 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.
1152
+
1153
+ ```bash
1154
+ fallow telemetry status # effective state, source, and config path
1155
+ fallow telemetry enable # opt in (user action only; agents must not run this)
1156
+ fallow telemetry disable # opt out
1157
+ fallow telemetry inspect --example # print an example payload + field purposes
1158
+ ```
1159
+
1160
+ Inspect the exact payload a real command would send, without sending it:
1161
+
1162
+ ```bash
1163
+ FALLOW_TELEMETRY=inspect fallow audit --format json --quiet
1164
+ ```
1165
+
1166
+ The inspected payload prints to stderr; stdout (including `--format json`) is untouched.
1167
+
1168
+ ### Behavior
1169
+
1170
+ - **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.
1171
+ - **CI is off** unless `FALLOW_TELEMETRY` is explicitly set in that CI environment; a local `enable` never turns on org CI telemetry.
1172
+ - **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.
1173
+ - **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.
1174
+
1175
+ ---
1176
+
1047
1177
  ## `coverage`: Production-Coverage Workflow
1048
1178
 
1049
1179
  Helper subcommand for runtime coverage setup, focused analysis, and cloud inventory upload. Three subcommands today:
@@ -1125,11 +1255,14 @@ Only plain JS/TS/JSX/TSX sources are walked. Declaration files (`*.d.ts`, `*.d.m
1125
1255
  - `FALLOW_COV_BIN` — explicit override for the sidecar binary (for `setup`). Wins over all other discovery paths. Must point to an existing file.
1126
1256
  - `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.
1127
1257
  - `FALLOW_API_URL` — base URL for cloud calls. Overridden by `--api-endpoint`.
1258
+ - `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.
1128
1259
 
1129
1260
  ### `coverage upload-source-maps` flags
1130
1261
 
1131
1262
  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.
1132
1263
 
1264
+ 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.
1265
+
1133
1266
  | Flag | Type | Default | Description |
1134
1267
  |------|------|---------|-------------|
1135
1268
  | `--dir <PATH>` | path | `dist` | Directory scanned recursively. |
@@ -1212,7 +1345,8 @@ Available on all commands:
1212
1345
  | `--performance` | bool | Show pipeline timing breakdown |
1213
1346
  | `-w, --workspace` | string | Scope to one or more workspaces (comma-separated, globs, `!` negation) |
1214
1347
  | `--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. |
1215
- | `--explain` | bool | Include metric definitions in JSON output (`_meta` object). Always on for MCP |
1348
+ | `--explain` | bool | JSON: include metric definitions in `_meta`. Human: print a `Description:` line under each section header. Always on for MCP. |
1349
+ | `--legacy-envelope` | bool | Emit the previous typed JSON root envelope without top-level `kind` |
1216
1350
  | `--only` | string | Run only specific analyses (e.g., `--only dead-code,dupes`). Values: `dead-code` (alias: `check`), `dupes`, `health` |
1217
1351
  | `--skip` | string | Skip specific analyses (e.g., `--skip health`). Values: `dead-code` (alias: `check`), `dupes`, `health` |
1218
1352
  | `--ci` | bool | CI mode: `--format sarif --fail-on-issues --quiet` |
@@ -1235,15 +1369,27 @@ Available on all commands:
1235
1369
  | `FALLOW_BIN` | Path to fallow binary (used by the MCP server). |
1236
1370
  | `FALLOW_TIMEOUT_SECS` | MCP server subprocess timeout in seconds (default: `120`). Increase for very large codebases. |
1237
1371
  | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
1372
+ | `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. |
1373
+ | `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. |
1238
1374
  | `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
1239
1375
  | `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
1240
1376
  | `FALLOW_CHANGED_SINCE` | GitLab CI: git ref for incremental analysis. Auto-detected in MR pipelines. |
1241
1377
  | `FALLOW_COMMENT` | GitLab CI: set to `true` to post MR summary comments. |
1242
1378
  | `FALLOW_REVIEW` | GitLab CI: set to `true` to post inline code review comments on MR diffs. |
1379
+ | `FALLOW_REVIEW_GUIDANCE` | Add collapsed "What to do" guidance blocks to `review-github` / `review-gitlab` inline comments. |
1380
+ | `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. |
1243
1381
  | `FALLOW_SCORE` | GitLab CI: set to `true` to compute health score in combined mode. Enables health delta header in MR comments. |
1244
1382
  | `FALLOW_TREND` | GitLab CI: set to `true` to compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. |
1245
1383
  | `FALLOW_EXTRA_ARGS` | GitLab CI: additional CLI flags passed through to fallow. |
1246
1384
  | `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. |
1385
+ | `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. |
1386
+ | `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). |
1387
+ | `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. |
1388
+ | `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. |
1389
+ | `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`. |
1390
+ | `FALLOW_TELEMETRY_DEBUG` | Forces inspect mode; outranks `FALLOW_TELEMETRY`. |
1391
+ | `DO_NOT_TRACK` | Honored as a top-precedence telemetry kill switch (consoledonottrack.com convention). |
1392
+ | `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. |
1247
1393
  | `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). |
1248
1394
 
1249
1395
  Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid passing flags on every invocation.
@@ -1269,6 +1415,8 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
1269
1415
 
1270
1416
  `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).
1271
1417
 
1418
+ 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.
1419
+
1272
1420
  ### Flags
1273
1421
 
1274
1422
  | Flag | Type | Description |
@@ -1301,6 +1449,8 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1301
1449
  | `FALLOW_CHANGED_SINCE` | auto | Git ref for incremental analysis. Auto-detected in MR pipelines (`origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`) |
1302
1450
  | `FALLOW_COMMENT` | `false` | Post a summary comment on the MR with findings |
1303
1451
  | `FALLOW_REVIEW` | `false` | Post inline code review comments on MR diff lines where issues were found |
1452
+ | `FALLOW_REVIEW_GUIDANCE` | `false` | Add collapsed "What to do" guidance blocks to inline review comments |
1453
+ | `FALLOW_SUMMARY_SCOPE` | `all` | Sticky summary scope: `all` keeps project-level findings outside the diff; `diff` applies the diff filter to those findings too |
1304
1454
  | `FALLOW_SCORE` | `false` | Compute health score (0-100 with letter grade) in combined mode. Enables the health delta header in MR comments |
1305
1455
  | `FALLOW_TREND` | `false` | Compare current health metrics against saved snapshot. Implies `FALLOW_SCORE`. Shows per-metric deltas |
1306
1456
  | `FALLOW_EXTRA_ARGS` | — | Additional CLI flags passed through to fallow |
@@ -1318,8 +1468,9 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1318
1468
 
1319
1469
  ```json
1320
1470
  {
1321
- "schema_version": 3,
1322
- "version": "2.75.0",
1471
+ "kind": "dead-code",
1472
+ "schema_version": 7,
1473
+ "version": "2.86.0",
1323
1474
  "elapsed_ms": 45,
1324
1475
  "total_issues": 12,
1325
1476
  "entry_points": {
@@ -1340,6 +1491,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1340
1491
  "type_only_dependencies": 0,
1341
1492
  "test_only_dependencies": 0,
1342
1493
  "circular_dependencies": 0,
1494
+ "re_export_cycles": 0,
1343
1495
  "boundary_violations": 0,
1344
1496
  "stale_suppressions": 0
1345
1497
  },
@@ -1354,6 +1506,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1354
1506
  "unlisted_dependencies": [{ "name": "chalk", "imported_from": [{ "path": "src/cli.ts", "line": 1, "col": 0 }] }],
1355
1507
  "duplicate_exports": [{ "name": "Config", "locations": ["src/config.ts:5", "src/types.ts:12"] }],
1356
1508
  "circular_dependencies": [{ "cycle": ["src/a.ts", "src/b.ts", "src/a.ts"], "line": 3, "col": 0, "is_cross_package": false }],
1509
+ "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" }] }],
1357
1510
  "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 }],
1358
1511
  "unused_optional_dependencies": [{ "name": "fsevents" }],
1359
1512
  "type_only_dependencies": [{ "name": "zod", "used_in": ["src/schema.ts"], "line": 12 }],
@@ -1449,9 +1602,9 @@ Health findings (`fallow health` JSON output) include an `actions` array. Primar
1449
1602
 
1450
1603
  The `coverage_tier` field is `"none"` (file not test-reachable / Istanbul 0%), `"partial"` (Istanbul `(0, 70)` / estimated 40%), or `"high"` (Istanbul `>= 70` / estimated 85%).
1451
1604
 
1452
- 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). Synthetic `<template>` findings on Angular `.html` templates use the `estimated_component_inherited` source and ship 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 foo.component.ts)` after the CRAP score on those rows. 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 tier 2 follow-up; when it lands, `coverage_source` will gain a `"measured_aot_source_map"` variant.
1605
+ 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.
1453
1606
 
1454
- 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.
1607
+ 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.
1455
1608
 
1456
1609
  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.
1457
1610
 
@@ -1475,8 +1628,9 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
1475
1628
 
1476
1629
  ```json
1477
1630
  {
1478
- "schema_version": 3,
1479
- "version": "2.75.0",
1631
+ "kind": "dupes",
1632
+ "schema_version": 7,
1633
+ "version": "2.86.0",
1480
1634
  "elapsed_ms": 82,
1481
1635
  "total_clones": 15,
1482
1636
  "total_lines_duplicated": 230,
@@ -1518,9 +1672,13 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1518
1672
 
1519
1673
  ```json
1520
1674
  {
1675
+ "kind": "combined",
1676
+ "schema_version": 7,
1677
+ "version": "2.86.0",
1678
+ "elapsed_ms": 159,
1521
1679
  "check": {
1522
- "schema_version": 3,
1523
- "version": "2.75.0",
1680
+ "schema_version": 7,
1681
+ "version": "2.86.0",
1524
1682
  "elapsed_ms": 45,
1525
1683
  "total_issues": 12,
1526
1684
  "unused_files": [],
@@ -1534,6 +1692,7 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1534
1692
  "unlisted_dependencies": [],
1535
1693
  "duplicate_exports": [],
1536
1694
  "circular_dependencies": [],
1695
+ "re_export_cycles": [],
1537
1696
  "boundary_violations": [],
1538
1697
  "unused_optional_dependencies": [],
1539
1698
  "type_only_dependencies": [],
@@ -1541,18 +1700,12 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1541
1700
  "stale_suppressions": []
1542
1701
  },
1543
1702
  "dupes": {
1544
- "schema_version": 3,
1545
- "version": "2.75.0",
1546
- "elapsed_ms": 82,
1547
1703
  "total_clones": 15,
1548
1704
  "total_lines_duplicated": 230,
1549
1705
  "duplication_percentage": 4.2,
1550
1706
  "clone_groups": []
1551
1707
  },
1552
1708
  "health": {
1553
- "schema_version": 3,
1554
- "version": "2.75.0",
1555
- "elapsed_ms": 32,
1556
1709
  "summary": {},
1557
1710
  "findings": [],
1558
1711
  "vital_signs": {}
@@ -1652,6 +1805,13 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1652
1805
  // ]
1653
1806
  },
1654
1807
 
1808
+ // Resolve framework convention auto-imports (Nuxt components) as graph edges.
1809
+ // Edges for `<Card001 />`-style template tags are always synthesized; setting
1810
+ // this to true also drops the Nuxt component entry patterns so an
1811
+ // unreferenced component is reported as unused-file. Kept conservative: a
1812
+ // `components:` key in nuxt.config keeps the entry patterns. Default false.
1813
+ "autoImports": false,
1814
+
1655
1815
  // Production mode
1656
1816
  "production": false,
1657
1817
 
@@ -1724,4 +1884,4 @@ preset = "bulletproof"
1724
1884
 
1725
1885
  ### Valid Issue Type Tokens
1726
1886
 
1727
- `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`, `boundary-violation`, `unused-optional-dependency`, `type-only-dependency`, `test-only-dependency`, `code-duplication`
1887
+ `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`
@@ -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 94 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 114 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
@@ -154,6 +154,8 @@ The action exposes `outputs.verdict` (`pass`/`warn`/`fail`) and `outputs.gate` f
154
154
  run: exit 1
155
155
  ```
156
156
 
157
+ Three additional outputs surface silent failures in the action's PR comment / review steps. `outputs.changed-files-unavailable` (`true`/`false`, default `false`) signals that the analyze step could not enumerate PR-changed files (transient GitHub API failure, expired token, missing permissions), so analysis ran against the full codebase. `outputs.post-skipped-reason` (`none`/`pagination_failure`) signals the Post review comments step aborted to avoid duplicate threads. `outputs.dedup-lookup-failed` (`true`/`false`) signals a dedup lookup failed on either the Post PR comment or Post review comments step. All three are always emitted regardless of which failure path was taken, so downstream `if:` gates can match positively without absent-vs-false ambiguity. Gate on these to detect degraded posting state and re-run the action.
158
+
157
159
  ### GitHub Actions: Inline PR Annotations (No Advanced Security)
158
160
 
159
161
  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.
@@ -226,9 +228,10 @@ fallow:
226
228
  extends: .fallow
227
229
  variables:
228
230
  FALLOW_COMMENT: "true"
231
+ FALLOW_SUMMARY_SCOPE: "diff"
229
232
  ```
230
233
 
231
- Posts a summary comment on the MR with issue counts and findings. In MR pipelines, `--changed-since` is auto-detected from `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`, so only issues from changed files are reported. Requires `GITLAB_TOKEN` CI/CD variable (project access token with `api` scope); `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API.
234
+ Posts a summary comment on the MR with issue counts and findings. In MR pipelines, `--changed-since` is auto-detected from `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`, so only issues from changed files are reported. `FALLOW_SUMMARY_SCOPE: "diff"` also hides project-level dependency/catalog/override findings whose anchor line is outside the diff. Requires `GITLAB_TOKEN` CI/CD variable (project access token with `api` scope); `CI_JOB_TOKEN` is read-only for MR notes in the official GitLab API.
232
235
 
233
236
  ### GitLab CI: With Inline Code Review Comments
234
237
 
@@ -240,9 +243,10 @@ fallow:
240
243
  extends: .fallow
241
244
  variables:
242
245
  FALLOW_REVIEW: "true"
246
+ FALLOW_REVIEW_GUIDANCE: "true"
243
247
  ```
244
248
 
245
- Posts inline review comments directly on the MR diff lines where issues were found. This gives developers precise feedback without leaving the code review flow. Can be combined with `FALLOW_COMMENT: "true"` for both a summary and inline comments. Requires `GITLAB_TOKEN`.
249
+ Posts inline review comments directly on the MR diff lines where issues were found. `FALLOW_REVIEW_GUIDANCE: "true"` adds collapsed "What to do" guidance blocks to each inline finding. This gives developers precise feedback without leaving the code review flow. Can be combined with `FALLOW_COMMENT: "true"` for both a summary and inline comments. Requires `GITLAB_TOKEN`.
246
250
 
247
251
  ### GitLab CI: Combined MR Comments + Review
248
252
 
@@ -254,11 +258,13 @@ fallow:
254
258
  extends: .fallow
255
259
  variables:
256
260
  FALLOW_COMMENT: "true"
261
+ FALLOW_SUMMARY_SCOPE: "diff"
257
262
  FALLOW_REVIEW: "true"
263
+ FALLOW_REVIEW_GUIDANCE: "true"
258
264
  FALLOW_FAIL_ON_ISSUES: "true"
259
265
  ```
260
266
 
261
- Posts both a summary comment and inline review comments on the MR. The template auto-detects the package manager (npm/pnpm/yarn) from lockfiles, so review comments show the correct commands for the project (e.g., `pnpm remove` instead of `npm uninstall`).
267
+ Posts both a summary comment and inline review comments on the MR. `FALLOW_SUMMARY_SCOPE: "diff"` only affects the sticky summary; inline review comments remain anchored to diff lines. The template auto-detects the package manager (npm/pnpm/yarn) from lockfiles, so review comments show the correct commands for the project (e.g., `pnpm remove` instead of `npm uninstall`).
262
268
 
263
269
  ### GitLab CI: With Health Score and Trend
264
270
 
@@ -621,7 +627,7 @@ Focus on findings that are BOTH dead code and duplicated:
621
627
 
622
628
  ## Custom Plugin Setup
623
629
 
624
- For frameworks not covered by the 95 built-in plugins.
630
+ For frameworks not covered by the 114 built-in plugins.
625
631
 
626
632
  ### Option 1: Inline framework config
627
633