fallow 2.88.2 → 2.89.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/package.json +9 -9
- package/scripts/run-binary.js +42 -7
- package/scripts/run-binary.test.js +37 -0
- package/scripts/verify-binary.js +81 -6
- package/scripts/verify-binary.test.js +78 -0
- package/skills/fallow/SKILL.md +1 -1
- package/skills/fallow/references/cli-reference.md +18 -8
- package/types/output-contract.d.ts +95 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fallow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.89.0",
|
|
4
4
|
"description": "Deterministic codebase intelligence for TypeScript and JavaScript. Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 96 framework plugins.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -83,13 +83,13 @@
|
|
|
83
83
|
"@tanstack/intent": "0.0.41"
|
|
84
84
|
},
|
|
85
85
|
"optionalDependencies": {
|
|
86
|
-
"@fallow-cli/darwin-arm64": "2.
|
|
87
|
-
"@fallow-cli/darwin-x64": "2.
|
|
88
|
-
"@fallow-cli/linux-x64-gnu": "2.
|
|
89
|
-
"@fallow-cli/linux-arm64-gnu": "2.
|
|
90
|
-
"@fallow-cli/linux-x64-musl": "2.
|
|
91
|
-
"@fallow-cli/linux-arm64-musl": "2.
|
|
92
|
-
"@fallow-cli/win32-arm64-msvc": "2.
|
|
93
|
-
"@fallow-cli/win32-x64-msvc": "2.
|
|
86
|
+
"@fallow-cli/darwin-arm64": "2.89.0",
|
|
87
|
+
"@fallow-cli/darwin-x64": "2.89.0",
|
|
88
|
+
"@fallow-cli/linux-x64-gnu": "2.89.0",
|
|
89
|
+
"@fallow-cli/linux-arm64-gnu": "2.89.0",
|
|
90
|
+
"@fallow-cli/linux-x64-musl": "2.89.0",
|
|
91
|
+
"@fallow-cli/linux-arm64-musl": "2.89.0",
|
|
92
|
+
"@fallow-cli/win32-arm64-msvc": "2.89.0",
|
|
93
|
+
"@fallow-cli/win32-x64-msvc": "2.89.0"
|
|
94
94
|
}
|
|
95
95
|
}
|
package/scripts/run-binary.js
CHANGED
|
@@ -14,6 +14,7 @@ const fs = require("node:fs");
|
|
|
14
14
|
|
|
15
15
|
const { getPlatformPackage } = require("./platform-package");
|
|
16
16
|
const { ensureVerified } = require("./lazy-verify");
|
|
17
|
+
const { isPreSigningVersion } = require("./verify-binary");
|
|
17
18
|
|
|
18
19
|
function resolvePlatformPackageName() {
|
|
19
20
|
if (process.platform !== "linux") {
|
|
@@ -29,13 +30,34 @@ function resolvePlatformPackageName() {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function isVersionQuery(argv) {
|
|
32
|
-
//
|
|
33
|
+
// The root command answers all three version flags (--version, -V, and the
|
|
34
|
+
// TS/JS-toolchain-style -v), so the verified-status line must be appended for
|
|
35
|
+
// every one of them, not just --version / -V.
|
|
33
36
|
const tail = argv.slice(2);
|
|
34
37
|
if (tail.length === 0) return false;
|
|
35
|
-
return tail[0] === "--version" || tail[0] === "-V";
|
|
38
|
+
return tail[0] === "--version" || tail[0] === "-V" || tail[0] === "-v";
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
// Signing status of the resolved CLI version, appended to the `verified:` line.
|
|
42
|
+
// Most informative on the `skipped` path: a fleet running with
|
|
43
|
+
// FALLOW_SKIP_BINARY_VERIFY can see whether the pinned version is even signable
|
|
44
|
+
// (pre-signing versions predate the 2.77.0 epoch and have no signature to
|
|
45
|
+
// verify). Best-effort: an unknown/unreadable version yields no annotation.
|
|
46
|
+
function describeSigning(version) {
|
|
47
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
return isPreSigningVersion(version)
|
|
51
|
+
? `; fallow ${version} unsigned (predates 2.77.0)`
|
|
52
|
+
: `; fallow ${version} signed`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function describeVerified(result, version) {
|
|
56
|
+
const status = describeVerifiedStatus(result);
|
|
57
|
+
return `${status}${describeSigning(version)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function describeVerifiedStatus(result) {
|
|
39
61
|
if (result.skipped) {
|
|
40
62
|
return `verified: skipped (${result.reason})`;
|
|
41
63
|
}
|
|
@@ -79,9 +101,21 @@ function printVerifyError(verifyResult) {
|
|
|
79
101
|
);
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
function writeVerifiedLineIfVersionQuery(verifyResult) {
|
|
104
|
+
function writeVerifiedLineIfVersionQuery(verifyResult, version) {
|
|
83
105
|
if (isVersionQuery(process.argv)) {
|
|
84
|
-
process.stdout.write(`${describeVerified(verifyResult)}\n`);
|
|
106
|
+
process.stdout.write(`${describeVerified(verifyResult, version)}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Read the resolved CLI version from the platform package manifest (its version
|
|
111
|
+
// is released in lockstep with the CLI). Best-effort: never throws, so a
|
|
112
|
+
// missing/garbled manifest just omits the signing annotation on --version.
|
|
113
|
+
function readResolvedVersion(manifestPath) {
|
|
114
|
+
try {
|
|
115
|
+
const version = JSON.parse(fs.readFileSync(manifestPath, "utf8")).version;
|
|
116
|
+
return typeof version === "string" ? version : undefined;
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
85
119
|
}
|
|
86
120
|
}
|
|
87
121
|
|
|
@@ -105,6 +139,7 @@ function guardBrokenStdout() {
|
|
|
105
139
|
function runBinary(binaryBaseName) {
|
|
106
140
|
guardBrokenStdout();
|
|
107
141
|
const { pkg, manifestPath, platformPkgDir } = resolvePlatformPaths();
|
|
142
|
+
const resolvedVersion = readResolvedVersion(manifestPath);
|
|
108
143
|
|
|
109
144
|
const binaryName = process.platform === "win32" ? `${binaryBaseName}.exe` : binaryBaseName;
|
|
110
145
|
const binaryPath = path.join(platformPkgDir, binaryName);
|
|
@@ -126,11 +161,11 @@ function runBinary(binaryBaseName) {
|
|
|
126
161
|
if (e.status === undefined) throw e;
|
|
127
162
|
// Child has already written its --version line via inherited stdio;
|
|
128
163
|
// append the verified line here only on a clean exit.
|
|
129
|
-
if (e.status === 0) writeVerifiedLineIfVersionQuery(verifyResult);
|
|
164
|
+
if (e.status === 0) writeVerifiedLineIfVersionQuery(verifyResult, resolvedVersion);
|
|
130
165
|
process.exit(e.status);
|
|
131
166
|
}
|
|
132
167
|
|
|
133
|
-
writeVerifiedLineIfVersionQuery(verifyResult);
|
|
168
|
+
writeVerifiedLineIfVersionQuery(verifyResult, resolvedVersion);
|
|
134
169
|
}
|
|
135
170
|
|
|
136
171
|
module.exports = {
|
|
@@ -37,3 +37,40 @@ test("guardBrokenStdout rethrows non-EPIPE stdout errors (exit 1)", () => {
|
|
|
37
37
|
assert.match(res.stderr, /Error: write ENOSPC/, "the rethrown error reaches stderr");
|
|
38
38
|
assert.doesNotMatch(res.stderr, /is not a function/, "guard must be present, not absent");
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
test("isVersionQuery recognizes --version, -V, and -v as the first argument", () => {
|
|
42
|
+
const { isVersionQuery } = require(RUN_BINARY);
|
|
43
|
+
assert.equal(isVersionQuery(["node", "fallow", "--version"]), true);
|
|
44
|
+
assert.equal(isVersionQuery(["node", "fallow", "-V"]), true);
|
|
45
|
+
assert.equal(
|
|
46
|
+
isVersionQuery(["node", "fallow", "-v"]),
|
|
47
|
+
true,
|
|
48
|
+
"-v must append the verified line too",
|
|
49
|
+
);
|
|
50
|
+
assert.equal(isVersionQuery(["node", "fallow"]), false);
|
|
51
|
+
assert.equal(isVersionQuery(["node", "fallow", "dead-code"]), false);
|
|
52
|
+
assert.equal(
|
|
53
|
+
isVersionQuery(["node", "fallow", "dead-code", "-v"]),
|
|
54
|
+
false,
|
|
55
|
+
"-v only counts as the first arg",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("describeVerified annotates the resolved version's signing status", () => {
|
|
60
|
+
const { describeVerified } = require(RUN_BINARY);
|
|
61
|
+
const ok = { ok: true, sentinelPath: "/c/s" };
|
|
62
|
+
// Signed-era version: appended as `signed`.
|
|
63
|
+
assert.match(
|
|
64
|
+
describeVerified(ok, "2.83.0"),
|
|
65
|
+
/verified: yes \(sentinel \/c\/s\); fallow 2\.83\.0 signed/,
|
|
66
|
+
);
|
|
67
|
+
// Pre-signing version: the fleet pre-flight signal, most useful on skip.
|
|
68
|
+
const skipped = { skipped: true, reason: "FALLOW_SKIP_BINARY_VERIFY is set" };
|
|
69
|
+
assert.match(
|
|
70
|
+
describeVerified(skipped, "2.76.0"),
|
|
71
|
+
/verified: skipped \(.*\); fallow 2\.76\.0 unsigned \(predates 2\.77\.0\)/,
|
|
72
|
+
);
|
|
73
|
+
// Unknown / unreadable version: no annotation, version line stays intact.
|
|
74
|
+
assert.equal(describeVerified(ok, undefined), "verified: yes (sentinel /c/s)");
|
|
75
|
+
assert.equal(describeVerified(ok, ""), "verified: yes (sentinel /c/s)");
|
|
76
|
+
});
|
package/scripts/verify-binary.js
CHANGED
|
@@ -81,6 +81,10 @@ function _verifyWithKey(binaryPath, rawPubKey) {
|
|
|
81
81
|
signature = fs.readFileSync(sigPath);
|
|
82
82
|
} catch (err) {
|
|
83
83
|
if (err && err.code === "ENOENT") {
|
|
84
|
+
// Low-level result: the version-aware guidance is attached one layer up
|
|
85
|
+
// in verifyOneBinary{,Sync}, which knows the resolved platform-package
|
|
86
|
+
// version and can distinguish a pre-signing version from a >=2.77.0
|
|
87
|
+
// package whose signature is missing (a tampering signal). Refs #944.
|
|
84
88
|
return { ok: false, code: "sig-missing", message: `signature not found at ${sigPath}` };
|
|
85
89
|
}
|
|
86
90
|
return {
|
|
@@ -389,16 +393,69 @@ function resolvePlatformPackageForVerify(opts) {
|
|
|
389
393
|
};
|
|
390
394
|
}
|
|
391
395
|
|
|
396
|
+
// Signed platform binaries ship from fallow 2.77.0 onward (the `.sig` staging
|
|
397
|
+
// step, the `files` entries, and this verifier all landed in #488, first
|
|
398
|
+
// released in 2.77.0). A resolved version below this epoch has no signature and
|
|
399
|
+
// never will (npm is immutable), so its missing-sig failure is expected and the
|
|
400
|
+
// fix is to upgrade the pin. A version at or above the epoch whose signature is
|
|
401
|
+
// absent is a different, alarming case (tampered or incomplete package).
|
|
402
|
+
const SIGNING_EPOCH = [2, 77, 0];
|
|
403
|
+
|
|
404
|
+
// True when `version` is a parseable semver strictly below the signing epoch.
|
|
405
|
+
// An unparsable / unknown version returns false so the caller uses the
|
|
406
|
+
// cautious (possible-tampering) message rather than telling the user to bump.
|
|
407
|
+
function isPreSigningVersion(version) {
|
|
408
|
+
if (typeof version !== "string") {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
412
|
+
if (!match) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
416
|
+
for (let i = 0; i < SIGNING_EPOCH.length; i += 1) {
|
|
417
|
+
if (parts[i] < SIGNING_EPOCH[i]) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (parts[i] > SIGNING_EPOCH[i]) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Attach version-aware remediation to a `sig-missing` low-level result. Other
|
|
428
|
+
// failure codes pass through untouched. The split distinguishes a pre-signing
|
|
429
|
+
// version (no signature exists and never will, so the fix is to upgrade the
|
|
430
|
+
// pin) from a >=2.77.0 package whose signature is unexpectedly absent (a
|
|
431
|
+
// tampering signal). Each message gives the CONSTRUCTIVE fix only; the bypass
|
|
432
|
+
// escape hatch (FALLOW_SKIP_BINARY_VERIFY) is owned by the caller's trailer and
|
|
433
|
+
// SECURITY.md, deliberately not surfaced here so a tampering victim is never
|
|
434
|
+
// nudged to bypass and the env is not normalized in CI logs.
|
|
435
|
+
function describeSigMissing(result, version) {
|
|
436
|
+
if (result.code !== "sig-missing") {
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
const message = isPreSigningVersion(version)
|
|
440
|
+
? `${result.message}. fallow ${version} predates signed binaries (signatures ship in 2.77.0 ` +
|
|
441
|
+
`and later), so this package has no signature to verify. Bump the \`fallow\` dependency in ` +
|
|
442
|
+
`your project's package.json to >=2.77.0 (for example \`npm install fallow@latest\`).`
|
|
443
|
+
: `${result.message}. fallow ${version} should be signed but its signature is missing; the ` +
|
|
444
|
+
`platform package may be tampered with or incomplete. Reinstall with \`npm install fallow@latest\` ` +
|
|
445
|
+
`and report it if it persists on a clean install.`;
|
|
446
|
+
return { ...result, message };
|
|
447
|
+
}
|
|
448
|
+
|
|
392
449
|
// Verify one binary against its sig + expected SHA-256. Used by both the
|
|
393
450
|
// sync and async verify-installed entry points; the digest provider may be
|
|
394
451
|
// sync (returns string) or async (returns Promise<string>), and the loop body
|
|
395
452
|
// awaits the value regardless. Keeps the outer functions a flat for-loop so
|
|
396
453
|
// cyclomatic + cognitive complexity stays low.
|
|
397
|
-
async function verifyOneBinary(target, dir, pkg, manifestPath, verifyFn, digestProvider) {
|
|
454
|
+
async function verifyOneBinary(target, dir, pkg, manifestPath, verifyFn, digestProvider, version) {
|
|
398
455
|
const binaryPath = path.join(dir, target.binary);
|
|
399
456
|
const sigResult = verifyFn(binaryPath);
|
|
400
457
|
if (!sigResult.ok) {
|
|
401
|
-
return { ...sigResult, binary: binaryPath, package: pkg };
|
|
458
|
+
return { ...describeSigMissing(sigResult, version), binary: binaryPath, package: pkg };
|
|
402
459
|
}
|
|
403
460
|
// Prefer the digest embedded in the platform package's package.json
|
|
404
461
|
// (written at release time by `npm-prep`). Falling back to the GitHub
|
|
@@ -478,7 +535,15 @@ async function verifyInstalled(options) {
|
|
|
478
535
|
const boundProvider = async (args) => digestProvider({ ...args, version });
|
|
479
536
|
|
|
480
537
|
for (const target of binaryTargetsForPlatform(platformId)) {
|
|
481
|
-
const result = await verifyOneBinary(
|
|
538
|
+
const result = await verifyOneBinary(
|
|
539
|
+
target,
|
|
540
|
+
dir,
|
|
541
|
+
pkg,
|
|
542
|
+
manifestPath,
|
|
543
|
+
verify,
|
|
544
|
+
boundProvider,
|
|
545
|
+
version,
|
|
546
|
+
);
|
|
482
547
|
if (!result.ok) return result;
|
|
483
548
|
}
|
|
484
549
|
return { ok: true, package: pkg, version };
|
|
@@ -518,7 +583,15 @@ function verifyInstalledSync(options) {
|
|
|
518
583
|
const { dir, manifestPath, pkg, version, platformId } = resolved;
|
|
519
584
|
|
|
520
585
|
for (const target of binaryTargetsForPlatform(platformId)) {
|
|
521
|
-
const result = verifyOneBinarySync(
|
|
586
|
+
const result = verifyOneBinarySync(
|
|
587
|
+
target,
|
|
588
|
+
dir,
|
|
589
|
+
pkg,
|
|
590
|
+
manifestPath,
|
|
591
|
+
verify,
|
|
592
|
+
digestProvider,
|
|
593
|
+
version,
|
|
594
|
+
);
|
|
522
595
|
if (!result.ok) return result;
|
|
523
596
|
}
|
|
524
597
|
return { ok: true, package: pkg, version };
|
|
@@ -529,11 +602,11 @@ function verifyInstalledSync(options) {
|
|
|
529
602
|
// actionable error pointing the user at `npm install fallow@latest` (since
|
|
530
603
|
// there is no network fallback in lazy mode), and the digestProvider is
|
|
531
604
|
// optional (tests inject one; production callers rely on the embedded digest).
|
|
532
|
-
function verifyOneBinarySync(target, dir, pkg, manifestPath, verifyFn, digestProvider) {
|
|
605
|
+
function verifyOneBinarySync(target, dir, pkg, manifestPath, verifyFn, digestProvider, version) {
|
|
533
606
|
const binaryPath = path.join(dir, target.binary);
|
|
534
607
|
const sigResult = verifyFn(binaryPath);
|
|
535
608
|
if (!sigResult.ok) {
|
|
536
|
-
return { ...sigResult, binary: binaryPath, package: pkg };
|
|
609
|
+
return { ...describeSigMissing(sigResult, version), binary: binaryPath, package: pkg };
|
|
537
610
|
}
|
|
538
611
|
let expectedDigest = manifestPath ? readEmbeddedDigest(manifestPath, target.binary) : null;
|
|
539
612
|
if (!expectedDigest && digestProvider) {
|
|
@@ -575,6 +648,8 @@ module.exports = {
|
|
|
575
648
|
verifyInstalled,
|
|
576
649
|
verifyInstalledSync,
|
|
577
650
|
_verifyWithKey,
|
|
651
|
+
isPreSigningVersion,
|
|
652
|
+
describeSigMissing,
|
|
578
653
|
normalizeDigest,
|
|
579
654
|
readEmbeddedDigest,
|
|
580
655
|
sha256Hex,
|
|
@@ -7,6 +7,8 @@ const path = require("node:path");
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
_verifyWithKey,
|
|
10
|
+
isPreSigningVersion,
|
|
11
|
+
describeSigMissing,
|
|
10
12
|
verifyBinaryAt,
|
|
11
13
|
verifyDigestAt,
|
|
12
14
|
verifyInstalled,
|
|
@@ -133,6 +135,56 @@ test("_verifyWithKey returns sig-missing when the signature file does not exist"
|
|
|
133
135
|
}
|
|
134
136
|
});
|
|
135
137
|
|
|
138
|
+
// The version-aware remediation lives in describeSigMissing (attached by
|
|
139
|
+
// verifyOneBinary{,Sync}), not in the low-level _verifyWithKey result. Refs #944.
|
|
140
|
+
test("isPreSigningVersion is true below 2.77.0 and false at/above it", () => {
|
|
141
|
+
assert.equal(isPreSigningVersion("2.76.0"), true);
|
|
142
|
+
assert.equal(isPreSigningVersion("2.73.0"), true);
|
|
143
|
+
assert.equal(isPreSigningVersion("1.9.0"), true);
|
|
144
|
+
assert.equal(isPreSigningVersion("2.77.0"), false);
|
|
145
|
+
assert.equal(isPreSigningVersion("2.83.0"), false);
|
|
146
|
+
assert.equal(isPreSigningVersion("2.88.2"), false);
|
|
147
|
+
// Unknown / unparsable versions default to false so the caller uses the
|
|
148
|
+
// cautious possible-tampering message rather than telling the user to bump.
|
|
149
|
+
assert.equal(isPreSigningVersion("unknown"), false);
|
|
150
|
+
assert.equal(isPreSigningVersion(undefined), false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("describeSigMissing gives upgrade guidance for a pre-signing version", () => {
|
|
154
|
+
const base = { ok: false, code: "sig-missing", message: "signature not found at /x/fallow.sig" };
|
|
155
|
+
const result = describeSigMissing(base, "2.76.0");
|
|
156
|
+
// The security-critical invariant: enriching the message must never flip the
|
|
157
|
+
// verdict. A sig-missing result stays a hard failure.
|
|
158
|
+
assert.equal(result.ok, false);
|
|
159
|
+
assert.equal(result.code, "sig-missing");
|
|
160
|
+
assert.match(result.message, /predates signed binaries/);
|
|
161
|
+
assert.match(result.message, /2\.77\.0/);
|
|
162
|
+
// Names WHERE the pin lives so the user can act.
|
|
163
|
+
assert.match(result.message, /package\.json/);
|
|
164
|
+
// The bypass escape hatch is owned by the caller's trailer + SECURITY.md, not
|
|
165
|
+
// surfaced inline, so it is not normalized in CI logs.
|
|
166
|
+
assert.doesNotMatch(result.message, new RegExp(SKIP_ENV));
|
|
167
|
+
// The original low-level detail is preserved.
|
|
168
|
+
assert.match(result.message, /signature not found/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("describeSigMissing flags possible tampering for a signed-era version", () => {
|
|
172
|
+
const base = { ok: false, code: "sig-missing", message: "signature not found at /x/fallow.sig" };
|
|
173
|
+
const result = describeSigMissing(base, "2.83.0");
|
|
174
|
+
assert.equal(result.ok, false);
|
|
175
|
+
assert.equal(result.code, "sig-missing");
|
|
176
|
+
assert.match(result.message, /tampered with or incomplete/);
|
|
177
|
+
assert.match(result.message, /Reinstall/);
|
|
178
|
+
// Must NOT nudge a possible-tampering victim toward bypassing verification.
|
|
179
|
+
assert.doesNotMatch(result.message, new RegExp(SKIP_ENV));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("describeSigMissing passes non-sig-missing results through untouched", () => {
|
|
183
|
+
const base = { ok: false, code: "digest-mismatch", message: "digest mismatch" };
|
|
184
|
+
const result = describeSigMissing(base, "2.76.0");
|
|
185
|
+
assert.equal(result.message, "digest mismatch");
|
|
186
|
+
});
|
|
187
|
+
|
|
136
188
|
test("_verifyWithKey returns binary-missing when the binary does not exist", () => {
|
|
137
189
|
const { rawPub } = makeKeypair();
|
|
138
190
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
|
|
@@ -343,6 +395,32 @@ test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent
|
|
|
343
395
|
assert.match(result.binary, /fallow-mcp/);
|
|
344
396
|
});
|
|
345
397
|
|
|
398
|
+
test("verifyInstalled threads the resolved version into the sig-missing message", async (t) => {
|
|
399
|
+
const { privateKey, rawPub } = makeKeypair();
|
|
400
|
+
const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
|
|
401
|
+
const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow-mcp" });
|
|
402
|
+
t.after(() => {
|
|
403
|
+
cleanup(preDir);
|
|
404
|
+
cleanup(eraDir);
|
|
405
|
+
});
|
|
406
|
+
const pre = await verifyInstalled({
|
|
407
|
+
dirOverride: preDir,
|
|
408
|
+
version: "2.76.0",
|
|
409
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
410
|
+
digestProvider: makeDigestProvider(preDir),
|
|
411
|
+
});
|
|
412
|
+
assert.equal(pre.code, "sig-missing");
|
|
413
|
+
assert.match(pre.message, /predates signed binaries/);
|
|
414
|
+
const era = await verifyInstalled({
|
|
415
|
+
dirOverride: eraDir,
|
|
416
|
+
version: "2.83.0",
|
|
417
|
+
verifyFn: (p) => _verifyWithKey(p, rawPub),
|
|
418
|
+
digestProvider: makeDigestProvider(eraDir),
|
|
419
|
+
});
|
|
420
|
+
assert.equal(era.code, "sig-missing");
|
|
421
|
+
assert.match(era.message, /tampered with or incomplete/);
|
|
422
|
+
});
|
|
423
|
+
|
|
346
424
|
test("verifyInstalled reports digest-mismatch when SHA-256 disagrees with the provider", async (t) => {
|
|
347
425
|
const { privateKey, rawPub } = makeKeypair();
|
|
348
426
|
const dir = makePlatformDir(privateKey);
|
package/skills/fallow/SKILL.md
CHANGED
|
@@ -122,7 +122,7 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
|
|
|
122
122
|
| `find_dupes` | Code duplication detection. Set `changed_since` to scope to changed files since a git ref. Set `min_occurrences` (≥ 2, default 2) to hide pair-only clones and focus on widespread copy-paste; JSON gains `stats.clone_groups_below_min_occurrences` when the filter hides anything. Each `clone_groups[]` entry carries a stable `fingerprint`, usually `dup:<8hex>` and widened only on rare report collisions; pass it to `trace_clone` to deep-dive that group |
|
|
123
123
|
| `fix_preview` | Dry-run auto-fix preview |
|
|
124
124
|
| `fix_apply` | Apply auto-fixes (destructive) |
|
|
125
|
-
| `check_health` | Complexity metrics, health scores, hotspots, and refactoring targets. Optional `runtime_coverage` merges a V8 or Istanbul dump; tune it with `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), and `low_traffic_threshold` (default 0.001). When runtime evidence combines with static usage, test coverage, CRAP/complexity, ownership, or change scope, read `coverage_intelligence` for stable `fallow:coverage-intel:<hash>` recommendations. Set `group_by` to `owner`, `directory`, `package`, or `section` for per-group `vital_signs` / `health_score`; SARIF results gain `properties.group`, CodeClimate issues gain a top-level `group` field |
|
|
125
|
+
| `check_health` | Complexity metrics, health scores, hotspots, and refactoring targets. Set `complexity_breakdown: true` to add a per-decision-point `contributions[]` array to each complexity finding (each `else-if`, nested `if`, boolean operator, loop, `case`, etc. with its source line and cyclomatic/cognitive weight) so you can explain WHY a function scored high and pinpoint refactor targets. Optional `runtime_coverage` merges a V8 or Istanbul dump; tune it with `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), and `low_traffic_threshold` (default 0.001). When runtime evidence combines with static usage, test coverage, CRAP/complexity, ownership, or change scope, read `coverage_intelligence` for stable `fallow:coverage-intel:<hash>` recommendations. Set `group_by` to `owner`, `directory`, `package`, or `section` for per-group `vital_signs` / `health_score`; SARIF results gain `properties.group`, CodeClimate issues gain a top-level `group` field |
|
|
126
126
|
| `check_runtime_coverage` | Merge V8 or Istanbul runtime-coverage data into the health report. One local capture is free; continuous/cloud or multi-capture runtime monitoring is paid. Required `coverage` param (V8 dir, V8 JSON, or Istanbul `coverage-final.json`). Tuning knobs: `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), `low_traffic_threshold` (default 0.001), `max_crap` (default 30.0), `top`, `group_by`. Cloud runtime rows can expose `resolutionStatus` / `mappingQuality` on function-list JSON and `resolution_status` / `mapping_quality` in runtime-context JSON. Use `coverage_intelligence` and the confidence table below before acting on file-level runtime signals. Long dumps may exceed the 120s MCP timeout; raise `FALLOW_TIMEOUT_SECS`. Pick this over `check_health` when you have a coverage dump. |
|
|
127
127
|
| `get_hot_paths` | Runtime-context slice over the same runtime coverage pipeline. Same params as `check_runtime_coverage`; read `runtime_coverage.hot_paths` for production hot paths. |
|
|
128
128
|
| `get_blast_radius` | Runtime-context slice for blast-radius review. Same params as `check_runtime_coverage`; read `runtime_coverage.blast_radius` for stable `fallow:blast:<hash>` IDs, caller counts, traffic-weighted caller reach, optional cloud deploy touch counts, and low/medium/high risk bands. |
|
|
@@ -51,7 +51,14 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
|
|
|
51
51
|
| `--workspace` | string | — | Scope to one or more workspaces. Comma-separated values, globs (`apps/*`, `@scope/*`), and `!`-prefixed negation (`!apps/legacy`) supported. Matched against package name AND workspace path relative to repo root. |
|
|
52
52
|
| `--changed-workspaces` | string (git ref) | — | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF` (e.g. `origin/main`). Auto-derives the workspace set from `git diff`. Mutually exclusive with `--workspace`. Missing ref is a hard error (exit 2), not silent full-scope fallback. |
|
|
53
53
|
| `--include-dupes` | bool | `false` | Cross-reference with duplication findings |
|
|
54
|
+
| `--dupes-mode` | enum | `config` | Override duplicate detection mode in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --mode`. |
|
|
55
|
+
| `--dupes-threshold` | number | `config` | Override the duplication percentage failure threshold in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --threshold`. |
|
|
56
|
+
| `--dupes-min-tokens` | number | `config` | Override the minimum token count for clone detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --min-tokens`. |
|
|
57
|
+
| `--dupes-min-lines` | number | `config` | Override the minimum line count for clone detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --min-lines`. |
|
|
54
58
|
| `--dupes-min-occurrences` | number | `config` | Override the minimum clone occurrences in combined mode (must be >= 2). Falls back to the config value when unset. Mirrors the standalone `dupes --min-occurrences`. |
|
|
59
|
+
| `--dupes-skip-local` | bool | `config` | Only report cross-directory duplicates in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --skip-local`. |
|
|
60
|
+
| `--dupes-cross-language` | bool | `config` | Enable TypeScript to JavaScript duplicate matching in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --cross-language`. |
|
|
61
|
+
| `--dupes-ignore-imports` | bool | `config` | Exclude import declarations from duplicate detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --ignore-imports`. |
|
|
55
62
|
| `--file` | path (multiple) | — | Scope output to specific files. Only issues in the specified files are reported. Project-wide dependency issues are suppressed. Warns on non-existent paths. Useful for lint-staged |
|
|
56
63
|
| `--include-entry-exports` | bool | `false` | Report unused exports in entry files (package.json `main`/`exports`, framework pages). Catches typos like `meatdata` vs `metadata`. Global flag, also accepted on combined mode (`fallow --include-entry-exports`) and `fallow audit`. Also configurable as `includeEntryExports: true` in fallow config |
|
|
57
64
|
| `--trace` | `FILE:EXPORT` | — | Trace export usage chain |
|
|
@@ -368,6 +375,7 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
|
|
|
368
375
|
| `--top` | number | — | Only show the top N most complex functions (and file scores/hotspots/targets) |
|
|
369
376
|
| `--sort` | `cyclomatic\|cognitive\|lines\|severity` | `cyclomatic` | Sort order for complexity findings |
|
|
370
377
|
| `--complexity` | bool | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. |
|
|
378
|
+
| `--complexity-breakdown` | bool | `false` | Add a per-decision-point `contributions[]` array to each complexity finding in `--format json`. Each entry names the construct (`if`, `else-if`, `ternary`, boolean operator, loop, `case`, `catch`, `optional-chain`, ...) and carries its source line, the metric it adds to (`cyclomatic` or `cognitive`), its weight, and the nesting depth, so a consumer can explain WHY a function scored high. Off by default (no change to existing JSON/SARIF/markdown). Used by the VS Code inline editor breakdown and the MCP `check_health` `complexity_breakdown` param. |
|
|
371
379
|
| `--file-scores` | bool | `false` | Show only per-file health scores (maintainability index, LOC, fan-in, fan-out, dead code ratio, complexity density, CRAP risk). Runs the full analysis pipeline. Sorted by risk-aware triage concern: lower maintainability index and higher CRAP risk first. When no section flags are set, all sections are shown by default. |
|
|
372
380
|
| `--hotspots` | bool | `false` | Show only hotspots: files that are both complex and frequently changing. Combines git churn history with complexity data. Requires a git repository. When no section flags are set, all sections are shown by default. |
|
|
373
381
|
| `--targets` | bool | `false` | Show only refactoring targets: ranked recommendations based on complexity, coupling, churn, and dead code signals. Categories: churn+complexity, circular dep, high impact, dead code, complexity, coupling. When no section flags are set, all sections are shown by default. |
|
|
@@ -376,8 +384,9 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
|
|
|
376
384
|
| `--min-score` | number | — | Fail (exit 1) only when the health score is below this threshold. Implies `--score`. Authoritative CI quality gate: when set, complexity findings are demoted to informational and the exit code is driven solely by the score, so `--min-score 0` always exits 0. Composes with `--min-severity`. |
|
|
377
385
|
| `--min-severity` | `moderate\|high\|critical` | — | Only exit with an error for findings at or above this severity. Composes with `--min-score` (the run fails if either gate trips). |
|
|
378
386
|
| `--report-only` | bool | `false` | Print the score and findings but never fail CI (always exit 0). Advisory mode. Mutually exclusive with `--min-score` and `--min-severity`. |
|
|
379
|
-
| `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). |
|
|
387
|
+
| `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). Ignored when `--churn-file` is set. |
|
|
380
388
|
| `--min-commits` | number | `3` | Minimum number of commits for a file to be included in hotspot ranking. |
|
|
389
|
+
| `--churn-file` | string | (none) | Import change history from a `fallow-churn/v1` JSON file (`{schema, events:[{path, timestamp, author, added, deleted}]}`, one entry per changed file per commit) instead of `git log`, so `--hotspots`/`--ownership`/`--targets` work on non-git VCS (Yandex Arc, Mercurial, Perforce). Resolved relative to `--root`; wins over git. Authoritative for the window, so `--since` then only labels output. Malformed file exits 2; `audit`/`impact`/`--changed-since` still require git. |
|
|
381
390
|
| `--ownership` | bool | `false` | Attach ownership signals to hotspot entries: bus factor (Avelino truck factor), contributor count, top contributor with stale-days, recent contributors (top-3), `suggested_reviewers`, declared CODEOWNERS owner, `ownership_state`, ownership drift, unowned-hotspot detection. Human output gains a project-level summary line. JSON adds `low-bus-factor`, `unowned-hotspot`, `ownership-drift` action types. Test files get a `[test]` tag. Implies `--hotspots`. Requires git. |
|
|
382
391
|
| `--ownership-emails` | `raw\|handle\|anonymized\|hash` | `handle` | Privacy mode for author emails. `handle` shows the local-part only (default, with GitHub noreply unwrap and deterministic same-handle disambiguation). `anonymized` emits stable `xxh3:` pseudonyms; `hash` remains accepted as the legacy spelling. `raw` shows full addresses. Use `anonymized` in regulated environments. Implies `--ownership`. Configure default via `health.ownership.emailMode`. |
|
|
383
392
|
| `--changed-since` | string | — | Only analyze files changed since a git ref |
|
|
@@ -486,7 +495,7 @@ fallow health --format json --quiet --trend
|
|
|
486
495
|
{
|
|
487
496
|
"kind": "health",
|
|
488
497
|
"schema_version": 7,
|
|
489
|
-
"version": "2.
|
|
498
|
+
"version": "2.89.0",
|
|
490
499
|
"elapsed_ms": 32,
|
|
491
500
|
"summary": {
|
|
492
501
|
"files_analyzed": 482,
|
|
@@ -876,7 +885,7 @@ fallow audit \
|
|
|
876
885
|
{
|
|
877
886
|
"kind": "audit",
|
|
878
887
|
"schema_version": 7,
|
|
879
|
-
"version": "2.
|
|
888
|
+
"version": "2.89.0",
|
|
880
889
|
"command": "audit",
|
|
881
890
|
"verdict": "fail",
|
|
882
891
|
"changed_files_count": 12,
|
|
@@ -949,7 +958,7 @@ fallow flags --format json --quiet --workspace my-package
|
|
|
949
958
|
```json
|
|
950
959
|
{
|
|
951
960
|
"schema_version": 7,
|
|
952
|
-
"version": "2.
|
|
961
|
+
"version": "2.89.0",
|
|
953
962
|
"elapsed_ms": 116,
|
|
954
963
|
"feature_flags": [],
|
|
955
964
|
"total_flags": 0
|
|
@@ -1389,6 +1398,7 @@ Available on all commands:
|
|
|
1389
1398
|
| `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
|
|
1390
1399
|
| `FALLOW_CACHE_MAX_SIZE` | Maximum on-disk extraction cache (`.fallow/cache.bin`) size in megabytes (default: `256`). Triggers LRU eviction when crossed. Wins over `cache.maxSizeMb` config field. Intended for CI runners with disk quotas. `--no-cache` short-circuits this knob. |
|
|
1391
1400
|
| `FALLOW_AUDIT_CACHE_MAX_AGE_DAYS` | Max age (in days since last reuse or fresh create) of a persistent reusable `fallow audit` base-snapshot worktree cache. Older entries are reclaimed at the top of the next `fallow audit` invocation (default: `30`). Wins over `audit.cacheMaxAgeDays` config field. `0` disables the GC; invalid values silently fall back to config / default. |
|
|
1401
|
+
| `FALLOW_UPDATE_CHECK` | Set to `off`, `0`, `false`, `disabled`, or `no` to disable the human-TTY upgrade nudge and its background latest-version check. `DO_NOT_TRACK`, `FALLOW_TELEMETRY_DISABLED`, and CI also suppress it. |
|
|
1392
1402
|
| `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
|
|
1393
1403
|
| `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
|
|
1394
1404
|
| `FALLOW_CHANGED_SINCE` | GitLab CI: git ref for incremental analysis. Auto-detected in MR pipelines. |
|
|
@@ -1488,7 +1498,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
|
|
|
1488
1498
|
{
|
|
1489
1499
|
"kind": "dead-code",
|
|
1490
1500
|
"schema_version": 7,
|
|
1491
|
-
"version": "2.
|
|
1501
|
+
"version": "2.89.0",
|
|
1492
1502
|
"elapsed_ms": 45,
|
|
1493
1503
|
"total_issues": 12,
|
|
1494
1504
|
"entry_points": {
|
|
@@ -1648,7 +1658,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
|
|
|
1648
1658
|
{
|
|
1649
1659
|
"kind": "dupes",
|
|
1650
1660
|
"schema_version": 7,
|
|
1651
|
-
"version": "2.
|
|
1661
|
+
"version": "2.89.0",
|
|
1652
1662
|
"elapsed_ms": 82,
|
|
1653
1663
|
"total_clones": 15,
|
|
1654
1664
|
"total_lines_duplicated": 230,
|
|
@@ -1692,11 +1702,11 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
|
|
|
1692
1702
|
{
|
|
1693
1703
|
"kind": "combined",
|
|
1694
1704
|
"schema_version": 7,
|
|
1695
|
-
"version": "2.
|
|
1705
|
+
"version": "2.89.0",
|
|
1696
1706
|
"elapsed_ms": 159,
|
|
1697
1707
|
"check": {
|
|
1698
1708
|
"schema_version": 7,
|
|
1699
|
-
"version": "2.
|
|
1709
|
+
"version": "2.89.0",
|
|
1700
1710
|
"elapsed_ms": 45,
|
|
1701
1711
|
"total_issues": 12,
|
|
1702
1712
|
"unused_files": [],
|
|
@@ -353,6 +353,18 @@ export type CoverageTier = ("none" | "partial" | "high")
|
|
|
353
353
|
* Provenance of a CRAP finding's coverage signal.
|
|
354
354
|
*/
|
|
355
355
|
export type CoverageSource = ("istanbul" | "estimated" | "estimated_component_inherited")
|
|
356
|
+
/**
|
|
357
|
+
* Which complexity metric a [`ComplexityContribution`] adds to.
|
|
358
|
+
*/
|
|
359
|
+
export type ComplexityMetric = ("cyclomatic" | "cognitive")
|
|
360
|
+
/**
|
|
361
|
+
* The syntactic construct that produced a single complexity increment.
|
|
362
|
+
*
|
|
363
|
+
* Mirrors `SonarSource` cognitive-complexity vocabulary where it overlaps.
|
|
364
|
+
* `Case` means a `case` label carrying a test; a bare `default` adds nothing
|
|
365
|
+
* to cyclomatic complexity and so produces no contribution.
|
|
366
|
+
*/
|
|
367
|
+
export type ComplexityContributionKind = ("if" | "else" | "else-if" | "ternary" | "logical-and" | "logical-or" | "nullish-coalescing" | "logical-assignment" | "optional-chain" | "for" | "for-in" | "for-of" | "while" | "do-while" | "switch" | "case" | "catch" | "labeled-break" | "labeled-continue")
|
|
356
368
|
/**
|
|
357
369
|
* Discriminant for [`HealthFindingAction::kind`]. Mirrors the action types
|
|
358
370
|
* emitted by `build_health_finding_actions`. A single finding's `actions`
|
|
@@ -1654,6 +1666,15 @@ line: number
|
|
|
1654
1666
|
* 0-based byte column offset of the import that starts the cycle.
|
|
1655
1667
|
*/
|
|
1656
1668
|
col: number
|
|
1669
|
+
/**
|
|
1670
|
+
* Per-file import anchors, one entry per hop in cycle order: `edges[i]`
|
|
1671
|
+
* is the import in `files[i]` pointing to `files[(i + 1) % len]`. Always
|
|
1672
|
+
* the same length as `files`. Drives the per-file LSP diagnostic
|
|
1673
|
+
* squiggly. `#[serde(default)]` so pre-`edges` baselines deserialize;
|
|
1674
|
+
* always emitted on output but intentionally not in the schema's
|
|
1675
|
+
* `required` set (see the struct doc).
|
|
1676
|
+
*/
|
|
1677
|
+
edges?: CircularDependencyEdge[]
|
|
1657
1678
|
/**
|
|
1658
1679
|
* Whether this cycle crosses workspace package boundaries.
|
|
1659
1680
|
*/
|
|
@@ -1669,6 +1690,34 @@ actions: IssueAction[]
|
|
|
1669
1690
|
*/
|
|
1670
1691
|
introduced?: (AuditIntroduced | null)
|
|
1671
1692
|
}
|
|
1693
|
+
/**
|
|
1694
|
+
* One import hop in a circular dependency: the file containing the import
|
|
1695
|
+
* and where that import statement sits.
|
|
1696
|
+
*
|
|
1697
|
+
* `edges[i]` is the import IN `path` (the hop SOURCE, equal to the cycle's
|
|
1698
|
+
* `files[i]`) that points to the NEXT file in the cycle
|
|
1699
|
+
* (`files[(i + 1) % files.len()]`); the target is not repeated here to keep
|
|
1700
|
+
* the wire compact. Enables a per-file diagnostic squiggly anchored under
|
|
1701
|
+
* the offending import rather than a single squiggly on the first file.
|
|
1702
|
+
*
|
|
1703
|
+
* `col` is a 0-based BYTE column, matching the cycle's top-level `col`;
|
|
1704
|
+
* converting it to a UTF-16 code-unit column for LSP clients is a tracked
|
|
1705
|
+
* follow-up shared with the existing field.
|
|
1706
|
+
*/
|
|
1707
|
+
export interface CircularDependencyEdge {
|
|
1708
|
+
/**
|
|
1709
|
+
* The file containing the import (the hop SOURCE; equal to `files[i]`).
|
|
1710
|
+
*/
|
|
1711
|
+
path: string
|
|
1712
|
+
/**
|
|
1713
|
+
* 1-based line number of the import statement pointing to the next file.
|
|
1714
|
+
*/
|
|
1715
|
+
line: number
|
|
1716
|
+
/**
|
|
1717
|
+
* 0-based byte column offset of the import statement.
|
|
1718
|
+
*/
|
|
1719
|
+
col: number
|
|
1720
|
+
}
|
|
1672
1721
|
/**
|
|
1673
1722
|
* Wire-shape envelope for a [`ReExportCycle`] finding. Mirrors
|
|
1674
1723
|
* [`CircularDependencyFinding`]: flattens the bare finding and carries a
|
|
@@ -2200,6 +2249,15 @@ line_count: number
|
|
|
2200
2249
|
* each group in the human listing.
|
|
2201
2250
|
*/
|
|
2202
2251
|
fingerprint: string
|
|
2252
|
+
/**
|
|
2253
|
+
* Best-effort human-readable name for the clone: the dominant repeated
|
|
2254
|
+
* identifier across the duplicated fragment (e.g. a shared `parseCsv`
|
|
2255
|
+
* function). `None` when the clone has no clear dominant name (generic or
|
|
2256
|
+
* tied identifiers); consumers then fall back to a file-based label. Lets
|
|
2257
|
+
* editors and agents label a clone by what it is rather than an opaque
|
|
2258
|
+
* ordinal.
|
|
2259
|
+
*/
|
|
2260
|
+
suggested_name?: (string | null)
|
|
2203
2261
|
/**
|
|
2204
2262
|
* Suggested next steps: an `extract-shared` primary and a
|
|
2205
2263
|
* `suppress-line` secondary. Always emitted (possibly empty for
|
|
@@ -2534,6 +2592,13 @@ coverage_tier?: (CoverageTier | null)
|
|
|
2534
2592
|
coverage_source?: (CoverageSource | null)
|
|
2535
2593
|
inherited_from?: (string | null)
|
|
2536
2594
|
component_rollup?: (ComponentRollup | null)
|
|
2595
|
+
/**
|
|
2596
|
+
* Per-decision-point complexity breakdown explaining WHICH constructs drove
|
|
2597
|
+
* the cyclomatic and cognitive scores. Populated only when the caller opts
|
|
2598
|
+
* in via `health --complexity-breakdown`; empty (and omitted from JSON)
|
|
2599
|
+
* otherwise so default and CI output stay lean.
|
|
2600
|
+
*/
|
|
2601
|
+
contributions?: ComplexityContribution[]
|
|
2537
2602
|
/**
|
|
2538
2603
|
* Machine-actionable fix and suppress hints.
|
|
2539
2604
|
*/
|
|
@@ -2553,6 +2618,36 @@ template_path: string
|
|
|
2553
2618
|
template_cyclomatic: number
|
|
2554
2619
|
template_cognitive: number
|
|
2555
2620
|
}
|
|
2621
|
+
/**
|
|
2622
|
+
* A single complexity increment, located at its source line/column.
|
|
2623
|
+
*
|
|
2624
|
+
* `weight` is the amount this construct added to `metric`; for nested
|
|
2625
|
+
* cognitive increments `weight == 1 + nesting`. Consumers that render inline
|
|
2626
|
+
* (the VS Code editor breakdown) group contributions by `line` and sum the
|
|
2627
|
+
* weights, deferring the per-kind list to a hover.
|
|
2628
|
+
*/
|
|
2629
|
+
export interface ComplexityContribution {
|
|
2630
|
+
/**
|
|
2631
|
+
* 1-based line number where the construct begins.
|
|
2632
|
+
*/
|
|
2633
|
+
line: number
|
|
2634
|
+
/**
|
|
2635
|
+
* 0-based byte column where the construct begins.
|
|
2636
|
+
*/
|
|
2637
|
+
col: number
|
|
2638
|
+
metric: ComplexityMetric
|
|
2639
|
+
kind: ComplexityContributionKind
|
|
2640
|
+
/**
|
|
2641
|
+
* The amount added to `metric` at this site (`1 + nesting` for nested
|
|
2642
|
+
* cognitive increments, otherwise `1`).
|
|
2643
|
+
*/
|
|
2644
|
+
weight: number
|
|
2645
|
+
/**
|
|
2646
|
+
* The nesting depth at the increment site (`0` when not nested). Lets a
|
|
2647
|
+
* consumer explain a cognitive `+3` as "+1 base, +2 nesting".
|
|
2648
|
+
*/
|
|
2649
|
+
nesting: number
|
|
2650
|
+
}
|
|
2556
2651
|
/**
|
|
2557
2652
|
* Suggested action attached to a [`ComplexityViolation`].
|
|
2558
2653
|
*
|