UNPKG

fallow

Version:

Deterministic codebase intelligence for TypeScript and JavaScript. Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents. Optional runtime intelligence layer (Fallow Runtime) adds production execution

740 lines (587 loc) 147 kB
# Fallow CLI Reference Complete command and flag specifications for all fallow CLI commands. --- ## Table of Contents - [`dead-code`: Dead Code Analysis](#dead-code-dead-code-analysis) - [`dupes`: Duplication Detection](#dupes-duplication-detection) - [`fix`: Auto-Remove Unused Code](#fix-auto-remove-unused-code) - [`list`: Project Introspection](#list-project-introspection) - [`init`: Config Generation](#init-config-generation) - [`migrate`: Config Migration](#migrate-config-migration) - [`health`: Function Complexity Analysis](#health-function-complexity-analysis) - [`audit`: Changed-File Quality Gate](#audit-changed-file-quality-gate) - [`flags`: Feature Flag Detection](#flags-feature-flag-detection) - [`security`: Security Candidate Detection](#security-security-candidate-detection) - [`explain`: Rule Explanation](#explain-rule-explanation) - [`schema`: CLI Introspection](#schema-cli-introspection) - [`config-schema`: Config JSON Schema](#config-schema-config-json-schema) - [`plugin-schema`: Plugin JSON Schema](#plugin-schema-plugin-json-schema) - [`rule-pack-schema`: Rule Pack JSON Schema](#rule-pack-schema-rule-pack-json-schema) - [`config`: Show Resolved Config](#config-show-resolved-config) - [Global Flags](#global-flags) - [Environment Variables](#environment-variables) - [Output Formats](#output-formats) - [JSON Output Structure](#json-output-structure) - [Configuration File Format](#configuration-file-format) - [Inline Suppression Comments](#inline-suppression-comments) --- ## `dead-code`: Dead Code Analysis Analyzes the project for unused files, exports, dependencies, types, members, and more. Running `fallow` with no subcommand runs all analyses (dead code + duplication + complexity). Use `fallow dead-code` for dead code only. ### Flags <!-- generated:flags:dead-code:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--include-dupes` | `bool` | `false` | Cross-reference with duplication findings | | `--trace` | `string` | - | Trace export usage chain | | `--trace-file` | `string` | - | Show all edges for a file | | `--trace-dependency` | `string` | - | Trace where a dependency is used | | `--top` | `string` | - | Show only the top N items per category | | `--file` | `string` | - | Scope output to specific files. Only issues in the specified files are reported. Project-wide dependency issues are suppressed. Warns on non-existent paths. Useful for lint-staged | Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags), [`--output-file`](#global-flags), [`--legacy-envelope`](#global-flags), [`--changed-since`](#global-flags), [`--max-file-size`](#global-flags), [`--production`](#global-flags), [`--no-production`](#global-flags), [`--production-dead-code`](#global-flags), [`--baseline`](#global-flags), [`--save-baseline`](#global-flags), [`--workspace`](#global-flags), [`--changed-workspaces`](#global-flags), [`--include-entry-exports`](#global-flags). <!-- generated:flags:dead-code:end --> ### Issue Type Filters <!-- generated:flags:dead-code-filters:start --> | Flag | Issue Type | |---|---| | `--unused-files` | Unused files | | `--unused-exports` | Unused exports | | `--unused-deps` | Unused dependencies, devDependencies, optionalDependencies, type-only production deps, and test-only production deps | | `--unused-types` | Unused types | | `--private-type-leaks` | Opt-in API hygiene check (default `off`) for exported signatures that reference same-file private types. Storybook `*.stories.*` story files and framework routing convention files (Next.js App + Pages Router, Gatsby, Remix v2, TanStack Router, Expo Router) are skipped to avoid noise. Enable via this flag or `private-type-leaks: "warn"` / `"error"` in [`rules`](#rules-configuration). | | `--unused-enum-members` | Unused enum members | | `--unused-class-members` | Unused class members | | `--unused-store-members` | Unused Pinia store members | | `--unprovided-injects` | inject() / getContext() reads a key that no provide() / setContext() supplies | | `--unrendered-components` | A Vue / Svelte component is reachable through a barrel but rendered nowhere | | `--unused-component-props` | A Vue defineProps prop or React component prop is referenced nowhere in its own component | | `--unused-component-emits` | A Vue <script setup> defineEmits event is emitted nowhere in its own component | | `--unused-server-actions` | A Next.js Server Action exported from a "use server" file is referenced by no code in the project | | `--unused-load-data-keys` | A SvelteKit load() return-object key is read by no consumer | | `--unresolved-imports` | Unresolved imports | | `--unlisted-deps` | Unlisted dependencies | | `--duplicate-exports` | Duplicate exports | | `--circular-deps` | Circular dependencies | | `--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). | | `--boundary-violations` | Boundary violations (imports crossing architecture zone boundaries, unzoned source files when `boundaries.coverage.requireAllFiles` is set, and forbidden calls from `boundaries.calls.forbidden`; suppression token `boundary-violation`, with `boundary-call-violation` and `boundary-call-violations` accepted as aliases for the whole family) | | `--policy-violations` | Rule-pack policy violations (banned calls and banned imports declared via the `rulePacks` config key) | | `--stale-suppressions` | Stale suppression comments or `@expected-unused` JSDoc tags | | `--unused-catalog-entries` | Unused pnpm catalog entries | | `--empty-catalog-groups` | Empty named pnpm catalog groups | | `--unresolved-catalog-references` | Package references to missing pnpm catalog entries | | `--unused-dependency-overrides` | Unused pnpm dependency overrides | | `--misconfigured-dependency-overrides` | Malformed pnpm dependency overrides | <!-- generated:flags:dead-code-filters:end --> ### Examples ```bash # Full analysis with JSON output fallow dead-code --format json --quiet # Only unused exports fallow dead-code --format json --quiet --unused-exports # PR check: only changed files fallow dead-code --format json --quiet --changed-since main --fail-on-issues # CI mode with SARIF upload fallow dead-code --ci # Production-only analysis fallow dead-code --format json --quiet --production # Single workspace package fallow dead-code --format json --quiet --workspace my-package # Multiple workspaces: comma-separated fallow dead-code --format json --quiet --workspace web,admin # Glob (matches package name OR relative path) fallow dead-code --format json --quiet --workspace 'apps/*' # Exclude a workspace from the set fallow dead-code --format json --quiet --workspace 'apps/*,!apps/legacy' # Monorepo CI: auto-scope to workspaces containing any file changed since origin/main fallow dead-code --format json --quiet --changed-workspaces origin/main # Debug: trace an export fallow dead-code --format json --quiet --trace src/utils.ts:myFunction # Incremental adoption with baseline fallow dead-code --format json --quiet --save-baseline fallow-baselines/dead-code.json fallow dead-code --format json --quiet --baseline fallow-baselines/dead-code.json --fail-on-issues # Regression detection: save baseline on main, compare on PRs fallow dead-code --format json --quiet --save-regression-baseline fallow dead-code --format json --quiet --fail-on-regression --tolerance 2% # Scope to specific files (e.g., lint-staged) fallow dead-code --format json --quiet --file src/utils.ts --file src/helpers.ts # Catch typos in entry file exports fallow dead-code --format json --quiet --include-entry-exports ``` --- ## `dupes`: Duplication Detection Finds code duplication and clones across the project. By default, `fallow dupes` skips generated framework output matching `**/.next/**`, `**/.nuxt/**`, `**/.svelte-kit/**`, `**/.turbo/**`, `**/.parcel-cache/**`, `**/.vite/**`, `**/.cache/**`, `**/out/**`, and `**/storybook-static/**`. These defaults merge with `duplicates.ignore`. Set `duplicates.ignoreDefaults = false` to opt out and use only your configured ignore list. If the reported duplication percentage drops after upgrading, this generated-output filtering is the expected reason. ### Flags <!-- generated:flags:dupes:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--mode` | `strict\|mild\|weak\|semantic` | - | Detection mode | | `--min-tokens` | `string` | - | Minimum token count for a clone | | `--min-lines` | `string` | - | Minimum line count for a clone | | `--min-occurrences` | `string` | - | Minimum number of occurrences before a clone group is reported (must be ≥ 2). Raise to skip pair-only clones and focus on widespread copy-paste worth refactoring. `fallow init` writes `minOccurrences: 3` into new projects. | | `--threshold` | `string` | - | Fail if duplication exceeds this percentage | | `--skip-local` | `bool` | `false` | Only report cross-directory duplicates | | `--cross-language` | `bool` | `false` | Strip type annotations for TS↔JS matching | | `--ignore-imports` | `bool` | `false` | Exclude module wiring from clone detection | | `--no-ignore-imports` | `bool` | `false` | Count module wiring as clone candidates (opt out of the default exclusion) | | `--top` | `string` | - | Show only the N most-duplicated clone groups (sorted by instance count desc, tiebreak: line count desc, then path/line). Summary stats reflect the full project. | | `--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 | 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). <!-- generated:flags:dupes:end --> ### Detection Modes | Mode | Behavior | |------|----------| | `strict` | Exact token match (no normalization) | | `mild` | Syntax normalized (whitespace, semicolons) | | `weak` | Different literal values treated as equivalent | | `semantic` | Renamed variables also treated as equivalent | ### Examples ```bash # Default duplication scan fallow dupes --format json --quiet # Semantic mode (detects renames) fallow dupes --format json --quiet --mode semantic # Cross-directory only, fail at 5% fallow dupes --format json --quiet --skip-local --threshold 5 # Trace clones at a specific location fallow dupes --format json --quiet --trace src/utils.ts:42 # Deep-dive a clone group by its dup:<id> fingerprint (from the listing or JSON) fallow dupes --format json --quiet --trace dup:7f3a2c1e # Only check duplication in changed files fallow dupes --format json --quiet --changed-since main # Incremental CI fallow dupes --format json --quiet --save-baseline fallow-baselines/dupes.json fallow dupes --format json --quiet --baseline fallow-baselines/dupes.json --threshold 5 ``` --- ## `fix`: Auto-Remove Unused Code Auto-removes unused exports, dependencies, enum members, and pnpm catalog entries. ### Flags <!-- generated:flags:fix:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--dry-run` | `bool` | `false` | Show what would be removed without modifying files. For `add-to-config` actions, prints a unified-diff preview of the proposed config write; JSON mode includes the diff under a `proposed_diff` field on the fix entry. | | `--yes` | `bool` | `false` | Skip confirmation prompt (**required** in non-TTY) | | `--no-create-config` | `bool` | `false` | Refuse to create a new `.fallowrc.json` when none exists. The duplicate-export config-add path is skipped with `skip_reason: "no_create_config"`; source-file edits proceed normally. Use in pre-commit hooks, CI bots, and `fallow watch` where silently materialising a new top-level file would surprise the user. | Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags). <!-- generated:flags:fix:end --> ### What gets fixed - Unused exports (removes the `export` keyword; whole-enum block when every member is unused) - Unused dependencies (removed from `package.json`) - Unused enum members (removed from the declaration) - 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. - 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. ### On-disk drift protection `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 dead-code 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. ### Low-confidence export removals 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: - **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. - **Files with an unresolved import.** The file itself imports something fallow could not resolve, so its local usage graph is incomplete. 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 dead-code` 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. ### File encoding contract `fallow fix` is UTF-8 only. Two encoding shapes that previously caused silent corruption are handled explicitly (issue #475): - **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. - **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. **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. ### Examples ```bash # Preview changes fallow fix --dry-run --format json --quiet # Apply changes (--yes required in agent/CI environments) fallow fix --yes --format json --quiet ``` --- ## `list`: Project Introspection Inspect discovered files, entry points, detected frameworks, and architecture boundary zones. ### Flags <!-- generated:flags:list:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--entry-points` | `bool` | `false` | List detected entry points | | `--files` | `bool` | `false` | List all discovered files | | `--plugins` | `bool` | `false` | List active framework plugins | | `--boundaries` | `bool` | `false` | Show architecture boundary zones, rules, per-zone file counts, and `logical_groups[]` for `autoDiscover` parents | | `--workspaces` | `bool` | `false` | 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. | Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#global-flags). <!-- generated:flags:list:end --> ### Examples ```bash fallow list --files --format json --quiet fallow list --entry-points --format json --quiet fallow list --plugins --format json --quiet fallow list --boundaries --format json --quiet fallow list --workspaces --format json --quiet fallow workspaces --format json --quiet # alias of `fallow list --workspaces` ``` 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 dead-code --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. 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`. --- ## `init`: Config Generation Creates a config file in the project root. ### Flags <!-- generated:flags:init:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--toml` | `bool` | `false` | Create `fallow.toml` instead of `.fallowrc.json` | | `--agents` | `bool` | `false` | Scaffold a starter `AGENTS.md` guide for coding agents. Prefills Install (from the `packageManager` field, or pnpm via `pnpm-workspace.yaml`), Test (only when exactly one of Vitest / Jest / Playwright is present), Typecheck (`tsc --noEmit` when `tsconfig.json` exists), and monorepo module-boundary lines; everything ambiguous stays blank (no lockfile sniffing). Prefilled command lines carry an HTML provenance comment. Refuses to overwrite an existing `AGENTS.md` | | `--hooks` | `bool` | `false` | Scaffold a pre-commit git hook that runs `fallow audit --base <ref> --quiet --gate-marker pre-commit`. Alias for `fallow hooks install --target git` | | `--branch` | `string` | - | Fallback base branch for the pre-commit hook when no upstream is set (default: `main`). Only used with `--hooks` | | `--decline` | `bool` | `false` | Record that this project deliberately stays unconfigured: persists a decline so the first-contact setup hint and the `setup` next-step stop appearing here. Writes no config file; idempotent | Common global flags for this command: [`--root`](#global-flags), [`--config`](#global-flags). <!-- generated:flags:init:end --> ### Examples ```bash fallow init # creates .fallowrc.json with $schema fallow init --toml # creates fallow.toml fallow init --agents # scaffolds a starter AGENTS.md prefilled from detected project info (never overwrites) fallow hooks install --target git fallow hooks install --target git --branch develop # fallback base branch when no upstream is set ``` ## `hooks`: Managed Hook Status And Installation ```bash fallow hooks status --format json fallow hooks install --target git fallow hooks install --target agent fallow hooks uninstall --target git fallow hooks uninstall --target agent ``` `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. --- ## `migrate`: Config Migration Migrates configuration from knip and/or jscpd to fallow. Auto-detects config files. ### Flags <!-- generated:flags:migrate:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--toml` | `bool` | `false` | Output as `fallow.toml` (mutually exclusive with `--jsonc`) | | `--jsonc` | `bool` | `false` | Write to `.fallowrc.jsonc` instead of `.fallowrc.json`. Same JSONC content either way; the `.jsonc` extension lets editors auto-detect JSON-with-comments syntax highlighting | | `--dry-run` | `bool` | `false` | Preview without writing | | `--from` | `string` | - | Specify source config file path | Common global flags for this command: [`--root`](#global-flags), [`--config`](#global-flags). <!-- generated:flags:migrate:end --> Without `--jsonc` or `--toml`, fallow auto-mirrors the source extension: a `knip.jsonc` migration writes `.fallowrc.jsonc`, a `knip.json` migration writes `.fallowrc.json`. ### Detected Source Configs - `knip.json`, `knip.jsonc`, `.knip.json`, `.knip.jsonc` - `package.json` embedded `knip` field - `.jscpd.json` - `package.json` embedded `jscpd` field ### Examples ```bash fallow migrate --dry-run # preview fallow migrate # auto-detect; mirrors source extension fallow migrate --jsonc # force .fallowrc.jsonc output fallow migrate --toml # output as fallow.toml fallow migrate --from knip.jsonc ``` --- ## `health`: Function Complexity & File Health Analysis Analyzes function complexity across the project using cyclomatic and cognitive complexity metrics. By default all sections are included (health score, complexity findings, file scores, hotspots, and refactoring targets). Use `--complexity`, `--file-scores`, `--hotspots`, `--targets`, or `--score` to show only specific sections. Angular templates contribute synthetic `<template>` complexity findings whenever they use `@if`/`@for`/`@switch`/`@case`/`@defer (when ...)`/`@let` blocks, legacy structural directives (`*ngIf`, `*ngFor`), bound attributes (`[x]`, `(x)`, `bind-x`, `on-x`), or `{{ }}` interpolations. Both standalone external `.html` files referenced via `templateUrl` AND inline `@Component({ template: \`...\` })` literals are scanned. Inline-template findings anchor at the host `.ts` file's `@Component` decorator line and emit a `suppress-line` action with `// fallow-ignore-next-line complexity` (place the comment directly above the `@Component` decorator). External-template findings emit a `suppress-file` action with `<!-- fallow-ignore-file complexity -->` (place at the top of the `.html` file; HTML cannot express line-level comments). Tagged template literals containing `${...}` interpolations and `template:` properties bound to a variable are skipped (out of scope for the first cut). ### Flags <!-- generated:flags:health:start --> | Flag | Type | Default | Description | |---|---|---|---| | `--max-cyclomatic` | `string` | - | Fail if any function exceeds this cyclomatic complexity | | `--max-cognitive` | `string` | - | Fail if any function exceeds this cognitive complexity | | `--max-crap` | `string` | - | Fail if any function has CRAP score >= threshold. CRAP combines complexity with coverage (`CC^2 * (1 - cov/100)^3 + CC`). Pair with `--coverage` for accurate per-function CRAP; without Istanbul data fallow estimates coverage from the module graph. | | `--top` | `string` | - | Only show the top N most complex functions (and file scores/hotspots/targets) | | `--sort` | `severity\|cyclomatic\|cognitive\|lines` | `cyclomatic` | Sort order for complexity findings | | `--complexity` | `bool` | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. | | `--complexity-breakdown` | `bool` | `false` | Add a per-decision-point `contributions[]` array to each complexity finding in `--format json`. Each entry names the construct (`if`, `else-if`, `ternary`, boolean operator, loop, `case`, `catch`, `optional-chain`, ...) and carries its source line, the metric it adds to (`cyclomatic` or `cognitive`), its weight, and the nesting depth, so a consumer can explain WHY a function scored high. Off by default (no change to existing JSON/SARIF/markdown). Used by the VS Code inline editor breakdown and the MCP `check_health` `complexity_breakdown` param. | | `--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. | | `--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`). | | `--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. | | `--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. | | `--ownership-emails` | `raw\|handle\|anonymized\|hash` | - | 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`. | | `--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. Each target's JSON can include `direct_callers[]` (direct importers with the symbols they import) and `clone_siblings[]` (duplicate-code siblings with stable `dup:<8hex>` fingerprints for `fallow dupes --trace`); both omitted when empty. Human output adds `importers:` / `clones:` lines only when that evidence is present. | | `--css` | `bool` | `false` | Add structural CSS analytics: specificity hotspots, !important density, over-complex selectors, deep nesting, and conservative cleanup candidates. Standard CSS is parsed structurally; preprocessor sources are scanned only where fallow can avoid expanding Sass/Less semantics. | | `--effort` | `low\|medium\|high` | - | Filter refactoring targets by effort level. Implies `--targets`. | | `--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. | | `--min-score` | `string` | - | 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`. | | `--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). | | `--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`. | | `--since` | `string` | - | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). Ignored when `--churn-file` is set. | | `--min-commits` | `string` | - | Minimum number of commits for a file to be included in hotspot ranking. | | `--save-snapshot` | `string` | - | Save vital signs snapshot for trend tracking. Forces file-scores + hotspot computation. | | `--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`. | | `--coverage` | `string` | - | 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`. Falls back to `FALLOW_COVERAGE`, then `health.coverage`, then auto-detection. | | `--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`. | | `--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. | | `--min-invocations-hot` | `string` | `100` | Invocation threshold for hot-path classification. Takes effect only when `--runtime-coverage` is set. | | `--min-observation-volume` | `string` | - | Minimum total trace volume before the sidecar may emit high-confidence `safe_to_delete` / `review_required` verdicts. Below this, confidence is capped at `medium`. | | `--low-traffic-threshold` | `string` | - | 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%). | 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), [`--save-baseline`](#global-flags), [`--production`](#global-flags), [`--no-production`](#global-flags), [`--explain`](#global-flags). <!-- generated:flags:health:end --> ### Exit Codes 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). | Invocation | Exit 0 when | Exit 1 when | |------------|-------------|-------------| | `fallow health` (no gate flag) | no function exceeds a threshold | any function exceeds a threshold | | `--min-score N` | score >= N (findings informational) | score < N | | `--min-severity LEVEL` | no finding at or above LEVEL | any finding at or above LEVEL | | `--min-score N --min-severity LEVEL` | score >= N AND no finding >= LEVEL | score < N OR a finding >= LEVEL | | `--report-only` | always | never | `--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`. ### Examples ```bash # Full complexity analysis with JSON output fallow health --format json --quiet # Project health score with letter grade fallow health --format json --quiet --score # CI gate: fail if score below 70 fallow health --format json --quiet --min-score 70 # Top 10 most complex functions fallow health --format json --quiet --top 10 # Sort by cognitive complexity fallow health --format json --quiet --sort cognitive # Custom thresholds fallow health --format json --quiet --max-cyclomatic 15 --max-cognitive 10 # Per-file health scores fallow health --format json --quiet --file-scores # Top 20 files by triage concern fallow health --format json --quiet --file-scores --top 20 # Only analyze files changed since main fallow health --format json --quiet --changed-since main # Single workspace package fallow health --format json --quiet --workspace my-package # Incremental adoption with baseline fallow health --format json --quiet --save-baseline fallow-baselines/health.json fallow health --format json --quiet --baseline fallow-baselines/health.json # CI: fail if any function is too complex fallow health --max-cyclomatic 25 --max-cognitive 20 --quiet # Hotspot analysis (complex + frequently changing files) fallow health --format json --quiet --hotspots # Hotspots from the last year fallow health --format json --quiet --hotspots --since 1y # Hotspots with at least 5 commits fallow health --format json --quiet --hotspots --min-commits 5 # Top 10 hotspots from the last 90 days fallow health --format json --quiet --hotspots --since 90d --top 10 # Ranked refactoring recommendations fallow health --format json --quiet --targets # Top 5 refactoring targets fallow health --format json --quiet --targets --top 5 # Only low-effort refactoring targets (quick wins) fallow health --format json --quiet --effort low # Save a vital signs snapshot for trend tracking fallow health --format json --quiet --save-snapshot # Save snapshot to a custom path fallow health --format json --quiet --save-snapshot .fallow/baseline-snapshot.json # Compare current metrics against the most recent snapshot fallow health --format json --quiet --trend ``` ### JSON Output Structure ```json { "kind": "health", "schema_version": 7, "version": "2.97.0", "elapsed_ms": 32, "summary": { "files_analyzed": 482, "functions_analyzed": 3200, "functions_above_threshold": 3, "max_cyclomatic_threshold": 20, "max_cognitive_threshold": 15 }, "findings": [ { "path": "src/parser.ts", "name": "parseExpression", "line": 42, "col": 0, "cyclomatic": 28, "cognitive": 22, "line_count": 95, "exceeded": "both" } ] } ``` `health.thresholdOverrides[]` config entries can raise local cyclomatic, cognitive, or CRAP ceilings for matching files and optional exact function names. When an override affects output, health JSON includes top-level `threshold_overrides[]` state entries (`active`, `stale`, or `no_match`). Complexity findings evaluated with local ceilings include `effective_thresholds` and `threshold_source: "override"` so agents can see which thresholds drove the finding and avoid treating configured exceptions as hidden suppressions. When the unit size very-high-risk percentage is >= 3%, the JSON output includes a `large_functions` array listing functions exceeding 60 lines of code: ```json { "large_functions": [ { "path": "src/parser.ts", "name": "parseExpression", "line": 42, "line_count": 95 } ] } ``` This drill-down shows which specific functions are driving the unit size penalty in the health score, making it actionable without a separate analysis pass. With `--file-scores`, the JSON output also includes `file_scores` array and `summary.files_scored` / `summary.average_maintainability`: ```json { "summary": { "files_scored": 482, "average_maintainability": 88.5, "coverage_model": "static_estimated", "coverage_source_consistency": "uniform" }, "file_scores": [ { "path": "src/parser.ts", "fan_in": 8, "fan_out": 4, "dead_code_ratio": 0.25, "complexity_density": 0.22, "maintainability_index": 75.1, "total_cyclomatic": 42, "total_cognitive": 35, "function_count": 12, "lines": 190, "crap_max": 42.0, "crap_above_threshold": 2 } ] } ``` 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. 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`. 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. With `--hotspots`, the JSON output includes a `hotspots` array and `hotspot_summary`: ```json { "hotspot_summary": { "since": "6m", "min_commits": 3, "files_analyzed": 482, "files_excluded": 312, "shallow_clone": false }, "hotspots": [ { "path": "src/parser.ts", "score": 92, "commits": 28, "weighted_commits": 34.5, "lines_added": 410, "lines_deleted": 180, "complexity_density": 0.22, "fan_in": 8, "trend": "Accelerating" } ] } ``` Hotspot score formula: `normalized_churn × normalized_complexity × 100`, scaled 0–100. Higher means more urgent to refactor. The `trend` field indicates recent change velocity: `Accelerating` (increasing churn), `Stable` (constant), or `Cooling` (decreasing). Files below `--min-commits` are excluded. The `shallow_clone` field warns when git history is truncated (shallow clone), which may undercount commits. With `--targets`, the JSON output includes a `targets` array with ranked refactoring recommendations: ```json { "targets": [ { "path": "src/parser.ts", "priority": 82.5, "efficiency": 27.5, "recommendation": "Split high-impact file - 25 dependents amplify every change", "category": "split_high_impact", "effort": "high", "confidence": "medium", "factors": [ { "metric": "complexity_density", "value": 0.75, "threshold": 0.3, "detail": "density 0.75 exceeds 0.3" }, { "metric": "fan_in", "value": 25.0, "threshold": 10.0, "detail": "25 files depend on this" } ] } ], "target_thresholds": { "fan_in_p95": 12.0, "fan_in_p75": 5.0, "fan_out_p95": 15.0, "fan_out_p90": 8 } } ``` Targets are sorted by `efficiency` (priority / effort_numeric) descending, surfacing quick wins first. The `target_thresholds` object exposes the adaptive percentile-based thresholds used for scoring. Priority formula: `min(complexity_density, 1) x 30 + hotspot_boost x 25 + dead_code_ratio x 20 + fan_in_norm x 15 + fan_out_norm x 10`, clamped to 0-100. Fan-in and fan-out normalization uses the project's p95 values (with floors). Categories: `urgent_churn_complexity`, `break_circular_dependency`, `split_high_impact`, `remove_dead_code`, `extract_complex_functions`, `extract_dependencies`, `add_test_coverage`. Each target includes `efficiency`, `effort` (low/medium/high), `confidence` (high/medium/low, data source reliability), and contributing `factors`. The `add_test_coverage` category fires when a file has 2+ functions with CRAP scores >= 30 and complexity density > 0.3. The `crap_max` metric appears in contributing factors for these targets. ### Vital Signs All `health` JSON output includes a `vital_signs` object with project-wide metrics: ```json { "vital_signs": { "dead_file_pct": 3.2, "dead_export_pct": 8.1, "avg_cyclomatic": 4.5, "critical_complexity_pct": 1.2, "p90_cyclomatic": 12, "maintainability_avg": 88.5, "maintainability_low_pct": 4.1, "hotspot_count": 7, "hotspot_top_pct_count": 3, "circular_dep_count": 2, "circular_deps_per_k_files": 4.1, "unused_dep_count": 3, "unused_deps_per_k_files": 6.2, "unit_size_profile": { "low_risk": 82.1, "medium_risk": 11.4, "high_risk": 4.3, "very_high_risk": 2.2 }, "functions_over_60_loc_per_k": 22.0, "unit_interfacing_profile": { "low_risk": 95.6, "medium_risk": 3.8, "high_risk": 0.5, "very_high_risk": 0.1 }, "p95_fan_in": 8, "coupling_high_pct": 2.3 } } ``` 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. With `--score`, the JSON output includes a `health_score` object: ```json { "health_score": { "formula_version": 2, "score": 76.9, "grade": "B", "penalties": { "dead_files": 3.1, "dead_exports": 6.0, "complexity": 0.0, "p90_complexity": 0.0, "maintainability": 0.0, "unused_deps": 10.0, "circular_deps": 4.0, "unit_size": 0.0, "coupling": 0.0, "duplication": 4.0 } } } ``` 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). ### Health Trend With `--trend`, the JSON output includes a `health_trend` object comparing current metrics against the most recent saved snapshot: ```json { "health_trend": { "compared_to": { "timestamp": "2026-03-25T14:30:00Z", "git_sha": "a1b2c3d", "score": 74.2, "grade": "B" }, "metrics": [ { "name": "score", "label": "Health Score", "previous": 74.2, "current": 76.9, "delta": 2.7, "direction": "improving"