fallow 3.17.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ npx fallow fix --dry-run # Preview automatic cleanup
28
28
 
29
29
  ## Output and exit codes
30
30
 
31
- Add `--format json --quiet` to any command for one typed JSON document on stdout. Exit code 1 means findings, not failure; 0 is clean (or an audit pass or warn verdict); 2 is a validation or runtime error, reported as a JSON error envelope rather than a stack trace.
31
+ Add `--format json --quiet` to any command for one typed JSON document on stdout. Exit code 1 means findings, not failure; 0 is clean (or an audit pass or warn verdict); 2 is a validation or runtime error, reported as a JSON error envelope rather than a stack trace. License, coverage setup, network, and security-gate workflows use additional documented codes; read `fallow schema.exit_codes` instead of suppressing the process status.
32
32
 
33
33
  Parsing the output in TypeScript? Import the typed shapes, version-pinned to the CLI you install:
34
34
 
package/capabilities.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.17.0",
3
+ "version": "3.18.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": [
@@ -815,7 +815,7 @@
815
815
  "name": "--unused-dependency-overrides",
816
816
  "type": "bool",
817
817
  "required": false,
818
- "description": "Only report unused pnpm dependency overrides",
818
+ "description": "Only report unused package-manager dependency overrides",
819
819
  "possible_values": [
820
820
  "true",
821
821
  "false"
@@ -825,7 +825,7 @@
825
825
  "name": "--misconfigured-dependency-overrides",
826
826
  "type": "bool",
827
827
  "required": false,
828
- "description": "Only report misconfigured pnpm dependency overrides",
828
+ "description": "Only report misconfigured package-manager dependency overrides",
829
829
  "possible_values": [
830
830
  "true",
831
831
  "false"
@@ -1659,13 +1659,13 @@
1659
1659
  "name": "--coverage",
1660
1660
  "type": "string",
1661
1661
  "required": false,
1662
- "description": "Path to Istanbul-format coverage data (coverage-final.json) for accurate per-function CRAP scores in the health sub-analysis. Also configurable via FALLOW_COVERAGE"
1662
+ "description": "Path to Istanbul-format coverage data (coverage-final.json) for accurate per-function CRAP scores in the health sub-analysis. Also configurable via FALLOW_COVERAGE or health.coverage"
1663
1663
  },
1664
1664
  {
1665
1665
  "name": "--coverage-root",
1666
1666
  "type": "string",
1667
1667
  "required": false,
1668
- "description": "Absolute prefix to strip from coverage data paths before CRAP matching. Use when coverage was generated under a different checkout root in CI or Docker"
1668
+ "description": "Absolute prefix to strip from coverage data paths before CRAP matching. Use when coverage was generated under a different checkout root in CI or Docker. Also configurable via FALLOW_COVERAGE_ROOT or health.coverageRoot"
1669
1669
  },
1670
1670
  {
1671
1671
  "name": "--no-css",
@@ -3094,7 +3094,7 @@
3094
3094
  "rule_id": "fallow/unused-dependency-override",
3095
3095
  "command": "dead-code",
3096
3096
  "category": "Dependencies",
3097
- "description": "pnpm.overrides entry targets a package not declared or resolved",
3097
+ "description": "Package-manager override target is not declared or resolved",
3098
3098
  "label": "Unused Dependency Overrides",
3099
3099
  "config_key": "unused-dependency-overrides",
3100
3100
  "registry_index": 42,
@@ -3135,7 +3135,7 @@
3135
3135
  "rule_id": "fallow/misconfigured-dependency-override",
3136
3136
  "command": "dead-code",
3137
3137
  "category": "Dependencies",
3138
- "description": "pnpm.overrides entry has an unparsable key or value",
3138
+ "description": "Package-manager override has an unparsable key or value",
3139
3139
  "label": "Misconfigured Dependency Overrides",
3140
3140
  "config_key": "misconfigured-dependency-overrides",
3141
3141
  "registry_index": 43,
@@ -5923,18 +5923,32 @@
5923
5923
  "sarif",
5924
5924
  "compact",
5925
5925
  "markdown",
5926
+ "md",
5926
5927
  "codeclimate",
5927
5928
  "gitlab-codequality",
5929
+ "gitlab-code-quality",
5928
5930
  "pr-comment-github",
5929
5931
  "pr-comment-gitlab",
5930
5932
  "review-github",
5931
5933
  "review-gitlab",
5932
- "badge"
5934
+ "badge",
5935
+ "github-annotations",
5936
+ "github-summary"
5933
5937
  ],
5934
5938
  "exit_codes": {
5935
5939
  "0": "Success (no error-severity issues found)",
5936
- "1": "Error-severity issues found (per rules config, or --fail-on-issues promotes warnerror)",
5937
- "2": "Error (invalid config, invalid input, etc.). When --format json is active, errors are emitted as structured JSON on stdout: {\"error\": true, \"message\": \"...\", \"exit_code\": 2}"
5940
+ "1": "Error-severity issues found (per rules config, or --fail-on-issues promotes warn to error)",
5941
+ "2": "Error (invalid config, invalid input, etc.). When --format json is active, errors are emitted as structured JSON on stdout: {\"error\": true, \"message\": \"...\", \"exit_code\": 2}",
5942
+ "3": "Requested resource unavailable: config --path found no config, or a license is missing, invalid, or beyond its offline hard-fail window",
5943
+ "4": "Runtime coverage sidecar is unavailable, unverifiable, protocol-incompatible, or terminated unexpectedly",
5944
+ "5": "Runtime coverage input could not be prepared or parsed",
5945
+ "6": "Runtime coverage sidecar reported an internal error",
5946
+ "7": "Network or cloud request failed during a license, coverage, or CI operation",
5947
+ "8": "Security gate matched a candidate selected by --gate",
5948
+ "10": "Coverage inventory or static-findings upload input or project validation failed",
5949
+ "11": "Coverage inventory or static-findings upload exceeded the server payload limit",
5950
+ "12": "Coverage inventory or static-findings upload authentication or authorization was rejected",
5951
+ "13": "Coverage inventory or static-findings upload failed after retries or returned another server error"
5938
5952
  },
5939
5953
  "environment_variables": {
5940
5954
  "FALLOW_FORMAT": "Default output format (json/human/sarif/compact/markdown/codeclimate/gitlab-codequality/pr-comment-github/pr-comment-gitlab/review-github/review-gitlab/badge/github-annotations/github-summary). CLI --format flag overrides this.",
@@ -5949,14 +5963,14 @@
5949
5963
  "FALLOW_PR_COMMENT_LAYOUT": "Sticky PR comment layout: default, compact, gate-only, or details.",
5950
5964
  "FALLOW_CONSOLIDATED_STATUS": "When split PR gate check runs are enabled, truthy values add one aggregate Fallow check alongside the per-gate checks.",
5951
5965
  "FALLOW_DIFF_CONTEXT": "Line radius around changed diff lines when scoping findings to a diff in the review/PR-comment formats (default 3).",
5952
- "FALLOW_BOT_LOGIN": "Bot or token username treated as fallow's own when reconciling existing PR/MR comments in review-github/review-gitlab. Required when posting with a personal access token (the author then carries a human identity).",
5966
+ "FALLOW_BOT_LOGIN": "Allowlisted posting username used to recognize fallow-owned finding roots and resolution replies. When set, it narrows ownership to that exact username (an empty value matches nobody); when unset, provider-native bot metadata is trusted. This is an authorship control, not a lifecycle or deduplication token.",
5953
5967
  "FALLOW_API_RETRIES": "Maximum HTTP attempts for review-comment reconciliation API calls (default 3).",
5954
5968
  "FALLOW_API_RETRY_DELAY": "Floor delay in seconds between HTTP retry attempts (default 2); a server-supplied Retry-After overrides it on 429 responses.",
5955
5969
  "FALLOW_CACHE_DIR": "Directory for fallow's persistent analysis cache. Relative paths resolve from the project root and override cache.dir.",
5956
5970
  "FALLOW_CACHE_MAX_SIZE": "Extraction cache size cap in megabytes (default 256). Wins over the cache.maxSizeMb config field.",
5957
5971
  "FALLOW_EXTENDS_TIMEOUT_SECS": "Timeout in seconds for fetching https:// configs referenced via the extends field (default 5).",
5958
- "FALLOW_COVERAGE": "Path to Istanbul coverage data (coverage-final.json) for accurate per-function CRAP scores. CLI --coverage flag overrides this.",
5959
- "FALLOW_COVERAGE_ROOT": "Absolute coverage-data path prefix for rebasing Istanbul paths in CI or containers. CLI --coverage-root flag overrides this.",
5972
+ "FALLOW_COVERAGE": "Path to Istanbul coverage data (coverage-final.json) for accurate per-function CRAP scores. CLI --coverage flag overrides this; it wins over the health.coverage config field. Honored by the health, bare fallow, and audit CLI commands and by the MCP audit and check_health tools on both their typed route and their CLI fallback, where the explicit coverage parameter overrides it.",
5973
+ "FALLOW_COVERAGE_ROOT": "Absolute coverage-data path prefix for rebasing Istanbul paths in CI or containers. CLI --coverage-root flag overrides this; it wins over the health.coverageRoot config field. Honored by the health, bare fallow, and audit CLI commands and by the MCP audit and check_health tools on both their typed route and their CLI fallback, where the explicit coverage_root parameter overrides it.",
5960
5974
  "FALLOW_MAX_FILE_SIZE": "Per-file size ceiling in megabytes for source discovery (default 5; 0 = no limit). CLI --max-file-size flag overrides this.",
5961
5975
  "FALLOW_TYPE_AWARE": "Enable or disable TypeScript semantic (type-aware) analysis for the run (true/false/1/0/yes/no/on/off). Precedence: --type-aware/--no-type-aware CLI flags, then FALLOW_TYPE_AWARE, then the audit.typeAware config field, then typeAware.enabled.",
5962
5976
  "FALLOW_TYPE_AWARE_BIN": "Trusted executable override for the TypeScript semantic refinement sidecar used by dead-code --type-aware. Relative paths resolve from the caller's working directory before --root is applied; project node_modules and PATH are intentionally not searched. Default: sibling of the active Fallow executable.",
@@ -6837,7 +6851,7 @@
6837
6851
  {
6838
6852
  "name": "trace_symbol",
6839
6853
  "kind": "trace",
6840
- "description": "Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat semantic.references, semantic.status, and semantic.identity as the authoritative exact evidence. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface.",
6854
+ "description": "Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat semantic.references, semantic.status, and semantic.identity as the authoritative exact evidence. The proof covers only the lane named by semantic.target.namespace, so a root trace that lists a reference the proof does not is wider evidence rather than stale. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface.",
6841
6855
  "cli_command": "fallow dead-code --type-aware --trace <file:export> --format json --quiet",
6842
6856
  "key_params": [
6843
6857
  "file",
@@ -1558,7 +1558,7 @@
1558
1558
  "rule_id": "fallow/unused-dependency-override",
1559
1559
  "command": "dead-code",
1560
1560
  "category": "Dependencies",
1561
- "description": "pnpm.overrides entry targets a package not declared or resolved",
1561
+ "description": "Package-manager override target is not declared or resolved",
1562
1562
  "label": "Unused Dependency Overrides",
1563
1563
  "config_key": "unused-dependency-overrides",
1564
1564
  "registry_index": 42,
@@ -1599,7 +1599,7 @@
1599
1599
  "rule_id": "fallow/misconfigured-dependency-override",
1600
1600
  "command": "dead-code",
1601
1601
  "category": "Dependencies",
1602
- "description": "pnpm.overrides entry has an unparsable key or value",
1602
+ "description": "Package-manager override has an unparsable key or value",
1603
1603
  "label": "Misconfigured Dependency Overrides",
1604
1604
  "config_key": "misconfigured-dependency-overrides",
1605
1605
  "registry_index": 43,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.17.0",
3
+ "version": "3.18.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": {
@@ -87,14 +87,14 @@
87
87
  "@tanstack/intent": "0.3.6"
88
88
  },
89
89
  "optionalDependencies": {
90
- "@fallow-cli/darwin-arm64": "3.17.0",
91
- "@fallow-cli/darwin-x64": "3.17.0",
92
- "@fallow-cli/linux-x64-gnu": "3.17.0",
93
- "@fallow-cli/linux-arm64-gnu": "3.17.0",
94
- "@fallow-cli/linux-x64-musl": "3.17.0",
95
- "@fallow-cli/linux-arm64-musl": "3.17.0",
96
- "@fallow-cli/win32-arm64-msvc": "3.17.0",
97
- "@fallow-cli/win32-x64-msvc": "3.17.0",
98
- "fallow-type-aware": "3.17.0"
90
+ "@fallow-cli/darwin-arm64": "3.18.0",
91
+ "@fallow-cli/darwin-x64": "3.18.0",
92
+ "@fallow-cli/linux-x64-gnu": "3.18.0",
93
+ "@fallow-cli/linux-arm64-gnu": "3.18.0",
94
+ "@fallow-cli/linux-x64-musl": "3.18.0",
95
+ "@fallow-cli/linux-arm64-musl": "3.18.0",
96
+ "@fallow-cli/win32-arm64-msvc": "3.18.0",
97
+ "@fallow-cli/win32-x64-msvc": "3.18.0",
98
+ "fallow-type-aware": "3.18.0"
99
99
  }
100
100
  }
package/schema.json CHANGED
@@ -1028,7 +1028,7 @@
1028
1028
  "default": 60
1029
1029
  },
1030
1030
  "coverage": {
1031
- "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.",
1031
+ "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`,\nand the MCP `audit` / `check_health` tools.",
1032
1032
  "type": [
1033
1033
  "string",
1034
1034
  "null"
@@ -1036,7 +1036,7 @@
1036
1036
  "default": null
1037
1037
  },
1038
1038
  "coverageRoot": {
1039
- "description": "Absolute prefix to strip from Istanbul file paths before CRAP matching.\nUse when coverage was generated under a different checkout root in CI\nor Docker. The CLI `--coverage-root` flag and `FALLOW_COVERAGE_ROOT`\nenvironment variable override this value.",
1039
+ "description": "Absolute prefix to strip from Istanbul file paths before CRAP matching.\nUse when coverage was generated under a different checkout root in CI\nor Docker. The CLI `--coverage-root` flag and `FALLOW_COVERAGE_ROOT`\nenvironment variable override this value. Consulted by `fallow health`,\nbare `fallow`, `fallow audit`, and the MCP `audit` / `check_health`\ntools.",
1040
1040
  "type": [
1041
1041
  "string",
1042
1042
  "null"
@@ -1458,12 +1458,12 @@
1458
1458
  "default": "error"
1459
1459
  },
1460
1460
  "unused-dependency-overrides": {
1461
- "description": "A pnpm `overrides:` / `pnpm.overrides` entry whose target package no\nworkspace `package.json` declares. Defaults to `warn`.",
1461
+ "description": "A pnpm, npm, or Bun override entry whose target package no workspace\n`package.json` declares and the active readable lockfile does not\nresolve. Defaults to `warn`.",
1462
1462
  "$ref": "#/$defs/Severity",
1463
1463
  "default": "warn"
1464
1464
  },
1465
1465
  "misconfigured-dependency-overrides": {
1466
- "description": "A pnpm `overrides:` / `pnpm.overrides` entry whose key or value cannot\nbe parsed into a valid pnpm shape. Defaults to `error`.",
1466
+ "description": "A pnpm, npm, or Bun override or Bun `resolutions` entry whose key or\nvalue cannot be parsed in its declaration source's grammar. Defaults to\n`error`.",
1467
1467
  "$ref": "#/$defs/Severity",
1468
1468
  "default": "error"
1469
1469
  },
@@ -235,6 +235,9 @@ function isSkipRequested(env) {
235
235
  // production install path always has fallowDigests)
236
236
  // env - process.env (defaults to process.env)
237
237
  // platform - process.platform (defaults to process.platform)
238
+ // homedir - os.homedir() override (tests only)
239
+ // isWritable - sentinel directory probe override (tests only)
240
+ // ensureDir - sentinel directory creation override (tests only)
238
241
  // logger - function (line: string) -> void (defaults to stderr)
239
242
  //
240
243
  // Returns one of:
@@ -253,6 +256,16 @@ function buildVerifyOptions(input, manifest) {
253
256
  return opts;
254
257
  }
255
258
 
259
+ function buildSentinelOptions(input, platformPkgDir, packageName, env, platform) {
260
+ const opts = { platformPkgDir, packageName, env, platform };
261
+ for (const key of ["homedir", "isWritable", "ensureDir"]) {
262
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
263
+ opts[key] = input[key];
264
+ }
265
+ }
266
+ return opts;
267
+ }
268
+
256
269
  // Persist the sentinel on a successful verify. Logs (warn-once) when the
257
270
  // resolved cache location is read-only or every cascade step failed.
258
271
  function persistSentinel(sentinel, platformPkgDir, manifest, platform) {
@@ -315,7 +328,9 @@ function ensureVerified(input) {
315
328
  };
316
329
  }
317
330
 
318
- const sentinel = resolveSentinelPath({ platformPkgDir, packageName, env, platform });
331
+ const sentinel = resolveSentinelPath(
332
+ buildSentinelOptions(input || {}, platformPkgDir, packageName, env, platform),
333
+ );
319
334
 
320
335
  // Cache hit: sentinel exists, schema matches, mtimes match, version matches.
321
336
  if (sentinel.path && isSentinelValid(sentinel.path, platformPkgDir, manifest, platform)) {
@@ -16,6 +16,9 @@ const { _verifyWithKey, SKIP_ENV } = require("./verify-binary");
16
16
 
17
17
  // ---- shared fixtures ------------------------------------------------------
18
18
 
19
+ const DEFAULT_PLATFORM = "linux";
20
+ const DEFAULT_PACKAGE_NAME = "@fallow-cli/test-platform";
21
+
19
22
  function makeKeypair() {
20
23
  const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
21
24
  const spki = publicKey.export({ format: "der", type: "spki" });
@@ -23,18 +26,18 @@ function makeKeypair() {
23
26
  return { privateKey, rawPub };
24
27
  }
25
28
 
26
- function ext() {
27
- return process.platform === "win32" ? ".exe" : "";
29
+ function packageNameForPlatform(platform) {
30
+ return platform === "win32" ? "@fallow-cli/win32-x64-msvc" : DEFAULT_PACKAGE_NAME;
28
31
  }
29
32
 
30
- function binaryNames() {
33
+ function binaryNames(platform) {
31
34
  // Platform packages ship a single multicall `fallow` binary.
32
- return [`fallow${ext()}`];
35
+ return [platform === "win32" ? "fallow.exe" : "fallow"];
33
36
  }
34
37
 
35
- function computeDigestsForDir(dir) {
38
+ function computeDigestsForDir(dir, platform) {
36
39
  const out = {};
37
- for (const base of binaryNames()) {
40
+ for (const base of binaryNames(platform)) {
38
41
  const full = path.join(dir, base);
39
42
  out[base] = "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
40
43
  }
@@ -43,8 +46,9 @@ function computeDigestsForDir(dir) {
43
46
 
44
47
  function mkPlatformDir(privateKey, options) {
45
48
  const opts = options || {};
49
+ const platform = opts.platform || DEFAULT_PLATFORM;
46
50
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-lazy-test-"));
47
- for (const base of binaryNames()) {
51
+ for (const base of binaryNames(platform)) {
48
52
  const binaryPath = path.join(dir, base);
49
53
  const content = Buffer.from(`mock ${base}`);
50
54
  fs.writeFileSync(binaryPath, content);
@@ -56,9 +60,9 @@ function mkPlatformDir(privateKey, options) {
56
60
  fs.writeFileSync(
57
61
  path.join(dir, "package.json"),
58
62
  JSON.stringify({
59
- name: opts.packageName || "@fallow-cli/test-platform",
63
+ name: opts.packageName || packageNameForPlatform(platform),
60
64
  version: opts.version || "2.81.0",
61
- fallowDigests: opts.skipDigests ? undefined : computeDigestsForDir(dir),
65
+ fallowDigests: opts.skipDigests ? undefined : computeDigestsForDir(dir, platform),
62
66
  }),
63
67
  );
64
68
  return dir;
@@ -88,13 +92,15 @@ function setupCacheRoot(t) {
88
92
  }
89
93
 
90
94
  function baseInput(dir, verifyFn, extras) {
95
+ const manifestPath = path.join(dir, "package.json");
96
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
91
97
  return {
92
98
  platformPkgDir: dir,
93
- packageName: "@fallow-cli/test-platform",
94
- manifestPath: path.join(dir, "package.json"),
99
+ packageName: manifest.name,
100
+ manifestPath,
95
101
  verifyFn,
96
102
  env: {},
97
- platform: process.platform,
103
+ platform: DEFAULT_PLATFORM,
98
104
  ...extras,
99
105
  };
100
106
  }
@@ -120,6 +126,29 @@ test("ensureVerified verifies on cache miss and writes the sentinel", (t) => {
120
126
  assert.equal(Object.keys(sentinel.binaries).length, 1);
121
127
  });
122
128
 
129
+ test("ensureVerified verifies and caches a win32 executable on any host", (t) => {
130
+ _resetWarningState();
131
+ const { privateKey, rawPub } = makeKeypair();
132
+ const dir = mkPlatformDir(privateKey, { platform: "win32" });
133
+ t.after(() => cleanup(dir));
134
+ const input = baseInput(dir, (binaryPath) => _verifyWithKey(binaryPath, rawPub), {
135
+ platform: "win32",
136
+ });
137
+
138
+ const verified = ensureVerified(input);
139
+ assert.equal(verified.ok, true);
140
+ assert.equal(verified.cached, false);
141
+ const sentinel = JSON.parse(fs.readFileSync(verified.sentinelPath, "utf8"));
142
+ assert.deepEqual(Object.keys(sentinel.binaries), ["fallow.exe"]);
143
+
144
+ const cached = ensureVerified({
145
+ ...input,
146
+ verifyFn: () => assert.fail("signature verification must not run on a cache hit"),
147
+ });
148
+ assert.equal(cached.ok, true);
149
+ assert.equal(cached.cached, true);
150
+ });
151
+
123
152
  test("ensureVerified returns cached:true on a valid sentinel", (t) => {
124
153
  _resetWarningState();
125
154
  const { privateKey, rawPub } = makeKeypair();
@@ -154,7 +183,7 @@ test("ensureVerified invalidates sentinel on mtime drift", (t) => {
154
183
 
155
184
  // Bump the mtime of one binary; sentinel should now be stale.
156
185
  const newTime = new Date(Date.now() + 10_000);
157
- fs.utimesSync(path.join(dir, `fallow${ext()}`), newTime, newTime);
186
+ fs.utimesSync(path.join(dir, binaryNames(DEFAULT_PLATFORM)[0]), newTime, newTime);
158
187
 
159
188
  let verifyCallCount = 0;
160
189
  const result = ensureVerified(
@@ -215,7 +244,9 @@ test("ensureVerified invalidates sentinel on packageName drift", (t) => {
215
244
  packageVersion: "2.81.0",
216
245
  packageName: "@fallow-cli/wrong-name",
217
246
  binaries: {
218
- [`fallow${ext()}`]: { mtimeMs: fs.statSync(path.join(dir, `fallow${ext()}`)).mtimeMs },
247
+ [binaryNames(DEFAULT_PLATFORM)[0]]: {
248
+ mtimeMs: fs.statSync(path.join(dir, binaryNames(DEFAULT_PLATFORM)[0])).mtimeMs,
249
+ },
219
250
  },
220
251
  }),
221
252
  );
@@ -261,7 +292,7 @@ test("ensureVerified invalidates sentinel on schemaVersion drift", (t) => {
261
292
  test("ensureVerified returns sig-invalid on a tampered signature", (t) => {
262
293
  _resetWarningState();
263
294
  const { privateKey, rawPub } = makeKeypair();
264
- const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow${ext()}` });
295
+ const dir = mkPlatformDir(privateKey, { corruptSigFor: binaryNames(DEFAULT_PLATFORM)[0] });
265
296
  t.after(() => cleanup(dir));
266
297
 
267
298
  const result = ensureVerified(baseInput(dir, (p) => _verifyWithKey(p, rawPub)));
@@ -289,89 +320,43 @@ test("ensureVerified returns digest-unavailable on a pre-#597 manifest", (t) =>
289
320
 
290
321
  test("ensureVerified honors FALLOW_VERIFY_CACHE_DIR when platform pkg dir is non-writable", (t) => {
291
322
  _resetWarningState();
292
- if (process.platform === "win32") {
293
- t.skip("Windows ACL chmod is not portable; covered by sentinel-path tests");
294
- return;
295
- }
296
323
  const { privateKey, rawPub } = makeKeypair();
297
324
  const dir = mkPlatformDir(privateKey);
298
325
  const cacheRoot = setupCacheRoot(t);
326
+ t.after(() => cleanup(dir));
299
327
 
300
- fs.chmodSync(dir, 0o555);
301
- try {
302
- const result = ensureVerified({
303
- ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
304
- env: { FALLOW_VERIFY_CACHE_DIR: cacheRoot },
305
- });
306
- assert.equal(result.ok, true);
307
- assert.equal(result.cached, false);
308
- assert.match(result.sentinelPath, new RegExp(cacheRoot.replace(/\\/g, "\\\\")));
309
- } finally {
310
- fs.chmodSync(dir, 0o755);
311
- cleanup(dir);
312
- }
328
+ const result = ensureVerified({
329
+ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
330
+ env: { FALLOW_VERIFY_CACHE_DIR: cacheRoot },
331
+ isWritable: (candidate) => candidate !== dir,
332
+ });
333
+ assert.equal(result.ok, true);
334
+ assert.equal(result.cached, false);
335
+ assert.match(result.sentinelPath, new RegExp(cacheRoot.replace(/\\/g, "\\\\")));
313
336
  });
314
337
 
315
- test("ensureVerified emits a single warning when sentinel write fails", (t) => {
338
+ test("ensureVerified emits a single warning when no sentinel location is writable", (t) => {
316
339
  _resetWarningState();
317
- if (process.platform === "win32") {
318
- t.skip("Windows ACL chmod is not portable");
319
- return;
320
- }
321
340
  const { privateKey, rawPub } = makeKeypair();
322
341
  const dir = mkPlatformDir(privateKey);
323
342
  const stderr = captureStderr(t);
343
+ t.after(() => cleanup(dir));
344
+ const input = {
345
+ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
346
+ homedir: undefined,
347
+ isWritable: () => false,
348
+ };
324
349
 
325
- // Make platform pkg dir non-writable AND point FALLOW_VERIFY_CACHE_DIR at
326
- // a non-existent path. resolveSentinelPath will still find the XDG / home
327
- // fallback, but we can simulate "every cache location read-only" by giving
328
- // it a writable dir that we then chmod down right before ensureVerified
329
- // runs writeSentinel. Simpler: instead of fighting the cascade in env,
330
- // simulate write failure via a non-writable FALLOW_VERIFY_CACHE_DIR that
331
- // PASSES isWritable() but then has its perms revoked between resolve and
332
- // write. Since that race is hard to script, we test the warn-once helper
333
- // via a synthetic path that fails the rename atomically: pass a sentinel-
334
- // chmod-locked dir.
335
- //
336
- // The portable shape: chmod the platform pkg dir read-only AND pass a
337
- // FALLOW_VERIFY_CACHE_DIR that is a regular file (not a dir). isWritable
338
- // returns false for both, so resolveSentinelPath falls through to XDG. If
339
- // the user has no HOME (synthetic env), the cascade returns null. We can
340
- // achieve this only by injecting both env.HOME='' AND env.XDG_CACHE_HOME=''
341
- // AND ensuring os.homedir() is not consulted; ensureVerified does not
342
- // accept a homedir override, so the warn-once path is unreachable via env
343
- // alone on a machine with a real HOME. Skip this test on machines with a
344
- // real homedir; the warn-once helper is otherwise covered by the
345
- // "no-writable-location" branch in sentinel-path tests.
346
- if (os.homedir() && os.homedir().length > 0) {
347
- t.skip(
348
- "warn-once-on-no-writable-cache requires homedir-override knob (covered by sentinel-path unit test)",
349
- );
350
- cleanup(dir);
351
- return;
352
- }
350
+ const result = ensureVerified(input);
351
+ assert.equal(result.sentinelPath, null);
352
+ const warnings = stderr.lines.filter((line) => line.includes("no writable cache location"));
353
+ assert.equal(warnings.length, 1);
353
354
 
354
- fs.chmodSync(dir, 0o555);
355
- try {
356
- const env = { HOME: "", XDG_CACHE_HOME: "" };
357
- const result = ensureVerified({
358
- ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
359
- env,
360
- });
361
- if (result.sentinelPath !== null) {
362
- t.diagnostic(`sentinel landed at ${result.sentinelPath} despite empty HOME; skipping`);
363
- return;
364
- }
365
- const warnings = stderr.lines.filter((l) => l.includes("no writable cache location"));
366
- assert.equal(warnings.length, 1);
367
- // Second call: no new warning.
368
- ensureVerified({ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)), env });
369
- const warnings2 = stderr.lines.filter((l) => l.includes("no writable cache location"));
370
- assert.equal(warnings2.length, 1);
371
- } finally {
372
- fs.chmodSync(dir, 0o755);
373
- cleanup(dir);
374
- }
355
+ ensureVerified(input);
356
+ const repeatedWarnings = stderr.lines.filter((line) =>
357
+ line.includes("no writable cache location"),
358
+ );
359
+ assert.equal(repeatedWarnings.length, 1);
375
360
  });
376
361
 
377
362
  // ---- FALLOW_SKIP_BINARY_VERIFY -------------------------------------------
@@ -457,10 +442,6 @@ test("ensureVerified is idempotent under concurrent first-runs", async (t) => {
457
442
 
458
443
  test("ensureVerified rejects a sentinel written for a different install dir", (t) => {
459
444
  _resetWarningState();
460
- if (process.platform === "win32") {
461
- t.skip("chmod-based read-only platform pkg dir is not portable on Windows");
462
- return;
463
- }
464
445
  // Two installs of the same package + version. install A is clean and writes
465
446
  // a sentinel to a shared cache; install B has a tampered binary at the same
466
447
  // package name + version. B must NOT trust A's sentinel via cache hit even
@@ -471,17 +452,14 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
471
452
  // Tamper install B's fallow binary AFTER mkPlatformDir wrote a valid sig
472
453
  // for the original bytes (mkPlatformDir does not expose a corrupt-binary
473
454
  // option, so simulate the attack by overwriting bytes here).
474
- fs.writeFileSync(path.join(installB, `fallow${ext()}`), Buffer.from("tampered bytes"));
455
+ fs.writeFileSync(
456
+ path.join(installB, binaryNames(DEFAULT_PLATFORM)[0]),
457
+ Buffer.from("tampered bytes"),
458
+ );
475
459
  const sharedCache = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-shared-cache-"));
460
+ const isWritable = (candidate) => candidate !== installA && candidate !== installB;
476
461
 
477
- // Force the shared-cache cascade by making both platform pkg dirs
478
- // non-writable (simulates yarn PnP / Docker layered images / pnpm
479
- // verify-store invariants).
480
- fs.chmodSync(installA, 0o555);
481
- fs.chmodSync(installB, 0o555);
482
462
  t.after(() => {
483
- fs.chmodSync(installA, 0o755);
484
- fs.chmodSync(installB, 0o755);
485
463
  cleanup(installA);
486
464
  cleanup(installB);
487
465
  cleanup(sharedCache);
@@ -492,6 +470,7 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
492
470
  const resultA = ensureVerified({
493
471
  ...baseInput(installA, (p) => _verifyWithKey(p, rawPub)),
494
472
  env: { FALLOW_VERIFY_CACHE_DIR: sharedCache },
473
+ isWritable,
495
474
  });
496
475
  assert.equal(resultA.ok, true);
497
476
  assert.match(resultA.sentinelPath, new RegExp(sharedCache.replace(/\\/g, "\\\\")));
@@ -502,7 +481,6 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
502
481
  // + SHA-256 binding must prevent that.
503
482
  for (const name of binaryNames()) {
504
483
  const aStat = fs.statSync(path.join(installA, name));
505
- fs.chmodSync(path.join(installB, name), 0o644);
506
484
  fs.utimesSync(path.join(installB, name), aStat.atime, aStat.mtime);
507
485
  }
508
486
 
@@ -513,6 +491,7 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
513
491
  return _verifyWithKey(p, rawPub);
514
492
  }),
515
493
  env: { FALLOW_VERIFY_CACHE_DIR: sharedCache },
494
+ isWritable,
516
495
  });
517
496
  assert.equal(resultB.ok, false);
518
497
  assert.equal(resultB.code, "sig-invalid");
@@ -530,7 +509,7 @@ test("ensureVerified rejects a sentinel where bytes drift but mtime stays", (t)
530
509
 
531
510
  // Tamper the binary in place AND restore the prior mtime, so the mtime
532
511
  // pre-filter matches but the bytes do not.
533
- const binPath = path.join(dir, `fallow${ext()}`);
512
+ const binPath = path.join(dir, binaryNames(DEFAULT_PLATFORM)[0]);
534
513
  const before = fs.statSync(binPath);
535
514
  fs.writeFileSync(binPath, Buffer.from("tampered"));
536
515
  fs.utimesSync(binPath, before.atime, before.mtime);
@@ -77,7 +77,7 @@ test("resolveSentinelPath falls back to FALLOW_VERIFY_CACHE_DIR when platform pk
77
77
  const cacheDir = mkTmp();
78
78
  try {
79
79
  const result = resolveSentinelPath({
80
- platformPkgDir: "/dev/null/not-a-dir",
80
+ platformPkgDir: path.join(cacheDir, "missing-platform-package"),
81
81
  packageName: "@fallow-cli/darwin-arm64",
82
82
  env: { FALLOW_VERIFY_CACHE_DIR: cacheDir },
83
83
  });
@@ -220,12 +220,19 @@ test("resolveSentinelPath honors injected isWritable + ensureDir for full test i
220
220
  test("resolveSentinelPath skips cache-dir-env when ensureDir fails for it", () => {
221
221
  // FALLOW_VERIFY_CACHE_DIR points at a non-creatable path, XDG points at a
222
222
  // creatable one. Confirm the resolver moves past the env override.
223
+ //
224
+ // A directory nested under a regular FILE is the portable way to make mkdir
225
+ // fail: `/dev/null/inside/a/file` only refuses on POSIX, and on Windows the
226
+ // recursive mkdir succeeds against the current drive, which both defeats the
227
+ // assertion and creates C:\dev outside any temp directory.
223
228
  const homeDir = mkTmp();
229
+ const blocker = path.join(homeDir, "not-a-directory");
230
+ fs.writeFileSync(blocker, "");
224
231
  try {
225
232
  const result = resolveSentinelPath({
226
233
  platformPkgDir: undefined,
227
234
  packageName: "@fallow-cli/darwin-arm64",
228
- env: { FALLOW_VERIFY_CACHE_DIR: "/dev/null/inside/a/file" },
235
+ env: { FALLOW_VERIFY_CACHE_DIR: path.join(blocker, "inside", "a", "file") },
229
236
  homedir: homeDir,
230
237
  platform: "darwin",
231
238
  });
@@ -239,10 +239,20 @@ test("verifyBinaryAt uses the embedded production public key", () => {
239
239
  }
240
240
  });
241
241
 
242
+ // Mirror binaryTargetsForPlatform: the suffix comes from the platformId under
243
+ // test, never from the live process.platform. A `dirOverride` run without an
244
+ // explicit platformId is labelled "test-platform" by
245
+ // resolvePlatformPackageForVerify, so the binary it looks for is plain `fallow`
246
+ // on every host -- a fixture keyed off process.platform writes `fallow.exe` on a
247
+ // Windows host and the verify then finds nothing.
248
+ function extForPlatformId(platformId) {
249
+ return typeof platformId === "string" && platformId.startsWith("win32") ? ".exe" : "";
250
+ }
251
+
242
252
  function makePlatformDir(privateKey, options) {
243
253
  const opts = options || {};
244
254
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
245
- const ext = process.platform === "win32" ? ".exe" : "";
255
+ const ext = extForPlatformId(opts.platformId);
246
256
  for (const base of ["fallow"]) {
247
257
  const binaryPath = path.join(dir, `${base}${ext}`);
248
258
  const content = Buffer.from(`mock ${base} contents`);
@@ -328,6 +338,42 @@ test("verifyInstalled with dirOverride returns ok when every binary verifies", a
328
338
  assert.equal(result.package, "<override>");
329
339
  });
330
340
 
341
+ // binaryTargetsForPlatform reads windows-ness off the platformId so a Windows
342
+ // verify can be synthesized anywhere. Nothing exercised that, which left the
343
+ // `.exe` target unverified on Linux CI and unverified on Windows too.
344
+ test("verifyInstalled verifies the .exe target for a win32 platformId on any host", async (t) => {
345
+ const { privateKey, rawPub } = makeKeypair();
346
+ const platformId = "win32-x64-msvc";
347
+ const dir = makePlatformDir(privateKey, { platformId });
348
+ t.after(() => cleanup(dir));
349
+ assert.ok(fs.existsSync(path.join(dir, "fallow.exe")), "fixture must write the .exe target");
350
+
351
+ const result = await verifyInstalled({
352
+ dirOverride: dir,
353
+ platformId,
354
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
355
+ digestProvider: makeDigestProvider(dir),
356
+ });
357
+ assert.equal(result.ok, true);
358
+ });
359
+
360
+ test("verifyInstalled reports the .exe name when a win32 signature is absent", async (t) => {
361
+ const { privateKey, rawPub } = makeKeypair();
362
+ const platformId = "win32-arm64-msvc";
363
+ const dir = makePlatformDir(privateKey, { platformId, skipSigFor: "fallow" });
364
+ t.after(() => cleanup(dir));
365
+
366
+ const result = await verifyInstalled({
367
+ dirOverride: dir,
368
+ platformId,
369
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
370
+ digestProvider: makeDigestProvider(dir),
371
+ });
372
+ assert.equal(result.ok, false);
373
+ assert.equal(result.code, "sig-missing");
374
+ assert.match(result.message, /fallow\.exe/);
375
+ });
376
+
331
377
  test("verifyInstalled resolves a global npm install from the fallow package directory", async (t) => {
332
378
  const pkg = currentPlatformPackage();
333
379
  if (!pkg) {
@@ -460,8 +506,8 @@ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
460
506
  assert.equal(result.skipped, true);
461
507
  });
462
508
 
463
- function computeDigestsForDir(dir) {
464
- const ext = process.platform === "win32" ? ".exe" : "";
509
+ function computeDigestsForDir(dir, platformId) {
510
+ const ext = extForPlatformId(platformId);
465
511
  const out = {};
466
512
  for (const base of ["fallow"]) {
467
513
  const fileName = `${base}${ext}`;
@@ -581,7 +627,7 @@ test("verifyInstalled returns digest-mismatch when the embedded digest disagrees
581
627
  const { privateKey, rawPub } = makeKeypair();
582
628
  const dir = makePlatformDir(privateKey);
583
629
  t.after(() => cleanup(dir));
584
- const ext = process.platform === "win32" ? ".exe" : "";
630
+ const ext = extForPlatformId();
585
631
  writeManifest(dir, {
586
632
  name: "@fallow-cli/x",
587
633
  version: "1.0.0",
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: fallow
3
- description: Codebase intelligence for TypeScript and JavaScript. Static analysis of code and styles reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Runtime coverage can merge production execution data for hot-path review, cold-path deletion confidence, and stale-flag evidence. 123 framework plugins, zero configuration, sub-second static analysis. Use when asked to audit PR risk, find unused code or dependencies, detect duplicates, check styling consistency, inspect architecture boundaries, merge runtime coverage, auto-fix supported issues, or run fallow.
3
+ description: Codebase intelligence for TypeScript and JavaScript. Static analysis of code and styles reports changed-code risk, cleanup opportunities, duplication, circular dependencies, complexity hotspots, architecture boundaries, design-system drift, feature flags, and opt-in security candidates. Runtime coverage can merge production execution data for hot-path review, cold-path deletion confidence, and stale-flag evidence. Broad framework plugin coverage, zero configuration, sub-second static analysis. Use when asked to audit PR risk, find unused code or dependencies, detect duplicates, check styling consistency, inspect architecture boundaries, merge runtime coverage, auto-fix supported issues, or run fallow.
4
4
  license: MIT
5
5
  ---
6
6
 
7
7
  # Fallow: codebase intelligence for TypeScript and JavaScript
8
8
 
9
- Codebase intelligence for TypeScript and JavaScript. The static layer analyzes code and styles and reports quality, changed-code risk, cleanup opportunities, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, design-system styling drift, feature flag patterns, and opt-in security candidates. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence, with a single local capture available by default and continuous/cloud runtime monitoring available as an optional mode. 123 framework plugins, zero configuration, sub-second static analysis.
9
+ Codebase intelligence for TypeScript and JavaScript. The static layer analyzes code and styles and reports quality, changed-code risk, cleanup opportunities, circular dependencies, code duplication, complexity hotspots, architecture boundary violations, design-system styling drift, feature flag patterns, and opt-in security candidates. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review, cold-path deletion confidence, and stale-flag evidence, with a single local capture available by default and continuous/cloud runtime monitoring available as an optional mode. Broad framework plugin coverage, zero configuration, sub-second static analysis.
10
10
 
11
11
  ## When to Use
12
12
  - Find cleanup opportunities: unused files, exports, types, members, dependencies, or stale flags.
@@ -42,8 +42,8 @@ cargo install fallow-cli # build from source
42
42
 
43
43
  ## Agent Rules
44
44
 
45
- 1. **Always use `--format json --quiet 2>/dev/null`** for machine-readable output and parse it as JSON. Compact JSON is the default; never depend on whitespace or add `--pretty` in agent pipelines. The `2>/dev/null` discards stderr so progress messages and threshold warnings don't corrupt the JSON on stdout. Never use `2>&1`
46
- 2. **Always append `|| true`** to every fallow command. Exit code 1 means "issues found" (normal), not a runtime error. Without `|| true`, the Bash tool treats exit 1 as failure and cancels parallel commands. Only exit code 2 is a real error (invalid config, parse failure)
45
+ 1. **Always use `--format json --quiet`** for machine-readable output and parse stdout as JSON. Compact JSON is the default; never depend on whitespace or add `--pretty` in agent pipelines. Keep stderr separate so diagnostics remain visible; never merge it into the JSON stream with `2>&1`.
46
+ 2. **Preserve and interpret the exit status.** Codes 0 and 1 are successful analysis outcomes: 0 is clean and 1 means findings. Treat every other code according to `fallow schema.exit_codes`. Do not force a successful status, because that hides validation, license, setup, network, and security-gate outcomes.
47
47
  3. **Use `--explain`** to include a `_meta` object in JSON output with metric definitions, ranges, and interpretation hints. In human format, `--explain` prints a `Description:` line under each section header.
48
48
  4. **Use the root `kind` field** to identify typed JSON envelopes (`dead-code`, `dead-code-grouped`, `health`, `dupes`, `combined`, `audit`, etc.).
49
49
  5. **Use issue type filters** (`--unused-exports`, `--unused-files`, etc.) to limit output scope
@@ -160,8 +160,8 @@ Run `fallow <command> --help` for the full flag list per command (see also refer
160
160
  | `unused-catalog-entry` | `--unused-catalog-entries` | yes | - | `pnpm-workspace.yaml` entries no workspace package.json references via `catalog:` (default `warn`) |
161
161
  | `empty-catalog-group` | `--empty-catalog-groups` | - | - | Named `catalogs.<name>:` groups in `pnpm-workspace.yaml` with no entries. Top-level `catalog:` placeholders are ignored. Default `warn`. |
162
162
  | `unresolved-catalog-reference` | `--unresolved-catalog-references` | - | - | `package.json` references to `catalog:` / `catalog:<name>` whose catalog does not declare the package; `pnpm install` would fail. Default `error`. Suppress via `ignoreCatalogReferences: [{ package, catalog?, consumer? }]` in fallow config (package.json has no comment syntax). |
163
- | `unused-dependency-override` | `--unused-dependency-overrides` | - | - | `pnpm-workspace.yaml#overrides` / `package.json#pnpm.overrides` entries whose target package is not declared by any workspace `package.json` and is not present in `pnpm-lock.yaml`. Default `warn`. When the lockfile is missing or unreadable the check degrades to a manifest-only fallback and every finding carries a `hint` reminding consumers to verify before removal. Suppress via `ignoreDependencyOverrides: [{ package, source? }]` in fallow config. |
164
- | `misconfigured-dependency-override` | `--misconfigured-dependency-overrides` | - | - | `pnpm.overrides` entries whose key is unparsable (empty, dangling separators, malformed selectors) or value is missing/empty. `pnpm install` would fail. Default `error`. Suppression: same `ignoreDependencyOverrides` config rule. |
163
+ | `unused-dependency-override` | `--unused-dependency-overrides` | - | - | Entries in `pnpm-workspace.yaml#overrides`, `package.json#pnpm.overrides`, npm or Bun `package.json#overrides`, or Bun `package.json#resolutions` whose target package is not declared by any workspace `package.json` and is not present in the active readable lockfile. Default `warn`. pnpm and npm projects without a readable lockfile degrade to a manifest-only fallback with a verification `hint`; Bun projects with only binary `bun.lockb` fail closed and emit no finding. Suppress via `ignoreDependencyOverrides: [{ package, source? }]` in fallow config. |
164
+ | `misconfigured-dependency-override` | `--misconfigured-dependency-overrides` | - | - | Package-manager override entries whose key is unparsable or whose value is missing or empty. The active package manager may reject or ignore the entry. Default `error`. Suppression: same `ignoreDependencyOverrides` config rule. |
165
165
  | `invalid-client-export` | - | - | `// fallow-ignore-next-line invalid-client-export` | "use client" file exports a server-only / route-config name; Requires the project to declare next |
166
166
  | `mixed-client-server-barrel` | - | - | `// fallow-ignore-next-line mixed-client-server-barrel` | Barrel re-exports both a "use client" module and a server-only module; Requires the project to declare next |
167
167
  | `misplaced-directive` | - | - | `// fallow-ignore-next-line misplaced-directive` | "use client" / "use server" directive is not in the leading position and is ignored; Requires the project to declare next |
@@ -257,7 +257,7 @@ fallow list --entry-points --format json --quiet
257
257
  fallow list --plugins --format json --quiet
258
258
  ```
259
259
 
260
- Shows detected entry points and active framework plugins (123 built-in: Next.js, Vite, Ember, Wuchale, Jest, Storybook, Tailwind, PandaCSS, Contentlayer, tap, tsd, etc.).
260
+ Shows detected entry points and active framework plugins. Read `fallow schema.plugins.count` when the exact current registry size matters.
261
261
 
262
262
  ### Production-only analysis
263
263
  ```bash
@@ -428,11 +428,7 @@ fallow hooks install --target git # pre-commit gate; --branch <ref> sets the f
428
428
 
429
429
  ## Exit Codes
430
430
 
431
- | Code | Meaning |
432
- |------|---------|
433
- | 0 | Success, no error-severity issues |
434
- | 1 | Error-severity issues found |
435
- | 2 | Runtime error (invalid config, parse failure, or `fix` without `--yes` in non-TTY) |
431
+ Codes 0 and 1 are successful analysis outcomes: 0 is clean and 1 means findings. Read `fallow schema.exit_codes` for validation, resource, runtime, network, security-gate, and upload failures instead of maintaining another copied table.
436
432
 
437
433
  When `--format json` is active and exit code is 2, errors are emitted as JSON on stdout:
438
434
  ```json
@@ -441,7 +437,7 @@ When `--format json` is active and exit code is 2, errors are emitted as JSON on
441
437
 
442
438
  ## Configuration
443
439
 
444
- Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to 123 auto-detecting framework plugins.
440
+ Fallow reads config from project root: `.fallowrc.json` > `.fallowrc.jsonc` > `fallow.toml` > `.fallow.toml`. Both `.fallowrc.json` and `.fallowrc.jsonc` accept JSON-with-comments syntax (same parser); the `.jsonc` extension lets editors auto-detect JSONC syntax highlighting. Most projects work with zero configuration thanks to auto-detecting framework plugins; read `fallow schema.plugins` for the current registry.
445
441
 
446
442
  ```jsonc
447
443
  {
@@ -478,7 +474,7 @@ export const deprecatedHelper = () => {};
478
474
  ## Key Gotchas
479
475
 
480
476
  - **`fix --yes` is required** in non-TTY (agent) environments. Without it, `fix` exits with code 2
481
- - **Zero config by default.** 123 framework plugins auto-detect, including Wuchale config, Contentlayer content roots, tap and tsd test entry points. Don't create config unless customization is needed
477
+ - **Zero config by default.** Built-in framework plugins auto-detect, including Wuchale config, Contentlayer content roots, tap and tsd test entry points. Read `fallow schema.plugins` for the current registry and don't create config unless customization is needed
482
478
  - **Syntactic analysis only.** No TypeScript compiler, so fully dynamic `import(variable)` is not resolved
483
479
  - **Function overloads are deduplicated.** TypeScript function overload signatures are merged into a single export (not reported as separate unused exports)
484
480
  - **Re-export chains are resolved.** Exports through barrel files are tracked, not falsely flagged
@@ -89,8 +89,8 @@ Common global flags for this command: [`--format`](#global-flags), [`--quiet`](#
89
89
  | `--unused-catalog-entries` | Unused pnpm catalog entries |
90
90
  | `--empty-catalog-groups` | Empty named pnpm catalog groups |
91
91
  | `--unresolved-catalog-references` | Package references to missing pnpm catalog entries |
92
- | `--unused-dependency-overrides` | Unused pnpm dependency overrides |
93
- | `--misconfigured-dependency-overrides` | Malformed pnpm dependency overrides |
92
+ | `--unused-dependency-overrides` | Unused package-manager dependency overrides |
93
+ | `--misconfigured-dependency-overrides` | Misconfigured package-manager dependency overrides |
94
94
  <!-- generated:flags:dead-code-filters:end -->
95
95
  ### Examples
96
96
 
@@ -298,7 +298,7 @@ fallow list --workspaces --format json --quiet
298
298
  fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
299
299
  ```
300
300
 
301
- The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
301
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`, `malformed-pnpm-workspace-yaml`, `skipped-large-file`, `skipped-minified-file`, `source-read-failure`, `bun-lockb-override-resolution-skipped`) with a typed payload (`error`, `pattern`, or none), and a `path` that is project-root-relative with forward slashes on every envelope that carries the array. The same `workspace_diagnostics[]` array is also surfaced on the `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes, at the top level of the bare combined `fallow --format json` envelope, on `fallow audit --format json` under `dead_code`, and on the `audit-brief` envelope shared by `fallow review --format json` and `fallow audit --brief --format json`, also under `dead_code` (omitted when empty). The combined carrier is the envelope root, not a section, so `--skip check`, `--only health`, and `--only dupes` all still report what their analyses recorded. The combined root is the union of what every analysis in the run recorded, deduplicated on the whole `kind` (typed payload included) plus `path`, so two overlapping globs still report the same package-less directory once per `pattern` (a declared glob's no-op `./` prefix is normalised away, so one glob written `"./apps/**"` in `package.json` and `apps/**` in `pnpm-workspace.yaml` stays one entry): a combined run walks the project once per analysis, and a per-analysis `production` mode (`production: { deadCode, health, dupes }`, `--production-health`) can give those walks different file sets, so only the union reports what the run as a whole saw. Each analysis contributes the workspace-discovery list its own config load produced, the same list `fallow list --workspaces` reports, so the combined root can carry an `undeclared-workspace` or `glob-matched-no-package-json` entry that the standalone `dead-code`, `check`, `health`, and `dupes` envelopes, which read the process diagnostics registry instead, do not. `fallow audit --format json` and the `audit-brief` envelope are on the same broad side: they fold the dead-code analysis's own list into their `dead_code.workspace_diagnostics[]`, so they too report an `undeclared-workspace` entry the standalone envelopes miss. The CLI and the programmatic route (MCP code mode, NAPI, embedders) agree on everything an analysis records: both folds close with the same process-registry read, which covers what an analysis records after its section captured its list (a `source-read-failure`, or the analysis-stage kinds a health run's own dead-code precompute records) and skips `skipped-large-file` and `skipped-minified-file`, since those reach an envelope only from the walk that recorded them. The two analysis-stage kinds (`malformed-pnpm-workspace-yaml`, `bun-lockb-override-resolution-skipped`) are recorded by the dead-code analyze pass, so they only appear on runs that include it: `fallow dupes --format json` and `fallow --only dupes` report the workspace-discovery and source-discovery kinds alone. A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
302
302
 
303
303
  The `--boundaries` JSON output carries `boundaries.logical_groups[]` alongside the existing `zones[]` / `rules[]` arrays. Each logical-group entry surfaces a user-authored `autoDiscover` parent zone (which expansion otherwise flattens into per-child zones like `features/auth` / `features/billing`): `name`, `children`, `auto_discover` (verbatim user strings), `status` (`ok` / `empty` / `invalid_path`), `source_zone_index`, summed `file_count`, optional `authored_rule` (the pre-expansion `{ allow, allowTypeOnly }` keyed on the parent), optional `fallback_zone` cross-reference when the parent also kept its own `patterns` (Bulletproof case), optional `merged_from` (parent zone indices when the user declared the same parent name twice; surfaces the duplicate in JSON instead of only in `tracing::warn!`), optional `original_zone_root` (echo of the parent's `root` subtree scope for monorepo patchers), and optional `child_source_indices` (parallel to `children`, attributing each child to a specific `auto_discover` entry when multiple paths were authored). The full shape is documented in `docs/output-schema.json` under `ListBoundariesOutput`.
304
304
 
@@ -838,8 +838,8 @@ Audits changed files for dead code, complexity, duplication, and styling. Return
838
838
  | `--health-baseline` | `string` | - | Baseline file (produced by `fallow health --save-baseline`). Pre-existing complexity findings are excluded from the verdict. |
839
839
  | `--dupes-baseline` | `string` | - | Baseline file (produced by `fallow dupes --save-baseline`). Pre-existing clone groups are excluded from the verdict. |
840
840
  | `--max-crap` | `string` | - | Forwarded to the health sub-analysis. Functions meeting or exceeding this CRAP score cause audit to fail. Same formula as `health --max-crap`. Pair with coverage data for accurate per-function CRAP. |
841
- | `--coverage` | `string` | - | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores in the health sub-analysis. Same format and semantics as `health --coverage`. Also configurable via `FALLOW_COVERAGE`. Relative paths resolve against `--root`. |
842
- | `--coverage-root` | `string` | - | Absolute prefix to strip from file paths in coverage data before prepending the project root. Also configurable via `FALLOW_COVERAGE_ROOT`. Use when coverage was generated under a different checkout root in CI / Docker (e.g., `/home/runner/work/myapp` on GitHub Actions). |
841
+ | `--coverage` | `string` | - | Path to Istanbul-format coverage data (`coverage-final.json`) for accurate per-function CRAP scores in the health sub-analysis. Same format and semantics as `health --coverage`. Also configurable via `FALLOW_COVERAGE`, then `health.coverage` (the same chain as `fallow health`). Relative paths resolve against `--root`. |
842
+ | `--coverage-root` | `string` | - | Absolute prefix to strip from file paths in coverage data before prepending the project root. Also configurable via `FALLOW_COVERAGE_ROOT`, then `health.coverageRoot`. Use when coverage was generated under a different checkout root in CI / Docker (e.g., `/home/runner/work/myapp` on GitHub Actions). |
843
843
  | `--no-css` | `bool` | `false` | Disable styling analytics in audit |
844
844
  | `--css-deep` | `bool` | `false` | Enable deep CSS analysis for audit explicitly: project-wide styling reachability, narrowed back to changed anchors. Deep CSS is on by default; use this to override `audit.cssDeep = false` |
845
845
  | `--no-css-deep` | `bool` | `false` | Disable deep CSS analysis while keeping local styling analytics on |
@@ -23,7 +23,7 @@ Always preview with `--dry-run` before applying. This is a destructive operation
23
23
 
24
24
  ## Don't Create Config Unless Needed
25
25
 
26
- Fallow works with zero configuration for most projects thanks to 123 auto-detecting framework plugins. Creating an unnecessary config file can mask issues or override detection behavior.
26
+ Fallow works with zero configuration for most projects thanks to auto-detecting framework plugins. Read `fallow schema.plugins` for the current registry. Creating an unnecessary config file can mask issues or override detection behavior.
27
27
 
28
28
  ```bash
29
29
  # WRONG: creating config for a standard Next.js project
@@ -38,7 +38,7 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
38
38
  | `impact` | introspection | free | `fallow impact --format json --quiet` | `root` | Read the local, opt-in Fallow Impact value report (`fallow impact --format json`). Runs no analysis: current surfacing counts, trend since the last recorded run, pre-commit gate containment, and (on impact v1.5+) resolved/suppressed attribution. History is read from a per-project file in the user's private config dir (never inside the repo). Read-only and `root`-only; the mutating `enable` / `disable` / `default` lifecycle is not exposed. A never-enabled project returns a populated `{"enabled": false, ...}` report (never `{}`); branch on `enabled` and `enabled_source` (`project` / `user` / `default`) then `record_count`, recommending `fallow impact enable` only when `explicit_decision` is `false` (never asked) and staying silent when `true` (deliberately disabled here). Local-developer signal: fallow never records in CI, so empty there and not a CI metric |
39
39
  | `impact_all` | introspection | free | `fallow impact --all --format json --quiet` | `sort`, `limit` | Roll every tracked fallow project on this machine into one cross-repo value report (hashed keys plus basename labels, never paths; local-dev only) |
40
40
  | `trace_export` | trace | free | `fallow dead-code --trace <file:export> --format json --quiet` | `file`, `export_name` | Trace why an export is used or unused (`fallow dead-code --trace FILE:EXPORT_NAME --format json`). Required `file` and `export_name`. Returns file reachability, entry-point status, direct references, re-export chains, and a reason string. If `export_name` is a class / enum / store MEMBER, returns a member trace instead (`member_name`, `member_kind`, `owner_export`, `owner_is_used`) plus a `--unused-<kind>-members` pointer; branch on field presence. Use before deleting a supposedly-unused export or debugging an unused-class-member finding |
41
- | `trace_symbol` | trace | free | `fallow dead-code --type-aware --trace <file:export> --format json --quiet` | `file`, `export_name`, `type_aware_projects`, `type_aware_require` | Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat `semantic.references`, `semantic.status`, and `semantic.identity` as the authoritative exact evidence. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface. |
41
+ | `trace_symbol` | trace | free | `fallow dead-code --type-aware --trace <file:export> --format json --quiet` | `file`, `export_name`, `type_aware_projects`, `type_aware_require` | Trace an exact TypeScript symbol with checker-backed references, namespace identity, aliases, and re-export hops. Root trace fields preserve syntactic context; treat `semantic.references`, `semantic.status`, and `semantic.identity` as the authoritative exact evidence. The proof covers only the lane named by `semantic.target.namespace`, so a root trace that lists a reference the proof does not is wider evidence rather than stale. This is project-wide evidence for Fallow decisions, not a compiler-diagnostic or lint-rule surface. |
42
42
  | `symbol_impact` | impact | free | `fallow dead-code --type-aware --symbol-impact <file:export-or-class.member> --format json --quiet` | `file`, `export_name`, `class_name`, `member_name`, `type_aware_projects`, `type_aware_require` | Return exact-symbol consumers, affected files, and targeted tests for a TypeScript export or exported class method. Select either `export_name`, or both `class_name` and `member_name`. Advisory change-impact evidence, not a substitute for `tsc` or Oxlint |
43
43
  | `trace_file` | trace | free | `fallow dead-code --trace-file <file> --format json --quiet` | `file` | Trace all graph edges for a file (`fallow dead-code --trace-file PATH --format json`). Required `file`. Returns reachability, exports, imports-from, imported-by, and re-exports. Use to decide whether a file is isolated, barrel-only, or imported by live entry points |
44
44
  | `impact_closure` | trace | free | `fallow dead-code --impact-closure <path> --format json --quiet` | `path` | Trace the transitive affected-but-not-in-diff set and coordination gaps for one file. Supports `root`, `config`, `production`, `workspace`, `no_cache`, and `threads`. Use as review-planning evidence for a file contract, not proof that affected files are wrong |
@@ -46,6 +46,29 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
46
46
  | `trace_clone` | trace | free | `fallow dupes --trace <file:line> --format json --quiet` | `file`, `line`, `fingerprint`, `near`, `min_occurrences` | Deep-dive a duplicate-code clone group (`fallow dupes --trace <spec> --format json`). Address by exactly one of: `file` + `line` (a source location), or `fingerprint` (a `dup:<id>` from a prior `find_dupes` `clone_groups[].fingerprint`, usually `dup:<8hex>` and widened only on rare report collisions). Returns the matched clone instance plus every clone group containing it; each traced group carries its `fingerprint`, an extract-function `suggestion` with estimated savings, and a best-effort `suggested_name` (omitted when no confident name). Supports `mode`, `near`, `min_tokens`, `min_lines`, `min_occurrences`, `threshold`, `skip_local`, `cross_language`, `ignore_imports`. Use the same `near` value as the originating `find_dupes` call. Use to consolidate duplication when you need exact sibling locations and a refactor target |
47
47
  <!-- generated:mcp-tools:end -->
48
48
 
49
+ ## How type-aware proof relates to the root trace
50
+
51
+ `trace_symbol` is the only tool that returns a checker-backed `semantic` block
52
+ next to a syntactic root trace. The checker resolves actual reads through local
53
+ aliases, import types, namespace-qualified names, and barrels to the exact type
54
+ or value declaration. Import and re-export declarations alone are not reads.
55
+ The root trace stays authoritative for graph reachability and star ambiguity,
56
+ and its optional `direct_references_by_namespace` keeps type and value evidence
57
+ separate without changing the selected root `namespace`. Type-aware
58
+ reconciliation fails closed: unreachable-only, re-export-only, or
59
+ different-declaration evidence cannot suppress a syntactic finding. Treat an
60
+ ambiguous root as an abstention, and investigate any remaining mismatch before
61
+ deleting a symbol.
62
+
63
+ `symbol_impact` carries no `semantic` block and no root trace. Its top-level
64
+ checker evidence uses the same declaration-safe alias and namespace resolution
65
+ as `trace_symbol`. A listed consumer's `relation` names the traced symbol's own
66
+ lane, not the consumer's syntax. Confirm a clean impact result with
67
+ `trace_export` or `trace_symbol` before deletion when the graph reports
68
+ ambiguity or reachable references.
69
+
70
+ `trace_export` never carries a `semantic` block: it is API-backed in-process and answers from the graph alone.
71
+
49
72
  ## Runtime source-map confidence for cloud runtime tools
50
73
 
51
74
  | Values | Meaning | Agent action |
@@ -645,7 +645,7 @@ Focus on findings that are BOTH dead code and duplicated:
645
645
 
646
646
  ## Custom Plugin Setup
647
647
 
648
- For frameworks not covered by the 123 built-in plugins.
648
+ For frameworks not covered by the current built-in registry from `fallow schema.plugins`.
649
649
 
650
650
  ### Option 1: Inline framework config
651
651
 
@@ -717,7 +717,7 @@ fallow dead-code --format sarif --quiet > fallow.sarif
717
717
  fallow dead-code --ci > fallow.sarif
718
718
  ```
719
719
 
720
- The `--ci` flag is equivalent to `--format sarif --fail-on-issues --quiet`. Note: `--fail-on-issues` means exit code 1 if issues exist, in CI scripts use `continue-on-error: true` or `|| true` to ensure the SARIF upload step still runs.
720
+ The `--ci` flag is equivalent to `--format sarif --fail-on-issues --quiet`. Exit code 1 means findings exist. Capture that status, let the SARIF upload step run, then reapply the captured status in a final gate step. Do not discard every outcome, because validation and execution failures need to remain distinguishable from findings.
721
721
 
722
722
  ---
723
723
 
@@ -363,9 +363,9 @@ type: "jsdoc_tag"
363
363
  */
364
364
  export type DependencyOverrideSource = ("pnpm-workspace.yaml" | "package.json")
365
365
  /**
366
- * Why a dependency-override entry is misconfigured. `pnpm install` would
367
- * either fail at install time or silently no-op on these entries; surfacing
368
- * them statically catches the issue before pnpm does.
366
+ * Why a dependency-override entry is misconfigured. The active package
367
+ * manager may fail at install time or silently no-op on these entries;
368
+ * surfacing them statically catches the issue first.
369
369
  */
370
370
  export type DependencyOverrideMisconfigReason = ("unparsable-key" | "empty-value")
371
371
  /**
@@ -441,6 +441,12 @@ kind: "skipped-minified-file"
441
441
  */
442
442
  error: string
443
443
  kind: "source-read-failure"
444
+ } | {
445
+ kind: "bun-lockb-override-resolution-skipped"
446
+ } | {
447
+ kind: "bun-lock-override-resolution-skipped"
448
+ } | {
449
+ kind: "bun-resolutions-shadowed-by-overrides"
444
450
  })
445
451
  /**
446
452
  * Discriminant for [`CloneGroupAction::kind`]. Mirrors the action types
@@ -2283,20 +2289,23 @@ empty_catalog_groups?: EmptyCatalogGroupFinding[]
2283
2289
  */
2284
2290
  unresolved_catalog_references?: UnresolvedCatalogReferenceFinding[]
2285
2291
  /**
2286
- * Entries in pnpm-workspace.yaml's overrides: section, package.json's
2287
- * pnpm.overrides block, or package.json's top-level npm overrides object,
2292
+ * Entries in pnpm-workspace.yaml's overrides section, package.json's
2293
+ * pnpm.overrides block, npm or Bun's top-level overrides object, or Bun's
2294
+ * top-level resolutions object,
2288
2295
  * whose target package is not declared by any workspace package and is
2289
- * not present in pnpm-lock.yaml or package-lock.json. Default severity
2290
- * is warn because projects without a readable lockfile fall back to
2291
- * manifest-only checks; the hint field flags those conservative cases.
2296
+ * not present in pnpm-lock.yaml, package-lock.json, npm-shrinkwrap.json,
2297
+ * or bun.lock. Default severity is warn because projects without a
2298
+ * readable lockfile fall back to manifest-only checks; the hint field
2299
+ * flags those conservative cases. When the only lockfile is bun's binary
2300
+ * bun.lockb, resolution cannot be read and the check emits nothing.
2292
2301
  * Wrapped in [`UnusedDependencyOverrideFinding`].
2293
2302
  */
2294
2303
  unused_dependency_overrides?: UnusedDependencyOverrideFinding[]
2295
2304
  /**
2296
- * pnpm.overrides or npm overrides entries whose key or value does not
2297
- * parse as a valid override spec (empty key, empty value, malformed
2298
- * selector, unbalanced parent matcher). The package manager will reject
2299
- * these at install time. Default severity is error. Wrapped in
2305
+ * Package-manager override or resolution entries whose key or value does
2306
+ * not parse in the declaration source's grammar (empty key, empty value,
2307
+ * malformed selector, unbalanced parent matcher). The package manager may
2308
+ * reject or ignore these at install time. Default severity is error. Wrapped in
2300
2309
  * [`MisconfiguredDependencyOverrideFinding`].
2301
2310
  */
2302
2311
  misconfigured_dependency_overrides?: MisconfiguredDependencyOverrideFinding[]
@@ -2454,7 +2463,24 @@ regression?: (RegressionResult | null)
2454
2463
  */
2455
2464
  _meta?: (Meta | null)
2456
2465
  /**
2457
- * Workspace-discovery diagnostics surfaced during config load.
2466
+ * Non-fatal diagnostics about the project itself, from all three stages
2467
+ * that record them (issue #473):
2468
+ *
2469
+ * - workspace discovery, at config load: `undeclared-workspace`,
2470
+ * `malformed-package-json`, `glob-matched-no-package-json`,
2471
+ * `malformed-tsconfig`, `tsconfig-reference-dir-missing`;
2472
+ * - source discovery, during the file walk: `skipped-large-file`,
2473
+ * `skipped-minified-file`, `source-read-failure`;
2474
+ * - dead-code analysis, from the dependency-catalog and override
2475
+ * detectors: `malformed-pnpm-workspace-yaml`,
2476
+ * `bun-lockb-override-resolution-skipped`.
2477
+ *
2478
+ * Analysis-stage kinds therefore reach only the envelopes whose run
2479
+ * includes a dead-code analyze pass, never a standalone
2480
+ * `fallow dupes --format json`. `path` is project-root-relative with
2481
+ * forward slashes; the array is omitted when empty. The same list is
2482
+ * repeated on each top-level command's envelope so single-command
2483
+ * consumers see it without having to look at a separate top-level field.
2458
2484
  */
2459
2485
  workspace_diagnostics?: WorkspaceDiagnostic[]
2460
2486
  /**
@@ -2638,12 +2664,12 @@ empty_catalog_groups: number
2638
2664
  */
2639
2665
  unresolved_catalog_references: number
2640
2666
  /**
2641
- * Pnpm `overrides:` entries whose target package is not declared by any
2642
- * workspace package and not present in the lockfile.
2667
+ * Package-manager overrides whose target package is not declared by any
2668
+ * workspace package and not present in the active readable lockfile.
2643
2669
  */
2644
2670
  unused_dependency_overrides: number
2645
2671
  /**
2646
- * Pnpm `overrides:` entries whose key or value cannot be parsed.
2672
+ * Package-manager overrides whose key or value cannot be parsed.
2647
2673
  */
2648
2674
  misconfigured_dependency_overrides: number
2649
2675
  /**
@@ -3886,7 +3912,7 @@ parent_package?: (string | null)
3886
3912
  */
3887
3913
  version_constraint?: (string | null)
3888
3914
  /**
3889
- * The right-hand side of the entry: the version pnpm should force.
3915
+ * The right-hand side of the entry: the version the package manager should force.
3890
3916
  */
3891
3917
  version_range: string
3892
3918
  source: DependencyOverrideSource
@@ -9036,6 +9062,18 @@ is_used: boolean
9036
9062
  * Files that reference this export directly.
9037
9063
  */
9038
9064
  direct_references: ExportReference[]
9065
+ /**
9066
+ * Reachable direct references grouped by namespace. This is additive to
9067
+ * `namespace` and `direct_references`, whose winning-lane meaning remains
9068
+ * unchanged for backwards compatibility.
9069
+ */
9070
+ direct_references_by_namespace?: NamespacedExportReferences[]
9071
+ /**
9072
+ * A star-export collision that makes the traced name ambiguous. When
9073
+ * present, `is_used: false` is an abstention rather than an unused-code
9074
+ * verdict.
9075
+ */
9076
+ star_export_ambiguity?: (StarExportAmbiguity | null)
9039
9077
  /**
9040
9078
  * Re-export chains that pass through this export.
9041
9079
  */
@@ -9062,6 +9100,36 @@ from_file: string
9062
9100
  */
9063
9101
  kind: string
9064
9102
  }
9103
+ /**
9104
+ * Direct references that credit one namespace of an export binding.
9105
+ */
9106
+ export interface NamespacedExportReferences {
9107
+ namespace: SemanticNamespace
9108
+ /**
9109
+ * Number of reachable references in this namespace.
9110
+ */
9111
+ reference_count: number
9112
+ /**
9113
+ * Reachable references in deterministic graph order.
9114
+ */
9115
+ references: ExportReference[]
9116
+ }
9117
+ /**
9118
+ * The `export *` collision that keeps a name from being exported.
9119
+ */
9120
+ export interface StarExportAmbiguity {
9121
+ /**
9122
+ * Files that each declare a colliding declaration under the traced name
9123
+ * (project-root-relative), sorted. These are the origins to fix: keep one,
9124
+ * rename or explicitly re-export the rest.
9125
+ */
9126
+ sources: string[]
9127
+ /**
9128
+ * The namespaces the collision occurs in, type before value. A name can
9129
+ * collide in type space, value space, or both.
9130
+ */
9131
+ namespaces: SemanticNamespace[]
9132
+ }
9065
9133
  /**
9066
9134
  * A re-export chain showing how an export is propagated.
9067
9135
  */
@@ -9105,6 +9173,7 @@ member_kind: string
9105
9173
  * The export that declares this member (the class / enum / store name).
9106
9174
  */
9107
9175
  owner_export: string
9176
+ owner_namespace?: SemanticNamespace
9108
9177
  /**
9109
9178
  * Whether the owning export is considered used.
9110
9179
  */
@@ -9423,22 +9492,6 @@ export interface UnresolvedCallee {
9423
9492
  callee: string
9424
9493
  reason: UnresolvedReason
9425
9494
  }
9426
- /**
9427
- * The `export *` collision that keeps a name from being exported.
9428
- */
9429
- export interface StarExportAmbiguity {
9430
- /**
9431
- * Files that each declare a colliding declaration under the traced name
9432
- * (project-root-relative), sorted. These are the origins to fix: keep one,
9433
- * rename or explicitly re-export the rest.
9434
- */
9435
- sources: string[]
9436
- /**
9437
- * The namespaces the collision occurs in, type before value. A name can
9438
- * collide in type space, value space, or both.
9439
- */
9440
- namespaces: SemanticNamespace[]
9441
- }
9442
9495
  /**
9443
9496
  * Envelope emitted by `fallow --format review-github` / `review-gitlab`.
9444
9497
  */
@@ -9576,7 +9629,8 @@ comments: number
9576
9629
  */
9577
9630
  current_fingerprints: number
9578
9631
  /**
9579
- * Distinct fingerprints found in existing comments.
9632
+ * Distinct fingerprints with an open Fallow lifecycle, including
9633
+ * provider-resolved discussions not yet closed by a Fallow marker.
9580
9634
  */
9581
9635
  existing_fingerprints: number
9582
9636
  /**
@@ -9584,7 +9638,7 @@ existing_fingerprints: number
9584
9638
  */
9585
9639
  new_fingerprints: number
9586
9640
  /**
9587
- * Fingerprints commented earlier whose findings no longer exist.
9641
+ * Fingerprints with open Fallow lifecycles whose findings no longer exist.
9588
9642
  */
9589
9643
  stale_fingerprints: number
9590
9644
  /**
@@ -9604,7 +9658,7 @@ provider_warning?: (string | null)
9604
9658
  */
9605
9659
  resolution_comments_posted: number
9606
9660
  /**
9607
- * Stale discussion threads resolved (GitLab).
9661
+ * Provider discussion threads resolved or re-closed.
9608
9662
  */
9609
9663
  threads_resolved: number
9610
9664
  /**
@@ -9914,8 +9968,10 @@ workspace_count: number
9914
9968
  workspaces: WorkspaceInfo[]
9915
9969
  /**
9916
9970
  * Workspace discovery diagnostics produced while reading workspace
9917
- * declarations. Present for compatibility with the current wire contract,
9918
- * even when empty.
9971
+ * declarations. Paths are project-root-relative and use forward slashes,
9972
+ * like `workspaces[].path` and like the `workspace_diagnostics[]` array on
9973
+ * the analysis envelopes. Present for compatibility with the current wire
9974
+ * contract, even when empty.
9919
9975
  */
9920
9976
  workspace_diagnostics: WorkspaceDiagnostic[]
9921
9977
  }
@@ -10090,7 +10146,10 @@ groups?: (HealthGroup[] | null)
10090
10146
  */
10091
10147
  _meta?: (Meta | null)
10092
10148
  /**
10093
- * Workspace-discovery diagnostics surfaced during config load.
10149
+ * Workspace-discovery, source-discovery, and analysis-stage diagnostics
10150
+ * for the run. See `CheckOutput::workspace_diagnostics` for the full
10151
+ * contract: the kinds each stage records, project-root-relative paths,
10152
+ * omitted when empty.
10094
10153
  */
10095
10154
  workspace_diagnostics?: WorkspaceDiagnostic[]
10096
10155
  /**
@@ -10239,11 +10298,13 @@ groups?: (DuplicationGroup[] | null)
10239
10298
  */
10240
10299
  _meta?: (Meta | null)
10241
10300
  /**
10242
- * Workspace-discovery diagnostics surfaced during config load
10301
+ * Workspace-discovery and source-discovery diagnostics for the run
10243
10302
  * (issue #473). See `CheckOutput::workspace_diagnostics` for the full
10244
10303
  * contract; the same list is repeated on each top-level command's
10245
10304
  * envelope so single-command consumers see it without having to look at
10246
- * a separate top-level field.
10305
+ * a separate top-level field. A standalone `fallow dupes` run has no
10306
+ * dead-code analyze pass, so the two analysis-stage kinds never appear
10307
+ * here.
10247
10308
  */
10248
10309
  workspace_diagnostics?: WorkspaceDiagnostic[]
10249
10310
  /**
@@ -10592,20 +10653,23 @@ empty_catalog_groups?: EmptyCatalogGroupFinding[]
10592
10653
  */
10593
10654
  unresolved_catalog_references?: UnresolvedCatalogReferenceFinding[]
10594
10655
  /**
10595
- * Entries in pnpm-workspace.yaml's overrides: section, package.json's
10596
- * pnpm.overrides block, or package.json's top-level npm overrides object,
10656
+ * Entries in pnpm-workspace.yaml's overrides section, package.json's
10657
+ * pnpm.overrides block, npm or Bun's top-level overrides object, or Bun's
10658
+ * top-level resolutions object,
10597
10659
  * whose target package is not declared by any workspace package and is
10598
- * not present in pnpm-lock.yaml or package-lock.json. Default severity
10599
- * is warn because projects without a readable lockfile fall back to
10600
- * manifest-only checks; the hint field flags those conservative cases.
10660
+ * not present in pnpm-lock.yaml, package-lock.json, npm-shrinkwrap.json,
10661
+ * or bun.lock. Default severity is warn because projects without a
10662
+ * readable lockfile fall back to manifest-only checks; the hint field
10663
+ * flags those conservative cases. When the only lockfile is bun's binary
10664
+ * bun.lockb, resolution cannot be read and the check emits nothing.
10601
10665
  * Wrapped in [`UnusedDependencyOverrideFinding`].
10602
10666
  */
10603
10667
  unused_dependency_overrides?: UnusedDependencyOverrideFinding[]
10604
10668
  /**
10605
- * pnpm.overrides or npm overrides entries whose key or value does not
10606
- * parse as a valid override spec (empty key, empty value, malformed
10607
- * selector, unbalanced parent matcher). The package manager will reject
10608
- * these at install time. Default severity is error. Wrapped in
10669
+ * Package-manager override or resolution entries whose key or value does
10670
+ * not parse in the declaration source's grammar (empty key, empty value,
10671
+ * malformed selector, unbalanced parent matcher). The package manager may
10672
+ * reject or ignore these at install time. Default severity is error. Wrapped in
10609
10673
  * [`MisconfiguredDependencyOverrideFinding`].
10610
10674
  */
10611
10675
  misconfigured_dependency_overrides?: MisconfiguredDependencyOverrideFinding[]
@@ -11022,6 +11086,10 @@ _meta?: (Meta | null)
11022
11086
  * distinguish "gate ran and passed" from "gate did not run" (absent).
11023
11087
  */
11024
11088
  gate?: (SecurityGate | null)
11089
+ /**
11090
+ * Diagnostics owned by this security analysis run.
11091
+ */
11092
+ workspace_diagnostics?: WorkspaceDiagnostic[]
11025
11093
  /**
11026
11094
  * Security candidates. Paths are project-root-relative, forward-slash.
11027
11095
  */
@@ -11639,6 +11707,10 @@ _meta?: (Meta | null)
11639
11707
  * Gate verdict, present only when `--gate <mode>` was set.
11640
11708
  */
11641
11709
  gate?: (SecurityGate | null)
11710
+ /**
11711
+ * Diagnostics owned by the full security analysis summarized here.
11712
+ */
11713
+ workspace_diagnostics?: WorkspaceDiagnostic[]
11642
11714
  summary: SecuritySummary
11643
11715
  }
11644
11716
  /**
@@ -11758,6 +11830,10 @@ export interface SecuritySurvivorsOutput {
11758
11830
  schema_version: SecuritySurvivorsSchemaVersion
11759
11831
  version: ToolVersion
11760
11832
  elapsed_ms: ElapsedMs
11833
+ /**
11834
+ * Diagnostics preserved from the candidate security report.
11835
+ */
11836
+ workspace_diagnostics?: WorkspaceDiagnostic[]
11761
11837
  summary: SecuritySurvivorsSummary
11762
11838
  /**
11763
11839
  * Verifier-retained candidates keyed by finding id.
@@ -11840,6 +11916,10 @@ export interface SecurityBlindSpotsOutput {
11840
11916
  schema_version: SecurityBlindSpotsSchemaVersion
11841
11917
  version: ToolVersion
11842
11918
  elapsed_ms: ElapsedMs
11919
+ /**
11920
+ * Diagnostics owned by the security analysis used for this view.
11921
+ */
11922
+ workspace_diagnostics?: WorkspaceDiagnostic[]
11843
11923
  summary: SecurityBlindSpotsSummary
11844
11924
  /**
11845
11925
  * Grouped unresolved callee diagnostics, derived from existing samples.
@@ -11918,6 +11998,15 @@ dupes?: (DupesReportPayload | null)
11918
11998
  * Health section of the combined run.
11919
11999
  */
11920
12000
  health?: (HealthReport | null)
12001
+ /**
12002
+ * Workspace-discovery, source-discovery, and analysis-stage diagnostics
12003
+ * for the run (issue #2366). See `CheckOutput::workspace_diagnostics` for
12004
+ * the full contract: root-relative paths, omitted when empty. The
12005
+ * combined envelope carries them here rather than inside a section, so a
12006
+ * run that skips a section (`--skip check`, `--only health`,
12007
+ * `--only dupes`) still reports every diagnostic its analyses recorded.
12008
+ */
12009
+ workspace_diagnostics?: WorkspaceDiagnostic[]
11921
12010
  /**
11922
12011
  * Read-only follow-up commands aggregated across the combined run's
11923
12012
  * findings. See `CheckOutput::next_steps` for the contract.