Repository navigation
Releases: fallow-rs/fallow
Release list
v3.33.1: health and duplication baselines in CI, Vitest fixes
Bug fixes
- The GitHub Action and the GitLab template can baseline health and duplication on the default run. A project with existing complexity can now gate the default
fallowrun with a baseline. Set thehealth-baselineanddupes-baselineinputs of the action, orFALLOW_HEALTH_BASELINEandFALLOW_DUPES_BASELINEin the GitLab template. Create the files withfallow health --save-baselineandfallow dupes --save-baseline. Other commands ignore these inputs. Both integrations report a stale health or duplication baseline, or one that another command saved.fail-on-stale-baseline(FALLOW_FAIL_ON_STALE_BASELINE) judges these baselines too. It also judges a--health-baselineor--dupes-baselineflag that you pass throughargs(FALLOW_ARGS). (#3316) - The action and the GitLab template no longer count health findings that a baseline accepts. With a health baseline, the CLI passed, but the action and the GitLab template failed the job with
fail-on-issues. Afallow healthrun also failed the job. The count now uses the findings that remain after the baseline. (#3316) - The job summary counts health findings after the baseline. With a loaded health baseline,
fallow report --format github-summaryshows the new findings and gives the accepted ones as context, for example "2 new functions exceed thresholds (74 in the baseline)". Without a baseline, the summary does not change. (#3316) fallow --helpnames the baseline flags of the bare run. On barefallow,--baselineand--save-baselinehold the dead-code baseline only. When a bare run gets afallow healthorfallow dupesbaseline through--baseline,fallownow prints a note that tells you to use--health-baselineor--dupes-baseline. (#3316)- The Vitest JSX import source follows the Vitest project model. On Vitest 5, an inline
test.projectsentry inherits the JSX import source of its config unless it setsextends: false. On Vitest 4, it inherits only withextends: true. Fallow reads the Vitest version fromnode_modules/vitest, else from the declared range. Project and baseincludeglobs add up. Fallow also reads a config thatextendsnames. It reads project configs with a custom name, such asvitest.e2e.config.ts, also when a glob entry names them. Fallow no longer reports these files as unused. (#3318) - Fallow reads
test.excludefor the Vitest JSX import source. Whentest.excludeor a negatedtest.includeentry removes a test file, Fallow no longer applies the JSX runtime of its project to that file. Withouttest.exclude, the Vitest default exclude applies. The globs are relative totest.dir,test.rootor the Viterootwhen the config sets one. (#3318) reactis no longer an unused dependency when Vitest test files use JSX with the default runtime. When a Vitest config sets no JSX import source, Vite imports thereactJSX runtime. Fallow now creditsreactwhen a test file of that config has JSX and no@jsxImportSourcepragma. A TypeScript test file gives no credit when its tsconfig setsjsxImportSource, or ajsxmode without the automatic runtime. The credit applies only to the workspace of the test file. Avite.config.*next to avitest.config.*gives no credit, because Vitest does not load it. (#3318)- The source-map warning of
fallow coverage upload-source-mapstries the same paths as Fallow Cloud. The CLI dropped the first segments of a source such aswebpack:///@scope/app/./src/x.ts, and the cloud did not. The warning could then check a path that the cloud never uses. A shared fixture of path cases now keeps the two implementations equal. (#3320)
Full Changelog: v3.33.0...v3.33.1
v3.33.0: fallow architecture, baseline pruning, faster musl binaries
Features
fallow architecturereports import cycles, boundary violations and rule-pack policy violations in one command. Use--cycles,--boundariesor--policyto select one kind. The command takes the same scope and output flags asfallow dead-code. Runfallow guard <files>before an edit andfallow architectureafter it. Barefallowshows these findings in an "Architecture" category after the other dead-code categories. It also shows the category with--quiet,--group-byand--summary. With--group-by,fallow architectureshows one "Architecture" heading per group.fallow architecture --format jsonwriteskind: "architecture"with its ownschema_version(1). With--group-by, the kind isarchitecture-grouped. The arrays, finding ids, actions, exit codes, gate outcomes and severities are the same as onfallow dead-code.next_stepsnamesfallow architecture. With--explain,_meta.docspoints to the architecture page, and_meta.ruleslists only the architecture rules.--save-baselinewrites a dead-code baseline (kind: "dead-code"), so the two commands read each other's baselines. The JSON schema and the generated TypeScript types have theArchitectureOutputandArchitectureGroupedOutputtypes.fallow report, MCP, the Node bindings and CI accept the architecture report.fallow report --fromrenders a savedarchitectureorarchitecture-groupedfile in every format, the same as the direct run. PR comments and reviews offallow architecturehave the title "Fallow architecture report".- The MCP server has a
check_architecturetool, and Code Mode exposes it ascheckArchitecture. The Node bindings havedetectArchitecture, andfallow_apihasrun_architecture. Each returns thearchitectureoutput.detectCircularDependencies,detectBoundaryViolationsand the MCPanalyzetool keep thedead-codeoutput. - Bare
fallowacceptsarchitecturein--onlyand--skip.--only architectureruns the dead-code analysis and reports only the architecture findings, and itsFailed:line namesarchitecture.--skip architectureremoves these findings from the dead-code section. The health score and the--save-baselinefile do not change. - The GitHub Action accepts
command: architecture, and the GitLab template acceptsFALLOW_COMMAND: architecture. For this command,issue-typesandFALLOW_ISSUE_TYPEStakecycles,boundariesandpolicy. Another value stops the job with exit code 2 and an error that names the valid values. - Telemetry records
fallow architectureruns as thearchitectureworkflow.
fallow baselines pruneremoves the baseline entries of fixed findings. The command reads the files thataudit.deadCodeBaseline,audit.healthBaselineandaudit.dupesBaselinename. It runs one whole-project analysis and removes each entry that--baselineno longer matches. It never adds an entry, so a new finding stays visible to the gates. After a prune,--fail-on-stale-baselinepasses, and the baseline hides the same findings as before.--checkwrites nothing and exits 1 when an entry can be pruned.--dead-code-baseline,--health-baselineand--dupes-baselineoverride the config paths.--coveragegives the coverage input for CRAP scores. The command skips a file with an older key form or with fields from a newer version. It prints the command that saves that file again. The stale-baseline warning and the gate message now name the prune command. Thanks @BenMcGit for the request (#3281).fallow hooks install --target git --prune-baselinesprunes the baselines in the pre-commit hook. The hook runsfallow baselines prunebefore the audit and stages the baseline files that the prune wrote. The prune reads the working tree, but the commit uses the index. So the hook skips the prune when the working tree has unstaged or untracked changes. It also skips the prune for a commit with paths (git commit <path>). A failed prune never blocks the commit. The hints for Lefthook and for an existing hook include the same step.fallow trace --dependency <package>reports how the code uses each imported name of a package. The report gives file and call counts, and sites with line and column. It follows one hop through project wrappers such asuseSelector.withTypes(), and it counts each use that it cannot resolve.--sites,--specifier,--limitand--cursorpage the sites.--callers --depth Nadds the files that import the users. The MCP tooltrace_dependencyaccepts the same options.- A JSX import source in a Vitest config now adds a module graph edge. With the automatic JSX runtime,
oxc.jsx.importSource(or the olderesbuild.jsxImportSource) makes each test file with JSX import<source>/jsx-dev-runtime. Fallow now adds this edge when the source is a path and the test file has JSX and no@jsxImportSourcepragma. A package source only marks the package as used. A local JSX runtime no longer shows as unused, and--impact-closureon a runtime file now includes these test files.- The edge applies to the files that
test.includematches. Fallow uses the root config when it has notest.projects, else each inline project config. - A project inherits the root source only with
extends: true. The classic runtime adds no edge. - A relative source resolves from the test file first, then from the config directory. A source that does not resolve adds no edge and no unresolved import.
- The edge applies to the files that
Changed
- The Linux musl binaries are about 4 times faster. The
mallocof musl is slow when many threads allocate, and fallow analyzes files in parallel. The musl binaries now use mimalloc. This applies to thelinux-x64-muslandlinux-arm64-muslnpm packages and the musl release assets. On a 10-core aarch64 Alpine container,check,dupesandhealthtook 77% less time over 15 fixture cases, with identical output. Peak memory is about a third higher. The Node addon keeps the default allocator. Glibc, macOS and Windows builds do not change. - Fallow hides the findings in an unused file by default. Fallow no longer lists an unused export, type, class member or enum member in a file that it reports as unused. Deleting the file removes these findings too. The JSON output has the count in
cascade_hidden, and SARIF and markdown (grouped or not) show the count too. To list the findings, pass--show-cascade, setshowCascadein the config, or use theshow_cascadeparameter of the MCP tools. The health vital signs still count them. When an issue-type filter leaves outunused-files, nothing is hidden. fallow vizshows where to start and says what is wrong.- The Overview panel opens with the health grade and one card per lens. Each card shows the number of files, the number of high findings, and the worst file with its reason. The Overview map colors files that have findings in any lens. Folder labels show how many of their files have findings.
- Each lens list opens with a short summary and a severity split. The list groups rows into high, medium and low. The full file name comes first, then a plain reason. For example, "File path built from input" replaces
path-traversal, and "High complexity, no test coverage" replaces a CRAP score. Security lists each file once with all of its candidates. - Folders with no imports to or from other folders now show when the active lens has findings in them, for example unused files. Health colors only files above the review threshold, so high-risk files stand out. Folders without findings fade.
- The Overview tab of the file panel opens with a list of the lenses that have findings in the file, each with its reason. The Health tab shows maintainability, change risk and importers. Then it lists each function to fix with its branches, lines, test coverage and next step. The treemap rings and names the selected file.
- Folder and import-group labels have two lines (the name, then the size and findings). The labels stay over their cluster and keep clear of the controls and the legend. Import groups that span folders read as
site/src + 5 moreinstead of(mixed). The status line shows the grouping or the path of the file in focus. The map hides disabled controls. - A click on a line between two folders lists the target files in use. Then it lists every import, with forbidden and cyclic imports first.
- Hover tooltips use the same plain reasons as the panel. During a search, the legend explains the match and importer rings.
- Below 700 px, the panel docks under the map as a sheet, so the map stays visible at full width. The legend becomes one line, crowded labels drop, and the file focus view fits both columns.
- The map now follows the design of fallow.tools and fallow.cloud: paper and ink, Barlow type, hairline rules, square panels and flat controls. Day paper is the default. The night theme follows the system or the stored choice. The report embeds the fonts, so it looks the same offline.
- On the graph, folders are octagons, the imports between them run at 0, 45 and 90 degrees, and flagged files have a ring. When you hover over an import line, the map picks the line under the pointer, gives it a pale blue halo and fades the others. Lines that are hidden at the current zoom no longer react. A hover over a file draws its imports the same way, in ink.
- With a file open, the panel shows its place in the active lens ("3 of 61 in Security") with previous and next buttons. The
jandkkeys do the same. - High findings, sparse medium findings, and the copies of an open duplicated block get a halo on the graph.
- The Architecture panel names the folders in an import loop that the map outlines, and says why the loop matters.
- Folders with high findings get a label first. Labels no longer cover the dots of high findings. A label that had to mo...
v3.32.0: grouped health trends, markdown from saved runs, dead-code accuracy fixes
Features
fallow dead-code --absent-component-propsreports optional props that callers never pass. The rule reports an optional component prop that no known reachable caller passes. It is off by default. It supports React, Preact, Solid, Qwik, Vue, Svelte, Astro, Angular, Lit and Ember components. Each finding shows the declaration, the callers, the framework and whether a default exists. Fallow gives no finding when it cannot see every caller of the component. Static analysis does not prove that a prop is unreachable at runtime. Review defaults and API intent manually. The rule has no automatic fix.fallow health --group-byselects groups and tracks a trend per group.--group <KEY>keeps only the matching groups. It accepts exact keys, globs and!negations. Each group gets severity and hotspot counts.--save-snapshoton a grouped run stores the group data (snapshot schema v11).--trendor the new--trend-from <PATH>then adds a trend per group. Markdown and the GitHub job summary get a## Health by <mode>table and one findings block per group.--top Nnow applies to each group. The MCPcheck_healthtool acceptsgroupandtrend_from.fallow report --from <file> --format markdownrenders saved JSON output. A saveddead-code,dupes,healthor bare combined run gives the same markdown as the live run with the same flags. You can analyze once and write both a job summary and a markdown report from one JSON file. The saved output of a combined run does not record--group-by, so its markdown is not grouped. A savedaudit,securityorfixrun, and a combined run with type-aware evidence, exit 2 with a reason.fallow dupesmarks symlinked clone instances. A clone instance under a symlink now hasis_symlink: truein JSON,symlink=truein compact output and a(symlink)marker in human output. To report only duplication between real files, setduplicates.ignoreSymlinks: trueor pass--ignore-symlinks.--no-ignore-symlinksoverrides the config value. The MCP tools, the Node API, the LSP and the VS Code settingfallow.duplication.ignoreSymlinksaccept the same option. Thanks @lzear for the report (#2961).fallow agent installadds a native commit gate for Codex. The install writes a PreToolUse handler to.codex/hooks.jsonand the gate script to.codex/hooks/fallow-gate.sh. The handler runs the audit from the nearest install root, also from a subdirectory. The Claude Code handler now also finds the nearest install root. Runfallow agent installagain to get the new handler. Codex runs a hook only after you trust it in/hooks.fallow agent statusshows the gate as its own row, andfallow hooks status --format jsonadds acodex_gateentry. TheAGENTS.mdblock is now routing guidance.fallow hooks install --target agent --agent codex --usernow writes the user gate and no longer writes the projectAGENTS.mdblock.- The new
fallow-setupskill tells an agent how to add code-quality tools. The skill is for a JavaScript or TypeScript project. It tells the agent to detect the existing tools, runfallow recommend, install Fallow as a dev dependency and add a CI gate with a baseline. It gives each tool one job, and it removes a tool only after a parity check.fallow agent installwrites it next to thefallowskill. - The
gdp-proof-producerrule-pack kind restricts proof producers. The opt-in rule restricts@gdp-ts/core.defineProofcalls toallowedFiles.proofKindsassigns literal proof labels to owner modules. The analysis follows static imports and unambiguous re-export chains. - Fallow has new built-in plugins.
release-itkeeps each.release-it.*config form and reads thepackage.jsonkey. It does not read the JSON5, YAML and TOML forms. It credits thepluginsandextendspackages.evemakes agent modules entry points.ag-uicredits the hooks of a class that extendsAbstractAgentdirectly.nestjs-trpccredits the methods that the module calls.kibanareadskibana.jsoncplugin manifests and adds their entry points.
- The health JSON names the sections that the run produced. The root array
sectionslists each section that the run computed. With--baseline,summary.baseline_staleness.remaining_findingsgives the number of functions above a threshold that the baseline does not accept. - The CLI, MCP and the LSP report the same
finding_idfor a security finding. The VS Code "Copy Fallow finding id" quick fix now works on a security candidate (#3035). - Fallow can suggest its Claude Code plugin. Inside a Claude Code session, Fallow writes one hint line to stderr for human output. Fallow writes the hint only without
--quiet, outside CI, and when no Fallow plugin or skill is installed. The hint has no effect until the official marketplace lists the Fallow plugin. SetFALLOW_CLAUDE_CODE_HINT=offorFALLOW_SUGGESTIONS=offto suppress it.
Changed
-
Tooling dependencies need evidence of use. Three kinds of devDependency no longer count as used by name alone:
- A command-line tool from the tooling catalogue.
- A tooling package of an active plugin.
- A
@types/Xpackage.
A tool counts as used when a script, a CI workflow or a git hook runs it. It also counts when its config file or its package.json key exists, or when a plugin credits it. Fallow now reads git hooks in
.husky/, lefthook, simple-git-hooks and lint-staged configs. A@types/Xpackage counts as used when the project declares or importsX, or namesXin tsconfigtypes. A short list of global type packages, such as@types/node, always counts.--trace-dependencynames each credit. Production dependencies still count as used by name. -
Root dependencies follow the nearest manifest. Each import now counts for the nearest manifest that installs the package. Fallow reports a root declaration as unused when each import uses a workspace declaration. The finding has a
declared_and_imported_infield that names those workspaces. Such an entry gets one finding, not a second test-only or type-only finding. -
export default Namelinks to the local bindingName. Fallow now reports unused members of a class that a file exports this way. WithignoreExportsUsedInFile, such a default export is used when the file usesName. -
fallow health --group-bycounts a clone in each group that holds an instance. The groupduplicated_linesvalues now sum to the project value. Group scores can drop once after the upgrade. -
CODEOWNERS matching follows GitHub. A pattern without a trailing
/owns the directory contents.*does not match/,apps/matches anappsdirectory at any depth, and/*owns only the files at the root. Owner groups can change. -
Gate rows show a count. The
health-findingsanderror-severity-findingsentries ingate_outcomesnow haveobservedandthreshold_label. The PR decision and the PR comment show the count, for example6 at or above error. -
fallow hooks install --target agentselects Codex from a.codex/directory, not fromAGENTS.mdalone.
Bug fixes
Dependencies
- Fallow no longer reports an optional peer of a used dependency as unused. Fallow also no longer reports a devDependency that the same manifest lists as a peer. An optional peer that the project does not list no longer credits its own peers.
- A peer-only entry no longer hides the ancestor workspace that installs a package. A workspace file can use a package that an ancestor manifest declares. This does not apply to production code in a publishable workspace. An ancestor declaration counts as used when a nested workspace uses it.
- A
postcss.config.*file credits the PostCSS plugins that it names, also without a declaredpostcsspackage. - A script binary credits every package that can provide it, for example
run-pfornpm-run-all2. - Fallow credits a runtime loader that a tool config needs, such as
ts-nodefor a TypeScript Jest config. The Mocha plugin reads.mocharcfiles and themochakey. - A tool config file in a workspace package credits the root devDependency of that tool.
require.resolveandimport.meta.resolvewith any subpath credit the package.require.resolve('pkg')reports an unlisted package. Thanks @DrJKL for the report (#3213).import {} from 'pkg', a module augmentationdeclare module 'pkg', and anode_modules/.bin/<name>path in source credit the package.- Fallow credits Expo config plugins in the app config, Oxlint
jsPluginsinsideoverrides, and packages in StarlightcustomCss. - React Router apps without
entry.serverno longer reportisbotas unused. - An esbuild call in a test file no longer removes the credit that a dependent workspace gets for the packages of a private sibling. Fallow removes that credit only when a build file or a package script sets every package external.
--trace-dependencygives the same credits as the dependency report. It names each manifest that the report flags.
Entry points and plugins
- The tsdown and tsup plugins read JSON configs, the
package.jsonkey and string or objectentryvalues. They also read config arrays, configs in workspace packages and tsdownworkspaceglobs. Thanks @moltar for the report (#3209). - Playwright reads every
defineConfigargument and the statictestDirandtestMatchsettings. - These files are now entry points: the test files of a bare
node --testscript, the tests of a vitest config that a script passes with--config, a file that a command substitution runs, and a file that a CI step runs with Bun. - The Vite
rootoption moves the default entries. An electron-vite config sets the main and preload entries. Default electron-builder script configs stay used. - Vercel functions in `a...
v3.31.0: per-package changedSince baselines, package cycles, stable dead-code finding ids, shrink-only baselines
Features
- Per-package
changedSincebaselines for monorepos. Map a workspace root to its own Git ref, for example"workspaces": { "changedSince": { "packages/web": "main", "packages/legacy": "release/2024.10" } }. For a mapped package,check,dead-code,dupesand the editor report findings only in files that changed since its ref. A combined run applies the map to those sections too. Unlisted packages and root files stay in full scope. A global--changed-sincereplaces the map for one run.audit,healthandsecurityignore the map. Write each key asfallow list --workspacesprints it. JSON reports list the applied refs inpackage_baselines, and the LSP sends the same rows aspackageBaselines.request_outcomesgets the entrypackage-baselines, with the valueappliedornot-applied. The value isnot-appliedwhen a key names no workspace or Git cannot resolve a ref. The run then reports every package in full scope and prints a warning. A malformed key or ref exits with code 2. To disable the map, use--no-package-baselinesfor one run orFALLOW_PACKAGE_BASELINES=falsefor every run of a process. In the editor, use the VS Code settingfallow.packageBaselines. The Node API optionnoPackageBaselinesand the MCP parameterno_package_baselinesofanalyzeandfind_dupesdo the same. When you save a dead-code baseline under the map, Fallow prints a warning that the file is partial. The baseline recordsscope_reasons, and a later run without that narrowing warns before it compares. A run that the map narrowed reportspackage-baselinesinbaseline_staleness.scope_reasons. The GitHub Action and the GitLab template then add--no-package-baselinesto their baseline re-read. Therecheck-baselinenext step also includes the flag. Thanks @M-Hassan-Raza for the contribution (#2969). package-cyclereports dependency cycles between workspace packages. Each workspace package is a node, and each resolved import from one package into another is an edge. The check finds a cycle such as@repro/a -> @repro/b -> @repro/aalso when the files form no file-level cycle. Packages in such a cycle cannot build in dependency order. Each finding inpackage_cycleslistspackagesandpackage_rootsin cycle order, and one example import per hop inedges. The example is the first runtime import of the hop, or else the first type-only import. Type-only imports are edges, because declaration builds still need an order. A hop with only type-only imports hastype_only: true. Declaredpackage.jsondependencies are not edges, and imports from test, spec, story, fixture and tooling config files do not count either. The rule ispackage-cycle(aliaspackage-cycles), and the default iswarn.--package-cyclesshows only this finding.// fallow-ignore-next-line package-cycleremoves one import or re-export statement from the package graph.// fallow-ignore-file package-cycle, or a per-file override tooff, removes every import of that file. A cycle goes away when you remove every import on one hop. When two packages share a name, the label isname (root). The first entry ofpackagesis the label that sorts first, so the baseline keys stay stable. A group of connected packages lists at most 20 cycles, or fewer on a very dense package graph. Each cycle in such a group hasgroup_truncated: true, and every output format shows a note.--group-by, workspace scope, per-file severity,--changed-sinceand diff scope use the file of the example import. Thecircular-dependenciescheck does not change. The MCPanalyzetool acceptsissue_types: ["package-cycles"]. The newfallow://tools/analyzeguide explains eachgroup_bymode. Thanks @azu for the report (#2955).- Dead-code findings have a stable
finding_idin JSON, LSP, MCP and SARIF output. Each dead-code finding, stale suppressions included, gets an id such asdc1:unused-export:81a349a3b9ea3b15. The id comes from the rule, the root-relative path and the symbol name. The line and the column are not inputs, so the id stays the same when you add lines, reformat a file or reorder declarations. A rename gives a new id. A second finding of one type with the same subject gets the suffix~1. Workspace scope,--changed-since,ignoreFindingsand baselines do not change the id of a finding that stays in the report. The field is optional in the JSON schema, soschema_versiondoes not change. LSP dead-code diagnostics add the id asdata.findingId. Security diagnostics do not have the key yet. The VS Code extension adds the quick fix "Copy Fallow finding id". The MCPanalyzeandcheck_changeddescriptions namefinding_id. They also say that an id absent from a scoped or differently configured run means unknown. The typed path, the CLI fallback and the Node bindings return the same ids. The per-flag detail ofanalyzemoved into thefallow://tools/analyzeguide resource. SARIF dead-code results addfallowFinding/v1topartialFingerprints.tools.fallow.fingerprint/v1andprimaryLocationLineHash/v1do not change, so GitHub code scanning keeps each open alert. Unlisted-dependency and duplicate-export results give one result per location and do not get the key.fallow report --fromon an older report gives no key. Security SARIF keepsfallowSecurity/v2. fallow dead-code --finding-id <id>reports only the findings you ask for. Repeat the flag or pass a comma-separated list. The filter runs after every other filter and after the baseline. JSON addsfinding_id_querywithrequested,found,missing,filtered,conclusiveandinconclusive_reasons. Whenconclusiveis true, a missing id means that the finding is fixed, suppressed or ignored by config. These options make the answer not conclusive: a scope,--changed-since, a workspace,--fileand an issue-type filter. Production mode,includeEntryExports, a baseline and a rule set tooffdo the same.filteredlists requested findings that still exist but that a filter removed. The answer also hasanalysis_fingerprint, a hash of the version, the config, the plugins, the detection options and the ignore files. The hash also covers the manifests, the tsconfig and jsconfig files and the plugin config files. Store the fingerprint with your decision. When a later fingerprint differs, treat a missing id as unknown. A malformed id exits with code 2. The MCPanalyzetool (finding_ids),DeadCodeOptions::finding_idsand the Node bindings (findingIds) take the same option.--fail-on-baseline-growthmakes a committed baseline shrink-only. Before, a change could add a finding and save the baseline again in the same commit.--baselineand--fail-on-stale-baselinethen passed. The new gate compares each loaded baseline with the same file at a base ref. It exits 1 when the baseline has a key that the base file does not have. It lists each new key per category on stderr. A renamed file gives a new key. In a dead-code baseline with line-free keys, one more occurrence of a key counts as growth, and a moved line does not.--baseline-base <ref>sets the base ref. Without it, the gate uses thefallow auditbase:--changed-sinceor--base, thenFALLOW_AUDIT_BASE, then the merge-base with the upstream or the remote default branch. A baseline that the base ref does not have is new, and the gate passes with a note. When Git cannot resolve the base ref, the gate exits 2, and the message namesgit fetchandfetch-depth: 0. The gate also exits 2 when the default base resolves toHEAD. In CI, pass--baseline-base origin/main. The gate applies to thedead-code,dupesandhealthbaselines and to the three baselines of the bare run. It also applies to thefallow auditflags--dead-code-baseline,--health-baselineand--dupes-baseline. The result is ingate_outcomes["baseline-growth"], whereobservedis the number of new keys.health --report-onlyand the review brief do not run the gate and say so on stderr. A command that loads no baseline rejects the flags. Thanks @tmak for the report (#2938).circularDependencies.ignoreLazyImportsremoves lazy edges from cycle detection. The option is off by default. When the option is on, cycle detection skips four kinds of lazy edge. These are animport()inside a function, a templateimport(), a lazyimport.meta.globand a worker URL. An edge that also has a static import stays. Fallow removes lazy edges before it counts the cycles of a group. Lazy cycles then can no longer fill the limit of 20 cycles and hide a static cycle. A top-levelawait import('./x')now loads eagerly in the module graph and counts in the startup import weight. A top-levelawait import()stays lazy in a Vue<script setup>block, a Svelte instance script or an Astro frontmatter. That code runs for each component instance. Thanks @tmak for the report (#2936).- A
!entry inignorePatternsrestores files that discovery skips. Before, no config could restore source in a directory that a built-in ignore matches, or in a hidden directory. Every detector skipped these files."!src/policy/coverage/**"now overrides the built-in**/coverage/**for that subtree only. A top-levelcoverage/output stays excluded."!.config/**"adds the hidden directory.configto discovery, with only the files that an exception matches. Fallow applies the built-in defaults first, then your patterns, then the!exceptions. A!entry that namesnode_modulesor.gitis a config error (exit 2). Theskipped-source-dotdirandexcluded-by-default-ignoremessages and the--explain-skippednote name the!form as the fix. Thanks @tmak for the report (#2940, #2452). ignoreDependenciesaccepts globs. An...
v3.30.0: flag retirement report, startup import weight, faster language server, Oxc 0.151
Features
fallow flags --retirementreports the flags that you can retire. Each row groups every site of one flag and lists the reasons to remove it. The reasons aresingle-read-site,test-only,literal-constant,identical-branches,empty-branch,guards-dead-codeanddefined-never-read. Theliteral-constantreason adds the new kindconstantfor a flag-prefixed module-levelconstwith a literal value, such asconst FEATURE_NEW_UI = true. A guard in the same module must test the flag. These flags are in the retirement report only, not infeature_flags[]. Thedefined-never-readreason also covers a Vercelflag()definition with an unused export, and an unused member of a flag registry enum. The reasons come from the code only, so they do not show the production state of the flag.- The retirement report works in every
fallow flagsformat and gives each flag an age from git. JSON adds a top-levelretirementobject, and human and markdown output add a "Retirement candidates" section. Compact addsflag-retire:lines, SARIF adds the rulefallow/flag-retirement-candidateat levelnote, and CodeClimate addsfallow/flag-retirementissues.--flag-age blame(the default),--flag-age pickaxeand--flag-age offset how Fallow measures the age.--reason,--min-age,--sort age|sites|nameand--topfilter and order the rows. Fallow removes no code. Every action hasauto_fixable: false.fallow explain flag-retirementdescribes the rule. fallow flags --retirement --flag-state <FILE>compares the code with a vendor flag export. The file is a local JSON file with one vendor-neutral schema, and Fallow reads it offline without credentials or network calls. The export adds the reasonsfully-rolled-out,archived-in-vendor,missing-in-vendorandvendor-only. Thevendor-onlyrows do not count insummary.distinct_flags. The newflags.vendorKeyPrefixconfig key removes a prefix from each vendor key before the match. An invalid file, or a file larger than 16 MiB, exits with code 2 and the error codeFALLOW_FLAG_STATE_INVALID.fallow flags --retirementcan gate CI on flag counts and flag age.--save-regression-baseline <PATH>writes a baseline with a newflagssection.--fail-on-regression --regression-baseline <PATH>exits with code 1 whendistinct_flagsgrows more than--tolerance.--max-flag-age <DAYS>exits with code 1 when a flag in scope is older than that limit. Each gate reports astatusofpass,exceededorskipped, and prints the result to stderr in every format. A run with--changed-sinceor--workspaceskips the regression gate and saves no baseline. Without--retirement, the regression options have no effect onfallow flags, and the command prints a warning.- The MCP
feature_flagstool and the Rust API can return the retirement report. The MCP tool adds theretirement,flag_stateandflag_ageparameters.FeatureFlagsOptionsadds aretirementfield. The CLI and the API build the report with the same engine function, so theretirementblock is the same on both. The regression gates stay CLI only. fallow flagsfinds more flag reads. The scan now reportsimport.meta.env.Xreads, and SDK calls with a registry member as the name, such asuseFlag(FLAGS.NewCheckout). It also finds flag reads inside a largerifor ternary test, such asif (process.env.FEATURE_X === 'true'). Flags in.jsfiles with JSX now match customsdkPatternsandenvPrefixes. More flag reads now have a guard, sodead_code_overlapcan find more unused exports.fallow list --entry-weightreports the startup import weight of each runtime entry point. The report counts the project modules and the source bytes that load before the entry runs. It splits the modules into eager, deferred and out of thread, and lists the packages on the startup path. The unit is source bytes on disk, so the value is not a bundle size. The output is human or JSON (entry_weightinfallow list --format json). The health score does not change.- An opt-in regression gate for the startup import weight.
fallow list --entry-weight --save-regression-baseline <PATH>writes the eager bytes, modules and packages of each entry into the baseline file. A later run with--regression-baseline <PATH>reports the change of each entry and the new eager packages. Add--fail-on-regressionto exit 1 when an entry grows more than--tolerance. A new entry never fails the gate.fallow dead-code --save-regression-baseline <PATH>now keeps the entry weights in the same file. fallow trace --pathandfallow vizshow dynamic imports. Each hop in the trace JSON has a newdynamicfield, and the human output tags such a hop[dynamic]. With--eager-only, the trace follows static value imports only. A new edge flag bit (2) marks a lazy edge. The focus view draws it with a short dash.- Built-in Waku plugin. The
wakuplugin activates from thewakudependency and follows the managed-mode file router. Every module under<srcDir>/pagesis an entry, except modules in_components,_hooksand_actionsfolders. The plugin readssrcDirfromwaku.config.*, withsrcas the default. Thanks @aheissenberger for the contribution (#2921). - The language server can parse the project before the first open. Set the initialization option
prewarmtotrue. Atinitialized, the server then loads the project session and parses the files, but analyzes and publishes nothing. The option is off by default and needs kept project sessions and a workspace root with apackage.json. --performancereports exact work counts, the time outside the pipeline and a span tree. The dead-code timings gain acountersobject with counts that do not change with the thread count or the machine. A standalonedead-coderun adds aprocessobject and aProcesssection with aWALLrow. The newspansarray gives the parent of each stage and marks concurrent spans. Health--performanceaddsgit_log_bytes, and the parse cache load gets its ownparse_cache_load_msfield.- Runtime hot paths show where speed work gives the largest gain. Each entry in
runtime_coverage.hot_pathsnow has anoptimization_targetblock. The block holdscost_score,cost_basis,inner_iterations_per_call,cognitive,cyclomaticandline_count. Comparecost_scoreonly between hot paths with the samecost_basis. The human output,--explainand the MCPget_hot_pathstool show the same fields.
Performance
- Import resolution, stylesheet scans and coverage remaps do less work. Each specifier in a file now resolves once, not once for each imported binding. A stylesheet is masked for comments one time, and the
health --csstoken scans count lines in one pass. Runtime coverage indexes each source-mapped script one time and finds each offset with a binary search. The findings do not change. - The language server keeps its project session between saves. A save now parses only the files that changed, and the other modules come from memory. A change to a config input, such as
package.json, a lockfile, atsconfigfile or a rule pack, loads the session again. On a platform without a file change time, such as Windows, each run reads the persisted parse cache as before. The server keeps sessions while their estimated memory is at most 512 MB in total. SetFALLOW_LSP_REUSE_SESSION=0to load a new session on each run. - The language server does less work for each analysis run. It publishes only the diagnostics that changed, and converts diagnostic columns in linear time. It reads an open file only when the buffer can differ from disk. Under autosave, a newer event cancels the run in flight, and the run after a cancelled one always finishes and publishes.
- Typed MCP tool calls share parsed modules. A later call on the same unchanged files takes the modules from memory. For the sequence
analyze,find_dupes,check_health,trace_file, run two times, the parse passes go from 6 to 1. The answers do not change. The store keeps at most 4 file lists and about 512 MiB of parsed modules. It does not keep a project with more than about 40 MiB of source. To stop the reuse, setFALLOW_MCP_WARM_SESSION=0. fallow flagsparses each file once with a customflagsconfig. Before,sdkPatterns,envPrefixesorconfigObjectHeuristicsmade the command parse every file a second time. The command also matches guarded flags to unused exports by file and line. Thedead_code_overlapoutput does not change.- Fallow starts fewer git processes. A run without
--diff-file,--diff-stdinorFALLOW_DIFF_FILEno longer callsgit rev-parsefor the diff base directories. The Impact project identity reads the git common directory and the toplevel with one call in a work tree. - The VS Code extension no longer searches the workspace for manifests at startup. It starts when the workspace root has a
package.jsonor a Fallow config file. It also starts when a JavaScript, TypeScript, Vue, Svelte, Astro or MDX file opens. A monorepo folder with no rootpackage.jsonnow starts the extension when the first source file opens. fallow vizpages load and respond faster. The page parses only the file list, the edges and the summary at start. On the Fallow repository, the data parsed at start drops from 2.49 MB to 64 KB. The HTML file drops from 2.8 MB to 1.9 MB. A hover on the treemap now makes 3 draw calls, not one call per tile. The page shows its frame before the script runs, so the layout does not shift.
Changed
fallow flagsgivesmediumconfidence to generic SDK names without a flag import. The namesisEnabled,getValueanduseFeaturealso occur in libraries that are not flag SDKs. A call to one of these names now has `confiden...
v3.29.0: raw V8 coverage, parse-error gate, complexity rule levels, deprecated export consumers
Features
health --coveragereads raw V8 coverage. Give it aNODE_V8_COVERAGEdirectory or one V8 coverage JSON file, and CRAP scores use measured coverage without a conversion step. The dumps of all test processes add up. A transpiled script maps back to its sources through the source map in the dump.health.coverage,FALLOW_COVERAGE,fallow auditand the MCP tools take the same inputs. The newsummary.coverage_input_formatfield isistanbulorv8(#2906).- A run can fail when fallow cannot parse a source file. The new
--fail-on-parse-errorflag and thefailOnParseErrorconfig key add aparse-errorgate ondead-code,health,auditand barefallow. The gate is off by default, andgate_outcomes["parse-error"]names each file with its parser error count. Thanks @fpresta0607 for the report (#2727). - Complexity findings can warn without failing the run. The new rules
complexity-cyclomatic,complexity-cognitiveandcomplexity-crapaccepterror(the default),warnoroff, also per file throughoverrides[].rules. Each complexity finding in the JSON output gets an optionaleffective_severity. Thanks @jwenger-notion for the report (#2783). fallow dead-codecan list the files that still use@deprecatedexports. The opt-indeprecated-export-in-usefinding gives aconsumer_count, a sample of up to 10 consumers and the deprecation message. Turn it on with"deprecated-exports-in-use": "warn"inrulesor--deprecated-exports-in-use. An unused deprecated export now hasdeprecated: trueon itsunused-exportfinding (#2598).- The review brief shows how many CODEOWNERS owner groups a change touches.
fallow reviewandfallow audit --briefget anownershipsection when the project has a CODEOWNERS file. It gives owner group counts, unowned changed files and independentslices. The briefschema_versionmoves from 10 to 11 (#2599). - Module Federation credits
sharedpackages and runtime remotes, and traces name the config. A package thatsharednames is no longer an unused dependency (#2794). LiteralregisterRemotesandloadRemotecalls register their remotes in a file that imports them from@module-federation/runtimeor@module-federation/enhanced/runtime(#2795).--trace-fileand--trace-dependencyname the Federation config and key in a newsourcesarray (#2796). --changed-sincestates how many analyzed files it kept. Thechanged-sinceentry ofrequest_outcomesnow hasscope_size, so a change to a README only givesscope_size: 0. The GitHub Action, the GitLab template, the PR and MR comments and the MCP warning state the empty scope (#2800).- A foreign baseline names the command that wrote it.
baseline_stalenessnow hassaved_bynext tounrecognised_format: true. The GitHub Action has a newbaseline-saved-byoutput (#2801). - Security candidates record strict origin checks.
fallow securityrecords anorigin-equality-guardcontrol when a file on the import trace compares.originstrictly with a known string and exits on a mismatch. The control is a hint for verification, not proof. It does not suppress a candidate or lower its severity.
Changed
- The rule, not the band, sets the CI level of a complexity finding.
errorgives::error, SARIFerrorand CodeClimatemajor.warngives::warning, SARIFwarningand CodeClimateminor. The band stays in the title and the message (#2783). --fail-on-issuesalso raiseswarncomplexity findings toerror. This applies tofallow healthand barefallow, also through--ci.fallow auditkeeps its own verdict (#2824).- JSON output always states whether the run passed. Bare
fallow,dead-code,check,health,securityandauditnow always includegate_outcomeswith the default rule of the command and itsstatus. No exit code changes. - The GitHub Action flags the legacy summary renderer, and a failed native render no longer falls back to it. For fallow before 3.4.2, the step log shows a notice and the job summary shows a footnote. The legacy renderers get no new finding types. On 3.4.2 or later, a failed native render writes a warning.
Bug fixes
- Dockerfiles with non-ASCII text no longer crash the run. A short line with a multi-byte character, for example in an embedded SQL seed script, now counts as an ordinary line. Thanks @ga-h-usuba for the report (#2896).
- CI formats state the rule severity of each dead-code finding. A finding with rule
errornow gives::erroringithub-annotations, and SARIF and CodeClimate read the per-fileoverrides[].rulesseverity. Error annotations come first, so themax-annotationscap keeps them. Thanks @jwenger-notion for the report (#2782). - Severity levels agree across commands and saved reports.
fallow dead-codenow uses theoverrides[].rulesentry for catalog and dependency-override findings, the same asfallow audit. Prop-drilling, thin-wrapper and duplicate-prop-shape findings show as SARIFwarningat most, because they never fail the run (#2826).fallow report --fromkeeps the saved level of each finding (#2827). - Nuxt
autoImportscredits global components, local layers and OG image templates. Global components and@nuxt/contentcomponents are now entry points, also when a package layer such asdocusregisters@nuxt/content(#2847, #2851). Fallow now credits OG image templates that a string names (#2849). Local layers are part of the project, and auto-import names from#componentsnamespace imports now credit their files (#2752). A name now credits only the files of the app that uses it and of its layers. - Module Federation reads more config shapes. Fallow now reads options behind an
export const, a spread,Object.assign, a wrapper call or a relative import (#2757). Anexposestarget in a sibling workspace is an entry point, and fallow reads runtime calls in.vueand.sveltescripts (#2757, #2876). A shape that fallow cannot read records aplugin-config-unreadablediagnostic. - Bundler entries resolve like the bundler resolves them. A bare rollup, rolldown or vite
inputsuch asmy-lib/clientcredits its package. An entry without an extension matches the file or the directory index (#2753). Fallow now reads webpack configs inconfig/,build/orwebpack/, and applies rspackcontextand rsbuildrootto entries (#2753). Fallow reads an absolute config path under the project root as that path (#2806). - Storybook
storiespatterns resolve against the.storybook/directory. A pattern such as../src/**/*.mdxnow matches, and@(ts|tsx)groups work (#2831). A central docs app now credits stories in a sibling workspace, and fallow now reads the object form{ directory, files, titlePrefix }(#2842). - Save and report files stay inside allowed directories. The save flags,
--output-fileand--sarif-fileexit 2 when the resolved file is outside the allowed directories (#2805, #2861). These are the project root, its Git work tree, the CI workspace,RUNNER_TEMPand the system temp directory.-o /dev/nullstill discards a report, and-o NULnow discards it on Windows (#2877). When.fallowresolves outside the project, the run does not use the cache and prints a note (#2861). - Flags that did nothing now exit 2.
--baselineand--save-baselineon a subcommand without a baseline, such asfallow list, now exit 2 (#2802, #2807).dupes,healthand a bare run without dead-code reject--sarif-fileand point to--format sarif --output-file(#2861, #2877). - Bare
fallownow applies dupes and health baselines. Usefallow --dupes-baseline <file>andfallow --health-baseline <file>.--fail-on-stale-baselinenow checks all three baselines. - Export tags attach to the right export. A JSDoc block before a decorator now attaches to the decorated export (#2835). In code without semicolons, a tag such as
@publicno longer applies to later exports. - A re-export through an unresolved import counts as used when a file imports the name. Before, the re-exported name was also an unused export. A re-exported name that no file imports is still an unused export (#2870).
- Inline suppressions work for component events. A
fallow-ignore-next-lineorfallow-ignore-filecomment forunused-component-emit,unused-component-input,unused-component-outputorunused-svelte-eventnow drops the finding. - Template complexity counts operators inside a template literal. In an Angular, Vue or Svelte template, a ternary,
&&,||or??inside${}now adds complexity (#2798). fallow auditshows a dependency finding only when its manifest changed. The changeset must touch thepackage.jsonor the catalog file that declares the dependency. An edit that only removes a tag such as@expected-unusedor@publicnow counts the finding as introduced. The MCPaudittool treats findings in renamed files as inherited, the same as the CLI.- The MCP tools and the Node API match the CLI. A bad
FALLOW_DIFF_FILEorFALLOW_CHANGED_SINCEnow gives anot-appliedentry inrequest_outcomes, notisError(#2799). Clone groups across workspaces and hidden duplicate exports now follow the CLI. The four MCP tools that can write a baseline declarereadOnlyHint: false, andcode_executerefuses the save parameters (#2755). - Health reports cover more repository layouts.
health --hotspotsworks when the project root is a subdirectory of the Git repository. A branch without commits gives ahotspots-skippeddiagnostic with the causeno-commits(#2803). With--type-aware-require complete,gate_outcomesofhealthand `a...
v3.28.0: baselines carry a kind, request outcomes on every surface, Module Federation and Nuxt reads
Features
- Every saved baseline now says which command wrote it.
--save-baselinewrites a top-levelkindfield withdead-code,dupesorhealth. Older baselines without it still load, and an older fallow still loads a baseline saved by this release. - Pointing
--baselineat the wrong command's file is no longer silent. The run prints a warning with both command names and the path, and the file suppresses nothing. In the JSON output,baseline_staleness.unrecognised_formatistrue. The same warning appears in the pull-request comment, the merge-request note and the Check Run. The GitHub job summary shows the file path through the newbaseline-pathoutput, andfallow auditprints one warning per baseline it cannot use. - The JSON output says what narrowed a run.
baseline_staleness.scope_reasonslists the reasons, for examplechanged-sinceorproduction. A narrowed run with a baseline also gets arecheck-baselineentry innext_steps, and the barefallowcommand gets it too. request_outcomesis now in the output offallow flags,fallow suppressionsandfallow security. The first two report theirchanged-sinceentry, andfallow security --sarif-filegets asarif-fileentry.- An empty diff no longer looks like a clean project. When a diff filter applies, its
request_outcomesentry has ascope_sizewith the number of added lines. A diff with zero added lines reportsscope_size: 0. The Action, the GitLab template, the MCP tools and the pull-request comment then all say that the clean result covers nothing. hotspots-skippednow has acause:not-a-repository,invalid-sinceorchurn-file-unreadable.- A config that a plugin cannot read shows up in
workspace_diagnostics. When a Module Federationexposesorremotesis not a static object literal, the run recordsplugin-config-unreadablewith the plugin, the key and the reason. It setsdegrades_analysis, so the Action and the GitLab template print it in their degraded-inputs warning. Nuxt has a related case: acomponents:orimports:shape that fallow cannot model. Such a project recordsplugin-effect-not-modeled, and that entry is silent. - The JSON output and both CI integrations report a run that widened to the whole project. They also report a
--sarif-filethat could not be written, and a format that cannot group results with--group-by. - fallow reads Module Federation
exposesandremotes. It readsmodule-federation.config.*and inline plugin options in webpack, rspack, rsbuild, vite and Next.js configs. The plugin call can sit anywhere in the config: in a nested plugin array, in a variable, undertools.rspack.pluginsor in awebpack(config)hook. Options held in aconstin the same file are read, and so is the array form ofexposes. Eachexposestarget becomes a runtime entry point, and eachremotesalias counts as a provided dependency for the code under that config's directory. - Nuxt
autoImportscredits aglobal/orislands/component under the name Nuxt gives it. Each workspace root now gets its own verdict from its own config.components: true,imports: {}andimports: { dirs: [] }count as the Nuxt default. A name imported or re-exported from#componentsor#importsmarks its file as used, and#layers/<name>/works as a path alias.
Changed
fallow dead-codeno longer exits 2 when the baseline belongs to another command. It warns and continues.--fail-on-stale-baselinefails the run on such a file. Saving over a baseline of anotherkindis refused with exit 2.- The GitHub Action and the GitLab template log why a JSON read failed. The cause goes to the debug log, and a green run gets no extra line.
fallow security --basewarns when it cannot map the analysis root into the base worktree.
Bug fixes
- A package name in a bundler
entryis credited as a dependency. A value likereact-hot-loader/patchorwebpack-hot-middleware/client?reload=truein a webpack, rspack or rsbuildentryused to become an entry pattern that matched no file. The package then showed up as unused. - The MCP
auditanddecision_surfacetools detect the base ref again. This regressed in 3.1.0. Thanks @codingthat for the report and the reproduction (#2699). autoImports: trueworks in a Nuxt project that turns the scan off.components: false,imports: { scan: false }and the other scan-off shapes no longer count as a custom layout, so unused convention files are reported. Thanks @Tsuyoshi84 for the report (#2695).
Upgrade notes
- A CI job that points
--baselineat another command's file now fails when--fail-on-stale-baselineis set. Give each command its own baseline file. - A Nuxt project with
autoImports: trueand one of the scan-off shapes will see newunused-filefindings. Add a file toentrywhen it is used in a way fallow cannot see. - If you switch on
workspace_diagnostics[].kind, handle the two new kinds. Every new JSON field is optional.
Full Changelog: v3.27.0...v3.28.0
v3.27.0: every CI gate can fail the job, baseline staleness in CI, gate results in MCP
Features
- An armed gate reports its result in the JSON output. A run that arms a gate gets
gate_outcomes, an object keyed by gate name. Each entry hasstatus(pass,warn,failorskipped) andenforced. Where a comparison happened, you also getobserved,thresholdandthreshold_label. Only the gates the run armed appear in the object. Adead-coderun can exit 1 on the default severity rules with no object at all. The key set is open, and a gate name you do not recognise means "some gate". (#2680, #2681, #2683, #2685) - The JSON output reports a stale baseline, and CI can fail on it. 3.26.0 printed the stale-baseline warning and
--fail-on-stale-baselineon stderr only, and--quietremoves that output. The GitHub Action and the GitLab template both run with--quiet.dead-code/check, the bare run,dupesandhealthreport onebaseline_stalenessobject, in grouped output too. It holds the entry counts,change_scoped, the warning result andgate_trips. Both integrations show a stale baseline as a warning and in the job summary. On a pull request the main run covers changed files only. The integrations therefore re-read the baseline once over the whole project. That re-read took 0.11s with a cache and 0.24s with type-aware analysis on an 870-file TypeScript project. Whether a stale baseline fails the job depends on the newfail-on-stale-baselineinput and theFALLOW_FAIL_ON_STALE_BASELINEvariable. The pull-request comment and the merge-request note do not include the warning yet. Thanks @cloud-walker for the report, which traced the gap through the action scripts. (#2673) min-scoreis an Action input and a GitLab variable. Before this release you could set it only throughargs:/FALLOW_ARGS.--min-scoreimplies--score, so the integrations add--complexitywhen no health section input is set. That keeps the annotations, the SARIF upload and the pull-request comment populated. The CLI turns its own findings rule off for such a run, and the integrations do the same, which leaves the decision to the score.min-scoreandmin-severityapply tocommand: healthand exit 2 elsewhere. (#2682)- A run that analyzed no source file reports it. The run prints a warning and passes by default.
fail-on-empty-analysis: true(FALLOW_FAIL_ON_EMPTY_ANALYSISon GitLab) makes it fail. The JSON output has ano-source-files-analyzeddiagnostic and the newworkspace_diagnostics[].degrades_analysisfield. When findings cover less than the whole project, the integrations print one combined warning with the diagnostic kinds and their counts. (#2686) - An MCP tool result reports what each gate concluded. The tools run the CLI with
--quietand turn exit 1 into a successful result, which hid every gate result from the agent. The rootwarningsarray reports a stale baseline together with the re-save remedy. It also lists every gate that concludedfailorwarn, with its numbers. A run that covered less than the whole project gets one entry too. The subprocess, Code Mode and typed routes all do this.find_dupeswith athresholdtakes the route that can evaluate it. A response with nothing to report is unchanged. (#2676) fallow report --fromshows the gate results in CI. It prints them as a notice annotation and as a line in the job summary, the pull-request comment and the merge-request note. The line is informational and never fails a step. (#2684)
Changed
security-gatefails the job independently offail-on-issues. In both integrations the security branch sat inside thefail-on-issuesconditional, andfail-on-issues: falsemeant that branch never ran.- The Action's inline
Check thresholdstep is gone. Its logic moved into the analyze step, and thegates-failedoutput names the gates that decided the result. - In combined mode the duplication threshold does not fail the run, and the JSON output has
enforced: falsefor it. Standalonedupesexits 1 as before. The default GitLab job forwards the threshold in combined mode, and that pipeline prints a warning with the reason. --fail-on-stale-baselinechanges one field in the output. That field isgate_outcomes["stale-baseline"].enforced. Nothing inbaseline_stalenessdepends on the flag, not evengate_trips.health --report-onlysetsenforced: falseon every gate it evaluated.- One stale baseline can produce two lines for a pull request. The Action's warning comes from its whole-project re-read. The gate line from
fallow reportdescribes the scoped run, where the gate fails nothing. - No
schema_versionin the JSON output changed.gate_outcomesandworkspace_diagnostics[].degrades_analysisare additive and optional.
Bug fixes
fail-on-regression,threshold,min-severityand the security gate fail the job. All four were documented as gates. Each one reported its result on stderr, which--quietremoves. Both integrations also drop the exit code when stdout parses as JSON. The integrations readgate_outcomesinstead. A gate fails the build when itsstatusisfailandenforcedis true, and only when the input that owns it was set. A flag passed throughargs:prints a warning and cannot overridefail-on-issues: false. (#2680, #2681, #2683, #2685)- Every failing gate is reported before the step exits. Both integrations stopped at the first failure, and a run with a tripped gate and findings reported one of the two. They print every reason, write the outputs and artifacts, and exit once. The security gate keeps exit 8, which outranks the generic 1.
- The duplication
thresholdapplies to the bare command on GitHub. The Action forwarded it oncommand: dupesonly.
Upgrade notes
- If you set
fail-on-regression,threshold,min-severityor a security gate and relied on the job passing, expect failures. Withsecurity-gateandfail-on-issues: false, unsetsecurity-gateto keep the old behaviour. - Remove
--fail-on-stale-baselinefromargs/FALLOW_ARGS, delete any separate unscoped gate step added as a workaround, and set thefail-on-stale-baselineinput. fail-on-stale-baselinewith nobaselineset, or onfixorsecurity, exits 2.- Do not point
baselineandsave-baselineat the same file. The run saves before it compares, and such a baseline can never have a stale entry. The integrations warn about it. - If a workflow references the
Check thresholdstep by name (continue-on-error,steps.*.outcome), point it at the analyze step and thegates-failedoutput. - With a pinned fallow older than 3.27.0, the integrations use the fields that version writes. A gate with no field in that version passes, and the integrations print one warning.
- If you import
npm/fallow/typesin TypeScript,HealthBaselineStalenessis nowBaselineStaleness. The old name remains as a deprecated alias.
Full Changelog: v3.26.0...v3.27.0
v3.26.0: stale-baseline gate, built-in exclusion diagnostics, rule overrides everywhere
Features
--fail-on-stale-baselineturns a rotting baseline into a failing build. The new global flag exits 1 when a loaded--baselinehas an entry that matched nothing this run. It works ondead-code/check, the bare run,dupesandhealth, in every output format. A run that cannot judge the baseline skips the flag and says so on stderr. That covers a scoped run,health --report-only,auditanddecision-surface. JSON output is unchanged. (#2637)dead-code --baselinewarns when the saved baseline has gone stale. The wording matches whathealth --baselinehas printed since 3.12.0. When a quarter or more of the entries match no current issue, the run says so and points at the re-save command. Exit codes are untouched. Thanks @cloud-walker for the report. (#2627)- A run can say which built-in ignore pattern removed source files. The walk assigns every excluded candidate to the pattern that removed it. It records one
excluded-by-default-ignoreentry per pattern inworkspace_diagnostics[]. The entry has the glob, an exact file count, the matched-directory count and a project-relative anchor.--explain-skippedprints the breakdown oncheck,dead-code,auditand the default run. Without the flag, only a run that discovered no source files at all prints a two-fact warning that points at the flag. (#2638) - Oxfmt is a built-in plugin. The plugin marks
oxfmt.config.tsand its siblings always-used, and credits static imports from those configs. Thanks @uzosrc for the request. (#2614) - fallow now recognizes the Expo Router route exports
SuspenseFallback,getNavOptionsandgenerateMetadata. Thanks @tilgovi for theSuspenseFallbackpatch (#2618).
Changed
- The built-in
buildexclusion now matches at any depth (**/build/**), which lines up with thedistandcoveragedefaults. Nested build output in a monorepo no longer produces unused-file, unused-export, duplication and health findings. The changelog states five consequences with the remedy for each. Among them, the walk now skips hand-written source in a nestedbuild/directory. Thanks @michalius for the report. (#2622) - Unused- and unlisted-dependency checks are faster on large workspace monorepos. The run resolves the owning workspace of each file once per analysis. On a repository with about 18,000 files and 800 workspaces the check went from roughly 30 seconds to 6 seconds. Thanks @Freakazo for the patch (#2624).
Bug fixes
- Rule severity, including per-path
overrides[].rules, applies everywhere fallow reports. Inline editor diagnostics now match the result setfallow dead-codereports. The programmatic runtime behind the MCP tools and the Node bindings matches it, and so do the decision surface and--type-awareCLI runs. The language server also watches every config file name the loader accepts, so editing.fallowrc.jsonrefreshes diagnostics. Thanks @JasonHassold for the report. (#2621, #2636) fallow auditno longer hangs on a sparse checkout of a large monorepo. The audit filters the committed tree through the sparse cone before it touches any blob. A blobless partial clone therefore no longer fetches every out-of-cone blob. Thanks @AndranikSimonian for the report and the reproduction. (#2615)- Istanbul statement coverage no longer charges a nested function body to the function that contains it. fallow assigns each statement to the innermost function whose body encloses it. For a function that lexically contains another function,
coverage_pct,coverage_tierandcrapnow describe its own body. Thanks @ntdkhang for the precise reproduction. (#2620) - The config JSON Schema declares the JSONC dialect fallow parses (
allowComments,allowTrailingCommas). An editor built on the JSON language service stops reporting trailing commas in.fallowrc.json. Thanks @michalius. (#2623) dupes --baselinewarns about a partially stale baseline. A project with nothing left to compare gets no baseline warning. A scopeddead-code --baselinerun no longer advises a re-save that would gut the baseline. Withhealth --production --baseline, a fresh baseline no longer counts as stale. (#2627, #2637)- The graph cache is reused again on a project where a dynamic-import pattern matches no files. Thanks @Freakazo for the patch (#2626).
Upgrade notes
- If a nested directory named
buildholds hand-written source, rename or move it, or analyze it as its own project withfallow --root <dir>.ignorePatternshas no negation. - Agent-facing JSON from the MCP tools and the Node bindings now applies default-off rules such as
private-type-leaks. That matches what the CLI has always shown.
Full Changelog: v3.25.0...v3.26.0
v3.25.0: positional path scope, runtime coverage joins callbacks and object members
Scope any file command to a path
Bare fallow, check, dupes, health, audit, security, fix, list and
similar-code now take an optional positional path:
npx fallow src/components
npx fallow health src/api/client.ts
npx fallow fix src/legacyfallow still builds the whole-project graph, and every cross-file fact stays
sound. Only the reported findings are limited to the path. fix plans and
applies only the fixes that touch scoped files.
Resolution is root-first for a bare relative path. fallow reads ./ and ../
as claims about the current directory. A missing path, or one outside the root,
gets an exit-2 error that says what to do. The run stops there instead of
analysing the whole project. The scope acts as one more
workspace root next to --workspace, and it intersects with --changed-since
and --diff-file. audit limits the set of changed files it considers, which
keeps its result and its base attribution consistent. Its base pass stays
unscoped, because that pass runs in another worktree.
Runtime coverage joins far more of your code
fallow coverage analyze --cloud matches cloud runtime rows against a static
index. That index came from the health and complexity pass, which lists
declarations and bindings and nothing else. The runtime instrumenter names more
shapes than that. An arrow passed to a call takes the name of its callee:
rows.map(...), sqliteTable("t", {}, (table) => [...]),
.references(() => ...). An object-literal method, a getter or setter, and a
function assigned to a member get their own names too. For all of those, the
index held nothing to match. Their rows went into
cloud_functions_unmatched and never appeared in findings or hot_paths. In a
typical service they are the highest-traffic functions, and the top of the
hot-path list was whatever declaration the pass happened to list.
The index now holds every function the instrumenter would name. It resolves them
through the same walker the static inventory upload uses, which makes the
identity match the stable_id the cloud stores. A function known only by the
callee it was passed to is marked as a callback. The text fallow prints for it
names the call site ("Callback passed to map; ...") instead of a declaration
that does not exist.
The static function inventory got the same fix. An object-literal method, a
function-valued property, and a getter or setter kept the (anonymous_N)
placeholder. So did a function assigned to a member expression, and an anonymous
export default. The instrumenter names those functions run, execute,
get closed, rollback and default. Both sides now agree, and an
uploaded inventory entry and the runtime row for one function share a single
identity.
The inventory blob reports its own size guard
fallow coverage upload-inventory --with-callers has always capped the
importer-edge map per callee. The cap keeps a pathological fan-in from bloating
the upload, and it shortened the list silently. Nobody reading the body could
tell a function that genuinely has as many importers as the cap from one that
lost some.
The version 3 body has a callerEdgeLimits header next to callerEdges. The
header holds maxSitesPerFunction, maxSymbolsPerSite and truncatedFunctions.
The command prints a warning that says how many functions lost importer sites.
The header appears only when callerEdges does, and a version 1 or version 2
body keeps exactly the shape it had.
Windows paths read correctly in check and health
Both human renderers printed the separator of the platform. A Windows user read
src\a.ts while dupes, list, fix and every JSON output said src/a.ts.
The split between the dimmed directory and the bold filename keys on /, so the
whole path lost that emphasis as well. Every path those two renderers put on
screen normalises the way the rest of the CLI already did. Path handling on disk
is untouched.
Install
npx fallow@3.25.0
npm install --save-dev fallow@3.25.0
cargo install fallow-cli@3.25.0Full Changelog: v3.24.1...v3.25.0