fallow 3.32.0 → 3.33.1

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/capabilities.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.32.0",
3
+ "version": "3.33.1",
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": [
@@ -142,7 +142,7 @@
142
142
  "name": "--baseline",
143
143
  "type": "string",
144
144
  "required": false,
145
- "description": "Compare against a previously saved baseline file. Used by bare `fallow`, `dead-code`, `dupes` and `health`; other subcommands reject it"
145
+ "description": "Compare against a previously saved baseline file. Used by bare `fallow`, `dead-code`, `architecture`, `dupes` and `health`; other subcommands reject it. On bare `fallow` it holds the dead-code baseline only: pass health and duplication baselines with `--health-baseline` and `--dupes-baseline`"
146
146
  },
147
147
  {
148
148
  "name": "--baseline-mode",
@@ -164,7 +164,7 @@
164
164
  "name": "--save-baseline",
165
165
  "type": "string",
166
166
  "required": false,
167
- "description": "Save the current results as a baseline file. Used by bare `fallow`, `dead-code`, `dupes` and `health`; other subcommands reject it. The path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory"
167
+ "description": "Save the current results as a baseline file. Used by bare `fallow`, `dead-code`, `architecture`, `dupes` and `health`; other subcommands reject it. On bare `fallow` it saves the dead-code baseline only: save the others with `fallow health --save-baseline` and `fallow dupes --save-baseline`. The path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory"
168
168
  },
169
169
  {
170
170
  "name": "--production",
@@ -311,7 +311,7 @@
311
311
  "name": "--sarif-file",
312
312
  "type": "string",
313
313
  "required": false,
314
- "description": "Write SARIF output to a file (in addition to the primary --format output). Used by bare `fallow`, `dead-code` and `security`. The path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory"
314
+ "description": "Write SARIF output to a file (in addition to the primary --format output). Used by bare `fallow`, `dead-code`, `architecture` and `security`. The path must resolve inside the project root, its Git work tree, the CI workspace or a temp directory"
315
315
  },
316
316
  {
317
317
  "name": "--output-file",
@@ -399,7 +399,8 @@
399
399
  "possible_values": [
400
400
  "dead-code",
401
401
  "dupes",
402
- "health"
402
+ "health",
403
+ "architecture"
403
404
  ]
404
405
  },
405
406
  {
@@ -410,7 +411,8 @@
410
411
  "possible_values": [
411
412
  "dead-code",
412
413
  "dupes",
413
- "health"
414
+ "health",
415
+ "architecture"
414
416
  ]
415
417
  },
416
418
  {
@@ -585,6 +587,16 @@
585
587
  "false"
586
588
  ]
587
589
  },
590
+ {
591
+ "name": "--show-cascade",
592
+ "type": "bool",
593
+ "required": false,
594
+ "description": "Also report the unused exports, types, class members and enum members of unused files. By default the report hides them, because deleting the file removes them, and counts them in `cascade_hidden`",
595
+ "possible_values": [
596
+ "true",
597
+ "false"
598
+ ]
599
+ },
588
600
  {
589
601
  "name": "--type-aware",
590
602
  "type": "bool",
@@ -625,7 +637,7 @@
625
637
  "commands": [
626
638
  {
627
639
  "name": "dead-code",
628
- "description": "Analyze project for unused code and circular dependencies",
640
+ "description": "Analyze project for unused code and dependency hygiene",
629
641
  "flags": [
630
642
  {
631
643
  "name": "--unused-files",
@@ -851,7 +863,7 @@
851
863
  "name": "--circular-deps",
852
864
  "type": "bool",
853
865
  "required": false,
854
- "description": "Only report circular dependencies",
866
+ "description": "Only report circular dependencies. Deprecated alias: use `fallow architecture --cycles`",
855
867
  "possible_values": [
856
868
  "true",
857
869
  "false"
@@ -861,7 +873,7 @@
861
873
  "name": "--re-export-cycles",
862
874
  "type": "bool",
863
875
  "required": false,
864
- "description": "Only report re-export cycles",
876
+ "description": "Only report re-export cycles. Deprecated alias: use `fallow architecture --cycles`",
865
877
  "possible_values": [
866
878
  "true",
867
879
  "false"
@@ -871,7 +883,7 @@
871
883
  "name": "--package-cycles",
872
884
  "type": "bool",
873
885
  "required": false,
874
- "description": "Only report dependency cycles between workspace packages",
886
+ "description": "Only report dependency cycles between workspace packages. Deprecated alias: use `fallow architecture --cycles`",
875
887
  "possible_values": [
876
888
  "true",
877
889
  "false"
@@ -881,7 +893,7 @@
881
893
  "name": "--boundary-violations",
882
894
  "type": "bool",
883
895
  "required": false,
884
- "description": "Only report boundary violations",
896
+ "description": "Only report boundary violations. Deprecated alias: use `fallow architecture --boundaries`",
885
897
  "possible_values": [
886
898
  "true",
887
899
  "false"
@@ -891,7 +903,7 @@
891
903
  "name": "--policy-violations",
892
904
  "type": "bool",
893
905
  "required": false,
894
- "description": "Only report rule-pack policy violations",
906
+ "description": "Only report rule-pack policy violations. Deprecated alias: use `fallow architecture --policy`",
895
907
  "possible_values": [
896
908
  "true",
897
909
  "false"
@@ -1023,6 +1035,60 @@
1023
1035
  }
1024
1036
  ]
1025
1037
  },
1038
+ {
1039
+ "name": "architecture",
1040
+ "description": "Check import cycles, boundaries and policy rules after editing",
1041
+ "flags": [
1042
+ {
1043
+ "name": "--cycles",
1044
+ "type": "bool",
1045
+ "required": false,
1046
+ "description": "Only report import cycles: circular dependencies, re-export cycles and package cycles",
1047
+ "possible_values": [
1048
+ "true",
1049
+ "false"
1050
+ ]
1051
+ },
1052
+ {
1053
+ "name": "--boundaries",
1054
+ "type": "bool",
1055
+ "required": false,
1056
+ "description": "Only report boundary violations, boundary coverage and forbidden calls",
1057
+ "possible_values": [
1058
+ "true",
1059
+ "false"
1060
+ ]
1061
+ },
1062
+ {
1063
+ "name": "--policy",
1064
+ "type": "bool",
1065
+ "required": false,
1066
+ "description": "Only report rule-pack policy violations",
1067
+ "possible_values": [
1068
+ "true",
1069
+ "false"
1070
+ ]
1071
+ },
1072
+ {
1073
+ "name": "--file",
1074
+ "type": "string",
1075
+ "required": false,
1076
+ "description": "Only report issues in the specified file(s). Accepts multiple values. The full project graph is still built, but only issues in matching files are reported"
1077
+ },
1078
+ {
1079
+ "name": "--finding-id",
1080
+ "type": "string",
1081
+ "required": false,
1082
+ "description": "Only report the findings with these `finding_id` values. Repeat the flag or pass a comma-separated list"
1083
+ },
1084
+ {
1085
+ "name": "path",
1086
+ "type": "string",
1087
+ "required": false,
1088
+ "description": "Scope reported findings to this file or directory (default: whole project). The full project graph is still built; only reported items are narrowed"
1089
+ }
1090
+ ]
1091
+ },
1026
1092
  {
1027
1093
  "name": "watch",
1028
1094
  "description": "Watch for changes and re-run analysis",
@@ -1131,7 +1197,41 @@
1131
1197
  "name": "symbol",
1132
1198
  "type": "string",
1133
1199
  "required": false,
1134
- "description": "Target symbol, formatted as FILE:SYMBOL (e.g. src/utils.ts:formatDate). Omitted when `--path` is used"
1200
+ "description": "Target symbol, formatted as FILE:SYMBOL (e.g. src/utils.ts:formatDate). Omitted when `--path` or `--dependency` is used"
1201
+ },
1202
+ {
1203
+ "name": "--dependency",
1204
+ "type": "string",
1205
+ "required": false,
1206
+ "description": "How the code uses each imported name of a package: file and call counts, one hop through project wrappers, and a count of each use that the trace cannot resolve"
1207
+ },
1208
+ {
1209
+ "name": "--specifier",
1210
+ "type": "string",
1211
+ "required": false,
1212
+ "description": "With `--dependency`, report only these imported names (repeatable or comma-separated). Turns on the site page"
1213
+ },
1214
+ {
1215
+ "name": "--sites",
1216
+ "type": "bool",
1217
+ "required": false,
1218
+ "description": "With `--dependency`, list the usage sites with file, line and column",
1219
+ "possible_values": [
1220
+ "true",
1221
+ "false"
1222
+ ]
1223
+ },
1224
+ {
1225
+ "name": "--limit",
1226
+ "type": "string",
1227
+ "required": false,
1228
+ "description": "With `--dependency`, the largest number of sites on a page (1 to 500, default 50). Turns on the site page"
1229
+ },
1230
+ {
1231
+ "name": "--cursor",
1232
+ "type": "string",
1233
+ "required": false,
1234
+ "description": "With `--dependency`, the `next_cursor` of the previous page. Turns on the site page"
1135
1235
  },
1136
1236
  {
1137
1237
  "name": "--path",
@@ -2161,6 +2261,11 @@
2161
2261
  "description": "Maintain reusable audit base-snapshot caches",
2162
2262
  "flags": []
2163
2263
  },
2264
+ {
2265
+ "name": "baselines",
2266
+ "description": "Maintain the committed baseline files that the config points at",
2267
+ "flags": []
2268
+ },
2164
2269
  {
2165
2270
  "name": "decision-surface",
2166
2271
  "description": "Surface the consequential structural DECISIONS a change embeds (the apex of the review brief), each framed as a judgment question with the routed expert to ask",
@@ -2268,7 +2373,7 @@
2268
2373
  "name": "--from",
2269
2374
  "type": "string",
2270
2375
  "required": true,
2271
- "description": "Path to a fallow JSON results file produced by `--format json` (dead-code, dupes, health, audit, security, or bare combined)"
2376
+ "description": "Path to a fallow JSON results file produced by `--format json` (dead-code, architecture, dupes, health, audit, security, or bare combined)"
2272
2377
  }
2273
2378
  ]
2274
2379
  },
@@ -7037,6 +7142,24 @@
7037
7142
  "read_only": false,
7038
7143
  "code_mode_alias": "checkChanged"
7039
7144
  },
7145
+ {
7146
+ "name": "check_architecture",
7147
+ "kind": "analysis",
7148
+ "description": "Architecture check: import cycles, boundary violations, and rule-pack policy violations in the `architecture` envelope, each with a stable finding_id",
7149
+ "cli_command": "fallow architecture --format json --quiet",
7150
+ "key_params": [
7151
+ "cycles",
7152
+ "boundaries",
7153
+ "policy",
7154
+ "baseline",
7155
+ "group_by",
7156
+ "file"
7157
+ ],
7158
+ "license": "free",
7159
+ "license_note": null,
7160
+ "read_only": true,
7161
+ "code_mode_alias": "checkArchitecture"
7162
+ },
7040
7163
  {
7041
7164
  "name": "security_candidates",
7042
7165
  "kind": "analysis",
@@ -7558,10 +7681,14 @@
7558
7681
  {
7559
7682
  "name": "trace_dependency",
7560
7683
  "kind": "trace",
7561
- "description": "Trace where a dependency is imported and whether scripts or CI use it",
7684
+ "description": "Trace where a dependency is imported, whether scripts or CI use it, and how the code uses each imported name",
7562
7685
  "cli_command": "fallow dead-code --trace-dependency <package> --format json --quiet",
7563
7686
  "key_params": [
7564
- "package_name"
7687
+ "package_name",
7688
+ "specifiers",
7689
+ "sites",
7690
+ "limit",
7691
+ "cursor"
7565
7692
  ],
7566
7693
  "license": "free",
7567
7694
  "license_note": null,
@@ -7674,7 +7801,7 @@
7674
7801
  ]
7675
7802
  },
7676
7803
  "plugins": {
7677
- "count": 133,
7804
+ "count": 134,
7678
7805
  "note": "Built-in framework plugins, auto-activated when their enabler dependency is present; run fallow list --plugins for the set active in a specific project",
7679
7806
  "names": [
7680
7807
  "nextjs",
@@ -7695,6 +7822,7 @@
7695
7822
  "firebase",
7696
7823
  "nestjs",
7697
7824
  "nestjs-trpc",
7825
+ "nest-commander",
7698
7826
  "adonis",
7699
7827
  "docusaurus",
7700
7828
  "gatsby",
@@ -7833,6 +7961,11 @@
7833
7961
  "command": "fallow dead-code --trace-dependency <name>",
7834
7962
  "note": null
7835
7963
  },
7964
+ {
7965
+ "task": "migrate a dependency",
7966
+ "command": "fallow trace --dependency <name> --sites",
7967
+ "note": "Counts each imported name, follows one hop through project wrappers, and counts each use that it cannot resolve."
7968
+ },
7836
7969
  {
7837
7970
  "task": "commit or open a PR",
7838
7971
  "command": "fallow audit --base <ref>",
@@ -7873,6 +8006,11 @@
7873
8006
  "command": "fallow guard <files>",
7874
8007
  "note": null
7875
8008
  },
8009
+ {
8010
+ "task": "check import cycles, boundaries and policy rules after changing code",
8011
+ "command": "fallow architecture",
8012
+ "note": null
8013
+ },
7876
8014
  {
7877
8015
  "task": "surface security candidates",
7878
8016
  "command": "fallow security",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "3.32.0",
3
+ "version": "3.33.1",
4
4
  "mcpName": "io.github.fallow-rs/fallow",
5
5
  "description": "Codebase intelligence for TypeScript and JavaScript: health, complexity, duplication, architecture, styling drift, and unused code from one graph. CLI, LSP, and MCP server. Zero config for over 100 frameworks.",
6
6
  "license": "MIT",
@@ -89,14 +89,14 @@
89
89
  "@tanstack/intent": "0.4.0"
90
90
  },
91
91
  "optionalDependencies": {
92
- "@fallow-cli/darwin-arm64": "3.32.0",
93
- "@fallow-cli/darwin-x64": "3.32.0",
94
- "@fallow-cli/linux-x64-gnu": "3.32.0",
95
- "@fallow-cli/linux-arm64-gnu": "3.32.0",
96
- "@fallow-cli/linux-x64-musl": "3.32.0",
97
- "@fallow-cli/linux-arm64-musl": "3.32.0",
98
- "@fallow-cli/win32-arm64-msvc": "3.32.0",
99
- "@fallow-cli/win32-x64-msvc": "3.32.0",
100
- "fallow-type-aware": "3.32.0"
92
+ "@fallow-cli/darwin-arm64": "3.33.1",
93
+ "@fallow-cli/darwin-x64": "3.33.1",
94
+ "@fallow-cli/linux-x64-gnu": "3.33.1",
95
+ "@fallow-cli/linux-arm64-gnu": "3.33.1",
96
+ "@fallow-cli/linux-x64-musl": "3.33.1",
97
+ "@fallow-cli/linux-arm64-musl": "3.33.1",
98
+ "@fallow-cli/win32-arm64-msvc": "3.33.1",
99
+ "@fallow-cli/win32-x64-msvc": "3.33.1",
100
+ "fallow-type-aware": "3.33.1"
101
101
  }
102
102
  }
package/schema.json CHANGED
@@ -380,6 +380,11 @@
380
380
  "type": "boolean",
381
381
  "default": false
382
382
  },
383
+ "showCascade": {
384
+ "description": "When true, the report keeps the unused exports, unused types, unused class members and unused enum members of a file that is an unused file. Boolean, defaults to false: deleting an unused file removes these findings, so the report hides them and counts them in cascade_hidden. The CLI flag --show-cascade applies the same behavior for one run.",
385
+ "type": "boolean",
386
+ "default": false
387
+ },
383
388
  "autoImports": {
384
389
  "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.",
385
390
  "type": "boolean",
@@ -42,12 +42,24 @@ const VERIFY_LOG_ENV = "FALLOW_VERIFY_LOG";
42
42
  // keyed by `code` so independent failure modes are still surfaced.
43
43
  const _warningEmitted = new Set();
44
44
 
45
+ // Write to fd 2 directly. Under Bun, the first use of `process.stderr` sets
46
+ // O_NONBLOCK on the pipe, and the platform binary inherits that pipe (and
47
+ // stdout too when stderr and stdout share one pipe). See issue #3276.
48
+ // Diagnostics are best-effort, so a failed write is ignored.
49
+ function writeStderr(text) {
50
+ try {
51
+ fs.writeSync(2, text);
52
+ } catch {
53
+ // Ignore: no reader, or a closed descriptor.
54
+ }
55
+ }
56
+
45
57
  function warnOnce(code, message) {
46
58
  if (_warningEmitted.has(code)) {
47
59
  return;
48
60
  }
49
61
  _warningEmitted.add(code);
50
- process.stderr.write(`fallow: ${message}\n`);
62
+ writeStderr(`fallow: ${message}\n`);
51
63
  }
52
64
 
53
65
  function isVerifyLogEnabled(env) {
@@ -68,7 +80,7 @@ function emitVerifyLog(env, payload) {
68
80
  parts.push(`${key}=${v}`);
69
81
  }
70
82
  }
71
- process.stderr.write(`fallow-verify ${parts.join(" ")}\n`);
83
+ writeStderr(`fallow-verify ${parts.join(" ")}\n`);
72
84
  }
73
85
 
74
86
  // The sentinel binds the same binaries that verify-binary checks, so a new
@@ -69,13 +69,15 @@ function cleanup(dir) {
69
69
 
70
70
  function captureStderr(t) {
71
71
  const lines = [];
72
- const original = process.stderr.write.bind(process.stderr);
73
- process.stderr.write = (chunk) => {
72
+ // lazy-verify writes diagnostics to fd 2 with fs.writeSync (issue #3276).
73
+ const original = fs.writeSync;
74
+ fs.writeSync = (fd, chunk, ...rest) => {
75
+ if (fd !== 2) return original(fd, chunk, ...rest);
74
76
  lines.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
75
- return true;
77
+ return chunk.length;
76
78
  };
77
79
  t.after(() => {
78
- process.stderr.write = original;
80
+ fs.writeSync = original;
79
81
  });
80
82
  return { lines };
81
83
  }
@@ -102,8 +102,13 @@ function printVerifyError(verifyResult) {
102
102
  );
103
103
  }
104
104
 
105
+ // Call only after the child exits. Bun sets O_NONBLOCK on fd 1 when a script
106
+ // first reads `process.stdout`. The child inherits that open file description,
107
+ // so a large write from the binary fails with EAGAIN and the output is cut
108
+ // short (issue #3276).
105
109
  function writeVerifiedLineIfVersionQuery(verifyResult, version) {
106
110
  if (isVersionQuery(process.argv)) {
111
+ guardBrokenStdout();
107
112
  process.stdout.write(`${describeVerified(verifyResult, version)}\n`);
108
113
  }
109
114
  }
@@ -205,7 +210,8 @@ function guardBrokenStdout() {
205
210
  // handling stay identical to a bare `fallow` invocation.
206
211
  function runBinary(binaryBaseName, options = {}) {
207
212
  const prependArgs = Array.isArray(options.prependArgs) ? options.prependArgs : [];
208
- guardBrokenStdout();
213
+ // Do not read `process.stdout` or write to `process.stderr` before the child
214
+ // exits: under Bun that makes the inherited pipes non-blocking for the child.
209
215
  const { pkg, manifestPath, platformPkgDir } = resolvePlatformPaths();
210
216
  const resolvedVersion = readResolvedVersion(manifestPath);
211
217
 
@@ -22,7 +22,9 @@ function currentPlatformPackage() {
22
22
  return getPlatformPackage(process.platform, process.arch, libcFamily);
23
23
  }
24
24
 
25
- function runLauncher(t, launcher, args) {
25
+ // Install a fake platform package whose `fallow` binary is the given Node
26
+ // script. Returns the scratch directory and the environment for the launcher.
27
+ function installFakeBinary(t, script) {
26
28
  const work = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-launcher-"));
27
29
  t.after(() => fs.rmSync(work, { recursive: true, force: true }));
28
30
 
@@ -36,27 +38,84 @@ function runLauncher(t, launcher, args) {
36
38
  );
37
39
 
38
40
  const binary = path.join(pkgDir, "fallow");
39
- fs.writeFileSync(
40
- binary,
41
- "#!/usr/bin/env node\n" +
42
- 'require("node:fs").writeFileSync(process.env.FALLOW_TEST_ARGS, JSON.stringify(process.argv.slice(2)));\n',
43
- );
41
+ fs.writeFileSync(binary, `#!/usr/bin/env node\n${script}`);
44
42
  fs.chmodSync(binary, 0o755);
45
43
 
44
+ const env = {
45
+ ...process.env,
46
+ NODE_PATH: path.join(work, "node_modules"),
47
+ FALLOW_SKIP_BINARY_VERIFY: "1",
48
+ };
49
+ return { work, env };
50
+ }
51
+
52
+ function runLauncher(t, launcher, args) {
53
+ const { work, env } = installFakeBinary(
54
+ t,
55
+ 'require("node:fs").writeFileSync(process.env.FALLOW_TEST_ARGS, JSON.stringify(process.argv.slice(2)));\n',
56
+ );
46
57
  const argsFile = path.join(work, "args.json");
47
58
  const result = spawnSync(process.execPath, [path.join(BIN_DIR, launcher), ...args], {
48
59
  encoding: "utf8",
49
- env: {
50
- ...process.env,
51
- NODE_PATH: path.join(work, "node_modules"),
52
- FALLOW_SKIP_BINARY_VERIFY: "1",
53
- FALLOW_TEST_ARGS: argsFile,
54
- },
60
+ env: { ...env, FALLOW_TEST_ARGS: argsFile },
55
61
  });
56
62
  assert.equal(result.status, 0, result.stderr);
57
63
  return JSON.parse(fs.readFileSync(argsFile, "utf8"));
58
64
  }
59
65
 
66
+ // The fake binary writes a large payload the way the Rust binary does: plain
67
+ // write calls on fd 1, and it stops at the first write error but exits 0.
68
+ // A non-blocking inherited pipe makes a write fail with EAGAIN, so the reader
69
+ // gets a short payload (issue #3276).
70
+ const LARGE_OUTPUT_BYTES = 4 * 1024 * 1024;
71
+ const LARGE_OUTPUT_BINARY =
72
+ 'const fs = require("node:fs");\n' +
73
+ `const total = ${LARGE_OUTPUT_BYTES};\n` +
74
+ 'const chunk = Buffer.alloc(64 * 1024, "x");\n' +
75
+ "let written = 0;\n" +
76
+ "try {\n" +
77
+ " while (written < total) written += fs.writeSync(1, chunk, 0, Math.min(chunk.length, total - written));\n" +
78
+ "} catch {}\n";
79
+
80
+ function bunAvailable() {
81
+ const probe = spawnSync("bun", ["--version"], { encoding: "utf8" });
82
+ return probe.status === 0;
83
+ }
84
+
85
+ function runLargeOutput(t, runtime, { mergeStderr = false } = {}) {
86
+ const { env } = installFakeBinary(t, LARGE_OUTPUT_BINARY);
87
+ const launcher = path.join(BIN_DIR, "fallow");
88
+ const [command, args] = mergeStderr
89
+ ? ["/bin/sh", ["-c", 'exec "$0" "$@" 2>&1', runtime, launcher, "check"]]
90
+ : [runtime, [launcher, "check"]];
91
+ return spawnSync(command, args, { env, maxBuffer: 2 * LARGE_OUTPUT_BYTES });
92
+ }
93
+
94
+ // Count only the payload bytes. In the merged case, stdout also holds the
95
+ // FALLOW_SKIP_BINARY_VERIFY warning line.
96
+ function payloadBytes(stdout) {
97
+ let count = 0;
98
+ for (const byte of stdout) if (byte === 0x78) count += 1;
99
+ return count;
100
+ }
101
+
102
+ for (const runtime of ["node", "bun"]) {
103
+ for (const mergeStderr of [false, true]) {
104
+ const name = mergeStderr ? "stdout and stderr on one pipe" : "stdout on its own pipe";
105
+ test(
106
+ `launcher under ${runtime} passes the complete binary output (${name})`,
107
+ { skip: process.platform === "win32" || (runtime === "bun" && !bunAvailable()) },
108
+ (t) => {
109
+ const result = runLargeOutput(t, runtime === "node" ? process.execPath : "bun", {
110
+ mergeStderr,
111
+ });
112
+ assert.equal(result.status, 0, String(result.stderr));
113
+ assert.equal(payloadBytes(result.stdout), LARGE_OUTPUT_BYTES);
114
+ },
115
+ );
116
+ }
117
+ }
118
+
60
119
  // Run a child that installs guardBrokenStdout, then emits a synthetic stdout
61
120
  // 'error' with the given code. Node delivers a broken-pipe failure as exactly
62
121
  // this event ("Emitted 'error' event on Socket instance"), so emitting it is a
@@ -44,7 +44,7 @@ cargo install fallow-cli # build from source
44
44
  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`.
45
45
  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.
46
46
  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.
47
- 4. **Use the root `kind` field** to identify typed JSON envelopes (`dead-code`, `dead-code-grouped`, `health`, `dupes`, `combined`, `audit`, etc.).
47
+ 4. **Use the root `kind` field** to identify typed JSON envelopes (`dead-code`, `dead-code-grouped`, `architecture`, `architecture-grouped`, `health`, `dupes`, `combined`, `audit`, etc.).
48
48
  5. **Use issue type filters** (`--unused-exports`, `--unused-files`, etc.) to limit output scope
49
49
  6. **Always `--dry-run` before `fix`**, then `fix --yes` to apply
50
50
  7. **All output paths are relative** to the project root
@@ -85,6 +85,7 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
85
85
  | prove a TypeScript symbol's exact consumers before refactoring | `fallow dead-code --type-aware --symbol-impact <file>:<export-or-class.method>` |
86
86
  | find how one module reaches another | `fallow trace --path <from> <to>`; Reports `reachable: false` instead of failing when no import path exists; type-only hops are reported, not skipped. |
87
87
  | delete an "unused" dependency | `fallow dead-code --trace-dependency <name>` |
88
+ | migrate a dependency | `fallow trace --dependency <name> --sites`; Counts each imported name, follows one hop through project wrappers, and counts each use that it cannot resolve. |
88
89
  | commit or open a PR | `fallow audit --base <ref>` |
89
90
  | read a diff before approving it | `fallow review --base <ref> --brief`; orientation, never gates: deterministic and always exit 0, unlike the audit row |
90
91
  | prioritize refactoring | `fallow health --hotspots --targets` |
@@ -93,6 +94,7 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
93
94
  | consolidate duplication | `fallow dupes --trace dup:<fingerprint>` |
94
95
  | find feature flags | `fallow flags` |
95
96
  | check which architecture rules apply to a file before changing it | `fallow guard <files>` |
97
+ | check import cycles, boundaries and policy rules after changing code | `fallow architecture` |
96
98
  | surface security candidates | `fallow security` |
97
99
  | understand a finding | `fallow explain <issue-type>` |
98
100
  | scope a monorepo | `--workspace <glob> / --changed-workspaces <ref>`; global flags, prefix any command |
@@ -106,7 +108,9 @@ Full command catalogue, one row per command: **[references/cli-reference.md](ref
106
108
 
107
109
  ## Issue Types
108
110
 
109
- Dead-code filter flags select issue categories (`--unused-exports`, `--unused-types`, `--unused-deps`, `--circular-deps`, and so on). Some flags select related types together; `--unused-deps` covers several dependency types. Passing one or more narrows `fallow dead-code` to those categories. Passing none applies no issue-type filter.
111
+ Dead-code filter flags select issue categories (`--unused-exports`, `--unused-types`, `--unused-deps`, `--unused-files`, and so on). Some flags select related types together; `--unused-deps` covers several dependency types. Passing one or more narrows `fallow dead-code` to those categories. Passing none applies no issue-type filter.
112
+
113
+ `fallow architecture` reports the architecture findings: circular dependencies, re-export cycles, package cycles, boundary violations and rule-pack policy violations. Use `--cycles`, `--boundaries` or `--policy` to select one kind. The JSON output has `kind: "architecture"` and its own `schema_version`, with the same arrays and finding ids as `dead-code`. `fallow dead-code` still reports these findings until the next major version. Its `--circular-deps`, `--re-export-cycles`, `--package-cycles`, `--boundary-violations` and `--policy-violations` flags are deprecated aliases. Run `fallow guard <files>` before an edit and `fallow architecture` after it. In bare `fallow`, `--only architecture` reports only these findings and `--skip architecture` removes them.
110
114
 
111
115
  For suppressible findings, use the placement listed in the issue catalogue: `// fallow-ignore-next-line <issue-type>` above the finding, or `// fallow-ignore-file <issue-type>` at the top of the file. Some types support only file-level suppression; others have no suppression comment. A bare supported form without a type suppresses all matching findings at that placement.
112
116
 
@@ -282,7 +286,7 @@ fallow dead-code --format json --quiet --save-baseline .fallow/snapshot.json
282
286
  fallow dead-code --format json --quiet --baseline .fallow/snapshot.json
283
287
  ```
284
288
 
285
- `--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.
289
+ `--save-regression-baseline` / `--regression-baseline` / `--fail-on-regression` / `--tolerance` are count-based gates for `dead-code`, `architecture`, 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). On bare `fallow` they hold the dead-code baseline only; pass health and duplication baselines with `--health-baseline` / `--dupes-baseline`. `audit` rejects the global baseline flags and uses `--dead-code-baseline` / `--health-baseline` / `--dupes-baseline` instead.
286
290
 
287
291
  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.
288
292