mjolnir-qa 2.0.0 → 2.0.2
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/CHANGELOG.md +12 -2
- package/README.ar.md +3 -3
- package/README.bn.md +3 -3
- package/README.br.md +3 -3
- package/README.bs.md +3 -3
- package/README.da.md +3 -3
- package/README.de.md +3 -3
- package/README.es.md +3 -3
- package/README.fr.md +3 -3
- package/README.gr.md +3 -3
- package/README.he.md +3 -3
- package/README.it.md +3 -3
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +66 -41
- package/README.no.md +3 -3
- package/README.pl.md +3 -3
- package/README.ru.md +3 -3
- package/README.th.md +3 -3
- package/README.tr.md +3 -3
- package/README.uk.md +3 -3
- package/README.vi.md +3 -3
- package/README.zh.md +3 -3
- package/README.zht.md +3 -3
- package/dist/cli.d.mts +136 -9
- package/dist/cli.mjs +8577 -21775
- package/dist/mcp/stdio.mjs +342 -152
- package/dist/scan-pipeline-C0ka-RmX.mjs +2 -0
- package/dist/scan-pipeline-D3Yk2cef.mjs +15309 -0
- package/package.json +9 -5
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import * as ts$2 from "ts-morph";
|
|
8
8
|
import ts, { Project, SyntaxKind, ts as ts$1 } from "ts-morph";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -109,31 +109,21 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
|
|
|
109
109
|
source: report.source,
|
|
110
110
|
testsExecuted
|
|
111
111
|
};
|
|
112
|
-
if (f.qaImpact === "FLAKY-RISK" && matched !== void 0 && !matched.skipped && (matched.passedOnRetry || matched.
|
|
112
|
+
if (f.qaImpact === "FLAKY-RISK" && matched !== void 0 && !matched.skipped && (matched.passedOnRetry || matched.finalStatus === "timedOut") && deriveTrustLevel(f, {
|
|
113
|
+
...corroboration,
|
|
114
|
+
level: "defect"
|
|
115
|
+
}) === "L5") corroboration.level = "defect";
|
|
113
116
|
f.runtimeCorroboration = corroboration;
|
|
114
117
|
f.trustLevel = deriveTrustLevel(f, corroboration);
|
|
115
118
|
corroborated++;
|
|
116
119
|
}
|
|
117
120
|
return corroborated;
|
|
118
121
|
}
|
|
119
|
-
/**
|
|
120
|
-
* The test whose declaration span contains `line`. Playwright JSON
|
|
121
|
-
* verdicts carry the spec's declaration line: the containing test is
|
|
122
|
-
* the one with the greatest declaration line ≤ the finding's line in
|
|
123
|
-
* the same file (specs are flat within a file). When the report cannot
|
|
124
|
-
* place lines (JUnit, or some verdicts lack them) the only HONEST
|
|
125
|
-
* claim is file-level corroboration — plus the unambiguous
|
|
126
|
-
* single-test-file case. Claiming a specific test without range
|
|
127
|
-
* knowledge would fabricate precision the report does not carry.
|
|
128
|
-
*/
|
|
129
122
|
function findContainingTest(verdicts, line) {
|
|
130
|
-
if (
|
|
123
|
+
if (!Number.isInteger(line) || line < 1) return void 0;
|
|
131
124
|
if (verdicts.some((v) => v.line === void 0)) return void 0;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
for (const v of sorted) if ((v.line ?? 0) <= line) match = v;
|
|
135
|
-
else break;
|
|
136
|
-
return match;
|
|
125
|
+
const matches = verdicts.filter((v) => v.line === line);
|
|
126
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
137
127
|
}
|
|
138
128
|
/**
|
|
139
129
|
* The deterministic L0–L5 derivation (see TRUST_ORDER). Static base
|
|
@@ -144,7 +134,7 @@ function findContainingTest(verdicts, line) {
|
|
|
144
134
|
function deriveTrustLevel(finding, corroboration) {
|
|
145
135
|
const level = finding.evidenceLevel ?? (finding.findingType === "observation" ? "E0" : finding.findingType === "heuristic-risk" ? "E1" : finding.confidence === "low" ? "E1" : "E2");
|
|
146
136
|
if (!corroboration) return level === "E0" ? "L0" : level === "E1" ? "L1" : "L2";
|
|
147
|
-
if (corroboration.level === "defect") return "L5";
|
|
137
|
+
if (corroboration.level === "defect") return level === "E0" ? "L4" : "L5";
|
|
148
138
|
if (corroboration.level === "test") return "L4";
|
|
149
139
|
return "L3";
|
|
150
140
|
}
|
|
@@ -441,7 +431,7 @@ const MEASURED_FP = {
|
|
|
441
431
|
"QA-PW-125": {
|
|
442
432
|
fpRate: 0,
|
|
443
433
|
n: 10,
|
|
444
|
-
detectorRevision:
|
|
434
|
+
detectorRevision: 2,
|
|
445
435
|
ciLow: 0,
|
|
446
436
|
ciHigh: .2775
|
|
447
437
|
},
|
|
@@ -914,7 +904,7 @@ function buildEvidenceGraph(parts) {
|
|
|
914
904
|
* scripts/sync-sarif-version.cjs and guarded by the version-consistency
|
|
915
905
|
* spec. cli.ts re-exports this as CLI_VERSION.
|
|
916
906
|
*/
|
|
917
|
-
const ENGINE_VERSION = "2.0.
|
|
907
|
+
const ENGINE_VERSION = "2.0.2";
|
|
918
908
|
//#endregion
|
|
919
909
|
//#region src/engine/contract-versions.ts
|
|
920
910
|
/**
|
|
@@ -1052,12 +1042,12 @@ function discoverWorkspace(rootDir) {
|
|
|
1052
1042
|
else if (rawWorkspaces && typeof rawWorkspaces === "object" && Array.isArray(rawWorkspaces.packages)) globs = rawWorkspaces.packages.filter((w) => typeof w === "string");
|
|
1053
1043
|
return {
|
|
1054
1044
|
root,
|
|
1055
|
-
name: typeof pkg["name"] === "string" ? pkg["name"] : basename(root),
|
|
1045
|
+
name: typeof pkg["name"] === "string" ? pkg["name"] : basename$1(root),
|
|
1056
1046
|
packageJson: pkg,
|
|
1057
1047
|
workspaceGlobs: globs
|
|
1058
1048
|
};
|
|
1059
1049
|
}
|
|
1060
|
-
function basename(p) {
|
|
1050
|
+
function basename$1(p) {
|
|
1061
1051
|
const parts = p.split(/[\\/]/);
|
|
1062
1052
|
return parts[parts.length - 1];
|
|
1063
1053
|
}
|
|
@@ -1713,41 +1703,25 @@ function getCodeOnlyText(file) {
|
|
|
1713
1703
|
try {
|
|
1714
1704
|
const text = file.text;
|
|
1715
1705
|
const ranges = [];
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
for (const d of sf.getDescendantsOfKind(ts$1.SyntaxKind.TemplateMiddle)) {
|
|
1738
|
-
const s = d.getStart();
|
|
1739
|
-
ranges.push({
|
|
1740
|
-
start: s,
|
|
1741
|
-
end: s + d.getWidth()
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
for (const d of sf.getDescendantsOfKind(ts$1.SyntaxKind.TemplateTail)) {
|
|
1745
|
-
const s = d.getStart();
|
|
1746
|
-
ranges.push({
|
|
1747
|
-
start: s,
|
|
1748
|
-
end: s + d.getWidth()
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1706
|
+
const compiler = sf.compilerNode;
|
|
1707
|
+
const stringAndTemplateKinds = {
|
|
1708
|
+
[ts$1.SyntaxKind.StringLiteral]: true,
|
|
1709
|
+
[ts$1.SyntaxKind.NoSubstitutionTemplateLiteral]: true,
|
|
1710
|
+
[ts$1.SyntaxKind.TemplateHead]: true,
|
|
1711
|
+
[ts$1.SyntaxKind.TemplateMiddle]: true,
|
|
1712
|
+
[ts$1.SyntaxKind.TemplateTail]: true
|
|
1713
|
+
};
|
|
1714
|
+
const collect = (node) => {
|
|
1715
|
+
if (stringAndTemplateKinds[node.kind]) {
|
|
1716
|
+
const s = node.getStart(compiler);
|
|
1717
|
+
ranges.push({
|
|
1718
|
+
start: s,
|
|
1719
|
+
end: s + node.getWidth()
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
ts$1.forEachChild(node, collect);
|
|
1723
|
+
};
|
|
1724
|
+
ts$1.forEachChild(compiler, collect);
|
|
1751
1725
|
for (const r of commentAndStringRanges({
|
|
1752
1726
|
...file,
|
|
1753
1727
|
ast: sf
|
|
@@ -6677,7 +6651,7 @@ const swallowedVerificationFailure = defineRule({
|
|
|
6677
6651
|
autofix: false,
|
|
6678
6652
|
detectionStrategy: "FRAMEWORK",
|
|
6679
6653
|
detectionNotes: "Groovy try/catch block scan (string-aware brace matching) over the Jenkinsfile text",
|
|
6680
|
-
introduced: "
|
|
6654
|
+
introduced: "1.1.1",
|
|
6681
6655
|
tier: "quarantine",
|
|
6682
6656
|
run(ctx) {
|
|
6683
6657
|
const findings = [];
|
|
@@ -8105,6 +8079,7 @@ const pwGlobalSetupSharedState = defineRule({
|
|
|
8105
8079
|
},
|
|
8106
8080
|
detectionNotes: "regex heuristic",
|
|
8107
8081
|
introduced: "0.3.0",
|
|
8082
|
+
detectorRevision: 2,
|
|
8108
8083
|
run(ctx) {
|
|
8109
8084
|
const text = ctx.text;
|
|
8110
8085
|
const findings = [];
|
|
@@ -8112,6 +8087,7 @@ const pwGlobalSetupSharedState = defineRule({
|
|
|
8112
8087
|
const re = /(?:execSync|exec|spawn|query|request)\s*\(\s*[`'"][^`'"]*(?:migrate|migration|seed|TRUNCATE|DROP\s+(?:TABLE|DATABASE)|DELETE\s+FROM)[^`'"]*[`'"]/gi;
|
|
8113
8088
|
let m;
|
|
8114
8089
|
while ((m = re.exec(text)) !== null) {
|
|
8090
|
+
if (isMasked(ctx, m.index)) continue;
|
|
8115
8091
|
const lineStart = text.lastIndexOf("\n", m.index) + 1;
|
|
8116
8092
|
const lineEnd = text.indexOf("\n", m.index);
|
|
8117
8093
|
const prevLineStart = text.lastIndexOf("\n", lineStart - 2) + 1;
|
|
@@ -11190,6 +11166,10 @@ const RULES = [
|
|
|
11190
11166
|
seCSharpSleepLookup,
|
|
11191
11167
|
sePythonSleepLookup
|
|
11192
11168
|
];
|
|
11169
|
+
/**
|
|
11170
|
+
* Look up a rule by its ID in the active registry.
|
|
11171
|
+
* Returns undefined if the ID is not found (including retired IDs).
|
|
11172
|
+
*/
|
|
11193
11173
|
function getRule(id) {
|
|
11194
11174
|
return RULES.find((r) => r.id === id);
|
|
11195
11175
|
}
|
|
@@ -11907,7 +11887,7 @@ function hasBlocker(findings) {
|
|
|
11907
11887
|
function analyzeMonorepo(packages, config) {
|
|
11908
11888
|
const results = packages.map((p) => ({
|
|
11909
11889
|
...p,
|
|
11910
|
-
verdict: verdictOf(p.score)
|
|
11890
|
+
verdict: hasBlocker(p.findings) ? "fail" : verdictOf(p.score)
|
|
11911
11891
|
}));
|
|
11912
11892
|
if (results.length === 0) return {
|
|
11913
11893
|
packages: results,
|
|
@@ -12014,7 +11994,7 @@ function configurableAggregation(results, config) {
|
|
|
12014
11994
|
*/
|
|
12015
11995
|
const CACHE_VERSION = 2;
|
|
12016
11996
|
/** Entry cap: a monorepo-scale suite stays far below this; bounded file. */
|
|
12017
|
-
const MAX_ENTRIES = 4096;
|
|
11997
|
+
const MAX_ENTRIES$1 = 4096;
|
|
12018
11998
|
/**
|
|
12019
11999
|
* Audit M5: total byte budget. The old cap counted only ENTRIES, so
|
|
12020
12000
|
* 4096 files × ~100KB of findings each could still produce a
|
|
@@ -12159,7 +12139,7 @@ function createScanCache(root) {
|
|
|
12159
12139
|
entryBytes.set(key, newBytes);
|
|
12160
12140
|
totalBytes = totalBytes - replacedBytes + newBytes;
|
|
12161
12141
|
let count = Object.keys(entries).length;
|
|
12162
|
-
while ((count > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES) && count > 1) {
|
|
12142
|
+
while ((count > MAX_ENTRIES$1 || totalBytes > MAX_TOTAL_BYTES) && count > 1) {
|
|
12163
12143
|
const oldest = Object.keys(entries)[0];
|
|
12164
12144
|
totalBytes -= entryBytes.get(oldest);
|
|
12165
12145
|
delete entries[oldest];
|
|
@@ -12601,6 +12581,7 @@ const ui$5 = plainContext();
|
|
|
12601
12581
|
const MAX_RECORDS = 1e5;
|
|
12602
12582
|
function analyze(records, source) {
|
|
12603
12583
|
const verdicts = [];
|
|
12584
|
+
const networkObservations = [];
|
|
12604
12585
|
let failed = 0;
|
|
12605
12586
|
let skipped = 0;
|
|
12606
12587
|
let retried = 0;
|
|
@@ -12610,6 +12591,16 @@ function analyze(records, source) {
|
|
|
12610
12591
|
const attempts = rec.attempts;
|
|
12611
12592
|
const finalStatus = attempts[attempts.length - 1]?.status ?? "skipped";
|
|
12612
12593
|
const totalDurationMs = attempts.reduce((s, a) => s + a.durationMs, 0);
|
|
12594
|
+
if (rec.evidenceKind === "network-observation") {
|
|
12595
|
+
networkObservations.push({
|
|
12596
|
+
file: rec.file,
|
|
12597
|
+
title: rec.title,
|
|
12598
|
+
outcome: finalStatus === "passed" ? "succeeded" : finalStatus === "skipped" ? "unknown" : "failed",
|
|
12599
|
+
durationMs: totalDurationMs,
|
|
12600
|
+
...rec.errors !== void 0 ? { errors: rec.errors } : {}
|
|
12601
|
+
});
|
|
12602
|
+
continue;
|
|
12603
|
+
}
|
|
12613
12604
|
totalDuration += totalDurationMs;
|
|
12614
12605
|
const everFailed = attempts.some((a) => a.status === "failed" || a.status === "timedOut");
|
|
12615
12606
|
const passedOnRetry = finalStatus === "passed" && attempts.length >= 2 && everFailed;
|
|
@@ -12650,9 +12641,14 @@ function analyze(records, source) {
|
|
|
12650
12641
|
flakyTests: flaky,
|
|
12651
12642
|
totalDurationMs: totalDuration,
|
|
12652
12643
|
verdicts,
|
|
12653
|
-
|
|
12644
|
+
...networkObservations.length > 0 ? {
|
|
12645
|
+
totalNetworkObservations: networkObservations.length,
|
|
12646
|
+
failedNetworkObservations: networkObservations.filter((observation) => observation.outcome === "failed").length,
|
|
12647
|
+
networkObservations
|
|
12648
|
+
} : {},
|
|
12649
|
+
analysisComplete: records.length <= MAX_RECORDS,
|
|
12654
12650
|
skippedReports: 0,
|
|
12655
|
-
incompleteReasons: []
|
|
12651
|
+
incompleteReasons: records.length > MAX_RECORDS ? ["record-count-limit"] : []
|
|
12656
12652
|
};
|
|
12657
12653
|
}
|
|
12658
12654
|
/** Flakiness Leaderboard: flakiest first, then slowest. */
|
|
@@ -12667,16 +12663,36 @@ function bar(ms, maxMs, width = 20) {
|
|
|
12667
12663
|
const filled = Math.round(ms / maxMs * width);
|
|
12668
12664
|
return "█".repeat(Math.min(width, filled)) + "░".repeat(Math.max(0, width - filled));
|
|
12669
12665
|
}
|
|
12666
|
+
function renderNetworkObservations(report) {
|
|
12667
|
+
if (!report.networkObservations?.length) return [];
|
|
12668
|
+
const failures = report.networkObservations.filter((observation) => observation.outcome === "failed");
|
|
12669
|
+
return [
|
|
12670
|
+
`${report.networkObservations.length} network observations · ${failures.length} network failures (not test outcomes)`,
|
|
12671
|
+
...failures.slice(0, 25).map((observation) => `NETWORK FAILURE ${observation.title} · ${(observation.durationMs / 1e3).toFixed(1)}s`),
|
|
12672
|
+
""
|
|
12673
|
+
];
|
|
12674
|
+
}
|
|
12675
|
+
function partialAnalysisMessage(report) {
|
|
12676
|
+
const skippedReasons = report.incompleteReasons.filter((reason) => reason !== "record-count-limit");
|
|
12677
|
+
const partialReasons = report.incompleteReasons.filter((reason) => reason === "record-count-limit").map(() => "record count limit reached");
|
|
12678
|
+
if (report.skippedReports > 0) {
|
|
12679
|
+
const skippedReasonText = skippedReasons.length > 0 ? ` (${skippedReasons.join(", ")})` : "";
|
|
12680
|
+
const suffix = partialReasons.length > 0 ? `; ${partialReasons.join(", ")}.` : ".";
|
|
12681
|
+
return `Analysis is partial — ${report.skippedReports} report(s) skipped${skippedReasonText}${suffix}`;
|
|
12682
|
+
}
|
|
12683
|
+
return `${partialReasons.length > 0 ? partialReasons.join(", ") : report.incompleteReasons.join(", ")}; analysis is partial.`;
|
|
12684
|
+
}
|
|
12670
12685
|
function renderLeaderboard(report) {
|
|
12671
12686
|
const lines = [];
|
|
12672
12687
|
lines.push(sectionHeader("FLAKINESS LEADERBOARD", ui$5));
|
|
12673
12688
|
lines.push("");
|
|
12674
12689
|
lines.push(`${report.totalTests} tests · ${report.failed} failed · ${report.flakyTests} flaky · ${report.retriedTests} retried`);
|
|
12675
12690
|
lines.push("");
|
|
12691
|
+
lines.push(...renderNetworkObservations(report));
|
|
12676
12692
|
const top = leaderboard(report);
|
|
12677
12693
|
if (top.length === 0) {
|
|
12678
|
-
lines.push("No failures or retries found — nothing suspicious this run.");
|
|
12679
|
-
if (!report.analysisComplete) lines.push(`⚠
|
|
12694
|
+
lines.push(report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 ? "No test outcomes available from network observations." : (report.totalNetworkObservations ?? 0) > 0 ? "No flaky or failing tests detected; network evidence is listed separately." : "No failures or retries found — nothing suspicious this run.");
|
|
12695
|
+
if (!report.analysisComplete) lines.push(`⚠ ${partialAnalysisMessage(report)}`);
|
|
12680
12696
|
return lines.join("\n");
|
|
12681
12697
|
}
|
|
12682
12698
|
const maxMs = Math.max(...top.map((v) => v.totalDurationMs), 1);
|
|
@@ -12686,7 +12702,7 @@ function renderLeaderboard(report) {
|
|
|
12686
12702
|
}
|
|
12687
12703
|
if (!report.analysisComplete) {
|
|
12688
12704
|
lines.push("");
|
|
12689
|
-
lines.push(`⚠
|
|
12705
|
+
lines.push(`⚠ ${partialAnalysisMessage(report)}`);
|
|
12690
12706
|
}
|
|
12691
12707
|
return lines.join("\n");
|
|
12692
12708
|
}
|
|
@@ -12700,12 +12716,13 @@ function renderFlakyMd(report) {
|
|
|
12700
12716
|
lines.push("");
|
|
12701
12717
|
lines.push(`**${report.totalTests}** tests analyzed · **${report.flakyTests}** true flakes · **${report.retriedTests}** retried · **${report.failed}** failing`);
|
|
12702
12718
|
lines.push("");
|
|
12719
|
+
lines.push(...renderNetworkObservations(report));
|
|
12703
12720
|
const top = leaderboard(report);
|
|
12704
12721
|
if (top.length === 0) {
|
|
12705
|
-
lines.push("_No flaky or failing tests detected in this run._");
|
|
12722
|
+
lines.push(report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 ? "_No test outcomes available from network observations._" : "_No flaky or failing tests detected in this run._");
|
|
12706
12723
|
if (!report.analysisComplete) {
|
|
12707
12724
|
lines.push("");
|
|
12708
|
-
lines.push(`> ⚠
|
|
12725
|
+
lines.push(`> ⚠ ${partialAnalysisMessage(report)}`);
|
|
12709
12726
|
}
|
|
12710
12727
|
return lines.join("\n");
|
|
12711
12728
|
}
|
|
@@ -12717,7 +12734,7 @@ function renderFlakyMd(report) {
|
|
|
12717
12734
|
}
|
|
12718
12735
|
if (!report.analysisComplete) {
|
|
12719
12736
|
lines.push("");
|
|
12720
|
-
lines.push(`> ⚠
|
|
12737
|
+
lines.push(`> ⚠ ${partialAnalysisMessage(report)}`);
|
|
12721
12738
|
}
|
|
12722
12739
|
lines.push("");
|
|
12723
12740
|
return lines.join("\n");
|
|
@@ -13236,6 +13253,92 @@ function parseVitestJson(json) {
|
|
|
13236
13253
|
return out;
|
|
13237
13254
|
}
|
|
13238
13255
|
//#endregion
|
|
13256
|
+
//#region src/forensics/parse-har.ts
|
|
13257
|
+
const MAX_ENTRIES = 2e4;
|
|
13258
|
+
function sanitizeTitleUrl(url) {
|
|
13259
|
+
try {
|
|
13260
|
+
const parsed = new URL(url);
|
|
13261
|
+
parsed.username = "";
|
|
13262
|
+
parsed.password = "";
|
|
13263
|
+
parsed.search = "";
|
|
13264
|
+
parsed.hash = "";
|
|
13265
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
13266
|
+
} catch {
|
|
13267
|
+
return url.split(/[?#]/, 1)[0] ?? url;
|
|
13268
|
+
}
|
|
13269
|
+
}
|
|
13270
|
+
function serializeTransportError(value) {
|
|
13271
|
+
if (typeof value === "string") return value;
|
|
13272
|
+
try {
|
|
13273
|
+
const serialized = JSON.stringify(value);
|
|
13274
|
+
return serialized === void 0 ? String(value) : serialized;
|
|
13275
|
+
} catch {
|
|
13276
|
+
return "[unserializable transport error]";
|
|
13277
|
+
}
|
|
13278
|
+
}
|
|
13279
|
+
function isValidHttpStatus(status) {
|
|
13280
|
+
return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599;
|
|
13281
|
+
}
|
|
13282
|
+
/**
|
|
13283
|
+
* Sniff: a HAR root is `{ log: { entries: [...] } }` — distinct from
|
|
13284
|
+
* every other JSON shape the forensics layer accepts (Playwright/Jest/
|
|
13285
|
+
* Vitest all key on `testResults` or `suites`).
|
|
13286
|
+
*/
|
|
13287
|
+
function looksLikeHarJson(json) {
|
|
13288
|
+
if (!json || typeof json !== "object") return false;
|
|
13289
|
+
const log = json.log;
|
|
13290
|
+
if (!log || typeof log !== "object") return false;
|
|
13291
|
+
return Array.isArray(log.entries);
|
|
13292
|
+
}
|
|
13293
|
+
function parseHarJsonDetailed(json) {
|
|
13294
|
+
if (!json || typeof json !== "object") return {
|
|
13295
|
+
records: [],
|
|
13296
|
+
truncated: false
|
|
13297
|
+
};
|
|
13298
|
+
const entries = json.log?.entries;
|
|
13299
|
+
if (!Array.isArray(entries)) return {
|
|
13300
|
+
records: [],
|
|
13301
|
+
truncated: false
|
|
13302
|
+
};
|
|
13303
|
+
const out = [];
|
|
13304
|
+
const truncated = entries.length > MAX_ENTRIES;
|
|
13305
|
+
for (const entry of entries.slice(0, MAX_ENTRIES)) {
|
|
13306
|
+
if (!entry || typeof entry !== "object") continue;
|
|
13307
|
+
const method = typeof entry.request?.method === "string" ? entry.request.method : "GET";
|
|
13308
|
+
const url = typeof entry.request?.url === "string" ? entry.request.url : void 0;
|
|
13309
|
+
if (url === void 0 || url.length === 0) continue;
|
|
13310
|
+
const status = typeof entry.response?.status === "number" ? entry.response.status : void 0;
|
|
13311
|
+
const transportError = entry.response?._error ?? entry.response?.error ?? void 0;
|
|
13312
|
+
const hasTransportError = transportError !== void 0 && transportError !== null;
|
|
13313
|
+
const hasValidStatus = isValidHttpStatus(status);
|
|
13314
|
+
if (!hasValidStatus && !hasTransportError) continue;
|
|
13315
|
+
const httpFailed = hasValidStatus && status >= 400;
|
|
13316
|
+
const errors = [];
|
|
13317
|
+
if (hasTransportError) errors.push(sanitizeErrorText(serializeTransportError(transportError)));
|
|
13318
|
+
if (httpFailed) {
|
|
13319
|
+
const text = typeof entry.response?.statusText === "string" && entry.response.statusText.length > 0 ? entry.response.statusText : `HTTP ${status}`;
|
|
13320
|
+
errors.push(sanitizeErrorText(`${status} ${text}`));
|
|
13321
|
+
}
|
|
13322
|
+
const durationMs = typeof entry.time === "number" && Number.isFinite(entry.time) ? Math.max(0, Math.round(entry.time)) : 0;
|
|
13323
|
+
const attempt = {
|
|
13324
|
+
index: 1,
|
|
13325
|
+
status: hasTransportError || httpFailed ? "failed" : "passed",
|
|
13326
|
+
durationMs
|
|
13327
|
+
};
|
|
13328
|
+
out.push({
|
|
13329
|
+
file: "har",
|
|
13330
|
+
evidenceKind: "network-observation",
|
|
13331
|
+
title: `${method} ${sanitizeTitleUrl(url)}`,
|
|
13332
|
+
attempts: [attempt],
|
|
13333
|
+
...errors.length > 0 ? { errors } : {}
|
|
13334
|
+
});
|
|
13335
|
+
}
|
|
13336
|
+
return {
|
|
13337
|
+
records: out,
|
|
13338
|
+
truncated
|
|
13339
|
+
};
|
|
13340
|
+
}
|
|
13341
|
+
//#endregion
|
|
13239
13342
|
//#region src/forensics/run.ts
|
|
13240
13343
|
/**
|
|
13241
13344
|
* `mjolnir forensics <dir-or-file>` — runtime evidence entry point (R4).
|
|
@@ -13252,6 +13355,16 @@ function parseVitestJson(json) {
|
|
|
13252
13355
|
const MAX_FILES = 500;
|
|
13253
13356
|
const MAX_REPORT_FILE_BYTES = 1048576;
|
|
13254
13357
|
const MAX_CUMULATIVE_BYTES = 52428800;
|
|
13358
|
+
/**
|
|
13359
|
+
* Run forensics analysis on test result artifacts.
|
|
13360
|
+
*
|
|
13361
|
+
* Parses test result files (Playwright JSON, JUnit XML, HAR, etc.),
|
|
13362
|
+
* analyzes them for flakiness, determinism, and trust signals, and
|
|
13363
|
+
* returns a structured report plus rendered output strings.
|
|
13364
|
+
*
|
|
13365
|
+
* @param target - path to the test result artifact or directory
|
|
13366
|
+
* @param options - writeFlakyMd (default true) controls FLAKY.md output
|
|
13367
|
+
*/
|
|
13255
13368
|
function runForensics(target, options = {}) {
|
|
13256
13369
|
const records = [];
|
|
13257
13370
|
let source = "playwright-json";
|
|
@@ -13264,7 +13377,7 @@ function runForensics(target, options = {}) {
|
|
|
13264
13377
|
output: [
|
|
13265
13378
|
renderLeaderboard(report),
|
|
13266
13379
|
"",
|
|
13267
|
-
|
|
13380
|
+
renderFlakyMdNotWritten(options)
|
|
13268
13381
|
].join("\n")
|
|
13269
13382
|
};
|
|
13270
13383
|
}
|
|
@@ -13299,15 +13412,8 @@ function runForensics(target, options = {}) {
|
|
|
13299
13412
|
].join("\n")
|
|
13300
13413
|
};
|
|
13301
13414
|
}
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
report,
|
|
13305
|
-
output: [
|
|
13306
|
-
renderLeaderboard(report),
|
|
13307
|
-
"",
|
|
13308
|
-
renderFlakyMdHint()
|
|
13309
|
-
].join("\n")
|
|
13310
|
-
};
|
|
13415
|
+
records.push(record);
|
|
13416
|
+
source = "playwright-trace";
|
|
13311
13417
|
} catch {
|
|
13312
13418
|
const report = analyze([], "playwright-trace");
|
|
13313
13419
|
report.analysisComplete = false;
|
|
@@ -13322,10 +13428,15 @@ function runForensics(target, options = {}) {
|
|
|
13322
13428
|
].join("\n")
|
|
13323
13429
|
};
|
|
13324
13430
|
}
|
|
13325
|
-
try {
|
|
13431
|
+
else try {
|
|
13326
13432
|
const parsed = parseFile(target, readFileSync(target, "utf8"));
|
|
13327
13433
|
records.push(...parsed.records);
|
|
13328
13434
|
source = parsed.source;
|
|
13435
|
+
if (parsed.parseFailed) {
|
|
13436
|
+
skippedReports = 1;
|
|
13437
|
+
incompleteReasons.push("parse-failure");
|
|
13438
|
+
}
|
|
13439
|
+
if (parsed.truncated && !incompleteReasons.includes("entry-count-limit")) incompleteReasons.push("entry-count-limit");
|
|
13329
13440
|
} catch {
|
|
13330
13441
|
skippedReports = 1;
|
|
13331
13442
|
incompleteReasons.push("parse-failure");
|
|
@@ -13357,13 +13468,18 @@ function runForensics(target, options = {}) {
|
|
|
13357
13468
|
if (/\.(?:zip|trace|ndjson)$/i.test(full)) {
|
|
13358
13469
|
const record = parseTraceArtifact(readFileSync(full), traceArtifactName(full));
|
|
13359
13470
|
if (record === void 0) continue;
|
|
13360
|
-
if (records.length === 0) source = "playwright-trace";
|
|
13471
|
+
if (records.length === 0 || source === "har") source = "playwright-trace";
|
|
13361
13472
|
records.push(record);
|
|
13362
13473
|
continue;
|
|
13363
13474
|
}
|
|
13364
13475
|
const parsed = parseFile(full, readFileSync(full, "utf8"));
|
|
13476
|
+
if (parsed.parseFailed) {
|
|
13477
|
+
skippedReports++;
|
|
13478
|
+
if (!incompleteReasons.includes("parse-failure")) incompleteReasons.push("parse-failure");
|
|
13479
|
+
}
|
|
13480
|
+
if (parsed.truncated && !incompleteReasons.includes("entry-count-limit")) incompleteReasons.push("entry-count-limit");
|
|
13365
13481
|
if (parsed.records.length === 0) continue;
|
|
13366
|
-
if (records.length === 0) source = parsed.source;
|
|
13482
|
+
if (records.length === 0 || source === "har" && parsed.source !== "har") source = parsed.source;
|
|
13367
13483
|
records.push(...parsed.records);
|
|
13368
13484
|
} catch {
|
|
13369
13485
|
skippedReports++;
|
|
@@ -13380,10 +13496,10 @@ function runForensics(target, options = {}) {
|
|
|
13380
13496
|
}
|
|
13381
13497
|
}
|
|
13382
13498
|
const report = analyze(records, source);
|
|
13383
|
-
if (skippedReports > 0) {
|
|
13499
|
+
if (skippedReports > 0 || incompleteReasons.length > 0) {
|
|
13384
13500
|
report.analysisComplete = false;
|
|
13385
13501
|
report.skippedReports = skippedReports;
|
|
13386
|
-
report.incompleteReasons = incompleteReasons;
|
|
13502
|
+
report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
|
|
13387
13503
|
}
|
|
13388
13504
|
let flakyMdPath;
|
|
13389
13505
|
if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
|
|
@@ -13400,7 +13516,7 @@ function runForensics(target, options = {}) {
|
|
|
13400
13516
|
output: [
|
|
13401
13517
|
renderLeaderboard(report),
|
|
13402
13518
|
"",
|
|
13403
|
-
flakyMdPath !== void 0 ? renderFlakyMdHint() : renderFlakyMdNotWritten(options)
|
|
13519
|
+
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options)
|
|
13404
13520
|
].join("\n"),
|
|
13405
13521
|
flakyMdPath
|
|
13406
13522
|
};
|
|
@@ -13430,14 +13546,33 @@ function parseFile(path, text) {
|
|
|
13430
13546
|
source: "junit-xml"
|
|
13431
13547
|
};
|
|
13432
13548
|
let json;
|
|
13549
|
+
const isHarFile = /\.har$/i.test(path);
|
|
13433
13550
|
try {
|
|
13434
13551
|
json = JSON.parse(text);
|
|
13435
13552
|
} catch {
|
|
13436
13553
|
return {
|
|
13437
13554
|
records: [],
|
|
13438
|
-
source: "playwright-json"
|
|
13555
|
+
source: isHarFile ? "har" : "playwright-json",
|
|
13556
|
+
parseFailed: true
|
|
13439
13557
|
};
|
|
13440
13558
|
}
|
|
13559
|
+
if (looksLikeHarJson(json)) {
|
|
13560
|
+
const parsed = parseHarJsonDetailed(json);
|
|
13561
|
+
return {
|
|
13562
|
+
records: parsed.records,
|
|
13563
|
+
source: "har",
|
|
13564
|
+
truncated: parsed.truncated
|
|
13565
|
+
};
|
|
13566
|
+
}
|
|
13567
|
+
if (isHarFile || json !== null && typeof json === "object" && [
|
|
13568
|
+
"suites",
|
|
13569
|
+
"specs",
|
|
13570
|
+
"testResults"
|
|
13571
|
+
].some((key) => key in json && !Array.isArray(json[key]))) return {
|
|
13572
|
+
records: [],
|
|
13573
|
+
source: isHarFile ? "har" : "playwright-json",
|
|
13574
|
+
parseFailed: true
|
|
13575
|
+
};
|
|
13441
13576
|
if (looksLikeJestJson(json)) return {
|
|
13442
13577
|
records: parseJestJson(json),
|
|
13443
13578
|
source: "jest-json"
|
|
@@ -13466,7 +13601,7 @@ function listFiles(dir) {
|
|
|
13466
13601
|
if (e.isDirectory()) {
|
|
13467
13602
|
if (["node_modules", ".git"].includes(e.name)) continue;
|
|
13468
13603
|
walk(full, depth + 1);
|
|
13469
|
-
} else if (e.isFile() && /\.(?:json|xml|zip|trace|ndjson)$/i.test(e.name)) out.push(full);
|
|
13604
|
+
} else if (e.isFile() && /\.(?:json|xml|zip|trace|ndjson|har)$/i.test(e.name)) out.push(full);
|
|
13470
13605
|
}
|
|
13471
13606
|
};
|
|
13472
13607
|
walk(dir, 0);
|
|
@@ -13813,18 +13948,22 @@ function loadJsonRule(path, result) {
|
|
|
13813
13948
|
for (const re of regexes) {
|
|
13814
13949
|
const run = new RegExp(re.source, "g");
|
|
13815
13950
|
let m;
|
|
13816
|
-
while ((m = run.exec(view)) !== null)
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13951
|
+
while ((m = run.exec(view)) !== null) {
|
|
13952
|
+
if (findings.length >= 1e4) throw new Error(`External rule ${id} exceeded its match limit`);
|
|
13953
|
+
if (m[0].length === 0) run.lastIndex = m.index + 1;
|
|
13954
|
+
findings.push({
|
|
13955
|
+
severity,
|
|
13956
|
+
confidence,
|
|
13957
|
+
findingType: "heuristic-risk",
|
|
13958
|
+
qaImpact,
|
|
13959
|
+
file: ctx.path,
|
|
13960
|
+
line: 1 + view.slice(0, m.index).split("\n").length - 1,
|
|
13961
|
+
column: m.index - (view.lastIndexOf("\n", m.index - 1) + 1) + 1,
|
|
13962
|
+
message,
|
|
13963
|
+
why,
|
|
13964
|
+
fix
|
|
13965
|
+
});
|
|
13966
|
+
}
|
|
13828
13967
|
}
|
|
13829
13968
|
return findings;
|
|
13830
13969
|
}
|
|
@@ -14044,20 +14183,27 @@ function pathMatchesGlob(path, glob) {
|
|
|
14044
14183
|
* Plan §16 + WI-11: locate a runtime run report next to the scan
|
|
14045
14184
|
* target, using the exact conventions the forensics ingestion already
|
|
14046
14185
|
* accepts. Zero-config search over conventional artifact names at
|
|
14047
|
-
* depth ≤ 2 (src/discovery/evidence-discovery.ts)
|
|
14048
|
-
* candidate
|
|
14049
|
-
*
|
|
14050
|
-
*
|
|
14051
|
-
*
|
|
14186
|
+
* depth ≤ 2 (src/discovery/evidence-discovery.ts). The FIRST parsable
|
|
14187
|
+
* candidate with `totalTests > 0` is kept as a fallback; the loop
|
|
14188
|
+
* continues and returns a later candidate when one with
|
|
14189
|
+
* `analysisComplete === true` is found. Returns the parsed report
|
|
14190
|
+
* alongside the path to avoid double-parsing.
|
|
14052
14191
|
*/
|
|
14053
14192
|
function discoverAndParseRuntimeReport(scanRoot) {
|
|
14193
|
+
let fallback;
|
|
14054
14194
|
for (const c of discoverEvidenceCandidates(scanRoot)) try {
|
|
14055
14195
|
const fr = runForensics(c.path, { writeFlakyMd: false });
|
|
14056
|
-
if (fr.report.totalTests
|
|
14196
|
+
if (fr.report.totalTests <= 0) continue;
|
|
14197
|
+
if (!fallback) fallback = {
|
|
14198
|
+
path: c.path,
|
|
14199
|
+
report: fr.report
|
|
14200
|
+
};
|
|
14201
|
+
if (fr.report.analysisComplete === true) return {
|
|
14057
14202
|
path: c.path,
|
|
14058
14203
|
report: fr.report
|
|
14059
14204
|
};
|
|
14060
14205
|
} catch {}
|
|
14206
|
+
return fallback;
|
|
14061
14207
|
}
|
|
14062
14208
|
/**
|
|
14063
14209
|
* Audit W10 — runtime shape validation at the rule→Finding boundary.
|
|
@@ -14161,6 +14307,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
14161
14307
|
continue;
|
|
14162
14308
|
}
|
|
14163
14309
|
let fileBudgetExceeded = false;
|
|
14310
|
+
let fileRuleFailed = false;
|
|
14164
14311
|
const relPath = relative(workspace.root, path).replaceAll("\\", "/");
|
|
14165
14312
|
if (!isCiAdapter) {
|
|
14166
14313
|
const lang = {
|
|
@@ -14227,6 +14374,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
14227
14374
|
} : parsedFile;
|
|
14228
14375
|
adapter.runRules(activeRules, fileForRules, (f, ruleId, category) => {
|
|
14229
14376
|
if (!isValidFindingRecord(f)) {
|
|
14377
|
+
fileRuleFailed = true;
|
|
14230
14378
|
onRuleCrash?.(ruleId, relPath, /* @__PURE__ */ new Error(`malformed finding record rejected (severity/line/message must be present, severity ∈ error|warning|info): ${JSON.stringify(f)}`));
|
|
14231
14379
|
return;
|
|
14232
14380
|
}
|
|
@@ -14236,6 +14384,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
14236
14384
|
category
|
|
14237
14385
|
});
|
|
14238
14386
|
}, (ruleId, error) => {
|
|
14387
|
+
fileRuleFailed = true;
|
|
14239
14388
|
onRuleCrash?.(ruleId, relPath, error);
|
|
14240
14389
|
}, {
|
|
14241
14390
|
deadline: Math.min(deadline, Date.now() + LIMITS$1.maxFileAnalysisMs),
|
|
@@ -14246,7 +14395,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
14246
14395
|
fileBudgetExceeded = true;
|
|
14247
14396
|
}
|
|
14248
14397
|
});
|
|
14249
|
-
cache.store(cacheKey, findings.slice(findingsStart), fileBudgetExceeded);
|
|
14398
|
+
if (!fileRuleFailed) cache.store(cacheKey, findings.slice(findingsStart), fileBudgetExceeded);
|
|
14250
14399
|
} catch {
|
|
14251
14400
|
skippedFiles++;
|
|
14252
14401
|
parseFailed++;
|
|
@@ -14263,6 +14412,31 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
14263
14412
|
scanned
|
|
14264
14413
|
};
|
|
14265
14414
|
}
|
|
14415
|
+
/**
|
|
14416
|
+
* Summarize forensic classifications from an ingested runtime report
|
|
14417
|
+
* (plan §10.4, WAVE 5). Returns undefined when no verdict carries a
|
|
14418
|
+
* classification — the machine contract slot stays absent rather than
|
|
14419
|
+
* reporting all-zero counts.
|
|
14420
|
+
*/
|
|
14421
|
+
function summarizeForensicVerdicts(report) {
|
|
14422
|
+
if (report.analysisComplete !== true) return void 0;
|
|
14423
|
+
const byVerdict = {};
|
|
14424
|
+
let classifications = 0;
|
|
14425
|
+
let inconclusive = 0;
|
|
14426
|
+
for (const v of report.verdicts) {
|
|
14427
|
+
if (!v.forensic) continue;
|
|
14428
|
+
classifications++;
|
|
14429
|
+
const label = v.forensic.verdict;
|
|
14430
|
+
byVerdict[label] = (byVerdict[label] ?? 0) + 1;
|
|
14431
|
+
if (label === "inconclusive") inconclusive++;
|
|
14432
|
+
}
|
|
14433
|
+
if (classifications === 0) return void 0;
|
|
14434
|
+
return {
|
|
14435
|
+
classifications,
|
|
14436
|
+
byVerdict,
|
|
14437
|
+
inconclusive
|
|
14438
|
+
};
|
|
14439
|
+
}
|
|
14266
14440
|
function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, declarationsByFile, testDeclarationCount, tierByRuleId, REVISION_BY_RULE_ID) {
|
|
14267
14441
|
let scopeInfo = { scope: "all" };
|
|
14268
14442
|
if (args.scopeChanged) {
|
|
@@ -14311,9 +14485,11 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
|
|
|
14311
14485
|
for (const f of findings) f.fixGroupId = f.ruleId;
|
|
14312
14486
|
const discoveredReport = discoverAndParseRuntimeReport(scanRoot.root);
|
|
14313
14487
|
const runtimeReportPath = discoveredReport?.path;
|
|
14314
|
-
|
|
14488
|
+
let forensicVerdicts;
|
|
14489
|
+
if (discoveredReport && discoveredReport.report.analysisComplete === true) try {
|
|
14315
14490
|
buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
|
|
14316
14491
|
stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
|
|
14492
|
+
forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
|
|
14317
14493
|
} catch {}
|
|
14318
14494
|
return {
|
|
14319
14495
|
testDeclarationCount,
|
|
@@ -14321,6 +14497,7 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
|
|
|
14321
14497
|
suppressionCount,
|
|
14322
14498
|
frameworks,
|
|
14323
14499
|
runtimeReportPath,
|
|
14500
|
+
forensicVerdicts,
|
|
14324
14501
|
config
|
|
14325
14502
|
};
|
|
14326
14503
|
}
|
|
@@ -14329,42 +14506,45 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
|
|
|
14329
14506
|
* Uses the dependency graph to assign findings to their nearest package
|
|
14330
14507
|
* manifest, falling back to the workspace root.
|
|
14331
14508
|
*/
|
|
14332
|
-
function partitionFindingsByPackage(findings, depGraph, workspaceRoot) {
|
|
14333
|
-
const packagePaths = depGraph.allPaths;
|
|
14334
|
-
if (packagePaths.length === 0) return [];
|
|
14335
|
-
const normalizedRoot = workspaceRoot.replaceAll("\\", "/");
|
|
14509
|
+
function partitionFindingsByPackage(findings, depGraph, workspaceRoot, declarationsByFile, analysisComplete) {
|
|
14336
14510
|
const packageMap = /* @__PURE__ */ new Map();
|
|
14337
|
-
|
|
14338
|
-
const
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14345
|
-
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14511
|
+
const addPackage = (path) => {
|
|
14512
|
+
const entry = {
|
|
14513
|
+
packageName: path === "" ? "root" : basename(path),
|
|
14514
|
+
path: path || ".",
|
|
14515
|
+
findings: [],
|
|
14516
|
+
testDeclarations: 0,
|
|
14517
|
+
testFileCount: 0
|
|
14518
|
+
};
|
|
14519
|
+
packageMap.set(path, entry);
|
|
14520
|
+
return entry;
|
|
14521
|
+
};
|
|
14522
|
+
for (const manifestPath of depGraph.allPaths) {
|
|
14523
|
+
const path = relative(workspaceRoot, dirname(manifestPath)).replaceAll("\\", "/");
|
|
14524
|
+
if (path === ".." || path.startsWith("../")) continue;
|
|
14525
|
+
if (!packageMap.has(path)) addPackage(path);
|
|
14526
|
+
}
|
|
14527
|
+
const packageForFile = (file) => {
|
|
14528
|
+
const normalizedFile = file.replaceAll("\\", "/");
|
|
14529
|
+
let bestPath = "";
|
|
14530
|
+
for (const path of packageMap.keys()) if (path.length > bestPath.length && normalizedFile.startsWith(path + "/")) bestPath = path;
|
|
14531
|
+
return packageMap.get(bestPath) ?? addPackage("");
|
|
14532
|
+
};
|
|
14533
|
+
for (const f of findings) packageForFile(f.file).findings.push(f);
|
|
14534
|
+
for (const [file, declarations] of declarationsByFile) {
|
|
14535
|
+
const target = packageForFile(file);
|
|
14536
|
+
target.testDeclarations += declarations;
|
|
14537
|
+
target.testFileCount++;
|
|
14362
14538
|
}
|
|
14363
14539
|
return [...packageMap.values()].map((p) => ({
|
|
14364
14540
|
packageName: p.packageName,
|
|
14365
14541
|
path: p.path,
|
|
14366
14542
|
findings: p.findings,
|
|
14367
|
-
score: p.
|
|
14543
|
+
score: analysisComplete && p.testDeclarations > 0 && p.testFileCount > 0 ? computeTotal(computeDimensions(p.findings), p.findings, {
|
|
14544
|
+
testDeclarations: p.testDeclarations,
|
|
14545
|
+
testFileCount: p.testFileCount,
|
|
14546
|
+
suiteInvalidatingRuleIds: SUITE_INVALIDATING_RULE_IDS
|
|
14547
|
+
}) : null
|
|
14368
14548
|
}));
|
|
14369
14549
|
}
|
|
14370
14550
|
function assembleScanResult(o) {
|
|
@@ -14461,6 +14641,7 @@ function assembleScanResult(o) {
|
|
|
14461
14641
|
...o.pluginsLoaded.length > 0 ? { plugins: o.pluginsLoaded } : {},
|
|
14462
14642
|
...suiteInvalidatedBy.length > 0 ? { suiteInvalidatedBy } : {},
|
|
14463
14643
|
agenticProfile: computeAgenticProfile(o.fileProvenance, o.findings),
|
|
14644
|
+
...o.forensicVerdicts !== void 0 ? { forensicVerdicts: o.forensicVerdicts } : {},
|
|
14464
14645
|
...o.args.cache ? { cache: {
|
|
14465
14646
|
hits: o.cache.stats.hits,
|
|
14466
14647
|
misses: o.cache.stats.misses,
|
|
@@ -14487,7 +14668,7 @@ function assembleScanResult(o) {
|
|
|
14487
14668
|
};
|
|
14488
14669
|
}
|
|
14489
14670
|
if (o.args.monorepo && o.dependencyGraph && o.dependencyGraph.size > 1) {
|
|
14490
|
-
const packages = partitionFindingsByPackage(o.findings, o.dependencyGraph, o.workspace.root);
|
|
14671
|
+
const packages = partitionFindingsByPackage(o.findings, o.dependencyGraph, o.workspace.root, o.declarationsByFile, !result.partial && scopeReasons.length === 0 && !o.args.scopeChanged);
|
|
14491
14672
|
if (packages.length > 1) {
|
|
14492
14673
|
const monorepoResult = analyzeMonorepo(packages, { weightingStrategy: "worst-package" });
|
|
14493
14674
|
result.monorepoAnalysis = {
|
|
@@ -14551,7 +14732,7 @@ async function runScan(args, hooks = {}) {
|
|
|
14551
14732
|
const REVISION_BY_RULE_ID = new Map(activeRules.map((r) => [r.id, r.detectorRevision ?? MEASURED_FP[r.id]?.detectorRevision ?? 1]));
|
|
14552
14733
|
const cache = args.cache ? createScanCache(workspace.root) : disabledScanCache;
|
|
14553
14734
|
const depGraph = buildDependencyGraph(workspace.root);
|
|
14554
|
-
const rulesDigest = computeRulesDigest(activeRules)
|
|
14735
|
+
const rulesDigest = `complete-file-v1:${computeRulesDigest(activeRules)}`;
|
|
14555
14736
|
if (args.cache && args.verbose) {
|
|
14556
14737
|
const safety = isIncrementalSafe([]);
|
|
14557
14738
|
if (!safety.safe) hooks.onConfigWarning?.(`incremental: ${safety.reasons.join("; ")}`);
|
|
@@ -14637,6 +14818,7 @@ async function runScan(args, hooks = {}) {
|
|
|
14637
14818
|
suppressionCount: postScan.suppressionCount,
|
|
14638
14819
|
frameworks: postScan.frameworks,
|
|
14639
14820
|
runtimeReportPath: postScan.runtimeReportPath,
|
|
14821
|
+
forensicVerdicts: postScan.forensicVerdicts,
|
|
14640
14822
|
config: postScan.config,
|
|
14641
14823
|
fileProvenance,
|
|
14642
14824
|
started,
|
|
@@ -14693,10 +14875,14 @@ function canonicalScanJson(result) {
|
|
|
14693
14875
|
});
|
|
14694
14876
|
}
|
|
14695
14877
|
/** Advisory when the DERIVED evidence level is E0 (same rule as scoring). */
|
|
14878
|
+
function effectiveEvidenceLevel(f) {
|
|
14879
|
+
return f.evidenceLevel ?? deriveEvidenceLevel(f.findingType, f.confidence);
|
|
14880
|
+
}
|
|
14696
14881
|
function isAdvisory(f) {
|
|
14697
|
-
return (f
|
|
14882
|
+
return effectiveEvidenceLevel(f) === "E0";
|
|
14698
14883
|
}
|
|
14699
|
-
function levelFor(f) {
|
|
14884
|
+
function levelFor(f, evidenceLevel = effectiveEvidenceLevel(f)) {
|
|
14885
|
+
if (evidenceLevel === "E0") return "notice";
|
|
14700
14886
|
if (f.severity === "error") return "failure";
|
|
14701
14887
|
if (f.severity === "warning") return "warning";
|
|
14702
14888
|
return "notice";
|
|
@@ -14710,15 +14896,18 @@ function countBy(findings, predicate) {
|
|
|
14710
14896
|
*/
|
|
14711
14897
|
function buildMachineContract(result) {
|
|
14712
14898
|
const digest = createHash("sha256").update(canonicalScanJson(result)).digest("hex");
|
|
14713
|
-
const annotations = result.findings.map((f) =>
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14899
|
+
const annotations = result.findings.map((f) => {
|
|
14900
|
+
const evidenceLevel = effectiveEvidenceLevel(f);
|
|
14901
|
+
return {
|
|
14902
|
+
path: f.file,
|
|
14903
|
+
start_line: f.line,
|
|
14904
|
+
annotation_level: levelFor(f, evidenceLevel),
|
|
14905
|
+
message: `${f.ruleId}: ${f.message}`,
|
|
14906
|
+
ruleId: f.ruleId,
|
|
14907
|
+
...f.detectorRevision !== void 0 ? { detectorRevision: f.detectorRevision } : {},
|
|
14908
|
+
advisory: evidenceLevel === "E0"
|
|
14909
|
+
};
|
|
14910
|
+
});
|
|
14722
14911
|
return {
|
|
14723
14912
|
contractVersion: 1,
|
|
14724
14913
|
summary: {
|
|
@@ -14743,7 +14932,8 @@ function buildMachineContract(result) {
|
|
|
14743
14932
|
durationMs: result.analysisStatus.durationMs
|
|
14744
14933
|
},
|
|
14745
14934
|
...result.trustSummary !== void 0 ? { trustSummary: result.trustSummary } : {},
|
|
14746
|
-
...result.agenticProfile !== void 0 ? { provenance: result.agenticProfile } : {}
|
|
14935
|
+
...result.agenticProfile !== void 0 ? { provenance: result.agenticProfile } : {},
|
|
14936
|
+
...result.forensicVerdicts !== void 0 ? { forensicVerdicts: result.forensicVerdicts } : {}
|
|
14747
14937
|
};
|
|
14748
14938
|
}
|
|
14749
14939
|
//#endregion
|