fallow 3.3.0 → 3.5.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ Static analysis is open source. An optional runtime layer adds production execut
20
20
  npm install --save-dev fallow # or: pnpm add -D fallow / yarn add -D fallow / bun add -d fallow
21
21
  ```
22
22
 
23
- Installs the `fallow` CLI plus the companion `fallow-lsp` and `fallow-mcp` binaries in your project.
23
+ Installs the `fallow` CLI in your project, along with the `fallow-lsp` and `fallow-mcp` launchers that start the LSP and MCP servers from the same binary.
24
24
 
25
25
  The package also ships a version-matched Agent Skill under `skills/fallow`.
26
26
  For tools that need CLI and issue-surface metadata without spawning the binary,
@@ -119,7 +119,7 @@ Create a config file in your project root, or run `fallow init`:
119
119
  ```jsonc
120
120
  // .fallowrc.json
121
121
  {
122
- "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
122
+ "$schema": "./node_modules/fallow/schema.json",
123
123
  "entry": ["src/workers/*.ts", "scripts/*.ts"],
124
124
  "ignorePatterns": ["**/*.generated.ts"],
125
125
  "rules": {
@@ -130,6 +130,16 @@ Create a config file in your project root, or run `fallow init`:
130
130
  }
131
131
  ```
132
132
 
133
+ `$schema` gives editors autocomplete and validation and has no effect on
134
+ analysis. As an npm package, `fallow` always ships a version-aligned schema at
135
+ `./node_modules/fallow/schema.json`, which `fallow init` and
136
+ `fallow recommend` point at by default: offline, no editor trust prompt. If
137
+ you install fallow another way (cargo, homebrew, a bare binary) with no
138
+ `node_modules/fallow` to point at, use the remote fallback instead:
139
+ `https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json`. Some
140
+ editors, including VS Code, refuse to load a remote schema URL until you
141
+ explicitly trust the domain.
142
+
133
143
  Also supports TOML (`fallow init --toml` creates `fallow.toml`).
134
144
 
135
145
  ## Documentation
package/bin/fallow-lsp CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Launcher shim: the platform packages ship a single multicall `fallow`
4
+ // binary, so `fallow-lsp` spawns `fallow lsp-server` with the same stdio,
5
+ // signal, and exit-code forwarding as a bare `fallow` invocation.
3
6
  const { runBinary } = require('../scripts/run-binary');
4
- runBinary('fallow-lsp');
7
+ runBinary('fallow', { prependArgs: ['lsp-server'] });
package/bin/fallow-mcp CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Launcher shim: the platform packages ship a single multicall `fallow`
4
+ // binary, so `fallow-mcp` spawns `fallow mcp-server` with the same stdio,
5
+ // signal, and exit-code forwarding as a bare `fallow` invocation.
3
6
  const { runBinary } = require('../scripts/run-binary');
4
- runBinary('fallow-mcp');
7
+ runBinary('fallow', { prependArgs: ['mcp-server'] });
package/capabilities.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "manifest_version": "1",
5
5
  "description": "Codebase analyzer for TypeScript/JavaScript: unused code, circular dependencies, code duplication, complexity hotspots, and architecture boundary violations",
6
6
  "global_flags": [
@@ -18,6 +18,16 @@
18
18
  "description": "Path to config file (.fallowrc.json, .fallowrc.jsonc, fallow.toml, or .fallow.toml)",
19
19
  "short": "-c"
20
20
  },
21
+ {
22
+ "name": "--allow-remote-extends",
23
+ "type": "bool",
24
+ "required": false,
25
+ "description": "Allow trusted config files to extend HTTPS URLs",
26
+ "possible_values": [
27
+ "true",
28
+ "false"
29
+ ]
30
+ },
21
31
  {
22
32
  "name": "--format",
23
33
  "type": "string",
@@ -36,7 +46,9 @@
36
46
  "pr-comment-gitlab",
37
47
  "review-github",
38
48
  "review-gitlab",
39
- "badge"
49
+ "badge",
50
+ "github-annotations",
51
+ "github-summary"
40
52
  ]
41
53
  },
42
54
  {
@@ -266,6 +278,12 @@
266
278
  "description": "Write the report to a file instead of stdout, for any --format (no ANSI codes). Useful on large projects where the terminal scrollback truncates the top. Progress and the confirmation stay on stderr",
267
279
  "short": "-o"
268
280
  },
281
+ {
282
+ "name": "--report-path-prefix",
283
+ "type": "string",
284
+ "required": false,
285
+ "description": "Prefix prepended to every path in the CI-facing formats (`github-annotations`, `github-summary`, `codeclimate`, `review-github`, `review-gitlab`). CI platforms address files by repository-root-relative path, so when the analyzed project lives in a subdirectory (e.g. `packages/app/`), paths need that offset. fallow detects the offset via the git toplevel automatically; this flag overrides the detection. Pass an empty string to disable rebasing and emit paths relative to `--root`"
286
+ },
269
287
  {
270
288
  "name": "--fail-on-regression",
271
289
  "type": "bool",
@@ -836,6 +854,16 @@
836
854
  "true",
837
855
  "false"
838
856
  ]
857
+ },
858
+ {
859
+ "name": "--churn",
860
+ "type": "bool",
861
+ "required": false,
862
+ "description": "OPT-IN: attach target-level git churn evidence from the health hotspot subsystem. Default off to avoid git-history latency",
863
+ "possible_values": [
864
+ "true",
865
+ "false"
866
+ ]
839
867
  }
840
868
  ]
841
869
  },
@@ -1448,6 +1476,18 @@
1448
1476
  }
1449
1477
  ]
1450
1478
  },
1479
+ {
1480
+ "name": "suppressions",
1481
+ "description": "List active fallow-ignore suppression markers (read-only inventory)",
1482
+ "flags": [
1483
+ {
1484
+ "name": "--file",
1485
+ "type": "string",
1486
+ "required": false,
1487
+ "description": "Only list suppressions in the specified files. Accepts multiple values"
1488
+ }
1489
+ ]
1490
+ },
1451
1491
  {
1452
1492
  "name": "explain",
1453
1493
  "description": "Explain one fallow issue type without running an analysis",
@@ -1753,6 +1793,18 @@
1753
1793
  }
1754
1794
  ]
1755
1795
  },
1796
+ {
1797
+ "name": "report",
1798
+ "description": "Render a saved `--format json` results file in another format without re-running analysis (analyze once, render annotations and the job summary from the same file). v1 renders the GitHub-native formats only: `--format github-annotations` or `--format github-summary`",
1799
+ "flags": [
1800
+ {
1801
+ "name": "--from",
1802
+ "type": "string",
1803
+ "required": true,
1804
+ "description": "Path to a fallow JSON results file produced by `--format json` (dead-code, dupes, health, audit, security, or bare combined)"
1805
+ }
1806
+ ]
1807
+ },
1756
1808
  {
1757
1809
  "name": "schema",
1758
1810
  "description": "Dump fallow's capability manifest (CLI commands and flags, issue types, MCP tools, framework plugins, env vars) as machine-readable JSON for agent introspection. Always JSON, regardless of --format",
@@ -6338,7 +6390,8 @@
6338
6390
  "cli_command": "fallow inspect --format json --quiet",
6339
6391
  "key_params": [
6340
6392
  "target",
6341
- "production"
6393
+ "production",
6394
+ "include_churn"
6342
6395
  ],
6343
6396
  "license": "free",
6344
6397
  "license_note": null,
@@ -6588,6 +6641,20 @@
6588
6641
  "license_note": null,
6589
6642
  "read_only": true
6590
6643
  },
6644
+ {
6645
+ "name": "list_suppressions",
6646
+ "kind": "analysis",
6647
+ "description": "List active fallow-ignore suppression markers grouped per file (line, kind, level, reason, and a stale cross-reference); a read-only governance inventory that always exits 0",
6648
+ "cli_command": "fallow suppressions --format json --quiet",
6649
+ "key_params": [
6650
+ "workspace",
6651
+ "changed_since",
6652
+ "file"
6653
+ ],
6654
+ "license": "free",
6655
+ "license_note": null,
6656
+ "read_only": true
6657
+ },
6591
6658
  {
6592
6659
  "name": "impact",
6593
6660
  "kind": "introspection",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Codebase intelligence for TypeScript and JavaScript. Free static analysis of code and styles, optional paid runtime intelligence (Fallow Runtime). Quality, risk, architecture, dependencies, duplication, and design-system drift for humans, CI, and the agents writing your code. Zero-config framework support.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "tanstack-intent"
37
37
  ],
38
38
  "engines": {
39
- "node": ">=16"
39
+ "node": ">=22"
40
40
  },
41
41
  "bin": {
42
42
  "fallow": "bin/fallow",
@@ -87,13 +87,13 @@
87
87
  "@tanstack/intent": "0.3.2"
88
88
  },
89
89
  "optionalDependencies": {
90
- "@fallow-cli/darwin-arm64": "3.3.0",
91
- "@fallow-cli/darwin-x64": "3.3.0",
92
- "@fallow-cli/linux-x64-gnu": "3.3.0",
93
- "@fallow-cli/linux-arm64-gnu": "3.3.0",
94
- "@fallow-cli/linux-x64-musl": "3.3.0",
95
- "@fallow-cli/linux-arm64-musl": "3.3.0",
96
- "@fallow-cli/win32-arm64-msvc": "3.3.0",
97
- "@fallow-cli/win32-x64-msvc": "3.3.0"
90
+ "@fallow-cli/darwin-arm64": "3.5.0",
91
+ "@fallow-cli/darwin-x64": "3.5.0",
92
+ "@fallow-cli/linux-x64-gnu": "3.5.0",
93
+ "@fallow-cli/linux-arm64-gnu": "3.5.0",
94
+ "@fallow-cli/linux-x64-musl": "3.5.0",
95
+ "@fallow-cli/linux-arm64-musl": "3.5.0",
96
+ "@fallow-cli/win32-arm64-msvc": "3.5.0",
97
+ "@fallow-cli/win32-x64-msvc": "3.5.0"
98
98
  }
99
99
  }
package/schema.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "object",
5
5
  "properties": {
6
6
  "$schema": {
7
- "description": "A string pointing at fallow's JSON Schema URL, used only by editors for autocomplete and validation of the config file; it has no effect on analysis and is stripped before serialization (serde skip_serializing, writeOnly in the schema). Set it to `https://fallow.dev/schema.json` to get editor IntelliSense; any other value is ignored by fallow.",
7
+ "description": "A string pointing at fallow's JSON Schema URL, used only by editors for autocomplete and validation of the config file; it has no effect on analysis and is stripped before serialization (serde skip_serializing, writeOnly in the schema). Set it to `./node_modules/fallow/schema.json` for npm installs (version-aligned, offline, avoids VS Code's untrusted-remote-schema prompt), or `https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json` for non-npm installs; any other value is ignored by fallow.",
8
8
  "type": [
9
9
  "string",
10
10
  "null"
@@ -66,8 +66,11 @@ function emitVerifyLog(env, payload) {
66
66
  }
67
67
 
68
68
  function binaryTargetsForPlatform(platform) {
69
+ // Platform packages ship one binary: the multicall `fallow`. The bundled
70
+ // lsp/mcp launchers spawn `fallow lsp-server` / `fallow mcp-server`, so there
71
+ // is only one binary to sentinel-track and verify.
69
72
  const ext = platform === "win32" ? ".exe" : "";
70
- return [`fallow${ext}`, `fallow-lsp${ext}`, `fallow-mcp${ext}`];
73
+ return [`fallow${ext}`];
71
74
  }
72
75
 
73
76
  function statMtimeMs(absPath) {
@@ -28,7 +28,8 @@ function ext() {
28
28
  }
29
29
 
30
30
  function binaryNames() {
31
- return [`fallow${ext()}`, `fallow-lsp${ext()}`, `fallow-mcp${ext()}`];
31
+ // Platform packages ship a single multicall `fallow` binary.
32
+ return [`fallow${ext()}`];
32
33
  }
33
34
 
34
35
  function computeDigestsForDir(dir) {
@@ -116,7 +117,7 @@ test("ensureVerified verifies on cache miss and writes the sentinel", (t) => {
116
117
  assert.equal(sentinel.schemaVersion, SENTINEL_SCHEMA_VERSION);
117
118
  assert.equal(sentinel.packageVersion, "2.81.0");
118
119
  assert.equal(sentinel.packageName, "@fallow-cli/test-platform");
119
- assert.equal(Object.keys(sentinel.binaries).length, 3);
120
+ assert.equal(Object.keys(sentinel.binaries).length, 1);
120
121
  });
121
122
 
122
123
  test("ensureVerified returns cached:true on a valid sentinel", (t) => {
@@ -164,7 +165,7 @@ test("ensureVerified invalidates sentinel on mtime drift", (t) => {
164
165
  );
165
166
  assert.equal(result.ok, true);
166
167
  assert.equal(result.cached, false);
167
- assert.equal(verifyCallCount, 3, "verify should rerun for all three binaries");
168
+ assert.equal(verifyCallCount, 1, "verify should rerun for the single binary");
168
169
  });
169
170
 
170
171
  test("ensureVerified invalidates sentinel on packageVersion drift", (t) => {
@@ -215,12 +216,6 @@ test("ensureVerified invalidates sentinel on packageName drift", (t) => {
215
216
  packageName: "@fallow-cli/wrong-name",
216
217
  binaries: {
217
218
  [`fallow${ext()}`]: { mtimeMs: fs.statSync(path.join(dir, `fallow${ext()}`)).mtimeMs },
218
- [`fallow-lsp${ext()}`]: {
219
- mtimeMs: fs.statSync(path.join(dir, `fallow-lsp${ext()}`)).mtimeMs,
220
- },
221
- [`fallow-mcp${ext()}`]: {
222
- mtimeMs: fs.statSync(path.join(dir, `fallow-mcp${ext()}`)).mtimeMs,
223
- },
224
219
  },
225
220
  }),
226
221
  );
@@ -266,13 +261,13 @@ test("ensureVerified invalidates sentinel on schemaVersion drift", (t) => {
266
261
  test("ensureVerified returns sig-invalid on a tampered signature", (t) => {
267
262
  _resetWarningState();
268
263
  const { privateKey, rawPub } = makeKeypair();
269
- const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow-lsp${ext()}` });
264
+ const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow${ext()}` });
270
265
  t.after(() => cleanup(dir));
271
266
 
272
267
  const result = ensureVerified(baseInput(dir, (p) => _verifyWithKey(p, rawPub)));
273
268
  assert.equal(result.ok, false);
274
269
  assert.equal(result.code, "sig-invalid");
275
- assert.match(result.binary, /fallow-lsp/);
270
+ assert.match(result.binary, /fallow/);
276
271
  // Sentinel must NOT have been written on failure
277
272
  assert.equal(fs.existsSync(path.join(dir, SENTINEL_FILENAME)), false);
278
273
  });
@@ -146,7 +146,15 @@ function guardBrokenStdout() {
146
146
  });
147
147
  }
148
148
 
149
- function runBinary(binaryBaseName) {
149
+ // Run the resolved platform binary. `options.prependArgs` (default none) is a
150
+ // list of leading arguments inserted before the process argv, used by the
151
+ // `fallow-lsp` / `fallow-mcp` launcher shims to spawn the multicall binary as
152
+ // `fallow lsp-server` / `fallow mcp-server`. Because the platform packages ship
153
+ // a single `fallow` binary, those shims pass `binaryBaseName = "fallow"` and
154
+ // the subcommand via `prependArgs`; signal, exit-code, and version-line
155
+ // handling stay identical to a bare `fallow` invocation.
156
+ function runBinary(binaryBaseName, options = {}) {
157
+ const prependArgs = Array.isArray(options.prependArgs) ? options.prependArgs : [];
150
158
  guardBrokenStdout();
151
159
  const { pkg, manifestPath, platformPkgDir } = resolvePlatformPaths();
152
160
  const resolvedVersion = readResolvedVersion(manifestPath);
@@ -166,7 +174,7 @@ function runBinary(binaryBaseName) {
166
174
  }
167
175
 
168
176
  try {
169
- execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
177
+ execFileSync(binaryPath, [...prependArgs, ...process.argv.slice(2)], { stdio: "inherit" });
170
178
  } catch (e) {
171
179
  if (e.status === undefined) throw e;
172
180
  if (e.status === null) {
@@ -293,11 +293,15 @@ function binaryTargetsForPlatform(platformId) {
293
293
  // and tests can synthesize a Windows verify without running on Windows.
294
294
  const isWindows = typeof platformId === "string" && platformId.startsWith("win32");
295
295
  const ext = isWindows ? ".exe" : "";
296
- return [
297
- { binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` },
298
- { binary: `fallow-lsp${ext}`, asset: `fallow-lsp-${platformId}${ext}` },
299
- { binary: `fallow-mcp${ext}`, asset: `fallow-mcp-${platformId}${ext}` },
300
- ];
296
+ // Platform packages ship ONE binary now: the multicall `fallow` (CLI + LSP +
297
+ // MCP in one engine). The bundled `fallow-lsp` / `fallow-mcp` launchers live
298
+ // in the wrapper and spawn `fallow lsp-server` / `fallow mcp-server`. The
299
+ // authoritative digest is the `fallowDigests.fallow` field the release
300
+ // pipeline embeds for every bundled package; the `asset` below is only the
301
+ // legacy GitHub-release fallback for pre-embedded-digest packages, which
302
+ // never shipped this bundled binary, so it is inert for bundled installs and
303
+ // fails closed (digest-mismatch) in the impossible case it is ever reached.
304
+ return [{ binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` }];
301
305
  }
302
306
 
303
307
  function isSkipRequested() {
@@ -243,7 +243,7 @@ function makePlatformDir(privateKey, options) {
243
243
  const opts = options || {};
244
244
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
245
245
  const ext = process.platform === "win32" ? ".exe" : "";
246
- for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
246
+ for (const base of ["fallow"]) {
247
247
  const binaryPath = path.join(dir, `${base}${ext}`);
248
248
  const content = Buffer.from(`mock ${base} contents`);
249
249
  fs.writeFileSync(binaryPath, content);
@@ -349,7 +349,7 @@ test("verifyInstalled resolves a global npm install from the fallow package dire
349
349
  );
350
350
 
351
351
  const ext = process.platform === "win32" ? ".exe" : "";
352
- for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
352
+ for (const base of ["fallow"]) {
353
353
  const binaryPath = path.join(platformDir, `${base}${ext}`);
354
354
  const content = Buffer.from(`global install ${base}`);
355
355
  fs.writeFileSync(binaryPath, content);
@@ -367,9 +367,9 @@ test("verifyInstalled resolves a global npm install from the fallow package dire
367
367
  assert.equal(result.version, "9.9.9");
368
368
  });
369
369
 
370
- test("verifyInstalled with dirOverride fails fast on the first bad signature", async (t) => {
370
+ test("verifyInstalled with dirOverride fails on a bad signature", async (t) => {
371
371
  const { privateKey, rawPub } = makeKeypair();
372
- const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow-lsp" });
372
+ const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
373
373
  t.after(() => cleanup(dir));
374
374
  const result = await verifyInstalled({
375
375
  dirOverride: dir,
@@ -378,12 +378,12 @@ test("verifyInstalled with dirOverride fails fast on the first bad signature", a
378
378
  });
379
379
  assert.equal(result.ok, false);
380
380
  assert.equal(result.code, "sig-invalid");
381
- assert.match(result.binary, /fallow-lsp/);
381
+ assert.match(result.binary, /fallow/);
382
382
  });
383
383
 
384
384
  test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent", async (t) => {
385
385
  const { privateKey, rawPub } = makeKeypair();
386
- const dir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
386
+ const dir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
387
387
  t.after(() => cleanup(dir));
388
388
  const result = await verifyInstalled({
389
389
  dirOverride: dir,
@@ -392,13 +392,13 @@ test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent
392
392
  });
393
393
  assert.equal(result.ok, false);
394
394
  assert.equal(result.code, "sig-missing");
395
- assert.match(result.binary, /fallow-mcp/);
395
+ assert.match(result.binary, /fallow/);
396
396
  });
397
397
 
398
398
  test("verifyInstalled threads the resolved version into the sig-missing message", async (t) => {
399
399
  const { privateKey, rawPub } = makeKeypair();
400
- const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
401
- const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
400
+ const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
401
+ const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
402
402
  t.after(() => {
403
403
  cleanup(preDir);
404
404
  cleanup(eraDir);
@@ -463,7 +463,7 @@ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
463
463
  function computeDigestsForDir(dir) {
464
464
  const ext = process.platform === "win32" ? ".exe" : "";
465
465
  const out = {};
466
- for (const base of ["fallow", "fallow-lsp", "fallow-mcp"]) {
466
+ for (const base of ["fallow"]) {
467
467
  const fileName = `${base}${ext}`;
468
468
  const full = path.join(dir, fileName);
469
469
  out[fileName] =
@@ -549,7 +549,7 @@ test("verifyInstalled falls back to the provider when the embedded digest is mis
549
549
  },
550
550
  });
551
551
  assert.equal(result.ok, true);
552
- assert.equal(providerCalls, 3);
552
+ assert.equal(providerCalls, 1);
553
553
  });
554
554
 
555
555
  test("verifyInstalled falls back to the provider when fallowDigests is partial / malformed", async (t) => {
@@ -573,8 +573,8 @@ test("verifyInstalled falls back to the provider when fallowDigests is partial /
573
573
  },
574
574
  });
575
575
  assert.equal(result.ok, true);
576
- // One call per binary because all three entries fail to normalize.
577
- assert.equal(providerCalls, 3);
576
+ // One call for the single shipped binary because its entry fails to normalize.
577
+ assert.equal(providerCalls, 1);
578
578
  });
579
579
 
580
580
  test("verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary", async (t) => {
@@ -587,8 +587,6 @@ test("verifyInstalled returns digest-mismatch when the embedded digest disagrees
587
587
  version: "1.0.0",
588
588
  fallowDigests: {
589
589
  [`fallow${ext}`]: "sha256:" + "a".repeat(64),
590
- [`fallow-lsp${ext}`]: "sha256:" + "a".repeat(64),
591
- [`fallow-mcp${ext}`]: "sha256:" + "a".repeat(64),
592
590
  },
593
591
  });
594
592
  const result = await verifyInstalled({
@@ -644,9 +642,9 @@ test("verifyInstalledSync with embedded digests returns ok end-to-end", (t) => {
644
642
  assert.equal(result.package, "<override>");
645
643
  });
646
644
 
647
- test("verifyInstalledSync fails fast on first bad signature", (t) => {
645
+ test("verifyInstalledSync fails on a bad signature", (t) => {
648
646
  const { privateKey, rawPub } = makeKeypair();
649
- const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow-lsp" });
647
+ const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
650
648
  t.after(() => cleanup(dir));
651
649
  writeManifest(dir, {
652
650
  name: "@fallow-cli/x",
@@ -659,7 +657,7 @@ test("verifyInstalledSync fails fast on first bad signature", (t) => {
659
657
  });
660
658
  assert.equal(result.ok, false);
661
659
  assert.equal(result.code, "sig-invalid");
662
- assert.match(result.binary, /fallow-lsp/);
660
+ assert.match(result.binary, /fallow/);
663
661
  });
664
662
 
665
663
  test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedded digest", (t) => {
@@ -671,11 +669,7 @@ test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedd
671
669
  version: "9.9.9",
672
670
  fallowDigests: {
673
671
  fallow: "sha256:" + "a".repeat(64),
674
- "fallow-lsp": "sha256:" + "a".repeat(64),
675
- "fallow-mcp": "sha256:" + "a".repeat(64),
676
672
  "fallow.exe": "sha256:" + "a".repeat(64),
677
- "fallow-lsp.exe": "sha256:" + "a".repeat(64),
678
- "fallow-mcp.exe": "sha256:" + "a".repeat(64),
679
673
  },
680
674
  });
681
675
  const result = verifyInstalledSync({
@@ -106,11 +106,13 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
106
106
  | `dupes` | Code duplication detection | `--mode`, `--threshold`, `--top`, `--changed-since`, `--workspace`, `--changed-workspaces`, `--skip-local`, `--cross-language`, `--ignore-imports`, `--no-ignore-imports`, `--explain-skipped`, `--fail-on-regression`, `--tolerance`, `--regression-baseline`, `--save-regression-baseline` |
107
107
  | `health` | Function complexity analysis (also covers Angular templates as synthetic `<template>` findings: external `.html` files via `templateUrl` AND inline `@Component({ template: \`...\` })` literals; suppress external with `<!-- fallow-ignore-file complexity -->` at the top of the `.html` file, suppress inline with `// fallow-ignore-next-line complexity` directly above the `@Component` decorator) | `--complexity`, `--max-cyclomatic`, `--max-cognitive`, `--max-crap`, `--top`, `--sort`, `--file-scores`, `--hotspots`, `--ownership`, `--ownership-emails`, `--targets`, `--effort`, `--score`, `--min-score`, `--since`, `--min-commits`, `--save-snapshot`, `--trend`, `--coverage-gaps`, `--coverage`, `--coverage-root`, `--runtime-coverage`, `--min-invocations-hot`, `--min-observation-volume`, `--low-traffic-threshold`, `--css`, `--complexity-breakdown`, `--min-severity`, `--report-only`, `--workspace`, `--changed-workspaces`, `--baseline`, `--save-baseline` |
108
108
  | `flags` | Detect feature flag patterns (env vars, SDK calls, config objects) | `--top` |
109
+ | `suppressions` | List active fallow-ignore suppression markers (read-only inventory) | `--file` |
109
110
  | `explain` | Explain one issue type without running analysis | `<issue-type>`, `--format json` |
110
111
  | `audit` | Combined dead-code + complexity + duplication + styling for changed files, returns a verdict; `fallow review` is an alias for `fallow audit --brief` (advisory orientation brief, always exits 0) | `--base`, `--gate`, `--brief`, `--max-decisions`, `--walkthrough-guide`, `--walkthrough-file`, `--show-deprioritized`, `--production`, `--production-dead-code`, `--production-health`, `--production-dupes`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--explain`, `--explain-skipped`, `--dead-code-baseline`, `--health-baseline`, `--dupes-baseline`, `--max-crap`, `--coverage`, `--coverage-root`, `--no-css`, `--css-deep`, `--no-css-deep`, `--include-entry-exports` |
111
112
  | `decision-surface` | Surface the consequential structural DECISIONS a change embeds (the apex of the review brief), each framed as a judgment question with the routed expert to ask | `--max-decisions` |
112
113
  | `impact` | Show what fallow has done for you: how many issues it is surfacing, the trend since the last recorded run, and how many commits it contained at the pre-commit gate | `--all`, `--sort`, `--limit` |
113
114
  | `security` | Surface opt-in local security candidates for agent verification (not confirmed vulnerabilities). Rule families include the graph rule `client-server-leak`, a data-driven `tainted-sink` catalogue, and the include-required `hardcoded-secret` category for provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers. Most catalogue rows require non-literal input; narrowly literal-aware rows flag deterministic unsafe literals. Rules default off; suppress a file with `// fallow-ignore-file security-sink`; scope categories with `security.categories`. Add project-local request object names with `security.requestReceivers`; it extends the built-in `req` / `request` / `ctx` / `context` / `event` allowlist for HTTP `query`, `params`, and `body` reads. `hardcoded-secret` runs only when listed in `security.categories.include`. | `--format human\|json\|sarif`, `--changed-since`, `--file`, `--diff-file`, `--workspace`, `--changed-workspaces`, `--surface`, `--ci`, `--fail-on-issues`, `--sarif-file`, `--summary` |
115
+ | `report` | Render a saved `--format json` results file in another format without re-running analysis (analyze once, render annotations and the job summary from the same file). | `--from` |
114
116
  | `schema` | Dump CLI definition as JSON | |
115
117
  | `ci-template` | Print or vendor CI integration templates | |
116
118
  | `migrate` | Convert knip/jscpd config | `--dry-run`, `--from PATH` |
@@ -509,7 +509,7 @@ fallow health --format json --quiet --trend
509
509
  {
510
510
  "kind": "health",
511
511
  "schema_version": 7,
512
- "version": "3.3.0",
512
+ "version": "3.5.0",
513
513
  "elapsed_ms": 32,
514
514
  "summary": {
515
515
  "files_analyzed": 482,
@@ -907,7 +907,7 @@ fallow audit \
907
907
  {
908
908
  "kind": "audit",
909
909
  "schema_version": 7,
910
- "version": "3.3.0",
910
+ "version": "3.5.0",
911
911
  "command": "audit",
912
912
  "verdict": "fail",
913
913
  "changed_files_count": 12,
@@ -982,7 +982,7 @@ fallow flags --format json --quiet --workspace my-package
982
982
  ```json
983
983
  {
984
984
  "schema_version": 7,
985
- "version": "3.3.0",
985
+ "version": "3.5.0",
986
986
  "elapsed_ms": 116,
987
987
  "feature_flags": [],
988
988
  "total_flags": 0
@@ -1083,7 +1083,7 @@ fallow security --gate newly-reachable --changed-since origin/main
1083
1083
  {
1084
1084
  "kind": "security",
1085
1085
  "schema_version": "4",
1086
- "version": "3.3.0",
1086
+ "version": "3.5.0",
1087
1087
  "elapsed_ms": 42,
1088
1088
  "config": {
1089
1089
  "rules": {
@@ -1112,7 +1112,7 @@ fallow security --gate newly-reachable --changed-since origin/main
1112
1112
  {
1113
1113
  "kind": "security",
1114
1114
  "schema_version": "4",
1115
- "version": "3.3.0",
1115
+ "version": "3.5.0",
1116
1116
  "elapsed_ms": 42,
1117
1117
  "config": {
1118
1118
  "rules": {
@@ -1185,6 +1185,7 @@ Compose one evidence bundle before editing a file or exported symbol. This is th
1185
1185
  ```bash
1186
1186
  fallow inspect --file src/api.ts --format json --quiet
1187
1187
  fallow inspect --symbol src/api.ts:fetchUser --format json --quiet
1188
+ fallow inspect --file src/api.ts --churn --format json --quiet
1188
1189
  ```
1189
1190
 
1190
1191
  ### Target Flags
@@ -1193,6 +1194,7 @@ fallow inspect --symbol src/api.ts:fetchUser --format json --quiet
1193
1194
  |------|-------------|
1194
1195
  | `--file <PATH>` | Inspect one project-relative file |
1195
1196
  | `--symbol <FILE:EXPORT>` | Inspect one exported symbol. Supporting dead-code, duplication, complexity, and security evidence is file-scoped in the first version |
1197
+ | `--churn` | Add target-level git churn evidence. Off by default; missing git history is reported as `unavailable` |
1196
1198
 
1197
1199
  Common global flags: `--format`, `--quiet`, `--root`, `--config`, `--workspace`, `--production`, `--no-cache`, `--threads`.
1198
1200
 
@@ -1215,13 +1217,14 @@ Common global flags: `--format`, `--quiet`, `--root`, `--config`, `--workspace`,
1215
1217
  "dead_code": { "status": "ok", "scope": "file", "data": {} },
1216
1218
  "duplication": { "status": "ok", "scope": "project_filtered_to_file", "data": {} },
1217
1219
  "complexity": { "status": "ok", "scope": "project_filtered_to_file", "data": {} },
1218
- "security": { "status": "ok", "scope": "file", "data": {} }
1220
+ "security": { "status": "ok", "scope": "file", "data": {} },
1221
+ "churn": { "status": "ok", "scope": "project_filtered_to_file", "data": {} }
1219
1222
  },
1220
1223
  "warnings": []
1221
1224
  }
1222
1225
  ```
1223
1226
 
1224
- Each evidence section carries `status` and `scope`. Non-fatal child-analysis failures become section-level errors and warnings, so callers can still use the remaining evidence.
1227
+ Each evidence section carries `status` and `scope`. The optional churn section can report `ok`, `unavailable`, or `error`. Non-fatal analysis failures become section-level errors and warnings, so callers can still use the remaining evidence.
1225
1228
 
1226
1229
  ---
1227
1230
 
@@ -1632,7 +1635,8 @@ Available on all commands:
1632
1635
  |---|---|---|---|
1633
1636
  | `-r, --root` | `string` | - | Project root directory |
1634
1637
  | `-c, --config` | `string` | - | Config file path |
1635
- | `-f, --format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab\|badge` | `human` | Output format (alias: --output) |
1638
+ | `--allow-remote-extends` | `bool` | `false` | Allow trusted config files to extend HTTPS URLs |
1639
+ | `-f, --format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab\|badge\|github-annotations\|github-summary` | `human` | Output format (alias: --output) |
1636
1640
  | `-q, --quiet` | `bool` | `false` | Suppress progress output |
1637
1641
  | `--no-cache` | `bool` | `false` | Disable incremental caching |
1638
1642
  | `--threads` | `string` | - | Number of parser threads |
@@ -1660,6 +1664,7 @@ Available on all commands:
1660
1664
  | `--fail-on-issues` | `bool` | `false` | Exit 1 if any issues found (promotes `warn` to `error`) |
1661
1665
  | `--sarif-file` | `string` | - | Write SARIF output to a file instead of stdout |
1662
1666
  | `-o, --output-file` | `string` | - | Write the report to a file instead of stdout, for any --format (no ANSI codes). Useful on large projects where the terminal scrollback truncates the top. Progress and the confirmation stay on stderr |
1667
+ | `--report-path-prefix` | `string` | - | Prefix prepended to every path in the CI-facing formats (`github-annotations`, `github-summary`, `codeclimate`, `review-github`, `review-gitlab`). CI platforms address files by repository-root-relative path, so when the analyzed project lives in a subdirectory (e.g. `packages/app/`), paths need that offset. fallow detects the offset via the git toplevel automatically; this flag overrides the detection. Pass an empty string to disable rebasing and emit paths relative to `--root` |
1663
1668
  | `--fail-on-regression` | `bool` | `false` | Fail if issue count increased beyond tolerance vs a regression baseline |
1664
1669
  | `--tolerance` | `string` | `0` | Allowed increase: `"2%"` (percentage) or `"5"` (absolute). Default: `"0"` |
1665
1670
  | `--regression-baseline` | `string` | - | Path to regression baseline file (default: `.fallow/regression-baseline.json`) |
@@ -1796,7 +1801,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1796
1801
 
1797
1802
  ## CI Integration
1798
1803
 
1799
- - **GitHub Actions**: `uses: fallow-rs/fallow@v2` - supports SARIF upload to Code Scanning, inline PR annotations (`annotations: true`), PR comments, all commands. Annotations use workflow commands (no Advanced Security required); limit with `max-annotations` (default 50). Set `score: true` to compute health score and enable the health delta header in PR comments
1804
+ - **GitHub Actions**: `uses: fallow-rs/fallow@v3` - supports SARIF upload to Code Scanning, inline PR annotations (`annotations: true`), PR comments, all commands. Annotations use workflow commands (no Advanced Security required); limit with `max-annotations` (default 50). Set `score: true` to compute health score and enable the health delta header in PR comments
1800
1805
  - **GitLab CI**: include `ci/gitlab-ci.yml` template and extend `.fallow` - generates Code Quality reports via `--format codeclimate` / `--format gitlab-codequality` (inline MR annotations), rich MR comments, code review comments, all commands. Use `fallow ci-template gitlab --vendor` when runners cannot reach `raw.githubusercontent.com`; commit the generated `ci/` and `action/` files and use GitLab's local include syntax. Variables use `FALLOW_` prefix (e.g., `FALLOW_COMMAND`, `FALLOW_FAIL_ON_ISSUES`). Set `FALLOW_SCORE: "true"` to compute health score; `FALLOW_TREND: "true"` to compare against saved snapshots
1801
1806
  - **Any CI**: `npx fallow --ci` - equivalent to `--format sarif --fail-on-issues --quiet`
1802
1807
 
@@ -1831,7 +1836,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1831
1836
  {
1832
1837
  "kind": "dead-code",
1833
1838
  "schema_version": 7,
1834
- "version": "3.3.0",
1839
+ "version": "3.5.0",
1835
1840
  "elapsed_ms": 45,
1836
1841
  "total_issues": 12,
1837
1842
  "entry_points": {
@@ -1991,7 +1996,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
1991
1996
  {
1992
1997
  "kind": "dupes",
1993
1998
  "schema_version": 7,
1994
- "version": "3.3.0",
1999
+ "version": "3.5.0",
1995
2000
  "elapsed_ms": 82,
1996
2001
  "total_clones": 15,
1997
2002
  "total_lines_duplicated": 230,
@@ -2035,11 +2040,11 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
2035
2040
  {
2036
2041
  "kind": "combined",
2037
2042
  "schema_version": 7,
2038
- "version": "3.3.0",
2043
+ "version": "3.5.0",
2039
2044
  "elapsed_ms": 159,
2040
2045
  "check": {
2041
2046
  "schema_version": 7,
2042
- "version": "3.3.0",
2047
+ "version": "3.5.0",
2043
2048
  "elapsed_ms": 45,
2044
2049
  "total_issues": 12,
2045
2050
  "unused_files": [],