fallow 3.31.0 → 3.33.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 +17 -10
- package/capabilities.json +420 -172
- package/issue-registry.json +190 -154
- package/package.json +12 -12
- package/schema.json +36 -1
- package/scripts/lazy-verify.js +14 -2
- package/scripts/lazy-verify.test.js +6 -4
- package/scripts/run-binary.js +7 -1
- package/scripts/run-binary.test.js +71 -12
- package/skills/fallow/SKILL.md +35 -11
- package/skills/fallow/references/cli-reference.md +155 -44
- package/skills/fallow/references/gotchas.md +22 -14
- package/skills/fallow/references/issue-types.md +3 -2
- package/skills/fallow/references/mcp.md +19 -6
- package/skills/fallow/references/node-bindings.md +9 -3
- package/skills/fallow/references/patterns.md +43 -15
- package/skills/fallow-setup/SKILL.md +50 -0
- package/skills/fallow-setup/agents/openai.yaml +4 -0
- package/skills/fallow-setup/references/ci-gate.md +61 -0
- package/skills/fallow-setup/references/configure-and-install.md +56 -0
- package/skills/fallow-setup/references/tooling-detection.md +44 -0
- package/types/output-contract.d.ts +1528 -211
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/fallow-rs/fallow.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://fallow.tools/docs/",
|
|
12
12
|
"funding": "https://github.com/sponsors/fallow-rs",
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/fallow-rs/fallow/issues"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"intent": {
|
|
17
17
|
"version": 1,
|
|
18
18
|
"repo": "fallow-rs/fallow",
|
|
19
|
-
"docs": "https://
|
|
19
|
+
"docs": "https://fallow.tools/docs/"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"code-intelligence",
|
|
@@ -89,14 +89,14 @@
|
|
|
89
89
|
"@tanstack/intent": "0.4.0"
|
|
90
90
|
},
|
|
91
91
|
"optionalDependencies": {
|
|
92
|
-
"@fallow-cli/darwin-arm64": "3.
|
|
93
|
-
"@fallow-cli/darwin-x64": "3.
|
|
94
|
-
"@fallow-cli/linux-x64-gnu": "3.
|
|
95
|
-
"@fallow-cli/linux-arm64-gnu": "3.
|
|
96
|
-
"@fallow-cli/linux-x64-musl": "3.
|
|
97
|
-
"@fallow-cli/linux-arm64-musl": "3.
|
|
98
|
-
"@fallow-cli/win32-arm64-msvc": "3.
|
|
99
|
-
"@fallow-cli/win32-x64-msvc": "3.
|
|
100
|
-
"fallow-type-aware": "3.
|
|
92
|
+
"@fallow-cli/darwin-arm64": "3.33.0",
|
|
93
|
+
"@fallow-cli/darwin-x64": "3.33.0",
|
|
94
|
+
"@fallow-cli/linux-x64-gnu": "3.33.0",
|
|
95
|
+
"@fallow-cli/linux-arm64-gnu": "3.33.0",
|
|
96
|
+
"@fallow-cli/linux-x64-musl": "3.33.0",
|
|
97
|
+
"@fallow-cli/linux-arm64-musl": "3.33.0",
|
|
98
|
+
"@fallow-cli/win32-arm64-msvc": "3.33.0",
|
|
99
|
+
"@fallow-cli/win32-x64-msvc": "3.33.0",
|
|
100
|
+
"fallow-type-aware": "3.33.0"
|
|
101
101
|
}
|
|
102
102
|
}
|
package/schema.json
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"default": []
|
|
137
137
|
},
|
|
138
138
|
"duplicates": {
|
|
139
|
-
"description": "Configures clone detection: `enabled` (default true), `mode` (`strict`, `mild` default, `weak`, `semantic`, from least to most identifier/literal blinding; `strict` and `mild` are equivalent under fallow's AST tokenizer, `weak` blinds string literals, `semantic` blinds all identifiers and literals for Type-2 renamed-variable detection), `near` (false, add bounded function-scoped near-miss detection), `minTokens` (50), `minLines` (5), `minOccurrences` (integer >= 2, deserialization fails below 2), `threshold` (max duplication percentage, 0 = no limit), `ignore` globs, `ignoredClones` (reviewed clone keys to hide until their content or occurrence count changes), `ignoreDefaults` (true, merge built-in generated-file ignores), `skipLocal` (only report cross-directory clones), `crossLanguage` (strip TS type annotations to match .ts against .js), `ignoreImports` (true, strip ES import/re-export/top-level require wiring from the token stream), and `normalization` (per-flag `ignoreIdentifiers`/`ignoreStringValues`/`ignoreNumericValues` overrides on top of `mode`). Raise `minOccurrences` to focus on widespread copy-paste, enable `near` for gapped structural clones, or set `mode` to `semantic` to catch renamed-variable exact clones.",
|
|
139
|
+
"description": "Configures clone detection: `enabled` (default true), `mode` (`strict`, `mild` default, `weak`, `semantic`, from least to most identifier/literal blinding; `strict` and `mild` are equivalent under fallow's AST tokenizer, `weak` blinds string literals, `semantic` blinds all identifiers and literals for Type-2 renamed-variable detection), `near` (false, add bounded function-scoped near-miss detection), `minTokens` (50), `minLines` (5), `minOccurrences` (integer >= 2, deserialization fails below 2), `threshold` (max duplication percentage, 0 = no limit), `ignore` globs, `ignoredClones` (reviewed clone keys to hide until their content or occurrence count changes), `ignoreDefaults` (true, merge built-in generated-file ignores), `skipLocal` (only report cross-directory clones), `ignoreSymlinks` (false, omit clone instances whose path is a symlink or lies under a symlinked directory), `crossLanguage` (strip TS type annotations to match .ts against .js), `ignoreImports` (true, strip ES import/re-export/top-level require wiring from the token stream), and `normalization` (per-flag `ignoreIdentifiers`/`ignoreStringValues`/`ignoreNumericValues` overrides on top of `mode`). Raise `minOccurrences` to focus on widespread copy-paste, enable `near` for gapped structural clones, or set `mode` to `semantic` to catch renamed-variable exact clones.",
|
|
140
140
|
"$ref": "#/$defs/DuplicatesConfig",
|
|
141
141
|
"default": {
|
|
142
142
|
"enabled": true,
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"ignoredClones": [],
|
|
151
151
|
"ignoreDefaults": true,
|
|
152
152
|
"skipLocal": false,
|
|
153
|
+
"ignoreSymlinks": false,
|
|
153
154
|
"crossLanguage": false,
|
|
154
155
|
"ignoreImports": true,
|
|
155
156
|
"normalization": {},
|
|
@@ -214,6 +215,7 @@
|
|
|
214
215
|
"unprovided-injects": "warn",
|
|
215
216
|
"unrendered-components": "warn",
|
|
216
217
|
"unused-component-props": "warn",
|
|
218
|
+
"absent-component-props": "off",
|
|
217
219
|
"unused-component-emits": "warn",
|
|
218
220
|
"unused-component-inputs": "warn",
|
|
219
221
|
"unused-component-outputs": "warn",
|
|
@@ -378,6 +380,11 @@
|
|
|
378
380
|
"type": "boolean",
|
|
379
381
|
"default": false
|
|
380
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
|
+
},
|
|
381
388
|
"autoImports": {
|
|
382
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.",
|
|
383
390
|
"type": "boolean",
|
|
@@ -949,6 +956,11 @@
|
|
|
949
956
|
"type": "boolean",
|
|
950
957
|
"default": false
|
|
951
958
|
},
|
|
959
|
+
"ignoreSymlinks": {
|
|
960
|
+
"description": "Omit clone instances whose file path is a symlink, or lies under a\nsymlinked directory, inside the project root.\n\nDefaults to `false`: symlinked instances stay in the report and carry\n`is_symlink: true` in JSON output. Set to `true` to report only\nduplication between real files. A clone group with fewer than two\nremaining instances is not reported. Symlinked files also leave the\ncorpus, so `stats` (files, lines, tokens and the duplication\npercentage that `threshold` checks) count only real files.",
|
|
961
|
+
"type": "boolean",
|
|
962
|
+
"default": false
|
|
963
|
+
},
|
|
952
964
|
"crossLanguage": {
|
|
953
965
|
"description": "Enable cross-language clone detection by stripping type annotations.\n\nWhen enabled, TypeScript type annotations (parameter types, return types,\ngenerics, interfaces, type aliases) are stripped from the token stream,\nallowing detection of clones between `.ts` and `.js` files.",
|
|
954
966
|
"type": "boolean",
|
|
@@ -1388,6 +1400,11 @@
|
|
|
1388
1400
|
"$ref": "#/$defs/Severity",
|
|
1389
1401
|
"default": "warn"
|
|
1390
1402
|
},
|
|
1403
|
+
"absent-component-props": {
|
|
1404
|
+
"description": "Used optional inputs absent from inspected callers. Requires manual review; off by default.",
|
|
1405
|
+
"$ref": "#/$defs/Severity",
|
|
1406
|
+
"default": "off"
|
|
1407
|
+
},
|
|
1391
1408
|
"unused-component-emits": {
|
|
1392
1409
|
"description": "Vue `<script setup>` `defineEmits` declared event emitted nowhere inside\nits own single-file component (no `emit('<name>')` call). The single-file\ndead-input direction. Defaults to `warn`, not `error`: an emit can be part\nof a deliberately-stable public component API, so analyzer confidence is\nlower; warn encodes that without failing CI.",
|
|
1393
1410
|
"$ref": "#/$defs/Severity",
|
|
@@ -2230,6 +2247,17 @@
|
|
|
2230
2247
|
}
|
|
2231
2248
|
]
|
|
2232
2249
|
},
|
|
2250
|
+
"absent-component-props": {
|
|
2251
|
+
"description": "Optional override for absent optional component inputs.",
|
|
2252
|
+
"anyOf": [
|
|
2253
|
+
{
|
|
2254
|
+
"$ref": "#/$defs/Severity"
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
"type": "null"
|
|
2258
|
+
}
|
|
2259
|
+
]
|
|
2260
|
+
},
|
|
2233
2261
|
"unused-component-emits": {
|
|
2234
2262
|
"description": "Optional override for [`RulesConfig::unused_component_emits`].",
|
|
2235
2263
|
"anyOf": [
|
|
@@ -2737,6 +2765,13 @@
|
|
|
2737
2765
|
"description": "Saved per-issue-type counts written by `--save-baseline` and compared by\n`--fail-on-regression`. Every count defaults to `0` when its key is missing,\nso hand-trimmed baselines stay loadable.",
|
|
2738
2766
|
"type": "object",
|
|
2739
2767
|
"properties": {
|
|
2768
|
+
"absentComponentProps": {
|
|
2769
|
+
"description": "Baseline count of optional component prop review candidates.",
|
|
2770
|
+
"type": "integer",
|
|
2771
|
+
"format": "uint",
|
|
2772
|
+
"minimum": 0,
|
|
2773
|
+
"default": 0
|
|
2774
|
+
},
|
|
2740
2775
|
"analysisIdentity": {
|
|
2741
2776
|
"description": "Compatibility identity for the analysis that produced these counts.\nMissing values in existing configs are treated as syntactic.",
|
|
2742
2777
|
"$ref": "#/$defs/SemanticAnalysisIdentity",
|
package/scripts/lazy-verify.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
77
|
+
return chunk.length;
|
|
76
78
|
};
|
|
77
79
|
t.after(() => {
|
|
78
|
-
|
|
80
|
+
fs.writeSync = original;
|
|
79
81
|
});
|
|
80
82
|
return { lines };
|
|
81
83
|
}
|
package/scripts/run-binary.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/skills/fallow/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ license: MIT
|
|
|
6
6
|
|
|
7
7
|
# Fallow: codebase intelligence for TypeScript and JavaScript
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Fallow analyzes code and styles for changed-code risk, cleanup opportunities, circular dependencies, duplication, complexity hotspots, architecture boundary violations, design-system styling drift, feature flags, and opt-in security candidates. Runtime coverage merges production execution data into the same `fallow health` report for hot-path review and cold-path deletion confidence. A single local capture is available by default. Continuous or cloud runtime monitoring is optional.
|
|
10
10
|
|
|
11
11
|
## When to Use
|
|
12
12
|
- Find cleanup opportunities: unused files, exports, types, members, dependencies, or feature flags that guard unused exports.
|
|
@@ -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
|
|
@@ -58,7 +58,23 @@ cargo install fallow-cli # build from source
|
|
|
58
58
|
15. **Check production runtime data before an edit or a delete, when Fallow Cloud is connected**. Prefer the scoped reads over the full `get_cloud_runtime_context` pull. Before an edit, run `fallow coverage review-packet --repo <owner/repo>` (MCP `get_cloud_review_packet`) for the changed files and read `hit_count`, `covered_by_test`, and the callers when present. Before a delete, require `period_tracking_state: "never_called"`, not only `tracking_state`, and read `evidence_window.observed_hours`: few observed hours or a low-traffic surface mean "not visited", not "dead". After a deploy, run `fallow coverage deployment-changes --repo <owner/repo>` (MCP `get_cloud_deployment_changes`) and, when `comparable` is `false`, report `reason` and claim no change. Open files by `repo_path`, not `file_path`. Production data is context, never a gate. Details: [references/mcp.md](references/mcp.md#scoped-cloud-reads).
|
|
59
59
|
|
|
60
60
|
## Onboarding And Insight
|
|
61
|
-
Offer setup
|
|
61
|
+
Offer setup after showing the findings from a human-requested analysis, only when all these conditions hold:
|
|
62
|
+
|
|
63
|
+
- `fallow config --path` exits 3.
|
|
64
|
+
- The run is outside CI and does not use a pipeline format.
|
|
65
|
+
- `fallow impact --format json --quiet` has `onboarding_declined: false`.
|
|
66
|
+
- No offer happened this session.
|
|
67
|
+
|
|
68
|
+
Offer these choices: guard commits and PRs, baseline the existing backlog and clean by category, add AGENTS.md guidance, or keep as-is. Mutate only after consent. On decline, run `fallow init --decline --quiet` and stay silent for this project.
|
|
69
|
+
|
|
70
|
+
For guards, inspect `fallow hooks status --format json --quiet`, then use `fallow hooks install --target agent` and `fallow hooks install --target git`. For large backlogs, pair the gate with `--save-baseline` and new-only guidance.
|
|
71
|
+
|
|
72
|
+
Offer `fallow impact enable` for local value tracking. Impact is separate from telemetry. Also offer it once on an already-configured project when `fallow impact status --format json` has `enabled: false` and `explicit_decision: false`. Record a decline with `fallow impact disable --quiet`.
|
|
73
|
+
|
|
74
|
+
Report value when there is evidence. If the agent gate blocked a commit or push and a later retry succeeded, describe the issues that caused the block. When `next_steps` has id `impact-report`, run its command and report the non-zero numbers in one line. On request, summarize non-zero Impact counts.
|
|
75
|
+
|
|
76
|
+
Ask about telemetry only after one of these events, and only if `fallow telemetry status --format json` has `explicit_decision: false`. Never run `fallow telemetry enable`.
|
|
77
|
+
|
|
62
78
|
## Task Cheat Sheet
|
|
63
79
|
Route by intent before reaching for the big analysis commands. Same matrix as `fallow schema` (`task_matrix`) and the generated AGENTS.md section.
|
|
64
80
|
|
|
@@ -69,6 +85,7 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
|
|
|
69
85
|
| prove a TypeScript symbol's exact consumers before refactoring | `fallow dead-code --type-aware --symbol-impact <file>:<export-or-class.method>` |
|
|
70
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. |
|
|
71
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. |
|
|
72
89
|
| commit or open a PR | `fallow audit --base <ref>` |
|
|
73
90
|
| read a diff before approving it | `fallow review --base <ref> --brief`; orientation, never gates: deterministic and always exit 0, unlike the audit row |
|
|
74
91
|
| prioritize refactoring | `fallow health --hotspots --targets` |
|
|
@@ -77,6 +94,7 @@ Route by intent before reaching for the big analysis commands. Same matrix as `f
|
|
|
77
94
|
| consolidate duplication | `fallow dupes --trace dup:<fingerprint>` |
|
|
78
95
|
| find feature flags | `fallow flags` |
|
|
79
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` |
|
|
80
98
|
| surface security candidates | `fallow security` |
|
|
81
99
|
| understand a finding | `fallow explain <issue-type>` |
|
|
82
100
|
| scope a monorepo | `--workspace <glob> / --changed-workspaces <ref>`; global flags, prefix any command |
|
|
@@ -90,7 +108,11 @@ Full command catalogue, one row per command: **[references/cli-reference.md](ref
|
|
|
90
108
|
|
|
91
109
|
## Issue Types
|
|
92
110
|
|
|
93
|
-
Dead-code filter flags
|
|
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.
|
|
114
|
+
|
|
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.
|
|
94
116
|
|
|
95
117
|
`fallow explain <issue-type>` describes one type without running analysis, and the MCP server serves the same catalogue as the `fallow://issue-types` resource.
|
|
96
118
|
|
|
@@ -100,7 +122,7 @@ Full catalogue, one row per type: **[references/issue-types.md](references/issue
|
|
|
100
122
|
|
|
101
123
|
Fallow ships an MCP server (`fallow-mcp`) that exposes these same analyses as agent tools. When the server is connected, its tools are already in your context with typed params and structured JSON returns, and each maps to a CLI fallback command. Prefer them when you want JSON without shelling out, or `code_execute` (Code Mode) to compose several read-only analyses in one sandboxed snippet (no single-call CLI equivalent). Otherwise use the CLI.
|
|
102
124
|
|
|
103
|
-
The server also serves read-only reference resources (no subprocess, no analysis run, cacheable by URI; your client reads them through its own resource tool): `fallow://tools`, `fallow://issue-types`, `fallow://explain/{issue_type}`, `fallow://task-matrix`, and the config, plugin, and rule-pack JSON Schemas. Every payload is JSON
|
|
125
|
+
The server also serves read-only reference resources (no subprocess, no analysis run, cacheable by URI; your client reads them through its own resource tool): `fallow://tools`, `fallow://issue-types`, `fallow://explain/{issue_type}`, `fallow://task-matrix`, and the config, plugin, and rule-pack JSON Schemas. Every resource payload is JSON. Each content item carries the server version in `_meta.fallow_version`.
|
|
104
126
|
|
|
105
127
|
Full tool catalogue, resource catalogue, key params, runtime source-map confidence tiers, shared timeouts, and the `next_steps` dispatch mapping: **[references/mcp.md](references/mcp.md)**.
|
|
106
128
|
|
|
@@ -113,6 +135,8 @@ Full tool catalogue, resource catalogue, key params, runtime source-map confiden
|
|
|
113
135
|
- [Similar Code](references/similar-code.md): snapshot-stable discovery, inspection, and verdict workflow
|
|
114
136
|
- [Node Bindings](references/node-bindings.md): embed the analysis engine in a Node.js process via NAPI
|
|
115
137
|
|
|
138
|
+
To set up or modernize the code-quality tooling of a repository (package install, config, agent wiring, CI gate), use the `fallow-setup` skill.
|
|
139
|
+
|
|
116
140
|
## Common Workflows
|
|
117
141
|
|
|
118
142
|
### Audit a project for cleanup opportunities
|
|
@@ -240,7 +264,7 @@ fallow health --format json --quiet --group-by owner --score --ownership --save-
|
|
|
240
264
|
fallow health --format json --quiet --group-by owner --score --workspace 'packages/*'
|
|
241
265
|
```
|
|
242
266
|
|
|
243
|
-
`--group-by owner` partitions every metric by CODEOWNERS team (last-match-wins, GitHub semantics) with a
|
|
267
|
+
`--group-by owner` partitions every metric by CODEOWNERS team (last-match-wins, GitHub semantics) with a native resolver, so there is no need to parse CODEOWNERS or aggregate per owner yourself. With `--score`, each `groups[]` entry carries a first-class `health_score` (`{ score, grade, penalties: { dead_files, complexity, p90_complexity, maintainability, unused_deps, circular_deps, unit_size, coupling, duplication } }`) alongside its own `vital_signs` and per-file `file_scores[]` (`complexity_density`, `maintainability_index`). Human output renders a `● Per-owner health` table (`score / grade / files / hot`). `--save-snapshot` records a point-in-time entry that `--trend` reads later. This one command replaces a hand-rolled CODEOWNERS-resolution + per-owner-aggregation + scoring script end to end.
|
|
244
268
|
|
|
245
269
|
Caveat for root-only path aliases: in monorepos where TypeScript path aliases (e.g. `@myorg/*`) are declared only in a root `tsconfig.base.json` that the per-package `tsconfig.json` files do not extend, imports through those aliases do not resolve, so dead-code signals (unused files/exports, and the `dead_files` penalty in the per-owner `health_score`) carry false positives. The complexity, maintainability, coupling, hotspot, and ownership signals are computed per file from the AST and git history and stay accurate regardless. Prefer `health` (not `dead-code`) for per-team quality tracking there.
|
|
246
270
|
|
|
@@ -262,7 +286,7 @@ fallow dead-code --format json --quiet --save-baseline .fallow/snapshot.json
|
|
|
262
286
|
fallow dead-code --format json --quiet --baseline .fallow/snapshot.json
|
|
263
287
|
```
|
|
264
288
|
|
|
265
|
-
`--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). `audit` rejects the global baseline flags and uses `--dead-code-baseline` / `--health-baseline` / `--dupes-baseline` instead.
|
|
266
290
|
|
|
267
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.
|
|
268
292
|
|
|
@@ -375,11 +399,11 @@ export const deprecatedHelper = () => {};
|
|
|
375
399
|
## Key Gotchas
|
|
376
400
|
|
|
377
401
|
- **`fix --yes` is required** in non-TTY (agent) environments. Without it, `fix` exits with code 2
|
|
378
|
-
- **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
|
|
379
|
-
- **
|
|
402
|
+
- **Zero config by default.** Built-in framework plugins auto-detect, including Wuchale config, Contentlayer content roots, Kibana `kibana.jsonc` plugin entries, tap and tsd test entry points. Read `fallow schema.plugins` for the current registry and don't create config unless customization is needed
|
|
403
|
+
- **Default analysis is syntactic.** Fully dynamic `import(variable)` is not resolved. Optional `--type-aware` analysis adds TypeScript checker evidence
|
|
380
404
|
- **Function overloads are deduplicated.** TypeScript function overload signatures are merged into a single export (not reported as separate unused exports)
|
|
381
|
-
- **Re-export chains are resolved.**
|
|
382
|
-
- **`--changed-since`
|
|
405
|
+
- **Re-export chains are resolved.** Fallow tracks exports through barrel files. Trace reported exports before removal when consumers may be outside static analysis
|
|
406
|
+
- **`--changed-since` scopes findings to changed files.** Existing findings in those files can remain, and dead-code dependency findings remain project-wide. Use `fallow audit --gate new-only` to distinguish introduced findings from inherited ones
|
|
383
407
|
|
|
384
408
|
For the full list with examples, see [references/gotchas.md](references/gotchas.md).
|
|
385
409
|
|