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/dist/cli.mjs CHANGED
@@ -1,21 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { $ as SCAN_ADAPTERS, A as box, B as headlineFor, C as buildFooter, D as plainContext, E as panel, F as scoreGauge, G as capForTier, H as EVIDENCE, I as shouldColorize, J as massCeiling, K as computeDimensions, L as shouldUseAscii, M as padTo, N as palette, O as sectionHeader, P as sanitizeData, Q as resolveGitPath, R as wrapText, S as FLAKE_GLYPH, St as isValidCategory, T as okIcon, U as TINT, V as BADGE_BAND, W as TRUST, X as RULES, Y as RETIRED_RULE_IDS, Z as getRule, _t as QA_IMPACT_LABELS, at as parseTsFile, b as renderSuppressions, bt as deriveEvidenceLevel, ct as createIgnoreMatcher, dt as loadConfig, et as SEARCHED_FOR, ft as isRecord, gt as DEDUCTIONS, h as scan_pipeline_exports, ht as MEASURED_FP, it as computeCodeText, j as measure, k as severityIcon, lt as isLintFixtureDir, m as runScan$1, mt as ENGINE_VERSION, n as KNOWN_RULE_IDS$1, nt as parseAzurePipeline, ot as detectFrameworks, pt as parseJsonFile, q as deductionFor, rt as parseWorkflow, st as DEFAULT_IGNORE_MATCHER, tt as isAzurePipelineFixture, ut as ConfigValidationError, v as loadLocalRules, vt as RULE_CATEGORIES, w as nextStep, x as runForensics, xt as isAdvisoryFinding, y as loadSuppressions, yt as SEVERITY_ORDER$1, z as deriveScoreState } from "./scan-pipeline-D3Yk2cef.mjs";
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 (action-based by default; scan + annotations + gate)",
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 an MCP server over stdio (scan / explain / diff / verify tools)",
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
- const pad = f.flag.padEnd(22);
423
- lines.push(` ${pad}${f.summary}`);
424
- }
425
- lines.push(" -v, --version print the installed version and exit");
426
- lines.push(" -h, --help show this help");
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
- const usage = e.usage.replace(/^mjolnir /, "").padEnd(46);
434
- lines.push(` ${usage}${e.summary}`);
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" : "keep the gate green (mjolnir ci install)");
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, p, ascii);
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
- lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
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
- * keeps the `*** FLAWLESS VICTORY ***` contract string (test-locked in
3148
- * empty-states/long-tail-arms).
3374
+ * labels the result as static rather than claiming a clean suite.
3149
3375
  */
3150
- function appendForgedBlock(lines, p, ascii) {
3376
+ function appendForgedBlock(lines, ui) {
3377
+ const { p, ascii } = ui;
3151
3378
  lines.push("");
3152
- if (ascii) lines.push(p.forged("*** FLAWLESS VICTORY ***"));
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. The suite is clean."));
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" : "nothing to triage — keep the gate green with `mjolnir ci install`";
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 — the suite is clean.");
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
- function generateGitHubWorkflow(findings) {
10631
- const hasErrors = findings.some((f) => f.severity === "error");
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@v4
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: npx mjolnir scan
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(findings) {
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
- - npx mjolnir scan
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(findings) {
10903
+ function generateJenkinsfile() {
10663
10904
  return `pipeline {
10664
10905
  agent any
10665
10906
  stages {
10666
10907
  stage('QA Check') {
10667
10908
  steps {
10668
- sh 'npx mjolnir scan'
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
- async function runCiAdapterCommand(argv, io) {
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
- let findings = [];
10693
- try {
10694
- const { runScan } = await import("./scan-pipeline-C0ka-RmX.mjs");
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(findings);
10935
+ output = generateGitHubWorkflow();
10716
10936
  filename = "qa-check.yml";
10717
10937
  break;
10718
10938
  case "gitlab":
10719
- output = generateGitLabCi(findings);
10939
+ output = generateGitLabCi();
10720
10940
  filename = ".gitlab-ci.yml";
10721
10941
  break;
10722
10942
  case "jenkins":
10723
- output = generateJenkinsfile(findings);
10943
+ output = generateJenkinsfile();
10724
10944
  filename = "Jenkinsfile";
10725
10945
  break;
10726
10946
  default: return 10;
10727
10947
  }
10728
- writeFileSync(filename, output);
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-C0ka-RmX.mjs");
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
- * Mjölnir CLI entry point (W1-02).
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
- * Tool version for `mjolnir --version`.
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
- const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
11108
- function parseArgs(argv, onError) {
11109
- const args = {
11110
- target: ".",
11111
- json: false,
11112
- verbose: false,
11113
- maxDurationMs: Number.POSITIVE_INFINITY,
11114
- scopeChanged: false,
11115
- format: "terminal"
11116
- };
11117
- const reject = (detail) => {
11118
- onError?.(detail);
11119
- return null;
11120
- };
11121
- for (let i = 0; i < argv.length; i++) {
11122
- const a = argv[i] ?? "";
11123
- if (a === "--json") {
11124
- args.json = true;
11125
- args.format = "json";
11126
- } else if (a === "--format") {
11127
- const fmt = argv[++i];
11128
- if (fmt === void 0) return reject({
11129
- flag: "--format",
11130
- token: "(missing)"
11131
- });
11132
- if (fmt === "sarif") args.format = "sarif";
11133
- else if (fmt === "mermaid") args.format = "mermaid";
11134
- else if (fmt === "codequality") args.format = "codequality";
11135
- else if (fmt === "json") {
11136
- args.format = "json";
11137
- args.json = true;
11138
- } else if (fmt !== "terminal") return reject({
11139
- flag: "--format",
11140
- token: fmt
11141
- });
11142
- } else if (a === "--verbose") args.verbose = true;
11143
- else if (a === "--scope") {
11144
- const mode = argv[++i];
11145
- if (mode === "changed") args.scopeChanged = true;
11146
- else return reject({
11147
- flag: "--scope",
11148
- token: mode
11149
- });
11150
- } else if (a === "--base") {
11151
- const ref = argv[++i];
11152
- if (!ref || ref.startsWith("-")) return reject({
11153
- flag: "--base",
11154
- token: ref
11155
- });
11156
- args.base = ref;
11157
- } else if (a === "--max-duration") {
11158
- const v = Number(argv[++i]);
11159
- if (!Number.isFinite(v) || v <= 0) return reject({
11160
- flag: "--max-duration",
11161
- token: argv[i]
11162
- });
11163
- args.maxDurationMs = v * 1e3;
11164
- } else if (a === "--width") {
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 result = ciInstall(resolve("."), gateArg ?? "error", {
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 ? "Plain-npx template (—no-action). Default mode: blocking — findings at the gate fail the job." : "Action-based template: uses Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b0 (immutable pin).");
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 = /* @__PURE__ */ new Set([
11386
- "scan",
11387
- "ci",
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 };