mjolnir-qa 2.0.2 → 2.1.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/CHANGELOG.md +83 -0
- package/README.md +59 -47
- package/dist/cli.d.mts +35 -3
- package/dist/cli.mjs +3716 -205
- package/dist/mcp/stdio.mjs +5 -2
- package/dist/scan-pipeline-C_Rs4u8N.mjs +2 -0
- package/dist/{scan-pipeline-D3Yk2cef.mjs → scan-pipeline-MQGUSey3.mjs} +29 -6
- package/package.json +12 -4
- package/dist/scan-pipeline-C0ka-RmX.mjs +0 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as massCeiling, A as box, At as isAdvisoryFinding, B as headlineFor, C as buildFooter, Ct as buildEvidenceGraph, D as plainContext, Dt as RULE_CATEGORIES, E as panel, Et as QA_IMPACT_LABELS, F as scoreGauge, G as buildDependencyGraph, H as EVIDENCE, I as shouldColorize, J as classifyProvenance, K as getReachableFiles, L as shouldUseAscii, M as padTo, N as palette, O as sectionHeader, Ot as SEVERITY_ORDER$1, P as sanitizeData, Q as deductionFor, R as wrapText, S as FLAKE_GLYPH, St as ENGINE_VERSION, T as okIcon, Tt as DEDUCTIONS, U as TINT, V as BADGE_BAND, W as TRUST, X as capForTier, Y as computeAgenticProfile, Z as computeDimensions, _t as ConfigValidationError, at as SEARCHED_FOR, b as renderSuppressions, bt as isRecord$2, ct as parseWorkflow, dt as parseTsFile, et as RETIRED_RULE_IDS, ft as detectFrameworks, gt as isLintFixtureDir, h as scan_pipeline_exports, ht as globToRegExp, it as SCAN_ADAPTERS, j as measure, jt as isValidCategory, k as severityIcon, kt as deriveEvidenceLevel, lt as parseYamlGuarded, m as runScan$1, mt as createIgnoreMatcher, n as KNOWN_RULE_IDS$1, nt as getRule, ot as isAzurePipelineFixture, pt as DEFAULT_IGNORE_MATCHER, q as correlateFindings, rt as resolveGitPath, st as parseAzurePipeline, tt as RULES, ut as computeCodeText, v as loadLocalRules, vt as isSuppressionActive, w as nextStep, wt as MEASURED_FP, x as runForensics, xt as parseJsonFile, y as loadSuppressions, yt as loadConfig, z as deriveScoreState } from "./scan-pipeline-MQGUSey3.mjs";
|
|
3
3
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { createHash, randomBytes } from "node:crypto";
|
|
8
8
|
import ts, { ts as ts$1 } from "ts-morph";
|
|
9
|
+
import { parse } from "yaml";
|
|
9
10
|
import { execFileSync } from "node:child_process";
|
|
10
11
|
import { createInterface } from "node:readline";
|
|
11
12
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
15
|
//#region src/commands/help.ts
|
|
14
16
|
/** Ordered registry — grouped by the same categories the overview uses. */
|
|
15
17
|
const HELP_ENTRIES = [
|
|
16
18
|
{
|
|
17
19
|
verb: "ci install",
|
|
18
|
-
summary: "generate the PR workflow (
|
|
20
|
+
summary: "generate the PR workflow (advisory by default; action-based; scan + annotations + gate)",
|
|
19
21
|
usage: "mjolnir ci install [--gate advisory|error|warning] [--no-action] [--force]",
|
|
20
22
|
examples: [
|
|
21
23
|
"mjolnir ci install",
|
|
@@ -167,6 +169,55 @@ const HELP_ENTRIES = [
|
|
|
167
169
|
usage: "mjolnir suppressions",
|
|
168
170
|
examples: ["mjolnir suppressions"]
|
|
169
171
|
},
|
|
172
|
+
{
|
|
173
|
+
verb: "ci-integrity",
|
|
174
|
+
summary: "verify CI workflow integrity and suppression governance",
|
|
175
|
+
usage: "mjolnir ci-integrity [path] [--policy <file>] [--json]",
|
|
176
|
+
examples: ["mjolnir ci-integrity", "mjolnir ci-integrity . --policy mjolnir.policy.json --json"],
|
|
177
|
+
next: "mjolnir suppressions"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
verb: "framework-maturity",
|
|
181
|
+
summary: "track framework maturity levels, especially Playwright F4→F5",
|
|
182
|
+
usage: "mjolnir framework-maturity [--framework <name>] [--json]",
|
|
183
|
+
examples: ["mjolnir framework-maturity", "mjolnir framework-maturity --framework playwright"],
|
|
184
|
+
next: "mjolnir ci-integrity"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
verb: "suppression-gate",
|
|
188
|
+
summary: "enforce suppression policy governance gate",
|
|
189
|
+
usage: "mjolnir suppression-gate [path] [--policy <file>] [--json]",
|
|
190
|
+
examples: ["mjolnir suppression-gate", "mjolnir suppression-gate . --policy mjolnir.policy.json"],
|
|
191
|
+
next: "mjolnir framework-maturity"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
verb: "cross-file",
|
|
195
|
+
summary: "analyze cross-file signals: duplicates, shared imports, circular deps",
|
|
196
|
+
usage: "mjolnir cross-file [path] [--json]",
|
|
197
|
+
examples: ["mjolnir cross-file", "mjolnir cross-file . --json"],
|
|
198
|
+
next: "mjolnir suppression-gate"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
verb: "contract-verify",
|
|
202
|
+
summary: "verify machine contract integrity against scan results",
|
|
203
|
+
usage: "mjolnir contract-verify [path] [--contract <scan-json>] [--json]",
|
|
204
|
+
examples: ["mjolnir contract-verify", "mjolnir contract-verify . --contract scan.json --json"],
|
|
205
|
+
next: "mjolnir cross-file"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
verb: "trust-trend",
|
|
209
|
+
summary: "track historical trust trends and detect regressions",
|
|
210
|
+
usage: "mjolnir trust-trend [path] [--history <file>] [--recorded-at <iso>] [--json]",
|
|
211
|
+
examples: ["mjolnir trust-trend", "mjolnir trust-trend . --history .mjolnir/trust-history.json --json"],
|
|
212
|
+
next: "mjolnir contract-verify"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
verb: "evidence-graph",
|
|
216
|
+
summary: "build and query the verification evidence graph",
|
|
217
|
+
usage: "mjolnir evidence-graph [path] [--file <path>|--rule <id>] [--json]",
|
|
218
|
+
examples: ["mjolnir evidence-graph", "mjolnir evidence-graph . --rule QA-TEST-001 --json"],
|
|
219
|
+
next: "mjolnir trust-trend"
|
|
220
|
+
},
|
|
170
221
|
{
|
|
171
222
|
verb: "create-rule",
|
|
172
223
|
summary: "scaffold a new rule + fixtures (must-fire, must-not-fire)",
|
|
@@ -193,7 +244,7 @@ const HELP_ENTRIES = [
|
|
|
193
244
|
},
|
|
194
245
|
{
|
|
195
246
|
verb: "mcp",
|
|
196
|
-
summary: "run as
|
|
247
|
+
summary: "run as a read-only MCP server over stdio (scan / explain / diff / verify / forensics / triage / pw-report)",
|
|
197
248
|
usage: "mjolnir mcp",
|
|
198
249
|
examples: ["mjolnir mcp"]
|
|
199
250
|
}
|
|
@@ -305,8 +356,8 @@ const ROOT_HELP_VERBS = /* @__PURE__ */ new Set([
|
|
|
305
356
|
"help"
|
|
306
357
|
]);
|
|
307
358
|
/** One per-verb help page: summary, usage, examples, next step. */
|
|
308
|
-
function renderVerbHelp(verb) {
|
|
309
|
-
if (!hasVerbHelp(verb) && ROOT_HELP_VERBS.has(verb)) return renderRootHelp();
|
|
359
|
+
function renderVerbHelp(verb, options = {}) {
|
|
360
|
+
if (!hasVerbHelp(verb) && ROOT_HELP_VERBS.has(verb)) return renderRootHelp(1, options);
|
|
310
361
|
const e = findEntry(verb);
|
|
311
362
|
if (!e) return [
|
|
312
363
|
` No detailed help for "${verb}".`,
|
|
@@ -348,7 +399,8 @@ const GROUPS = [
|
|
|
348
399
|
"impact",
|
|
349
400
|
"baseline",
|
|
350
401
|
"diff",
|
|
351
|
-
"verify"
|
|
402
|
+
"verify",
|
|
403
|
+
"ci-integrity"
|
|
352
404
|
]
|
|
353
405
|
},
|
|
354
406
|
{
|
|
@@ -375,7 +427,13 @@ const GROUPS = [
|
|
|
375
427
|
"init",
|
|
376
428
|
"doctor",
|
|
377
429
|
"release-trust",
|
|
378
|
-
"create-rule"
|
|
430
|
+
"create-rule",
|
|
431
|
+
"cross-file",
|
|
432
|
+
"contract-verify",
|
|
433
|
+
"trust-trend",
|
|
434
|
+
"evidence-graph",
|
|
435
|
+
"framework-maturity",
|
|
436
|
+
"suppression-gate"
|
|
379
437
|
]
|
|
380
438
|
},
|
|
381
439
|
{
|
|
@@ -391,6 +449,58 @@ const GROUPS = [
|
|
|
391
449
|
}
|
|
392
450
|
];
|
|
393
451
|
const SCAN_SUMMARY_LINES = ["mjolnir [path] full-repo scan + WORTHINESS score"];
|
|
452
|
+
const DEFAULT_HELP_WIDTH = 88;
|
|
453
|
+
/**
|
|
454
|
+
* Normalizes the terminal width for help rendering. When no width is
|
|
455
|
+
* supplied, falls back to the default help width (88). Clamps to a
|
|
456
|
+
* readable minimum so narrow terminals do not produce unusably short
|
|
457
|
+
* columns.
|
|
458
|
+
*/
|
|
459
|
+
function normalizeHelpWidth(width) {
|
|
460
|
+
if (width === void 0 || !Number.isFinite(width)) return DEFAULT_HELP_WIDTH;
|
|
461
|
+
return Math.max(48, Math.floor(width));
|
|
462
|
+
}
|
|
463
|
+
function wrapWords(text, width) {
|
|
464
|
+
if (text.length <= width) return [text];
|
|
465
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
466
|
+
const lines = [];
|
|
467
|
+
let current = "";
|
|
468
|
+
for (const word of words) {
|
|
469
|
+
if (current.length === 0) {
|
|
470
|
+
current = word;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (current.length + 1 + word.length <= width) {
|
|
474
|
+
current += ` ${word}`;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
lines.push(current);
|
|
478
|
+
current = word;
|
|
479
|
+
}
|
|
480
|
+
if (current.length > 0) lines.push(current);
|
|
481
|
+
return lines.length > 0 ? lines : [text];
|
|
482
|
+
}
|
|
483
|
+
function pushWrappedIndented(lines, text, indent, width) {
|
|
484
|
+
for (const line of wrapWords(text, Math.max(16, width - indent.length))) lines.push(`${indent}${line}`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Renders a label-summary row, wrapping the summary across continuation
|
|
488
|
+
* lines when the terminal is narrower than the label + summary.
|
|
489
|
+
*/
|
|
490
|
+
function pushAlignedHelpRow(lines, label, summary, options) {
|
|
491
|
+
const indent = " ".repeat(options.indent ?? 2);
|
|
492
|
+
const gap = " ";
|
|
493
|
+
const summaryIndent = `${indent}${" ".repeat(options.labelWidth)}${gap}`;
|
|
494
|
+
const summaryWidth = Math.max(16, options.width - summaryIndent.length);
|
|
495
|
+
if (label.length <= options.labelWidth) {
|
|
496
|
+
const summaryLines = wrapWords(summary, summaryWidth);
|
|
497
|
+
lines.push(`${indent}${label.padEnd(options.labelWidth)}${gap}${summaryLines[0] ?? ""}`);
|
|
498
|
+
for (const line of summaryLines.slice(1)) lines.push(`${summaryIndent}${line}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
pushWrappedIndented(lines, label, indent, options.width);
|
|
502
|
+
pushWrappedIndented(lines, summary, `${indent} `, options.width);
|
|
503
|
+
}
|
|
394
504
|
/**
|
|
395
505
|
* The redesigned root help (plan M2): grouped sections, one-line
|
|
396
506
|
* descriptions, copy-pasteable examples, the frozen exit-code table and
|
|
@@ -398,7 +508,8 @@ const SCAN_SUMMARY_LINES = ["mjolnir [path] full-repo scan + WOR
|
|
|
398
508
|
* caller decides (runHelpCommand passes a resolved palette; printUsage
|
|
399
509
|
* stays plain).
|
|
400
510
|
*/
|
|
401
|
-
function renderRootHelp(schemaVersion = 1) {
|
|
511
|
+
function renderRootHelp(schemaVersion = 1, options = {}) {
|
|
512
|
+
const width = normalizeHelpWidth(options.width);
|
|
402
513
|
const byVerb = new Map(HELP_ENTRIES.map((e) => [e.verb, e]));
|
|
403
514
|
const lines = [];
|
|
404
515
|
lines.push("🔨 mjölnir — verification trust engine for test suites and CI pipelines");
|
|
@@ -418,20 +529,28 @@ function renderRootHelp(schemaVersion = 1) {
|
|
|
418
529
|
lines.push(" mjolnir rules --unmeasured the rules running on assumption, not measurement");
|
|
419
530
|
lines.push("");
|
|
420
531
|
lines.push("Options:");
|
|
421
|
-
for (const f of HELP_FLAGS) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
lines
|
|
426
|
-
|
|
532
|
+
for (const f of HELP_FLAGS) pushAlignedHelpRow(lines, f.flag, f.summary, {
|
|
533
|
+
width,
|
|
534
|
+
labelWidth: 22
|
|
535
|
+
});
|
|
536
|
+
pushAlignedHelpRow(lines, "-v, --version", "print the installed version and exit", {
|
|
537
|
+
width,
|
|
538
|
+
labelWidth: 22
|
|
539
|
+
});
|
|
540
|
+
pushAlignedHelpRow(lines, "-h, --help", "show this help", {
|
|
541
|
+
width,
|
|
542
|
+
labelWidth: 22
|
|
543
|
+
});
|
|
427
544
|
lines.push("");
|
|
428
545
|
for (const g of GROUPS) {
|
|
429
546
|
lines.push(`Subcommands — ${g.title}:`);
|
|
430
547
|
for (const verb of g.verbs) {
|
|
431
548
|
const e = byVerb.get(verb);
|
|
432
549
|
if (!e) continue;
|
|
433
|
-
|
|
434
|
-
|
|
550
|
+
pushAlignedHelpRow(lines, e.usage.replace(/^mjolnir /, ""), e.summary, {
|
|
551
|
+
width,
|
|
552
|
+
labelWidth: 46
|
|
553
|
+
});
|
|
435
554
|
}
|
|
436
555
|
lines.push("");
|
|
437
556
|
}
|
|
@@ -1082,6 +1201,8 @@ function renderExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
|
1082
1201
|
if (r.languages?.length) lines.push(`Languages: ${r.languages.join(", ")}`);
|
|
1083
1202
|
if (r.frameworks?.length) lines.push(`Frameworks: ${r.frameworks.join(", ")}`);
|
|
1084
1203
|
lines.push("");
|
|
1204
|
+
appendReviewComment(lines, r, result, evidenceLevel, width);
|
|
1205
|
+
lines.push("");
|
|
1085
1206
|
if (result.exampleFinding) {
|
|
1086
1207
|
const f = result.exampleFinding;
|
|
1087
1208
|
lines.push("WHAT WAS FOUND (real detector output, not a mockup)");
|
|
@@ -1109,6 +1230,54 @@ function renderExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
|
1109
1230
|
return lines.join("\n");
|
|
1110
1231
|
}
|
|
1111
1232
|
/**
|
|
1233
|
+
* Renders the copy-ready review-comment block. Quarantine-tier findings
|
|
1234
|
+
* are advisory (E0, never gate CI — see whatWouldChangeTheVerdict), so
|
|
1235
|
+
* their lead-in must not read as a merge blocker; every other tier keeps
|
|
1236
|
+
* the blocking wording. The finding's own severity is NOT consulted
|
|
1237
|
+
* here — only the rule's tier, because the gate policy is tier-driven,
|
|
1238
|
+
* not severity-driven (a quarantine rule may carry error severity and
|
|
1239
|
+
* still never block).
|
|
1240
|
+
*/
|
|
1241
|
+
function appendReviewComment(lines, rule, result, evidenceLevel, width) {
|
|
1242
|
+
const measured = MEASURED_FP[rule.id];
|
|
1243
|
+
const finding = result.exampleFinding;
|
|
1244
|
+
const pushBody = (text) => {
|
|
1245
|
+
for (const seg of wrapText(text, Math.max(20, width - 2))) lines.push(` ${seg}`);
|
|
1246
|
+
};
|
|
1247
|
+
const fpStatus = measured !== void 0 ? `measured FP ${Math.round(measured.fpRate * 100)}% (${measured.n} verdicts)` : "measured FP not available yet; treat as rule-author assumption";
|
|
1248
|
+
const isQuarantine = effectiveTier(rule) === "quarantine";
|
|
1249
|
+
lines.push("COPY-READY REVIEW COMMENT");
|
|
1250
|
+
pushBody(isQuarantine ? `Advisory finding — this does not block merging: ${reviewSentence(finding?.message ?? rule.title)}.` : `Please fix this before merging: ${reviewSentence(finding?.message ?? rule.title)}.`);
|
|
1251
|
+
pushBody(`Why it weakens verification: ${reviewSentence(finding?.why ?? QA_IMPACT_LABELS[rule.qaImpact])}.`);
|
|
1252
|
+
pushBody(`Confidence: ${rule.confidence}, evidence ${evidenceLevel}, tier ${effectiveTier(rule)}${isProvisional(rule) ? " (PROVISIONAL)" : ""}; ${fpStatus}.`);
|
|
1253
|
+
pushBody(`Suggested fix: ${reviewSentence(finding?.fix ?? defaultFixGuidance(rule, finding === void 0))}.`);
|
|
1254
|
+
pushBody(`Verify with: mjolnir --scope changed, then mjolnir explain ${rule.id} if the finding still appears.`);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Honest fallback for the "Suggested fix" line when the rule produced no
|
|
1258
|
+
* real example finding. There is no `fix` field on RuleMeta — the
|
|
1259
|
+
* guidance above is always derived from a real detector output, so when
|
|
1260
|
+
* that is absent there is nothing to quote. Claiming otherwise ("apply
|
|
1261
|
+
* the rule guidance above") pointed at a section that did not exist.
|
|
1262
|
+
*
|
|
1263
|
+
* Quarantine rules additionally have no merge-blocking fix to suggest;
|
|
1264
|
+
* the honest move is to name the tier and point at the catalog.
|
|
1265
|
+
*/
|
|
1266
|
+
function defaultFixGuidance(rule, noExample) {
|
|
1267
|
+
if (noExample) return `fixture-derived guidance is unavailable for ${rule.id} — run this command from a mjolnir checkout (or pass --fixtures-root) so the rule's own must-fire fixture can be shown, or browse the full catalog with \`mjolnir rules --md\`.`;
|
|
1268
|
+
return `apply the rule guidance above (tier ${effectiveTier(rule)})`;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Strips a trailing run of sentence-ending punctuation from a finding
|
|
1272
|
+
* field so the review comment reads as one clause rather than a
|
|
1273
|
+
* fragment followed by a period. Trims whitespace first so a field like
|
|
1274
|
+
* "fix the thing." becomes "fix the thing" and the renderer's own period
|
|
1275
|
+
* is the only one.
|
|
1276
|
+
*/
|
|
1277
|
+
function reviewSentence(text) {
|
|
1278
|
+
return text.trim().replace(/[.。]+$/u, "");
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1112
1281
|
* WHAT WOULD CHANGE THE VERDICT for a rule-level explanation (plan §26
|
|
1113
1282
|
* WI-7, §8): the honest list of state changes that move this rule's
|
|
1114
1283
|
* findings on the trust ladder or the census. Deterministic, derived
|
|
@@ -1125,11 +1294,17 @@ function whatWouldChangeTheVerdict(r) {
|
|
|
1125
1294
|
/** Structural validation of the loaded JSON (hostile-input safe). */
|
|
1126
1295
|
function parseScanJson(raw) {
|
|
1127
1296
|
try {
|
|
1128
|
-
return parseJsonFile(raw, "scan result", (v) => isRecord(v) && v.schemaVersion === 1 && Array.isArray(v.findings) && Boolean(v.analysisStatus) && typeof v.partial === "boolean");
|
|
1297
|
+
return parseJsonFile(raw, "scan result", (v) => isRecord$2(v) && v.schemaVersion === 1 && Array.isArray(v.findings) && Boolean(v.analysisStatus) && typeof v.partial === "boolean");
|
|
1129
1298
|
} catch {
|
|
1130
1299
|
return;
|
|
1131
1300
|
}
|
|
1132
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Loads a canonical scan result from a JSON file for verdict-mode
|
|
1304
|
+
* explain (`mjolnir explain <scan-result.json>`). The file is treated
|
|
1305
|
+
* as hostile input: parse failure or schema mismatch returns ok:false
|
|
1306
|
+
* with a message naming the contract, never a thrown exception.
|
|
1307
|
+
*/
|
|
1133
1308
|
function explainVerdict(jsonPath) {
|
|
1134
1309
|
let raw;
|
|
1135
1310
|
try {
|
|
@@ -1182,6 +1357,13 @@ function verdictWhatWouldChange(scan) {
|
|
|
1182
1357
|
changes.push("fixing the top trust risks (mjolnir explain <RULE-ID>) moves the score and the verdict band");
|
|
1183
1358
|
return changes;
|
|
1184
1359
|
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Renders the verdict-mode explanation (`mjolnir explain <scan-result.json>`):
|
|
1362
|
+
* the saved scan's score, trust level, evidence checklist, correlation,
|
|
1363
|
+
* ceiling reasons, and the honest list of what would change the verdict.
|
|
1364
|
+
* Every line is derived from the parsed scan — nothing is invented, and
|
|
1365
|
+
* a missing trust summary renders as L0 rather than a fabricated score.
|
|
1366
|
+
*/
|
|
1185
1367
|
function renderVerdictExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
1186
1368
|
if (!result.ok || !result.scan) return `explain failed: ${result.error ?? "unknown error"}`;
|
|
1187
1369
|
const scan = result.scan;
|
|
@@ -1218,10 +1400,11 @@ function renderVerdictExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
|
1218
1400
|
for (const c of verdictWhatWouldChange(scan)) pushBody(`- ${c}`);
|
|
1219
1401
|
lines.push("");
|
|
1220
1402
|
lines.push("NEXT ACTION");
|
|
1221
|
-
pushBody(scan.partial ? "re-run with a higher --max-duration to close the truncated surface" : errors > 0 ? "mjolnir triage <test-results-dir-or-report> — then fix the top trust risk" : "
|
|
1403
|
+
pushBody(scan.partial ? "re-run with a higher --max-duration to close the truncated surface" : errors > 0 ? "mjolnir triage <test-results-dir-or-report> — then fix the top trust risk" : "no findings on the analyzed surface — install advisory CI and re-scan the changed scope");
|
|
1222
1404
|
lines.push("");
|
|
1223
1405
|
return lines.join("\n");
|
|
1224
1406
|
}
|
|
1407
|
+
/** Formats a trust-summary ratio (0–1) as a whole-percentage string. */
|
|
1225
1408
|
function pctOf(v) {
|
|
1226
1409
|
return `${Math.round(v * 100)}%`;
|
|
1227
1410
|
}
|
|
@@ -2806,6 +2989,7 @@ const GROUP_THRESHOLD = 3;
|
|
|
2806
2989
|
/** Reflow floor: below this, box wrapping targets a minimum readable
|
|
2807
2990
|
* width rather than shrinking further (an 8-column box is useless). */
|
|
2808
2991
|
const MIN_BOX_WIDTH = 20;
|
|
2992
|
+
/** Renders a scan as a compact terminal report, prioritizing actionable fixes before diagnostics. */
|
|
2809
2993
|
function renderTerminal(result, opts) {
|
|
2810
2994
|
const p = palette(shouldColorize(opts.isTTY));
|
|
2811
2995
|
const width = Math.max(MIN_BOX_WIDTH, opts.width ?? process.stdout.columns ?? 80);
|
|
@@ -2830,8 +3014,6 @@ function renderTerminal(result, opts) {
|
|
|
2830
3014
|
lines.push(ui.p.dim(` staged surface: ${result.staged.files} file(s) scanned; score reflects that surface`));
|
|
2831
3015
|
lines.push("");
|
|
2832
3016
|
}
|
|
2833
|
-
appendDimensions(lines, result, ui);
|
|
2834
|
-
appendDeductions(lines, result, counts, ui);
|
|
2835
3017
|
const filtered = opts.visibleFindings;
|
|
2836
3018
|
const filtering = filtered !== void 0 && filtered.length < result.findings.length;
|
|
2837
3019
|
const display = filtering ? {
|
|
@@ -2839,12 +3021,16 @@ function renderTerminal(result, opts) {
|
|
|
2839
3021
|
findings: filtered
|
|
2840
3022
|
} : result;
|
|
2841
3023
|
appendFixThisFirst(lines, display, ui);
|
|
3024
|
+
appendDimensions(lines, result, ui);
|
|
3025
|
+
appendDeductions(lines, result, counts, ui);
|
|
2842
3026
|
if (filtering) {
|
|
2843
3027
|
lines.push(ui.p.dim(` filtered view: ${filtered?.length} of ${result.findings.length} findings shown; score reflects the full scan`));
|
|
2844
3028
|
lines.push("");
|
|
2845
3029
|
}
|
|
2846
3030
|
appendFindings(lines, display, counts, opts.verbose === true, ui, opts.tone);
|
|
2847
|
-
if (counts.total === 0 && result.score === 100) appendForgedBlock(lines,
|
|
3031
|
+
if (counts.total === 0 && result.score === 100 && !result.partial) appendForgedBlock(lines, ui);
|
|
3032
|
+
else if (counts.total === 0 && result.score === 100 && result.partial) appendPartialCleanGuidance(lines, ui);
|
|
3033
|
+
appendNextActions(lines, display, result, ui);
|
|
2848
3034
|
appendFooter(lines, result, ui);
|
|
2849
3035
|
return lines.join("\n");
|
|
2850
3036
|
}
|
|
@@ -2858,6 +3044,7 @@ function verdictFor(score) {
|
|
|
2858
3044
|
const verdict = deriveScoreState(score).verdict;
|
|
2859
3045
|
return verdict === "FORGED" ? "WORTHY" : verdict;
|
|
2860
3046
|
}
|
|
3047
|
+
/** Appends the score, gauge, verdict, and honesty metadata for the full scan. */
|
|
2861
3048
|
function appendScoreSection(lines, result, p, width, ascii) {
|
|
2862
3049
|
const state = deriveScoreState(result.score);
|
|
2863
3050
|
const verdictColored = colorizeVerdict(verdictFor(result.score), state.band, p);
|
|
@@ -2866,7 +3053,8 @@ function appendScoreSection(lines, result, p, width, ascii) {
|
|
|
2866
3053
|
lines.push(` ${p.bold("WORTHINESS")} ${p.bold(scoreText)}${p.dim("/100")} ${verdictColored}`);
|
|
2867
3054
|
const gaugeWidth = Math.max(10, Math.min(30, width - 4));
|
|
2868
3055
|
lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
|
|
2869
|
-
|
|
3056
|
+
const headline = result.partial ? "Partial scan — findings reflect analyzed surface only." : headlineFor(state, result.findings.length);
|
|
3057
|
+
lines.push(` ${p.dim(headline)}`);
|
|
2870
3058
|
if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
|
|
2871
3059
|
if (result.effectiveDeductions !== void 0 && result.score !== null) {
|
|
2872
3060
|
const ceiling = massCeiling(result.effectiveDeductions);
|
|
@@ -2941,10 +3129,20 @@ function appendDeductions(lines, result, counts, ui) {
|
|
|
2941
3129
|
for (const row of panel(rows, ui)) lines.push(row);
|
|
2942
3130
|
lines.push("");
|
|
2943
3131
|
}
|
|
3132
|
+
/** Prepends up to three highest-gain fixes with rationale and a concrete next command. */
|
|
2944
3133
|
function appendFixThisFirst(lines, result, ui) {
|
|
2945
3134
|
const fixes = topFixes(result.findings, 3);
|
|
2946
|
-
if (fixes.length === 0) return;
|
|
3135
|
+
if (fixes.length === 0 && result.findings.length === 0) return;
|
|
2947
3136
|
lines.push(sectionHeader("FIX THIS FIRST", ui));
|
|
3137
|
+
pushWrapped(lines, ui.p, fixFirstWhy(result), ui.width);
|
|
3138
|
+
if (fixes.length === 0) {
|
|
3139
|
+
pushWrapped(lines, ui.p, "Next action: review the advisory findings, then re-run with full detail if you need the evidence trail.", ui.width);
|
|
3140
|
+
lines.push(nextStep("mjolnir --verbose", ui));
|
|
3141
|
+
lines.push("");
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
pushWrapped(lines, ui.p, "Next action: fix the highest score-gain item below, then re-run the changed scope.", ui.width);
|
|
3145
|
+
lines.push(nextStep("mjolnir --scope changed", ui));
|
|
2948
3146
|
for (const { finding: f, scoreGain, autofixable } of fixes) {
|
|
2949
3147
|
const gainText = `+${scoreGain} pt${scoreGain === 1 ? "" : "s"}`;
|
|
2950
3148
|
const autofixTag = autofixable ? ui.p.ok(" [autofix available]") : "";
|
|
@@ -2953,6 +3151,35 @@ function appendFixThisFirst(lines, result, ui) {
|
|
|
2953
3151
|
}
|
|
2954
3152
|
lines.push("");
|
|
2955
3153
|
}
|
|
3154
|
+
/** Returns the default-report rationale based on scan completeness and finding severity. */
|
|
3155
|
+
function fixFirstWhy(result) {
|
|
3156
|
+
const counts = countBySeverity(result);
|
|
3157
|
+
if (result.partial || result.analysisStatus.discovery === "partial" || result.analysisStatus.rules === "partial") return "Why it matters: this scan is partial, so fix the visible risks but do not treat missing findings as proof of a clean suite.";
|
|
3158
|
+
if (counts.error > 0) return `Why it matters: ${counts.error} error finding${counts.error === 1 ? "" : "s"} can let a false-green or release-blocking test issue survive review.`;
|
|
3159
|
+
if (counts.warning > 0) return `Why it matters: ${counts.warning} warning finding${counts.warning === 1 ? "" : "s"} can turn into flaky triage or weak release confidence.`;
|
|
3160
|
+
return "Why it matters: these advisory findings do not gate CI, but they still mark places where the test signal is weaker than it looks.";
|
|
3161
|
+
}
|
|
3162
|
+
function appendNextActions(lines, display, fullResult, ui) {
|
|
3163
|
+
const partial = fullResult.partial || fullResult.analysisStatus.discovery === "partial" || fullResult.analysisStatus.rules === "partial";
|
|
3164
|
+
const findings = display.findings;
|
|
3165
|
+
if (findings.length === 0 && fullResult.score !== 100 && !partial) return;
|
|
3166
|
+
lines.push(sectionHeader("NEXT ACTIONS", ui));
|
|
3167
|
+
if (partial) pushWrapped(lines, ui.p, "Partial scan: do not trust this as a release gate yet. Fix scan coverage or rerun with a larger budget before treating the result as clean.", ui.width);
|
|
3168
|
+
if (findings.length > 0) {
|
|
3169
|
+
const first = findings[0];
|
|
3170
|
+
if (first === void 0) return;
|
|
3171
|
+
const loc = `${sanitizeData(first.file)}:${first.line}`;
|
|
3172
|
+
lines.push(nextStep(`mjolnir explain ${sanitizeData(first.ruleId)}`, ui));
|
|
3173
|
+
lines.push(nextStep(`mjolnir why ${loc}`, ui));
|
|
3174
|
+
pushWrapped(lines, ui.p, "Existing debt path: capture the current state once, then review only new or worse findings on future changes.", ui.width);
|
|
3175
|
+
lines.push(nextStep("mjolnir baseline", ui));
|
|
3176
|
+
lines.push(nextStep("mjolnir diff", ui));
|
|
3177
|
+
} else if (fullResult.score === 100) {
|
|
3178
|
+
pushWrapped(lines, ui.p, "Clean path: install the advisory PR workflow so new trust debt is caught before it reaches main.", ui.width);
|
|
3179
|
+
lines.push(nextStep("mjolnir ci install", ui));
|
|
3180
|
+
} else if (partial) lines.push(nextStep("mjolnir --verbose", ui));
|
|
3181
|
+
lines.push("");
|
|
3182
|
+
}
|
|
2956
3183
|
function evidenceTag$1(f) {
|
|
2957
3184
|
const level = f.evidenceLevel ?? deriveEvidenceLevel(f.findingType, f.confidence);
|
|
2958
3185
|
let tag = `${level} · ${level === "E2" ? "deterministic" : level === "E1" ? "heuristic" : "observation"}`;
|
|
@@ -3144,17 +3371,36 @@ function maxSeverity(findings) {
|
|
|
3144
3371
|
* VICTORY line: wordmark + the trophy retained inside, all in the
|
|
3145
3372
|
* forged gold-white pair. The halo hammer itself is the score
|
|
3146
3373
|
* instrument above — one mark, calmly (brand usage rule); ASCII mode
|
|
3147
|
-
*
|
|
3148
|
-
* empty-states/long-tail-arms).
|
|
3374
|
+
* labels the result as static rather than claiming a clean suite.
|
|
3149
3375
|
*/
|
|
3150
|
-
function appendForgedBlock(lines,
|
|
3376
|
+
function appendForgedBlock(lines, ui) {
|
|
3377
|
+
const { p, ascii } = ui;
|
|
3151
3378
|
lines.push("");
|
|
3152
|
-
if (ascii) lines.push(p.forged("***
|
|
3379
|
+
if (ascii) lines.push(p.forged("*** ZERO FINDINGS (STATIC) ***"));
|
|
3153
3380
|
else lines.push(` ${p.forged(FORGED_WORDMARK)}`);
|
|
3154
|
-
lines.push(p.forged(" FORGED — zero findings
|
|
3381
|
+
lines.push(p.forged(" FORGED — zero findings on the analyzed surface; runtime evidence is still required for higher trust."));
|
|
3155
3382
|
lines.push("");
|
|
3156
3383
|
lines.push(p.forged(TROPHY));
|
|
3157
3384
|
lines.push("");
|
|
3385
|
+
pushWrapped(lines, p, "Next: re-scan changed tests and keep the advisory CI workflow installed so new regressions are surfaced.", ui.width);
|
|
3386
|
+
lines.push(nextStep("mjolnir --scope changed", ui));
|
|
3387
|
+
lines.push("");
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Guidance for partial scans with zero findings and score 100.
|
|
3391
|
+
* A partial scan cannot claim a clean suite — the absence of findings
|
|
3392
|
+
* may be due to incomplete analysis, not actual cleanliness.
|
|
3393
|
+
*/
|
|
3394
|
+
function appendPartialCleanGuidance(lines, ui) {
|
|
3395
|
+
const { p } = ui;
|
|
3396
|
+
lines.push("");
|
|
3397
|
+
lines.push(p.warning(" ⚠ PARTIAL SCAN — no findings, but analysis was incomplete"));
|
|
3398
|
+
lines.push("");
|
|
3399
|
+
pushWrapped(lines, p, "This scan did not analyze the full test surface. Zero findings here does not mean the suite is clean — it means the scan was cut short.", ui.width);
|
|
3400
|
+
lines.push("");
|
|
3401
|
+
pushWrapped(lines, p, "Next action: fix the cause of the partial scan (timeouts, exclusions, parse failures), then re-run a complete scan before trusting the gate.", ui.width);
|
|
3402
|
+
lines.push(nextStep("mjolnir --scope changed", ui));
|
|
3403
|
+
lines.push("");
|
|
3158
3404
|
}
|
|
3159
3405
|
/**
|
|
3160
3406
|
* Pushes dimmed prose that respects the terminal width.
|
|
@@ -3336,7 +3582,7 @@ function topTrustRisks(findings, n = 3) {
|
|
|
3336
3582
|
function nextAction(result) {
|
|
3337
3583
|
if (result.partial) return "re-run with a higher --max-duration to close the truncated surface";
|
|
3338
3584
|
const risks = topTrustRisks(result.findings, 1);
|
|
3339
|
-
if (risks.length === 0) return result.testDeclarationCount === 0 ? "add tests — a repo without test declarations has nothing to verify" : "
|
|
3585
|
+
if (risks.length === 0) return result.testDeclarationCount === 0 ? "add tests — a repo without test declarations has nothing to verify" : "no findings on the analyzed surface — install advisory CI (`mjolnir ci install`) and re-scan the changed scope";
|
|
3340
3586
|
return `mjolnir explain ${risks[0]?.ruleId ?? ""} — then fix the top risk first`;
|
|
3341
3587
|
}
|
|
3342
3588
|
/**
|
|
@@ -4085,7 +4331,7 @@ async function runTrustReportCommand(argv, io) {
|
|
|
4085
4331
|
}
|
|
4086
4332
|
const targetArg = argv.find((a) => !a.startsWith("-")) ?? ".";
|
|
4087
4333
|
const target = resolve(targetArg);
|
|
4088
|
-
if (!validateTarget(target)) {
|
|
4334
|
+
if (!validateTarget$1(target)) {
|
|
4089
4335
|
io.err(`error: not a scannable target: ${targetArg}`);
|
|
4090
4336
|
return 10;
|
|
4091
4337
|
}
|
|
@@ -4112,7 +4358,7 @@ async function runTrustReportCommand(argv, io) {
|
|
|
4112
4358
|
return 20;
|
|
4113
4359
|
}
|
|
4114
4360
|
}
|
|
4115
|
-
function validateTarget(target) {
|
|
4361
|
+
function validateTarget$1(target) {
|
|
4116
4362
|
try {
|
|
4117
4363
|
return statSync(target).isDirectory();
|
|
4118
4364
|
} catch {
|
|
@@ -4978,7 +5224,7 @@ function renderDebt(result) {
|
|
|
4978
5224
|
lines.push(sectionHeader("TEST DEBT REGISTER", ui$16));
|
|
4979
5225
|
lines.push("");
|
|
4980
5226
|
if (classes.length === 0) {
|
|
4981
|
-
lines.push("No tracked debt classes found
|
|
5227
|
+
lines.push("No tracked debt classes found in the current register.");
|
|
4982
5228
|
return lines.join("\n");
|
|
4983
5229
|
}
|
|
4984
5230
|
const rows = ["DEBT CLASS", ""];
|
|
@@ -5733,7 +5979,7 @@ function tryReadPackageJson(rootDir) {
|
|
|
5733
5979
|
const p = join(rootDir, "package.json");
|
|
5734
5980
|
if (!existsSync(p)) return null;
|
|
5735
5981
|
try {
|
|
5736
|
-
return parseJsonFile(readFileSync(p, "utf8"), p, isRecord);
|
|
5982
|
+
return parseJsonFile(readFileSync(p, "utf8"), p, isRecord$2);
|
|
5737
5983
|
} catch {
|
|
5738
5984
|
return null;
|
|
5739
5985
|
}
|
|
@@ -7455,7 +7701,7 @@ function computeDetectorHashes(rules, rulesDir, retiredIds = []) {
|
|
|
7455
7701
|
return manifest;
|
|
7456
7702
|
}
|
|
7457
7703
|
function loadManifest(path) {
|
|
7458
|
-
return parseJsonFile(readFileSync(path, "utf8"), path, (v) => isRecord(v));
|
|
7704
|
+
return parseJsonFile(readFileSync(path, "utf8"), path, (v) => isRecord$2(v));
|
|
7459
7705
|
}
|
|
7460
7706
|
//#endregion
|
|
7461
7707
|
//#region src/commands/doctor.ts
|
|
@@ -7632,7 +7878,7 @@ function checkTierEnforcement(verdictsDir, rules = RULES) {
|
|
|
7632
7878
|
const lines = readFileSync(join(verdictsDir, f), "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
7633
7879
|
for (const line of lines) try {
|
|
7634
7880
|
const parsed = JSON.parse(line);
|
|
7635
|
-
if (!isRecord(parsed)) continue;
|
|
7881
|
+
if (!isRecord$2(parsed)) continue;
|
|
7636
7882
|
const entry = parsed;
|
|
7637
7883
|
if (entry.verdict === "TP" || entry.verdict === "FP") {
|
|
7638
7884
|
if (typeof entry.ruleId === "string") live.set(entry.ruleId, (live.get(entry.ruleId) ?? 0) + 1);
|
|
@@ -7740,7 +7986,7 @@ function checkFixtureIntegrity(fixturesRoot, rules = RULES) {
|
|
|
7740
7986
|
const allowlistPath = join(fixturesRoot, "typecheck-allowlist.json");
|
|
7741
7987
|
if (existsSync(allowlistPath)) try {
|
|
7742
7988
|
const raw = JSON.parse(readFileSync(allowlistPath, "utf8"));
|
|
7743
|
-
if (!isRecord(raw)) {
|
|
7989
|
+
if (!isRecord$2(raw)) {
|
|
7744
7990
|
ok = false;
|
|
7745
7991
|
details.push("typecheck-allowlist.json is not an object");
|
|
7746
7992
|
} else if (Array.isArray(raw.entries)) details.push(`Layer A typecheck allowlist: ${raw.entries.length} justified entr(ies)`);
|
|
@@ -7854,7 +8100,7 @@ function checkMeasurementConsistency(verdictsDir, sidecarPath, rules = RULES, me
|
|
|
7854
8100
|
let row;
|
|
7855
8101
|
try {
|
|
7856
8102
|
const parsed = JSON.parse(trimmed);
|
|
7857
|
-
if (!isRecord(parsed)) {
|
|
8103
|
+
if (!isRecord$2(parsed)) {
|
|
7858
8104
|
failures.push(`${f}: unparseable verdict row (corpus integrity)`);
|
|
7859
8105
|
continue;
|
|
7860
8106
|
}
|
|
@@ -7871,7 +8117,7 @@ function checkMeasurementConsistency(verdictsDir, sidecarPath, rules = RULES, me
|
|
|
7871
8117
|
let sidecar = {};
|
|
7872
8118
|
if (existsSync(sidecarPath)) try {
|
|
7873
8119
|
const parsed = JSON.parse(readFileSync(sidecarPath, "utf8"));
|
|
7874
|
-
if (!isRecord(parsed)) failures.push("sidecar is not a valid object — regenerate the measurement artifacts");
|
|
8120
|
+
if (!isRecord$2(parsed)) failures.push("sidecar is not a valid object — regenerate the measurement artifacts");
|
|
7875
8121
|
else sidecar = parsed;
|
|
7876
8122
|
} catch (e) {
|
|
7877
8123
|
failures.push(`sidecar unreadable/malformed: ${errorText(e)} — regenerate the measurement artifacts`);
|
|
@@ -10627,60 +10873,48 @@ function runAnalyzeCommand(argv, io) {
|
|
|
10627
10873
|
* gitlab — generate GitLab CI YAML
|
|
10628
10874
|
* jenkins — generate Jenkins Jenkinsfile Groovy
|
|
10629
10875
|
*/
|
|
10630
|
-
|
|
10631
|
-
|
|
10876
|
+
const SCAN_COMMAND = `npx --yes mjolnir-qa@${ENGINE_VERSION} --blocking error`;
|
|
10877
|
+
function generateGitHubWorkflow() {
|
|
10632
10878
|
return `name: QA Check
|
|
10633
10879
|
on: [push, pull_request]
|
|
10634
10880
|
jobs:
|
|
10635
10881
|
qa:
|
|
10636
10882
|
runs-on: ubuntu-latest
|
|
10637
10883
|
steps:
|
|
10638
|
-
- uses: actions/checkout@
|
|
10884
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
10639
10885
|
- name: Install dependencies
|
|
10640
10886
|
run: npm ci
|
|
10641
10887
|
- name: Run Mjölnir scan
|
|
10642
|
-
run:
|
|
10643
|
-
${findings.map((f) => ` - name: Report ${f.ruleId}\n run: echo "${f.message}"`).join("\n")}
|
|
10644
|
-
- name: Report result
|
|
10645
|
-
if: ${hasErrors ? "failure()" : "success()"}
|
|
10646
|
-
run: echo "Quality gate: ${hasErrors ? "FAILED" : "PASSED"}"
|
|
10888
|
+
run: ${SCAN_COMMAND}
|
|
10647
10889
|
`;
|
|
10648
10890
|
}
|
|
10649
|
-
function generateGitLabCi(
|
|
10891
|
+
function generateGitLabCi() {
|
|
10650
10892
|
return `stages:
|
|
10651
10893
|
- test
|
|
10652
10894
|
- qa
|
|
10653
10895
|
qa-check:
|
|
10654
10896
|
stage: qa
|
|
10655
10897
|
script:
|
|
10656
|
-
-
|
|
10657
|
-
- ${findings.some((f) => f.severity === "error") ? "echo \"Quality gate FAILED\" && exit 1" : "echo \"Quality gate PASSED\""}
|
|
10898
|
+
- ${SCAN_COMMAND}
|
|
10658
10899
|
rules:
|
|
10659
10900
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
10660
10901
|
`;
|
|
10661
10902
|
}
|
|
10662
|
-
function generateJenkinsfile(
|
|
10903
|
+
function generateJenkinsfile() {
|
|
10663
10904
|
return `pipeline {
|
|
10664
10905
|
agent any
|
|
10665
10906
|
stages {
|
|
10666
10907
|
stage('QA Check') {
|
|
10667
10908
|
steps {
|
|
10668
|
-
sh '
|
|
10669
|
-
sh 'echo "Quality gate: ${findings.some((f) => f.severity === "error") ? "FAILED" : "PASSED"}"'
|
|
10909
|
+
sh '${SCAN_COMMAND}'
|
|
10670
10910
|
}
|
|
10671
10911
|
}
|
|
10672
10912
|
}
|
|
10673
|
-
post {
|
|
10674
|
-
failure {
|
|
10675
|
-
echo 'QA findings detected — review required'
|
|
10676
|
-
}
|
|
10677
|
-
}
|
|
10678
10913
|
}
|
|
10679
10914
|
`;
|
|
10680
10915
|
}
|
|
10681
|
-
|
|
10916
|
+
function runCiAdapterCommand(argv, io) {
|
|
10682
10917
|
const adapter = argv[0] ?? "";
|
|
10683
|
-
const target = (argv.length >= 2 ? argv.slice(1).find((a) => !a.startsWith("-")) : void 0) ?? ".";
|
|
10684
10918
|
if (![
|
|
10685
10919
|
"github",
|
|
10686
10920
|
"gitlab",
|
|
@@ -10689,46 +10923,36 @@ async function runCiAdapterCommand(argv, io) {
|
|
|
10689
10923
|
io.err("Usage: mjolnir ci-adapter <github|gitlab|jenkins> [target]");
|
|
10690
10924
|
return 10;
|
|
10691
10925
|
}
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
findings = (await runScan({
|
|
10696
|
-
target,
|
|
10697
|
-
json: true,
|
|
10698
|
-
verbose: false,
|
|
10699
|
-
maxDurationMs: Number.POSITIVE_INFINITY,
|
|
10700
|
-
scopeChanged: false,
|
|
10701
|
-
format: "json",
|
|
10702
|
-
strict: false
|
|
10703
|
-
})).findings.map((f) => ({
|
|
10704
|
-
ruleId: f.ruleId,
|
|
10705
|
-
severity: f.severity,
|
|
10706
|
-
message: f.message
|
|
10707
|
-
}));
|
|
10708
|
-
} catch {
|
|
10709
|
-
findings.length = 0;
|
|
10926
|
+
if (argv.length > 2 || argv.slice(1).some((arg) => arg.startsWith("-"))) {
|
|
10927
|
+
io.err("Usage: mjolnir ci-adapter <github|gitlab|jenkins> [target]");
|
|
10928
|
+
return 10;
|
|
10710
10929
|
}
|
|
10930
|
+
const target = resolve(argv[1] ?? ".");
|
|
10711
10931
|
let output;
|
|
10712
10932
|
let filename;
|
|
10713
10933
|
switch (adapter) {
|
|
10714
10934
|
case "github":
|
|
10715
|
-
output = generateGitHubWorkflow(
|
|
10935
|
+
output = generateGitHubWorkflow();
|
|
10716
10936
|
filename = "qa-check.yml";
|
|
10717
10937
|
break;
|
|
10718
10938
|
case "gitlab":
|
|
10719
|
-
output = generateGitLabCi(
|
|
10939
|
+
output = generateGitLabCi();
|
|
10720
10940
|
filename = ".gitlab-ci.yml";
|
|
10721
10941
|
break;
|
|
10722
10942
|
case "jenkins":
|
|
10723
|
-
output = generateJenkinsfile(
|
|
10943
|
+
output = generateJenkinsfile();
|
|
10724
10944
|
filename = "Jenkinsfile";
|
|
10725
10945
|
break;
|
|
10726
10946
|
default: return 10;
|
|
10727
10947
|
}
|
|
10728
|
-
|
|
10948
|
+
try {
|
|
10949
|
+
writeFileSync(join(target, filename), output);
|
|
10950
|
+
} catch (error) {
|
|
10951
|
+
io.err(`Unable to write CI template: ${error instanceof Error ? error.message : String(error)}`);
|
|
10952
|
+
return 20;
|
|
10953
|
+
}
|
|
10729
10954
|
io.out(`CI ADAPTER: ${adapter.toUpperCase()}`);
|
|
10730
|
-
io.out(`Generated ${filename}`);
|
|
10731
|
-
io.out(`Findings processed: ${findings.length}`);
|
|
10955
|
+
io.out(`Generated ${join(target, filename)}`);
|
|
10732
10956
|
io.out(output);
|
|
10733
10957
|
return 0;
|
|
10734
10958
|
}
|
|
@@ -10787,7 +11011,7 @@ async function runDashboardCommand(argv, io) {
|
|
|
10787
11011
|
return 10;
|
|
10788
11012
|
}
|
|
10789
11013
|
try {
|
|
10790
|
-
const { runScan } = await import("./scan-pipeline-
|
|
11014
|
+
const { runScan } = await import("./scan-pipeline-C_Rs4u8N.mjs");
|
|
10791
11015
|
const result = await runScan({
|
|
10792
11016
|
target,
|
|
10793
11017
|
json: true,
|
|
@@ -11085,83 +11309,3382 @@ function runMaturityCommand(argv, io) {
|
|
|
11085
11309
|
return 10;
|
|
11086
11310
|
}
|
|
11087
11311
|
//#endregion
|
|
11088
|
-
//#region src/cli.ts
|
|
11312
|
+
//#region src/engine/cli-command-names.ts
|
|
11313
|
+
const CLI_COMMAND_NAMES = [
|
|
11314
|
+
"scan",
|
|
11315
|
+
"suppressions",
|
|
11316
|
+
"forensics",
|
|
11317
|
+
"triage",
|
|
11318
|
+
"mutation",
|
|
11319
|
+
"badge",
|
|
11320
|
+
"trust-report",
|
|
11321
|
+
"debt",
|
|
11322
|
+
"impact",
|
|
11323
|
+
"business-case",
|
|
11324
|
+
"release-report",
|
|
11325
|
+
"report",
|
|
11326
|
+
"trend",
|
|
11327
|
+
"exec-report",
|
|
11328
|
+
"policy",
|
|
11329
|
+
"quarantine",
|
|
11330
|
+
"analyze",
|
|
11331
|
+
"ci-adapter",
|
|
11332
|
+
"dashboard",
|
|
11333
|
+
"enterprise",
|
|
11334
|
+
"maturity",
|
|
11335
|
+
"baseline",
|
|
11336
|
+
"diff",
|
|
11337
|
+
"verify",
|
|
11338
|
+
"pr-comment",
|
|
11339
|
+
"summary",
|
|
11340
|
+
"stats",
|
|
11341
|
+
"fix",
|
|
11342
|
+
"create-rule",
|
|
11343
|
+
"handover",
|
|
11344
|
+
"init",
|
|
11345
|
+
"pw-report",
|
|
11346
|
+
"doctor",
|
|
11347
|
+
"release-trust",
|
|
11348
|
+
"rules",
|
|
11349
|
+
"explain",
|
|
11350
|
+
"doctor:playwright",
|
|
11351
|
+
"why",
|
|
11352
|
+
"handoff",
|
|
11353
|
+
"install",
|
|
11354
|
+
"ci-integrity",
|
|
11355
|
+
"framework-maturity",
|
|
11356
|
+
"suppression-gate",
|
|
11357
|
+
"cross-file",
|
|
11358
|
+
"contract-verify",
|
|
11359
|
+
"trust-trend",
|
|
11360
|
+
"evidence-graph",
|
|
11361
|
+
"ci",
|
|
11362
|
+
"mcp",
|
|
11363
|
+
"help"
|
|
11364
|
+
];
|
|
11365
|
+
const CLI_NON_SCAN_COMMANDS = new Set(CLI_COMMAND_NAMES.filter((command) => command !== "scan"));
|
|
11366
|
+
//#endregion
|
|
11367
|
+
//#region src/engine/cross-file.ts
|
|
11368
|
+
const JS_TEST_NAME_RE = /\b(?:test|it)\s*\(\s*['"`]([^'"`]+)['"`]\s*,/g;
|
|
11369
|
+
const PY_TEST_NAME_RE = /^def\s+(test_\w+)\s*\(/gm;
|
|
11370
|
+
/** Collect declared test names from one file's text. */
|
|
11371
|
+
function collectTestNames(path, text) {
|
|
11372
|
+
const names = [];
|
|
11373
|
+
if (path.endsWith(".py")) {
|
|
11374
|
+
let m;
|
|
11375
|
+
const re = new RegExp(PY_TEST_NAME_RE.source, "gm");
|
|
11376
|
+
while ((m = re.exec(text)) !== null) names.push(m[1]);
|
|
11377
|
+
return names;
|
|
11378
|
+
}
|
|
11379
|
+
if (/\.(?:spec|test)\.[tj]sx?$/.test(path)) {
|
|
11380
|
+
let m;
|
|
11381
|
+
const re = new RegExp(JS_TEST_NAME_RE.source, "g");
|
|
11382
|
+
while ((m = re.exec(text)) !== null) names.push(m[1]);
|
|
11383
|
+
}
|
|
11384
|
+
return names;
|
|
11385
|
+
}
|
|
11386
|
+
/** Find test names declared in more than one file. */
|
|
11387
|
+
function findDuplicateTestNames(files) {
|
|
11388
|
+
const byName = /* @__PURE__ */ new Map();
|
|
11389
|
+
for (const { path, text } of files) for (const name of collectTestNames(path, text)) {
|
|
11390
|
+
const set = byName.get(name) ?? /* @__PURE__ */ new Set();
|
|
11391
|
+
set.add(path);
|
|
11392
|
+
byName.set(name, set);
|
|
11393
|
+
}
|
|
11394
|
+
const out = [];
|
|
11395
|
+
for (const [name, filesSet] of byName) if (filesSet.size > 1) out.push({
|
|
11396
|
+
name,
|
|
11397
|
+
files: [...filesSet].sort()
|
|
11398
|
+
});
|
|
11399
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
11400
|
+
}
|
|
11401
|
+
//#endregion
|
|
11402
|
+
//#region src/engine/cross-file-analysis.ts
|
|
11403
|
+
/**
|
|
11404
|
+
* Analyze cross-file signals across the scanned test files.
|
|
11405
|
+
* Pure function — same input always produces same output.
|
|
11406
|
+
*/
|
|
11407
|
+
function analyzeCrossFileSignals(files, findings, root) {
|
|
11408
|
+
const orderedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
11409
|
+
const duplicateTestNames = findDuplicateTestNames(orderedFiles).map((dup) => ({
|
|
11410
|
+
type: "duplicate-test-name",
|
|
11411
|
+
files: dup.files,
|
|
11412
|
+
message: `Duplicate test name "${dup.name}" found in ${dup.files.length} files`,
|
|
11413
|
+
severity: "warning"
|
|
11414
|
+
}));
|
|
11415
|
+
const sharedImports = detectSharedImports(orderedFiles).map((imp) => ({
|
|
11416
|
+
type: "shared-import",
|
|
11417
|
+
files: imp.files,
|
|
11418
|
+
message: imp.message,
|
|
11419
|
+
severity: "info"
|
|
11420
|
+
}));
|
|
11421
|
+
const circularDependencies = detectCircularDependencies(orderedFiles).map((circ) => ({
|
|
11422
|
+
type: "circular-dep",
|
|
11423
|
+
files: circ.files,
|
|
11424
|
+
message: circ.message,
|
|
11425
|
+
severity: "warning"
|
|
11426
|
+
}));
|
|
11427
|
+
const correlationConclusions = correlateFindings(findings).map((c) => `${c.conclusionType}: ${c.corroboration}`).join("; ");
|
|
11428
|
+
const graph = buildDependencyGraph(root);
|
|
11429
|
+
const reachableFiles = getReachableFiles(orderedFiles.map((f) => f.path), graph);
|
|
11430
|
+
return {
|
|
11431
|
+
signals: [
|
|
11432
|
+
...duplicateTestNames,
|
|
11433
|
+
...sharedImports,
|
|
11434
|
+
...circularDependencies
|
|
11435
|
+
],
|
|
11436
|
+
duplicateTestNames,
|
|
11437
|
+
sharedImports,
|
|
11438
|
+
circularDependencies,
|
|
11439
|
+
correlationConclusions,
|
|
11440
|
+
dependencyGraphSize: graph.size,
|
|
11441
|
+
reachableFilesCount: reachableFiles.length
|
|
11442
|
+
};
|
|
11443
|
+
}
|
|
11444
|
+
function detectSharedImports(files) {
|
|
11445
|
+
const importByModule = /* @__PURE__ */ new Map();
|
|
11446
|
+
for (const { path, text } of files) {
|
|
11447
|
+
const importRegex = /\b(?:from\s+|import\s*)['"]([^'"]+)['"]/g;
|
|
11448
|
+
let match;
|
|
11449
|
+
while ((match = importRegex.exec(text)) !== null) {
|
|
11450
|
+
const module = match[1];
|
|
11451
|
+
const set = importByModule.get(module) ?? /* @__PURE__ */ new Set();
|
|
11452
|
+
set.add(path);
|
|
11453
|
+
importByModule.set(module, set);
|
|
11454
|
+
}
|
|
11455
|
+
}
|
|
11456
|
+
const out = [];
|
|
11457
|
+
for (const [module, paths] of importByModule) if (paths.size > 1) out.push({
|
|
11458
|
+
files: [...paths].sort(),
|
|
11459
|
+
message: `Module "${module}" imported by ${paths.size} files`
|
|
11460
|
+
});
|
|
11461
|
+
return out.sort((a, b) => a.message.localeCompare(b.message));
|
|
11462
|
+
}
|
|
11463
|
+
function detectCircularDependencies(files) {
|
|
11464
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
11465
|
+
const filePaths = new Set(files.map((f) => f.path));
|
|
11466
|
+
for (const { path: filePath, text } of files) {
|
|
11467
|
+
const imports = [];
|
|
11468
|
+
const importRegex = /\b(?:from\s+|import\s*)['"]([^'"]+)['"]/g;
|
|
11469
|
+
let match;
|
|
11470
|
+
while ((match = importRegex.exec(text)) !== null) {
|
|
11471
|
+
const imported = match[1];
|
|
11472
|
+
let resolved = imported;
|
|
11473
|
+
if (imported.startsWith("./") || imported.startsWith("../")) resolved = filePath.substring(0, filePath.lastIndexOf("/") + 1) + imported.replace(/^\.\//, "");
|
|
11474
|
+
if (!filePaths.has(resolved)) {
|
|
11475
|
+
for (const ext of [
|
|
11476
|
+
".ts",
|
|
11477
|
+
".tsx",
|
|
11478
|
+
".js",
|
|
11479
|
+
".jsx"
|
|
11480
|
+
]) if (filePaths.has(resolved + ext)) {
|
|
11481
|
+
resolved = resolved + ext;
|
|
11482
|
+
break;
|
|
11483
|
+
}
|
|
11484
|
+
}
|
|
11485
|
+
if (filePaths.has(resolved) || filePaths.has(imported)) imports.push(filePaths.has(resolved) ? resolved : imported);
|
|
11486
|
+
}
|
|
11487
|
+
importMap.set(filePath, imports);
|
|
11488
|
+
}
|
|
11489
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11490
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
11491
|
+
const cycles = [];
|
|
11492
|
+
function dfs(node, path) {
|
|
11493
|
+
if (recursionStack.has(node)) {
|
|
11494
|
+
const cycleStart = path.indexOf(node);
|
|
11495
|
+
if (cycleStart !== -1) cycles.push(path.slice(cycleStart).concat(node));
|
|
11496
|
+
return;
|
|
11497
|
+
}
|
|
11498
|
+
if (visited.has(node)) return;
|
|
11499
|
+
visited.add(node);
|
|
11500
|
+
recursionStack.add(node);
|
|
11501
|
+
path.push(node);
|
|
11502
|
+
const deps = importMap.get(node) ?? [];
|
|
11503
|
+
for (const dep of deps) dfs(dep, path);
|
|
11504
|
+
path.pop();
|
|
11505
|
+
recursionStack.delete(node);
|
|
11506
|
+
}
|
|
11507
|
+
for (const node of importMap.keys()) if (!visited.has(node)) dfs(node, []);
|
|
11508
|
+
return cycles.map((cycle) => ({
|
|
11509
|
+
files: cycle,
|
|
11510
|
+
message: `Circular dependency detected: ${cycle.join(" → ")}`
|
|
11511
|
+
}));
|
|
11512
|
+
}
|
|
11513
|
+
function renderCrossFileAnalysis(result) {
|
|
11514
|
+
const lines = [];
|
|
11515
|
+
lines.push("Cross-File Analysis Report");
|
|
11516
|
+
lines.push(`Signals detected: ${result.signals.length}`);
|
|
11517
|
+
lines.push(`Duplicate test names: ${result.duplicateTestNames.length}`);
|
|
11518
|
+
lines.push(`Shared imports: ${result.sharedImports.length}`);
|
|
11519
|
+
lines.push(`Circular dependencies: ${result.circularDependencies.length}`);
|
|
11520
|
+
lines.push(`Dependency graph size: ${result.dependencyGraphSize}`);
|
|
11521
|
+
lines.push(`Reachable files: ${result.reachableFilesCount}`);
|
|
11522
|
+
lines.push("");
|
|
11523
|
+
if (result.signals.length > 0) {
|
|
11524
|
+
lines.push("Signals:");
|
|
11525
|
+
for (const signal of result.signals) {
|
|
11526
|
+
const icon = signal.severity === "warning" ? "⚠" : "ℹ";
|
|
11527
|
+
lines.push(` ${icon} [${signal.type}] ${signal.message}`);
|
|
11528
|
+
}
|
|
11529
|
+
lines.push("");
|
|
11530
|
+
}
|
|
11531
|
+
if (result.correlationConclusions) {
|
|
11532
|
+
lines.push("Correlation Conclusions:");
|
|
11533
|
+
lines.push(` ${result.correlationConclusions}`);
|
|
11534
|
+
lines.push("");
|
|
11535
|
+
}
|
|
11536
|
+
return lines.join("\n").trimEnd();
|
|
11537
|
+
}
|
|
11538
|
+
//#endregion
|
|
11539
|
+
//#region src/engine/evidence-graph.ts
|
|
11540
|
+
/**
|
|
11541
|
+
* Build a complete evidence graph from a scan result.
|
|
11542
|
+
* Pure function — deterministic output for the same input.
|
|
11543
|
+
*/
|
|
11544
|
+
function buildEvidenceGraphFromScan(result, runIdentity, files) {
|
|
11545
|
+
const nodes = [];
|
|
11546
|
+
const edges = [];
|
|
11547
|
+
const repoNode = {
|
|
11548
|
+
id: `repo:${runIdentity.inputFingerprint}`,
|
|
11549
|
+
type: "repository",
|
|
11550
|
+
label: "Scanned Repository",
|
|
11551
|
+
properties: {
|
|
11552
|
+
scanId: runIdentity.scanId,
|
|
11553
|
+
engineVersion: runIdentity.engineVersion
|
|
11554
|
+
}
|
|
11555
|
+
};
|
|
11556
|
+
nodes.push(repoNode);
|
|
11557
|
+
const runNode = {
|
|
11558
|
+
id: `run:${runIdentity.scanId}`,
|
|
11559
|
+
type: "artifact",
|
|
11560
|
+
label: "Scan Run",
|
|
11561
|
+
properties: {
|
|
11562
|
+
scanId: runIdentity.scanId,
|
|
11563
|
+
rulesDigest: runIdentity.rulesDigest,
|
|
11564
|
+
configFingerprint: runIdentity.configFingerprint
|
|
11565
|
+
}
|
|
11566
|
+
};
|
|
11567
|
+
nodes.push(runNode);
|
|
11568
|
+
edges.push({
|
|
11569
|
+
source: repoNode.id,
|
|
11570
|
+
target: runNode.id,
|
|
11571
|
+
type: "CONTAINS",
|
|
11572
|
+
properties: {}
|
|
11573
|
+
});
|
|
11574
|
+
const fileNodes = /* @__PURE__ */ new Map();
|
|
11575
|
+
for (const finding of result.findings) {
|
|
11576
|
+
if (!fileNodes.has(finding.file)) {
|
|
11577
|
+
const fileNode = {
|
|
11578
|
+
id: `file:${finding.file}`,
|
|
11579
|
+
type: "file",
|
|
11580
|
+
label: finding.file,
|
|
11581
|
+
properties: { path: finding.file }
|
|
11582
|
+
};
|
|
11583
|
+
fileNodes.set(finding.file, fileNode);
|
|
11584
|
+
nodes.push(fileNode);
|
|
11585
|
+
edges.push({
|
|
11586
|
+
source: repoNode.id,
|
|
11587
|
+
target: fileNode.id,
|
|
11588
|
+
type: "CONTAINS",
|
|
11589
|
+
properties: {}
|
|
11590
|
+
});
|
|
11591
|
+
}
|
|
11592
|
+
const findingNode = {
|
|
11593
|
+
id: `finding:${finding.findingId ?? `${finding.ruleId}:${finding.file}:${finding.line}`}`,
|
|
11594
|
+
type: "finding",
|
|
11595
|
+
label: `${finding.ruleId}: ${finding.message}`,
|
|
11596
|
+
properties: {
|
|
11597
|
+
ruleId: finding.ruleId,
|
|
11598
|
+
severity: finding.severity,
|
|
11599
|
+
evidenceLevel: finding.evidenceLevel,
|
|
11600
|
+
trustLevel: finding.trustLevel,
|
|
11601
|
+
confidence: finding.confidence,
|
|
11602
|
+
findingType: finding.findingType
|
|
11603
|
+
}
|
|
11604
|
+
};
|
|
11605
|
+
nodes.push(findingNode);
|
|
11606
|
+
edges.push({
|
|
11607
|
+
source: fileNodes.get(finding.file)?.id ?? "",
|
|
11608
|
+
target: findingNode.id,
|
|
11609
|
+
type: "CONTAINS",
|
|
11610
|
+
properties: {}
|
|
11611
|
+
});
|
|
11612
|
+
edges.push({
|
|
11613
|
+
source: runNode.id,
|
|
11614
|
+
target: findingNode.id,
|
|
11615
|
+
type: "PRODUCED_BY",
|
|
11616
|
+
properties: { detectorRevision: finding.detectorRevision }
|
|
11617
|
+
});
|
|
11618
|
+
}
|
|
11619
|
+
const evidenceCounts = {
|
|
11620
|
+
total: result.findings.length,
|
|
11621
|
+
failed: result.findings.filter((f) => f.severity === "error").length,
|
|
11622
|
+
flaky: 0,
|
|
11623
|
+
skipped: 0,
|
|
11624
|
+
timedOut: 0
|
|
11625
|
+
};
|
|
11626
|
+
const filesForProvenance = files.map((file) => ({
|
|
11627
|
+
path: file.path,
|
|
11628
|
+
provenance: classifyProvenance(file)
|
|
11629
|
+
}));
|
|
11630
|
+
const agenticProfile = computeAgenticProfile(filesForProvenance, result.findings);
|
|
11631
|
+
return {
|
|
11632
|
+
graph: buildEvidenceGraph({
|
|
11633
|
+
runId: runIdentity,
|
|
11634
|
+
source: "scan"
|
|
11635
|
+
}),
|
|
11636
|
+
nodes,
|
|
11637
|
+
edges,
|
|
11638
|
+
agenticProfile,
|
|
11639
|
+
evidenceCounts,
|
|
11640
|
+
provenanceSummary: {
|
|
11641
|
+
totalFiles: filesForProvenance.length,
|
|
11642
|
+
generatedMarkedFiles: agenticProfile.generatedMarkedFiles,
|
|
11643
|
+
codegenLikeFiles: agenticProfile.codegenLikeFiles,
|
|
11644
|
+
shareMarkedGenerated: agenticProfile.shareMarkedGenerated
|
|
11645
|
+
}
|
|
11646
|
+
};
|
|
11647
|
+
}
|
|
11089
11648
|
/**
|
|
11090
|
-
*
|
|
11091
|
-
* Exit codes (§24.1, frozen): 0 clean · 1 findings ≥ gate · 2 partial ·
|
|
11092
|
-
* 10 usage error · 20 internal error.
|
|
11649
|
+
* Query the evidence graph for findings related to a specific file.
|
|
11093
11650
|
*/
|
|
11651
|
+
function queryFindingsByFile(result, filePath) {
|
|
11652
|
+
return result.findings.filter((f) => f.file === filePath);
|
|
11653
|
+
}
|
|
11094
11654
|
/**
|
|
11095
|
-
*
|
|
11096
|
-
*
|
|
11097
|
-
* A literal, not a package.json read: the shipped artifact is a single
|
|
11098
|
-
* bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
|
|
11099
|
-
* where the file happens to sit after install. This follows the same
|
|
11100
|
-
* discipline as SARIF's `driver.version` — kept in sync by
|
|
11101
|
-
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
11102
|
-
* `tests/version-consistency.spec.ts` locally. R4c moved the literal to
|
|
11103
|
-
* src/engine/version.ts (a leaf module) so the scan pipeline's run
|
|
11104
|
-
* identity can carry it without a cli.ts import cycle; this re-export
|
|
11105
|
-
* keeps every existing consumer stable.
|
|
11655
|
+
* Query the evidence graph for findings by rule ID.
|
|
11106
11656
|
*/
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
};
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
};
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11657
|
+
function queryFindingsByRule(result, ruleId) {
|
|
11658
|
+
return result.findings.filter((f) => f.ruleId === ruleId);
|
|
11659
|
+
}
|
|
11660
|
+
function renderEvidenceGraphResult(result) {
|
|
11661
|
+
const lines = [];
|
|
11662
|
+
lines.push("Evidence Graph Report");
|
|
11663
|
+
lines.push(`Nodes: ${result.nodes.length}`);
|
|
11664
|
+
lines.push(`Edges: ${result.edges.length}`);
|
|
11665
|
+
lines.push(`Total Findings: ${result.nodes.filter((n) => n.type === "finding").length}`);
|
|
11666
|
+
lines.push(`Run ID: ${result.graph.runId?.scanId ?? "N/A"}`);
|
|
11667
|
+
lines.push("");
|
|
11668
|
+
lines.push("Evidence Counts:");
|
|
11669
|
+
lines.push(` Total: ${result.evidenceCounts.total}`);
|
|
11670
|
+
lines.push(` Failed: ${result.evidenceCounts.failed}`);
|
|
11671
|
+
lines.push(` Flaky: ${result.evidenceCounts.flaky}`);
|
|
11672
|
+
lines.push(` Skipped: ${result.evidenceCounts.skipped}`);
|
|
11673
|
+
lines.push(` Timed Out: ${result.evidenceCounts.timedOut}`);
|
|
11674
|
+
lines.push("");
|
|
11675
|
+
lines.push("Provenance Summary:");
|
|
11676
|
+
lines.push(` Total files: ${result.provenanceSummary.totalFiles}`);
|
|
11677
|
+
lines.push(` Generated-marked: ${result.provenanceSummary.generatedMarkedFiles}`);
|
|
11678
|
+
lines.push(` Codegen-like: ${result.provenanceSummary.codegenLikeFiles}`);
|
|
11679
|
+
lines.push(` Share marked generated: ${(result.provenanceSummary.shareMarkedGenerated * 100).toFixed(1)}%`);
|
|
11680
|
+
lines.push("");
|
|
11681
|
+
lines.push("Agentic Profile Note:");
|
|
11682
|
+
lines.push(` ${result.agenticProfile.note}`);
|
|
11683
|
+
return lines.join("\n").trimEnd();
|
|
11684
|
+
}
|
|
11685
|
+
//#endregion
|
|
11686
|
+
//#region src/frameworks/framework-inventory.ts
|
|
11687
|
+
const FRAMEWORK_INVENTORY = [
|
|
11688
|
+
{
|
|
11689
|
+
frameworkId: "playwright",
|
|
11690
|
+
entityType: "E2E_FRAMEWORK",
|
|
11691
|
+
language: "TypeScript/JavaScript",
|
|
11692
|
+
maturity: "F4",
|
|
11693
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11694
|
+
targetMaturity: "F5",
|
|
11695
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11696
|
+
validatedVersions: [
|
|
11697
|
+
"1.44",
|
|
11698
|
+
"1.45",
|
|
11699
|
+
"1.46",
|
|
11700
|
+
"1.47",
|
|
11701
|
+
"1.48"
|
|
11702
|
+
],
|
|
11703
|
+
keyGapForNextLevel: "GAP-PW-006 parallelism/worker safety analysis"
|
|
11704
|
+
},
|
|
11705
|
+
{
|
|
11706
|
+
frameworkId: "jest",
|
|
11707
|
+
entityType: "TEST_FRAMEWORK",
|
|
11708
|
+
language: "TypeScript/JavaScript",
|
|
11709
|
+
maturity: "F3",
|
|
11710
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11711
|
+
targetMaturity: "F4",
|
|
11712
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11713
|
+
validatedVersions: ["29", "30"],
|
|
11714
|
+
keyGapForNextLevel: "GAP-JEST-004 lifecycle modeling"
|
|
11715
|
+
},
|
|
11716
|
+
{
|
|
11717
|
+
frameworkId: "vitest",
|
|
11718
|
+
entityType: "TEST_FRAMEWORK",
|
|
11719
|
+
language: "TypeScript/JavaScript",
|
|
11720
|
+
maturity: "F3",
|
|
11721
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11722
|
+
targetMaturity: "F4",
|
|
11723
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11724
|
+
validatedVersions: [
|
|
11725
|
+
"1.6",
|
|
11726
|
+
"2.0",
|
|
11727
|
+
"2.1"
|
|
11728
|
+
],
|
|
11729
|
+
keyGapForNextLevel: "GAP-VIT-004 lifecycle modeling"
|
|
11730
|
+
},
|
|
11731
|
+
{
|
|
11732
|
+
frameworkId: "pytest",
|
|
11733
|
+
entityType: "TEST_FRAMEWORK",
|
|
11734
|
+
language: "Python",
|
|
11735
|
+
maturity: "F2",
|
|
11736
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11737
|
+
targetMaturity: "F3",
|
|
11738
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11739
|
+
validatedVersions: [
|
|
11740
|
+
"7.4",
|
|
11741
|
+
"8.0",
|
|
11742
|
+
"8.1",
|
|
11743
|
+
"8.2",
|
|
11744
|
+
"8.3"
|
|
11745
|
+
],
|
|
11746
|
+
keyGapForNextLevel: "GAP-PY-004 lifecycle modeling"
|
|
11747
|
+
},
|
|
11748
|
+
{
|
|
11749
|
+
frameworkId: "junit",
|
|
11750
|
+
entityType: "TEST_FRAMEWORK",
|
|
11751
|
+
language: "Java",
|
|
11752
|
+
maturity: "F2",
|
|
11753
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11754
|
+
targetMaturity: "F3",
|
|
11755
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11756
|
+
validatedVersions: ["5.10", "5.11"],
|
|
11757
|
+
keyGapForNextLevel: "GAP-JU-004 lifecycle modeling"
|
|
11758
|
+
},
|
|
11759
|
+
{
|
|
11760
|
+
frameworkId: "nunit",
|
|
11761
|
+
entityType: "TEST_FRAMEWORK",
|
|
11762
|
+
language: "C#",
|
|
11763
|
+
maturity: "F2",
|
|
11764
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11765
|
+
targetMaturity: "F3",
|
|
11766
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11767
|
+
validatedVersions: ["3.14", "4.2"],
|
|
11768
|
+
keyGapForNextLevel: "GAP-NU-004 lifecycle modeling"
|
|
11769
|
+
},
|
|
11770
|
+
{
|
|
11771
|
+
frameworkId: "xunit",
|
|
11772
|
+
entityType: "TEST_FRAMEWORK",
|
|
11773
|
+
language: "C#",
|
|
11774
|
+
maturity: "F2",
|
|
11775
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11776
|
+
targetMaturity: "F3",
|
|
11777
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11778
|
+
validatedVersions: ["2.9"],
|
|
11779
|
+
keyGapForNextLevel: "GAP-XU-004 lifecycle modeling"
|
|
11780
|
+
},
|
|
11781
|
+
{
|
|
11782
|
+
frameworkId: "cypress",
|
|
11783
|
+
entityType: "E2E_FRAMEWORK",
|
|
11784
|
+
language: "TypeScript/JavaScript",
|
|
11785
|
+
maturity: "F2",
|
|
11786
|
+
supportStatus: "EXPERIMENTAL",
|
|
11787
|
+
targetMaturity: "F3",
|
|
11788
|
+
targetSupportStatus: "OFFICIAL_PARTIAL",
|
|
11789
|
+
validatedVersions: ["13"],
|
|
11790
|
+
keyGapForNextLevel: "GAP-CY-004 lifecycle modeling"
|
|
11791
|
+
},
|
|
11792
|
+
{
|
|
11793
|
+
frameworkId: "selenium",
|
|
11794
|
+
entityType: "AUTOMATION_LIBRARY",
|
|
11795
|
+
language: "Java/TypeScript/Python",
|
|
11796
|
+
maturity: "F1",
|
|
11797
|
+
supportStatus: "EXPERIMENTAL",
|
|
11798
|
+
targetMaturity: "F2",
|
|
11799
|
+
targetSupportStatus: "OFFICIAL_PARTIAL",
|
|
11800
|
+
validatedVersions: [
|
|
11801
|
+
"4.20",
|
|
11802
|
+
"4.21",
|
|
11803
|
+
"4.22",
|
|
11804
|
+
"4.23",
|
|
11805
|
+
"4.24",
|
|
11806
|
+
"4.25"
|
|
11807
|
+
],
|
|
11808
|
+
keyGapForNextLevel: "GAP-SE-002 AST-based usage analysis"
|
|
11809
|
+
},
|
|
11810
|
+
{
|
|
11811
|
+
frameworkId: "testng",
|
|
11812
|
+
entityType: "TEST_FRAMEWORK",
|
|
11813
|
+
language: "Java",
|
|
11814
|
+
maturity: "F1",
|
|
11815
|
+
supportStatus: "EXPERIMENTAL",
|
|
11816
|
+
targetMaturity: "F2",
|
|
11817
|
+
targetSupportStatus: "OFFICIAL_PARTIAL",
|
|
11818
|
+
validatedVersions: ["7.10", "7.11"],
|
|
11819
|
+
keyGapForNextLevel: "GAP-TN-002 AST-based usage analysis"
|
|
11820
|
+
},
|
|
11821
|
+
{
|
|
11822
|
+
frameworkId: "github-actions",
|
|
11823
|
+
entityType: "CI_PROVIDER",
|
|
11824
|
+
language: "YAML",
|
|
11825
|
+
maturity: "F3",
|
|
11826
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11827
|
+
targetMaturity: "F4",
|
|
11828
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11829
|
+
validatedVersions: ["v4"],
|
|
11830
|
+
keyGapForNextLevel: "GAP-GHA-004 advanced matrix strategy modeling"
|
|
11831
|
+
},
|
|
11832
|
+
{
|
|
11833
|
+
frameworkId: "azure-devops",
|
|
11834
|
+
entityType: "CI_PROVIDER",
|
|
11835
|
+
language: "YAML",
|
|
11836
|
+
maturity: "F3",
|
|
11837
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11838
|
+
targetMaturity: "F4",
|
|
11839
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11840
|
+
validatedVersions: ["2024"],
|
|
11841
|
+
keyGapForNextLevel: "GAP-AZ-004 stage dependency graph modeling"
|
|
11842
|
+
},
|
|
11843
|
+
{
|
|
11844
|
+
frameworkId: "jenkins",
|
|
11845
|
+
entityType: "CI_PROVIDER",
|
|
11846
|
+
language: "Groovy/YAML",
|
|
11847
|
+
maturity: "F3",
|
|
11848
|
+
supportStatus: "OFFICIAL_PARTIAL",
|
|
11849
|
+
targetMaturity: "F4",
|
|
11850
|
+
targetSupportStatus: "OFFICIAL_FULL",
|
|
11851
|
+
validatedVersions: ["2.440", "2.450"],
|
|
11852
|
+
keyGapForNextLevel: "GAP-JK-004 declarative pipeline advanced features"
|
|
11853
|
+
},
|
|
11854
|
+
{
|
|
11855
|
+
frameworkId: "gitlab-ci",
|
|
11856
|
+
entityType: "CI_PROVIDER",
|
|
11857
|
+
language: "YAML",
|
|
11858
|
+
maturity: "F0",
|
|
11859
|
+
supportStatus: "UNSUPPORTED",
|
|
11860
|
+
targetMaturity: "F1",
|
|
11861
|
+
targetSupportStatus: "DISCOVERED",
|
|
11862
|
+
validatedVersions: [],
|
|
11863
|
+
keyGapForNextLevel: "GAP-GL-001 basic pipeline discovery"
|
|
11864
|
+
}
|
|
11865
|
+
];
|
|
11866
|
+
function getFrameworkById(frameworkId) {
|
|
11867
|
+
return FRAMEWORK_INVENTORY.find((f) => f.frameworkId === frameworkId);
|
|
11868
|
+
}
|
|
11869
|
+
//#endregion
|
|
11870
|
+
//#region src/frameworks/scorecard.ts
|
|
11871
|
+
const SCORECARD_DIMENSIONS = [
|
|
11872
|
+
"discovery",
|
|
11873
|
+
"astUsage",
|
|
11874
|
+
"semanticUnderstanding",
|
|
11875
|
+
"assertionUnderstanding",
|
|
11876
|
+
"lifecycleModeling",
|
|
11877
|
+
"asyncSemantics",
|
|
11878
|
+
"fixtureModeling",
|
|
11879
|
+
"mockingSemantics",
|
|
11880
|
+
"sharedStateAnalysis",
|
|
11881
|
+
"errorClassification",
|
|
11882
|
+
"retrySemantics",
|
|
11883
|
+
"parameterization",
|
|
11884
|
+
"snapshotSemantics",
|
|
11885
|
+
"parallelismWorkerSafety",
|
|
11886
|
+
"runtimeEvidence",
|
|
11887
|
+
"adversarialCorpus",
|
|
11888
|
+
"knownLimitationCoverage",
|
|
11889
|
+
"ciIntegration",
|
|
11890
|
+
"reportingQuality",
|
|
11891
|
+
"configDiscovery",
|
|
11892
|
+
"dependencyGraph",
|
|
11893
|
+
"versionCompatibility",
|
|
11894
|
+
"documentationCoverage",
|
|
11895
|
+
"migrationGuidance",
|
|
11896
|
+
"performanceProfiling",
|
|
11897
|
+
"securityAuditing",
|
|
11898
|
+
"crossFrameworkInterop"
|
|
11899
|
+
];
|
|
11900
|
+
function makeScorecard(frameworkId, overrides) {
|
|
11901
|
+
return {
|
|
11902
|
+
frameworkId,
|
|
11903
|
+
entries: SCORECARD_DIMENSIONS.map((dim) => {
|
|
11904
|
+
const base = {
|
|
11905
|
+
dimension: dim,
|
|
11906
|
+
current: "NOT_APPLICABLE",
|
|
11907
|
+
target: "NOT_APPLICABLE",
|
|
11908
|
+
gapId: null
|
|
11909
|
+
};
|
|
11910
|
+
const ov = overrides[dim];
|
|
11911
|
+
return ov ? {
|
|
11912
|
+
...base,
|
|
11913
|
+
...ov
|
|
11914
|
+
} : base;
|
|
11915
|
+
})
|
|
11916
|
+
};
|
|
11917
|
+
}
|
|
11918
|
+
const COMMON_MATURE_OVERRIDES = {
|
|
11919
|
+
reportingQuality: {
|
|
11920
|
+
current: "GOOD",
|
|
11921
|
+
target: "EXCELLENT"
|
|
11922
|
+
},
|
|
11923
|
+
configDiscovery: {
|
|
11924
|
+
current: "GOOD",
|
|
11925
|
+
target: "EXCELLENT"
|
|
11926
|
+
},
|
|
11927
|
+
dependencyGraph: {
|
|
11928
|
+
current: "PARTIAL",
|
|
11929
|
+
target: "GOOD"
|
|
11930
|
+
},
|
|
11931
|
+
versionCompatibility: {
|
|
11932
|
+
current: "GOOD",
|
|
11933
|
+
target: "EXCELLENT"
|
|
11934
|
+
},
|
|
11935
|
+
documentationCoverage: {
|
|
11936
|
+
current: "PARTIAL",
|
|
11937
|
+
target: "GOOD"
|
|
11938
|
+
},
|
|
11939
|
+
migrationGuidance: {
|
|
11940
|
+
current: "PARTIAL",
|
|
11941
|
+
target: "GOOD"
|
|
11942
|
+
},
|
|
11943
|
+
performanceProfiling: {
|
|
11944
|
+
current: "MISSING",
|
|
11945
|
+
target: "PARTIAL"
|
|
11946
|
+
},
|
|
11947
|
+
securityAuditing: {
|
|
11948
|
+
current: "MISSING",
|
|
11949
|
+
target: "PARTIAL"
|
|
11950
|
+
},
|
|
11951
|
+
crossFrameworkInterop: {
|
|
11952
|
+
current: "PARTIAL",
|
|
11953
|
+
target: "GOOD"
|
|
11954
|
+
}
|
|
11955
|
+
};
|
|
11956
|
+
const COMMON_CI_OVERRIDES = {
|
|
11957
|
+
discovery: {
|
|
11958
|
+
current: "EXCELLENT",
|
|
11959
|
+
target: "EXCELLENT"
|
|
11960
|
+
},
|
|
11961
|
+
semanticUnderstanding: {
|
|
11962
|
+
current: "GOOD",
|
|
11963
|
+
target: "EXCELLENT"
|
|
11964
|
+
},
|
|
11965
|
+
errorClassification: {
|
|
11966
|
+
current: "GOOD",
|
|
11967
|
+
target: "EXCELLENT"
|
|
11968
|
+
},
|
|
11969
|
+
retrySemantics: {
|
|
11970
|
+
current: "GOOD",
|
|
11971
|
+
target: "EXCELLENT"
|
|
11972
|
+
},
|
|
11973
|
+
parameterization: {
|
|
11974
|
+
current: "GOOD",
|
|
11975
|
+
target: "EXCELLENT"
|
|
11976
|
+
},
|
|
11977
|
+
runtimeEvidence: {
|
|
11978
|
+
current: "PARTIAL",
|
|
11979
|
+
target: "GOOD"
|
|
11980
|
+
},
|
|
11981
|
+
adversarialCorpus: {
|
|
11982
|
+
current: "PARTIAL",
|
|
11983
|
+
target: "GOOD"
|
|
11984
|
+
},
|
|
11985
|
+
knownLimitationCoverage: {
|
|
11986
|
+
current: "PARTIAL",
|
|
11987
|
+
target: "GOOD"
|
|
11988
|
+
},
|
|
11989
|
+
ciIntegration: {
|
|
11990
|
+
current: "EXCELLENT",
|
|
11991
|
+
target: "EXCELLENT"
|
|
11992
|
+
},
|
|
11993
|
+
reportingQuality: {
|
|
11994
|
+
current: "GOOD",
|
|
11995
|
+
target: "EXCELLENT"
|
|
11996
|
+
},
|
|
11997
|
+
configDiscovery: {
|
|
11998
|
+
current: "EXCELLENT",
|
|
11999
|
+
target: "EXCELLENT"
|
|
12000
|
+
},
|
|
12001
|
+
versionCompatibility: {
|
|
12002
|
+
current: "GOOD",
|
|
12003
|
+
target: "EXCELLENT"
|
|
12004
|
+
},
|
|
12005
|
+
documentationCoverage: {
|
|
12006
|
+
current: "PARTIAL",
|
|
12007
|
+
target: "GOOD"
|
|
12008
|
+
},
|
|
12009
|
+
migrationGuidance: {
|
|
12010
|
+
current: "PARTIAL",
|
|
12011
|
+
target: "GOOD"
|
|
12012
|
+
},
|
|
12013
|
+
performanceProfiling: {
|
|
12014
|
+
current: "MISSING",
|
|
12015
|
+
target: "PARTIAL"
|
|
12016
|
+
},
|
|
12017
|
+
securityAuditing: {
|
|
12018
|
+
current: "MISSING",
|
|
12019
|
+
target: "PARTIAL"
|
|
12020
|
+
},
|
|
12021
|
+
crossFrameworkInterop: {
|
|
12022
|
+
current: "PARTIAL",
|
|
12023
|
+
target: "GOOD"
|
|
12024
|
+
}
|
|
12025
|
+
};
|
|
12026
|
+
function withCommonMature(overrides) {
|
|
12027
|
+
return {
|
|
12028
|
+
...COMMON_MATURE_OVERRIDES,
|
|
12029
|
+
...overrides
|
|
12030
|
+
};
|
|
12031
|
+
}
|
|
12032
|
+
const FRAMEWORK_OVERRIDES = {
|
|
12033
|
+
playwright: withCommonMature({
|
|
12034
|
+
discovery: {
|
|
12035
|
+
current: "EXCELLENT",
|
|
12036
|
+
target: "EXCELLENT"
|
|
12037
|
+
},
|
|
12038
|
+
astUsage: {
|
|
12039
|
+
current: "PARTIAL",
|
|
12040
|
+
target: "EXCELLENT",
|
|
12041
|
+
gapId: "GAP-PW-001"
|
|
12042
|
+
},
|
|
12043
|
+
lifecycleModeling: {
|
|
12044
|
+
current: "PARTIAL",
|
|
12045
|
+
target: "GOOD",
|
|
12046
|
+
gapId: "GAP-PW-002"
|
|
12047
|
+
},
|
|
12048
|
+
fixtureModeling: {
|
|
12049
|
+
current: "PARTIAL",
|
|
12050
|
+
target: "GOOD",
|
|
12051
|
+
gapId: "GAP-PW-002"
|
|
12052
|
+
},
|
|
12053
|
+
mockingSemantics: {
|
|
12054
|
+
current: "MISSING",
|
|
12055
|
+
target: "PARTIAL",
|
|
12056
|
+
gapId: "GAP-PW-003"
|
|
12057
|
+
},
|
|
12058
|
+
sharedStateAnalysis: {
|
|
12059
|
+
current: "MISSING",
|
|
12060
|
+
target: "PARTIAL",
|
|
12061
|
+
gapId: "GAP-PW-004"
|
|
12062
|
+
},
|
|
12063
|
+
retrySemantics: {
|
|
12064
|
+
current: "GOOD",
|
|
12065
|
+
target: "EXCELLENT"
|
|
12066
|
+
},
|
|
12067
|
+
parameterization: {
|
|
12068
|
+
current: "PARTIAL",
|
|
12069
|
+
target: "GOOD",
|
|
12070
|
+
gapId: "GAP-PW-005"
|
|
12071
|
+
},
|
|
12072
|
+
snapshotSemantics: {
|
|
12073
|
+
current: "NOT_APPLICABLE",
|
|
12074
|
+
target: "NOT_APPLICABLE"
|
|
12075
|
+
},
|
|
12076
|
+
parallelismWorkerSafety: {
|
|
12077
|
+
current: "MISSING",
|
|
12078
|
+
target: "PARTIAL",
|
|
12079
|
+
gapId: "GAP-PW-006"
|
|
12080
|
+
},
|
|
12081
|
+
runtimeEvidence: {
|
|
12082
|
+
current: "GOOD",
|
|
12083
|
+
target: "EXCELLENT"
|
|
12084
|
+
},
|
|
12085
|
+
adversarialCorpus: {
|
|
12086
|
+
current: "PARTIAL",
|
|
12087
|
+
target: "GOOD"
|
|
12088
|
+
},
|
|
12089
|
+
knownLimitationCoverage: {
|
|
12090
|
+
current: "PARTIAL",
|
|
12091
|
+
target: "GOOD"
|
|
12092
|
+
},
|
|
12093
|
+
ciIntegration: {
|
|
12094
|
+
current: "GOOD",
|
|
12095
|
+
target: "EXCELLENT"
|
|
12096
|
+
}
|
|
12097
|
+
}),
|
|
12098
|
+
jest: withCommonMature({
|
|
12099
|
+
discovery: {
|
|
12100
|
+
current: "EXCELLENT",
|
|
12101
|
+
target: "EXCELLENT"
|
|
12102
|
+
},
|
|
12103
|
+
astUsage: {
|
|
12104
|
+
current: "GOOD",
|
|
12105
|
+
target: "EXCELLENT"
|
|
12106
|
+
},
|
|
12107
|
+
semanticUnderstanding: {
|
|
12108
|
+
current: "WEAK",
|
|
12109
|
+
target: "GOOD",
|
|
12110
|
+
gapId: "GAP-JEST-002"
|
|
12111
|
+
},
|
|
12112
|
+
assertionUnderstanding: {
|
|
12113
|
+
current: "WEAK",
|
|
12114
|
+
target: "GOOD",
|
|
12115
|
+
gapId: "GAP-JEST-003"
|
|
12116
|
+
},
|
|
12117
|
+
lifecycleModeling: {
|
|
12118
|
+
current: "MISSING",
|
|
12119
|
+
target: "PARTIAL",
|
|
12120
|
+
gapId: "GAP-JEST-004"
|
|
12121
|
+
},
|
|
12122
|
+
asyncSemantics: {
|
|
12123
|
+
current: "MISSING",
|
|
12124
|
+
target: "PARTIAL",
|
|
12125
|
+
gapId: "GAP-JEST-005"
|
|
12126
|
+
},
|
|
12127
|
+
fixtureModeling: {
|
|
12128
|
+
current: "MISSING",
|
|
12129
|
+
target: "PARTIAL",
|
|
12130
|
+
gapId: "GAP-JEST-006"
|
|
12131
|
+
},
|
|
12132
|
+
mockingSemantics: {
|
|
12133
|
+
current: "MISSING",
|
|
12134
|
+
target: "PARTIAL",
|
|
12135
|
+
gapId: "GAP-JEST-007"
|
|
12136
|
+
},
|
|
12137
|
+
sharedStateAnalysis: {
|
|
12138
|
+
current: "MISSING",
|
|
12139
|
+
target: "PARTIAL",
|
|
12140
|
+
gapId: "GAP-JEST-008"
|
|
12141
|
+
},
|
|
12142
|
+
errorClassification: {
|
|
12143
|
+
current: "PARTIAL",
|
|
12144
|
+
target: "GOOD"
|
|
12145
|
+
},
|
|
12146
|
+
retrySemantics: {
|
|
12147
|
+
current: "PARTIAL",
|
|
12148
|
+
target: "GOOD",
|
|
12149
|
+
gapId: "GAP-JEST-009"
|
|
12150
|
+
},
|
|
12151
|
+
parameterization: {
|
|
12152
|
+
current: "MISSING",
|
|
12153
|
+
target: "PARTIAL",
|
|
12154
|
+
gapId: "GAP-JEST-010"
|
|
12155
|
+
},
|
|
12156
|
+
snapshotSemantics: {
|
|
12157
|
+
current: "MISSING",
|
|
12158
|
+
target: "PARTIAL",
|
|
12159
|
+
gapId: "GAP-JEST-011"
|
|
12160
|
+
},
|
|
12161
|
+
parallelismWorkerSafety: {
|
|
12162
|
+
current: "MISSING",
|
|
12163
|
+
target: "PARTIAL",
|
|
12164
|
+
gapId: "GAP-JEST-012"
|
|
12165
|
+
},
|
|
12166
|
+
runtimeEvidence: {
|
|
12167
|
+
current: "PARTIAL",
|
|
12168
|
+
target: "GOOD",
|
|
12169
|
+
gapId: "GAP-JEST-013"
|
|
12170
|
+
},
|
|
12171
|
+
adversarialCorpus: {
|
|
12172
|
+
current: "PARTIAL",
|
|
12173
|
+
target: "GOOD",
|
|
12174
|
+
gapId: "GAP-JEST-014"
|
|
12175
|
+
},
|
|
12176
|
+
knownLimitationCoverage: {
|
|
12177
|
+
current: "PARTIAL",
|
|
12178
|
+
target: "GOOD",
|
|
12179
|
+
gapId: "GAP-JEST-015"
|
|
12180
|
+
},
|
|
12181
|
+
ciIntegration: {
|
|
12182
|
+
current: "GOOD",
|
|
12183
|
+
target: "EXCELLENT"
|
|
12184
|
+
}
|
|
12185
|
+
}),
|
|
12186
|
+
vitest: withCommonMature({
|
|
12187
|
+
discovery: {
|
|
12188
|
+
current: "EXCELLENT",
|
|
12189
|
+
target: "EXCELLENT"
|
|
12190
|
+
},
|
|
12191
|
+
astUsage: {
|
|
12192
|
+
current: "GOOD",
|
|
12193
|
+
target: "EXCELLENT"
|
|
12194
|
+
},
|
|
12195
|
+
semanticUnderstanding: {
|
|
12196
|
+
current: "WEAK",
|
|
12197
|
+
target: "GOOD",
|
|
12198
|
+
gapId: "GAP-VIT-002"
|
|
12199
|
+
},
|
|
12200
|
+
assertionUnderstanding: {
|
|
12201
|
+
current: "WEAK",
|
|
12202
|
+
target: "GOOD",
|
|
12203
|
+
gapId: "GAP-VIT-003"
|
|
12204
|
+
},
|
|
12205
|
+
lifecycleModeling: {
|
|
12206
|
+
current: "MISSING",
|
|
12207
|
+
target: "PARTIAL",
|
|
12208
|
+
gapId: "GAP-VIT-004"
|
|
12209
|
+
},
|
|
12210
|
+
asyncSemantics: {
|
|
12211
|
+
current: "MISSING",
|
|
12212
|
+
target: "PARTIAL",
|
|
12213
|
+
gapId: "GAP-VIT-005"
|
|
12214
|
+
},
|
|
12215
|
+
fixtureModeling: {
|
|
12216
|
+
current: "MISSING",
|
|
12217
|
+
target: "PARTIAL",
|
|
12218
|
+
gapId: "GAP-VIT-006"
|
|
12219
|
+
},
|
|
12220
|
+
mockingSemantics: {
|
|
12221
|
+
current: "MISSING",
|
|
12222
|
+
target: "PARTIAL",
|
|
12223
|
+
gapId: "GAP-VIT-007"
|
|
12224
|
+
},
|
|
12225
|
+
sharedStateAnalysis: {
|
|
12226
|
+
current: "MISSING",
|
|
12227
|
+
target: "PARTIAL",
|
|
12228
|
+
gapId: "GAP-VIT-008"
|
|
12229
|
+
},
|
|
12230
|
+
errorClassification: {
|
|
12231
|
+
current: "PARTIAL",
|
|
12232
|
+
target: "GOOD"
|
|
12233
|
+
},
|
|
12234
|
+
retrySemantics: {
|
|
12235
|
+
current: "PARTIAL",
|
|
12236
|
+
target: "GOOD",
|
|
12237
|
+
gapId: "GAP-VIT-009"
|
|
12238
|
+
},
|
|
12239
|
+
parameterization: {
|
|
12240
|
+
current: "MISSING",
|
|
12241
|
+
target: "PARTIAL",
|
|
12242
|
+
gapId: "GAP-VIT-010"
|
|
12243
|
+
},
|
|
12244
|
+
snapshotSemantics: {
|
|
12245
|
+
current: "MISSING",
|
|
12246
|
+
target: "PARTIAL",
|
|
12247
|
+
gapId: "GAP-VIT-011"
|
|
12248
|
+
},
|
|
12249
|
+
parallelismWorkerSafety: {
|
|
12250
|
+
current: "MISSING",
|
|
12251
|
+
target: "PARTIAL",
|
|
12252
|
+
gapId: "GAP-VIT-012"
|
|
12253
|
+
},
|
|
12254
|
+
runtimeEvidence: {
|
|
12255
|
+
current: "PARTIAL",
|
|
12256
|
+
target: "GOOD",
|
|
12257
|
+
gapId: "GAP-VIT-013"
|
|
12258
|
+
},
|
|
12259
|
+
adversarialCorpus: {
|
|
12260
|
+
current: "PARTIAL",
|
|
12261
|
+
target: "GOOD",
|
|
12262
|
+
gapId: "GAP-VIT-014"
|
|
12263
|
+
},
|
|
12264
|
+
knownLimitationCoverage: {
|
|
12265
|
+
current: "PARTIAL",
|
|
12266
|
+
target: "GOOD",
|
|
12267
|
+
gapId: "GAP-VIT-015"
|
|
12268
|
+
},
|
|
12269
|
+
ciIntegration: {
|
|
12270
|
+
current: "GOOD",
|
|
12271
|
+
target: "EXCELLENT"
|
|
12272
|
+
}
|
|
12273
|
+
}),
|
|
12274
|
+
pytest: withCommonMature({
|
|
12275
|
+
discovery: {
|
|
12276
|
+
current: "GOOD",
|
|
12277
|
+
target: "EXCELLENT"
|
|
12278
|
+
},
|
|
12279
|
+
astUsage: {
|
|
12280
|
+
current: "PARTIAL",
|
|
12281
|
+
target: "GOOD",
|
|
12282
|
+
gapId: "GAP-PY-001"
|
|
12283
|
+
},
|
|
12284
|
+
semanticUnderstanding: {
|
|
12285
|
+
current: "WEAK",
|
|
12286
|
+
target: "PARTIAL",
|
|
12287
|
+
gapId: "GAP-PY-002"
|
|
12288
|
+
},
|
|
12289
|
+
assertionUnderstanding: {
|
|
12290
|
+
current: "WEAK",
|
|
12291
|
+
target: "PARTIAL",
|
|
12292
|
+
gapId: "GAP-PY-003"
|
|
12293
|
+
},
|
|
12294
|
+
lifecycleModeling: {
|
|
12295
|
+
current: "MISSING",
|
|
12296
|
+
target: "PARTIAL",
|
|
12297
|
+
gapId: "GAP-PY-004"
|
|
12298
|
+
},
|
|
12299
|
+
asyncSemantics: {
|
|
12300
|
+
current: "MISSING",
|
|
12301
|
+
target: "PARTIAL",
|
|
12302
|
+
gapId: "GAP-PY-005"
|
|
12303
|
+
},
|
|
12304
|
+
fixtureModeling: {
|
|
12305
|
+
current: "MISSING",
|
|
12306
|
+
target: "PARTIAL",
|
|
12307
|
+
gapId: "GAP-PY-006"
|
|
12308
|
+
},
|
|
12309
|
+
mockingSemantics: {
|
|
12310
|
+
current: "MISSING",
|
|
12311
|
+
target: "PARTIAL",
|
|
12312
|
+
gapId: "GAP-PY-007"
|
|
12313
|
+
},
|
|
12314
|
+
sharedStateAnalysis: {
|
|
12315
|
+
current: "MISSING",
|
|
12316
|
+
target: "PARTIAL",
|
|
12317
|
+
gapId: "GAP-PY-008"
|
|
12318
|
+
},
|
|
12319
|
+
errorClassification: {
|
|
12320
|
+
current: "PARTIAL",
|
|
12321
|
+
target: "GOOD"
|
|
12322
|
+
},
|
|
12323
|
+
retrySemantics: {
|
|
12324
|
+
current: "MISSING",
|
|
12325
|
+
target: "PARTIAL",
|
|
12326
|
+
gapId: "GAP-PY-009"
|
|
12327
|
+
},
|
|
12328
|
+
parameterization: {
|
|
12329
|
+
current: "PARTIAL",
|
|
12330
|
+
target: "GOOD",
|
|
12331
|
+
gapId: "GAP-PY-010"
|
|
12332
|
+
},
|
|
12333
|
+
snapshotSemantics: {
|
|
12334
|
+
current: "NOT_APPLICABLE",
|
|
12335
|
+
target: "NOT_APPLICABLE"
|
|
12336
|
+
},
|
|
12337
|
+
parallelismWorkerSafety: {
|
|
12338
|
+
current: "MISSING",
|
|
12339
|
+
target: "PARTIAL",
|
|
12340
|
+
gapId: "GAP-PY-011"
|
|
12341
|
+
},
|
|
12342
|
+
runtimeEvidence: {
|
|
12343
|
+
current: "MISSING",
|
|
12344
|
+
target: "PARTIAL",
|
|
12345
|
+
gapId: "GAP-PY-012"
|
|
12346
|
+
},
|
|
12347
|
+
adversarialCorpus: {
|
|
12348
|
+
current: "MISSING",
|
|
12349
|
+
target: "PARTIAL",
|
|
12350
|
+
gapId: "GAP-PY-013"
|
|
12351
|
+
},
|
|
12352
|
+
knownLimitationCoverage: {
|
|
12353
|
+
current: "MISSING",
|
|
12354
|
+
target: "PARTIAL",
|
|
12355
|
+
gapId: "GAP-PY-014"
|
|
12356
|
+
},
|
|
12357
|
+
ciIntegration: {
|
|
12358
|
+
current: "PARTIAL",
|
|
12359
|
+
target: "GOOD"
|
|
12360
|
+
}
|
|
12361
|
+
}),
|
|
12362
|
+
junit: withCommonMature({
|
|
12363
|
+
discovery: {
|
|
12364
|
+
current: "GOOD",
|
|
12365
|
+
target: "EXCELLENT"
|
|
12366
|
+
},
|
|
12367
|
+
astUsage: {
|
|
12368
|
+
current: "PARTIAL",
|
|
12369
|
+
target: "GOOD",
|
|
12370
|
+
gapId: "GAP-JU-001"
|
|
12371
|
+
},
|
|
12372
|
+
semanticUnderstanding: {
|
|
12373
|
+
current: "WEAK",
|
|
12374
|
+
target: "PARTIAL",
|
|
12375
|
+
gapId: "GAP-JU-002"
|
|
12376
|
+
},
|
|
12377
|
+
assertionUnderstanding: {
|
|
12378
|
+
current: "WEAK",
|
|
12379
|
+
target: "PARTIAL",
|
|
12380
|
+
gapId: "GAP-JU-003"
|
|
12381
|
+
},
|
|
12382
|
+
lifecycleModeling: {
|
|
12383
|
+
current: "MISSING",
|
|
12384
|
+
target: "PARTIAL",
|
|
12385
|
+
gapId: "GAP-JU-004"
|
|
12386
|
+
},
|
|
12387
|
+
asyncSemantics: {
|
|
12388
|
+
current: "MISSING",
|
|
12389
|
+
target: "PARTIAL",
|
|
12390
|
+
gapId: "GAP-JU-005"
|
|
12391
|
+
},
|
|
12392
|
+
fixtureModeling: {
|
|
12393
|
+
current: "MISSING",
|
|
12394
|
+
target: "PARTIAL",
|
|
12395
|
+
gapId: "GAP-JU-006"
|
|
12396
|
+
},
|
|
12397
|
+
mockingSemantics: {
|
|
12398
|
+
current: "MISSING",
|
|
12399
|
+
target: "PARTIAL",
|
|
12400
|
+
gapId: "GAP-JU-007"
|
|
12401
|
+
},
|
|
12402
|
+
sharedStateAnalysis: {
|
|
12403
|
+
current: "MISSING",
|
|
12404
|
+
target: "PARTIAL",
|
|
12405
|
+
gapId: "GAP-JU-008"
|
|
12406
|
+
},
|
|
12407
|
+
errorClassification: {
|
|
12408
|
+
current: "PARTIAL",
|
|
12409
|
+
target: "GOOD"
|
|
12410
|
+
},
|
|
12411
|
+
retrySemantics: {
|
|
12412
|
+
current: "MISSING",
|
|
12413
|
+
target: "PARTIAL",
|
|
12414
|
+
gapId: "GAP-JU-009"
|
|
12415
|
+
},
|
|
12416
|
+
parameterization: {
|
|
12417
|
+
current: "PARTIAL",
|
|
12418
|
+
target: "GOOD",
|
|
12419
|
+
gapId: "GAP-JU-010"
|
|
12420
|
+
},
|
|
12421
|
+
snapshotSemantics: {
|
|
12422
|
+
current: "NOT_APPLICABLE",
|
|
12423
|
+
target: "NOT_APPLICABLE"
|
|
12424
|
+
},
|
|
12425
|
+
parallelismWorkerSafety: {
|
|
12426
|
+
current: "MISSING",
|
|
12427
|
+
target: "PARTIAL",
|
|
12428
|
+
gapId: "GAP-JU-011"
|
|
12429
|
+
},
|
|
12430
|
+
runtimeEvidence: {
|
|
12431
|
+
current: "MISSING",
|
|
12432
|
+
target: "PARTIAL",
|
|
12433
|
+
gapId: "GAP-JU-012"
|
|
12434
|
+
},
|
|
12435
|
+
adversarialCorpus: {
|
|
12436
|
+
current: "MISSING",
|
|
12437
|
+
target: "PARTIAL",
|
|
12438
|
+
gapId: "GAP-JU-013"
|
|
12439
|
+
},
|
|
12440
|
+
knownLimitationCoverage: {
|
|
12441
|
+
current: "MISSING",
|
|
12442
|
+
target: "PARTIAL",
|
|
12443
|
+
gapId: "GAP-JU-014"
|
|
12444
|
+
},
|
|
12445
|
+
ciIntegration: {
|
|
12446
|
+
current: "PARTIAL",
|
|
12447
|
+
target: "GOOD"
|
|
12448
|
+
}
|
|
12449
|
+
}),
|
|
12450
|
+
nunit: withCommonMature({
|
|
12451
|
+
discovery: {
|
|
12452
|
+
current: "GOOD",
|
|
12453
|
+
target: "EXCELLENT"
|
|
12454
|
+
},
|
|
12455
|
+
astUsage: {
|
|
12456
|
+
current: "PARTIAL",
|
|
12457
|
+
target: "GOOD",
|
|
12458
|
+
gapId: "GAP-NU-001"
|
|
12459
|
+
},
|
|
12460
|
+
semanticUnderstanding: {
|
|
12461
|
+
current: "WEAK",
|
|
12462
|
+
target: "PARTIAL",
|
|
12463
|
+
gapId: "GAP-NU-002"
|
|
12464
|
+
},
|
|
12465
|
+
assertionUnderstanding: {
|
|
12466
|
+
current: "WEAK",
|
|
12467
|
+
target: "PARTIAL",
|
|
12468
|
+
gapId: "GAP-NU-003"
|
|
12469
|
+
},
|
|
12470
|
+
lifecycleModeling: {
|
|
12471
|
+
current: "MISSING",
|
|
12472
|
+
target: "PARTIAL",
|
|
12473
|
+
gapId: "GAP-NU-004"
|
|
12474
|
+
},
|
|
12475
|
+
asyncSemantics: {
|
|
12476
|
+
current: "MISSING",
|
|
12477
|
+
target: "PARTIAL",
|
|
12478
|
+
gapId: "GAP-NU-005"
|
|
12479
|
+
},
|
|
12480
|
+
fixtureModeling: {
|
|
12481
|
+
current: "MISSING",
|
|
12482
|
+
target: "PARTIAL",
|
|
12483
|
+
gapId: "GAP-NU-006"
|
|
12484
|
+
},
|
|
12485
|
+
mockingSemantics: {
|
|
12486
|
+
current: "MISSING",
|
|
12487
|
+
target: "PARTIAL",
|
|
12488
|
+
gapId: "GAP-NU-007"
|
|
12489
|
+
},
|
|
12490
|
+
sharedStateAnalysis: {
|
|
12491
|
+
current: "MISSING",
|
|
12492
|
+
target: "PARTIAL",
|
|
12493
|
+
gapId: "GAP-NU-008"
|
|
12494
|
+
},
|
|
12495
|
+
errorClassification: {
|
|
12496
|
+
current: "PARTIAL",
|
|
12497
|
+
target: "GOOD"
|
|
12498
|
+
},
|
|
12499
|
+
retrySemantics: {
|
|
12500
|
+
current: "MISSING",
|
|
12501
|
+
target: "PARTIAL",
|
|
12502
|
+
gapId: "GAP-NU-009"
|
|
12503
|
+
},
|
|
12504
|
+
parameterization: {
|
|
12505
|
+
current: "PARTIAL",
|
|
12506
|
+
target: "GOOD",
|
|
12507
|
+
gapId: "GAP-NU-010"
|
|
12508
|
+
},
|
|
12509
|
+
snapshotSemantics: {
|
|
12510
|
+
current: "NOT_APPLICABLE",
|
|
12511
|
+
target: "NOT_APPLICABLE"
|
|
12512
|
+
},
|
|
12513
|
+
parallelismWorkerSafety: {
|
|
12514
|
+
current: "MISSING",
|
|
12515
|
+
target: "PARTIAL",
|
|
12516
|
+
gapId: "GAP-NU-011"
|
|
12517
|
+
},
|
|
12518
|
+
runtimeEvidence: {
|
|
12519
|
+
current: "MISSING",
|
|
12520
|
+
target: "PARTIAL",
|
|
12521
|
+
gapId: "GAP-NU-012"
|
|
12522
|
+
},
|
|
12523
|
+
adversarialCorpus: {
|
|
12524
|
+
current: "MISSING",
|
|
12525
|
+
target: "PARTIAL",
|
|
12526
|
+
gapId: "GAP-NU-013"
|
|
12527
|
+
},
|
|
12528
|
+
knownLimitationCoverage: {
|
|
12529
|
+
current: "MISSING",
|
|
12530
|
+
target: "PARTIAL",
|
|
12531
|
+
gapId: "GAP-NU-014"
|
|
12532
|
+
},
|
|
12533
|
+
ciIntegration: {
|
|
12534
|
+
current: "PARTIAL",
|
|
12535
|
+
target: "GOOD"
|
|
12536
|
+
}
|
|
12537
|
+
}),
|
|
12538
|
+
xunit: withCommonMature({
|
|
12539
|
+
discovery: {
|
|
12540
|
+
current: "GOOD",
|
|
12541
|
+
target: "EXCELLENT"
|
|
12542
|
+
},
|
|
12543
|
+
astUsage: {
|
|
12544
|
+
current: "PARTIAL",
|
|
12545
|
+
target: "GOOD",
|
|
12546
|
+
gapId: "GAP-XU-001"
|
|
12547
|
+
},
|
|
12548
|
+
semanticUnderstanding: {
|
|
12549
|
+
current: "WEAK",
|
|
12550
|
+
target: "PARTIAL",
|
|
12551
|
+
gapId: "GAP-XU-002"
|
|
12552
|
+
},
|
|
12553
|
+
assertionUnderstanding: {
|
|
12554
|
+
current: "WEAK",
|
|
12555
|
+
target: "PARTIAL",
|
|
12556
|
+
gapId: "GAP-XU-003"
|
|
12557
|
+
},
|
|
12558
|
+
lifecycleModeling: {
|
|
12559
|
+
current: "MISSING",
|
|
12560
|
+
target: "PARTIAL",
|
|
12561
|
+
gapId: "GAP-XU-004"
|
|
12562
|
+
},
|
|
12563
|
+
asyncSemantics: {
|
|
12564
|
+
current: "MISSING",
|
|
12565
|
+
target: "PARTIAL",
|
|
12566
|
+
gapId: "GAP-XU-005"
|
|
12567
|
+
},
|
|
12568
|
+
fixtureModeling: {
|
|
12569
|
+
current: "MISSING",
|
|
12570
|
+
target: "PARTIAL",
|
|
12571
|
+
gapId: "GAP-XU-006"
|
|
12572
|
+
},
|
|
12573
|
+
mockingSemantics: {
|
|
12574
|
+
current: "MISSING",
|
|
12575
|
+
target: "PARTIAL",
|
|
12576
|
+
gapId: "GAP-XU-007"
|
|
12577
|
+
},
|
|
12578
|
+
sharedStateAnalysis: {
|
|
12579
|
+
current: "MISSING",
|
|
12580
|
+
target: "PARTIAL",
|
|
12581
|
+
gapId: "GAP-XU-008"
|
|
12582
|
+
},
|
|
12583
|
+
errorClassification: {
|
|
12584
|
+
current: "PARTIAL",
|
|
12585
|
+
target: "GOOD"
|
|
12586
|
+
},
|
|
12587
|
+
retrySemantics: {
|
|
12588
|
+
current: "MISSING",
|
|
12589
|
+
target: "PARTIAL",
|
|
12590
|
+
gapId: "GAP-XU-009"
|
|
12591
|
+
},
|
|
12592
|
+
parameterization: {
|
|
12593
|
+
current: "PARTIAL",
|
|
12594
|
+
target: "GOOD",
|
|
12595
|
+
gapId: "GAP-XU-010"
|
|
12596
|
+
},
|
|
12597
|
+
snapshotSemantics: {
|
|
12598
|
+
current: "NOT_APPLICABLE",
|
|
12599
|
+
target: "NOT_APPLICABLE"
|
|
12600
|
+
},
|
|
12601
|
+
parallelismWorkerSafety: {
|
|
12602
|
+
current: "MISSING",
|
|
12603
|
+
target: "PARTIAL",
|
|
12604
|
+
gapId: "GAP-XU-011"
|
|
12605
|
+
},
|
|
12606
|
+
runtimeEvidence: {
|
|
12607
|
+
current: "MISSING",
|
|
12608
|
+
target: "PARTIAL",
|
|
12609
|
+
gapId: "GAP-XU-012"
|
|
12610
|
+
},
|
|
12611
|
+
adversarialCorpus: {
|
|
12612
|
+
current: "MISSING",
|
|
12613
|
+
target: "PARTIAL",
|
|
12614
|
+
gapId: "GAP-XU-013"
|
|
12615
|
+
},
|
|
12616
|
+
knownLimitationCoverage: {
|
|
12617
|
+
current: "MISSING",
|
|
12618
|
+
target: "PARTIAL",
|
|
12619
|
+
gapId: "GAP-XU-014"
|
|
12620
|
+
},
|
|
12621
|
+
ciIntegration: {
|
|
12622
|
+
current: "PARTIAL",
|
|
12623
|
+
target: "GOOD"
|
|
12624
|
+
}
|
|
12625
|
+
}),
|
|
12626
|
+
cypress: withCommonMature({
|
|
12627
|
+
discovery: {
|
|
12628
|
+
current: "GOOD",
|
|
12629
|
+
target: "EXCELLENT"
|
|
12630
|
+
},
|
|
12631
|
+
astUsage: {
|
|
12632
|
+
current: "PARTIAL",
|
|
12633
|
+
target: "GOOD",
|
|
12634
|
+
gapId: "GAP-CY-001"
|
|
12635
|
+
},
|
|
12636
|
+
semanticUnderstanding: {
|
|
12637
|
+
current: "WEAK",
|
|
12638
|
+
target: "PARTIAL",
|
|
12639
|
+
gapId: "GAP-CY-002"
|
|
12640
|
+
},
|
|
12641
|
+
assertionUnderstanding: {
|
|
12642
|
+
current: "WEAK",
|
|
12643
|
+
target: "PARTIAL",
|
|
12644
|
+
gapId: "GAP-CY-003"
|
|
12645
|
+
},
|
|
12646
|
+
lifecycleModeling: {
|
|
12647
|
+
current: "MISSING",
|
|
12648
|
+
target: "PARTIAL",
|
|
12649
|
+
gapId: "GAP-CY-004"
|
|
12650
|
+
},
|
|
12651
|
+
asyncSemantics: {
|
|
12652
|
+
current: "MISSING",
|
|
12653
|
+
target: "PARTIAL",
|
|
12654
|
+
gapId: "GAP-CY-005"
|
|
12655
|
+
},
|
|
12656
|
+
fixtureModeling: {
|
|
12657
|
+
current: "MISSING",
|
|
12658
|
+
target: "PARTIAL",
|
|
12659
|
+
gapId: "GAP-CY-006"
|
|
12660
|
+
},
|
|
12661
|
+
mockingSemantics: {
|
|
12662
|
+
current: "MISSING",
|
|
12663
|
+
target: "PARTIAL",
|
|
12664
|
+
gapId: "GAP-CY-007"
|
|
12665
|
+
},
|
|
12666
|
+
sharedStateAnalysis: {
|
|
12667
|
+
current: "MISSING",
|
|
12668
|
+
target: "PARTIAL",
|
|
12669
|
+
gapId: "GAP-CY-008"
|
|
12670
|
+
},
|
|
12671
|
+
errorClassification: {
|
|
12672
|
+
current: "PARTIAL",
|
|
12673
|
+
target: "GOOD"
|
|
12674
|
+
},
|
|
12675
|
+
retrySemantics: {
|
|
12676
|
+
current: "PARTIAL",
|
|
12677
|
+
target: "GOOD"
|
|
12678
|
+
},
|
|
12679
|
+
parameterization: {
|
|
12680
|
+
current: "MISSING",
|
|
12681
|
+
target: "PARTIAL",
|
|
12682
|
+
gapId: "GAP-CY-009"
|
|
12683
|
+
},
|
|
12684
|
+
snapshotSemantics: {
|
|
12685
|
+
current: "MISSING",
|
|
12686
|
+
target: "PARTIAL",
|
|
12687
|
+
gapId: "GAP-CY-010"
|
|
12688
|
+
},
|
|
12689
|
+
parallelismWorkerSafety: {
|
|
12690
|
+
current: "NOT_APPLICABLE",
|
|
12691
|
+
target: "NOT_APPLICABLE"
|
|
12692
|
+
},
|
|
12693
|
+
runtimeEvidence: {
|
|
12694
|
+
current: "MISSING",
|
|
12695
|
+
target: "PARTIAL",
|
|
12696
|
+
gapId: "GAP-CY-011"
|
|
12697
|
+
},
|
|
12698
|
+
adversarialCorpus: {
|
|
12699
|
+
current: "MISSING",
|
|
12700
|
+
target: "PARTIAL",
|
|
12701
|
+
gapId: "GAP-CY-012"
|
|
12702
|
+
},
|
|
12703
|
+
knownLimitationCoverage: {
|
|
12704
|
+
current: "MISSING",
|
|
12705
|
+
target: "PARTIAL",
|
|
12706
|
+
gapId: "GAP-CY-013"
|
|
12707
|
+
},
|
|
12708
|
+
ciIntegration: {
|
|
12709
|
+
current: "PARTIAL",
|
|
12710
|
+
target: "GOOD"
|
|
12711
|
+
}
|
|
12712
|
+
}),
|
|
12713
|
+
selenium: withCommonMature({
|
|
12714
|
+
discovery: {
|
|
12715
|
+
current: "PARTIAL",
|
|
12716
|
+
target: "GOOD",
|
|
12717
|
+
gapId: "GAP-SE-001"
|
|
12718
|
+
},
|
|
12719
|
+
astUsage: {
|
|
12720
|
+
current: "MISSING",
|
|
12721
|
+
target: "PARTIAL",
|
|
12722
|
+
gapId: "GAP-SE-002"
|
|
12723
|
+
},
|
|
12724
|
+
semanticUnderstanding: {
|
|
12725
|
+
current: "MISSING",
|
|
12726
|
+
target: "WEAK",
|
|
12727
|
+
gapId: "GAP-SE-003"
|
|
12728
|
+
},
|
|
12729
|
+
assertionUnderstanding: {
|
|
12730
|
+
current: "MISSING",
|
|
12731
|
+
target: "WEAK",
|
|
12732
|
+
gapId: "GAP-SE-004"
|
|
12733
|
+
},
|
|
12734
|
+
lifecycleModeling: {
|
|
12735
|
+
current: "MISSING",
|
|
12736
|
+
target: "WEAK",
|
|
12737
|
+
gapId: "GAP-SE-005"
|
|
12738
|
+
},
|
|
12739
|
+
asyncSemantics: {
|
|
12740
|
+
current: "MISSING",
|
|
12741
|
+
target: "WEAK",
|
|
12742
|
+
gapId: "GAP-SE-006"
|
|
12743
|
+
},
|
|
12744
|
+
fixtureModeling: {
|
|
12745
|
+
current: "MISSING",
|
|
12746
|
+
target: "WEAK",
|
|
12747
|
+
gapId: "GAP-SE-007"
|
|
12748
|
+
},
|
|
12749
|
+
mockingSemantics: {
|
|
12750
|
+
current: "NOT_APPLICABLE",
|
|
12751
|
+
target: "NOT_APPLICABLE"
|
|
12752
|
+
},
|
|
12753
|
+
sharedStateAnalysis: {
|
|
12754
|
+
current: "MISSING",
|
|
12755
|
+
target: "WEAK",
|
|
12756
|
+
gapId: "GAP-SE-008"
|
|
12757
|
+
},
|
|
12758
|
+
errorClassification: {
|
|
12759
|
+
current: "MISSING",
|
|
12760
|
+
target: "WEAK"
|
|
12761
|
+
},
|
|
12762
|
+
retrySemantics: {
|
|
12763
|
+
current: "MISSING",
|
|
12764
|
+
target: "WEAK",
|
|
12765
|
+
gapId: "GAP-SE-009"
|
|
12766
|
+
},
|
|
12767
|
+
parameterization: {
|
|
12768
|
+
current: "MISSING",
|
|
12769
|
+
target: "WEAK",
|
|
12770
|
+
gapId: "GAP-SE-010"
|
|
12771
|
+
},
|
|
12772
|
+
snapshotSemantics: {
|
|
12773
|
+
current: "NOT_APPLICABLE",
|
|
12774
|
+
target: "NOT_APPLICABLE"
|
|
12775
|
+
},
|
|
12776
|
+
parallelismWorkerSafety: {
|
|
12777
|
+
current: "NOT_APPLICABLE",
|
|
12778
|
+
target: "NOT_APPLICABLE"
|
|
12779
|
+
},
|
|
12780
|
+
runtimeEvidence: {
|
|
12781
|
+
current: "MISSING",
|
|
12782
|
+
target: "WEAK",
|
|
12783
|
+
gapId: "GAP-SE-011"
|
|
12784
|
+
},
|
|
12785
|
+
adversarialCorpus: {
|
|
12786
|
+
current: "MISSING",
|
|
12787
|
+
target: "WEAK",
|
|
12788
|
+
gapId: "GAP-SE-012"
|
|
12789
|
+
},
|
|
12790
|
+
knownLimitationCoverage: {
|
|
12791
|
+
current: "MISSING",
|
|
12792
|
+
target: "WEAK",
|
|
12793
|
+
gapId: "GAP-SE-013"
|
|
12794
|
+
},
|
|
12795
|
+
ciIntegration: {
|
|
12796
|
+
current: "MISSING",
|
|
12797
|
+
target: "PARTIAL"
|
|
12798
|
+
}
|
|
12799
|
+
}),
|
|
12800
|
+
testng: withCommonMature({
|
|
12801
|
+
discovery: {
|
|
12802
|
+
current: "PARTIAL",
|
|
12803
|
+
target: "GOOD",
|
|
12804
|
+
gapId: "GAP-TN-001"
|
|
12805
|
+
},
|
|
12806
|
+
astUsage: {
|
|
12807
|
+
current: "MISSING",
|
|
12808
|
+
target: "PARTIAL",
|
|
12809
|
+
gapId: "GAP-TN-002"
|
|
12810
|
+
},
|
|
12811
|
+
semanticUnderstanding: {
|
|
12812
|
+
current: "MISSING",
|
|
12813
|
+
target: "WEAK",
|
|
12814
|
+
gapId: "GAP-TN-003"
|
|
12815
|
+
},
|
|
12816
|
+
assertionUnderstanding: {
|
|
12817
|
+
current: "MISSING",
|
|
12818
|
+
target: "WEAK",
|
|
12819
|
+
gapId: "GAP-TN-004"
|
|
12820
|
+
},
|
|
12821
|
+
lifecycleModeling: {
|
|
12822
|
+
current: "MISSING",
|
|
12823
|
+
target: "WEAK",
|
|
12824
|
+
gapId: "GAP-TN-005"
|
|
12825
|
+
},
|
|
12826
|
+
asyncSemantics: {
|
|
12827
|
+
current: "MISSING",
|
|
12828
|
+
target: "WEAK",
|
|
12829
|
+
gapId: "GAP-TN-006"
|
|
12830
|
+
},
|
|
12831
|
+
fixtureModeling: {
|
|
12832
|
+
current: "MISSING",
|
|
12833
|
+
target: "WEAK",
|
|
12834
|
+
gapId: "GAP-TN-007"
|
|
12835
|
+
},
|
|
12836
|
+
mockingSemantics: {
|
|
12837
|
+
current: "NOT_APPLICABLE",
|
|
12838
|
+
target: "NOT_APPLICABLE"
|
|
12839
|
+
},
|
|
12840
|
+
sharedStateAnalysis: {
|
|
12841
|
+
current: "MISSING",
|
|
12842
|
+
target: "WEAK",
|
|
12843
|
+
gapId: "GAP-TN-008"
|
|
12844
|
+
},
|
|
12845
|
+
errorClassification: {
|
|
12846
|
+
current: "MISSING",
|
|
12847
|
+
target: "WEAK"
|
|
12848
|
+
},
|
|
12849
|
+
retrySemantics: {
|
|
12850
|
+
current: "MISSING",
|
|
12851
|
+
target: "WEAK",
|
|
12852
|
+
gapId: "GAP-TN-009"
|
|
12853
|
+
},
|
|
12854
|
+
parameterization: {
|
|
12855
|
+
current: "MISSING",
|
|
12856
|
+
target: "WEAK",
|
|
12857
|
+
gapId: "GAP-TN-010"
|
|
12858
|
+
},
|
|
12859
|
+
snapshotSemantics: {
|
|
12860
|
+
current: "NOT_APPLICABLE",
|
|
12861
|
+
target: "NOT_APPLICABLE"
|
|
12862
|
+
},
|
|
12863
|
+
parallelismWorkerSafety: {
|
|
12864
|
+
current: "MISSING",
|
|
12865
|
+
target: "WEAK",
|
|
12866
|
+
gapId: "GAP-TN-011"
|
|
12867
|
+
},
|
|
12868
|
+
runtimeEvidence: {
|
|
12869
|
+
current: "MISSING",
|
|
12870
|
+
target: "WEAK",
|
|
12871
|
+
gapId: "GAP-TN-012"
|
|
12872
|
+
},
|
|
12873
|
+
adversarialCorpus: {
|
|
12874
|
+
current: "MISSING",
|
|
12875
|
+
target: "WEAK",
|
|
12876
|
+
gapId: "GAP-TN-013"
|
|
12877
|
+
},
|
|
12878
|
+
knownLimitationCoverage: {
|
|
12879
|
+
current: "MISSING",
|
|
12880
|
+
target: "WEAK",
|
|
12881
|
+
gapId: "GAP-TN-014"
|
|
12882
|
+
},
|
|
12883
|
+
ciIntegration: {
|
|
12884
|
+
current: "MISSING",
|
|
12885
|
+
target: "PARTIAL"
|
|
12886
|
+
}
|
|
12887
|
+
}),
|
|
12888
|
+
"github-actions": COMMON_CI_OVERRIDES,
|
|
12889
|
+
"azure-devops": COMMON_CI_OVERRIDES,
|
|
12890
|
+
jenkins: COMMON_CI_OVERRIDES,
|
|
12891
|
+
"gitlab-ci": {
|
|
12892
|
+
discovery: {
|
|
12893
|
+
current: "MISSING",
|
|
12894
|
+
target: "PARTIAL",
|
|
12895
|
+
gapId: "GAP-GL-001"
|
|
12896
|
+
},
|
|
12897
|
+
semanticUnderstanding: {
|
|
12898
|
+
current: "MISSING",
|
|
12899
|
+
target: "WEAK",
|
|
12900
|
+
gapId: "GAP-GL-002"
|
|
12901
|
+
},
|
|
12902
|
+
errorClassification: {
|
|
12903
|
+
current: "MISSING",
|
|
12904
|
+
target: "WEAK",
|
|
12905
|
+
gapId: "GAP-GL-003"
|
|
12906
|
+
},
|
|
12907
|
+
retrySemantics: {
|
|
12908
|
+
current: "MISSING",
|
|
12909
|
+
target: "WEAK",
|
|
12910
|
+
gapId: "GAP-GL-004"
|
|
12911
|
+
},
|
|
12912
|
+
parameterization: {
|
|
12913
|
+
current: "MISSING",
|
|
12914
|
+
target: "WEAK",
|
|
12915
|
+
gapId: "GAP-GL-005"
|
|
12916
|
+
},
|
|
12917
|
+
runtimeEvidence: {
|
|
12918
|
+
current: "MISSING",
|
|
12919
|
+
target: "WEAK",
|
|
12920
|
+
gapId: "GAP-GL-006"
|
|
12921
|
+
},
|
|
12922
|
+
adversarialCorpus: {
|
|
12923
|
+
current: "MISSING",
|
|
12924
|
+
target: "WEAK",
|
|
12925
|
+
gapId: "GAP-GL-007"
|
|
12926
|
+
},
|
|
12927
|
+
knownLimitationCoverage: {
|
|
12928
|
+
current: "MISSING",
|
|
12929
|
+
target: "WEAK",
|
|
12930
|
+
gapId: "GAP-GL-008"
|
|
12931
|
+
},
|
|
12932
|
+
ciIntegration: {
|
|
12933
|
+
current: "MISSING",
|
|
12934
|
+
target: "PARTIAL",
|
|
12935
|
+
gapId: "GAP-GL-009"
|
|
12936
|
+
},
|
|
12937
|
+
reportingQuality: {
|
|
12938
|
+
current: "MISSING",
|
|
12939
|
+
target: "PARTIAL"
|
|
12940
|
+
},
|
|
12941
|
+
configDiscovery: {
|
|
12942
|
+
current: "MISSING",
|
|
12943
|
+
target: "PARTIAL",
|
|
12944
|
+
gapId: "GAP-GL-010"
|
|
12945
|
+
},
|
|
12946
|
+
versionCompatibility: {
|
|
12947
|
+
current: "MISSING",
|
|
12948
|
+
target: "PARTIAL"
|
|
12949
|
+
},
|
|
12950
|
+
documentationCoverage: {
|
|
12951
|
+
current: "MISSING",
|
|
12952
|
+
target: "PARTIAL"
|
|
12953
|
+
},
|
|
12954
|
+
migrationGuidance: {
|
|
12955
|
+
current: "MISSING",
|
|
12956
|
+
target: "PARTIAL"
|
|
12957
|
+
},
|
|
12958
|
+
crossFrameworkInterop: {
|
|
12959
|
+
current: "MISSING",
|
|
12960
|
+
target: "PARTIAL"
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
};
|
|
12964
|
+
const FRAMEWORK_SCORECARDS = [
|
|
12965
|
+
makeScorecard("playwright", FRAMEWORK_OVERRIDES["playwright"]),
|
|
12966
|
+
makeScorecard("jest", FRAMEWORK_OVERRIDES["jest"]),
|
|
12967
|
+
makeScorecard("vitest", FRAMEWORK_OVERRIDES["vitest"]),
|
|
12968
|
+
makeScorecard("pytest", FRAMEWORK_OVERRIDES["pytest"]),
|
|
12969
|
+
makeScorecard("junit", FRAMEWORK_OVERRIDES["junit"]),
|
|
12970
|
+
makeScorecard("nunit", FRAMEWORK_OVERRIDES["nunit"]),
|
|
12971
|
+
makeScorecard("xunit", FRAMEWORK_OVERRIDES["xunit"]),
|
|
12972
|
+
makeScorecard("cypress", FRAMEWORK_OVERRIDES["cypress"]),
|
|
12973
|
+
makeScorecard("selenium", FRAMEWORK_OVERRIDES["selenium"]),
|
|
12974
|
+
makeScorecard("testng", FRAMEWORK_OVERRIDES["testng"]),
|
|
12975
|
+
makeScorecard("github-actions", FRAMEWORK_OVERRIDES["github-actions"]),
|
|
12976
|
+
makeScorecard("azure-devops", FRAMEWORK_OVERRIDES["azure-devops"]),
|
|
12977
|
+
makeScorecard("jenkins", FRAMEWORK_OVERRIDES["jenkins"]),
|
|
12978
|
+
makeScorecard("gitlab-ci", FRAMEWORK_OVERRIDES["gitlab-ci"])
|
|
12979
|
+
];
|
|
12980
|
+
//#endregion
|
|
12981
|
+
//#region src/engine/framework-maturity.ts
|
|
12982
|
+
/**
|
|
12983
|
+
* M5 — Framework Maturity Tracking for Playwright.
|
|
12984
|
+
*
|
|
12985
|
+
* Provides functions to track and report framework maturity levels,
|
|
12986
|
+
* with special focus on Playwright's progression from F4 toward F5.
|
|
12987
|
+
*/
|
|
12988
|
+
const MATURITY_SCORES = {
|
|
12989
|
+
F0: 0,
|
|
12990
|
+
F1: 20,
|
|
12991
|
+
F2: 40,
|
|
12992
|
+
F3: 60,
|
|
12993
|
+
F4: 80,
|
|
12994
|
+
F5: 95
|
|
12995
|
+
};
|
|
12996
|
+
const SUPPORT_SCORES = {
|
|
12997
|
+
OFFICIAL_FULL: 100,
|
|
12998
|
+
OFFICIAL_PARTIAL: 75,
|
|
12999
|
+
EXPERIMENTAL: 50,
|
|
13000
|
+
DISCOVERED: 25,
|
|
13001
|
+
UNSUPPORTED: 0,
|
|
13002
|
+
DEGRADED: 30,
|
|
13003
|
+
DEPRECATED: 10
|
|
13004
|
+
};
|
|
13005
|
+
function getPlaywrightMaturity() {
|
|
13006
|
+
const progress = getFrameworkMaturity("playwright");
|
|
13007
|
+
if (!progress) throw new Error("Playwright is missing from the framework inventory");
|
|
13008
|
+
return progress;
|
|
13009
|
+
}
|
|
13010
|
+
function getPlaywrightMaturityReport() {
|
|
13011
|
+
const progress = getPlaywrightMaturity();
|
|
13012
|
+
const entries = FRAMEWORK_SCORECARDS.find((s) => s.frameworkId === "playwright")?.entries ?? [];
|
|
13013
|
+
const recommendations = [];
|
|
13014
|
+
const missingDimensions = entries.filter((e) => e.current === "MISSING" || e.current === "WEAK");
|
|
13015
|
+
for (const dim of missingDimensions) recommendations.push(`Improve ${dim.dimension} from ${dim.current} to ${dim.target} (gap: ${dim.gapId ?? "none"})`);
|
|
13016
|
+
if (progress.currentMaturity !== progress.targetMaturity) recommendations.push(`Target ${progress.targetMaturity} maturity: complete remaining framework dimensions and achieve OFFICIAL_FULL support`);
|
|
13017
|
+
if (progress.currentSupportStatus !== "OFFICIAL_FULL") recommendations.push(`Upgrade support status from ${progress.currentSupportStatus} to OFFICIAL_FULL`);
|
|
13018
|
+
return {
|
|
13019
|
+
...progress,
|
|
13020
|
+
scorecardEntries: Object.freeze(entries),
|
|
13021
|
+
recommendations
|
|
13022
|
+
};
|
|
13023
|
+
}
|
|
13024
|
+
function getFrameworkMaturity(frameworkId) {
|
|
13025
|
+
const fw = getFrameworkById(frameworkId);
|
|
13026
|
+
if (!fw) return void 0;
|
|
13027
|
+
const entries = FRAMEWORK_SCORECARDS.find((s) => s.frameworkId === frameworkId)?.entries ?? [];
|
|
13028
|
+
const totalDimensions = entries.length;
|
|
13029
|
+
const completeDimensions = entries.filter((e) => e.current === "EXCELLENT" || e.current === "GOOD").length;
|
|
13030
|
+
const gaps = entries.filter((e) => e.gapId !== null).map((e) => e.gapId ?? "");
|
|
13031
|
+
const currentScore = MATURITY_SCORES[fw.maturity] ?? 0;
|
|
13032
|
+
const supportScore = SUPPORT_SCORES[fw.supportStatus] ?? 0;
|
|
13033
|
+
const dimensionScore = totalDimensions > 0 ? Math.round(completeDimensions / totalDimensions * 100) : 0;
|
|
13034
|
+
const maturityScore = Math.round(currentScore * .4 + supportScore * .3 + dimensionScore * .3);
|
|
13035
|
+
return {
|
|
13036
|
+
frameworkId,
|
|
13037
|
+
currentMaturity: fw.maturity,
|
|
13038
|
+
targetMaturity: fw.targetMaturity,
|
|
13039
|
+
currentSupportStatus: fw.supportStatus,
|
|
13040
|
+
targetSupportStatus: fw.targetSupportStatus,
|
|
13041
|
+
maturityScore,
|
|
13042
|
+
dimensionsComplete: completeDimensions,
|
|
13043
|
+
dimensionsTotal: totalDimensions,
|
|
13044
|
+
gaps,
|
|
13045
|
+
progressPercentage: Math.min(100, Math.round(maturityScore / 100 * 100))
|
|
13046
|
+
};
|
|
13047
|
+
}
|
|
13048
|
+
function getAllFrameworkMaturity() {
|
|
13049
|
+
return FRAMEWORK_INVENTORY.map((fw) => getFrameworkMaturity(fw.frameworkId)).filter((m) => m !== void 0);
|
|
13050
|
+
}
|
|
13051
|
+
function renderPlaywrightMaturityReport(report) {
|
|
13052
|
+
const lines = [];
|
|
13053
|
+
lines.push(`Playwright Framework Maturity Report`);
|
|
13054
|
+
lines.push(`Current: ${report.currentMaturity} → Target: ${report.targetMaturity}`);
|
|
13055
|
+
lines.push(`Support: ${report.currentSupportStatus} → ${report.targetSupportStatus}`);
|
|
13056
|
+
lines.push(`Maturity Score: ${report.maturityScore}/100 (${report.progressPercentage}%)`);
|
|
13057
|
+
lines.push(`Dimensions: ${report.dimensionsComplete}/${report.dimensionsTotal} complete`);
|
|
13058
|
+
lines.push("");
|
|
13059
|
+
if (report.gaps.length > 0) {
|
|
13060
|
+
lines.push("Open Gaps:");
|
|
13061
|
+
for (const gap of report.gaps) lines.push(` - ${gap}`);
|
|
13062
|
+
lines.push("");
|
|
13063
|
+
}
|
|
13064
|
+
if (report.recommendations.length > 0) {
|
|
13065
|
+
lines.push("Recommendations:");
|
|
13066
|
+
for (const rec of report.recommendations) lines.push(` • ${rec}`);
|
|
13067
|
+
}
|
|
13068
|
+
return lines.join("\n").trimEnd();
|
|
13069
|
+
}
|
|
13070
|
+
//#endregion
|
|
13071
|
+
//#region src/engine/historical-trust.ts
|
|
13072
|
+
/**
|
|
13073
|
+
* Compute a trust snapshot from a scan result and run identity.
|
|
13074
|
+
* Pure function — deterministic output for the same input.
|
|
13075
|
+
*/
|
|
13076
|
+
function computeTrustSnapshot(result, runIdentity, trustSummary, recordedAt) {
|
|
13077
|
+
return {
|
|
13078
|
+
scanId: runIdentity.scanId,
|
|
13079
|
+
timestamp: recordedAt,
|
|
13080
|
+
score: result.score,
|
|
13081
|
+
findings: result.findings.length,
|
|
13082
|
+
errors: result.findings.filter((f) => f.severity === "error").length,
|
|
13083
|
+
warnings: result.findings.filter((f) => f.severity === "warning").length,
|
|
13084
|
+
infos: result.findings.filter((f) => f.severity === "info").length,
|
|
13085
|
+
advisory: result.findings.filter((f) => f.evidenceLevel === "E0").length,
|
|
13086
|
+
trustLevel: trustSummary.level,
|
|
13087
|
+
confidence: trustSummary.confidence,
|
|
13088
|
+
evidenceCoverage: trustSummary.evidenceCoverage,
|
|
13089
|
+
inconclusiveRate: trustSummary.inconclusiveRate,
|
|
13090
|
+
partial: result.partial,
|
|
13091
|
+
frameworkCount: result.frameworkDetectionUnknown ? 0 : 1,
|
|
13092
|
+
ruleCount: result.findings.length > 0 ? new Set(result.findings.map((f) => f.ruleId)).size : 0
|
|
13093
|
+
};
|
|
13094
|
+
}
|
|
13095
|
+
/**
|
|
13096
|
+
* Analyze trust trends across multiple snapshots.
|
|
13097
|
+
* Pure function — deterministic output for the same input.
|
|
13098
|
+
*/
|
|
13099
|
+
function analyzeTrustTrends(snapshots) {
|
|
13100
|
+
if (snapshots.length < 2) return {
|
|
13101
|
+
snapshots: [...snapshots],
|
|
13102
|
+
overallDirection: "insufficient-data",
|
|
13103
|
+
scoreDelta: 0,
|
|
13104
|
+
findingDelta: 0,
|
|
13105
|
+
confidenceDelta: 0,
|
|
13106
|
+
regressions: [],
|
|
13107
|
+
improvements: [],
|
|
13108
|
+
categorizedDebt: []
|
|
13109
|
+
};
|
|
13110
|
+
const first = snapshots[0];
|
|
13111
|
+
const last = snapshots[snapshots.length - 1];
|
|
13112
|
+
const scoreDelta = (last.score ?? 0) - (first.score ?? 0);
|
|
13113
|
+
const findingDelta = last.findings - first.findings;
|
|
13114
|
+
const confidenceDelta = last.confidence - first.confidence;
|
|
13115
|
+
const regressions = [];
|
|
13116
|
+
const improvements = [];
|
|
13117
|
+
if (scoreDelta < 0) regressions.push({
|
|
13118
|
+
type: "score-drop",
|
|
13119
|
+
description: `Score dropped by ${Math.abs(scoreDelta)} points`,
|
|
13120
|
+
magnitude: Math.abs(scoreDelta),
|
|
13121
|
+
snapshot: last
|
|
13122
|
+
});
|
|
13123
|
+
else if (scoreDelta > 0) improvements.push({
|
|
13124
|
+
type: "score-increase",
|
|
13125
|
+
description: `Score increased by ${scoreDelta} points`,
|
|
13126
|
+
magnitude: scoreDelta,
|
|
13127
|
+
snapshot: last
|
|
13128
|
+
});
|
|
13129
|
+
if (findingDelta > 0) regressions.push({
|
|
13130
|
+
type: "finding-increase",
|
|
13131
|
+
description: `${findingDelta} more findings detected`,
|
|
13132
|
+
magnitude: findingDelta,
|
|
13133
|
+
snapshot: last
|
|
13134
|
+
});
|
|
13135
|
+
else if (findingDelta < 0) improvements.push({
|
|
13136
|
+
type: "finding-decrease",
|
|
13137
|
+
description: `${Math.abs(findingDelta)} fewer findings detected`,
|
|
13138
|
+
magnitude: Math.abs(findingDelta),
|
|
13139
|
+
snapshot: last
|
|
13140
|
+
});
|
|
13141
|
+
if (confidenceDelta < 0) regressions.push({
|
|
13142
|
+
type: "confidence-drop",
|
|
13143
|
+
description: `Confidence dropped by ${Math.abs(confidenceDelta)}`,
|
|
13144
|
+
magnitude: Math.abs(confidenceDelta),
|
|
13145
|
+
snapshot: last
|
|
13146
|
+
});
|
|
13147
|
+
else if (confidenceDelta > 0) improvements.push({
|
|
13148
|
+
type: "confidence-increase",
|
|
13149
|
+
description: `Confidence increased by ${confidenceDelta}`,
|
|
13150
|
+
magnitude: confidenceDelta,
|
|
13151
|
+
snapshot: last
|
|
13152
|
+
});
|
|
13153
|
+
if (last.errors > first.errors) regressions.push({
|
|
13154
|
+
type: "new-error",
|
|
13155
|
+
description: `${last.errors - first.errors} new error(s) detected`,
|
|
13156
|
+
magnitude: last.errors - first.errors,
|
|
13157
|
+
snapshot: last
|
|
13158
|
+
});
|
|
13159
|
+
const overallDirection = regressions.length > improvements.length ? "degrading" : improvements.length > regressions.length ? "improving" : "stable";
|
|
13160
|
+
const categorizedDebt = categorizeTrustDebt(snapshots);
|
|
13161
|
+
return {
|
|
13162
|
+
snapshots: [...snapshots],
|
|
13163
|
+
overallDirection,
|
|
13164
|
+
scoreDelta,
|
|
13165
|
+
findingDelta,
|
|
13166
|
+
confidenceDelta,
|
|
13167
|
+
regressions,
|
|
13168
|
+
improvements,
|
|
13169
|
+
categorizedDebt
|
|
13170
|
+
};
|
|
13171
|
+
}
|
|
13172
|
+
/**
|
|
13173
|
+
* Categorize trust debt from scan snapshots.
|
|
13174
|
+
*/
|
|
13175
|
+
function categorizeTrustDebt(snapshots) {
|
|
13176
|
+
const debt = [];
|
|
13177
|
+
const latest = snapshots[snapshots.length - 1];
|
|
13178
|
+
if (latest) {
|
|
13179
|
+
if (latest.advisory > 0) debt.push({
|
|
13180
|
+
category: "suppression",
|
|
13181
|
+
description: `${latest.advisory} advisory findings that may be masking issues`,
|
|
13182
|
+
count: latest.advisory,
|
|
13183
|
+
severity: latest.advisory > 10 ? "high" : "medium"
|
|
13184
|
+
});
|
|
13185
|
+
if (latest.inconclusiveRate > .3) debt.push({
|
|
13186
|
+
category: "evidence",
|
|
13187
|
+
description: `High inconclusive rate (${(latest.inconclusiveRate * 100).toFixed(1)}%) suggests incomplete analysis`,
|
|
13188
|
+
count: Math.round(latest.inconclusiveRate * latest.findings),
|
|
13189
|
+
severity: "high"
|
|
13190
|
+
});
|
|
13191
|
+
if (latest.partial) debt.push({
|
|
13192
|
+
category: "framework",
|
|
13193
|
+
description: "Partial scan — some files were not analyzed",
|
|
13194
|
+
count: 1,
|
|
13195
|
+
severity: "medium"
|
|
13196
|
+
});
|
|
13197
|
+
if (latest.warnings > latest.errors * 2) debt.push({
|
|
13198
|
+
category: "weak-assertion",
|
|
13199
|
+
description: "Warning-to-error ratio suggests weak assertions",
|
|
13200
|
+
count: latest.warnings,
|
|
13201
|
+
severity: "low"
|
|
13202
|
+
});
|
|
13203
|
+
}
|
|
13204
|
+
return debt.sort((a, b) => {
|
|
13205
|
+
const severityOrder = {
|
|
13206
|
+
high: 0,
|
|
13207
|
+
medium: 1,
|
|
13208
|
+
low: 2
|
|
13209
|
+
};
|
|
13210
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
13211
|
+
});
|
|
13212
|
+
}
|
|
13213
|
+
function renderTrustTrend(trend) {
|
|
13214
|
+
const lines = [];
|
|
13215
|
+
lines.push("Historical Trust Trend Analysis");
|
|
13216
|
+
lines.push(`Snapshots: ${trend.snapshots.length}`);
|
|
13217
|
+
lines.push(`Overall Direction: ${trend.overallDirection}`);
|
|
13218
|
+
lines.push(`Score Delta: ${trend.scoreDelta > 0 ? "+" : ""}${trend.scoreDelta}`);
|
|
13219
|
+
lines.push(`Finding Delta: ${trend.findingDelta > 0 ? "+" : ""}${trend.findingDelta}`);
|
|
13220
|
+
lines.push(`Confidence Delta: ${trend.confidenceDelta > 0 ? "+" : ""}${trend.confidenceDelta}`);
|
|
13221
|
+
lines.push("");
|
|
13222
|
+
if (trend.regressions.length > 0) {
|
|
13223
|
+
lines.push("Regressions:");
|
|
13224
|
+
for (const r of trend.regressions) lines.push(` ✗ ${r.description} (magnitude: ${r.magnitude})`);
|
|
13225
|
+
lines.push("");
|
|
13226
|
+
}
|
|
13227
|
+
if (trend.improvements.length > 0) {
|
|
13228
|
+
lines.push("Improvements:");
|
|
13229
|
+
for (const i of trend.improvements) lines.push(` ✓ ${i.description} (magnitude: ${i.magnitude})`);
|
|
13230
|
+
lines.push("");
|
|
13231
|
+
}
|
|
13232
|
+
if (trend.categorizedDebt.length > 0) {
|
|
13233
|
+
lines.push("Trust Debt:");
|
|
13234
|
+
for (const d of trend.categorizedDebt) {
|
|
13235
|
+
const icon = d.severity === "high" ? "🔴" : d.severity === "medium" ? "🟡" : "🟢";
|
|
13236
|
+
lines.push(` ${icon} [${d.category}] ${d.description} (${d.count})`);
|
|
13237
|
+
}
|
|
13238
|
+
lines.push("");
|
|
13239
|
+
}
|
|
13240
|
+
return lines.join("\n").trimEnd();
|
|
13241
|
+
}
|
|
13242
|
+
//#endregion
|
|
13243
|
+
//#region src/engine/machine-contract-verification.ts
|
|
13244
|
+
/**
|
|
13245
|
+
* M11 — Machine Contract Verification.
|
|
13246
|
+
*
|
|
13247
|
+
* Verifies that the machine contract output is consistent with
|
|
13248
|
+
* the scan results. Ensures the contract is a faithful
|
|
13249
|
+
* projection of the canonical scan result — no fabricated fields,
|
|
13250
|
+
* no missing evidence, no drift between the contract and the
|
|
13251
|
+
* source data.
|
|
13252
|
+
*
|
|
13253
|
+
* The contract is the machine-readable truth: every field must
|
|
13254
|
+
* be derivable from the scan result, and every derivation must
|
|
13255
|
+
* be deterministic.
|
|
13256
|
+
*/
|
|
13257
|
+
function isRecord$1(value) {
|
|
13258
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13259
|
+
}
|
|
13260
|
+
function optionalDeepEqual(actual, expected) {
|
|
13261
|
+
return actual === void 0 && expected === void 0 ? true : isDeepStrictEqual(actual, expected);
|
|
13262
|
+
}
|
|
13263
|
+
/**
|
|
13264
|
+
* Verify that a machine contract is consistent with the scan
|
|
13265
|
+
* result it claims to represent. Returns a detailed verification
|
|
13266
|
+
* report with any violations found.
|
|
13267
|
+
*/
|
|
13268
|
+
function verifyMachineContract(result, contract = buildMachineContract(result)) {
|
|
13269
|
+
const violations = [];
|
|
13270
|
+
const checks = [];
|
|
13271
|
+
const expected = buildMachineContract(result);
|
|
13272
|
+
const contractRecord = isRecord$1(contract) ? contract : Object.create(null);
|
|
13273
|
+
const contractVersion = typeof contractRecord.contractVersion === "number" ? contractRecord.contractVersion : 0;
|
|
13274
|
+
const versionMatches = contractVersion === 1;
|
|
13275
|
+
if (!versionMatches) violations.push(`Unsupported contract version ${contractVersion}`);
|
|
13276
|
+
checks.push({
|
|
13277
|
+
name: "contract-version",
|
|
13278
|
+
status: versionMatches ? "pass" : "fail",
|
|
13279
|
+
detail: versionMatches ? `Contract version ${contractVersion} confirmed` : `Unsupported contract version ${contractVersion}`
|
|
13280
|
+
});
|
|
13281
|
+
const suppliedSummary = contractRecord.summary;
|
|
13282
|
+
const digestMatch = isRecord$1(suppliedSummary) && suppliedSummary.digest === expected.summary.digest;
|
|
13283
|
+
if (!digestMatch) violations.push("Machine contract digest mismatch");
|
|
13284
|
+
checks.push({
|
|
13285
|
+
name: "digest-integrity",
|
|
13286
|
+
status: digestMatch ? "pass" : "fail",
|
|
13287
|
+
detail: digestMatch ? "Digest matches scan result" : "Digest does not match scan result"
|
|
13288
|
+
});
|
|
13289
|
+
const summaryMatch = isDeepStrictEqual(suppliedSummary, expected.summary);
|
|
13290
|
+
if (!summaryMatch) violations.push("Machine contract summary mismatch");
|
|
13291
|
+
checks.push({
|
|
13292
|
+
name: "summary",
|
|
13293
|
+
status: summaryMatch ? "pass" : "fail",
|
|
13294
|
+
detail: summaryMatch ? "Summary matches scan result" : "Summary does not match scan result"
|
|
13295
|
+
});
|
|
13296
|
+
const trustSummaryMatch = optionalDeepEqual(contractRecord.trustSummary, expected.trustSummary);
|
|
13297
|
+
if (!trustSummaryMatch) violations.push("Trust summary mismatch");
|
|
13298
|
+
checks.push({
|
|
13299
|
+
name: "trust-summary",
|
|
13300
|
+
status: trustSummaryMatch ? "pass" : "fail",
|
|
13301
|
+
detail: trustSummaryMatch ? "Trust summary consistent with contract" : "Trust summary does not match scan result"
|
|
13302
|
+
});
|
|
13303
|
+
const provenanceMatch = optionalDeepEqual(contractRecord.provenance, expected.provenance);
|
|
13304
|
+
if (!provenanceMatch) violations.push("Provenance mismatch");
|
|
13305
|
+
checks.push({
|
|
13306
|
+
name: "provenance",
|
|
13307
|
+
status: provenanceMatch ? "pass" : "fail",
|
|
13308
|
+
detail: provenanceMatch ? "Provenance matches scan result" : "Provenance does not match scan result"
|
|
13309
|
+
});
|
|
13310
|
+
const forensicVerdictsMatch = optionalDeepEqual(contractRecord.forensicVerdicts, expected.forensicVerdicts);
|
|
13311
|
+
if (!forensicVerdictsMatch) violations.push("Forensic verdicts mismatch");
|
|
13312
|
+
checks.push({
|
|
13313
|
+
name: "forensic-verdicts",
|
|
13314
|
+
status: forensicVerdictsMatch ? "pass" : "fail",
|
|
13315
|
+
detail: forensicVerdictsMatch ? "Forensic verdicts match scan result" : "Forensic verdicts do not match scan result"
|
|
13316
|
+
});
|
|
13317
|
+
const evidenceIntegrity = verifyEvidenceIntegrity(result);
|
|
13318
|
+
if (!evidenceIntegrity) {
|
|
13319
|
+
violations.push("Evidence integrity check failed");
|
|
13320
|
+
checks.push({
|
|
13321
|
+
name: "evidence-integrity",
|
|
13322
|
+
status: "fail",
|
|
13323
|
+
detail: "Evidence in contract does not match scan result"
|
|
13324
|
+
});
|
|
13325
|
+
} else checks.push({
|
|
13326
|
+
name: "evidence-integrity",
|
|
13327
|
+
status: "pass",
|
|
13328
|
+
detail: "All evidence in contract matches scan result"
|
|
13329
|
+
});
|
|
13330
|
+
const completenessCheck = verifyCompleteness(result, contractRecord.completeness, expected.completeness);
|
|
13331
|
+
if (completenessCheck.length > 0) {
|
|
13332
|
+
violations.push(...completenessCheck);
|
|
13333
|
+
checks.push({
|
|
13334
|
+
name: "completeness",
|
|
13335
|
+
status: "fail",
|
|
13336
|
+
detail: `${completenessCheck.length} completeness violation(s) found`
|
|
13337
|
+
});
|
|
13338
|
+
} else checks.push({
|
|
13339
|
+
name: "completeness",
|
|
13340
|
+
status: "pass",
|
|
13341
|
+
detail: "All completeness fields verified"
|
|
13342
|
+
});
|
|
13343
|
+
const findingCount = Array.isArray(result.findings) ? result.findings.length : 0;
|
|
13344
|
+
const annotationsMatch = isDeepStrictEqual(contractRecord.annotations, expected.annotations) && contractRecord.annotationsTruncated === expected.annotationsTruncated;
|
|
13345
|
+
if (!annotationsMatch) violations.push("Annotation projection mismatch");
|
|
13346
|
+
checks.push({
|
|
13347
|
+
name: "annotations",
|
|
13348
|
+
status: annotationsMatch ? "pass" : "fail",
|
|
13349
|
+
detail: annotationsMatch ? `Expected ${findingCount} annotation(s)` : `Expected ${expected.annotations.length} annotation(s)`
|
|
13350
|
+
});
|
|
13351
|
+
return {
|
|
13352
|
+
passed: violations.length === 0,
|
|
13353
|
+
contractVersion,
|
|
13354
|
+
violations,
|
|
13355
|
+
digestMatch,
|
|
13356
|
+
summaryMatch,
|
|
13357
|
+
completenessCheck,
|
|
13358
|
+
trustSummaryMatch,
|
|
13359
|
+
provenanceMatch,
|
|
13360
|
+
forensicVerdictsMatch,
|
|
13361
|
+
annotationsMatch,
|
|
13362
|
+
evidenceIntegrity,
|
|
13363
|
+
checks,
|
|
13364
|
+
summary: {
|
|
13365
|
+
findings: findingCount,
|
|
13366
|
+
score: result.score
|
|
13367
|
+
}
|
|
13368
|
+
};
|
|
13369
|
+
}
|
|
13370
|
+
function verifyEvidenceIntegrity(result) {
|
|
13371
|
+
if (!Array.isArray(result.findings)) return false;
|
|
13372
|
+
for (const finding of result.findings) if (!finding.ruleId || !finding.file || !finding.message || !Number.isInteger(finding.line) || finding.line < 1 || !Number.isInteger(finding.column) || finding.column < 1) return false;
|
|
13373
|
+
return true;
|
|
13374
|
+
}
|
|
13375
|
+
function verifyCompleteness(result, completeness, expected) {
|
|
13376
|
+
const violations = [];
|
|
13377
|
+
if (!isRecord$1(completeness)) return ["Missing completeness"];
|
|
13378
|
+
const supplied = completeness;
|
|
13379
|
+
if (supplied.partial !== result.partial) violations.push("Partial flag mismatch");
|
|
13380
|
+
if (!isDeepStrictEqual(supplied, expected)) violations.push("Completeness projection mismatch");
|
|
13381
|
+
if (!result.analysisStatus) violations.push("Missing analysisStatus");
|
|
13382
|
+
return violations;
|
|
13383
|
+
}
|
|
13384
|
+
function renderContractVerification(result) {
|
|
13385
|
+
const lines = [];
|
|
13386
|
+
lines.push("Machine Contract Verification");
|
|
13387
|
+
lines.push(`Result: ${result.passed ? "PASSED" : "FAILED"}`);
|
|
13388
|
+
lines.push(`Contract Version: ${result.contractVersion}`);
|
|
13389
|
+
lines.push(`Digest Match: ${result.digestMatch}`);
|
|
13390
|
+
lines.push(`Summary Match: ${result.summaryMatch}`);
|
|
13391
|
+
lines.push(`Trust Summary Match: ${result.trustSummaryMatch}`);
|
|
13392
|
+
lines.push(`Provenance Match: ${result.provenanceMatch}`);
|
|
13393
|
+
lines.push(`Forensic Verdicts Match: ${result.forensicVerdictsMatch}`);
|
|
13394
|
+
lines.push(`Annotations Match: ${result.annotationsMatch}`);
|
|
13395
|
+
lines.push(`Evidence Integrity: ${result.evidenceIntegrity}`);
|
|
13396
|
+
if (result.freshScanMatch !== void 0) lines.push(`Fresh Scan Match: ${result.freshScanMatch}`);
|
|
13397
|
+
if (result.artifactResultMatch !== void 0) lines.push(`Artifact Result Match: ${result.artifactResultMatch}`);
|
|
13398
|
+
lines.push(`Findings: ${result.summary.findings}`);
|
|
13399
|
+
lines.push(`Score: ${result.summary.score}`);
|
|
13400
|
+
lines.push("");
|
|
13401
|
+
if (result.violations.length > 0) {
|
|
13402
|
+
lines.push("Violations:");
|
|
13403
|
+
for (const v of result.violations) lines.push(` ✗ ${v}`);
|
|
13404
|
+
lines.push("");
|
|
13405
|
+
}
|
|
13406
|
+
return lines.join("\n").trimEnd();
|
|
13407
|
+
}
|
|
13408
|
+
//#endregion
|
|
13409
|
+
//#region src/engine/suppression-integrity.ts
|
|
13410
|
+
/**
|
|
13411
|
+
* ENGINE-006 — Suppression Integrity: detect suppression abuse,
|
|
13412
|
+
* unknown rules, expired entries, and compute a full integrity report.
|
|
13413
|
+
*
|
|
13414
|
+
* Pure module: no filesystem, no clock injection beyond an optional
|
|
13415
|
+
* `now` parameter — deterministic by construction.
|
|
13416
|
+
*/
|
|
13417
|
+
/**
|
|
13418
|
+
* Deterministic sha256 fingerprint of the suppression set. Canonical
|
|
13419
|
+
* JSON sorted by ruleId+files+reason+expires so order in the config
|
|
13420
|
+
* file does not affect the hash.
|
|
13421
|
+
*/
|
|
13422
|
+
function suppressionFingerprint(suppressions) {
|
|
13423
|
+
const canonical = suppressions.map((s) => ({
|
|
13424
|
+
ruleId: s.ruleId,
|
|
13425
|
+
files: [...s.files ?? []].sort(),
|
|
13426
|
+
reason: s.reason,
|
|
13427
|
+
...s.expires !== void 0 ? { expires: s.expires } : {}
|
|
13428
|
+
})).sort((a, b) => a.ruleId.localeCompare(b.ruleId));
|
|
13429
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
13430
|
+
}
|
|
13431
|
+
/**
|
|
13432
|
+
* Detect when suppressions cover an unusually large fraction of
|
|
13433
|
+
* findings. Default threshold is 50% (0.5).
|
|
13434
|
+
*/
|
|
13435
|
+
function detectMassSuppression(suppressions, findings, threshold = .5) {
|
|
13436
|
+
const matchers = suppressions.map((suppression) => ({
|
|
13437
|
+
ruleId: suppression.ruleId,
|
|
13438
|
+
patterns: (suppression.files ?? []).map((glob) => globToRegExp(glob.replaceAll("\\", "/"))),
|
|
13439
|
+
ruleOnly: !suppression.files?.length
|
|
13440
|
+
}));
|
|
13441
|
+
const suppressedCount = findings.filter((finding) => {
|
|
13442
|
+
const file = finding.file.replaceAll("\\", "/");
|
|
13443
|
+
return matchers.some((matcher) => matcher.ruleId === finding.ruleId && (matcher.ruleOnly || matcher.patterns.some((pattern) => pattern.test(file))));
|
|
13444
|
+
}).length;
|
|
13445
|
+
const totalFindings = findings.length;
|
|
13446
|
+
const ratio = totalFindings === 0 ? 0 : suppressedCount / totalFindings;
|
|
13447
|
+
return {
|
|
13448
|
+
isMassSuppression: ratio >= threshold,
|
|
13449
|
+
suppressedCount,
|
|
13450
|
+
totalFindings,
|
|
13451
|
+
ratio,
|
|
13452
|
+
threshold
|
|
13453
|
+
};
|
|
13454
|
+
}
|
|
13455
|
+
/**
|
|
13456
|
+
* Find suppressions that reference ruleIds not in the known set.
|
|
13457
|
+
*/
|
|
13458
|
+
function detectUnknownRuleSuppressions(suppressions, knownRuleIds) {
|
|
13459
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
13460
|
+
for (const s of suppressions) if (!knownRuleIds.has(s.ruleId)) unknown.add(s.ruleId);
|
|
13461
|
+
return [...unknown].sort();
|
|
13462
|
+
}
|
|
13463
|
+
/**
|
|
13464
|
+
* Find suppressions whose expiration date has passed.
|
|
13465
|
+
*/
|
|
13466
|
+
function detectExpiredSuppressions(suppressions, now = /* @__PURE__ */ new Date()) {
|
|
13467
|
+
const nowMs = now.getTime();
|
|
13468
|
+
return suppressions.filter((s) => {
|
|
13469
|
+
if (s.expires === void 0) return false;
|
|
13470
|
+
return new Date(s.expires).getTime() <= nowMs;
|
|
13471
|
+
});
|
|
13472
|
+
}
|
|
13473
|
+
/**
|
|
13474
|
+
* Full suppression integrity report combining all checks.
|
|
13475
|
+
*/
|
|
13476
|
+
function computeSuppressionIntegrity(suppressions, findings, knownRuleIds, now = /* @__PURE__ */ new Date()) {
|
|
13477
|
+
return {
|
|
13478
|
+
fingerprint: suppressionFingerprint(suppressions),
|
|
13479
|
+
massSuppression: detectMassSuppression(suppressions, findings),
|
|
13480
|
+
unknownRuleSuppressions: detectUnknownRuleSuppressions(suppressions, knownRuleIds),
|
|
13481
|
+
expiredSuppressions: detectExpiredSuppressions(suppressions, now)
|
|
13482
|
+
};
|
|
13483
|
+
}
|
|
13484
|
+
//#endregion
|
|
13485
|
+
//#region src/engine/suppression-governance.ts
|
|
13486
|
+
/**
|
|
13487
|
+
* M6 — Suppression Policy Governance Gate.
|
|
13488
|
+
*
|
|
13489
|
+
* Provides functions to enforce suppression policy governance,
|
|
13490
|
+
* detecting abuse, expired entries, unknown rules, and computing
|
|
13491
|
+
* a full governance gate result.
|
|
13492
|
+
*/
|
|
13493
|
+
const DEFAULT_SUPPRESSION_POLICY = {
|
|
13494
|
+
maxMassSuppressionRatio: .5,
|
|
13495
|
+
requireExpiration: false,
|
|
13496
|
+
allowedRuleIds: [],
|
|
13497
|
+
maxTotalSuppressions: 100,
|
|
13498
|
+
maxExpiredSuppressions: 10
|
|
13499
|
+
};
|
|
13500
|
+
function computeSuppressionGovernanceGate(suppressions, findings, policy = DEFAULT_SUPPRESSION_POLICY, knownRuleIds, now = /* @__PURE__ */ new Date()) {
|
|
13501
|
+
const allIntegrityReport = computeSuppressionIntegrity(suppressions, findings, knownRuleIds ?? /* @__PURE__ */ new Set(), now);
|
|
13502
|
+
const activeSuppressions = suppressions.filter((suppression) => isSuppressionActive(suppression, now));
|
|
13503
|
+
const integrityReport = {
|
|
13504
|
+
...allIntegrityReport,
|
|
13505
|
+
massSuppression: detectMassSuppression(activeSuppressions, findings)
|
|
13506
|
+
};
|
|
13507
|
+
const policyViolations = [];
|
|
13508
|
+
if (integrityReport.massSuppression.ratio > policy.maxMassSuppressionRatio) policyViolations.push(`Mass suppression ratio ${integrityReport.massSuppression.ratio.toFixed(2)} exceeds policy maximum ${policy.maxMassSuppressionRatio}`);
|
|
13509
|
+
if (policy.requireExpiration) {
|
|
13510
|
+
const permanentCount = suppressions.filter((suppression) => suppression.expires === void 0).length;
|
|
13511
|
+
if (permanentCount > 0) policyViolations.push(`Suppressions without an expiration: ${permanentCount}`);
|
|
13512
|
+
const expired = integrityReport.expiredSuppressions;
|
|
13513
|
+
if (expired.length > policy.maxExpiredSuppressions) policyViolations.push(`Expired suppressions (${expired.length}) exceed policy maximum ${policy.maxExpiredSuppressions}`);
|
|
13514
|
+
}
|
|
13515
|
+
if (suppressions.length > policy.maxTotalSuppressions) policyViolations.push(`Total suppressions (${suppressions.length}) exceed policy maximum ${policy.maxTotalSuppressions}`);
|
|
13516
|
+
if (knownRuleIds !== void 0 && integrityReport.unknownRuleSuppressions.length > 0) {
|
|
13517
|
+
const disallowedUnknown = policy.allowedRuleIds.length > 0 ? integrityReport.unknownRuleSuppressions.filter((ruleId) => !policy.allowedRuleIds.includes(ruleId)) : integrityReport.unknownRuleSuppressions;
|
|
13518
|
+
if (disallowedUnknown.length > 0) policyViolations.push(`Unknown rule suppressions: ${disallowedUnknown.join(", ")}`);
|
|
13519
|
+
}
|
|
13520
|
+
const expiredCount = integrityReport.expiredSuppressions.length;
|
|
13521
|
+
const unknownRuleCount = integrityReport.unknownRuleSuppressions.length;
|
|
13522
|
+
return {
|
|
13523
|
+
passed: policyViolations.length === 0,
|
|
13524
|
+
integrityReport,
|
|
13525
|
+
policyViolations,
|
|
13526
|
+
massSuppression: integrityReport.massSuppression,
|
|
13527
|
+
expiredCount,
|
|
13528
|
+
unknownRuleCount,
|
|
13529
|
+
totalSuppressions: suppressions.length,
|
|
13530
|
+
fingerprint: integrityReport.fingerprint
|
|
13531
|
+
};
|
|
13532
|
+
}
|
|
13533
|
+
function renderSuppressionGovernanceResult(result) {
|
|
13534
|
+
const lines = [];
|
|
13535
|
+
lines.push(`Suppression Policy Governance Gate`);
|
|
13536
|
+
lines.push(`Result: ${result.passed ? "PASSED" : "FAILED"}`);
|
|
13537
|
+
lines.push(`Total Suppressions: ${result.totalSuppressions}`);
|
|
13538
|
+
lines.push(`Mass Suppression Ratio: ${result.massSuppression.ratio.toFixed(2)} (threshold: ${result.massSuppression.threshold})`);
|
|
13539
|
+
lines.push(`Expired: ${result.expiredCount}, Unknown Rules: ${result.unknownRuleCount}`);
|
|
13540
|
+
lines.push(`Fingerprint: ${result.fingerprint}`);
|
|
13541
|
+
lines.push("");
|
|
13542
|
+
if (result.policyViolations.length > 0) {
|
|
13543
|
+
lines.push("Policy Violations:");
|
|
13544
|
+
for (const v of result.policyViolations) lines.push(` ✗ ${v}`);
|
|
13545
|
+
} else lines.push("All suppression policy checks passed.");
|
|
13546
|
+
return lines.join("\n").trimEnd();
|
|
13547
|
+
}
|
|
13548
|
+
//#endregion
|
|
13549
|
+
//#region src/adapters/gitlab-ci.ts
|
|
13550
|
+
function parseGitLabCi(content) {
|
|
13551
|
+
const parsed = parse(content);
|
|
13552
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("GitLab CI YAML is not a valid object.");
|
|
13553
|
+
return parsed;
|
|
13554
|
+
}
|
|
13555
|
+
//#endregion
|
|
13556
|
+
//#region src/engine/verification-intelligence.ts
|
|
13557
|
+
const CI_WORKFLOW_FILES = [
|
|
13558
|
+
".github/workflows/ci.yml",
|
|
13559
|
+
".github/workflows/test.yml",
|
|
13560
|
+
".github/workflows/qa.yml",
|
|
13561
|
+
".gitlab-ci.yml",
|
|
13562
|
+
".gitlab-ci.yaml",
|
|
13563
|
+
"Jenkinsfile"
|
|
13564
|
+
];
|
|
13565
|
+
const OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set([
|
|
13566
|
+
"--base",
|
|
13567
|
+
"--blocking",
|
|
13568
|
+
"--category",
|
|
13569
|
+
"--config",
|
|
13570
|
+
"--fail-on",
|
|
13571
|
+
"--format",
|
|
13572
|
+
"--max-duration",
|
|
13573
|
+
"--output",
|
|
13574
|
+
"--rules",
|
|
13575
|
+
"--scope",
|
|
13576
|
+
"--width"
|
|
13577
|
+
]);
|
|
13578
|
+
const GITLAB_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
13579
|
+
"after_script",
|
|
13580
|
+
"before_script",
|
|
13581
|
+
"cache",
|
|
13582
|
+
"default",
|
|
13583
|
+
"image",
|
|
13584
|
+
"include",
|
|
13585
|
+
"services",
|
|
13586
|
+
"stages",
|
|
13587
|
+
"variables",
|
|
13588
|
+
"workflow"
|
|
13589
|
+
]);
|
|
13590
|
+
function isRecord(value) {
|
|
13591
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13592
|
+
}
|
|
13593
|
+
function conditionState(value) {
|
|
13594
|
+
if (value === void 0 || value === null || value === true) return "active";
|
|
13595
|
+
if (value === false) return "disabled";
|
|
13596
|
+
if (typeof value !== "string") return "dynamic";
|
|
13597
|
+
const normalized = value.trim().toLowerCase();
|
|
13598
|
+
if (normalized.startsWith("${{")) return "dynamic";
|
|
13599
|
+
if (normalized === "false") return "disabled";
|
|
13600
|
+
if ([
|
|
13601
|
+
"",
|
|
13602
|
+
"true",
|
|
13603
|
+
"always()",
|
|
13604
|
+
"success()"
|
|
13605
|
+
].includes(normalized)) return "active";
|
|
13606
|
+
return "dynamic";
|
|
13607
|
+
}
|
|
13608
|
+
function isContinueOnError(value) {
|
|
13609
|
+
if (value === void 0 || value === null || value === false) return false;
|
|
13610
|
+
if (value === true) return true;
|
|
13611
|
+
if (typeof value !== "string") return true;
|
|
13612
|
+
const normalized = value.trim().toLowerCase();
|
|
13613
|
+
if (normalized.startsWith("${{")) return true;
|
|
13614
|
+
return normalized !== "false";
|
|
13615
|
+
}
|
|
13616
|
+
function parseGithubJobs(content) {
|
|
13617
|
+
const document = parseYamlGuarded(content, {
|
|
13618
|
+
invalidPrefix: "Invalid workflow YAML",
|
|
13619
|
+
rootMessage: "Workflow root must be a mapping",
|
|
13620
|
+
depthMessage: "Workflow nesting depth exceeds limit"
|
|
13621
|
+
});
|
|
13622
|
+
if (document === null || document === void 0) return {};
|
|
13623
|
+
if (!isRecord(document)) throw new Error("Workflow root must be a mapping");
|
|
13624
|
+
const jobs = document.jobs;
|
|
13625
|
+
if (jobs === void 0 || jobs === null) return {};
|
|
13626
|
+
if (!isRecord(jobs)) throw new Error("\"jobs\" must be a mapping");
|
|
13627
|
+
const result = Object.create(null);
|
|
13628
|
+
for (const [name, value] of Object.entries(jobs)) if (isRecord(value)) result[name] = value;
|
|
13629
|
+
return result;
|
|
13630
|
+
}
|
|
13631
|
+
function isEnvironmentName(name) {
|
|
13632
|
+
const first = name.charCodeAt(0);
|
|
13633
|
+
if (!(first >= 65 && first <= 90 || first >= 97 && first <= 122 || first === 95)) return false;
|
|
13634
|
+
for (let index = 1; index < name.length; index++) {
|
|
13635
|
+
const code = name.charCodeAt(index);
|
|
13636
|
+
if (!(code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95)) return false;
|
|
13637
|
+
}
|
|
13638
|
+
return true;
|
|
13639
|
+
}
|
|
13640
|
+
function stripEnvironmentAssignments(command) {
|
|
13641
|
+
let normalized = command.trim();
|
|
13642
|
+
while (true) {
|
|
13643
|
+
const equals = normalized.indexOf("=");
|
|
13644
|
+
const space = normalized.search(/\s/);
|
|
13645
|
+
if (equals <= 0 || space >= 0 && equals > space) break;
|
|
13646
|
+
if (!isEnvironmentName(normalized.slice(0, equals))) break;
|
|
13647
|
+
let end = equals + 1;
|
|
13648
|
+
const quote = normalized[end];
|
|
13649
|
+
if (quote === "\"" || quote === "'") {
|
|
13650
|
+
const closing = normalized.indexOf(quote, end + 1);
|
|
13651
|
+
if (closing < 0) break;
|
|
13652
|
+
end = closing + 1;
|
|
13653
|
+
} else {
|
|
13654
|
+
const nextSpace = normalized.slice(end).search(/\s/);
|
|
13655
|
+
end = nextSpace < 0 ? normalized.length : end + nextSpace;
|
|
13656
|
+
}
|
|
13657
|
+
normalized = normalized.slice(end).trimStart();
|
|
13658
|
+
}
|
|
13659
|
+
return normalized;
|
|
13660
|
+
}
|
|
13661
|
+
function normalizeCommand(command) {
|
|
13662
|
+
let normalized = command.trim();
|
|
13663
|
+
const prefixes = [
|
|
13664
|
+
"if ",
|
|
13665
|
+
"then ",
|
|
13666
|
+
"do ",
|
|
13667
|
+
"else ",
|
|
13668
|
+
"elif ",
|
|
13669
|
+
"while ",
|
|
13670
|
+
"until "
|
|
13671
|
+
];
|
|
13672
|
+
if (normalized.startsWith("!")) return "";
|
|
13673
|
+
while (prefixes.some((prefix) => normalized.startsWith(prefix))) normalized = normalized.slice(normalized.indexOf(" ") + 1).trimStart();
|
|
13674
|
+
normalized = stripEnvironmentAssignments(normalized);
|
|
13675
|
+
for (const prefix of [
|
|
13676
|
+
"sudo ",
|
|
13677
|
+
"command ",
|
|
13678
|
+
"exec ",
|
|
13679
|
+
"env "
|
|
13680
|
+
]) if (normalized.startsWith(prefix)) {
|
|
13681
|
+
normalized = normalized.slice(prefix.length).trimStart();
|
|
13682
|
+
break;
|
|
13683
|
+
}
|
|
13684
|
+
return stripEnvironmentAssignments(normalized).trim();
|
|
13685
|
+
}
|
|
13686
|
+
function isMjolnirPackage(token) {
|
|
13687
|
+
return token === "mjolnir" || token === "mjolnir.cmd" || token === "mjolnir-qa" || token === "mjolnir-qa.cmd" || token.startsWith("mjolnir@") || token.startsWith("mjolnir-qa@") || /^https?:\/\/[^ ]*\/mjolnir-qa(?:-|\/)/i.test(token);
|
|
13688
|
+
}
|
|
13689
|
+
function commandInvocation(command) {
|
|
13690
|
+
const tokens = normalizeCommand(command).split(/\s+/).filter(Boolean);
|
|
13691
|
+
let runnerIndex = -1;
|
|
13692
|
+
if (tokens[0] && isMjolnirPackage(tokens[0])) runnerIndex = 0;
|
|
13693
|
+
else if (tokens[0] === "npx") {
|
|
13694
|
+
const optionsWithValues = /* @__PURE__ */ new Set([
|
|
13695
|
+
"--cache",
|
|
13696
|
+
"--package",
|
|
13697
|
+
"-p",
|
|
13698
|
+
"--registry"
|
|
13699
|
+
]);
|
|
13700
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
13701
|
+
const token = Reflect.get(tokens, index);
|
|
13702
|
+
if (token.length === 2 && token[0] === "-" && token[1] === "-") continue;
|
|
13703
|
+
if (optionsWithValues.has(token)) {
|
|
13704
|
+
index++;
|
|
13705
|
+
continue;
|
|
13706
|
+
}
|
|
13707
|
+
if (token.startsWith("-")) continue;
|
|
13708
|
+
if (isMjolnirPackage(token)) runnerIndex = index;
|
|
13709
|
+
break;
|
|
13710
|
+
}
|
|
13711
|
+
} else if (tokens[0] === "npm" && tokens[1] === "exec") {
|
|
13712
|
+
const optionsWithValues = /* @__PURE__ */ new Set(["--package", "-p"]);
|
|
13713
|
+
for (let index = 2; index < tokens.length; index++) {
|
|
13714
|
+
const token = Reflect.get(tokens, index);
|
|
13715
|
+
if (token.length === 2 && token[0] === "-" && token[1] === "-") continue;
|
|
13716
|
+
if (optionsWithValues.has(token)) {
|
|
13717
|
+
index++;
|
|
13718
|
+
continue;
|
|
13719
|
+
}
|
|
13720
|
+
if (token.startsWith("-")) continue;
|
|
13721
|
+
if (isMjolnirPackage(token)) runnerIndex = index;
|
|
13722
|
+
break;
|
|
13723
|
+
}
|
|
13724
|
+
} else if (tokens[0] === "npm" && tokens[1] === "run") {
|
|
13725
|
+
if (tokens[2] === "self-scan") runnerIndex = 2;
|
|
13726
|
+
} else if (tokens[0] === "node" && (tokens[1] === "dist/cli.mjs" || tokens[1] === "./dist/cli.mjs")) runnerIndex = 1;
|
|
13727
|
+
return runnerIndex >= 0 ? { args: tokens.slice(runnerIndex + 1).join(" ") } : null;
|
|
13728
|
+
}
|
|
13729
|
+
function firstPositional(args) {
|
|
13730
|
+
const tokens = args.split(/\s+/).filter(Boolean);
|
|
13731
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
13732
|
+
const token = tokens[index];
|
|
13733
|
+
if (!token || /^(?:[<>]|>>?)$/.test(token)) continue;
|
|
13734
|
+
if (token.startsWith("-")) {
|
|
13735
|
+
if (!token.includes("=") && OPTIONS_WITH_VALUES.has(token)) index++;
|
|
13736
|
+
continue;
|
|
13737
|
+
}
|
|
13738
|
+
return token;
|
|
13739
|
+
}
|
|
13740
|
+
return null;
|
|
13741
|
+
}
|
|
13742
|
+
function isPotentialScanInvocation(command) {
|
|
13743
|
+
const invocation = commandInvocation(command);
|
|
13744
|
+
if (!invocation) return false;
|
|
13745
|
+
if (/(?:^|\s)(?:-h|--help|--version|-v)(?=\s|$)/.test(invocation.args)) return false;
|
|
13746
|
+
const positional = firstPositional(invocation.args);
|
|
13747
|
+
return positional === null || !CLI_NON_SCAN_COMMANDS.has(positional);
|
|
13748
|
+
}
|
|
13749
|
+
function isScanInvocation(command) {
|
|
13750
|
+
if (!isPotentialScanInvocation(command)) return false;
|
|
13751
|
+
const invocation = commandInvocation(command);
|
|
13752
|
+
if (!invocation) return false;
|
|
13753
|
+
return !/(?:^|\s)--blocking(?:=|\s+)none(?=\s|$)/.test(invocation.args);
|
|
13754
|
+
}
|
|
13755
|
+
function isMjolnirInvocation(command) {
|
|
13756
|
+
return isPotentialScanInvocation(command);
|
|
13757
|
+
}
|
|
13758
|
+
function runParts(run) {
|
|
13759
|
+
const result = [];
|
|
13760
|
+
let current = "";
|
|
13761
|
+
let quote = null;
|
|
13762
|
+
let escaped = false;
|
|
13763
|
+
const push = (terminator) => {
|
|
13764
|
+
if (current.trim() && !current.trim().startsWith("#")) result.push(terminator ? {
|
|
13765
|
+
command: current,
|
|
13766
|
+
terminator
|
|
13767
|
+
} : { command: current });
|
|
13768
|
+
current = "";
|
|
13769
|
+
};
|
|
13770
|
+
for (let index = 0; index < run.length; index++) {
|
|
13771
|
+
const character = run[index];
|
|
13772
|
+
if (escaped) {
|
|
13773
|
+
current += character;
|
|
13774
|
+
escaped = false;
|
|
13775
|
+
continue;
|
|
13776
|
+
}
|
|
13777
|
+
if (character === "\\" && quote !== "'") {
|
|
13778
|
+
current += character;
|
|
13779
|
+
escaped = true;
|
|
13780
|
+
continue;
|
|
13781
|
+
}
|
|
13782
|
+
if (quote) {
|
|
13783
|
+
current += character;
|
|
13784
|
+
if (character === quote) quote = null;
|
|
13785
|
+
continue;
|
|
13786
|
+
}
|
|
13787
|
+
if (character === "\"" || character === "'") {
|
|
13788
|
+
quote = character;
|
|
13789
|
+
current += character;
|
|
13790
|
+
continue;
|
|
13791
|
+
}
|
|
13792
|
+
if (character === ";" || character === "\n" || character === "\r") {
|
|
13793
|
+
push(";");
|
|
13794
|
+
continue;
|
|
13795
|
+
}
|
|
13796
|
+
if (character === "&" && run[index + 1] === "&") {
|
|
13797
|
+
push("&&");
|
|
13798
|
+
index++;
|
|
13799
|
+
continue;
|
|
13800
|
+
}
|
|
13801
|
+
if (character === "|" && run[index + 1] === "|") {
|
|
13802
|
+
push("||");
|
|
13803
|
+
index++;
|
|
13804
|
+
continue;
|
|
13805
|
+
}
|
|
13806
|
+
if (character === "&" || character === "|") {
|
|
13807
|
+
push(character);
|
|
13808
|
+
continue;
|
|
13809
|
+
}
|
|
13810
|
+
current += character;
|
|
13811
|
+
}
|
|
13812
|
+
push();
|
|
13813
|
+
return result;
|
|
13814
|
+
}
|
|
13815
|
+
function hasPipefail(run) {
|
|
13816
|
+
const tokens = run.trim().split(/\s+/);
|
|
13817
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
13818
|
+
if (tokens[index] !== "set") continue;
|
|
13819
|
+
let hasPipefailOption = false;
|
|
13820
|
+
for (let next = index + 1; next < tokens.length; next++) {
|
|
13821
|
+
const token = tokens[next] ?? "";
|
|
13822
|
+
if (token.length >= 8 && token.startsWith("pipefail")) return hasPipefailOption;
|
|
13823
|
+
if (!token.startsWith("-")) break;
|
|
13824
|
+
if (token.length > 1 && token[0] === "-" && token.includes("o")) hasPipefailOption = true;
|
|
13825
|
+
}
|
|
13826
|
+
}
|
|
13827
|
+
return false;
|
|
13828
|
+
}
|
|
13829
|
+
function hasDynamicShellCondition(run) {
|
|
13830
|
+
return runParts(run).some(({ command }) => {
|
|
13831
|
+
const tokens = command.trim().split(/\s+/);
|
|
13832
|
+
const control = tokens[0];
|
|
13833
|
+
if (control && [
|
|
13834
|
+
"while",
|
|
13835
|
+
"until",
|
|
13836
|
+
"for",
|
|
13837
|
+
"case"
|
|
13838
|
+
].includes(control)) return true;
|
|
13839
|
+
const ifIndex = tokens.indexOf("if");
|
|
13840
|
+
if (ifIndex < 0 || ifIndex === tokens.length - 1) return false;
|
|
13841
|
+
const condition = tokens.slice(ifIndex + 1).join(" ");
|
|
13842
|
+
return condition !== "true" && condition !== "success()" && condition !== "always()";
|
|
13843
|
+
});
|
|
13844
|
+
}
|
|
13845
|
+
function runHasScanGate(run) {
|
|
13846
|
+
if (/\bset\s+\+e\b/.test(run)) return false;
|
|
13847
|
+
if (runParts(run).filter((part) => part.terminator === "|").length > 0 && !hasPipefail(run)) return false;
|
|
13848
|
+
if (hasDynamicShellCondition(run)) return false;
|
|
13849
|
+
const parts = runParts(run);
|
|
13850
|
+
for (let index = 0; index < parts.length; index++) {
|
|
13851
|
+
const part = parts[index];
|
|
13852
|
+
if (!part || part.terminator === "||" || part.terminator === "&") continue;
|
|
13853
|
+
if (!isScanInvocation(part.command)) continue;
|
|
13854
|
+
const next = parts[index + 1]?.command.trim();
|
|
13855
|
+
if (part.terminator === ";" && next !== void 0 && /^(?:true|:|exit\s+0)$/.test(next)) return false;
|
|
13856
|
+
return true;
|
|
13857
|
+
}
|
|
13858
|
+
return false;
|
|
13859
|
+
}
|
|
13860
|
+
function runHasMjolnirCommand(run) {
|
|
13861
|
+
return runParts(run).some(({ command }) => isMjolnirInvocation(command));
|
|
13862
|
+
}
|
|
13863
|
+
function analyzeGithubWorkflow(content) {
|
|
13864
|
+
const jobs = parseGithubJobs(content);
|
|
13865
|
+
let hasGate = false;
|
|
13866
|
+
let hasCandidate = false;
|
|
13867
|
+
let hasNonGatingCandidate = false;
|
|
13868
|
+
for (const job of Object.values(jobs)) {
|
|
13869
|
+
const jobState = conditionState(job.if);
|
|
13870
|
+
const steps = Array.isArray(job.steps) ? job.steps : [];
|
|
13871
|
+
for (const rawStep of steps) {
|
|
13872
|
+
if (!isRecord(rawStep) || typeof rawStep.run !== "string") continue;
|
|
13873
|
+
if (!runHasMjolnirCommand(rawStep.run)) continue;
|
|
13874
|
+
hasCandidate = true;
|
|
13875
|
+
const stepState = conditionState(rawStep.if);
|
|
13876
|
+
const continueOnError = isContinueOnError(rawStep["continue-on-error"] ?? job["continue-on-error"]);
|
|
13877
|
+
if (jobState === "active" && stepState === "active" && !continueOnError && runHasScanGate(rawStep.run)) hasGate = true;
|
|
13878
|
+
else hasNonGatingCandidate = true;
|
|
13879
|
+
}
|
|
13880
|
+
}
|
|
13881
|
+
return {
|
|
13882
|
+
hasGate,
|
|
13883
|
+
hasCandidate,
|
|
13884
|
+
hasNonGatingCandidate
|
|
13885
|
+
};
|
|
13886
|
+
}
|
|
13887
|
+
function asCommands(value) {
|
|
13888
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string");
|
|
13889
|
+
return typeof value === "string" ? [value] : [];
|
|
13890
|
+
}
|
|
13891
|
+
function gitlabRuleState(rules) {
|
|
13892
|
+
if (!Array.isArray(rules)) return "dynamic";
|
|
13893
|
+
let unconditional = false;
|
|
13894
|
+
for (const rawRule of rules) {
|
|
13895
|
+
if (!isRecord(rawRule)) continue;
|
|
13896
|
+
if (rawRule.allow_failure !== void 0 && rawRule.allow_failure !== false) return "disabled";
|
|
13897
|
+
const when = rawRule.when;
|
|
13898
|
+
if (when === "never") continue;
|
|
13899
|
+
if (when === "manual" || when === "delayed" || when === "on_failure") return "dynamic";
|
|
13900
|
+
const state = conditionState(rawRule.if);
|
|
13901
|
+
if (state === "active" && rawRule.if === void 0) unconditional = true;
|
|
13902
|
+
else if (state !== "disabled") return "dynamic";
|
|
13903
|
+
}
|
|
13904
|
+
return unconditional ? "active" : "disabled";
|
|
13905
|
+
}
|
|
13906
|
+
function gitlabJobState(job) {
|
|
13907
|
+
const allowFailure = job.allow_failure;
|
|
13908
|
+
if (allowFailure !== void 0 && allowFailure !== false) return "disabled";
|
|
13909
|
+
const when = job.when;
|
|
13910
|
+
if (when === "never" || when === "manual" || when === "delayed" || when === "on_failure") return "disabled";
|
|
13911
|
+
if (job.only !== void 0 || job.except !== void 0) return "dynamic";
|
|
13912
|
+
if (job.rules !== void 0) return gitlabRuleState(job.rules);
|
|
13913
|
+
return "active";
|
|
13914
|
+
}
|
|
13915
|
+
function analyzeGitlabWorkflow(content) {
|
|
13916
|
+
const config = parseGitLabCi(content);
|
|
13917
|
+
const globalCommands = [...asCommands(config.before_script).map((command) => ({
|
|
13918
|
+
command,
|
|
13919
|
+
advisory: false
|
|
13920
|
+
})), ...asCommands(config.after_script).map((command) => ({
|
|
13921
|
+
command,
|
|
13922
|
+
advisory: true
|
|
13923
|
+
}))];
|
|
13924
|
+
const workflowState = isRecord(config.workflow) && config.workflow.rules !== void 0 ? gitlabRuleState(config.workflow.rules) : "active";
|
|
13925
|
+
let hasGate = false;
|
|
13926
|
+
let hasCandidate = false;
|
|
13927
|
+
let hasNonGatingCandidate = false;
|
|
13928
|
+
for (const [name, value] of Object.entries(config)) {
|
|
13929
|
+
if (GITLAB_RESERVED_KEYS.has(name) || !isRecord(value)) continue;
|
|
13930
|
+
const state = gitlabJobState(value);
|
|
13931
|
+
const commands = [
|
|
13932
|
+
...globalCommands,
|
|
13933
|
+
...asCommands(value.before_script).map((command) => ({
|
|
13934
|
+
command,
|
|
13935
|
+
advisory: false
|
|
13936
|
+
})),
|
|
13937
|
+
...asCommands(value.script).map((command) => ({
|
|
13938
|
+
command,
|
|
13939
|
+
advisory: false
|
|
13940
|
+
})),
|
|
13941
|
+
...asCommands(value.after_script).map((command) => ({
|
|
13942
|
+
command,
|
|
13943
|
+
advisory: true
|
|
13944
|
+
}))
|
|
13945
|
+
];
|
|
13946
|
+
for (const { command, advisory } of commands) {
|
|
13947
|
+
if (!runHasMjolnirCommand(command)) continue;
|
|
13948
|
+
hasCandidate = true;
|
|
13949
|
+
if (!advisory && workflowState === "active" && state === "active" && runHasScanGate(command)) hasGate = true;
|
|
13950
|
+
else hasNonGatingCandidate = true;
|
|
13951
|
+
}
|
|
13952
|
+
}
|
|
13953
|
+
return {
|
|
13954
|
+
hasGate,
|
|
13955
|
+
hasCandidate,
|
|
13956
|
+
hasNonGatingCandidate
|
|
13957
|
+
};
|
|
13958
|
+
}
|
|
13959
|
+
function detectCIWorkflows(root = ".") {
|
|
13960
|
+
return CI_WORKFLOW_FILES.filter((workflow) => existsSync(join(root, workflow)));
|
|
13961
|
+
}
|
|
13962
|
+
function inspectWorkflow(content, workflowName) {
|
|
13963
|
+
try {
|
|
13964
|
+
if (workflowName === "Jenkinsfile") return {
|
|
13965
|
+
name: workflowName,
|
|
13966
|
+
status: "warn",
|
|
13967
|
+
detail: "Jenkins pipeline detected; blocking scan verification is unsupported"
|
|
13968
|
+
};
|
|
13969
|
+
const analysis = workflowName.startsWith(".gitlab-ci.") ? analyzeGitlabWorkflow(content) : analyzeGithubWorkflow(content);
|
|
13970
|
+
if (analysis.hasGate && !analysis.hasNonGatingCandidate) return {
|
|
13971
|
+
name: workflowName,
|
|
13972
|
+
status: "pass",
|
|
13973
|
+
detail: "CI workflow contains a guaranteed blocking Mjölnir scan"
|
|
13974
|
+
};
|
|
13975
|
+
if (analysis.hasCandidate) return {
|
|
13976
|
+
name: workflowName,
|
|
13977
|
+
status: "fail",
|
|
13978
|
+
detail: "Mjölnir command is disabled, conditional, or allowed to fail; no guaranteed blocking scan"
|
|
13979
|
+
};
|
|
13980
|
+
} catch (error) {
|
|
13981
|
+
return {
|
|
13982
|
+
name: workflowName,
|
|
13983
|
+
status: "fail",
|
|
13984
|
+
detail: `Invalid CI workflow YAML/provider: ${error instanceof Error ? error.message : String(error)}`
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
return {
|
|
13988
|
+
name: workflowName,
|
|
13989
|
+
status: "warn",
|
|
13990
|
+
detail: "No Mjölnir command detected in this CI workflow"
|
|
13991
|
+
};
|
|
13992
|
+
}
|
|
13993
|
+
function checkSuppressionIntegrity(suppressions, findings, policy = DEFAULT_SUPPRESSION_POLICY, knownRuleIds, now = /* @__PURE__ */ new Date()) {
|
|
13994
|
+
const result = computeSuppressionGovernanceGate(suppressions, findings, policy, knownRuleIds, now);
|
|
13995
|
+
return result.passed ? {
|
|
13996
|
+
name: "suppression-integrity",
|
|
13997
|
+
status: "pass",
|
|
13998
|
+
detail: `Suppression integrity passed: ${suppressions.length} suppression(s), ${findings.length} finding(s)`
|
|
13999
|
+
} : {
|
|
14000
|
+
name: "suppression-integrity",
|
|
14001
|
+
status: "fail",
|
|
14002
|
+
detail: result.policyViolations.join("; ")
|
|
14003
|
+
};
|
|
14004
|
+
}
|
|
14005
|
+
function computeVerificationIntelligence(root = ".", suppressions = [], findings = [], policy = DEFAULT_SUPPRESSION_POLICY, knownRuleIds, now = /* @__PURE__ */ new Date()) {
|
|
14006
|
+
const workflows = detectCIWorkflows(root);
|
|
14007
|
+
const checks = [];
|
|
14008
|
+
if (workflows.length === 0) checks.push({
|
|
14009
|
+
name: "ci-workflow",
|
|
14010
|
+
status: "fail",
|
|
14011
|
+
detail: "No CI workflow files detected in repository"
|
|
14012
|
+
});
|
|
14013
|
+
else for (const workflow of workflows) try {
|
|
14014
|
+
checks.push(inspectWorkflow(readFileSync(join(root, workflow), "utf8"), workflow));
|
|
14015
|
+
} catch (error) {
|
|
14016
|
+
checks.push({
|
|
14017
|
+
name: workflow,
|
|
14018
|
+
status: "fail",
|
|
14019
|
+
detail: `Unable to read CI workflow: ${error instanceof Error ? error.message : String(error)}`
|
|
14020
|
+
});
|
|
14021
|
+
}
|
|
14022
|
+
if (suppressions.length > 0) checks.push(checkSuppressionIntegrity(suppressions, findings, policy, knownRuleIds, now));
|
|
14023
|
+
const passed = checks.filter((check) => check.status === "pass").length;
|
|
14024
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
14025
|
+
const warned = checks.filter((check) => check.status === "warn").length;
|
|
14026
|
+
const overallStatus = failed > 0 ? "broken" : warned > 0 ? "degraded" : "healthy";
|
|
14027
|
+
return {
|
|
14028
|
+
workflow: workflows.length > 0 ? workflows.join(", ") : "none",
|
|
14029
|
+
checks,
|
|
14030
|
+
passed,
|
|
14031
|
+
failed,
|
|
14032
|
+
warned,
|
|
14033
|
+
overallStatus
|
|
14034
|
+
};
|
|
14035
|
+
}
|
|
14036
|
+
function renderCIIntegrityReport(report) {
|
|
14037
|
+
const lines = [
|
|
14038
|
+
"CI Workflow Integrity Report",
|
|
14039
|
+
`Workflow: ${report.workflow}`,
|
|
14040
|
+
`Status: ${report.overallStatus.toUpperCase()}`,
|
|
14041
|
+
`Passed: ${report.passed}, Failed: ${report.failed}, Warned: ${report.warned}`,
|
|
14042
|
+
""
|
|
14043
|
+
];
|
|
14044
|
+
for (const check of report.checks) {
|
|
14045
|
+
const icon = check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "⚠";
|
|
14046
|
+
lines.push(`${icon} ${check.name}: ${check.detail}`);
|
|
14047
|
+
}
|
|
14048
|
+
return lines.join("\n").trimEnd();
|
|
14049
|
+
}
|
|
14050
|
+
//#endregion
|
|
14051
|
+
//#region src/commands/milestone.ts
|
|
14052
|
+
const STRICT_SUPPRESSION_POLICY = {
|
|
14053
|
+
...DEFAULT_SUPPRESSION_POLICY,
|
|
14054
|
+
requireExpiration: true,
|
|
14055
|
+
maxExpiredSuppressions: 0
|
|
14056
|
+
};
|
|
14057
|
+
function startsWithFlag(value) {
|
|
14058
|
+
return value.charCodeAt(0) === 45;
|
|
14059
|
+
}
|
|
14060
|
+
function parseMilestoneArgs(argv, io) {
|
|
14061
|
+
const args = {
|
|
14062
|
+
target: ".",
|
|
14063
|
+
json: false,
|
|
14064
|
+
maxDurationMs: Number.POSITIVE_INFINITY
|
|
14065
|
+
};
|
|
14066
|
+
let targetSeen = false;
|
|
14067
|
+
const value = (index, flag) => {
|
|
14068
|
+
const candidate = argv[index];
|
|
14069
|
+
if (candidate === void 0 || startsWithFlag(candidate)) {
|
|
14070
|
+
io.err(`mjolnir: ${flag} requires a value`);
|
|
14071
|
+
return null;
|
|
14072
|
+
}
|
|
14073
|
+
return candidate;
|
|
14074
|
+
};
|
|
14075
|
+
for (let index = 0; index < argv.length; index++) {
|
|
14076
|
+
const token = argv[index] ?? "";
|
|
14077
|
+
switch (token) {
|
|
14078
|
+
case "--json":
|
|
14079
|
+
args.json = true;
|
|
14080
|
+
break;
|
|
14081
|
+
case "--format": {
|
|
14082
|
+
const format = value(++index, "--format");
|
|
14083
|
+
if (format === null) return null;
|
|
14084
|
+
if (format === "json") args.json = true;
|
|
14085
|
+
else if (format !== "terminal") {
|
|
14086
|
+
io.err("mjolnir: milestone commands support --format terminal|json");
|
|
14087
|
+
return null;
|
|
14088
|
+
}
|
|
14089
|
+
break;
|
|
14090
|
+
}
|
|
14091
|
+
case "--no-progress": break;
|
|
14092
|
+
case "--max-duration": {
|
|
14093
|
+
const raw = value(++index, "--max-duration");
|
|
14094
|
+
if (raw === null) return null;
|
|
14095
|
+
const seconds = Number(raw);
|
|
14096
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
14097
|
+
io.err("mjolnir: --max-duration requires positive seconds");
|
|
14098
|
+
return null;
|
|
14099
|
+
}
|
|
14100
|
+
args.maxDurationMs = seconds * 1e3;
|
|
14101
|
+
break;
|
|
14102
|
+
}
|
|
14103
|
+
case "--framework": {
|
|
14104
|
+
const framework = value(++index, "--framework");
|
|
14105
|
+
if (framework === null) return null;
|
|
14106
|
+
args.framework = framework;
|
|
14107
|
+
break;
|
|
14108
|
+
}
|
|
14109
|
+
case "--policy": {
|
|
14110
|
+
const policy = value(++index, "--policy");
|
|
14111
|
+
if (policy === null) return null;
|
|
14112
|
+
args.policy = policy;
|
|
14113
|
+
break;
|
|
14114
|
+
}
|
|
14115
|
+
case "--history": {
|
|
14116
|
+
const history = value(++index, "--history");
|
|
14117
|
+
if (history === null) return null;
|
|
14118
|
+
args.history = history;
|
|
14119
|
+
break;
|
|
14120
|
+
}
|
|
14121
|
+
case "--recorded-at": {
|
|
14122
|
+
const recordedAt = value(++index, "--recorded-at");
|
|
14123
|
+
if (recordedAt === null || Number.isNaN(Date.parse(recordedAt))) {
|
|
14124
|
+
io.err("mjolnir: --recorded-at requires an ISO-8601 timestamp");
|
|
14125
|
+
return null;
|
|
14126
|
+
}
|
|
14127
|
+
args.recordedAt = recordedAt;
|
|
14128
|
+
break;
|
|
14129
|
+
}
|
|
14130
|
+
case "--contract": {
|
|
14131
|
+
const contract = value(++index, "--contract");
|
|
14132
|
+
if (contract === null) return null;
|
|
14133
|
+
args.contract = contract;
|
|
14134
|
+
break;
|
|
14135
|
+
}
|
|
14136
|
+
case "--file": {
|
|
14137
|
+
const file = value(++index, "--file");
|
|
14138
|
+
if (file === null) return null;
|
|
14139
|
+
args.file = file;
|
|
14140
|
+
break;
|
|
14141
|
+
}
|
|
14142
|
+
case "--rule": {
|
|
14143
|
+
const rule = value(++index, "--rule");
|
|
14144
|
+
if (rule === null) return null;
|
|
14145
|
+
args.rule = rule;
|
|
14146
|
+
break;
|
|
14147
|
+
}
|
|
14148
|
+
default:
|
|
14149
|
+
if (startsWithFlag(token)) {
|
|
14150
|
+
io.err(`mjolnir: unknown milestone command flag "${token}"`);
|
|
14151
|
+
return null;
|
|
14152
|
+
}
|
|
14153
|
+
if (targetSeen) {
|
|
14154
|
+
io.err("mjolnir: milestone commands accept one target path");
|
|
14155
|
+
return null;
|
|
14156
|
+
}
|
|
14157
|
+
args.target = token;
|
|
14158
|
+
targetSeen = true;
|
|
14159
|
+
}
|
|
14160
|
+
}
|
|
14161
|
+
return args;
|
|
14162
|
+
}
|
|
14163
|
+
function rejectUnexpectedOptions(args, allowed, io) {
|
|
14164
|
+
const unexpected = [
|
|
14165
|
+
"framework",
|
|
14166
|
+
"policy",
|
|
14167
|
+
"history",
|
|
14168
|
+
"recordedAt",
|
|
14169
|
+
"contract",
|
|
14170
|
+
"file",
|
|
14171
|
+
"rule"
|
|
14172
|
+
].find((key) => args[key] !== void 0 && !allowed.has(key));
|
|
14173
|
+
if (!unexpected) return false;
|
|
14174
|
+
io.err(`mjolnir: option --${unexpected} is not valid for this command`);
|
|
14175
|
+
return true;
|
|
14176
|
+
}
|
|
14177
|
+
function validateTarget(target, io) {
|
|
14178
|
+
if (!existsSync(target)) {
|
|
14179
|
+
io.err(`mjolnir: scan target does not exist: ${target}`);
|
|
14180
|
+
return { code: 10 };
|
|
14181
|
+
}
|
|
14182
|
+
if (!statSync(target).isDirectory()) {
|
|
14183
|
+
io.err(`mjolnir: scan target is not a directory: ${target}`);
|
|
14184
|
+
return { code: 10 };
|
|
14185
|
+
}
|
|
14186
|
+
return { target };
|
|
14187
|
+
}
|
|
14188
|
+
function isWorkflowFile(path) {
|
|
14189
|
+
const normalized = path.replaceAll("\\", "/");
|
|
14190
|
+
return normalized.startsWith(".github/workflows/") || normalized === ".gitlab-ci.yml" || normalized === ".gitlab-ci.yaml" || basename(normalized) === "Jenkinsfile";
|
|
14191
|
+
}
|
|
14192
|
+
async function runMilestoneScan(args, io, readSources, strict = false) {
|
|
14193
|
+
const target = resolve(args.target);
|
|
14194
|
+
const valid = validateTarget(target, io);
|
|
14195
|
+
if ("code" in valid) return valid;
|
|
14196
|
+
let discovered = [];
|
|
14197
|
+
let preSuppressionFindings = [];
|
|
14198
|
+
const scanArgs = {
|
|
14199
|
+
target,
|
|
14200
|
+
json: false,
|
|
14201
|
+
verbose: false,
|
|
14202
|
+
maxDurationMs: args.maxDurationMs,
|
|
14203
|
+
scopeChanged: false,
|
|
14204
|
+
format: "terminal",
|
|
14205
|
+
strict
|
|
14206
|
+
};
|
|
14207
|
+
const result = await runScan$1(scanArgs, {
|
|
14208
|
+
onConfigWarning: (message) => io.err(message),
|
|
14209
|
+
onGateNotice: (notice) => io.err(notice),
|
|
14210
|
+
onTestFilesDiscovered: (files) => {
|
|
14211
|
+
const contractPath = args.contract ? resolve(args.target, args.contract) : void 0;
|
|
14212
|
+
if (contractPath) {
|
|
14213
|
+
const mutable = files;
|
|
14214
|
+
for (let index = mutable.length - 1; index >= 0; index--) {
|
|
14215
|
+
const file = mutable[index];
|
|
14216
|
+
if (file && (isAbsolute(file) ? resolve(file) : resolve(target, file)) === contractPath) mutable.splice(index, 1);
|
|
14217
|
+
}
|
|
14218
|
+
}
|
|
14219
|
+
discovered = [...files];
|
|
14220
|
+
},
|
|
14221
|
+
onPreSuppressionFindings: (findings) => {
|
|
14222
|
+
preSuppressionFindings = [...findings];
|
|
14223
|
+
}
|
|
14224
|
+
});
|
|
14225
|
+
const files = [];
|
|
14226
|
+
let readSkipped = 0;
|
|
14227
|
+
if (readSources) {
|
|
14228
|
+
const contractPath = args.contract ? resolve(args.target, args.contract) : void 0;
|
|
14229
|
+
for (const file of discovered) {
|
|
14230
|
+
const absoluteFile = isAbsolute(file) ? resolve(file) : resolve(target, file);
|
|
14231
|
+
if (contractPath && absoluteFile === contractPath) continue;
|
|
14232
|
+
const relativePath = relative(target, absoluteFile).replaceAll("\\", "/");
|
|
14233
|
+
if (isWorkflowFile(relativePath)) continue;
|
|
14234
|
+
try {
|
|
14235
|
+
files.push({
|
|
14236
|
+
path: relativePath,
|
|
14237
|
+
text: readFileSync(absoluteFile, "utf8").replace(/^\uFEFF/, "")
|
|
14238
|
+
});
|
|
14239
|
+
} catch {
|
|
14240
|
+
readSkipped++;
|
|
14241
|
+
}
|
|
14242
|
+
}
|
|
14243
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
14244
|
+
}
|
|
14245
|
+
return {
|
|
14246
|
+
result,
|
|
14247
|
+
preSuppressionFindings,
|
|
14248
|
+
files,
|
|
14249
|
+
readSkipped
|
|
14250
|
+
};
|
|
14251
|
+
}
|
|
14252
|
+
function milestoneError(error, io, debug) {
|
|
14253
|
+
if (error instanceof ConfigValidationError) {
|
|
14254
|
+
io.err(error.message);
|
|
14255
|
+
return 10;
|
|
14256
|
+
}
|
|
14257
|
+
internalErrorMessage(error, io.err, debug);
|
|
14258
|
+
return 20;
|
|
14259
|
+
}
|
|
14260
|
+
function suppressionEntries(root) {
|
|
14261
|
+
return loadSuppressions(root).entries.map((entry) => ({
|
|
14262
|
+
ruleId: entry.ruleId,
|
|
14263
|
+
reason: entry.reason,
|
|
14264
|
+
...entry.files ? { files: entry.files } : {},
|
|
14265
|
+
...entry.expires ? { expires: entry.expires } : {}
|
|
14266
|
+
}));
|
|
14267
|
+
}
|
|
14268
|
+
function numericPolicyValue(record, key, fallback, integer) {
|
|
14269
|
+
const value = record[key];
|
|
14270
|
+
if (value === void 0) return fallback;
|
|
14271
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || integer && !Number.isSafeInteger(value)) throw new ConfigValidationError(`suppression policy field ${key} must be a non-negative${integer ? " integer" : " number"}`);
|
|
14272
|
+
return value;
|
|
14273
|
+
}
|
|
14274
|
+
function loadPolicy(path) {
|
|
14275
|
+
if (!path) return STRICT_SUPPRESSION_POLICY;
|
|
14276
|
+
let text;
|
|
14277
|
+
try {
|
|
14278
|
+
text = readFileSync(path, "utf8");
|
|
14279
|
+
} catch (error) {
|
|
14280
|
+
throw new ConfigValidationError(`unable to read suppression policy: ${path}`, { cause: error });
|
|
14281
|
+
}
|
|
14282
|
+
let record;
|
|
14283
|
+
try {
|
|
14284
|
+
record = parseJsonFile(text, path, isRecord$2);
|
|
14285
|
+
} catch (error) {
|
|
14286
|
+
throw new ConfigValidationError(`invalid suppression policy: ${path}`, { cause: error });
|
|
14287
|
+
}
|
|
14288
|
+
const requireExpiration = record.requireExpiration;
|
|
14289
|
+
if (requireExpiration !== void 0 && typeof requireExpiration !== "boolean") throw new ConfigValidationError("suppression policy field requireExpiration must be boolean");
|
|
14290
|
+
const allowedRuleIds = record.allowedRuleIds;
|
|
14291
|
+
if (allowedRuleIds !== void 0 && (!Array.isArray(allowedRuleIds) || allowedRuleIds.some((ruleId) => typeof ruleId !== "string"))) throw new ConfigValidationError("suppression policy field allowedRuleIds must be a string array");
|
|
14292
|
+
const maxMassSuppressionRatio = numericPolicyValue(record, "maxMassSuppressionRatio", DEFAULT_SUPPRESSION_POLICY.maxMassSuppressionRatio, false);
|
|
14293
|
+
if (maxMassSuppressionRatio > 1) throw new ConfigValidationError("suppression policy field maxMassSuppressionRatio must be between 0 and 1");
|
|
14294
|
+
return {
|
|
14295
|
+
requireExpiration: requireExpiration ?? DEFAULT_SUPPRESSION_POLICY.requireExpiration,
|
|
14296
|
+
allowedRuleIds: allowedRuleIds ?? [],
|
|
14297
|
+
maxTotalSuppressions: numericPolicyValue(record, "maxTotalSuppressions", DEFAULT_SUPPRESSION_POLICY.maxTotalSuppressions, true),
|
|
14298
|
+
maxExpiredSuppressions: numericPolicyValue(record, "maxExpiredSuppressions", DEFAULT_SUPPRESSION_POLICY.maxExpiredSuppressions, true),
|
|
14299
|
+
maxMassSuppressionRatio
|
|
14300
|
+
};
|
|
14301
|
+
}
|
|
14302
|
+
function isTrustSnapshot(value) {
|
|
14303
|
+
return isRecord$2(value) && typeof value.scanId === "string" && typeof value.timestamp === "string" && (typeof value.score === "number" || value.score === null) && typeof value.findings === "number" && typeof value.errors === "number" && typeof value.warnings === "number" && typeof value.infos === "number" && typeof value.advisory === "number" && typeof value.trustLevel === "string" && typeof value.confidence === "number" && typeof value.evidenceCoverage === "number" && typeof value.inconclusiveRate === "number" && typeof value.partial === "boolean" && typeof value.frameworkCount === "number" && typeof value.ruleCount === "number";
|
|
14304
|
+
}
|
|
14305
|
+
function loadTrustHistory(path) {
|
|
14306
|
+
if (!existsSync(path)) return [];
|
|
14307
|
+
try {
|
|
14308
|
+
const text = readFileSync(path, "utf8");
|
|
14309
|
+
return parseJsonFile(text, path, (candidate) => {
|
|
14310
|
+
return Array.isArray(candidate) && candidate.every((entry) => isTrustSnapshot(entry));
|
|
14311
|
+
});
|
|
14312
|
+
} catch (error) {
|
|
14313
|
+
throw new ConfigValidationError(`invalid trust history: ${path}`, { cause: error });
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
function isFiniteNumber(value) {
|
|
14317
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
14318
|
+
}
|
|
14319
|
+
function isNonNegativeInteger(value) {
|
|
14320
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
14321
|
+
}
|
|
14322
|
+
function isPositiveInteger(value) {
|
|
14323
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
14324
|
+
}
|
|
14325
|
+
function isStringArray(value) {
|
|
14326
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
14327
|
+
}
|
|
14328
|
+
function isValidAnalysisStatus(value) {
|
|
14329
|
+
if (!isRecord$2(value)) return false;
|
|
14330
|
+
return (value.discovery === "complete" || value.discovery === "partial") && (value.rules === "complete" || value.rules === "partial") && isNonNegativeInteger(value.skippedFiles) && isFiniteNumber(value.durationMs) && value.durationMs >= 0 && (value.truncationReasons === void 0 || isStringArray(value.truncationReasons)) && (value.rulesCrashed === void 0 || isNonNegativeInteger(value.rulesCrashed));
|
|
14331
|
+
}
|
|
14332
|
+
function isValidDimension(value) {
|
|
14333
|
+
return isRecord$2(value) && typeof value.category === "string" && isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100 && isNonNegativeInteger(value.errors) && isNonNegativeInteger(value.warnings) && isNonNegativeInteger(value.infos);
|
|
14334
|
+
}
|
|
14335
|
+
function isValidFinding(value) {
|
|
14336
|
+
if (!isRecord$2(value)) return false;
|
|
14337
|
+
return typeof value.ruleId === "string" && value.ruleId.length > 0 && typeof value.category === "string" && typeof value.file === "string" && value.file.length > 0 && isPositiveInteger(value.line) && isPositiveInteger(value.column) && typeof value.message === "string" && value.message.length > 0 && typeof value.severity === "string" && typeof value.confidence === "string" && typeof value.findingType === "string" && typeof value.qaImpact === "string" && typeof value.why === "string" && typeof value.fix === "string";
|
|
14338
|
+
}
|
|
14339
|
+
function isValidTrustSummary(value) {
|
|
14340
|
+
if (!isRecord$2(value)) return false;
|
|
14341
|
+
return [
|
|
14342
|
+
"L0",
|
|
14343
|
+
"L1",
|
|
14344
|
+
"L2",
|
|
14345
|
+
"L3",
|
|
14346
|
+
"L4",
|
|
14347
|
+
"L5"
|
|
14348
|
+
].includes(String(value.level)) && isFiniteNumber(value.confidence) && value.confidence >= 0 && value.confidence <= 1 && isFiniteNumber(value.evidenceCoverage) && value.evidenceCoverage >= 0 && value.evidenceCoverage <= 1 && isFiniteNumber(value.inconclusiveRate) && value.inconclusiveRate >= 0 && value.inconclusiveRate <= 1 && (value.measuredFpOfFiredRules === void 0 || isFiniteNumber(value.measuredFpOfFiredRules) && value.measuredFpOfFiredRules >= 0 && value.measuredFpOfFiredRules <= 1) && isStringArray(value.provisionalRuleIds) && (value.confidenceCeiling === void 0 || isFiniteNumber(value.confidenceCeiling) && value.confidenceCeiling >= 0 && value.confidenceCeiling <= 1) && isStringArray(value.ceilingReasons);
|
|
14349
|
+
}
|
|
14350
|
+
function isValidProvenance(value) {
|
|
14351
|
+
if (!isRecord$2(value)) return false;
|
|
14352
|
+
return isNonNegativeInteger(value.testFiles) && isNonNegativeInteger(value.generatedMarkedFiles) && isNonNegativeInteger(value.codegenLikeFiles) && isFiniteNumber(value.shareMarkedGenerated) && value.shareMarkedGenerated >= 0 && value.shareMarkedGenerated <= 1 && isNonNegativeInteger(value.findingsInGeneratedFiles) && isNonNegativeInteger(value.findingsInUnmarkedFiles) && typeof value.note === "string";
|
|
14353
|
+
}
|
|
14354
|
+
function isValidForensicVerdicts(value) {
|
|
14355
|
+
if (!isRecord$2(value) || !isRecord$2(value.byVerdict)) return false;
|
|
14356
|
+
return isNonNegativeInteger(value.classifications) && isNonNegativeInteger(value.inconclusive) && Object.values(value.byVerdict).every((count) => isNonNegativeInteger(count));
|
|
14357
|
+
}
|
|
14358
|
+
function isValidSummary(value) {
|
|
14359
|
+
if (!isRecord$2(value)) return false;
|
|
14360
|
+
return typeof value.digest === "string" && value.digest.length > 0 && isNonNegativeInteger(value.findings) && (value.score === null || isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100) && isNonNegativeInteger(value.errors) && isNonNegativeInteger(value.warnings) && isNonNegativeInteger(value.infos) && isNonNegativeInteger(value.advisory);
|
|
14361
|
+
}
|
|
14362
|
+
function isValidAnnotation(value) {
|
|
14363
|
+
if (!isRecord$2(value)) return false;
|
|
14364
|
+
return typeof value.path === "string" && value.path.length > 0 && isPositiveInteger(value.start_line) && (value.annotation_level === "failure" || value.annotation_level === "warning" || value.annotation_level === "notice") && typeof value.message === "string" && typeof value.ruleId === "string" && value.ruleId.length > 0 && (value.detectorRevision === void 0 || isPositiveInteger(value.detectorRevision)) && typeof value.advisory === "boolean";
|
|
14365
|
+
}
|
|
14366
|
+
function isValidCompleteness(value) {
|
|
14367
|
+
if (!isRecord$2(value)) return false;
|
|
14368
|
+
return typeof value.partial === "boolean" && (value.discovery === "complete" || value.discovery === "partial") && (value.rules === "complete" || value.rules === "partial") && isNonNegativeInteger(value.skippedFiles) && isNonNegativeInteger(value.rulesCrashed) && isStringArray(value.truncationReasons) && typeof value.frameworkDetectionUnknown === "boolean" && isFiniteNumber(value.durationMs) && value.durationMs >= 0;
|
|
14369
|
+
}
|
|
14370
|
+
function isValidContract(value) {
|
|
14371
|
+
if (!isRecord$2(value)) return false;
|
|
14372
|
+
return Number.isSafeInteger(value.contractVersion) && isValidSummary(value.summary) && Array.isArray(value.annotations) && value.annotations.length <= 50 && value.annotations.every((annotation) => isValidAnnotation(annotation)) && typeof value.annotationsTruncated === "boolean" && isValidCompleteness(value.completeness) && (value.trustSummary === void 0 || isValidTrustSummary(value.trustSummary)) && (value.provenance === void 0 || isValidProvenance(value.provenance)) && (value.forensicVerdicts === void 0 || isValidForensicVerdicts(value.forensicVerdicts));
|
|
14373
|
+
}
|
|
14374
|
+
function isScanResultDocument(value) {
|
|
14375
|
+
if (!isRecord$2(value) || value.schemaVersion !== 1) return false;
|
|
14376
|
+
return (value.score === null || isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100) && typeof value.partial === "boolean" && isStringArray(value.frameworks) && Array.isArray(value.dimensions) && value.dimensions.every((dimension) => isValidDimension(dimension)) && Array.isArray(value.findings) && value.findings.every((finding) => isValidFinding(finding)) && isValidAnalysisStatus(value.analysisStatus) && (value.agenticProfile === void 0 || isValidProvenance(value.agenticProfile)) && (value.trustSummary === void 0 || isValidTrustSummary(value.trustSummary)) && (value.forensicVerdicts === void 0 || isValidForensicVerdicts(value.forensicVerdicts)) && isValidContract(value.contract);
|
|
14377
|
+
}
|
|
14378
|
+
function emitVerification(verification, json, io) {
|
|
14379
|
+
if (json) io.out(JSON.stringify(verification, null, 2));
|
|
14380
|
+
else io.out(renderContractVerification(verification));
|
|
14381
|
+
}
|
|
14382
|
+
function cloneContract(contract) {
|
|
14383
|
+
return JSON.parse(JSON.stringify(contract));
|
|
14384
|
+
}
|
|
14385
|
+
function verifyPersistedContract(document, freshResult) {
|
|
14386
|
+
const artifactVerification = verifyMachineContract(document, document.contract);
|
|
14387
|
+
const freshComparable = cloneContract(document.contract);
|
|
14388
|
+
const expected = buildMachineContract(freshResult);
|
|
14389
|
+
freshComparable.completeness.durationMs = expected.completeness.durationMs;
|
|
14390
|
+
const freshVerification = verifyMachineContract(freshResult, freshComparable);
|
|
14391
|
+
const violations = [...artifactVerification.violations, ...freshVerification.violations].filter((value, index, all) => all.indexOf(value) === index);
|
|
14392
|
+
if (!freshVerification.passed) violations.push("Persisted contract is not bound to a fresh scan");
|
|
14393
|
+
if (!artifactVerification.passed) violations.push("Persisted scan result is not consistent with its contract");
|
|
14394
|
+
const checks = [
|
|
14395
|
+
...artifactVerification.checks,
|
|
14396
|
+
...freshVerification.checks,
|
|
14397
|
+
{
|
|
14398
|
+
name: "fresh-scan-binding",
|
|
14399
|
+
status: freshVerification.passed && artifactVerification.passed ? "pass" : "fail",
|
|
14400
|
+
detail: freshVerification.passed && artifactVerification.passed ? "Contract matches an independent fresh scan" : "Contract is not bound to the current scan"
|
|
14401
|
+
}
|
|
14402
|
+
];
|
|
14403
|
+
return {
|
|
14404
|
+
...freshVerification,
|
|
14405
|
+
passed: violations.length === 0,
|
|
14406
|
+
violations: violations.filter((value, index, all) => all.indexOf(value) === index),
|
|
14407
|
+
checks,
|
|
14408
|
+
freshScanMatch: freshVerification.passed,
|
|
14409
|
+
artifactResultMatch: artifactVerification.passed
|
|
14410
|
+
};
|
|
14411
|
+
}
|
|
14412
|
+
function runFrameworkMaturityCommand(argv, io = {
|
|
14413
|
+
out,
|
|
14414
|
+
err
|
|
14415
|
+
}) {
|
|
14416
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14417
|
+
if (!args) return 10;
|
|
14418
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["framework"]), io)) return 10;
|
|
14419
|
+
const selected = args.framework ? getFrameworkMaturity(args.framework) : void 0;
|
|
14420
|
+
if (args.framework && !selected) {
|
|
14421
|
+
io.err(`mjolnir: unknown framework "${args.framework}"`);
|
|
14422
|
+
return 10;
|
|
14423
|
+
}
|
|
14424
|
+
const frameworks = selected ? [selected] : getAllFrameworkMaturity();
|
|
14425
|
+
if (args.json) io.out(JSON.stringify({
|
|
14426
|
+
calibrationAuthority: "human",
|
|
14427
|
+
automatedClosureAllowed: false,
|
|
14428
|
+
frameworks
|
|
14429
|
+
}, null, 2));
|
|
14430
|
+
else if (selected?.frameworkId === "playwright") io.out(`Calibration authority: human; automated closure is not permitted.\n${renderPlaywrightMaturityReport(getPlaywrightMaturityReport())}`);
|
|
14431
|
+
else {
|
|
14432
|
+
io.out("Calibration authority: human; automated closure is not permitted.");
|
|
14433
|
+
for (const framework of frameworks) io.out(`${framework.frameworkId}: ${framework.currentMaturity} → ${framework.targetMaturity} (${framework.maturityScore}/100)`);
|
|
14434
|
+
}
|
|
14435
|
+
return 0;
|
|
14436
|
+
}
|
|
14437
|
+
async function runSuppressionGateCommand(argv, io = {
|
|
14438
|
+
out,
|
|
14439
|
+
err
|
|
14440
|
+
}) {
|
|
14441
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14442
|
+
if (!args) return 10;
|
|
14443
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["policy"]), io)) return 10;
|
|
14444
|
+
try {
|
|
14445
|
+
const scan = await runMilestoneScan(args, io, false, true);
|
|
14446
|
+
if ("code" in scan) return scan.code;
|
|
14447
|
+
const target = resolve(args.target);
|
|
14448
|
+
const gate = computeSuppressionGovernanceGate(suppressionEntries(target), scan.preSuppressionFindings, loadPolicy(args.policy ? resolve(target, args.policy) : void 0), new Set(KNOWN_RULE_IDS$1), /* @__PURE__ */ new Date());
|
|
14449
|
+
if (args.json) io.out(JSON.stringify(gate, null, 2));
|
|
14450
|
+
else io.out(renderSuppressionGovernanceResult(gate));
|
|
14451
|
+
if (scan.result.partial) return 2;
|
|
14452
|
+
return gate.passed ? 0 : 1;
|
|
14453
|
+
} catch (error) {
|
|
14454
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14455
|
+
}
|
|
14456
|
+
}
|
|
14457
|
+
async function runCrossFileCommand(argv, io = {
|
|
14458
|
+
out,
|
|
14459
|
+
err
|
|
14460
|
+
}) {
|
|
14461
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14462
|
+
if (!args) return 10;
|
|
14463
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(), io)) return 10;
|
|
14464
|
+
try {
|
|
14465
|
+
const scan = await runMilestoneScan(args, io, true);
|
|
14466
|
+
if ("code" in scan) return scan.code;
|
|
14467
|
+
const analysis = analyzeCrossFileSignals(scan.files, scan.result.findings, resolve(args.target));
|
|
14468
|
+
if (args.json) io.out(JSON.stringify(analysis, null, 2));
|
|
14469
|
+
else io.out(renderCrossFileAnalysis(analysis));
|
|
14470
|
+
if (scan.result.partial || scan.readSkipped > 0 || scan.files.length === 0) return 2;
|
|
14471
|
+
return analysis.signals.length > 0 ? 1 : 0;
|
|
14472
|
+
} catch (error) {
|
|
14473
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14474
|
+
}
|
|
14475
|
+
}
|
|
14476
|
+
async function runContractVerifyCommand(argv, io = {
|
|
14477
|
+
out,
|
|
14478
|
+
err
|
|
14479
|
+
}) {
|
|
14480
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14481
|
+
if (!args) return 10;
|
|
14482
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["contract"]), io)) return 10;
|
|
14483
|
+
try {
|
|
14484
|
+
if (args.contract) {
|
|
14485
|
+
const target = resolve(args.target);
|
|
14486
|
+
const valid = validateTarget(target, io);
|
|
14487
|
+
if ("code" in valid) return valid.code;
|
|
14488
|
+
const path = resolve(target, args.contract);
|
|
14489
|
+
let document;
|
|
14490
|
+
try {
|
|
14491
|
+
document = parseJsonFile(readFileSync(path, "utf8"), path, isScanResultDocument);
|
|
14492
|
+
} catch (error) {
|
|
14493
|
+
throw new ConfigValidationError(`invalid contract document: ${path}`, { cause: error });
|
|
14494
|
+
}
|
|
14495
|
+
const relativePath = relative(target, path).replaceAll("\\", "/");
|
|
14496
|
+
const hiddenDirectory = relativePath !== "" && !relativePath.startsWith("../") && !isAbsolute(relativePath) && existsSync(path) ? mkdtempSync(join(tmpdir(), "mjolnir-contract-")) : void 0;
|
|
14497
|
+
const hiddenPath = hiddenDirectory ? join(hiddenDirectory, basename(path)) : void 0;
|
|
14498
|
+
if (hiddenPath) renameSync(path, hiddenPath);
|
|
14499
|
+
let fresh;
|
|
14500
|
+
try {
|
|
14501
|
+
fresh = await runMilestoneScan(args, io, false);
|
|
14502
|
+
} finally {
|
|
14503
|
+
if (hiddenPath && existsSync(hiddenPath)) renameSync(hiddenPath, path);
|
|
14504
|
+
if (hiddenDirectory) rmSync(hiddenDirectory, {
|
|
14505
|
+
recursive: true,
|
|
14506
|
+
force: true
|
|
14507
|
+
});
|
|
14508
|
+
}
|
|
14509
|
+
if ("code" in fresh) return fresh.code;
|
|
14510
|
+
const verification = verifyPersistedContract(document, fresh.result);
|
|
14511
|
+
emitVerification(verification, args.json, io);
|
|
14512
|
+
if (!verification.passed) return 1;
|
|
14513
|
+
if (document.partial || fresh.result.partial) return 2;
|
|
14514
|
+
return 0;
|
|
14515
|
+
}
|
|
14516
|
+
const scan = await runMilestoneScan(args, io, false);
|
|
14517
|
+
if ("code" in scan) return scan.code;
|
|
14518
|
+
const verification = verifyMachineContract(scan.result, buildMachineContract(scan.result));
|
|
14519
|
+
emitVerification(verification, args.json, io);
|
|
14520
|
+
if (scan.result.partial) return 2;
|
|
14521
|
+
return verification.passed ? 0 : 1;
|
|
14522
|
+
} catch (error) {
|
|
14523
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14524
|
+
}
|
|
14525
|
+
}
|
|
14526
|
+
async function runTrustTrendCommand(argv, io = {
|
|
14527
|
+
out,
|
|
14528
|
+
err
|
|
14529
|
+
}) {
|
|
14530
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14531
|
+
if (!args) return 10;
|
|
14532
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["history", "recordedAt"]), io)) return 10;
|
|
14533
|
+
try {
|
|
14534
|
+
const scan = await runMilestoneScan(args, io, false);
|
|
14535
|
+
if ("code" in scan) return scan.code;
|
|
14536
|
+
if (!scan.result.runIdentity || !scan.result.trustSummary) {
|
|
14537
|
+
if (args.json) io.out(JSON.stringify({ error: "trust_inputs_unavailable" }, null, 2));
|
|
14538
|
+
else io.err("mjolnir: scan did not produce run identity and trust summary");
|
|
14539
|
+
return 2;
|
|
14540
|
+
}
|
|
14541
|
+
const target = resolve(args.target);
|
|
14542
|
+
const historyPath = resolve(target, args.history ?? join(".mjolnir", "trust-history.json"));
|
|
14543
|
+
const history = loadTrustHistory(historyPath);
|
|
14544
|
+
const snapshot = computeTrustSnapshot(scan.result, scan.result.runIdentity, scan.result.trustSummary, args.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
14545
|
+
const merged = new Map(history.map((entry) => [`${entry.scanId}\u0000${entry.timestamp}`, entry]));
|
|
14546
|
+
merged.set(`${snapshot.scanId}\u0000${snapshot.timestamp}`, snapshot);
|
|
14547
|
+
const snapshots = [...merged.values()].sort((left, right) => left.timestamp.localeCompare(right.timestamp) || left.scanId.localeCompare(right.scanId));
|
|
14548
|
+
writeFileAtomic(historyPath, `${JSON.stringify(snapshots, null, 2)}\n`);
|
|
14549
|
+
const trend = analyzeTrustTrends(snapshots);
|
|
14550
|
+
if (args.json) io.out(JSON.stringify(trend, null, 2));
|
|
14551
|
+
else io.out(renderTrustTrend(trend));
|
|
14552
|
+
return scan.result.partial ? 2 : 0;
|
|
14553
|
+
} catch (error) {
|
|
14554
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14555
|
+
}
|
|
14556
|
+
}
|
|
14557
|
+
async function runEvidenceGraphCommand(argv, io = {
|
|
14558
|
+
out,
|
|
14559
|
+
err
|
|
14560
|
+
}) {
|
|
14561
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14562
|
+
if (!args) return 10;
|
|
14563
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["file", "rule"]), io)) return 10;
|
|
14564
|
+
if (args.file && args.rule) {
|
|
14565
|
+
io.err("mjolnir: evidence-graph accepts --file or --rule, not both");
|
|
14566
|
+
return 10;
|
|
14567
|
+
}
|
|
14568
|
+
try {
|
|
14569
|
+
const scan = await runMilestoneScan(args, io, true);
|
|
14570
|
+
if ("code" in scan) return scan.code;
|
|
14571
|
+
if (!scan.result.runIdentity) {
|
|
14572
|
+
if (args.json) io.out(JSON.stringify({ error: "run_identity_unavailable" }, null, 2));
|
|
14573
|
+
else io.err("mjolnir: scan did not produce a run identity");
|
|
14574
|
+
return 2;
|
|
14575
|
+
}
|
|
14576
|
+
const graph = buildEvidenceGraphFromScan(scan.result, scan.result.runIdentity, scan.files);
|
|
14577
|
+
if (args.file || args.rule) {
|
|
14578
|
+
const findings = args.file ? queryFindingsByFile(scan.result, args.file) : queryFindingsByRule(scan.result, args.rule ?? "");
|
|
14579
|
+
if (args.json) io.out(JSON.stringify({ findings }, null, 2));
|
|
14580
|
+
else io.out(findings.length === 0 ? "No matching findings." : findings.map((finding) => `${finding.ruleId} ${finding.file}:${finding.line}`).join("\n"));
|
|
14581
|
+
return scan.result.partial ? 2 : 0;
|
|
14582
|
+
}
|
|
14583
|
+
if (args.json) io.out(JSON.stringify(graph, null, 2));
|
|
14584
|
+
else io.out(renderEvidenceGraphResult(graph));
|
|
14585
|
+
return scan.result.partial || scan.readSkipped > 0 || scan.files.length === 0 ? 2 : 0;
|
|
14586
|
+
} catch (error) {
|
|
14587
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14590
|
+
async function runCIIntegrityCommand(argv, io = {
|
|
14591
|
+
out,
|
|
14592
|
+
err
|
|
14593
|
+
}) {
|
|
14594
|
+
const args = parseMilestoneArgs(argv, io);
|
|
14595
|
+
if (!args) return 10;
|
|
14596
|
+
if (rejectUnexpectedOptions(args, /* @__PURE__ */ new Set(["policy"]), io)) return 10;
|
|
14597
|
+
try {
|
|
14598
|
+
const scan = await runMilestoneScan(args, io, false);
|
|
14599
|
+
if ("code" in scan) return scan.code;
|
|
14600
|
+
const target = resolve(args.target);
|
|
14601
|
+
const report = computeVerificationIntelligence(target, suppressionEntries(target), scan.preSuppressionFindings, loadPolicy(args.policy ? resolve(target, args.policy) : void 0), new Set(KNOWN_RULE_IDS$1), /* @__PURE__ */ new Date());
|
|
14602
|
+
if (args.json) io.out(JSON.stringify(report, null, 2));
|
|
14603
|
+
else io.out(renderCIIntegrityReport(report));
|
|
14604
|
+
if (scan.result.partial) return 2;
|
|
14605
|
+
return report.failed > 0 || report.warned > 0 ? 1 : 0;
|
|
14606
|
+
} catch (error) {
|
|
14607
|
+
return milestoneError(error, io, argv.includes("--debug"));
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
//#endregion
|
|
14611
|
+
//#region src/cli.ts
|
|
14612
|
+
/**
|
|
14613
|
+
* Mjölnir CLI entry point (W1-02).
|
|
14614
|
+
* Exit codes (§24.1, frozen): 0 clean · 1 findings ≥ gate · 2 partial ·
|
|
14615
|
+
* 10 usage error · 20 internal error.
|
|
14616
|
+
*/
|
|
14617
|
+
/**
|
|
14618
|
+
* Tool version for `mjolnir --version`.
|
|
14619
|
+
*
|
|
14620
|
+
* A literal, not a package.json read: the shipped artifact is a single
|
|
14621
|
+
* bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
|
|
14622
|
+
* where the file happens to sit after install. This follows the same
|
|
14623
|
+
* discipline as SARIF's `driver.version` — kept in sync by
|
|
14624
|
+
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
14625
|
+
* `tests/version-consistency.spec.ts` locally. R4c moved the literal to
|
|
14626
|
+
* src/engine/version.ts (a leaf module) so the scan pipeline's run
|
|
14627
|
+
* identity can carry it without a cli.ts import cycle; this re-export
|
|
14628
|
+
* keeps every existing consumer stable.
|
|
14629
|
+
*/
|
|
14630
|
+
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
|
|
14631
|
+
function parseArgs(argv, onError) {
|
|
14632
|
+
const args = {
|
|
14633
|
+
target: ".",
|
|
14634
|
+
json: false,
|
|
14635
|
+
verbose: false,
|
|
14636
|
+
maxDurationMs: Number.POSITIVE_INFINITY,
|
|
14637
|
+
scopeChanged: false,
|
|
14638
|
+
format: "terminal"
|
|
14639
|
+
};
|
|
14640
|
+
const reject = (detail) => {
|
|
14641
|
+
onError?.(detail);
|
|
14642
|
+
return null;
|
|
14643
|
+
};
|
|
14644
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14645
|
+
const a = argv[i] ?? "";
|
|
14646
|
+
if (a === "--json") {
|
|
14647
|
+
args.json = true;
|
|
14648
|
+
args.format = "json";
|
|
14649
|
+
} else if (a === "--format") {
|
|
14650
|
+
const fmt = argv[++i];
|
|
14651
|
+
if (fmt === void 0) return reject({
|
|
14652
|
+
flag: "--format",
|
|
14653
|
+
token: "(missing)"
|
|
14654
|
+
});
|
|
14655
|
+
if (fmt === "sarif") args.format = "sarif";
|
|
14656
|
+
else if (fmt === "mermaid") args.format = "mermaid";
|
|
14657
|
+
else if (fmt === "codequality") args.format = "codequality";
|
|
14658
|
+
else if (fmt === "json") {
|
|
14659
|
+
args.format = "json";
|
|
14660
|
+
args.json = true;
|
|
14661
|
+
} else if (fmt !== "terminal") return reject({
|
|
14662
|
+
flag: "--format",
|
|
14663
|
+
token: fmt
|
|
14664
|
+
});
|
|
14665
|
+
} else if (a === "--verbose") args.verbose = true;
|
|
14666
|
+
else if (a === "--scope") {
|
|
14667
|
+
const mode = argv[++i];
|
|
14668
|
+
if (mode === "changed") args.scopeChanged = true;
|
|
14669
|
+
else return reject({
|
|
14670
|
+
flag: "--scope",
|
|
14671
|
+
token: mode
|
|
14672
|
+
});
|
|
14673
|
+
} else if (a === "--base") {
|
|
14674
|
+
const ref = argv[++i];
|
|
14675
|
+
if (!ref || ref.startsWith("-")) return reject({
|
|
14676
|
+
flag: "--base",
|
|
14677
|
+
token: ref
|
|
14678
|
+
});
|
|
14679
|
+
args.base = ref;
|
|
14680
|
+
} else if (a === "--max-duration") {
|
|
14681
|
+
const v = Number(argv[++i]);
|
|
14682
|
+
if (!Number.isFinite(v) || v <= 0) return reject({
|
|
14683
|
+
flag: "--max-duration",
|
|
14684
|
+
token: argv[i]
|
|
14685
|
+
});
|
|
14686
|
+
args.maxDurationMs = v * 1e3;
|
|
14687
|
+
} else if (a === "--width") {
|
|
11165
14688
|
const v = Number(argv[++i]);
|
|
11166
14689
|
if (!Number.isFinite(v) || v <= 0) return reject({
|
|
11167
14690
|
flag: "--width",
|
|
@@ -11284,10 +14807,23 @@ function parseArgsOrUsage(argv, io) {
|
|
|
11284
14807
|
reported = true;
|
|
11285
14808
|
io.err(usageErrorMessage(detail));
|
|
11286
14809
|
});
|
|
11287
|
-
if (!args && !reported) printUsage(io.out);
|
|
14810
|
+
if (!args && !reported) printUsage(io.out, resolveHelpWidth(argv));
|
|
11288
14811
|
return args;
|
|
11289
14812
|
}
|
|
11290
14813
|
/**
|
|
14814
|
+
* Resolves the effective terminal width for help rendering. Scans the
|
|
14815
|
+
* raw argv for `--width <cols>` (the help path runs before full arg
|
|
14816
|
+
* parsing, so we read it directly); falls back to the detected TTY
|
|
14817
|
+
* column width, then the default help width of 88.
|
|
14818
|
+
*/
|
|
14819
|
+
function resolveHelpWidth(argv) {
|
|
14820
|
+
for (let i = 0; i < argv.length; i++) if (argv[i] === "--width") {
|
|
14821
|
+
const v = Number(argv[i + 1]);
|
|
14822
|
+
if (Number.isFinite(v)) return Math.max(1, Math.floor(v));
|
|
14823
|
+
}
|
|
14824
|
+
return process$1.stdout.columns ?? 88;
|
|
14825
|
+
}
|
|
14826
|
+
/**
|
|
11291
14827
|
* Audit H-4 (extended to every scanning subcommand): a nonexistent or
|
|
11292
14828
|
* non-directory target is a usage error — a typo'd CI path must be a
|
|
11293
14829
|
* loud red, never a silent green. Returns the exit code (10) or null
|
|
@@ -11345,7 +14881,8 @@ function runCiInstall(argv, io = {
|
|
|
11345
14881
|
io.err("Unknown gate level. Use: advisory | error | warning");
|
|
11346
14882
|
return 10;
|
|
11347
14883
|
}
|
|
11348
|
-
const
|
|
14884
|
+
const gate = gateArg ?? "advisory";
|
|
14885
|
+
const result = ciInstall(resolve("."), gate, {
|
|
11349
14886
|
force,
|
|
11350
14887
|
action: !noAction
|
|
11351
14888
|
});
|
|
@@ -11357,7 +14894,8 @@ function runCiInstall(argv, io = {
|
|
|
11357
14894
|
return 10;
|
|
11358
14895
|
}
|
|
11359
14896
|
io.out(`${result.existed ? "Updated" : "Created"} ${result.written}`);
|
|
11360
|
-
io.out(noAction ?
|
|
14897
|
+
io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b0 (immutable pin). Gate: ${gate}.`);
|
|
14898
|
+
io.out(gate === "advisory" ? "Advisory mode reports findings without blocking. Opt in with --gate error or --gate warning." : `Blocking mode fails on ${gate} findings.`);
|
|
11361
14899
|
io.out("Change with: mjolnir ci install --gate error|warning|advisory");
|
|
11362
14900
|
if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
|
|
11363
14901
|
return 0;
|
|
@@ -11382,44 +14920,9 @@ function runSuppressions(io = { out }) {
|
|
|
11382
14920
|
* nor an existing path is a TYPO, and a typo must not silently fall
|
|
11383
14921
|
* through to a scan of whatever the remaining arguments parse to.
|
|
11384
14922
|
*/
|
|
11385
|
-
const SUBCOMMANDS =
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
"suppressions",
|
|
11389
|
-
"forensics",
|
|
11390
|
-
"triage",
|
|
11391
|
-
"badge",
|
|
11392
|
-
"trust-report",
|
|
11393
|
-
"debt",
|
|
11394
|
-
"impact",
|
|
11395
|
-
"baseline",
|
|
11396
|
-
"report",
|
|
11397
|
-
"policy",
|
|
11398
|
-
"quarantine",
|
|
11399
|
-
"analyze",
|
|
11400
|
-
"ci-adapter",
|
|
11401
|
-
"dashboard",
|
|
11402
|
-
"enterprise",
|
|
11403
|
-
"maturity",
|
|
11404
|
-
"diff",
|
|
11405
|
-
"pr-comment",
|
|
11406
|
-
"stats",
|
|
11407
|
-
"fix",
|
|
11408
|
-
"create-rule",
|
|
11409
|
-
"handover",
|
|
11410
|
-
"init",
|
|
11411
|
-
"pw-report",
|
|
11412
|
-
"doctor",
|
|
11413
|
-
"rules",
|
|
11414
|
-
"explain",
|
|
11415
|
-
"release-trust",
|
|
11416
|
-
"doctor:playwright",
|
|
11417
|
-
"mcp",
|
|
11418
|
-
"business-case",
|
|
11419
|
-
"release-report"
|
|
11420
|
-
]);
|
|
11421
|
-
function printUsage(print) {
|
|
11422
|
-
print(renderRootHelp());
|
|
14923
|
+
const SUBCOMMANDS = new Set(CLI_COMMAND_NAMES);
|
|
14924
|
+
function printUsage(print, width = 88) {
|
|
14925
|
+
print(renderRootHelp(1, { width }));
|
|
11423
14926
|
}
|
|
11424
14927
|
async function main(argv = process$1.argv.slice(2), io = {
|
|
11425
14928
|
out,
|
|
@@ -11473,7 +14976,14 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
11473
14976
|
"doctor:playwright": (a, o) => runDoctorPlaywright(a, o),
|
|
11474
14977
|
why: (a, o) => runWhyCommand(a, o),
|
|
11475
14978
|
handoff: (a, o) => runHandoffCommand(a, o),
|
|
11476
|
-
install: (a, o) => runInstallCommand(a, o)
|
|
14979
|
+
install: (a, o) => runInstallCommand(a, o),
|
|
14980
|
+
"ci-integrity": (a, o) => runCIIntegrityCommand(a, o),
|
|
14981
|
+
"framework-maturity": (a, o) => runFrameworkMaturityCommand(a, o),
|
|
14982
|
+
"suppression-gate": (a, o) => runSuppressionGateCommand(a, o),
|
|
14983
|
+
"cross-file": (a, o) => runCrossFileCommand(a, o),
|
|
14984
|
+
"contract-verify": (a, o) => runContractVerifyCommand(a, o),
|
|
14985
|
+
"trust-trend": (a, o) => runTrustTrendCommand(a, o),
|
|
14986
|
+
"evidence-graph": (a, o) => runEvidenceGraphCommand(a, o)
|
|
11477
14987
|
};
|
|
11478
14988
|
const verb = argv[0] ?? "";
|
|
11479
14989
|
const handler = Object.hasOwn(VERBS, verb) ? VERBS[verb] : void 0;
|
|
@@ -11485,7 +14995,7 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
11485
14995
|
if (argv[0] === "help") return runHelpCommand(argv.slice(1), io);
|
|
11486
14996
|
if (SUBCOMMANDS.has(argv[0] ?? "")) {
|
|
11487
14997
|
io.err(`mjolnir: incomplete or unknown subcommand "${argv[0]}".`);
|
|
11488
|
-
printUsage(io.out);
|
|
14998
|
+
printUsage(io.out, resolveHelpWidth(argv));
|
|
11489
14999
|
return 10;
|
|
11490
15000
|
}
|
|
11491
15001
|
if (argv[0] !== void 0 && argv[0].length > 0 && !argv[0].startsWith("-") && !existsSync(argv[0]) && argv[0].match(/^[a-z][\w:-]*$/i) !== null) {
|
|
@@ -11506,18 +15016,19 @@ function runHelpCommand(argv, io = {
|
|
|
11506
15016
|
err
|
|
11507
15017
|
}) {
|
|
11508
15018
|
const tokens = argv.filter((a) => !a.startsWith("-"));
|
|
15019
|
+
const helpWidth = resolveHelpWidth(argv);
|
|
11509
15020
|
if (tokens.length >= 2) {
|
|
11510
15021
|
const joined = `${tokens[0]} ${tokens[1]}`;
|
|
11511
15022
|
if (hasVerbHelp(joined)) {
|
|
11512
|
-
io.out(renderVerbHelp(joined));
|
|
15023
|
+
io.out(renderVerbHelp(joined, { width: helpWidth }));
|
|
11513
15024
|
return 0;
|
|
11514
15025
|
}
|
|
11515
15026
|
}
|
|
11516
15027
|
if (tokens.length > 0) {
|
|
11517
|
-
io.out(renderVerbHelp(tokens[0]));
|
|
15028
|
+
io.out(renderVerbHelp(tokens[0], { width: helpWidth }));
|
|
11518
15029
|
return 0;
|
|
11519
15030
|
}
|
|
11520
|
-
io.out(renderRootHelp(1));
|
|
15031
|
+
io.out(renderRootHelp(1, { width: helpWidth }));
|
|
11521
15032
|
return 0;
|
|
11522
15033
|
}
|
|
11523
15034
|
function isEntryPoint() {
|
|
@@ -11536,4 +15047,4 @@ if (isEntryPoint()) try {
|
|
|
11536
15047
|
process$1.exitCode = 20;
|
|
11537
15048
|
}
|
|
11538
15049
|
//#endregion
|
|
11539
|
-
export { ENGINE_VERSION as CLI_VERSION, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderScanOutput, runAnalyzeCommand, runBadgeCommand, runBaselineCommand, runBusinessCaseCommand, runCiAdapterCommand, runCiInstall, runCreateRuleCommand, runDashboardCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runEnterpriseCommand, runExecReportCommand, runExplainCommand, runFixCommand, runForensicsCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runInstallCommand, runMaturityCommand, runMutationCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runQuarantineCommand, runReleaseReportCommand, runReleaseTrustCommand, runReportPlaywrightCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSummaryCommand, runSuppressions, runTrendCommand, runTriageCommand, runTrustReportCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
15050
|
+
export { ENGINE_VERSION as CLI_VERSION, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderScanOutput, runAnalyzeCommand, runBadgeCommand, runBaselineCommand, runBusinessCaseCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCreateRuleCommand, runCrossFileCommand, runDashboardCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runEnterpriseCommand, runEvidenceGraphCommand, runExecReportCommand, runExplainCommand, runFixCommand, runForensicsCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runInstallCommand, runMaturityCommand, runMutationCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runQuarantineCommand, runReleaseReportCommand, runReleaseTrustCommand, runReportPlaywrightCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSummaryCommand, runSuppressionGateCommand, runSuppressions, runTrendCommand, runTriageCommand, runTrustReportCommand, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|