fallow 3.31.0 → 3.32.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.
@@ -52,7 +52,7 @@ Every fallow command with its purpose and key flags. The table is regenerated fr
52
52
  | `doctor` | Diagnose project readiness without analysis or mutation | |
53
53
  | `similar-code` | Find semantically similar functions with a pinned local model (opt-in) | `--threshold`, `--min-lines`, `--top`, `--file` |
54
54
  | `inspect` | Compose one evidence bundle for a file or exported symbol | `--file <path>`, `--symbol <file>:<export>` |
55
- | `trace` | Trace a symbol's call chain (best-effort, syntactic; OFF the ranked path) | `symbol`, `--callers`, `--callees`, `--depth`, `--path`, `--eager-only` |
55
+ | `trace` | Trace a symbol's call chain, or with `--path` the shortest import path between two modules (best-effort, syntactic; OFF the ranked path) | `symbol`, `--path <FROM> <TO>`, `--eager-only`, `--callers`, `--callees`, `--depth` |
56
56
  | `trace-error` | Resolve a runtime stack trace's frames to the definitions they name (best-effort, syntactic; OFF the ranked path) | `trace_file` |
57
57
  | `fix` | Auto-remove unused exports/deps | `--dry-run`, `--yes` (required in non-TTY) |
58
58
  | `init` | Generate config file, AGENTS.md agent guide, or pre-commit hook | `--toml`, `--agents`, `--hooks`, `--branch` |
@@ -134,6 +134,7 @@ Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#
134
134
  | `--unprovided-injects` | inject() / getContext() reads a key that no provide() / setContext() supplies |
135
135
  | `--unrendered-components` | A Vue / Svelte component is reachable through a barrel but rendered nowhere |
136
136
  | `--unused-component-props` | A Vue defineProps prop or React component prop is referenced nowhere in its own component |
137
+ | `--absent-component-props` | Known reachable callers omit an optional prop consumed inside its component |
137
138
  | `--unused-component-emits` | A Vue <script setup> defineEmits event is emitted nowhere in its own component |
138
139
  | `--unused-component-inputs` | An Angular @Input() / signal input() / model() is read nowhere in its own component (class body or template); needs `@angular/core` dep |
139
140
  | `--unused-component-outputs` | An Angular @Output() / signal output() is emitted (.emit()) nowhere in its own component; needs `@angular/core` dep |
@@ -155,6 +156,9 @@ Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#
155
156
  | `--unused-dependency-overrides` | Unused package-manager dependency overrides |
156
157
  | `--misconfigured-dependency-overrides` | Misconfigured package-manager dependency overrides |
157
158
  <!-- generated:flags:dead-code-filters:end -->
159
+
160
+ `--deprecated-exports-in-use` is an opt-in migration sweep (default `off`). Each finding has the exact `consumer_count` and a sample of up to 10 consumers. `fallow dead-code --trace FILE:EXPORT` lists all of them. Enable it with this flag or with `deprecated-exports-in-use: "warn"` or `"error"` in [`rules`](#configuration-file-format).
161
+
158
162
  ### Examples
159
163
 
160
164
  ```bash
@@ -230,12 +234,17 @@ By default, `fallow dupes` skips generated framework output matching `**/.next/*
230
234
  | `--cross-language` | `bool` | `false` | Strip type annotations for TS↔JS matching |
231
235
  | `--ignore-imports` | `bool` | `false` | Exclude module wiring from clone detection |
232
236
  | `--no-ignore-imports` | `bool` | `false` | Count module wiring as clone candidates (opt out of the default exclusion) |
237
+ | `--ignore-symlinks` | `bool` | `false` | Omit clone instances whose path is a symlink, or lies under a symlinked directory. A clone group with fewer than two remaining instances is not reported. Without this flag, JSON output marks these instances with `is_symlink: true` |
238
+ | `--no-ignore-symlinks` | `bool` | `false` | Report symlinked clone instances (opt out of a config `duplicates.ignoreSymlinks: true`) |
233
239
  | `--top` | `string` | - | Show only the N highest-ranked clone groups. Ranking multiplies token count and occurrences, then adds a capped spread boost for distant files or same-file locations. `clone_families[]` narrows with the groups. Summary stats reflect the scoped project; `clone_groups_shown` / `clone_groups_omitted` and `clone_families_shown` / `clone_families_omitted` report both splits. Refused with exit code 2 alongside `--group-by`, which reports per-bucket stats over every clone group in a bucket that a global top-N truncation would contradict. |
234
240
  | `--no-fragments` | `bool` | `false` | Omit the verbatim source text from each clone instance in `--format json`. The file and line/column range still address the same code, and this is most of the payload on a duplicated codebase |
235
241
  | `--trace` | `string` | - | 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 |
236
242
 
237
243
  Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags), [`--changed-since`](#global-flags), [`--baseline`](#global-flags), [`--save-baseline`](#global-flags), [`--workspace`](#global-flags), [`--changed-workspaces`](#global-flags), [`--group-by`](#global-flags), [`--explain-skipped`](#global-flags).
238
244
  <!-- generated:flags:dupes:end -->
245
+
246
+ With `--group-by`, each clone group goes to its **largest owner** (the most instances; an 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` (each group has `primary_owner` and a per-instance `owner`), and `clone_families`. SARIF results carry `properties.group`, and CodeClimate issues carry a top-level `group` field. Compact and markdown output fall back to ungrouped output with a stderr note.
247
+
239
248
  ### Detection Modes
240
249
 
241
250
  | Mode | Behavior |
@@ -291,6 +300,9 @@ Auto-removes unused exports, dependencies, enum members, and pnpm catalog entrie
291
300
 
292
301
  Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags).
293
302
  <!-- generated:flags:fix:end -->
303
+
304
+ `--force` is an alias for `--yes`.
305
+
294
306
  ### What gets fixed
295
307
 
296
308
  - Unused exports (removes the `export` keyword; whole-enum block when every member is unused)
@@ -369,7 +381,7 @@ The `--entry-weight` JSON output carries `entry_weight.entries[]`, one row per r
369
381
 
370
382
  To gate eager growth in CI, save a baseline file on the main branch with `fallow list --entry-weight --save-regression-baseline <PATH>`. A later run with `--regression-baseline <PATH>` adds `entry_weight.regression`: per-entry `baseline_eager_bytes`, `current_eager_bytes`, `new_eager_packages` and `exceeded`. The comparison is report-only until you add `--fail-on-regression`; then an entry that grew more than `--tolerance` (bytes, or a percentage such as `5%`) exits 1. A new entry never fails the gate.
371
383
 
372
- 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`, `malformed-pnpm-workspace-yaml`, `skipped-large-file`, `skipped-minified-file`, `skipped-source-dotdir`, `source-read-failure`, `bun-lockb-override-resolution-skipped`) with a typed payload (`error`, `pattern`, or none), and a `path` that is project-root-relative with forward slashes on every envelope that carries the array. The same `workspace_diagnostics[]` array is also surfaced on the `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes, at the top level of the bare combined `fallow --format json` envelope, on `fallow audit --format json` under `dead_code`, and on the `audit-brief` envelope shared by `fallow review --format json` and `fallow audit --brief --format json`, also under `dead_code` (omitted when empty). The combined carrier is the envelope root, not a section, so `--skip check`, `--only health`, and `--only dupes` all still report what their analyses recorded. The combined root is the union of what every analysis in the run recorded, deduplicated on the whole `kind` (typed payload included) plus `path`, so two overlapping globs still report the same package-less directory once per `pattern` (a declared glob's no-op `./` prefix is normalised away, so one glob written `"./apps/**"` in `package.json` and `apps/**` in `pnpm-workspace.yaml` stays one entry): a combined run walks the project once per analysis, and a per-analysis `production` mode (`production: { deadCode, health, dupes }`, `--production-health`) can give those walks different file sets, so only the union reports what the run as a whole saw. Each analysis contributes the workspace-discovery list its own config load produced, the same list `fallow list --workspaces` reports, so the combined root can carry an `undeclared-workspace` or `glob-matched-no-package-json` entry that the standalone `dead-code`, `check`, `health`, and `dupes` envelopes, which read the process diagnostics registry instead, do not. `fallow audit --format json` and the `audit-brief` envelope are on the same broad side: they fold the dead-code analysis's own list into their `dead_code.workspace_diagnostics[]`, so they too report an `undeclared-workspace` entry the standalone envelopes miss. The CLI and the programmatic route (MCP code mode, NAPI, embedders) agree on everything an analysis records: both folds close with the same process-registry read, which covers what an analysis records after its section captured its list (a `source-read-failure`, or the analysis-stage kinds a health run's own dead-code precompute records) and skips `skipped-large-file`, `skipped-minified-file`, and `skipped-source-dotdir`, since those reach an envelope only from the walk that recorded them. The two analysis-stage kinds (`malformed-pnpm-workspace-yaml`, `bun-lockb-override-resolution-skipped`) are recorded by the dead-code analyze pass, so they only appear on runs that include it: `fallow dupes --format json` and `fallow --only dupes` report the workspace-discovery and source-discovery kinds alone. A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
384
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (for example `undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`, `malformed-pnpm-workspace-yaml`, `skipped-large-file`, `skipped-minified-file`, `skipped-source-dotdir`, `source-read-failure`, `npm-lock-override-resolution-skipped`) with a typed payload (`error`, `pattern`, or none), and a `path` that is project-root-relative with forward slashes on every envelope that carries the array. The same `workspace_diagnostics[]` array is also surfaced on the `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes, at the top level of the bare combined `fallow --format json` envelope, on `fallow audit --format json` under `dead_code`, and on the `audit-brief` envelope shared by `fallow review --format json` and `fallow audit --brief --format json`, also under `dead_code` (omitted when empty). The combined carrier is the envelope root, not a section, so `--skip check`, `--only health`, and `--only dupes` all still report what their analyses recorded. The combined root is the union of what every analysis in the run recorded, deduplicated on the whole `kind` (typed payload included) plus `path`, so two overlapping globs still report the same package-less directory once per `pattern` (a declared glob's no-op `./` prefix is normalised away, so one glob written `"./apps/**"` in `package.json` and `apps/**` in `pnpm-workspace.yaml` stays one entry): a combined run walks the project once per analysis, and a per-analysis `production` mode (`production: { deadCode, health, dupes }`, `--production-health`) can give those walks different file sets, so only the union reports what the run as a whole saw. Each analysis contributes the workspace-discovery list its own config load produced, the same list `fallow list --workspaces` reports, so the combined root can carry an `undeclared-workspace` or `glob-matched-no-package-json` entry that the standalone `dead-code`, `check`, `health`, and `dupes` envelopes, which read the process diagnostics registry instead, do not. `fallow audit --format json` and the `audit-brief` envelope are on the same broad side: they fold the dead-code analysis's own list into their `dead_code.workspace_diagnostics[]`, so they too report an `undeclared-workspace` entry the standalone envelopes miss. The CLI and the programmatic route (MCP code mode, NAPI, embedders) agree on everything an analysis records: both folds close with the same process-registry read, which covers what an analysis records after its section captured its list (a `source-read-failure`, or the analysis-stage kinds a health run's own dead-code precompute records) and skips `skipped-large-file`, `skipped-minified-file`, and `skipped-source-dotdir`, since those reach an envelope only from the walk that recorded them. The analysis-stage kinds (`malformed-pnpm-workspace-yaml`, `bun-lockb-override-resolution-skipped`, `bun-lock-override-resolution-skipped`, `pnpm-lock-override-resolution-skipped`, `npm-lock-override-resolution-skipped`, `bun-resolutions-shadowed-by-overrides`, `pnpm-workspace-overrides-ignored`, `boundaries-not-configured`, `rule-packs-not-configured`) are recorded by the dead-code analyze pass, so they only appear on runs that include it: `fallow dupes --format json` and `fallow --only dupes` report the workspace-discovery and source-discovery kinds alone. A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues. The dead-code result adds two more kinds, each with a `pattern` payload and `path: "."`: `ignore-dependencies-glob-unmatched` (an `ignoreDependencies` glob matched no declared dependency; omitted when the run reports no dependency finding, for example with `--unused-files` or `--file`) and `ignore-findings-pattern-unmatched` (an `ignoreFindings` pattern matched no finding). SARIF carries the same entries under `invocations[0].toolConfigurationNotifications[]` of the dead-code run. Markdown, `github-summary`, `pr-comment-github`, `pr-comment-gitlab` and the summary `body` of `review-github` and `review-gitlab` add an `Unmatched config patterns` section. Human, compact, CodeClimate and `github-annotations` print a stderr note. `fallow report --from` uses the same place as the live run of that format.
373
385
 
374
386
  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`.
375
387
 
@@ -412,26 +424,26 @@ fallow hooks uninstall --target git
412
424
  fallow hooks uninstall --target agent
413
425
  ```
414
426
 
415
- `hooks status` is read-only and reports `git`, `claude`, and `codex` surfaces. Each surface includes `installed`, `managed_block_present`, `user_edited`, and `path`; generated agent scripts also include `script_version` and `min_version_floor`. Use it before mutating setup so agents can distinguish fallow-managed artifacts from user-owned hooks or partial managed blocks.
427
+ `hooks status` is read-only and reports `git`, `claude`, `codex` (the `AGENTS.md` routing block), and `codex_gate` (the `.codex/hooks.json` gate) surfaces. Each surface includes `installed`, `managed_block_present`, `user_edited`, and `path`; generated agent scripts also include `script_version` and `min_version_floor`. Use it before mutating setup so agents can distinguish fallow-managed artifacts from user-owned hooks or partial managed blocks.
416
428
 
417
429
  ---
418
430
 
419
431
  ## `agent`: One-Pass Agent Onboarding
420
432
 
421
- Wires fallow into the coding-agent harnesses a project uses. `install` detects Claude Code, Codex, and Cursor from the project (`.claude/`, `CLAUDE.md`, `.mcp.json`, `.codex/`, `.cursor/`; `AGENTS.md` is not a signal because every harness and fallow itself write it), the home directory, and the session environment (`CLAUDECODE`, `CODEX_THREAD_ID`, `CURSOR_AGENT`), or takes `--harness`. When nothing is detected only harness-neutral files are written (`AGENTS.md` and `.agents/skills/fallow`).
433
+ Wires fallow into the coding-agent harnesses a project uses. `install` detects Claude Code, Codex, and Cursor from the project (`.claude/`, `CLAUDE.md`, `.mcp.json`, `.codex/`, `.cursor/`; `AGENTS.md` is not a signal because every harness and fallow itself write it), the home directory, and the session environment (`CLAUDECODE`, `CODEX_THREAD_ID`, `CURSOR_AGENT`), or takes `--harness`. When nothing is detected only harness-neutral files are written (`AGENTS.md`, `.agents/skills/fallow`, and `.agents/skills/fallow-setup`).
422
434
 
423
435
  Steps per harness:
424
436
 
425
437
  | Step | Claude Code | Codex | Cursor |
426
438
  |---|---|---|---|
427
439
  | `guide` | `AGENTS.md` task map; `CLAUDE.md` gains an `@AGENTS.md` import (created when absent, appended as a marked block otherwise) | `AGENTS.md` task map | `AGENTS.md` task map (Cursor reads it) |
428
- | `skill` | `.claude/skills/fallow/` | `.agents/skills/fallow/` | `.agents/skills/fallow/` |
440
+ | `skill` | `.claude/skills/fallow/` and `.claude/skills/fallow-setup/` | `.agents/skills/fallow/` and `.agents/skills/fallow-setup/` | `.agents/skills/fallow/` and `.agents/skills/fallow-setup/` |
429
441
  | `mcp` | `mcpServers.fallow` in `.mcp.json` (`--approve` also lists it in `.claude/settings.local.json`) | `[mcp_servers.fallow]` in `.codex/config.toml` (applies once the project is trusted; the `codex mcp add` next step works immediately) | `mcpServers.fallow` in `.cursor/mcp.json` |
430
- | `hooks` | `.claude/settings.json` PreToolUse gate plus `.claude/hooks/fallow-gate.sh` | marked gate block in `AGENTS.md` | skipped (`unsupported_harness`) |
442
+ | `hooks` | `.claude/settings.json` PreToolUse gate plus `.claude/hooks/fallow-gate.sh` | `.codex/hooks.json` PreToolUse gate plus `.codex/hooks/fallow-gate.sh` (Codex runs it after you trust it in `/hooks`), and a marked routing block in `AGENTS.md` | skipped (`unsupported_harness`) |
431
443
 
432
- The skill is a small pointer to `node_modules/fallow/skills/fallow` when that copy exists (so it never drifts from the installed binary); otherwise the tree embedded in the binary is written. The MCP command is probed before anything is written: `npx --no fallow-mcp` for an npm-installed project, `fallow-mcp` from `PATH`, or the running multicall binary; when none exists the step is `skipped` with `mcp_entry_unavailable` rather than writing a config that cannot start.
444
+ The step writes each released skill: `fallow` for analysis and `fallow-setup` for setting up code-quality tooling. Each skill is a small pointer to `node_modules/fallow/skills/<name>` when that copy exists (so it never drifts from the installed binary); otherwise the tree embedded in the binary is written. Each skill picks its source on its own, so an older npm package without `fallow-setup` still gets the embedded copy of that skill. The MCP command is probed before anything is written: `npx --no fallow-mcp` for an npm-installed project, `fallow-mcp` from `PATH`, or the running multicall binary; when none exists the step is `skipped` with `mcp_entry_unavailable` rather than writing a config that cannot start.
433
445
 
434
- Every file or block carries a `<!-- fallow:agent-install v1 ... -->` marker. Re-running is byte-stable. An existing skill named `fallow` without a marker is `refused` (`skill_name_taken`) unless `--force`. JSON and TOML cannot carry a marker, so a `fallow` MCP entry counts as fallow-managed only when its command is one fallow writes; any other entry is `refused` (`mcp_entry_foreign`) and never removed without `--force`. `--force` on an unparsable config file saves the old bytes as `<file>.fallow-bak` before rewriting. `uninstall` removes managed content, deletes a config file it emptied (and an emptied `.cursor/` or `.codex/` directory), and deletes `AGENTS.md` or `CLAUDE.md` only while the file still matches what fallow authored.
446
+ Every file or block carries a `<!-- fallow:agent-install v1 ... -->` marker. Re-running is byte-stable. An existing skill named `fallow` or `fallow-setup` without a marker is `refused` (`skill_name_taken`) unless `--force`; the other skill is still written. JSON and TOML cannot carry a marker, so a `fallow` MCP entry counts as fallow-managed only when its command is one fallow writes; any other entry is `refused` (`mcp_entry_foreign`) and never removed without `--force`. `--force` on an unparsable config file saves the old bytes as `<file>.fallow-bak` before rewriting. `uninstall` removes managed content, deletes a config file it emptied (and an emptied `.cursor/` or `.codex/` directory), and deletes `AGENTS.md` or `CLAUDE.md` only while the file still matches what fallow authored.
435
447
 
436
448
  ### Flags
437
449
 
@@ -442,7 +454,7 @@ Every file or block carries a `<!-- fallow:agent-install v1 ... -->` marker. Re-
442
454
  | `--dry-run` | `install`, `uninstall` | Print the plan without touching the filesystem |
443
455
  | `--force` | `install`, `uninstall` | Replace or remove skills, hook scripts, or config files fallow did not write |
444
456
  | `--approve` | `install` | Pre-approve the project MCP server for yourself in `.claude/settings.local.json`; refused when that file is tracked by git |
445
- | `--user` | `install`, `uninstall` | Skill and MCP config under `$HOME` (`~/.claude/skills`, `~/.agents/skills`, `~/.codex/config.toml`, `~/.cursor/mcp.json`); the guide step is skipped, and Claude Code prints the `claude mcp add --scope user` command instead of editing `~/.claude.json` |
457
+ | `--user` | `install`, `uninstall` | Skill, MCP config, and gate under `$HOME` (`~/.claude/skills`, `~/.agents/skills`, `~/.codex/config.toml`, `~/.cursor/mcp.json`, `~/.claude/hooks`, `~/.codex/hooks.json`); the guide step and the `AGENTS.md` routing block are skipped, and Claude Code prints the `claude mcp add --scope user` command instead of editing `~/.claude.json` |
446
458
  | `--gitignore-claude` | `install` | Append `.claude/` to `.gitignore` |
447
459
 
448
460
  Root: the git toplevel of the current directory unless `--root` is passed explicitly, so a run from a monorepo package still writes where the harnesses read. The chosen root is the first line of output and `root` in JSON.
@@ -455,7 +467,7 @@ Human output groups paths under "Shared with your team (commit these)" and "Loca
455
467
  {
456
468
  "kind": "agent-install",
457
469
  "schema_version": 1,
458
- "fallow_version": "3.31.0",
470
+ "fallow_version": "3.32.0",
459
471
  "root": "/abs/path",
460
472
  "mode": "install",
461
473
  "dry_run": false,
@@ -555,7 +567,8 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
555
567
  | `--min-commits` | `string` | - | Minimum number of commits for a file to be included in hotspot ranking. |
556
568
  | `--save-snapshot` | `string` | - | Save vital signs snapshot for trend tracking. Forces file-scores + hotspot computation. |
557
569
  | `--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`. |
558
- | `--coverage` | `string` | - | Path to coverage data for accurate per-function CRAP scores: an Istanbul map (`coverage-final.json`), a directory containing one, a raw V8 coverage directory (`NODE_V8_COVERAGE=<dir> node --test`), or a single V8 coverage JSON file. Transpiled V8 scripts (tsx, bundles) map back to their source files through the source map that Node records in the dump; a script that differs from the file on disk and has no source map keeps the estimate. Uses `CC^2 * (1-cov/100)^3 + CC` instead of static binary model. Relative paths resolve against `--root`. Falls back to `FALLOW_COVERAGE`, then `health.coverage`, then auto-detection. |
570
+ | `--trend-from` | `string` | - | Compare current metrics against this snapshot file instead of the newest file in `.fallow/snapshots/`. Use it to restore a baseline from external storage in CI. With --group-by, groups are compared by key when the snapshot holds the same grouping. Implies --trend |
571
+ | `--coverage` | `string` | - | Path to coverage data for accurate per-function CRAP scores: an Istanbul map (`coverage-final.json`), a directory containing one, a raw V8 coverage directory (`NODE_V8_COVERAGE=<dir> node --test`), or a single V8 coverage JSON file. Transpiled V8 scripts (tsx, bundles) map back to their source files through the source map that Node records in the dump; a script that differs from the file on disk and has no source map keeps the estimate. Uses `CC^2 * (1-cov/100)^3 + CC` instead of the default estimated coverage. Relative paths resolve against `--root`. Falls back to `FALLOW_COVERAGE`, then `health.coverage`, then auto-detection. |
559
572
  | `--coverage-root` | `string` | - | 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. Falls back to `FALLOW_COVERAGE_ROOT`, then `health.coverageRoot`. |
560
573
  | `--runtime-coverage` | `string` | - | 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. |
561
574
  | `--min-invocations-hot` | `string` | `100` | Invocation threshold for hot-path classification. Takes effect only when `--runtime-coverage` is set. |
@@ -564,6 +577,11 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
564
577
 
565
578
  Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags), [`--changed-since`](#global-flags), [`--churn-file`](#global-flags), [`--workspace`](#global-flags), [`--group-by`](#global-flags), [`--baseline`](#global-flags), [`--baseline-mode`](#global-flags), [`--save-baseline`](#global-flags), [`--production`](#global-flags), [`--no-production`](#global-flags), [`--explain`](#global-flags).
566
579
  <!-- generated:flags:health:end -->
580
+
581
+ With `--workspace`, vital signs, the health score, hotspots, file scores, findings, and `summary.files_analyzed` are all recomputed against the scoped subset.
582
+
583
+ With `--group-by`, JSON adds `grouped_by` plus a `groups` array. Each group has its own `vital_signs`, `health_score`, `findings`, `file_scores`, `hotspots`, `large_functions`, and `targets`, recomputed against the files of the group. The top-level metrics stay project-wide, so a consumer that ignores grouping still sees the project headline. Human output adds a per-group score / files / hot / p90 summary block (worst first when `--score` is set). Markdown adds a grouped health table. SARIF results carry `properties.group`, and CodeClimate issues carry a top-level `group` field. Compact and badge output fall back to ungrouped output with a stderr note.
584
+
567
585
  ### Exit Codes
568
586
 
569
587
  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).
@@ -659,7 +677,7 @@ fallow health --format json --quiet --trend
659
677
  {
660
678
  "kind": "health",
661
679
  "schema_version": 7,
662
- "version": "3.31.0",
680
+ "version": "3.32.0",
663
681
  "elapsed_ms": 32,
664
682
  "summary": {
665
683
  "files_analyzed": 482,
@@ -852,15 +870,19 @@ All `health` JSON output includes a `vital_signs` object with project-wide metri
852
870
  }
853
871
  ```
854
872
 
855
- 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.
873
+ 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).
874
+
875
+ Health score formula v3 retains the scale-invariant density and tail fields: `critical_complexity_pct`, `maintainability_low_pct`, `unused_deps_per_k_files`, `circular_deps_per_k_files`, and `functions_over_60_loc_per_k`. The hotspot penalty is `hotspot_count / max(ceil(total_files × 0.01), 1) × 10`, capped at 10. Only files with a hotspot score of at least 50 enter `hotspot_count`; an empty scope has no hotspot penalty. `hotspot_top_pct_count` remains a rank diagnostic and does not determine the penalty.
876
+
877
+ 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.
856
878
 
857
879
  With `--score`, the JSON output includes a `health_score` object:
858
880
 
859
881
  ```json
860
882
  {
861
883
  "health_score": {
862
- "formula_version": 2,
863
- "score": 76.9,
884
+ "formula_version": 3,
885
+ "score": 72.9,
864
886
  "grade": "B",
865
887
  "penalties": {
866
888
  "dead_files": 3.1,
@@ -878,7 +900,7 @@ With `--score`, the JSON output includes a `health_score` object:
878
900
  }
879
901
  ```
880
902
 
881
- 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).
903
+ Score is reproducible: `100 - sum(penalties) == score`. `formula_version` identifies the scoring formula; version 3 retains the density and tail metrics from version 2 and corrects the hotspot penalty to use the thresholded count. 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).
882
904
 
883
905
  ### Health Trend
884
906
 
@@ -890,15 +912,16 @@ With `--trend`, the JSON output includes a `health_trend` object comparing curre
890
912
  "compared_to": {
891
913
  "timestamp": "2026-03-25T14:30:00Z",
892
914
  "git_sha": "a1b2c3d",
893
- "score": 74.2,
894
- "grade": "B"
915
+ "score": 70.2,
916
+ "grade": "B",
917
+ "score_formula_version": 3
895
918
  },
896
919
  "metrics": [
897
920
  {
898
921
  "name": "score",
899
922
  "label": "Health Score",
900
- "previous": 74.2,
901
- "current": 76.9,
923
+ "previous": 70.2,
924
+ "current": 72.9,
902
925
  "delta": 2.7,
903
926
  "direction": "improving",
904
927
  "unit": ""
@@ -921,7 +944,7 @@ With `--trend`, the JSON output includes a `health_trend` object comparing curre
921
944
  }
922
945
  ```
923
946
 
924
- 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.
947
+ 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 and denominator. `--trend` requires at least one saved snapshot in `.fallow/snapshots/`. Current snapshots use schema version 12 and record `score_formula_version`. A score delta is emitted only when both scores exist and their formula versions are known and equal. An older snapshot without this metadata retains its historical score and grade, but has no score delta. Raw metric trends still compare across formula changes. The JSON baseline exposes the stored formula version when known; human, Markdown, GitHub and compact output explain an omitted score comparison.
925
948
 
926
949
  ### Vital Signs Snapshots
927
950
 
@@ -929,8 +952,9 @@ Metrics tracked: `score`, `dead_file_pct`, `dead_export_pct`, `avg_cyclomatic`,
929
952
 
930
953
  ```json
931
954
  {
932
- "snapshot_schema_version": 8,
955
+ "snapshot_schema_version": 12,
933
956
  "timestamp": "2025-12-01T10:30:00Z",
957
+ "score_formula_version": 3,
934
958
  "vital_signs": {
935
959
  "dead_file_pct": 3.2,
936
960
  "dead_export_pct": 8.1,
@@ -1062,7 +1086,7 @@ fallow audit \
1062
1086
  {
1063
1087
  "kind": "audit",
1064
1088
  "schema_version": 7,
1065
- "version": "3.31.0",
1089
+ "version": "3.32.0",
1066
1090
  "command": "audit",
1067
1091
  "verdict": "fail",
1068
1092
  "changed_files_count": 12,
@@ -1150,7 +1174,7 @@ fallow flags --format json --quiet --workspace my-package
1150
1174
  ```json
1151
1175
  {
1152
1176
  "schema_version": 7,
1153
- "version": "3.31.0",
1177
+ "version": "3.32.0",
1154
1178
  "elapsed_ms": 116,
1155
1179
  "feature_flags": [],
1156
1180
  "total_flags": 0
@@ -1251,7 +1275,7 @@ fallow security --gate newly-reachable --changed-since origin/main
1251
1275
  {
1252
1276
  "kind": "security",
1253
1277
  "schema_version": "4",
1254
- "version": "3.31.0",
1278
+ "version": "3.32.0",
1255
1279
  "elapsed_ms": 42,
1256
1280
  "config": {
1257
1281
  "rules": {
@@ -1280,7 +1304,7 @@ fallow security --gate newly-reachable --changed-since origin/main
1280
1304
  {
1281
1305
  "kind": "security",
1282
1306
  "schema_version": "4",
1283
- "version": "3.31.0",
1307
+ "version": "3.32.0",
1284
1308
  "elapsed_ms": 42,
1285
1309
  "config": {
1286
1310
  "rules": {
@@ -1471,7 +1495,7 @@ fallow explain fallow/code-duplication --format json --quiet
1471
1495
  "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.",
1472
1496
  "example": "export const formatPrice = ... exists in src/money.ts, but no module imports formatPrice.",
1473
1497
  "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.",
1474
- "docs": "https://docs.fallow.tools/explanations/dead-code#unused-exports"
1498
+ "docs": "https://fallow.tools/docs/explanations/dead-code/#unused-exports"
1475
1499
  }
1476
1500
  ```
1477
1501
 
@@ -1561,7 +1585,7 @@ fallow license deactivate
1561
1585
  |------------|---------|
1562
1586
  | `activate` | Install a JWT or start a 30-day trial. JWT input precedence: positional arg > `--from-file` > `--stdin`. |
1563
1587
  | `status` | Print tier, seats, features, days-until-expiry, and (when `refresh_after` has passed) a proactive refresh hint. |
1564
- | `refresh` | Fetch a fresh JWT using the currently stored one as identity proof. Exit 7 on network failure. |
1588
+ | `refresh` | Fetch a fresh JWT using the currently stored one as identity proof, falling back to a full-access API key when that token is missing or too stale. Exit 7 on network failure. |
1565
1589
  | `deactivate` | Remove the local license file. |
1566
1590
 
1567
1591
  ### `activate` flags
@@ -1573,6 +1597,19 @@ fallow license deactivate
1573
1597
  | `--from-file <PATH>` | path | Read a JWT from a file. |
1574
1598
  | `--stdin` | bool | Read a JWT from stdin. Conflicts with `--from-file` and positional JWT. |
1575
1599
 
1600
+ ### `refresh` flags
1601
+
1602
+ | Flag | Type | Description |
1603
+ |------|------|-------------|
1604
+ | `--api-key <KEY>` | string | Full-access API key used as the bearer when the stored license JWT is missing or the cloud reports it as `token_stale`. Precedence: this flag > `$FALLOW_API_KEY`. Prefer the environment variable on shared runners so the key stays out of argv. |
1605
+
1606
+ ### Credential order for `refresh`
1607
+
1608
+ 1. The stored license JWT (`FALLOW_LICENSE`, `FALLOW_LICENSE_PATH`, or `~/.fallow/license.jwt`).
1609
+ 2. A full-access API key, when the stored JWT is absent or the cloud answers `token_stale`.
1610
+
1611
+ A machine that has not run fallow for weeks holds a JWT the cloud refuses, and the trial endpoint rejects an organisation that already pays, so the API key is the recovery path. With neither credential available, `refresh` names that route instead of the trial flow.
1612
+
1576
1613
  ### Storage precedence
1577
1614
 
1578
1615
  1. `FALLOW_LICENSE` (env var holding the full JWT string)
@@ -1593,7 +1630,7 @@ On HTTP error from `api.fallow.cloud`, fallow parses the `{error, message, code}
1593
1630
 
1594
1631
  | Operation + code | CLI message |
1595
1632
  |------------------|-------------|
1596
- | `refresh` + `token_stale` | `your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr>` |
1633
+ | `refresh` + `token_stale` | your stored license is too stale to refresh: set `FALLOW_API_KEY` to a full-access key and run `fallow license refresh` again (generate one at `https://fallow.cloud/settings#api-keys`) |
1597
1634
  | `refresh` + `invalid_token` | `your stored license token is missing required claims. Reactivate with: fallow license activate --trial --email <addr>` |
1598
1635
  | `refresh` or `trial` + `unauthorized` | `authentication failed. Reactivate with: fallow license activate --trial --email <addr>` |
1599
1636
  | `trial` + `rate_limit_exceeded` | `trial creation is rate-limited to 5 per hour per IP. Wait an hour or retry from a different network (in CI, start the trial locally and set FALLOW_LICENSE on the runner).` |
@@ -1676,7 +1713,7 @@ The command does not enable tracking. Only the user may opt in with
1676
1713
 
1677
1714
  ## `coverage`: Production-Coverage Workflow
1678
1715
 
1679
- Helper subcommand for runtime coverage setup, focused analysis, and cloud inventory upload. Three subcommands today:
1716
+ Use the `coverage` subcommands for runtime setup, analysis, inventory upload, and scoped cloud reads:
1680
1717
 
1681
1718
  - `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`.
1682
1719
  - `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.
@@ -1730,13 +1767,14 @@ fallow coverage upload-source-maps --dry-run # print maps and fileNam
1730
1767
  | `--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. |
1731
1768
  | `--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. |
1732
1769
  | `--importance` | bool | false | Show the first-class importance section in human output. JSON always includes `runtime_coverage.importance` whenever runtime coverage analysis runs. |
1770
+ | `--debug-unmatched` | bool | false | Cloud mode only. List every cloud runtime function with no local counterpart on stderr, highest traffic first, instead of only counting them in the `cloud_functions_unmatched` warning. Stdout stays machine-readable. |
1733
1771
  | `--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. |
1734
1772
  | `--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`. |
1735
1773
  | `--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`. |
1736
1774
  | `--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%. |
1737
1775
  | `--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. |
1738
1776
 
1739
- 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.
1777
+ 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. Matching resolves a runtime file path onto the local tree by file name plus a segment-wise suffix comparison, so a containerized `/app/src/a.ts` reaches `src/a.ts`, and a function whose runtime name differs from the source name (an anonymous callback carrying its callee's name, an accessor keeping its `get` prefix) is matched on position within that file. Ambiguity is never guessed: two local files equally entitled to a runtime path, or two definitions opening on one line with no end line to separate them, stay unmatched. Pass `--debug-unmatched` to list what remains.
1740
1778
 
1741
1779
  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.
1742
1780
 
@@ -1866,6 +1904,7 @@ Available on all commands:
1866
1904
  | `-w, --workspace` | `string` | - | Scope to one or more workspaces (comma-separated, globs, `!` negation) |
1867
1905
  | `--changed-workspaces` | `string` | - | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF`. Mutually exclusive with `--workspace`. Missing ref is a hard error. |
1868
1906
  | `--group-by` | `owner\|directory\|package\|section` | - | Group output by CODEOWNERS ownership (`owner`), first path component (`directory`), workspace package (`package`, aliases: `workspace`, `pkg`), or GitLab CODEOWNERS `[Section]` headers (`section`, alias: `gl-section`). All output formats partition issues into labeled groups. `section` mode attaches an `owners` array to each group in JSON output |
1907
+ | `--group` | `string` | - | Keep only the matching groups of a `--group-by` health run. Accepts exact group keys, glob patterns, and `!`-prefixed negations. Values can be comma-separated or repeated. Project-level sections are not filtered. Supported by `fallow health` only |
1869
1908
  | `--performance` | `bool` | `false` | Show pipeline timing breakdown |
1870
1909
  | `--explain` | `bool` | `false` | JSON: include metric definitions in `_meta`. Human: print a `Description:` line under each section header. Always on for MCP. |
1871
1910
  | `--explain-skipped` | `bool` | `false` | Human/markdown only: show per-pattern counts for files skipped by the default duplicates ignores. `dupes` prints only that breakdown; on `check`, `dead-code`, `audit` and the default run the same flag reports source files the built-in discovery ignores removed |
@@ -1895,8 +1934,11 @@ Available on all commands:
1895
1934
  | `--dupes-cross-language` | `bool` | `false` | Enable cross-language duplicate detection in combined mode |
1896
1935
  | `--dupes-ignore-imports` | `bool` | `false` | Exclude module wiring from duplicate detection in combined mode |
1897
1936
  | `--dupes-no-ignore-imports` | `bool` | `false` | Count module wiring as clone candidates in combined mode (opt out of the default exclusion) |
1937
+ | `--dupes-ignore-symlinks` | `bool` | `false` | Omit clone instances whose path is a symlink, or lies under a symlinked directory, in combined mode |
1938
+ | `--dupes-no-ignore-symlinks` | `bool` | `false` | Report symlinked clone instances in combined mode (opt out of a config `duplicates.ignoreSymlinks: true`) |
1898
1939
  | `--score` | `bool` | `false` | Compute health score (0-100 with letter grade) in combined mode. Enables the health delta header in PR comments. JSON includes `health_score` object with `score`, `grade`, and `penalties` breakdown |
1899
1940
  | `--trend` | `bool` | `false` | Compare current health metrics against saved snapshot. Implies `--score`. Shows per-metric deltas with directional indicators. Requires at least one saved snapshot in `.fallow/snapshots/` |
1941
+ | `--trend-from` | `string` | - | Compare current health metrics against this snapshot file in combined mode. Implies --trend and --score |
1900
1942
  | `--save-snapshot` | `string` | - | Save vital signs snapshot for trend tracking. Default path: `.fallow/snapshots/<timestamp>.json`. Forces file-scores + hotspot computation |
1901
1943
  | `--coverage` | `string` | - | Path to Istanbul or raw V8 coverage data for exact CRAP scores in combined mode. Also settable via `FALLOW_COVERAGE` or `health.coverage` |
1902
1944
  | `--coverage-root` | `string` | - | Absolute prefix to strip from Istanbul file paths in combined mode. Also settable via `FALLOW_COVERAGE_ROOT` or `health.coverageRoot` |
@@ -1966,6 +2008,7 @@ These are global flags with behavior specific to bare `fallow` combined mode.
1966
2008
  | `FALLOW_AUDIT_BASE` | Pin the `fallow audit` comparison base when `--base` / `--changed-since` is unset (precedence: flag > env > auto-detect). Escape hatch for the agent gate and forks, e.g. `FALLOW_AUDIT_BASE=upstream/main`. When unset, audit auto-detects the `git merge-base` against the branch's upstream or the remote default. A malformed value exits 2. |
1967
2009
  | `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 log a warning and fall back to config / default. Each sweep also reclaims abandoned entries from other repo identities (deleted or moved repos, other git worktrees); entries whose recorded owner root still exists are left to that repo's own sweep and setting. |
1968
2010
  | `FALLOW_UPDATE_CHECK` | Set to `off`, `0`, `false`, `disabled`, or `no` to disable the human-TTY upgrade nudge and its background latest-version check. `DO_NOT_TRACK`, `FALLOW_TELEMETRY_DISABLED`, and CI also suppress it. |
2011
+ | `FALLOW_CLAUDE_CODE_HINT` | Set to `off`, `0`, `false`, `no`, or `disabled` to suppress the one-line Claude Code plugin hint. Fallow writes the hint to stderr only inside a Claude Code session, for human output without `--quiet`, outside CI, and when the project has no Fallow plugin or skill. |
1969
2012
  | `FALLOW_SUGGESTIONS` | Set to `off`, `0`, `false`, `no`, or `disabled` to suppress the top-level `next_steps[]` array of read-only follow-up commands in JSON output (and the human `Next:` line on bare `fallow`). Default on. Inherited by the MCP-spawned CLI, so it disables `next_steps` on MCP responses too. Useful for CI consumers that snapshot-diff raw `--format json`. |
1970
2013
  | `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
1971
2014
  | `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
@@ -2014,6 +2057,8 @@ Set `FALLOW_FORMAT=json` and `FALLOW_QUIET=1` in your agent environment to avoid
2014
2057
 
2015
2058
  Provider mutations are isolated per fingerprint. A failed mutation blocks only the remaining operations of that same fingerprint, which is retried whole on the next run, while every other stale fingerprint is still applied. (A preflight failure is different: preflight runs before any mutation, and a failure there abandons the whole plan because the state snapshot is untrustworthy.) 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. `fallow ci post-review` reports those same three fields for the reconcile pass it runs after posting new inline comments.
2016
2059
 
2060
+ Review comments end with a `<!-- fallow-fingerprint:v3: <fp> -->` marker. A dead-code fingerprint comes from the `finding_id`, so a line shift above the finding keeps the thread. When a comment had another fingerprint in an older release, the envelope comment carries that value as `legacy_fingerprint`. For one release, both commands match an open thread with the older `v2` marker through `legacy_fingerprint`: they do not post the finding again and do not resolve the thread as stale.
2061
+
2017
2062
  ### Flags
2018
2063
 
2019
2064
  | Flag | Type | Description |
@@ -2068,7 +2113,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
2068
2113
  {
2069
2114
  "kind": "dead-code",
2070
2115
  "schema_version": 7,
2071
- "version": "3.31.0",
2116
+ "version": "3.32.0",
2072
2117
  "elapsed_ms": 45,
2073
2118
  "total_issues": 12,
2074
2119
  "entry_points": {
@@ -2228,7 +2273,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
2228
2273
  {
2229
2274
  "kind": "dupes",
2230
2275
  "schema_version": 7,
2231
- "version": "3.31.0",
2276
+ "version": "3.32.0",
2232
2277
  "elapsed_ms": 82,
2233
2278
  "total_clones": 15,
2234
2279
  "total_lines_duplicated": 230,
@@ -2272,11 +2317,11 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
2272
2317
  {
2273
2318
  "kind": "combined",
2274
2319
  "schema_version": 7,
2275
- "version": "3.31.0",
2320
+ "version": "3.32.0",
2276
2321
  "elapsed_ms": 159,
2277
2322
  "check": {
2278
2323
  "schema_version": 7,
2279
- "version": "3.31.0",
2324
+ "version": "3.32.0",
2280
2325
  "elapsed_ms": 45,
2281
2326
  "total_issues": 12,
2282
2327
  "unused_files": [],
@@ -2339,8 +2384,8 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
2339
2384
  // Files to ignore (glob patterns)
2340
2385
  "ignorePatterns": ["**/*.generated.ts", "**/*.d.ts"],
2341
2386
 
2342
- // Dependencies to ignore
2343
- "ignoreDependencies": ["autoprefixer"],
2387
+ // Dependencies to ignore (exact names, or globs such as "@acme/*")
2388
+ "ignoreDependencies": ["autoprefixer", "@acme/*"],
2344
2389
 
2345
2390
  // Suppress unused-export findings when the symbol is referenced inside its
2346
2391
  // declaring file (knip parity). Boolean or { type, interface } object form.
@@ -2383,7 +2428,7 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
2383
2428
  "ignoreDefaults": true,
2384
2429
  "ignoredClones": ["dup:6f12ab34:2"],
2385
2430
  "skipLocal": false,
2386
- "ignorePatterns": ["**/*.generated.ts"]
2431
+ "ignore": ["**/*.generated.ts"]
2387
2432
  },
2388
2433
 
2389
2434
  // Extraction cache settings. FALLOW_CACHE_DIR overrides cache.dir.
@@ -55,16 +55,18 @@ fallow dead-code | grep "unused"
55
55
  fallow dead-code --format json --quiet
56
56
  ```
57
57
 
58
- The `--quiet` flag suppresses progress bars on stderr. Without it, stderr output may interfere with stdout parsing.
58
+ The `--quiet` flag suppresses progress bars on stderr. Keep stderr separate from stdout when parsing JSON.
59
59
 
60
60
  ---
61
61
 
62
- ## `--changed-since` Shows Only New Issues
62
+ <a id="--changed-since-shows-only-new-issues"></a>
63
63
 
64
- The `--changed-since` flag limits analysis to files modified since a git ref. It only reports issues in those files, not all issues in the project. Works with both `dead-code` and `dupes`.
64
+ ## `--changed-since` Scopes Findings to Changed Files
65
+
66
+ The `--changed-since` flag scopes findings to files modified since a git ref. It works with both `dead-code` and `dupes`. Existing findings in those files can remain; dead-code dependency findings remain project-wide. Use `fallow audit --gate new-only` to distinguish introduced findings from inherited ones.
65
67
 
66
68
  ```bash
67
- # This only shows issues in files changed since main
69
+ # File-scoped findings are limited to files changed since main
68
70
  fallow dead-code --format json --quiet --changed-since main
69
71
 
70
72
  # Same for duplication, only clone groups involving changed files
@@ -109,10 +111,10 @@ fallow dead-code --format json --quiet
109
111
 
110
112
  ## Syntactic Analysis: No TypeScript Compiler
111
113
 
112
- Fallow uses Oxc for pure syntactic analysis. It does not run the TypeScript compiler. This means:
114
+ Default analysis uses Oxc for syntactic references. The optional `--type-aware` mode adds TypeScript checker evidence. Syntactic analysis has these limits:
113
115
 
114
116
  - **Fully dynamic imports** (`import(variable)`) are not resolved. Only static strings, template literals with static prefixes, `import.meta.glob`, and `require.context` patterns
115
- - **Value-level type narrowing** is not performed. Fallow can't know that `if (x instanceof Foo)` means `Foo` is "used"
117
+ - **General type narrowing** is outside syntactic analysis. Fallow does recognize `if (x instanceof Foo)` guards and credits member calls on `x` as uses of `Foo` members
116
118
  - **Conditional exports** based on runtime values are not analyzed
117
119
  - **Function overload signatures are deduplicated**: TypeScript function overloads (multiple signatures for the same function name) are merged into a single export. They are not reported as separate unused exports
118
120
 
@@ -151,7 +153,7 @@ export * from './utils';
151
153
  import { helper } from './index'; // Resolves through the chain
152
154
  ```
153
155
 
154
- If an export IS flagged as unused despite being in a barrel file, it means no downstream consumer actually imports it. The barrel file re-exports it, but nobody uses it from there.
156
+ A re-export alone does not prove that an export is used. If Fallow reports an export from a barrel, trace its consumers before removal. Dynamic imports and external callers may be outside static analysis.
155
157
 
156
158
  ---
157
159
 
@@ -163,10 +165,10 @@ If an export IS flagged as unused despite being in a barrel file, it means no do
163
165
  | 1 | Error-severity issues found | Review findings |
164
166
  | 2 | Runtime error (`fix` without `--yes` in non-TTY, invalid config) | Fix config or add `--yes` |
165
167
 
166
- Exit code 1 is triggered by issues with `"error"` severity in the rules config. Without a rules section, all issue types default to `"error"`. Use the rules system to control which issues fail CI:
168
+ Error-severity findings can trigger exit code 1. Default severity varies by rule: some rules default to `"warn"` or `"off"`. Use the rules system to control which findings fail CI:
167
169
 
168
170
  ```jsonc
169
- // Only fail on unused files and deps, warn on everything else
171
+ // Warn on unused exports and types; other rules keep their defaults
170
172
  {
171
173
  "rules": {
172
174
  "unused-files": "error",
@@ -177,6 +179,8 @@ Exit code 1 is triggered by issues with `"error"` severity in the rules config.
177
179
  }
178
180
  ```
179
181
 
182
+ Exit code 1 always means that an enforced gate failed. A load warning or a workspace diagnostic (for example `node-modules-missing` or a tsconfig `extends` that does not resolve) never changes the exit code. The only exception is `source-parse-degraded` with `--fail-on-parse-error`. To find the gate, read `gate_outcomes` in the JSON output and look for an entry with `status: "fail"` and `enforced: true`. Under `--quiet` and in every machine format, fallow also prints one stderr line that names the failed gates, for example `[X] Exit code 1: gate health-findings (3 at or above error) failed.` On `health`, the `complexity-*` rules default to `error`, so each complexity finding fails the run. Set them to `warn` or pass `--report-only` to report without a failure.
183
+
180
184
  ---
181
185
 
182
186
  ## `--fail-on-issues` Promotes Warn to Error
@@ -215,7 +219,7 @@ Commit the baseline file to your repo. Update it periodically as you fix existin
215
219
 
216
220
  ## Duplication Modes Affect What's Detected
217
221
 
218
- The detection mode significantly affects results. Choose based on your needs:
222
+ Each detection mode normalizes different syntax. Choose the mode that fits the comparison:
219
223
 
220
224
  ```bash
221
225
  # strict: exact token match only
@@ -237,6 +241,10 @@ fallow dupes --format json --quiet --mode semantic
237
241
 
238
242
  `semantic` mode produces the most findings but may include false positives where similar structure is coincidental.
239
243
 
244
+ Use `--near` separately when you want function-level clones with small inserted,
245
+ removed, or changed regions. Exact detection still follows `--mode`; near
246
+ detection uses semantic shingles and reports a `similarity` value.
247
+
240
248
  ---
241
249
 
242
250
  ## Workspace Flag Scopes Output, Not Analysis
@@ -333,7 +341,7 @@ If you use utility decorators that DO NOT imply reflective use (Playwright's `@s
333
341
 
334
342
  Conservative semantics: a method carrying any decorator NOT in the list still gets skipped. So `@step` + `@Inject` on the same method stays treated as framework-managed. Matching rule: entries containing `.` (`"decorators.log"`) match the full dotted path; bare entries (`"step"` or `"decorators"`) match the leftmost segment, so a single bare `"decorators"` entry collapses an entire `@decorators.*` namespace. Both `"@step"` and `"step"` round-trip equivalently. Unmatched entries (a decorator name in the config that never appears in your codebase) surface as a one-time warning at end of run.
335
343
 
336
- The default empty list preserves today's skip-all behavior, so existing NestJS / Angular / TypeORM projects see no change.
344
+ With the default empty list every decorated method is treated as framework-managed, which is what NestJS, Angular, and TypeORM projects need.
337
345
 
338
346
  ### Angular `@Input()` / `@Output()` are still covered by the component rules
339
347
 
@@ -654,14 +662,14 @@ Both require a `GITLAB_TOKEN` CI/CD variable (project access token with `api` sc
654
662
  `fallow license refresh` and `fallow license activate --trial` can fail with a backend error. The CLI always appends the raw HTTP status and the backend error code after the human hint, so scripts can grep for the code without parsing prose:
655
663
 
656
664
  ```
657
- fallow license refresh: your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr> (HTTP 401, code token_stale)
665
+ fallow license refresh: your stored license is too stale to refresh: set FALLOW_API_KEY to a full-access key and run `fallow license refresh` again (generate one at https://fallow.cloud/settings#api-keys) (HTTP 401, code token_stale)
658
666
  ```
659
667
 
660
668
  Stable codes the CLI surfaces today:
661
669
 
662
670
  | Code | Operation | Meaning |
663
671
  |------|-----------|---------|
664
- | `token_stale` | `refresh` | Stored JWT is more than 45 days past its `exp`. Reactivate. |
672
+ | `token_stale` | `refresh` | Stored JWT is more than 45 days past its `exp`. Surfaced only when no full-access API key was available to retry with. |
665
673
  | `invalid_token` | `refresh` | Stored JWT is missing required claims (e.g. `sub`). Reactivate. |
666
674
  | `unauthorized` | `refresh` or `trial` | Auth failed. Reactivate. |
667
675
  | `rate_limit_exceeded` | `trial` | Trial endpoint is capped at 5 per hour per IP. Wait or use a different network. |
@@ -30,11 +30,11 @@ The `generated:issue-types` table below is regenerated from `fallow schema` by s
30
30
  | `duplicate-export` | `--duplicate-exports` | - | `// fallow-ignore-file duplicate-export` | Same symbol exported from multiple modules |
31
31
  | `circular-dependency` | `--circular-deps` | - | `// fallow-ignore-next-line circular-dependency` | Import cycles in the module graph |
32
32
  | `re-export-cycle` | `--re-export-cycles` | - | `// fallow-ignore-file re-export-cycle` | Barrel files re-exporting from each other in a loop (`kind: "multi-node"`) or a barrel re-exporting from itself (`kind: "self-loop"`). Chain propagation through the loop is a structural no-op so imports through any member may silently come up empty. Default `warn`. Distinct from `circular-dependencies` (runtime cycles, sometimes intentional). File-scoped suppression only: `// fallow-ignore-file re-export-cycle` on any member breaks the cycle. |
33
- | `package-cycle` | `--package-cycles` | - | `// fallow-ignore-next-line package-cycle` | Two or more workspace packages import each other in a loop, so they cannot be built in dependency order. Edges are resolved imports, not declared `package.json` dependencies. Imports from test, spec, story, fixture and tooling config files do not count. Type-only imports count, and each hop in `edges` has a `type_only` flag. Default `warn`. Requires a workspace with two or more packages. A suppression removes one import; the cycle goes away when every import on one hop is suppressed. `group_truncated: true` means the package group has more cycles than listed. `--changed-since` and diff scope use the example import of each hop (`edges[].path`) |
33
+ | `package-cycle` | `--package-cycles` | - | `// fallow-ignore-next-line package-cycle` | Two or more workspace packages import each other in a loop, so they cannot be built in dependency order. Edges are resolved imports, not declared `package.json` dependencies, so a package cycle can exist without a file-level cycle. Imports from test, spec, story, fixture and tooling config files do not count. Each finding lists `packages` in cycle order and one example import per hop in `edges`. Type-only imports count, and each hop has a `type_only` flag (a type-only hop still matters for declaration builds). Default `warn`. Requires a workspace with two or more packages. A suppression removes one import; the cycle goes away when every import on one hop is suppressed. `group_truncated: true` means the package group has more cycles than listed. `--changed-since` and diff scope use the example import of each hop (`edges[].path`). |
34
34
  | `boundary-violation` | `--boundary-violations` | - | `// fallow-ignore-next-line boundary-violation` | Imports crossing architecture zone boundaries. Presets: `layered`, `hexagonal`, `feature-sliced`, `bulletproof`; `autoDiscover` can create one zone per feature directory; per-rule `allowTypeOnly: [zones]` admits `import type` / `export type` crossings while still blocking value imports. Named and default imports through a barrel are judged against the zone of the origin module (`to_path`), and `via_path` names the barrel. Optional sections: `boundaries.coverage.requireAllFiles` reports unzoned source files (`allowUnmatched` globs exempt intentional ones), and `boundaries.calls.forbidden` bans callee patterns per zone (segment-aware and import-resolved, so `child_process.*` covers `node:child_process` named/namespace/default imports; direct callees only, zoned files only). The whole family shares the `boundary-violation` rule and suppression token (`boundary-call-violation` and `boundary-call-violations` accepted as aliases); start the rule at `warn` for a staged rollout |
35
35
  | `boundary-coverage` | `--boundary-violations` | - | `// fallow-ignore-file boundary-violation` | Source file matches no configured architecture boundary zone; Requires boundaries.coverage.requireAllFiles |
36
36
  | `boundary-call-violation` | `--boundary-violations` | - | `// fallow-ignore-next-line boundary-call-violation` | Zoned file calls a callee its zone forbids; Requires boundaries.calls.forbidden patterns |
37
- | `policy-violation` | `--policy-violations` | - | `// fallow-ignore-next-line policy-violation` | Calls, imports, or catalogue-derived effects banned by a declarative rule pack (`rulePacks` config key lists standalone JSON/JSONC files of `banned-call`, `banned-import`, and `banned-effect` rules; pure data, no project code executes). Findings identified as `<pack>/<rule-id>`. Default `warn` master; per-rule `severity` overrides per finding and the exit gate reads the effective severity. Invalid or missing packs fail config load with exit 2. `fallow rule-pack-schema` prints the pack JSON Schema. Use the scoped token to suppress one rule; bare `policy-violation` still covers every pack rule on the line or file. |
37
+ | `policy-violation` | `--policy-violations` | - | `// fallow-ignore-next-line policy-violation` | Calls, imports, exports, catalogue-derived effects, and graph-resolved GDP proof producer ownership checked by declarative rule packs (`rulePacks` lists standalone JSON/JSONC files; pure data, no project code executes). Rule kinds: `banned-call`, `banned-import`, `banned-export`, `banned-effect`, and `gdp-proof-producer`. GDP rules trace `@gdp-ts/core` proof producers through unambiguous re-exports and require project-relative `allowedFiles`, optionally scoped by exact static `proofKinds`; they do not prove runtime authorization. Findings identified as `<pack>/<rule-id>`. Default `warn` master; per-rule `severity` overrides per finding and the exit gate reads the effective severity. Invalid or missing packs fail config load with exit 2. `fallow rule-pack-schema` prints the pack JSON Schema. Use `policy-violation:<pack>/<rule-id>` to suppress one rule; bare `policy-violation` covers every pack rule on the line or file. |
38
38
  | `stale-suppression` | `--stale-suppressions` | - | - | `fallow-ignore` comments or `@expected-unused` JSDoc tags that no longer match any issue |
39
39
  | `missing-suppression-reason` | `--stale-suppressions` | - | - | Suppression comment omits a required reason |
40
40
  | `unused-catalog-entry` | `--unused-catalog-entries` | yes | - | `pnpm-workspace.yaml` entries no workspace package.json references via `catalog:` (default `warn`) |
@@ -47,6 +47,7 @@ The `generated:issue-types` table below is regenerated from `fallow schema` by s
47
47
  | `misplaced-directive` | - | - | `// fallow-ignore-next-line misplaced-directive` | "use client" / "use server" directive is not in the leading position and is ignored; Requires the project to declare next |
48
48
  | `unprovided-inject` | `--unprovided-injects` | - | `// fallow-ignore-next-line unprovided-inject` | inject() / getContext() reads a key that no provide() / setContext() supplies |
49
49
  | `unrendered-component` | `--unrendered-components` | - | `// fallow-ignore-next-line unrendered-component` | A Vue / Svelte component is reachable through a barrel but rendered nowhere |
50
+ | `absent-component-prop` | `--absent-component-props` | - | `// fallow-ignore-next-line absent-component-prop` | Known reachable callers omit an optional prop consumed inside its component; Opt-in manual review candidate; defaults to off. Inspect caller evidence and defaults before changing the component. No automatic fix. |
50
51
  | `unused-component-prop` | `--unused-component-props` | - | `// fallow-ignore-next-line unused-component-prop` | A Vue defineProps prop or React component prop is referenced nowhere in its own component |
51
52
  | `unused-component-emit` | `--unused-component-emits` | - | `// fallow-ignore-next-line unused-component-emit` | A Vue <script setup> defineEmits event is emitted nowhere in its own component |
52
53
  | `unused-component-input` | `--unused-component-inputs` | - | `// fallow-ignore-next-line unused-component-input` | An Angular @Input() / signal input() / model() is read nowhere in its own component (class body or template); needs `@angular/core` dep |