fallow 3.28.0 → 3.30.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/schema.json CHANGED
@@ -190,13 +190,14 @@
190
190
  "$ref": "#/$defs/TypeAwareConfig"
191
191
  },
192
192
  "rules": {
193
- "description": "Sets per-issue-type severity, keyed by kebab-case rule id: `error` reports and fails CI (non-zero exit), `warn` reports without failing, `off` disables detection and reporting entirely (e.g. `{ \"unused-files\": \"error\", \"unused-exports\": \"warn\", \"private-type-leaks\": \"off\" }`). Set a rule `off` to silence it, `warn` to demote below CI gating, or `error` to promote a warn/off-default rule to gating; most rules default to `error`, dev/optional-dependency and component/store/inject/CSS/catalog rules default to `warn`, and opt-in rules (`private-type-leaks`, `security-*`, `prop-drilling`, `thin-wrapper`, `duplicate-prop-shape`, `coverage-gaps`, `feature-flags`, `require-suppression-reason`) default to `off`. Singular aliases (`unused-file`) and `warning`/`none` severity spellings are accepted.",
193
+ "description": "Sets per-issue-type severity, keyed by kebab-case rule id: `error` reports and fails CI (non-zero exit), `warn` reports without failing, `off` disables detection and reporting entirely (e.g. `{ \"unused-files\": \"error\", \"unused-exports\": \"warn\", \"private-type-leaks\": \"off\" }`). Set a rule `off` to silence it, `warn` to demote below CI gating, or `error` to promote a warn/off-default rule to gating; most rules default to `error`, dev/optional-dependency and component/store/inject/CSS/catalog rules default to `warn`, and opt-in rules (`private-type-leaks`, `deprecated-exports-in-use`, `security-*`, `prop-drilling`, `thin-wrapper`, `duplicate-prop-shape`, `coverage-gaps`, `feature-flags`, `require-suppression-reason`) default to `off`. Singular aliases (`unused-file`) and `warning`/`none` severity spellings are accepted.",
194
194
  "$ref": "#/$defs/RulesConfig",
195
195
  "default": {
196
196
  "unused-files": "error",
197
197
  "unused-exports": "error",
198
198
  "unused-types": "error",
199
199
  "private-type-leaks": "off",
200
+ "deprecated-exports-in-use": "off",
200
201
  "unused-dependencies": "error",
201
202
  "unused-dev-dependencies": "warn",
202
203
  "unused-optional-dependencies": "warn",
@@ -220,6 +221,9 @@
220
221
  "css-selector-complexity": "warn",
221
222
  "css-dead-surface": "warn",
222
223
  "css-broken-reference": "warn",
224
+ "complexity-cyclomatic": "error",
225
+ "complexity-cognitive": "error",
226
+ "complexity-crap": "error",
223
227
  "unresolved-imports": "error",
224
228
  "unlisted-dependencies": "error",
225
229
  "duplicate-exports": "error",
@@ -261,7 +265,7 @@
261
265
  }
262
266
  },
263
267
  "flags": {
264
- "description": "Configures feature-flag detection: `sdkPatterns` (custom flag-evaluating call signatures, each `{ function, nameArg (zero-based arg index of the flag name, default 0), provider? }`, merged with built-ins for LaunchDarkly, Statsig, Unleash, GrowthBook, Split, PostHog, Vercel Flags, ConfigCat, Flagsmith, Optimizely, and Eppo), `envPrefixes` (env-var prefixes marking `process.env.*` accesses as flags, merged with built-ins), and `configObjectHeuristics` (default false; when true, property accesses on objects whose name contains `feature`/`flag`/`toggle` are reported as low-confidence flags). Set `sdkPatterns`/`envPrefixes` to teach fallow a proprietary flag SDK or naming convention, or enable `configObjectHeuristics` for projects that read flags off config objects (higher false-positive rate). Feature-flag findings surface only when the `feature-flags` rule is enabled (default `off`).",
268
+ "description": "Configures feature-flag detection: `sdkPatterns` (custom flag-evaluating call signatures, each `{ function, nameArg (zero-based arg index of the flag name, default 0), provider? }`, merged with built-ins for LaunchDarkly, Statsig, Unleash, GrowthBook, Split, PostHog, Vercel Flags, ConfigCat, Flagsmith, Optimizely, and Eppo), `envPrefixes` (env-var prefixes marking `process.env.*` and `import.meta.env.*` accesses as flags, merged with built-ins), and `configObjectHeuristics` (default false; when true, property accesses on objects whose name contains `feature`/`flag`/`toggle` are reported as low-confidence flags). Set `sdkPatterns`/`envPrefixes` to teach fallow a proprietary flag SDK or naming convention, or enable `configObjectHeuristics` for projects that read flags off config objects (higher false-positive rate). Feature-flag findings surface only when the `feature-flags` rule is enabled (default `off`).",
265
269
  "$ref": "#/$defs/FlagsConfig",
266
270
  "default": {
267
271
  "configObjectHeuristics": false
@@ -363,7 +367,12 @@
363
367
  "default": false
364
368
  },
365
369
  "autoImports": {
366
- "description": "When true, drops Nuxt convention-based entry-pattern fallbacks so genuinely-unreferenced convention files surface as unused-file. Component fallbacks are kept when nuxt.config customizes components: in a way fallow does not model, and composable/util fallbacks are kept when it customizes imports:; a config that scans no more than the Nuxt defaults (components: false, components: [], components: { dirs: [] }, components: true, imports: { scan: false }, imports: {}, imports: { dirs: [] }) is treated like the default and its fallbacks are dropped; imports: { autoImport: false } on its own is not, because it only switches the injection off while the same directories stay registered behind #imports. A nuxt.config that carries any top-level key fallow cannot read statically, a computed key, an accessor, or a spread such as { ...base, devtools: {} }, keeps both surfaces' fallbacks for that root, because the spread may hold the keys that decide. Each root is classified on its own config, so one custom nuxt.config in a monorepo does not keep every other workspace's fallbacks. Component names follow Nuxt: a component under components/global or components/islands is named after its own directory (components/global/Foo.vue is `<Foo>`), and .client, .server, .global and .island suffixes are stripped. A name imported or re-exported by hand from #components or #imports credits its convention file just like a template tag or a bare call; a namespace import names nothing and credits nothing. Boolean, defaults to false; set it for a Nuxt project that has explicitly configured its auto-import directories. Synthesis of auto-import graph edges (resolving `<Card />` or `useUserStore()` to their convention files) happens regardless of this flag.",
370
+ "description": "When true, drops Nuxt convention-based entry-pattern fallbacks so genuinely-unreferenced convention files surface as unused-file. Component fallbacks are kept when nuxt.config customizes components: in a way fallow does not model, and composable/util fallbacks are kept when it customizes imports:; a config that scans no more than the Nuxt defaults (components: false, components: [], components: { dirs: [] }, components: true, imports: { scan: false }, imports: {}, imports: { dirs: [] }) is treated like the default and its fallbacks are dropped; imports: { autoImport: false } on its own is not, because it only switches the injection off while the same directories stay registered behind #imports. A nuxt.config that carries any top-level key fallow cannot read statically, a computed key, an accessor, or a spread such as { ...base, devtools: {} }, keeps both surfaces' fallbacks for that root, because the spread may hold the keys that decide. Only top-level keys count when the config object is readable: a nested key such as a routeRules path ending in components does not keep a fallback. Nested components or imports keys in $production, $development, $test or `$env.<name>`, and components or imports hooks (a components:* key or a nested components object in hooks, also inside those overrides), count as that surface's key; such an override object or hooks object that fallow cannot read statically falls back to a text match for the keys. Each root is classified on its own config plus the configs of its local layers, so one custom nuxt.config in a monorepo does not keep every other workspace's fallbacks. A local layer is a directory named in extends, or a `layers/<name>` directory, that holds a nuxt.config; its convention files are entry points with the flag off and auto-import sources with it on, and `#layers/<name>/` resolves to it by its $meta.name (a `layers/<name>` directory without one uses its directory name). A remote or package layer is only a dependency reference. Component names follow Nuxt: a component under components/global or components/islands is named after its own directory (components/global/Foo.vue is `<Foo>`), and .client, .server, .global and .island suffixes are stripped. A name imported or re-exported by hand from #components or #imports credits its convention file just like a template tag or a bare call; a namespace import names nothing and credits nothing. Boolean, defaults to false; set it for a Nuxt project that has explicitly configured its auto-import directories. Synthesis of auto-import graph edges (resolving `<Card />` or `useUserStore()` to their convention files) happens regardless of this flag.",
371
+ "type": "boolean",
372
+ "default": false
373
+ },
374
+ "failOnParseError": {
375
+ "description": "When true, the run fails with exit code 1 if fallow could not parse a source file cleanly (a `source-parse-degraded` entry in `workspace_diagnostics[]`), and the `parse-error` entry of `gate_outcomes` names each such file. Boolean, defaults to false, so a file the parser rejects only warns. Applies to `fallow dead-code`, `fallow health`, `fallow audit` and the bare `fallow` run; the CLI flag `--fail-on-parse-error` arms the same gate for one run. Older fallow versions reject this key as unknown, so pair it with `minimumVersion` set to the release that added it.",
367
376
  "type": "boolean",
368
377
  "default": false
369
378
  },
@@ -1055,7 +1064,7 @@
1055
1064
  "default": 15
1056
1065
  },
1057
1066
  "maxCrap": {
1058
- "description": "Maximum allowed CRAP (Change Risk Anti-Patterns) score per function\n(default: 30.0). CRAP combines cyclomatic complexity with test\ncoverage: high complexity plus low coverage produces a high CRAP\nscore. Functions meeting or exceeding this threshold are reported.\nUse `--coverage` with Istanbul data for accurate per-function CRAP;\notherwise fallow estimates coverage from the module graph. Governs\nfindings and the threshold-relative file-score signals\n(`crap_above_threshold`, the `risk` triage tag, and the\n`add_test_coverage` refactoring target); measured values such as\n`crap_max` and the overall health score never move with it. Set to\n`0` to disable CRAP enforcement entirely: no findings, nothing counts\nabove threshold, and file-score rows disclose baseline breaches as\nexempt instead.",
1067
+ "description": "Maximum allowed CRAP (Change Risk Anti-Patterns) score per function\n(default: 30.0). CRAP combines cyclomatic complexity with test\ncoverage: high complexity plus low coverage produces a high CRAP\nscore. Functions meeting or exceeding this threshold are reported.\nUse `--coverage` with Istanbul data for accurate per-function CRAP;\notherwise fallow estimates coverage from the module graph. Governs\nfindings and the threshold-relative file-score signals\n(`crap_above_threshold`, the `risk` triage tag, and the\n`add_test_coverage` refactoring target); measured values such as\n`crap_max` and the overall health score never move with it. Set to\n`0` to disable CRAP enforcement entirely: no findings, nothing counts\nabove threshold, and file-score rows disclose baseline breaches as\nexempt instead. The `complexity-crap` rule is different: it decides if\na finding fails the run, and `off` hides the findings but keeps the\nfile-score signals.",
1059
1068
  "type": "number",
1060
1069
  "format": "double",
1061
1070
  "default": 30.0
@@ -1076,7 +1085,7 @@
1076
1085
  "default": 60
1077
1086
  },
1078
1087
  "coverage": {
1079
- "description": "Path to Istanbul-format coverage data for accurate per-function CRAP\nscores. Relative paths resolve against the project root. The CLI\n`--coverage` flag and `FALLOW_COVERAGE` environment variable override\nthis value. Consulted by `fallow health`, bare `fallow`, `fallow audit`,\n`fallow viz`, and the MCP `audit` / `check_health` tools.",
1088
+ "description": "Path to Istanbul coverage data (coverage-final.json) or raw V8 coverage\n(a `NODE_V8_COVERAGE` directory or one V8 JSON file) for accurate\nper-function CRAP scores. Relative paths resolve against the project root. The CLI\n`--coverage` flag and `FALLOW_COVERAGE` environment variable override\nthis value. Consulted by `fallow health`, bare `fallow`, `fallow audit`,\n`fallow viz`, and the MCP `audit` / `check_health` tools.",
1080
1089
  "type": [
1081
1090
  "string",
1082
1091
  "null"
@@ -1310,6 +1319,11 @@
1310
1319
  "$ref": "#/$defs/Severity",
1311
1320
  "default": "off"
1312
1321
  },
1322
+ "deprecated-exports-in-use": {
1323
+ "description": "An export marked `@deprecated` that still has at least one reachable\nreference. Opt-in; defaults to `off`. A per-path override resolves on\nthe file that declares the export, not on the consumer.",
1324
+ "$ref": "#/$defs/Severity",
1325
+ "default": "off"
1326
+ },
1313
1327
  "unused-dependencies": {
1314
1328
  "description": "A declared `dependencies` entry never observed used. Defaults to\n`error`.",
1315
1329
  "$ref": "#/$defs/Severity",
@@ -1425,6 +1439,21 @@
1425
1439
  "$ref": "#/$defs/Severity",
1426
1440
  "default": "warn"
1427
1441
  },
1442
+ "complexity-cyclomatic": {
1443
+ "description": "A function above the cyclomatic ceiling (`health.maxCyclomatic` or a\n`thresholdOverrides` entry). The threshold decides if the finding\nexists; this rule decides if it fails the run. Defaults to `error`.\n`warn` reports the finding without a failure, and `off` hides it. A\nfinding above several ceilings takes the most severe of their rules.\nThe rule applies before the `health --min-severity` band gate.\nAfter you set a kind to `off`, save the health baseline again: its\nentries for the hidden findings no longer match.",
1444
+ "$ref": "#/$defs/Severity",
1445
+ "default": "error"
1446
+ },
1447
+ "complexity-cognitive": {
1448
+ "description": "A function above the cognitive ceiling (`health.maxCognitive` or a\n`thresholdOverrides` entry). The threshold decides if the finding\nexists; this rule decides if it fails the run. Defaults to `error`.",
1449
+ "$ref": "#/$defs/Severity",
1450
+ "default": "error"
1451
+ },
1452
+ "complexity-crap": {
1453
+ "description": "A function above the CRAP ceiling (`health.maxCrap` or a\n`thresholdOverrides` entry). The threshold decides if the finding\nexists; this rule decides if it fails the run. Defaults to `error`.\n`off` hides the findings only. `health.maxCrap: 0` also turns off the\nthreshold-relative file-score signals.",
1454
+ "$ref": "#/$defs/Severity",
1455
+ "default": "error"
1456
+ },
1428
1457
  "unresolved-imports": {
1429
1458
  "description": "An import specifier that resolves to no file or package. Defaults to\n`error`.",
1430
1459
  "$ref": "#/$defs/Severity",
@@ -1795,7 +1824,7 @@
1795
1824
  }
1796
1825
  },
1797
1826
  "envPrefixes": {
1798
- "description": "Environment variable prefixes that indicate feature flags.\nMerged with built-in prefixes. Only `process.env.*` accesses matching\nthese prefixes are reported as feature flags.",
1827
+ "description": "Environment variable prefixes that indicate feature flags.\nMerged with built-in prefixes. Only `process.env.*` and\n`import.meta.env.*` accesses matching these prefixes are reported as\nfeature flags.",
1799
1828
  "type": "array",
1800
1829
  "items": {
1801
1830
  "type": "string"
@@ -1805,6 +1834,13 @@
1805
1834
  "description": "Enable config object heuristic detection.\nWhen true, property accesses on objects whose name contains \"feature\",\n\"flag\", or \"toggle\" are reported as low-confidence feature flags.\nDefault: false (opt-in due to higher false positive rate).",
1806
1835
  "type": "boolean",
1807
1836
  "default": false
1837
+ },
1838
+ "vendorKeyPrefix": {
1839
+ "description": "Prefix to remove from each key of a `--flag-state` vendor export\nbefore the key is compared with the flag names in the code. For\nexample, `\"web.\"` makes the vendor key `web.new-checkout` match the\ncode flag `new-checkout`. A key without the prefix is compared as it\nis.",
1840
+ "type": [
1841
+ "string",
1842
+ "null"
1843
+ ]
1808
1844
  }
1809
1845
  }
1810
1846
  },
@@ -2049,6 +2085,17 @@
2049
2085
  }
2050
2086
  ]
2051
2087
  },
2088
+ "deprecated-exports-in-use": {
2089
+ "description": "Optional override for [`RulesConfig::deprecated_exports_in_use`].",
2090
+ "anyOf": [
2091
+ {
2092
+ "$ref": "#/$defs/Severity"
2093
+ },
2094
+ {
2095
+ "type": "null"
2096
+ }
2097
+ ]
2098
+ },
2052
2099
  "unused-dependencies": {
2053
2100
  "description": "Optional override for [`RulesConfig::unused_dependencies`].",
2054
2101
  "anyOf": [
@@ -2302,6 +2349,39 @@
2302
2349
  }
2303
2350
  ]
2304
2351
  },
2352
+ "complexity-cyclomatic": {
2353
+ "description": "Optional override for [`RulesConfig::complexity_cyclomatic`].",
2354
+ "anyOf": [
2355
+ {
2356
+ "$ref": "#/$defs/Severity"
2357
+ },
2358
+ {
2359
+ "type": "null"
2360
+ }
2361
+ ]
2362
+ },
2363
+ "complexity-cognitive": {
2364
+ "description": "Optional override for [`RulesConfig::complexity_cognitive`].",
2365
+ "anyOf": [
2366
+ {
2367
+ "$ref": "#/$defs/Severity"
2368
+ },
2369
+ {
2370
+ "type": "null"
2371
+ }
2372
+ ]
2373
+ },
2374
+ "complexity-crap": {
2375
+ "description": "Optional override for [`RulesConfig::complexity_crap`].",
2376
+ "anyOf": [
2377
+ {
2378
+ "$ref": "#/$defs/Severity"
2379
+ },
2380
+ {
2381
+ "type": "null"
2382
+ }
2383
+ ]
2384
+ },
2305
2385
  "unresolved-imports": {
2306
2386
  "description": "Optional override for [`RulesConfig::unresolved_imports`].",
2307
2387
  "anyOf": [
@@ -15,7 +15,7 @@
15
15
  // bin/fallow exits non-zero before execing the binary. FALLOW_SKIP_BINARY_VERIFY
16
16
  // remains the documented escape hatch.
17
17
  //
18
- // Refs: SECURITY.md "Binary distribution and verification".
18
+ // See SECURITY.md for binary distribution and verification.
19
19
  //
20
20
  // No external deps beyond node:fs / node:path / node:crypto.
21
21
 
@@ -24,7 +24,12 @@ const path = require("node:path");
24
24
  const crypto = require("node:crypto");
25
25
 
26
26
  const { resolveSentinelPath } = require("./sentinel-path");
27
- const { verifyInstalledSync, SKIP_ENV } = require("./verify-binary");
27
+ const {
28
+ verifyInstalledSync,
29
+ binaryTargetsForPlatform,
30
+ isSkipRequested,
31
+ SKIP_ENV,
32
+ } = require("./verify-binary");
28
33
 
29
34
  // Bumped to 2 when SHA-256 + platformPkgDir binding landed (closes the
30
35
  // cross-install reuse gap in the shared $XDG fallback cache), and to 3 when
@@ -66,11 +71,10 @@ function emitVerifyLog(env, payload) {
66
71
  process.stderr.write(`fallow-verify ${parts.join(" ")}\n`);
67
72
  }
68
73
 
69
- function binaryTargetsForPlatform(platform) {
70
- // Track every executable the multicall CLI may launch without another
71
- // wrapper verification boundary.
72
- const ext = platform === "win32" ? ".exe" : "";
73
- return [`fallow${ext}`, `fallow-similar-code${ext}`];
74
+ // The sentinel binds the same binaries that verify-binary checks, so a new
75
+ // binary cannot pass verification without also invalidating the sentinel.
76
+ function binaryNamesForPlatform(platform) {
77
+ return binaryTargetsForPlatform(platform).map((t) => t.binary);
74
78
  }
75
79
 
76
80
  function statMtimeMs(absPath) {
@@ -145,7 +149,7 @@ function sha256OfFile(absPath) {
145
149
  // integrity gate that defends against same-mtime cross-install reuse where a
146
150
  // tampered binary happens to land with the recorded mtime.
147
151
  function sentinelBinariesMatch(parsed, platformPkgDir, platform) {
148
- for (const target of binaryTargetsForPlatform(platform)) {
152
+ for (const target of binaryNamesForPlatform(platform)) {
149
153
  const recorded = parsed.binaries[target];
150
154
  if (!recorded || typeof recorded.mtimeMs !== "number") return false;
151
155
  if (typeof recorded.sha256 !== "string" || recorded.sha256.length !== 64) return false;
@@ -173,7 +177,7 @@ function isSentinelValid(sentinelPath, platformPkgDir, manifest, platform) {
173
177
 
174
178
  function buildSentinelPayload(platformPkgDir, manifest, platform) {
175
179
  const binaries = {};
176
- for (const target of binaryTargetsForPlatform(platform)) {
180
+ for (const target of binaryNamesForPlatform(platform)) {
177
181
  const binaryPath = path.join(platformPkgDir, target);
178
182
  const mtimeMs = statMtimeMs(binaryPath);
179
183
  const sha256 = sha256OfFile(binaryPath);
@@ -213,11 +217,6 @@ function writeSentinel(sentinelPath, payload) {
213
217
  }
214
218
  }
215
219
 
216
- function isSkipRequested(env) {
217
- const v = (env || process.env)[SKIP_ENV];
218
- return v === "1" || v === "true" || v === "yes";
219
- }
220
-
221
220
  // Main entry point. Synchronous by design: bin/fallow runs this before
222
221
  // execFileSync, so the verify result must be available without awaiting.
223
222
  //
@@ -300,7 +299,7 @@ function ensureVerified(input) {
300
299
  platform = process.platform,
301
300
  } = input || {};
302
301
 
303
- if (isSkipRequested(env)) {
302
+ if (isSkipRequested(env || process.env)) {
304
303
  const reason = `${SKIP_ENV} is set`;
305
304
  // Warn once per process so the bypass stays visible in CI logs and
306
305
  // vendor audits regardless of whether the user runs `--version` or
@@ -364,6 +363,5 @@ function _resetWarningState() {
364
363
  module.exports = {
365
364
  ensureVerified,
366
365
  SENTINEL_SCHEMA_VERSION,
367
- VERIFY_LOG_ENV,
368
366
  _resetWarningState,
369
367
  };
@@ -5,14 +5,9 @@ const fs = require("node:fs");
5
5
  const os = require("node:os");
6
6
  const path = require("node:path");
7
7
 
8
- const {
9
- ensureVerified,
10
- SENTINEL_SCHEMA_VERSION,
11
- VERIFY_LOG_ENV,
12
- _resetWarningState,
13
- } = require("./lazy-verify");
8
+ const { ensureVerified, SENTINEL_SCHEMA_VERSION, _resetWarningState } = require("./lazy-verify");
14
9
  const { SENTINEL_FILENAME } = require("./sentinel-path");
15
- const { _verifyWithKey, SKIP_ENV } = require("./verify-binary");
10
+ const { _verifyWithKey, binaryTargetsForPlatform, SKIP_ENV } = require("./verify-binary");
16
11
 
17
12
  // ---- shared fixtures ------------------------------------------------------
18
13
 
@@ -149,6 +144,28 @@ test("ensureVerified verifies and caches a win32 executable on any host", (t) =>
149
144
  assert.equal(cached.cached, true);
150
145
  });
151
146
 
147
+ test("sentinel records the same binaries that verify-binary verifies", (t) => {
148
+ const cases = [
149
+ { platform: "linux", platformId: "linux-x64-gnu" },
150
+ { platform: "win32", platformId: "win32-x64-msvc" },
151
+ ];
152
+ for (const { platform, platformId } of cases) {
153
+ _resetWarningState();
154
+ const { privateKey, rawPub } = makeKeypair();
155
+ const dir = mkPlatformDir(privateKey, { platform });
156
+ t.after(() => cleanup(dir));
157
+ const input = baseInput(dir, (binaryPath) => _verifyWithKey(binaryPath, rawPub), {
158
+ platform,
159
+ });
160
+
161
+ const result = ensureVerified(input);
162
+ assert.equal(result.ok, true);
163
+ const sentinel = JSON.parse(fs.readFileSync(result.sentinelPath, "utf8"));
164
+ const verified = binaryTargetsForPlatform(platformId).map((target) => target.binary);
165
+ assert.deepEqual(Object.keys(sentinel.binaries), verified);
166
+ }
167
+ });
168
+
152
169
  test("ensureVerified returns cached:true on a valid sentinel", (t) => {
153
170
  _resetWarningState();
154
171
  const { privateKey, rawPub } = makeKeypair();
@@ -543,9 +560,3 @@ test("ensureVerified warns once on stderr when FALLOW_SKIP_BINARY_VERIFY is set"
543
560
  );
544
561
  assert.equal(warnings.length, 1, "warning should fire exactly once per process");
545
562
  });
546
-
547
- // ---- VERIFY_LOG_ENV export ------------------------------------------------
548
-
549
- test("VERIFY_LOG_ENV is exported with the documented name", () => {
550
- assert.equal(VERIFY_LOG_ENV, "FALLOW_VERIFY_LOG");
551
- });
@@ -9,7 +9,7 @@
9
9
  // 4. Every location read-only: returns { path: null, location: 'none', writable: false }.
10
10
  // Callers run verify on every invocation and surface FALLOW_SKIP_BINARY_VERIFY=1 as the escape.
11
11
  //
12
- // Refs RFC 868 (npm/cli#9360). See .plans/rfc-868-lazy-binary-verify.md.
12
+ // See SECURITY.md for binary distribution and verification.
13
13
 
14
14
  const fs = require("node:fs");
15
15
  const os = require("node:os");
@@ -19,8 +19,8 @@ const SENTINEL_FILENAME = ".fallow-verified";
19
19
 
20
20
  // Returns true when the directory exists and the current process can create
21
21
  // a file in it. Tries an atomic O_CREAT|O_EXCL write so we never disturb an
22
- // existing sentinel during the writability probe. Falls back to fs.accessSync
23
- // when mkdtempSync fails for non-permission reasons.
22
+ // existing sentinel during the writability probe. Any failed probe makes this
23
+ // location unavailable so resolution can try the next cache directory.
24
24
  function isWritable(dir) {
25
25
  if (typeof dir !== "string" || dir.length === 0) {
26
26
  return false;
@@ -126,7 +126,7 @@ function tryXdgFallback(env, homeDir, platformId, filename, ensureDir, writableP
126
126
  }
127
127
 
128
128
  // Resolve the sentinel path according to the cascade documented above.
129
- // Dependency-inject env / homedir / platform / fsProbe so the unit tests can
129
+ // Dependency-inject env / homedir / platform / isWritable / ensureDir so tests can
130
130
  // exercise every branch without touching the real filesystem state.
131
131
  //
132
132
  // Returns: {
@@ -89,10 +89,7 @@ test("resolveSentinelPath falls back to FALLOW_VERIFY_CACHE_DIR when platform pk
89
89
  }
90
90
  });
91
91
 
92
- test("resolveSentinelPath honors FALLOW_VERIFY_CACHE_DIR even when platform pkg dir IS writable", () => {
93
- // Per the cascade documented in the source, the platform pkg dir wins when
94
- // writable. The cache-dir env is the FALLBACK for when the platform dir is
95
- // read-only. We pass a non-existent platform dir to force the fallback.
92
+ test("resolveSentinelPath uses FALLOW_VERIFY_CACHE_DIR when platform pkg dir is unset", () => {
96
93
  const cacheDir = mkTmp();
97
94
  try {
98
95
  const result = resolveSentinelPath({
@@ -308,8 +308,8 @@ function binaryTargetsForPlatform(platformId) {
308
308
  ];
309
309
  }
310
310
 
311
- function isSkipRequested() {
312
- const v = process.env[SKIP_ENV];
311
+ function isSkipRequested(env = process.env) {
312
+ const v = env[SKIP_ENV];
313
313
  return v === "1" || v === "true" || v === "yes";
314
314
  }
315
315
 
@@ -664,4 +664,6 @@ module.exports = {
664
664
  EMBEDDED_PUBLIC_KEY,
665
665
  ED25519_SPKI_HEADER,
666
666
  SKIP_ENV,
667
+ binaryTargetsForPlatform,
668
+ isSkipRequested,
667
669
  };
@@ -6,10 +6,10 @@ license: MIT
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. Broad framework plugin coverage, 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 and cold-path deletion confidence, 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
- - Find cleanup opportunities: unused files, exports, types, members, dependencies, or stale flags.
12
+ - Find cleanup opportunities: unused files, exports, types, members, dependencies, or feature flags that guard unused exports.
13
13
  - Detect code duplication, circular dependencies, architecture boundary issues, and complexity hotspots.
14
14
  - Find functions that may implement the same intent despite different names, syntax, or control flow (`fallow similar-code`).
15
15
  - Check styling consistency, CSS dead surface, and design-token drift.
@@ -192,9 +192,10 @@ Reports unused exports in entry files (package.json `main`/`exports`, framework
192
192
  ```bash
193
193
  fallow flags --format json --quiet
194
194
  fallow flags --format json --quiet --top 20
195
+ fallow flags --retirement --format json --quiet
195
196
  ```
196
197
 
197
- Reports environment-variable gates (`process.env.FEATURE_*`), SDK calls from common flag providers, and config-object patterns, with flag locations, detection confidence, and a cross-reference against dead code. Only `--top N` is command-specific.
198
+ Reports environment-variable gates (`process.env.FEATURE_*`), SDK calls from common flag providers, and config-object patterns, with flag locations, detection confidence, and a cross-reference against dead code. `--top N` limits the list. `--retirement` adds a `retirement` object with one row per flag, the reasons it can be retired (`single-read-site`, `test-only`, `literal-constant`, `identical-branches`, `empty-branch`, `guards-dead-code`, `defined-never-read`), and its age from git (`--flag-age blame|pickaxe|off`; blame gives a lower bound). Filter with `--reason <CODE>` and `--min-age <DAYS>`, order with `--sort age|sites|name`. `--flag-state <FILE>` reads an offline vendor export in one vendor-neutral schema and adds `fully-rolled-out`, `archived-in-vendor`, `missing-in-vendor` and `vendor-only`. With `--retirement`, `--save-regression-baseline <PATH>` and `--fail-on-regression --regression-baseline <PATH>` gate on `distinct_flags` (plus each `--reason` count), and the opt-in `--max-flag-age <DAYS>` fails on old flags. Every format works: compact prints `flag-retire:<reason>:<path>:<line>:<name>`, SARIF adds the rule `fallow/flag-retirement-candidate`, CodeClimate adds `fallow/flag-retirement`. The report is advisory: every action has `auto_fixable: false`, and a person decides what to remove.
198
199
 
199
200
  ### Surface security candidates for verification
200
201
  ```bash
@@ -260,7 +261,7 @@ fallow dead-code --format json --quiet --save-baseline .fallow/snapshot.json
260
261
  fallow dead-code --format json --quiet --baseline .fallow/snapshot.json
261
262
  ```
262
263
 
263
- `--save-regression-baseline` / `--regression-baseline` / `--fail-on-regression` / `--tolerance` are count-based gates for `dead-code` and bare combined mode. `--save-baseline` / `--baseline` are identity-based (track finding identity, fail on new). `audit` rejects the global baseline flags and uses `--dead-code-baseline` / `--health-baseline` / `--dupes-baseline` instead.
264
+ `--save-regression-baseline` / `--regression-baseline` / `--fail-on-regression` / `--tolerance` are count-based gates for `dead-code`, bare combined mode, and `flags --retirement` (a flags baseline needs a PATH; without `--retirement` the options have no effect on `flags` and it warns). `--save-baseline` / `--baseline` are identity-based (track finding identity, fail on new). `audit` rejects the global baseline flags and uses `--dead-code-baseline` / `--health-baseline` / `--dupes-baseline` instead.
264
265
 
265
266
  With no path, `--save-regression-baseline` updates `regression.baseline` in the discovered fallow config, or creates `.fallowrc.json` when none exists. Pass a path only when a standalone baseline file is preferred.
266
267