mjolnir-qa 0.5.35 → 0.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ Rule behavior changes (new rules, FP-rate changes against the corpus,
9
9
  severity changes) are first-class entries here — rule IDs are immutable
10
10
  once shipped, so this file is the record of what changed between versions.
11
11
 
12
+ ## [0.5.36] — 2026-09-08
13
+
14
+ ### Changes since 0
15
+
16
+ - P4: forensics breadth — Jest + Vitest JSON ingestion (honest degradation) (#66)
17
+
12
18
  ## [0.5.35] — 2026-09-08
13
19
 
14
20
  ### Changes since 0
package/README.md CHANGED
@@ -499,7 +499,8 @@ scanning.
499
499
  ## Runtime forensics
500
500
 
501
501
  Static analysis reasons about code that was never run. Forensics reads what
502
- actually happened — Playwright JSON reports and JUnit XML from any runner:
502
+ actually happened — Playwright JSON, Jest JSON, Vitest JSON, and JUnit XML
503
+ from any runner:
503
504
 
504
505
  ```text
505
506
  Static analysis → what the code appears to do
package/dist/cli.d.mts CHANGED
@@ -72,8 +72,15 @@ type TrustLevel = (typeof TRUST_ORDER)[number];
72
72
  interface RuntimeCorroboration {
73
73
  /** Granularity of what the runtime report could vouch for. */
74
74
  level: "file" | "test" | "defect";
75
- /** Report format the evidence came from. */
76
- source: "playwright-json" | "junit-xml";
75
+ /**
76
+ * Report format the evidence came from. Widened additively in P4
77
+ * (plan 1788853205786) to mirror ForensicsReport's source union:
78
+ * Jest/Vitest JSON reports corroborate at file/test level exactly
79
+ * like Playwright's — their one-attempt-per-record shape only
80
+ * constrains TRUE-FLAKE derivation, which lives in the analysis, not
81
+ * in the provenance label.
82
+ */
83
+ source: "playwright-json" | "junit-xml" | "jest-json" | "vitest-json";
77
84
  /** Number of tests executed in the finding's file (any level). */
78
85
  testsExecuted: number;
79
86
  /**
@@ -737,7 +744,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
737
744
  * `scripts/sync-sarif-version.cjs` on release and guarded by
738
745
  * `tests/version-consistency.spec.ts` locally.
739
746
  */
740
- declare const CLI_VERSION = "0.5.35";
747
+ declare const CLI_VERSION = "0.5.36";
741
748
  /** A usage-error detail: the offending token, when one exists. */
742
749
  interface UsageErrorDetail {
743
750
  /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
package/dist/cli.mjs CHANGED
@@ -11123,6 +11123,60 @@ function renderFlakyMd(report) {
11123
11123
  return lines.join("\n");
11124
11124
  }
11125
11125
  //#endregion
11126
+ //#region src/forensics/parse-jest-json.ts
11127
+ function toStatus$2(raw) {
11128
+ switch (raw) {
11129
+ case "passed": return "passed";
11130
+ case "failed": return "failed";
11131
+ case "skipped":
11132
+ case "pending":
11133
+ case "todo":
11134
+ case "disabled": return "skipped";
11135
+ default: return "interrupted";
11136
+ }
11137
+ }
11138
+ /**
11139
+ * Sniff: does this parsed JSON have the Jest shape? Exported for the
11140
+ * discovery dispatcher (run.ts) — Jest and Vitest share the top-level
11141
+ * `testResults` name; the discriminator is the per-file array's member
11142
+ * name (`testResults` = Jest, `assertionResults` = Vitest).
11143
+ */
11144
+ function looksLikeJestJson(json) {
11145
+ if (!json || typeof json !== "object") return false;
11146
+ const testResults = json.testResults;
11147
+ if (!Array.isArray(testResults) || testResults.length === 0) return false;
11148
+ return testResults.some((r) => r && typeof r === "object" && Array.isArray(r.testResults));
11149
+ }
11150
+ function parseJestJson(json) {
11151
+ if (!json || typeof json !== "object") return [];
11152
+ const root = json;
11153
+ const files = Array.isArray(root.testResults) ? root.testResults : [];
11154
+ const out = [];
11155
+ for (const fileResult of files.slice(0, 1e4)) {
11156
+ if (!fileResult || typeof fileResult !== "object") continue;
11157
+ const assertions = Array.isArray(fileResult.testResults) ? fileResult.testResults : [];
11158
+ const file = fileResult.testFilePath ?? "unknown";
11159
+ for (const assertion of assertions) {
11160
+ if (!assertion || typeof assertion !== "object") continue;
11161
+ const durationMs = Number.isFinite(assertion.duration) && assertion.duration >= 0 ? Math.round(assertion.duration) : 0;
11162
+ const attempt = {
11163
+ index: 1,
11164
+ status: toStatus$2(assertion.status),
11165
+ durationMs
11166
+ };
11167
+ const line = typeof assertion.location?.line === "number" && assertion.location.line > 0 ? assertion.location.line : void 0;
11168
+ const title = assertion.title ?? "(unnamed)";
11169
+ out.push({
11170
+ file,
11171
+ title,
11172
+ attempts: [attempt],
11173
+ ...line !== void 0 ? { line } : {}
11174
+ });
11175
+ }
11176
+ }
11177
+ return out;
11178
+ }
11179
+ //#endregion
11126
11180
  //#region src/forensics/parse-junit.ts
11127
11181
  const MAX_INPUT = 20971520;
11128
11182
  function decodeEntities(s) {
@@ -11178,7 +11232,7 @@ function parseJunitXml(xml) {
11178
11232
  //#endregion
11179
11233
  //#region src/forensics/parse-playwright-json.ts
11180
11234
  const MAX_DEPTH = 32;
11181
- function toStatus(raw) {
11235
+ function toStatus$1(raw) {
11182
11236
  switch (raw) {
11183
11237
  case "passed": return "passed";
11184
11238
  case "failed": return "failed";
@@ -11211,7 +11265,7 @@ function parsePlaywrightJson(json) {
11211
11265
  if (!r || typeof r !== "object") continue;
11212
11266
  attempts.push({
11213
11267
  index: ++i,
11214
- status: toStatus(r.status),
11268
+ status: toStatus$1(r.status),
11215
11269
  durationMs: Number.isFinite(r.duration) ? Math.max(0, r.duration) : 0
11216
11270
  });
11217
11271
  }
@@ -11230,6 +11284,54 @@ function parsePlaywrightJson(json) {
11230
11284
  return out;
11231
11285
  }
11232
11286
  //#endregion
11287
+ //#region src/forensics/parse-vitest-json.ts
11288
+ function toStatus(raw) {
11289
+ switch (raw) {
11290
+ case "passed": return "passed";
11291
+ case "failed": return "failed";
11292
+ case "skipped":
11293
+ case "pending":
11294
+ case "todo":
11295
+ case "disabled": return "skipped";
11296
+ default: return "interrupted";
11297
+ }
11298
+ }
11299
+ /** Sniff: the Vitest variant of the Jest-shaped report (see module doc). */
11300
+ function looksLikeVitestJson(json) {
11301
+ if (!json || typeof json !== "object") return false;
11302
+ const testResults = json.testResults;
11303
+ if (!Array.isArray(testResults) || testResults.length === 0) return false;
11304
+ return testResults.some((r) => r && typeof r === "object" && Array.isArray(r.assertionResults));
11305
+ }
11306
+ function parseVitestJson(json) {
11307
+ if (!json || typeof json !== "object") return [];
11308
+ const root = json;
11309
+ const files = Array.isArray(root.testResults) ? root.testResults : [];
11310
+ const out = [];
11311
+ for (const fileResult of files.slice(0, 1e4)) {
11312
+ if (!fileResult || typeof fileResult !== "object") continue;
11313
+ const assertions = Array.isArray(fileResult.assertionResults) ? fileResult.assertionResults : [];
11314
+ const file = fileResult.filepath ?? "unknown";
11315
+ for (const assertion of assertions) {
11316
+ if (!assertion || typeof assertion !== "object") continue;
11317
+ const durationMs = Number.isFinite(assertion.duration) && assertion.duration >= 0 ? Math.round(assertion.duration) : 0;
11318
+ const attempt = {
11319
+ index: 1,
11320
+ status: toStatus(assertion.status),
11321
+ durationMs
11322
+ };
11323
+ const line = typeof assertion.location?.line === "number" && assertion.location.line > 0 ? assertion.location.line : void 0;
11324
+ out.push({
11325
+ file,
11326
+ title: assertion.title ?? "(unnamed)",
11327
+ attempts: [attempt],
11328
+ ...line !== void 0 ? { line } : {}
11329
+ });
11330
+ }
11331
+ }
11332
+ return out;
11333
+ }
11334
+ //#endregion
11233
11335
  //#region src/forensics/run.ts
11234
11336
  /**
11235
11337
  * `mjolnir forensics <dir-or-file>` — runtime evidence entry point (R4).
@@ -11237,7 +11339,9 @@ function parsePlaywrightJson(json) {
11237
11339
  * Accepts either a single report file or a directory. In a directory it
11238
11340
  * looks for, in priority order:
11239
11341
  * 1. report.json / playwright-report.json — Playwright JSON report
11240
- * 2. *.xml — JUnit XML reports
11342
+ * 2. jest-report.json / vitest-report.json — Jest/Vitest JSON reports
11343
+ * 3. *.json (shape-sniffed: Playwright, Jest, Vitest)
11344
+ * 4. *.xml — JUnit XML reports
11241
11345
  *
11242
11346
  * Writes FLAKY.md next to the scan target unless --no-flaky-md.
11243
11347
  */
@@ -11308,17 +11412,27 @@ function parseFile(path, text) {
11308
11412
  records: parseJunitXml(text),
11309
11413
  source: "junit-xml"
11310
11414
  };
11415
+ let json;
11311
11416
  try {
11312
- return {
11313
- records: parsePlaywrightJson(JSON.parse(text)),
11314
- source: "playwright-json"
11315
- };
11417
+ json = JSON.parse(text);
11316
11418
  } catch {
11317
11419
  return {
11318
11420
  records: [],
11319
11421
  source: "playwright-json"
11320
11422
  };
11321
11423
  }
11424
+ if (looksLikeJestJson(json)) return {
11425
+ records: parseJestJson(json),
11426
+ source: "jest-json"
11427
+ };
11428
+ if (looksLikeVitestJson(json)) return {
11429
+ records: parseVitestJson(json),
11430
+ source: "vitest-json"
11431
+ };
11432
+ return {
11433
+ records: parsePlaywrightJson(json),
11434
+ source: "playwright-json"
11435
+ };
11322
11436
  }
11323
11437
  function listFiles(dir) {
11324
11438
  const out = [];
@@ -13350,7 +13464,7 @@ function renderSarif(result, repoRootUri) {
13350
13464
  tool: { driver: {
13351
13465
  name: "Mjölnir",
13352
13466
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13353
- version: "0.5.35",
13467
+ version: "0.5.36",
13354
13468
  rules: [...rules.values()].map((r) => {
13355
13469
  const meta = RULES.find((x) => x.id === r.id);
13356
13470
  return {
@@ -18908,7 +19022,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18908
19022
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18909
19023
  * `tests/version-consistency.spec.ts` locally.
18910
19024
  */
18911
- const CLI_VERSION = "0.5.35";
19025
+ const CLI_VERSION = "0.5.36";
18912
19026
  function parseArgs(argv, onError) {
18913
19027
  const args = {
18914
19028
  target: ".",
@@ -11122,6 +11122,60 @@ function renderFlakyMd(report) {
11122
11122
  return lines.join("\n");
11123
11123
  }
11124
11124
  //#endregion
11125
+ //#region src/forensics/parse-jest-json.ts
11126
+ function toStatus$2(raw) {
11127
+ switch (raw) {
11128
+ case "passed": return "passed";
11129
+ case "failed": return "failed";
11130
+ case "skipped":
11131
+ case "pending":
11132
+ case "todo":
11133
+ case "disabled": return "skipped";
11134
+ default: return "interrupted";
11135
+ }
11136
+ }
11137
+ /**
11138
+ * Sniff: does this parsed JSON have the Jest shape? Exported for the
11139
+ * discovery dispatcher (run.ts) — Jest and Vitest share the top-level
11140
+ * `testResults` name; the discriminator is the per-file array's member
11141
+ * name (`testResults` = Jest, `assertionResults` = Vitest).
11142
+ */
11143
+ function looksLikeJestJson(json) {
11144
+ if (!json || typeof json !== "object") return false;
11145
+ const testResults = json.testResults;
11146
+ if (!Array.isArray(testResults) || testResults.length === 0) return false;
11147
+ return testResults.some((r) => r && typeof r === "object" && Array.isArray(r.testResults));
11148
+ }
11149
+ function parseJestJson(json) {
11150
+ if (!json || typeof json !== "object") return [];
11151
+ const root = json;
11152
+ const files = Array.isArray(root.testResults) ? root.testResults : [];
11153
+ const out = [];
11154
+ for (const fileResult of files.slice(0, 1e4)) {
11155
+ if (!fileResult || typeof fileResult !== "object") continue;
11156
+ const assertions = Array.isArray(fileResult.testResults) ? fileResult.testResults : [];
11157
+ const file = fileResult.testFilePath ?? "unknown";
11158
+ for (const assertion of assertions) {
11159
+ if (!assertion || typeof assertion !== "object") continue;
11160
+ const durationMs = Number.isFinite(assertion.duration) && assertion.duration >= 0 ? Math.round(assertion.duration) : 0;
11161
+ const attempt = {
11162
+ index: 1,
11163
+ status: toStatus$2(assertion.status),
11164
+ durationMs
11165
+ };
11166
+ const line = typeof assertion.location?.line === "number" && assertion.location.line > 0 ? assertion.location.line : void 0;
11167
+ const title = assertion.title ?? "(unnamed)";
11168
+ out.push({
11169
+ file,
11170
+ title,
11171
+ attempts: [attempt],
11172
+ ...line !== void 0 ? { line } : {}
11173
+ });
11174
+ }
11175
+ }
11176
+ return out;
11177
+ }
11178
+ //#endregion
11125
11179
  //#region src/forensics/parse-junit.ts
11126
11180
  const MAX_INPUT = 20971520;
11127
11181
  function decodeEntities(s) {
@@ -11177,7 +11231,7 @@ function parseJunitXml(xml) {
11177
11231
  //#endregion
11178
11232
  //#region src/forensics/parse-playwright-json.ts
11179
11233
  const MAX_DEPTH = 32;
11180
- function toStatus(raw) {
11234
+ function toStatus$1(raw) {
11181
11235
  switch (raw) {
11182
11236
  case "passed": return "passed";
11183
11237
  case "failed": return "failed";
@@ -11210,7 +11264,7 @@ function parsePlaywrightJson(json) {
11210
11264
  if (!r || typeof r !== "object") continue;
11211
11265
  attempts.push({
11212
11266
  index: ++i,
11213
- status: toStatus(r.status),
11267
+ status: toStatus$1(r.status),
11214
11268
  durationMs: Number.isFinite(r.duration) ? Math.max(0, r.duration) : 0
11215
11269
  });
11216
11270
  }
@@ -11229,6 +11283,54 @@ function parsePlaywrightJson(json) {
11229
11283
  return out;
11230
11284
  }
11231
11285
  //#endregion
11286
+ //#region src/forensics/parse-vitest-json.ts
11287
+ function toStatus(raw) {
11288
+ switch (raw) {
11289
+ case "passed": return "passed";
11290
+ case "failed": return "failed";
11291
+ case "skipped":
11292
+ case "pending":
11293
+ case "todo":
11294
+ case "disabled": return "skipped";
11295
+ default: return "interrupted";
11296
+ }
11297
+ }
11298
+ /** Sniff: the Vitest variant of the Jest-shaped report (see module doc). */
11299
+ function looksLikeVitestJson(json) {
11300
+ if (!json || typeof json !== "object") return false;
11301
+ const testResults = json.testResults;
11302
+ if (!Array.isArray(testResults) || testResults.length === 0) return false;
11303
+ return testResults.some((r) => r && typeof r === "object" && Array.isArray(r.assertionResults));
11304
+ }
11305
+ function parseVitestJson(json) {
11306
+ if (!json || typeof json !== "object") return [];
11307
+ const root = json;
11308
+ const files = Array.isArray(root.testResults) ? root.testResults : [];
11309
+ const out = [];
11310
+ for (const fileResult of files.slice(0, 1e4)) {
11311
+ if (!fileResult || typeof fileResult !== "object") continue;
11312
+ const assertions = Array.isArray(fileResult.assertionResults) ? fileResult.assertionResults : [];
11313
+ const file = fileResult.filepath ?? "unknown";
11314
+ for (const assertion of assertions) {
11315
+ if (!assertion || typeof assertion !== "object") continue;
11316
+ const durationMs = Number.isFinite(assertion.duration) && assertion.duration >= 0 ? Math.round(assertion.duration) : 0;
11317
+ const attempt = {
11318
+ index: 1,
11319
+ status: toStatus(assertion.status),
11320
+ durationMs
11321
+ };
11322
+ const line = typeof assertion.location?.line === "number" && assertion.location.line > 0 ? assertion.location.line : void 0;
11323
+ out.push({
11324
+ file,
11325
+ title: assertion.title ?? "(unnamed)",
11326
+ attempts: [attempt],
11327
+ ...line !== void 0 ? { line } : {}
11328
+ });
11329
+ }
11330
+ }
11331
+ return out;
11332
+ }
11333
+ //#endregion
11232
11334
  //#region src/forensics/run.ts
11233
11335
  /**
11234
11336
  * `mjolnir forensics <dir-or-file>` — runtime evidence entry point (R4).
@@ -11236,7 +11338,9 @@ function parsePlaywrightJson(json) {
11236
11338
  * Accepts either a single report file or a directory. In a directory it
11237
11339
  * looks for, in priority order:
11238
11340
  * 1. report.json / playwright-report.json — Playwright JSON report
11239
- * 2. *.xml — JUnit XML reports
11341
+ * 2. jest-report.json / vitest-report.json — Jest/Vitest JSON reports
11342
+ * 3. *.json (shape-sniffed: Playwright, Jest, Vitest)
11343
+ * 4. *.xml — JUnit XML reports
11240
11344
  *
11241
11345
  * Writes FLAKY.md next to the scan target unless --no-flaky-md.
11242
11346
  */
@@ -11307,17 +11411,27 @@ function parseFile(path, text) {
11307
11411
  records: parseJunitXml(text),
11308
11412
  source: "junit-xml"
11309
11413
  };
11414
+ let json;
11310
11415
  try {
11311
- return {
11312
- records: parsePlaywrightJson(JSON.parse(text)),
11313
- source: "playwright-json"
11314
- };
11416
+ json = JSON.parse(text);
11315
11417
  } catch {
11316
11418
  return {
11317
11419
  records: [],
11318
11420
  source: "playwright-json"
11319
11421
  };
11320
11422
  }
11423
+ if (looksLikeJestJson(json)) return {
11424
+ records: parseJestJson(json),
11425
+ source: "jest-json"
11426
+ };
11427
+ if (looksLikeVitestJson(json)) return {
11428
+ records: parseVitestJson(json),
11429
+ source: "vitest-json"
11430
+ };
11431
+ return {
11432
+ records: parsePlaywrightJson(json),
11433
+ source: "playwright-json"
11434
+ };
11321
11435
  }
11322
11436
  function listFiles(dir) {
11323
11437
  const out = [];
@@ -13349,7 +13463,7 @@ function renderSarif(result, repoRootUri) {
13349
13463
  tool: { driver: {
13350
13464
  name: "Mjölnir",
13351
13465
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13352
- version: "0.5.35",
13466
+ version: "0.5.36",
13353
13467
  rules: [...rules.values()].map((r) => {
13354
13468
  const meta = RULES.find((x) => x.id === r.id);
13355
13469
  return {
@@ -18544,7 +18658,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18544
18658
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18545
18659
  * `tests/version-consistency.spec.ts` locally.
18546
18660
  */
18547
- const CLI_VERSION = "0.5.35";
18661
+ const CLI_VERSION = "0.5.36";
18548
18662
  function parseArgs(argv, onError) {
18549
18663
  const args = {
18550
18664
  target: ".",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mjolnir-qa",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
4
4
  "description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
5
5
  "type": "module",
6
6
  "engines": {