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 +1 -1
- package/capabilities.json +28 -14
- package/issue-registry.json +2 -2
- package/package.json +10 -10
- package/schema.json +4 -4
- package/scripts/lazy-verify.js +16 -1
- package/scripts/lazy-verify.test.js +79 -100
- package/scripts/sentinel-path.test.js +9 -2
- package/scripts/verify-binary.test.js +50 -4
- package/skills/fallow/SKILL.md +10 -14
- package/skills/fallow/references/cli-reference.md +5 -5
- package/skills/fallow/references/gotchas.md +1 -1
- package/skills/fallow/references/mcp.md +24 -1
- package/skills/fallow/references/patterns.md +2 -2
- package/types/output-contract.d.ts +139 -50
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.
|
|
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
|
|
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
|
|
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": "
|
|
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": "
|
|
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 warn
|
|
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": "
|
|
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",
|
package/issue-registry.json
CHANGED
|
@@ -1558,7 +1558,7 @@
|
|
|
1558
1558
|
"rule_id": "fallow/unused-dependency-override",
|
|
1559
1559
|
"command": "dead-code",
|
|
1560
1560
|
"category": "Dependencies",
|
|
1561
|
-
"description": "
|
|
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": "
|
|
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.
|
|
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.
|
|
91
|
-
"@fallow-cli/darwin-x64": "3.
|
|
92
|
-
"@fallow-cli/linux-x64-gnu": "3.
|
|
93
|
-
"@fallow-cli/linux-arm64-gnu": "3.
|
|
94
|
-
"@fallow-cli/linux-x64-musl": "3.
|
|
95
|
-
"@fallow-cli/linux-arm64-musl": "3.
|
|
96
|
-
"@fallow-cli/win32-arm64-msvc": "3.
|
|
97
|
-
"@fallow-cli/win32-x64-msvc": "3.
|
|
98
|
-
"fallow-type-aware": "3.
|
|
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
|
|
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
|
|
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
|
},
|
package/scripts/lazy-verify.js
CHANGED
|
@@ -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(
|
|
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
|
|
27
|
-
return
|
|
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 [
|
|
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 ||
|
|
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:
|
|
94
|
-
manifestPath
|
|
99
|
+
packageName: manifest.name,
|
|
100
|
+
manifestPath,
|
|
95
101
|
verifyFn,
|
|
96
102
|
env: {},
|
|
97
|
-
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,
|
|
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
|
-
[
|
|
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:
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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(
|
|
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,
|
|
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: "
|
|
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: "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
630
|
+
const ext = extForPlatformId();
|
|
585
631
|
writeManifest(dir, {
|
|
586
632
|
name: "@fallow-cli/x",
|
|
587
633
|
version: "1.0.0",
|
package/skills/fallow/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
46
|
-
2. **
|
|
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
|
|
164
|
-
| `misconfigured-dependency-override` | `--misconfigured-dependency-overrides` | - | - |
|
|
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
|
|
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
|
-
|
|
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
|
|
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.**
|
|
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
|
|
93
|
-
| `--misconfigured-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
|
|
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
|
|
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
|
|
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`.
|
|
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.
|
|
367
|
-
*
|
|
368
|
-
* them statically catches the issue
|
|
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
|
|
2287
|
-
* pnpm.overrides block, or
|
|
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
|
|
2290
|
-
* is warn because projects without a
|
|
2291
|
-
* manifest-only checks; the hint field
|
|
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
|
-
*
|
|
2297
|
-
* parse
|
|
2298
|
-
* selector, unbalanced parent matcher). The package manager
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
9918
|
-
*
|
|
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
|
|
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
|
|
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
|
|
10596
|
-
* pnpm.overrides block, or
|
|
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
|
|
10599
|
-
* is warn because projects without a
|
|
10600
|
-
* manifest-only checks; the hint field
|
|
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
|
-
*
|
|
10606
|
-
* parse
|
|
10607
|
-
* selector, unbalanced parent matcher). The package manager
|
|
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.
|