fallow 3.41.0 → 3.43.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.
@@ -23,6 +23,31 @@ function getPlatformPackage(platform, arch, libcFamily) {
23
23
  return null;
24
24
  }
25
25
 
26
+ // The platform packages declare `libc`, so npm installs only the package for
27
+ // the libc that it detects at install time. The musl binaries are statically
28
+ // linked and also run on glibc, so a glibc host accepts an installed musl
29
+ // package (for example a node_modules tree that was built on Alpine). A glibc
30
+ // binary does not run on musl, so there is no fallback in that direction.
31
+ function getPlatformPackageCandidates(platform, arch, libcFamily) {
32
+ const primary = getPlatformPackage(platform, arch, libcFamily);
33
+ if (primary === null) return [];
34
+ if (primary.endsWith("-gnu")) return [primary, primary.replace(/-gnu$/, "-musl")];
35
+ return [primary];
36
+ }
37
+
38
+ function resolveInstalledPlatformPackage(candidates, resolve = require.resolve) {
39
+ for (const pkg of candidates) {
40
+ try {
41
+ return { pkg, manifestPath: resolve(`${pkg}/package.json`) };
42
+ } catch {
43
+ // Not installed; try the next candidate.
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
26
49
  module.exports = {
27
50
  getPlatformPackage,
51
+ getPlatformPackageCandidates,
52
+ resolveInstalledPlatformPackage,
28
53
  };
@@ -13,20 +13,23 @@ const path = require("node:path");
13
13
  const fs = require("node:fs");
14
14
  const os = require("node:os");
15
15
 
16
- const { getPlatformPackage } = require("./platform-package");
16
+ const {
17
+ getPlatformPackageCandidates,
18
+ resolveInstalledPlatformPackage,
19
+ } = require("./platform-package");
17
20
  const { ensureVerified } = require("./lazy-verify");
18
21
  const { isPreSigningVersion } = require("./verify-binary");
19
22
 
20
- function resolvePlatformPackageName() {
21
- if (process.platform !== "linux") {
22
- return getPlatformPackage(process.platform, process.arch);
23
- }
23
+ const SIMILAR_CODE_PROVIDER_ENV = "FALLOW_SIMILAR_CODE_PROVIDER";
24
+
25
+ function detectLibcFamily() {
26
+ if (process.platform !== "linux") return undefined;
24
27
  try {
25
- const { familySync } = require("detect-libc");
26
- return getPlatformPackage(process.platform, process.arch, familySync());
28
+ return require("detect-libc").familySync();
27
29
  } catch {
28
- // musl binaries are statically linked and work on both glibc and musl
29
- return getPlatformPackage(process.platform, process.arch, "musl");
30
+ // Unknown libc selects the musl package: musl binaries are statically
31
+ // linked and work on both glibc and musl.
32
+ return null;
30
33
  }
31
34
  }
32
35
 
@@ -77,20 +80,28 @@ function describeVerifiedStatus(result) {
77
80
  // Resolve the platform package directory + manifest path, or print an
78
81
  // actionable error and exit. Keeps `runBinary` a flat top-level sequence.
79
82
  function resolvePlatformPaths() {
80
- const pkg = resolvePlatformPackageName();
81
- if (!pkg) {
83
+ const libcFamily = detectLibcFamily();
84
+ const candidates = getPlatformPackageCandidates(process.platform, process.arch, libcFamily);
85
+ if (candidates.length === 0) {
82
86
  process.stderr.write(`Unsupported platform: ${process.platform}-${process.arch}\n`);
83
87
  process.exit(1);
84
88
  }
85
- try {
86
- const manifestPath = require.resolve(`${pkg}/package.json`);
87
- return { pkg, manifestPath, platformPkgDir: path.dirname(manifestPath) };
88
- } catch {
89
- process.stderr.write(
90
- `Could not find ${pkg}. Run 'npm install' to install the platform-specific binary.\n`,
91
- );
89
+ const resolved = resolveInstalledPlatformPackage(candidates);
90
+ if (resolved === null) {
91
+ process.stderr.write(missingPlatformPackageMessage(candidates[0], libcFamily));
92
92
  process.exit(1);
93
93
  }
94
+ return { ...resolved, platformPkgDir: path.dirname(resolved.manifestPath) };
95
+ }
96
+
97
+ // npm installs a Linux platform package only for the libc that it detects at
98
+ // install time, so a tree installed for another libc, or from macOS or
99
+ // Windows without `--libc`, has no matching binary.
100
+ function missingPlatformPackageMessage(pkg, libcFamily) {
101
+ const hint = libcFamily
102
+ ? ` This host uses ${libcFamily}. When node_modules was installed on another libc or OS, reinstall with \`npm install --os=linux --cpu=${process.arch} --libc=${libcFamily}\`.`
103
+ : "";
104
+ return `Could not find ${pkg}. Run 'npm install' to install the platform-specific binary.${hint}\n`;
94
105
  }
95
106
 
96
107
  function printVerifyError(verifyResult) {
@@ -134,22 +145,113 @@ function readResolvedVersion(manifestPath) {
134
145
  }
135
146
  }
136
147
 
148
+ // `fallow` does not depend on its companions (`fallow-type-aware`,
149
+ // `fallow-similar-code`): a project installs one at the same version when it
150
+ // uses it. npm and Yarn hoist it next to `fallow`. pnpm links it only into the
151
+ // project node_modules, so a second lookup starts at the project root. That
152
+ // lookup accepts a companion only when it is in the same node_modules tree as
153
+ // the running `fallow` package, so a global `fallow` never runs a companion
154
+ // that a cloned project ships. Only an exact version match is accepted.
155
+ function resolveCompanionManifest(
156
+ packageName,
157
+ fallowVersion,
158
+ {
159
+ resolvePackage = require.resolve,
160
+ readFile = fs.readFileSync,
161
+ projectRoot = process.cwd(),
162
+ launcherDir = __dirname,
163
+ } = {},
164
+ ) {
165
+ const local = matchingManifest(packageName, fallowVersion, resolvePackage, readFile, undefined);
166
+ if (local !== undefined) return local;
167
+ const project = matchingManifest(packageName, fallowVersion, resolvePackage, readFile, {
168
+ paths: [projectRoot],
169
+ });
170
+ if (project === undefined) return undefined;
171
+ const tree = installTree(launcherDir);
172
+ return tree !== null && tree === installTree(project) ? project : undefined;
173
+ }
174
+
175
+ function matchingManifest(packageName, fallowVersion, resolvePackage, readFile, options) {
176
+ try {
177
+ const manifestPath = resolvePackage(`${packageName}/package.json`, options);
178
+ const manifest = JSON.parse(readFile(manifestPath, "utf8"));
179
+ return manifest.version === fallowVersion ? manifestPath : undefined;
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ // The outermost node_modules directory of a real path, or null outside one.
186
+ function installTree(file) {
187
+ let real;
188
+ try {
189
+ real = fs.realpathSync.native(file);
190
+ } catch {
191
+ return null;
192
+ }
193
+ const parts = real.split(path.sep);
194
+ const index = parts.indexOf("node_modules");
195
+ return index === -1 ? null : parts.slice(0, index + 1).join(path.sep);
196
+ }
197
+
137
198
  function resolveTypeAwareCompanion(
138
199
  fallowVersion,
139
200
  resolvePackage = require.resolve,
140
201
  readFile = fs.readFileSync,
202
+ projectRoot = process.cwd(),
203
+ launcherDir = __dirname,
141
204
  ) {
205
+ const manifestPath = resolveCompanionManifest("fallow-type-aware", fallowVersion, {
206
+ resolvePackage,
207
+ readFile,
208
+ projectRoot,
209
+ launcherDir,
210
+ });
211
+ if (manifestPath === undefined) return undefined;
212
+ const companion = path.join(path.dirname(manifestPath), "fallow-type-aware.mjs");
213
+ return fs.existsSync(companion) ? companion : undefined;
214
+ }
215
+
216
+ // The binary of the exact-version `fallow-similar-code` package, after its own
217
+ // resolver and verifier accept it (the Node API loader uses the same pair).
218
+ // The Rust code checks the signature again before it runs the binary.
219
+ function resolveSimilarCodeProvider(fallowVersion, options = {}) {
220
+ const { report = reportToStderr } = options;
221
+ const manifestPath = resolveCompanionManifest("fallow-similar-code", fallowVersion, options);
222
+ if (manifestPath === undefined) return undefined;
223
+ const scripts = path.join(path.dirname(manifestPath), "scripts");
142
224
  try {
143
- const manifestPath = resolvePackage("fallow-type-aware/package.json");
144
- const manifest = JSON.parse(readFile(manifestPath, "utf8"));
145
- if (manifest.version !== fallowVersion) return undefined;
146
- const companion = path.join(path.dirname(manifestPath), "fallow-type-aware.mjs");
147
- return fs.existsSync(companion) ? companion : undefined;
225
+ const { resolveBinaryArtifact, resolvePlatformPackage } = require(
226
+ path.join(scripts, "run-binary.js"),
227
+ );
228
+ const { verifyBinary } = require(path.join(scripts, "verify-binary.js"));
229
+ const artifact = resolveBinaryArtifact(resolvePlatformPackage());
230
+ const verification = verifyBinary(artifact);
231
+ if (verification.ok) return artifact.binaryPath;
232
+ report(`fallow: the similar-code companion failed verification: ${verification.message}\n`);
233
+ } catch (error) {
234
+ report(`fallow: the similar-code companion failed verification: ${error.message}\n`);
235
+ }
236
+ return undefined;
237
+ }
238
+
239
+ // Write to fd 2 directly: under Bun, `process.stderr` makes the pipes that the
240
+ // child inherits non-blocking (issue #3276). A failed write is ignored.
241
+ function reportToStderr(message) {
242
+ try {
243
+ fs.writeSync(2, message);
148
244
  } catch {
149
- return undefined;
245
+ // Best effort.
150
246
  }
151
247
  }
152
248
 
249
+ // Only these commands start the similar-code provider, so other runs skip the
250
+ // lookup and its digest check.
251
+ function usesSimilarCode(args) {
252
+ return args.includes("similar-code") || args.includes("mcp-server");
253
+ }
254
+
153
255
  function typeAwareCommand(
154
256
  companion,
155
257
  { platform = process.platform, execPath = process.execPath } = {},
@@ -163,14 +265,27 @@ function childEnvironment(
163
265
  resolvedVersion,
164
266
  resolveCompanion = resolveTypeAwareCompanion,
165
267
  commandOptions,
268
+ { args = [], resolveProvider = resolveSimilarCodeProvider } = {},
166
269
  ) {
167
- if (process.env.FALLOW_TYPE_AWARE_BIN) return process.env;
168
- const companion = resolveCompanion(resolvedVersion);
169
- if (companion === undefined) return process.env;
270
+ let environment = process.env;
271
+ if (!process.env.FALLOW_TYPE_AWARE_BIN) {
272
+ const companion = resolveCompanion(resolvedVersion);
273
+ if (companion !== undefined) {
274
+ environment = typeAwareEnvironment(environment, typeAwareCommand(companion, commandOptions));
275
+ }
276
+ }
277
+ if (!process.env[SIMILAR_CODE_PROVIDER_ENV] && usesSimilarCode(args)) {
278
+ const provider = resolveProvider(resolvedVersion);
279
+ if (provider !== undefined) {
280
+ environment = { ...environment, [SIMILAR_CODE_PROVIDER_ENV]: provider };
281
+ }
282
+ }
283
+ return environment;
284
+ }
170
285
 
171
- const command = typeAwareCommand(companion, commandOptions);
286
+ function typeAwareEnvironment(base, command) {
172
287
  const environment = {
173
- ...process.env,
288
+ ...base,
174
289
  FALLOW_TYPE_AWARE_BIN: command.binary,
175
290
  // Marks the wiring as launcher-provided node_modules resolution so
176
291
  // `type-aware status` does not report it as a user-set override.
@@ -232,7 +347,9 @@ function runBinary(binaryBaseName, options = {}) {
232
347
  try {
233
348
  execFileSync(binaryPath, [...prependArgs, ...process.argv.slice(2)], {
234
349
  stdio: "inherit",
235
- env: childEnvironment(resolvedVersion),
350
+ env: childEnvironment(resolvedVersion, undefined, undefined, {
351
+ args: [...prependArgs, ...process.argv.slice(2)],
352
+ }),
236
353
  });
237
354
  } catch (e) {
238
355
  if (e.status === undefined) throw e;
@@ -254,6 +371,8 @@ module.exports = {
254
371
  isVersionQuery, // test-only
255
372
  guardBrokenStdout, // test-only
256
373
  exitCodeForChildFailure, // test-only
374
+ missingPlatformPackageMessage, // test-only
257
375
  resolveTypeAwareCompanion, // test-only
376
+ resolveSimilarCodeProvider, // test-only
258
377
  childEnvironment, // test-only
259
378
  };
@@ -31,7 +31,10 @@ const crypto = require("node:crypto");
31
31
  const fs = require("node:fs");
32
32
  const https = require("node:https");
33
33
  const path = require("node:path");
34
- const { getPlatformPackage } = require("./platform-package");
34
+ const {
35
+ getPlatformPackageCandidates,
36
+ resolveInstalledPlatformPackage,
37
+ } = require("./platform-package");
35
38
 
36
39
  const GITHUB_REPO = "fallow-rs/fallow";
37
40
  const DIGEST_TIMEOUT_MS = 10000;
@@ -296,16 +299,10 @@ function binaryTargetsForPlatform(platformId) {
296
299
  // and tests can synthesize a Windows verify without running on Windows.
297
300
  const isWindows = typeof platformId === "string" && platformId.startsWith("win32");
298
301
  const ext = isWindows ? ".exe" : "";
299
- // The platform package ships the multicall CLI plus the local-only
300
- // similar-code provider. Both are signed and digest-pinned. Verifying them
301
- // together keeps the Rust sibling lookup free of project or PATH overrides.
302
- return [
303
- { binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` },
304
- {
305
- binary: `fallow-similar-code${ext}`,
306
- asset: `fallow-similar-code-${platformId}${ext}`,
307
- },
308
- ];
302
+ // The platform package ships only the multicall CLI. The similar-code
303
+ // provider ships in the `fallow-similar-code` package, whose own verifier
304
+ // checks it.
305
+ return [{ binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` }];
309
306
  }
310
307
 
311
308
  function isSkipRequested(env = process.env) {
@@ -313,17 +310,24 @@ function isSkipRequested(env = process.env) {
313
310
  return v === "1" || v === "true" || v === "yes";
314
311
  }
315
312
 
316
- function currentPlatformPackageName() {
317
- if (process.platform !== "linux") {
318
- return getPlatformPackage(process.platform, process.arch);
319
- }
313
+ // The same selection as the launcher in run-binary.js, so that verification
314
+ // checks the binary that the launcher runs.
315
+ function currentPlatformPackageName(resolveFrom) {
320
316
  let libcFamily;
321
- try {
322
- libcFamily = require("detect-libc").familySync();
323
- } catch {
324
- libcFamily = undefined;
317
+ if (process.platform === "linux") {
318
+ try {
319
+ libcFamily = require("detect-libc").familySync();
320
+ } catch {
321
+ libcFamily = undefined;
322
+ }
325
323
  }
326
- return getPlatformPackage(process.platform, process.arch, libcFamily);
324
+ const candidates = getPlatformPackageCandidates(process.platform, process.arch, libcFamily);
325
+ if (candidates.length === 0) return null;
326
+ const options = resolveFrom ? { paths: [resolveFrom] } : undefined;
327
+ const installed = resolveInstalledPlatformPackage(candidates, (request) =>
328
+ require.resolve(request, options),
329
+ );
330
+ return installed ? installed.pkg : candidates[0];
327
331
  }
328
332
 
329
333
  function readManifestForPackage(manifestPath, pkg) {
@@ -366,7 +370,7 @@ function resolvePlatformPackageForVerify(opts) {
366
370
  };
367
371
  }
368
372
 
369
- const pkg = currentPlatformPackageName();
373
+ const pkg = currentPlatformPackageName(opts.resolveFrom);
370
374
  if (!pkg) {
371
375
  return {
372
376
  ok: false,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fallow
3
- description: Codebase intelligence for TypeScript and JavaScript. Static analysis reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Optional local similar-code discovery finds functions that may implement the same intent despite different syntax. Runtime coverage can merge production execution data. Use when asked to audit PR risk, find unused code or dependencies, compare semantically similar functions, detect duplicates, inspect architecture boundaries, merge runtime coverage, auto-fix supported issues, or run fallow.
3
+ description: Codebase intelligence for TypeScript and JavaScript. Static analysis reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Opt-in mutation testing reports the code changes that no test detects. Optional local similar-code discovery finds functions that may implement the same intent despite different syntax. Runtime coverage can merge production execution data. Use when asked to audit PR risk, find unused code or dependencies, compare semantically similar functions, detect duplicates, inspect architecture boundaries, merge runtime coverage, improve or check the tests of a change, auto-fix supported issues, or run fallow.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -19,6 +19,7 @@ Fallow analyzes code and styles for changed-code risk, cleanup opportunities, ci
19
19
  - Investigate why a specific export, dependency, file, or issue type was reported.
20
20
  - Surface local security candidates for an agent to verify (`fallow security`).
21
21
  - Find untested but runtime-reachable code (`fallow health --coverage-gaps`).
22
+ - Improve or check the tests of a change: find the small code changes (mutants) that no test detects (`fallow mutation`, opt-in, because a run executes the project tests).
22
23
  - Rank complexity hotspots, owners, and refactoring targets (`fallow health --hotspots --ownership --targets`).
23
24
  - Review what fallow has surfaced over time (`fallow impact`).
24
25
  - Confirm exact TypeScript symbol use, affected tests, API leaks, or public type coupling when syntactic evidence is insufficient (`--type-aware`).
@@ -92,6 +93,7 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
92
93
  | prioritize refactoring | `fallow health --hotspots --targets` |
93
94
  | ask who owns code | `fallow health --ownership` |
94
95
  | check untested-but-reachable code | `fallow health --coverage-gaps` |
96
+ | improve or check the tests of a change | `fallow mutation --plan --base <ref>`; The plan runs no code. Without `--plan`, the command runs the project tests against each mutant, so ask the user first. |
95
97
  | consolidate duplication | `fallow dupes --trace dup:<fingerprint>` |
96
98
  | find feature flags | `fallow flags` |
97
99
  | check which architecture rules apply to a file before changing it | `fallow guard <files>` |
@@ -183,6 +185,17 @@ Step 3 is your review. Read `digest.decisions[]` and `change_anchors[]` in the g
183
185
 
184
186
  Fallow validates the judgment against the live graph. It rejects an id that it did not emit. It refuses the whole file as `stale` when the hash no longer matches. Report only the `accepted` judgments. Keep the guide outside the project: a new file in the tree changes the hash. Do not create `.fallow/` for it, because a folder that you create has no ignore file. On `stale: true`, fetch the guide again.
185
187
 
188
+ ### Improve the tests of a change
189
+ ```bash
190
+ fallow mutation --plan --base main --format json --quiet # the plan: runs no code
191
+ fallow mutation --base main --format json --quiet # the run: executes the project tests
192
+ fallow mutation tasks --format json --quiet # the open mutants: runs no code
193
+ fallow mutation inspect <mutant_id> --format json --quiet # one mutant: runs no code
194
+ fallow mutation --recheck --format json --quiet # after new tests: runs only the open mutants
195
+ ```
196
+
197
+ Start the run when the user asks for better tests or for mutation testing. In each other task, show the plan and ask. A run exits 2 on uncommitted files: pass `--allow-dirty`, and do not commit for the user. A `survived` mutant is a code change that no test detects. Write a behavior test for each one that a caller can observe, then recheck. Do not write a test that only pins an implementation detail, and do not pass `--allow-side-effects` unless the user asks. The runners are Vitest, `node:test` and `bun test`. The Fallow plugin has a `fallow-mutation` skill with the full method.
198
+
186
199
  ### Find code duplication
187
200
  ```bash
188
201
  fallow dupes --format json --quiet
@@ -215,6 +228,13 @@ fallow dead-code --format json --quiet --production
215
228
 
216
229
  Excludes test/dev files (`*.test.*`, `*.spec.*`, `*.stories.*`) and only analyzes production scripts.
217
230
 
231
+ ### Review parameters that each call passes in one way
232
+ ```bash
233
+ fallow dead-code --constant-parameters --production --format json --quiet
234
+ ```
235
+
236
+ The opt-in rule reports a function parameter that each visible call passes the same value (`identical-argument`), or an optional parameter that no call passes (`never-passed`). The same value is the same literal, or the same `const` with a literal initializer or enum member (`value.kind` is `binding`). A finding with `option_key` is about one key of an object-pattern parameter, such as `retries` in `{ retries = 3 }`. It reports only when it sees each caller, and it never reports a function that a test file declares. Start with findings where `condition_reads` is above 0: a condition tests the constant, so a branch cannot run. Read the `inspected_call_sites` and the intent before a change. Keep a deliberate option, a test seam, or a parameter that matches a sibling API. `--production` drops test callers. With `--type-aware`, the rule also reports `unused-default` (each call passes a value whose type excludes `undefined`, so the default never applies) and reads the methods of exported classes (`function_name` is `Class.method`). These findings carry `evidence: "type-aware"`: a type does not prove a run-time value, so check that no `any` value or cast reaches the parameter. There is no auto-fix.
237
+
218
238
  ### Analyze specific workspaces
219
239
  ```bash
220
240
  fallow dead-code --format json --quiet --workspace my-package # single package (lists: web,admin)
@@ -355,7 +375,9 @@ fallow dead-code --type-aware --symbol-impact src/api.ts:Client --format json --
355
375
  fallow health --type-aware --type-coupling --format json --quiet
356
376
  ```
357
377
 
358
- The optional companion must match the installed Fallow version. Semantic
378
+ The `fallow-type-aware` companion is an install on demand
379
+ (`npm install --save-dev fallow-type-aware@<fallow version>`), and it must
380
+ match the installed Fallow version. Semantic
359
381
  results expose completeness, per-candidate decisions, and omissions.
360
382
  `confirmed-used` and `contract-preserved` remove syntactic false positives.
361
383
  `confirmed-no-static-references` retains the finding and only enables a guarded
@@ -375,7 +397,7 @@ Auto-detects `knip.json`, `knip.jsonc`, `.knip.json`, `.knip.jsonc`, `.jscpd.jso
375
397
  ```bash
376
398
  fallow init # creates .fallowrc.json, adds .fallow/ to .gitignore (--toml for fallow.toml)
377
399
  fallow init --agents # scaffolds a starter AGENTS.md prefilled from detected project info (never overwrites)
378
- fallow hooks install --target git # pre-commit gate; --branch <ref> sets the fallback base branch
400
+ fallow hooks install --target git # pre-commit gate: audits the staged index (fallow audit --staged)
379
401
  ```
380
402
 
381
403
  ## Exit Codes