fallow 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -162,16 +162,32 @@ test("resolveTypeAwareCompanion accepts only an exact matching package", (t) =>
162
162
  test("childEnvironment marks launcher-wired companions as npm-wrapper", (t) => {
163
163
  const { childEnvironment } = require(RUN_BINARY);
164
164
  const previousBin = process.env.FALLOW_TYPE_AWARE_BIN;
165
+ const previousScript = process.env.FALLOW_TYPE_AWARE_SCRIPT;
165
166
  delete process.env.FALLOW_TYPE_AWARE_BIN;
167
+ delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
166
168
  t.after(() => {
167
169
  if (previousBin === undefined) delete process.env.FALLOW_TYPE_AWARE_BIN;
168
170
  else process.env.FALLOW_TYPE_AWARE_BIN = previousBin;
171
+ if (previousScript === undefined) delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
172
+ else process.env.FALLOW_TYPE_AWARE_SCRIPT = previousScript;
169
173
  });
170
174
 
171
- const env = childEnvironment("3.8.0", () => "/tmp/fallow-type-aware.mjs");
175
+ const env = childEnvironment("3.8.0", () => "/tmp/fallow-type-aware.mjs", {
176
+ platform: "linux",
177
+ execPath: "/usr/bin/node",
178
+ });
172
179
  assert.equal(env.FALLOW_TYPE_AWARE_BIN, "/tmp/fallow-type-aware.mjs");
180
+ assert.equal(env.FALLOW_TYPE_AWARE_SCRIPT, undefined);
173
181
  assert.equal(env.FALLOW_TYPE_AWARE_BIN_SOURCE, "npm-wrapper");
174
182
 
183
+ const windowsEnv = childEnvironment("3.8.0", () => "C:\\pkg\\fallow-type-aware.mjs", {
184
+ platform: "win32",
185
+ execPath: "C:\\Program Files\\nodejs\\node.exe",
186
+ });
187
+ assert.equal(windowsEnv.FALLOW_TYPE_AWARE_BIN, "C:\\Program Files\\nodejs\\node.exe");
188
+ assert.equal(windowsEnv.FALLOW_TYPE_AWARE_SCRIPT, "C:\\pkg\\fallow-type-aware.mjs");
189
+ assert.equal(windowsEnv.FALLOW_TYPE_AWARE_BIN_SOURCE, "npm-wrapper");
190
+
175
191
  // No resolvable companion: the environment passes through untouched.
176
192
  const untouched = childEnvironment("3.8.0", () => undefined);
177
193
  assert.equal(untouched, process.env);
@@ -181,14 +197,19 @@ test("childEnvironment marks launcher-wired companions as npm-wrapper", (t) => {
181
197
  test("childEnvironment leaves a user-set override unmarked", (t) => {
182
198
  const { childEnvironment } = require(RUN_BINARY);
183
199
  const previousBin = process.env.FALLOW_TYPE_AWARE_BIN;
200
+ const previousScript = process.env.FALLOW_TYPE_AWARE_SCRIPT;
184
201
  process.env.FALLOW_TYPE_AWARE_BIN = "/opt/custom-sidecar";
202
+ process.env.FALLOW_TYPE_AWARE_SCRIPT = "/opt/custom-sidecar.mjs";
185
203
  t.after(() => {
186
204
  if (previousBin === undefined) delete process.env.FALLOW_TYPE_AWARE_BIN;
187
205
  else process.env.FALLOW_TYPE_AWARE_BIN = previousBin;
206
+ if (previousScript === undefined) delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
207
+ else process.env.FALLOW_TYPE_AWARE_SCRIPT = previousScript;
188
208
  });
189
209
 
190
210
  const env = childEnvironment("3.8.0", () => "/tmp/fallow-type-aware.mjs");
191
211
  assert.equal(env, process.env);
212
+ assert.equal(env.FALLOW_TYPE_AWARE_SCRIPT, "/opt/custom-sidecar.mjs");
192
213
  assert.equal(env.FALLOW_TYPE_AWARE_BIN_SOURCE, undefined);
193
214
  });
194
215
 
@@ -77,7 +77,7 @@ test("resolveSentinelPath falls back to FALLOW_VERIFY_CACHE_DIR when platform pk
77
77
  const cacheDir = mkTmp();
78
78
  try {
79
79
  const result = resolveSentinelPath({
80
- platformPkgDir: "/dev/null/not-a-dir",
80
+ platformPkgDir: path.join(cacheDir, "missing-platform-package"),
81
81
  packageName: "@fallow-cli/darwin-arm64",
82
82
  env: { FALLOW_VERIFY_CACHE_DIR: cacheDir },
83
83
  });
@@ -220,12 +220,19 @@ test("resolveSentinelPath honors injected isWritable + ensureDir for full test i
220
220
  test("resolveSentinelPath skips cache-dir-env when ensureDir fails for it", () => {
221
221
  // FALLOW_VERIFY_CACHE_DIR points at a non-creatable path, XDG points at a
222
222
  // creatable one. Confirm the resolver moves past the env override.
223
+ //
224
+ // A directory nested under a regular FILE is the portable way to make mkdir
225
+ // fail: `/dev/null/inside/a/file` only refuses on POSIX, and on Windows the
226
+ // recursive mkdir succeeds against the current drive, which both defeats the
227
+ // assertion and creates C:\dev outside any temp directory.
223
228
  const homeDir = mkTmp();
229
+ const blocker = path.join(homeDir, "not-a-directory");
230
+ fs.writeFileSync(blocker, "");
224
231
  try {
225
232
  const result = resolveSentinelPath({
226
233
  platformPkgDir: undefined,
227
234
  packageName: "@fallow-cli/darwin-arm64",
228
- env: { FALLOW_VERIFY_CACHE_DIR: "/dev/null/inside/a/file" },
235
+ env: { FALLOW_VERIFY_CACHE_DIR: path.join(blocker, "inside", "a", "file") },
229
236
  homedir: homeDir,
230
237
  platform: "darwin",
231
238
  });
@@ -239,10 +239,20 @@ test("verifyBinaryAt uses the embedded production public key", () => {
239
239
  }
240
240
  });
241
241
 
242
+ // Mirror binaryTargetsForPlatform: the suffix comes from the platformId under
243
+ // test, never from the live process.platform. A `dirOverride` run without an
244
+ // explicit platformId is labelled "test-platform" by
245
+ // resolvePlatformPackageForVerify, so the binary it looks for is plain `fallow`
246
+ // on every host -- a fixture keyed off process.platform writes `fallow.exe` on a
247
+ // Windows host and the verify then finds nothing.
248
+ function extForPlatformId(platformId) {
249
+ return typeof platformId === "string" && platformId.startsWith("win32") ? ".exe" : "";
250
+ }
251
+
242
252
  function makePlatformDir(privateKey, options) {
243
253
  const opts = options || {};
244
254
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
245
- const ext = process.platform === "win32" ? ".exe" : "";
255
+ const ext = extForPlatformId(opts.platformId);
246
256
  for (const base of ["fallow"]) {
247
257
  const binaryPath = path.join(dir, `${base}${ext}`);
248
258
  const content = Buffer.from(`mock ${base} contents`);
@@ -328,6 +338,42 @@ test("verifyInstalled with dirOverride returns ok when every binary verifies", a
328
338
  assert.equal(result.package, "<override>");
329
339
  });
330
340
 
341
+ // binaryTargetsForPlatform reads windows-ness off the platformId so a Windows
342
+ // verify can be synthesized anywhere. Nothing exercised that, which left the
343
+ // `.exe` target unverified on Linux CI and unverified on Windows too.
344
+ test("verifyInstalled verifies the .exe target for a win32 platformId on any host", async (t) => {
345
+ const { privateKey, rawPub } = makeKeypair();
346
+ const platformId = "win32-x64-msvc";
347
+ const dir = makePlatformDir(privateKey, { platformId });
348
+ t.after(() => cleanup(dir));
349
+ assert.ok(fs.existsSync(path.join(dir, "fallow.exe")), "fixture must write the .exe target");
350
+
351
+ const result = await verifyInstalled({
352
+ dirOverride: dir,
353
+ platformId,
354
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
355
+ digestProvider: makeDigestProvider(dir),
356
+ });
357
+ assert.equal(result.ok, true);
358
+ });
359
+
360
+ test("verifyInstalled reports the .exe name when a win32 signature is absent", async (t) => {
361
+ const { privateKey, rawPub } = makeKeypair();
362
+ const platformId = "win32-arm64-msvc";
363
+ const dir = makePlatformDir(privateKey, { platformId, skipSigFor: "fallow" });
364
+ t.after(() => cleanup(dir));
365
+
366
+ const result = await verifyInstalled({
367
+ dirOverride: dir,
368
+ platformId,
369
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
370
+ digestProvider: makeDigestProvider(dir),
371
+ });
372
+ assert.equal(result.ok, false);
373
+ assert.equal(result.code, "sig-missing");
374
+ assert.match(result.message, /fallow\.exe/);
375
+ });
376
+
331
377
  test("verifyInstalled resolves a global npm install from the fallow package directory", async (t) => {
332
378
  const pkg = currentPlatformPackage();
333
379
  if (!pkg) {
@@ -460,8 +506,8 @@ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
460
506
  assert.equal(result.skipped, true);
461
507
  });
462
508
 
463
- function computeDigestsForDir(dir) {
464
- const ext = process.platform === "win32" ? ".exe" : "";
509
+ function computeDigestsForDir(dir, platformId) {
510
+ const ext = extForPlatformId(platformId);
465
511
  const out = {};
466
512
  for (const base of ["fallow"]) {
467
513
  const fileName = `${base}${ext}`;
@@ -581,7 +627,7 @@ test("verifyInstalled returns digest-mismatch when the embedded digest disagrees
581
627
  const { privateKey, rawPub } = makeKeypair();
582
628
  const dir = makePlatformDir(privateKey);
583
629
  t.after(() => cleanup(dir));
584
- const ext = process.platform === "win32" ? ".exe" : "";
630
+ const ext = extForPlatformId();
585
631
  writeManifest(dir, {
586
632
  name: "@fallow-cli/x",
587
633
  version: "1.0.0",
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: fallow
3
- description: Codebase intelligence for TypeScript and JavaScript. Static analysis of code and styles reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Runtime coverage can merge production execution data for hot-path review, cold-path deletion confidence, and stale-flag evidence. 123 framework plugins, zero configuration, sub-second static analysis. Use when asked to audit PR risk, find unused code or dependencies, detect duplicates, check styling consistency, inspect architecture boundaries, merge runtime coverage, auto-fix supported issues, or run fallow.
3
+ description: Codebase intelligence for TypeScript and JavaScript. Static analysis of code and styles reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Runtime coverage can merge production execution data for hot-path review, cold-path deletion confidence, and stale-flag evidence. Broad framework plugin coverage, zero configuration, sub-second static analysis. Use when asked to audit PR risk, find unused code or dependencies, detect duplicates, check styling consistency, inspect architecture boundaries, merge runtime coverage, auto-fix supported issues, or run fallow.
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # Fallow: codebase intelligence for TypeScript and JavaScript
8
8
 
9
- Codebase intelligence for TypeScript and JavaScript. The static layer analyzes code and styles and reports quality, changed-code risk, cleanup opportunities, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, design-system styling drift, feature flag patterns, and opt-in security candidates. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence, with a single local capture available by default and continuous/cloud runtime monitoring available as an optional mode. 123 framework plugins, zero configuration, sub-second static analysis.
9
+ Codebase intelligence for TypeScript and JavaScript. The static layer analyzes code and styles and reports quality, changed-code risk, cleanup opportunities, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, design-system styling drift, feature flag patterns, and opt-in security candidates. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence, with a single local capture available by default and continuous/cloud runtime monitoring available as an optional mode. Broad framework plugin coverage, zero configuration, sub-second static analysis.
10
10
 
11
11
  ## When to Use
12
12
  - Find cleanup opportunities: unused files, exports, types, members, dependencies, or stale flags.
@@ -42,8 +42,8 @@ cargo install fallow-cli # build from source
42
42
 
43
43
  ## Agent Rules
44
44
 
45
- 1. **Always use `--format json --quiet 2>/dev/null`** for machine-readable output and parse it as JSON. Compact JSON is the default; never depend on whitespace or add `--pretty` in agent pipelines. The `2>/dev/null` discards stderr so progress messages and threshold warnings don't corrupt the JSON on stdout. Never use `2>&1`
46
- 2. **Always append `|| true`** to every fallow command. Exit code 1 means "issues found" (normal), not a runtime error. Without `|| true`, the Bash tool treats exit 1 as failure and cancels parallel commands. Only exit code 2 is a real error (invalid config, parse failure)
45
+ 1. **Always use `--format json --quiet`** for machine-readable output and parse stdout as JSON. Compact JSON is the default; never depend on whitespace or add `--pretty` in agent pipelines. Keep stderr separate so diagnostics remain visible; never merge it into the JSON stream with `2>&1`.
46
+ 2. **Preserve and interpret the exit status.** Codes 0 and 1 are successful analysis outcomes: 0 is clean and 1 means findings. Treat every other code according to `fallow schema.exit_codes`. Do not force a successful status, because that hides validation, license, setup, network, and security-gate outcomes.
47
47
  3. **Use `--explain`** to include a `_meta` object in JSON output with metric definitions, ranges, and interpretation hints. In human format, `--explain` prints a `Description:` line under each section header.
48
48
  4. **Use the root `kind` field** to identify typed JSON envelopes (`dead-code`, `dead-code-grouped`, `health`, `dupes`, `combined`, `audit`, etc.).
49
49
  5. **Use issue type filters** (`--unused-exports`, `--unused-files`, etc.) to limit output scope
@@ -160,8 +160,8 @@ Run `fallow <command> --help` for the full flag list per command (see also refer
160
160
  | `unused-catalog-entry` | `--unused-catalog-entries` | yes | - | `pnpm-workspace.yaml` entries no workspace package.json references via `catalog:` (default `warn`) |
161
161
  | `empty-catalog-group` | `--empty-catalog-groups` | - | - | Named `catalogs.<name>:` groups in `pnpm-workspace.yaml` with no entries. Top-level `catalog:` placeholders are ignored. Default `warn`. |
162
162
  | `unresolved-catalog-reference` | `--unresolved-catalog-references` | - | - | `package.json` references to `catalog:` / `catalog:<name>` whose catalog does not declare the package; `pnpm install` would fail. Default `error`. Suppress via `ignoreCatalogReferences: [{ package, catalog?, consumer? }]` in fallow config (package.json has no comment syntax). |
163
- | `unused-dependency-override` | `--unused-dependency-overrides` | - | - | `pnpm-workspace.yaml#overrides` / `package.json#pnpm.overrides` entries whose target package is not declared by any workspace `package.json` and is not present in `pnpm-lock.yaml`. Default `warn`. When the lockfile is missing or unreadable the check degrades to a manifest-only fallback and every finding carries a `hint` reminding consumers to verify before removal. Suppress via `ignoreDependencyOverrides: [{ package, source? }]` in fallow config. |
164
- | `misconfigured-dependency-override` | `--misconfigured-dependency-overrides` | - | - | `pnpm.overrides` entries whose key is unparsable (empty, dangling separators, malformed selectors) or value is missing/empty. `pnpm install` would fail. Default `error`. Suppression: same `ignoreDependencyOverrides` config rule. |
163
+ | `unused-dependency-override` | `--unused-dependency-overrides` | - | - | Entries in `pnpm-workspace.yaml#overrides`, `package.json#pnpm.overrides`, npm or Bun `package.json#overrides`, or Bun `package.json#resolutions` whose target package is not declared by any workspace `package.json` and is not present in the active readable lockfile. Default `warn`. pnpm and npm projects without a readable lockfile degrade to a manifest-only fallback with a verification `hint`; Bun projects with only binary `bun.lockb` fail closed and emit no finding. Suppress via `ignoreDependencyOverrides: [{ package, source? }]` in fallow config. |
164
+ | `misconfigured-dependency-override` | `--misconfigured-dependency-overrides` | - | - | Package-manager override entries whose key is unparsable or whose value is missing or empty. The active package manager may reject or ignore the entry. Default `error`. Suppression: same `ignoreDependencyOverrides` config rule. |
165
165
  | `invalid-client-export` | - | - | `// fallow-ignore-next-line invalid-client-export` | "use client" file exports a server-only / route-config name; Requires the project to declare next |
166
166
  | `mixed-client-server-barrel` | - | - | `// fallow-ignore-next-line mixed-client-server-barrel` | Barrel re-exports both a "use client" module and a server-only module; Requires the project to declare next |
167
167
  | `misplaced-directive` | - | - | `// fallow-ignore-next-line misplaced-directive` | "use client" / "use server" directive is not in the leading position and is ignored; Requires the project to declare next |
@@ -257,7 +257,7 @@ fallow list --entry-points --format json --quiet
257
257
  fallow list --plugins --format json --quiet
258
258
  ```
259
259
 
260
- Shows detected entry points and active framework plugins (123 built-in: Next.js, Vite, Ember, Wuchale, Jest, Storybook, Tailwind, PandaCSS, Contentlayer, tap, tsd, etc.).
260
+ Shows detected entry points and active framework plugins. Read `fallow schema.plugins.count` when the exact current registry size matters.
261
261
 
262
262
  ### Production-only analysis
263
263
  ```bash
@@ -428,11 +428,7 @@ fallow hooks install --target git # pre-commit gate; --branch <ref> sets the f
428
428
 
429
429
  ## Exit Codes
430
430
 
431
- | Code | Meaning |
432
- |------|---------|
433
- | 0 | Success, no error-severity issues |
434
- | 1 | Error-severity issues found |
435
- | 2 | Runtime error (invalid config, parse failure, or `fix` without `--yes` in non-TTY) |
431
+ Codes 0 and 1 are successful analysis outcomes: 0 is clean and 1 means findings. Read `fallow schema.exit_codes` for validation, resource, runtime, network, security-gate, and upload failures instead of maintaining another copied table.
436
432
 
437
433
  When `--format json` is active and exit code is 2, errors are emitted as JSON on stdout:
438
434
  ```json
@@ -441,7 +437,7 @@ When `--format json` is active and exit code is 2, errors are emitted as JSON on
441
437
 
442
438
  ## Configuration
443
439
 
444
- Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to 123 auto-detecting framework plugins.
440
+ Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to auto-detecting framework plugins; read `fallow schema.plugins` for the current registry.
445
441
 
446
442
  ```jsonc
447
443
  {
@@ -478,7 +474,7 @@ export const deprecatedHelper = () => {};
478
474
  ## Key Gotchas
479
475
 
480
476
  - **`fix --yes` is required** in non-TTY (agent) environments. Without it, `fix` exits with code 2
481
- - **Zero config by default.** 123 framework plugins auto-detect, including Wuchale config, Contentlayer content roots, tap and tsd test entry points. Don't create config unless customization is needed
477
+ - **Zero config by default.** Built-in framework plugins auto-detect, including Wuchale config, Contentlayer content roots, tap and tsd test entry points. Read `fallow schema.plugins` for the current registry and don't create config unless customization is needed
482
478
  - **Syntactic analysis only.** No TypeScript compiler, so fully dynamic `import(variable)` is not resolved
483
479
  - **Function overloads are deduplicated.** TypeScript function overload signatures are merged into a single export (not reported as separate unused exports)
484
480
  - **Re-export chains are resolved.** Exports through barrel files are tracked, not falsely flagged
@@ -89,8 +89,8 @@ Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#
89
89
  | `--unused-catalog-entries` | Unused pnpm catalog entries |
90
90
  | `--empty-catalog-groups` | Empty named pnpm catalog groups |
91
91
  | `--unresolved-catalog-references` | Package references to missing pnpm catalog entries |
92
- | `--unused-dependency-overrides` | Unused pnpm dependency overrides |
93
- | `--misconfigured-dependency-overrides` | Malformed pnpm dependency overrides |
92
+ | `--unused-dependency-overrides` | Unused package-manager dependency overrides |
93
+ | `--misconfigured-dependency-overrides` | Misconfigured package-manager dependency overrides |
94
94
  <!-- generated:flags:dead-code-filters:end -->
95
95
  ### Examples
96
96
 
@@ -298,7 +298,7 @@ fallow list --workspaces --format json --quiet
298
298
  fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
299
299
  ```
300
300
 
301
- 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.
301
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`, `malformed-pnpm-workspace-yaml`, `skipped-large-file`, `skipped-minified-file`, `source-read-failure`, `bun-lockb-override-resolution-skipped`) with a typed payload (`error`, `pattern`, or none), and a `path` that is project-root-relative with forward slashes on every envelope that carries the array. The same `workspace_diagnostics[]` array is also surfaced on the `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes, at the top level of the bare combined `fallow --format json` envelope, on `fallow audit --format json` under `dead_code`, and on the `audit-brief` envelope shared by `fallow review --format json` and `fallow audit --brief --format json`, also under `dead_code` (omitted when empty). The combined carrier is the envelope root, not a section, so `--skip check`, `--only health`, and `--only dupes` all still report what their analyses recorded. The combined root is the union of what every analysis in the run recorded, deduplicated on the whole `kind` (typed payload included) plus `path`, so two overlapping globs still report the same package-less directory once per `pattern` (a declared glob's no-op `./` prefix is normalised away, so one glob written `"./apps/**"` in `package.json` and `apps/**` in `pnpm-workspace.yaml` stays one entry): a combined run walks the project once per analysis, and a per-analysis `production` mode (`production: { deadCode, health, dupes }`, `--production-health`) can give those walks different file sets, so only the union reports what the run as a whole saw. Each analysis contributes the workspace-discovery list its own config load produced, the same list `fallow list --workspaces` reports, so the combined root can carry an `undeclared-workspace` or `glob-matched-no-package-json` entry that the standalone `dead-code`, `check`, `health`, and `dupes` envelopes, which read the process diagnostics registry instead, do not. `fallow audit --format json` and the `audit-brief` envelope are on the same broad side: they fold the dead-code analysis's own list into their `dead_code.workspace_diagnostics[]`, so they too report an `undeclared-workspace` entry the standalone envelopes miss. The CLI and the programmatic route (MCP code mode, NAPI, embedders) agree on everything an analysis records: both folds close with the same process-registry read, which covers what an analysis records after its section captured its list (a `source-read-failure`, or the analysis-stage kinds a health run's own dead-code precompute records) and skips `skipped-large-file` and `skipped-minified-file`, since those reach an envelope only from the walk that recorded them. The two analysis-stage kinds (`malformed-pnpm-workspace-yaml`, `bun-lockb-override-resolution-skipped`) are recorded by the dead-code analyze pass, so they only appear on runs that include it: `fallow dupes --format json` and `fallow --only dupes` report the workspace-discovery and source-discovery kinds alone. A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
302
302
 
303
303
  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`.
304
304
 
@@ -838,8 +838,8 @@ Audits changed files for dead code, complexity, duplication, and styling. Return
838
838
  | `--health-baseline` | `string` | - | Baseline file (produced by `fallow health --save-baseline`). Pre-existing complexity findings are excluded from the verdict. |
839
839
  | `--dupes-baseline` | `string` | - | Baseline file (produced by `fallow dupes --save-baseline`). Pre-existing clone groups are excluded from the verdict. |
840
840
  | `--max-crap` | `string` | - | Forwarded to the health sub-analysis. Functions meeting or exceeding this CRAP score cause audit to fail. Same formula as `health --max-crap`. Pair with coverage data for accurate per-function CRAP. |
841
- | `--coverage` | `string` | - | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores in the health sub-analysis. Same format and semantics as `health --coverage`. Also configurable via `FALLOW_COVERAGE`. Relative paths resolve against `--root`. |
842
- | `--coverage-root` | `string` | - | Absolute prefix to strip from file paths in coverage data before prepending the project root. Also configurable via `FALLOW_COVERAGE_ROOT`. Use when coverage was generated under a different checkout root in CI / Docker (e.g., `/home/runner/work/myapp` on GitHub Actions). |
841
+ | `--coverage` | `string` | - | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores in the health sub-analysis. Same format and semantics as `health --coverage`. Also configurable via `FALLOW_COVERAGE`, then `health.coverage` (the same chain as `fallow health`). Relative paths resolve against `--root`. |
842
+ | `--coverage-root` | `string` | - | Absolute prefix to strip from file paths in coverage data before prepending the project root. Also configurable via `FALLOW_COVERAGE_ROOT`, then `health.coverageRoot`. Use when coverage was generated under a different checkout root in CI / Docker (e.g., `/home/runner/work/myapp` on GitHub Actions). |
843
843
  | `--no-css` | `bool` | `false` | Disable styling analytics in audit |
844
844
  | `--css-deep` | `bool` | `false` | Enable deep CSS analysis for audit explicitly: project-wide styling reachability, narrowed back to changed anchors. Deep CSS is on by default; use this to override `audit.cssDeep = false` |
845
845
  | `--no-css-deep` | `bool` | `false` | Disable deep CSS analysis while keeping local styling analytics on |
@@ -23,7 +23,7 @@ Always preview with `--dry-run` before applying. This is a destructive operation
23
23
 
24
24
  ## Don't Create Config Unless Needed
25
25
 
26
- Fallow works with zero configuration for most projects thanks to 123 auto-detecting framework plugins. Creating an unnecessary config file can mask issues or override detection behavior.
26
+ Fallow works with zero configuration for most projects thanks to auto-detecting framework plugins. Read `fallow schema.plugins` for the current registry. Creating an unnecessary config file can mask issues or override detection behavior.
27
27
 
28
28
  ```bash
29
29
  # WRONG: creating config for a standard Next.js project
@@ -38,7 +38,7 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
38
38
  | `impact` | introspection | free | `fallow impact --format json --quiet` | `root` | Read the local, opt-in Fallow Impact value report (`fallow impact --format json`). Runs no analysis: current surfacing counts, trend since the last recorded run, pre-commit gate containment, and (on impact v1.5+) resolved/suppressed attribution. History is read from a per-project file in the user's private config dir (never inside the repo). Read-only and `root`-only; the mutating `enable` / `disable` / `default` lifecycle is not exposed. A never-enabled project returns a populated `{"enabled": false, ...}` report (never `{}`); branch on `enabled` and `enabled_source` (`project` / `user` / `default`) then `record_count`, recommending `fallow impact enable` only when `explicit_decision` is `false` (never asked) and staying silent when `true` (deliberately disabled here). Local-developer signal: fallow never records in CI, so empty there and not a CI metric |
39
39
  | `impact_all` | introspection | free | `fallow impact --all --format json --quiet` | `sort`, `limit` | Roll every tracked fallow project on this machine into one cross-repo value report (hashed keys plus basename labels, never paths; local-dev only) |
40
40
  | `trace_export` | trace | free | `fallow dead-code --trace <file:export> --format json --quiet` | `file`, `export_name` | Trace why an export is used or unused (`fallow dead-code --trace FILE:EXPORT_NAME --format json`). Required `file` and `export_name`. Returns file reachability, entry-point status, direct references, re-export chains, and a reason string. If `export_name` is a class / enum / store MEMBER, returns a member trace instead (`member_name`, `member_kind`, `owner_export`, `owner_is_used`) plus a `--unused-<kind>-members` pointer; branch on field presence. Use before deleting a supposedly-unused export or debugging an unused-class-member finding |
41
- | `trace_symbol` | trace | free | `fallow dead-code --type-aware --trace <file:export> --format json --quiet` | `file`, `export_name`, `type_aware_projects`, `type_aware_require` | Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat `semantic.references`, `semantic.status`, and `semantic.identity` as the authoritative exact evidence. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface. |
41
+ | `trace_symbol` | trace | free | `fallow dead-code --type-aware --trace <file:export> --format json --quiet` | `file`, `export_name`, `type_aware_projects`, `type_aware_require` | Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat `semantic.references`, `semantic.status`, and `semantic.identity` as the authoritative exact evidence. The proof covers only the lane named by `semantic.target.namespace`, so a root trace that lists a reference the proof does not is wider evidence rather than stale. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface. |
42
42
  | `symbol_impact` | impact | free | `fallow dead-code --type-aware --symbol-impact <file:export-or-class.member> --format json --quiet` | `file`, `export_name`, `class_name`, `member_name`, `type_aware_projects`, `type_aware_require` | Return exact-symbol consumers, affected files, and targeted tests for a TypeScript export or exported class method. Select either `export_name`, or both `class_name` and `member_name`. Advisory change-impact evidence, not a substitute for `tsc` or Oxlint |
43
43
  | `trace_file` | trace | free | `fallow dead-code --trace-file <file> --format json --quiet` | `file` | Trace all graph edges for a file (`fallow dead-code --trace-file PATH --format json`). Required `file`. Returns reachability, exports, imports-from, imported-by, and re-exports. Use to decide whether a file is isolated, barrel-only, or imported by live entry points |
44
44
  | `impact_closure` | trace | free | `fallow dead-code --impact-closure <path> --format json --quiet` | `path` | Trace the transitive affected-but-not-in-diff set and coordination gaps for one file. Supports `root`, `config`, `production`, `workspace`, `no_cache`, and `threads`. Use as review-planning evidence for a file contract, not proof that affected files are wrong |
@@ -46,6 +46,29 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
46
46
  | `trace_clone` | trace | free | `fallow dupes --trace <file:line> --format json --quiet` | `file`, `line`, `fingerprint`, `near`, `min_occurrences` | Deep-dive a duplicate-code clone group (`fallow dupes --trace <spec> --format json`). Address by exactly one of: `file` + `line` (a source location), or `fingerprint` (a `dup:<id>` from a prior `find_dupes` `clone_groups[].fingerprint`, usually `dup:<8hex>` and widened only on rare report collisions). Returns the matched clone instance plus every clone group containing it; each traced group carries its `fingerprint`, an extract-function `suggestion` with estimated savings, and a best-effort `suggested_name` (omitted when no confident name). Supports `mode`, `near`, `min_tokens`, `min_lines`, `min_occurrences`, `threshold`, `skip_local`, `cross_language`, `ignore_imports`. Use the same `near` value as the originating `find_dupes` call. Use to consolidate duplication when you need exact sibling locations and a refactor target |
47
47
  <!-- generated:mcp-tools:end -->
48
48
 
49
+ ## How type-aware proof relates to the root trace
50
+
51
+ `trace_symbol` is the only tool that returns a checker-backed `semantic` block
52
+ next to a syntactic root trace. The checker resolves actual reads through local
53
+ aliases, import types, namespace-qualified names, and barrels to the exact type
54
+ or value declaration. Import and re-export declarations alone are not reads.
55
+ The root trace stays authoritative for graph reachability and star ambiguity,
56
+ and its optional `direct_references_by_namespace` keeps type and value evidence
57
+ separate without changing the selected root `namespace`. Type-aware
58
+ reconciliation fails closed: unreachable-only, re-export-only, or
59
+ different-declaration evidence cannot suppress a syntactic finding. Treat an
60
+ ambiguous root as an abstention, and investigate any remaining mismatch before
61
+ deleting a symbol.
62
+
63
+ `symbol_impact` carries no `semantic` block and no root trace. Its top-level
64
+ checker evidence uses the same declaration-safe alias and namespace resolution
65
+ as `trace_symbol`. A listed consumer's `relation` names the traced symbol's own
66
+ lane, not the consumer's syntax. Confirm a clean impact result with
67
+ `trace_export` or `trace_symbol` before deletion when the graph reports
68
+ ambiguity or reachable references.
69
+
70
+ `trace_export` never carries a `semantic` block: it is API-backed in-process and answers from the graph alone.
71
+
49
72
  ## Runtime source-map confidence for cloud runtime tools
50
73
 
51
74
  | Values | Meaning | Agent action |
@@ -645,7 +645,7 @@ Focus on findings that are BOTH dead code and duplicated:
645
645
 
646
646
  ## Custom Plugin Setup
647
647
 
648
- For frameworks not covered by the 123 built-in plugins.
648
+ For frameworks not covered by the current built-in registry from `fallow schema.plugins`.
649
649
 
650
650
  ### Option 1: Inline framework config
651
651
 
@@ -717,7 +717,7 @@ fallow dead-code --format sarif --quiet > fallow.sarif
717
717
  fallow dead-code --ci > fallow.sarif
718
718
  ```
719
719
 
720
- The `--ci` flag is equivalent to `--format sarif --fail-on-issues --quiet`. Note: `--fail-on-issues` means exit code 1 if issues exist, in CI scripts use `continue-on-error: true` or `|| true` to ensure the SARIF upload step still runs.
720
+ The `--ci` flag is equivalent to `--format sarif --fail-on-issues --quiet`. Exit code 1 means findings exist. Capture that status, let the SARIF upload step run, then reapply the captured status in a final gate step. Do not discard every outcome, because validation and execution failures need to remain distinguishable from findings.
721
721
 
722
722
  ---
723
723