mjolnir-qa 6.0.0 → 6.0.1

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,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
3
2
  import { createRequire } from "node:module";
4
- import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
3
+ import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
4
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
5
  import process$1 from "node:process";
7
6
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -17,7 +16,18 @@ import { createInterface } from "node:readline";
17
16
  import { Buffer as Buffer$1 } from "node:buffer";
18
17
  import { isDeepStrictEqual } from "node:util";
19
18
  import { tmpdir } from "node:os";
20
- //#region src/types.ts
19
+ // \0rolldown/runtime.js
20
+ var __defProp = Object.defineProperty;
21
+ var __exportAll = (all, no_symbols) => {
22
+ let target = {};
23
+ for (var name in all) __defProp(target, name, {
24
+ get: all[name],
25
+ enumerable: true
26
+ });
27
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
28
+ return target;
29
+ };
30
+ // src/types.ts
21
31
  /** Severity ladder. Order matters for sorting and gating. */
22
32
  const SEVERITY_ORDER$1 = [
23
33
  "error",
@@ -146,8 +156,7 @@ const DEDUCTIONS = {
146
156
  warning: 3,
147
157
  info: 1
148
158
  };
149
- //#endregion
150
- //#region src/claim-evidence.ts
159
+ // src/claim-evidence.ts
151
160
  /**
152
161
  * The one place a rendered surface decides whether it may claim "clean".
153
162
  *
@@ -204,8 +213,7 @@ function scanExitCode(input) {
204
213
  if (input.gate === "advisory") return 0;
205
214
  return input.findings.some((f) => isAtGate(f, input.gate, input.isAdvisory)) ? 1 : 0;
206
215
  }
207
- //#endregion
208
- //#region src/engine/completion.ts
216
+ // src/engine/completion.ts
209
217
  function deriveCompletion(input) {
210
218
  const truncationReasons = [...new Set(input.truncationReasons)].sort();
211
219
  const degradations = [...input.degradations ?? []].sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
@@ -247,8 +255,7 @@ function deriveCompletion(input) {
247
255
  }
248
256
  };
249
257
  }
250
- //#endregion
251
- //#region src/engine/degradation-ledger.ts
258
+ // src/engine/degradation-ledger.ts
252
259
  /**
253
260
  * Append-only. Records are never edited and never removed by a recording
254
261
  * site; a reader takes a window, and the window is defined by index, so
@@ -284,8 +291,7 @@ function summarizeDegradations(window) {
284
291
  count
285
292
  })).sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
286
293
  }
287
- //#endregion
288
- //#region src/engine/runtime-corroboration.ts
294
+ // src/engine/runtime-corroboration.ts
289
295
  /**
290
296
  * Stamp runtime corroboration + trust levels onto findings (mutates in
291
297
  * place, the same contract as stampEvidenceLevels). Returns the number
@@ -367,8 +373,7 @@ function deriveTrustLevel(finding, corroboration) {
367
373
  if (corroboration.level === "test") return "L4";
368
374
  return "L3";
369
375
  }
370
- //#endregion
371
- //#region src/rules/measured-fp.generated.ts
376
+ // src/rules/measured-fp.generated.ts
372
377
  const MEASURED_FP = {
373
378
  "QA-CI-001": {
374
379
  fpRate: .105,
@@ -1429,8 +1434,7 @@ const MEASURED_FP_RAW = {
1429
1434
  ciHigh: .4343
1430
1435
  }
1431
1436
  };
1432
- //#endregion
1433
- //#region src/engine/trust-summary.ts
1437
+ // src/engine/trust-summary.ts
1434
1438
  /**
1435
1439
  * Trust level of the summary — the BEST trust level any finding in the
1436
1440
  * scan reached. Static-only findings settle at L2 (deriveTrustLevel's
@@ -1554,8 +1558,7 @@ function buildTrustSummary(result, declarationsByFile) {
1554
1558
  ceilingReasons: reasons
1555
1559
  };
1556
1560
  }
1557
- //#endregion
1558
- //#region src/governance/run-mode.ts
1561
+ // src/governance/run-mode.ts
1559
1562
  /**
1560
1563
  * Which mode a run is in, and how the other one is reached.
1561
1564
  *
@@ -1606,8 +1609,7 @@ function resolveRunMode(input) {
1606
1609
  if (declared === "local-only" || declared === "hosted") return declared;
1607
1610
  throw new Error(`${HOSTED_MODE_FLAG}=${String(declared)} is not a run mode. Expected "hosted" or "local-only". Falling back to a default here would let a mistyped flag look like a deliberate choice.`);
1608
1611
  }
1609
- //#endregion
1610
- //#region src/engine/run-identity.ts
1612
+ // src/engine/run-identity.ts
1611
1613
  /**
1612
1614
  * Run Identity (product-gap master plan §7, plan 1789009691197 R4c) —
1613
1615
  * the deterministic binding that anchors every trust claim to its
@@ -1736,8 +1738,7 @@ function buildEvidenceGraph(parts) {
1736
1738
  } } : {}
1737
1739
  };
1738
1740
  }
1739
- //#endregion
1740
- //#region src/engine/file-executor.ts
1741
+ // src/engine/file-executor.ts
1741
1742
  function cacheHitOutcome(job) {
1742
1743
  return {
1743
1744
  path: job.path,
@@ -1828,8 +1829,7 @@ function createExecutor(name, concurrency, runOne) {
1828
1829
  execute: runOne
1829
1830
  };
1830
1831
  }
1831
- //#endregion
1832
- //#region src/scope/git-resolve.ts
1832
+ // src/scope/git-resolve.ts
1833
1833
  /**
1834
1834
  * Absolute-path git resolution (audit S1).
1835
1835
  *
@@ -1904,8 +1904,7 @@ function runGit(root, args) {
1904
1904
  return null;
1905
1905
  }
1906
1906
  }
1907
- //#endregion
1908
- //#region src/lib/safe-json.ts
1907
+ // src/lib/safe-json.ts
1909
1908
  /**
1910
1909
  * Reusable safe-JSON parse-and-validate helper.
1911
1910
  *
@@ -1942,8 +1941,7 @@ function isRecord(v) {
1942
1941
  return false;
1943
1942
  }
1944
1943
  }
1945
- //#endregion
1946
- //#region src/engine/candidate-binding.ts
1944
+ // src/engine/candidate-binding.ts
1947
1945
  /**
1948
1946
  * Candidate binding (plan V5-010).
1949
1947
  *
@@ -2076,18 +2074,17 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
2076
2074
  return null;
2077
2075
  }
2078
2076
  }
2079
- //#endregion
2080
- //#region src/engine/version.ts
2077
+ // src/engine/version.ts
2081
2078
  /**
2082
2079
  * Engine version, as a leaf module: `runIdentity` (R4c) needs the engine
2083
2080
  * version inside scan-pipeline WITHOUT importing cli.ts (a cycle — cli
2084
2081
  * imports the pipeline). The literal follows the same discipline as
2085
2082
  * CLI_VERSION and SARIF's driver.version: kept in sync on release by
2086
- * scripts/archive/sync-sarif-version.cjs (archived; superseded by the release
2087
- * workflow's own version stamping) and guarded by the version-consistency
2083
+ * `npm run version:surface:sync` (this file is an identity surface) and
2084
+ * guarded by the version-consistency
2088
2085
  * spec. cli.ts re-exports this as CLI_VERSION.
2089
2086
  */
2090
- const ENGINE_VERSION = "6.0.0";
2087
+ const ENGINE_VERSION = "6.0.1";
2091
2088
  /**
2092
2089
  * Build identity — WHICH BUILD IS RUNNING, as distinct from WHICH RELEASE IS
2093
2090
  * PUBLISHED.
@@ -2148,16 +2145,7 @@ function resolveBuildId() {
2148
2145
  };
2149
2146
  return buildIdFrom(runGit);
2150
2147
  }
2151
- //#endregion
2152
- //#region src/engine/contract-versions.ts
2153
- /**
2154
- * Versioned contract registry (VERSION-001).
2155
- *
2156
- * Single source of truth for every version constant the engine exposes
2157
- * to consumers (run identity, evidence graph, machine contract).
2158
- * Constants are `as const` — literal types flow into the identity
2159
- * surface without widening.
2160
- */
2148
+ // src/engine/contract-versions.ts
2161
2149
  const TRUST_MODEL_VERSION = "1.0.0";
2162
2150
  /**
2163
2151
  * 2.0.0 (ADR 0014): the core tier carries an evidence FLOOR. A core rule's
@@ -2169,8 +2157,7 @@ const TRUST_MODEL_VERSION = "1.0.0";
2169
2157
  */
2170
2158
  const SCORING_MODEL_VERSION = "2.0.0";
2171
2159
  const FRAMEWORK_SUPPORT_MATRIX_VERSION = "1.0.0";
2172
- //#endregion
2173
- //#region src/discovery/evidence-discovery.ts
2160
+ // src/discovery/evidence-discovery.ts
2174
2161
  /**
2175
2162
  * Zero-config evidence discovery (Mega MVP Master Plan v3.1 §26 WI-11).
2176
2163
  *
@@ -2262,8 +2249,7 @@ function discoverEvidenceCandidates(scanRoot) {
2262
2249
  }
2263
2250
  return out.sort((a, b) => a.depth - b.depth || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
2264
2251
  }
2265
- //#endregion
2266
- //#region src/discovery/workspace.ts
2252
+ // src/discovery/workspace.ts
2267
2253
  /**
2268
2254
  * Repository discovery (Sprint-Plan W1-03).
2269
2255
  * Finds project root, parses package.json, detects npm/yarn/pnpm workspaces.
@@ -2310,8 +2296,7 @@ function basename$1(p) {
2310
2296
  const parts = p.split(/[\\/]/);
2311
2297
  return parts[parts.length - 1];
2312
2298
  }
2313
- //#endregion
2314
- //#region src/lib/fs-bounded.ts
2299
+ // src/lib/fs-bounded.ts
2315
2300
  function closeQuiet(fd) {
2316
2301
  try {
2317
2302
  closeSync(fd);
@@ -2363,8 +2348,7 @@ function readFileBounded(path, maxBytes) {
2363
2348
  if (fd !== void 0) closeQuiet(fd);
2364
2349
  }
2365
2350
  }
2366
- //#endregion
2367
- //#region src/config/config-schema.ts
2351
+ // src/config/config-schema.ts
2368
2352
  const VALID_GATES = /* @__PURE__ */ new Set([
2369
2353
  "advisory",
2370
2354
  "error",
@@ -2434,8 +2418,7 @@ function validateConfigSchema(config) {
2434
2418
  errors
2435
2419
  };
2436
2420
  }
2437
- //#endregion
2438
- //#region src/config/config.ts
2421
+ // src/config/config.ts
2439
2422
  /**
2440
2423
  * Config file support (Sprint-Plan W7, Product-MVP §27 + GAP-F).
2441
2424
  * Zero-config preserved: config presence never changes detection
@@ -2569,8 +2552,7 @@ function applySeverityOverrides(findings, cfg) {
2569
2552
  if (override && SEVERITY_ORDER$1.includes(override)) f.severity = override;
2570
2553
  }
2571
2554
  }
2572
- //#endregion
2573
- //#region src/lib/compare.ts
2555
+ // src/lib/compare.ts
2574
2556
  /**
2575
2557
  * Order that does not depend on who is asking.
2576
2558
  *
@@ -2616,8 +2598,7 @@ function compareCodePoints(a, b) {
2616
2598
  function compareLocalized(locale) {
2617
2599
  return (a, b) => a.localeCompare(b, locale);
2618
2600
  }
2619
- //#endregion
2620
- //#region src/lib/glob.ts
2601
+ // src/lib/glob.ts
2621
2602
  /**
2622
2603
  * The shared segment compiler: a glob to a regex BODY, with no anchors.
2623
2604
  *
@@ -2671,8 +2652,7 @@ function anyDepthGlobRegExp(glob) {
2671
2652
  function pathMatchesGlob$1(path, glob) {
2672
2653
  return anchoredGlobRegExp(glob).test(path.replaceAll("\\", "/"));
2673
2654
  }
2674
- //#endregion
2675
- //#region src/discovery/ignores.ts
2655
+ // src/discovery/ignores.ts
2676
2656
  /**
2677
2657
  * Ignore resolution (Product-MVP.txt R1 — Tier-1 launch blocker).
2678
2658
  *
@@ -2829,8 +2809,7 @@ function isLintFixtureDir(absoluteDir) {
2829
2809
  return false;
2830
2810
  }
2831
2811
  }
2832
- //#endregion
2833
- //#region src/discovery/shared-walk.ts
2812
+ // src/discovery/shared-walk.ts
2834
2813
  /**
2835
2814
  * Shared directory walker (audit P-2).
2836
2815
  *
@@ -2893,8 +2872,7 @@ function sharedWalk(options) {
2893
2872
  };
2894
2873
  walk(options.root);
2895
2874
  }
2896
- //#endregion
2897
- //#region src/frameworks/framework-inventory.ts
2875
+ // src/frameworks/framework-inventory.ts
2898
2876
  const FRAMEWORK_INVENTORY = [
2899
2877
  {
2900
2878
  frameworkId: "playwright",
@@ -3095,8 +3073,7 @@ const FRAMEWORK_INVENTORY = [
3095
3073
  function getFrameworkById(frameworkId) {
3096
3074
  return FRAMEWORK_INVENTORY.find((f) => f.frameworkId === frameworkId);
3097
3075
  }
3098
- //#endregion
3099
- //#region src/discovery/frameworks.ts
3076
+ // src/discovery/frameworks.ts
3100
3077
  /**
3101
3078
  * Framework detection via config resolution (Sprint-Plan W3-01, R8).
3102
3079
  *
@@ -3179,8 +3156,7 @@ function detectFrameworks(ws) {
3179
3156
  unknown: false
3180
3157
  };
3181
3158
  }
3182
- //#endregion
3183
- //#region src/engine/ts-ast.ts
3159
+ // src/engine/ts-ast.ts
3184
3160
  /**
3185
3161
  * AST precision layer (Upgrade-Plan-v3 Phase 3).
3186
3162
  *
@@ -3379,8 +3355,7 @@ function getCodeOnlyText(file) {
3379
3355
  function resetTsMorphProject() {
3380
3356
  looseProject = null;
3381
3357
  }
3382
- //#endregion
3383
- //#region src/engine/code-text.ts
3358
+ // src/engine/code-text.ts
3384
3359
  /**
3385
3360
  * Code-only text masking (Phase 1 — Tempering Plan).
3386
3361
  *
@@ -3740,8 +3715,7 @@ function getCodeTextForCounting(text, language) {
3740
3715
  function blankRange(chars, start, end) {
3741
3716
  for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
3742
3717
  }
3743
- //#endregion
3744
- //#region src/engine/adapter.ts
3718
+ // src/engine/adapter.ts
3745
3719
  /**
3746
3720
  * The §15.1 framework filter, shared by every language adapter: a rule
3747
3721
  * that declares `frameworks` runs on a file only when the file carries
@@ -3755,8 +3729,7 @@ function frameworkFilterApplies(rule, file) {
3755
3729
  if (tags === void 0 || tags.length === 0) return true;
3756
3730
  return rule.frameworks.some((f) => tags.includes(f));
3757
3731
  }
3758
- //#endregion
3759
- //#region src/engine/shared-run-rules.ts
3732
+ // src/engine/shared-run-rules.ts
3760
3733
  /**
3761
3734
  * Run ONE rule with crash isolation, and return what it produced.
3762
3735
  *
@@ -3793,8 +3766,7 @@ function runRuleIsolated(rule, file, onCrash) {
3793
3766
  return null;
3794
3767
  }
3795
3768
  }
3796
- //#endregion
3797
- //#region src/adapters/typescript.ts
3769
+ // src/adapters/typescript.ts
3798
3770
  /**
3799
3771
  * TypeScript/JavaScript adapter (R1).
3800
3772
  * Wraps the proven regex-based engine behind the LanguageAdapter
@@ -4047,8 +4019,7 @@ function loadWorkspaceShim(root) {
4047
4019
  return null;
4048
4020
  }
4049
4021
  }
4050
- //#endregion
4051
- //#region src/engine/tree-sitter-ast.ts
4022
+ // src/engine/tree-sitter-ast.ts
4052
4023
  /**
4053
4024
  * Tree-sitter WASM AST layer for Java and C# — Sprint 8 Task 36
4054
4025
  * (Master-Stabilization-Plan.md).
@@ -4267,8 +4238,7 @@ async function releaseTreeSitterResources() {
4267
4238
  } catch {}
4268
4239
  }
4269
4240
  }
4270
- //#endregion
4271
- //#region src/adapters/python.ts
4241
+ // src/adapters/python.ts
4272
4242
  /**
4273
4243
  * Python/pytest adapter (Upgrade-Plan-v2 R2).
4274
4244
  *
@@ -4426,8 +4396,7 @@ const pythonAdapter = {
4426
4396
  function readText$2(p) {
4427
4397
  return readFileSync(p, "utf8");
4428
4398
  }
4429
- //#endregion
4430
- //#region src/adapters/java.ts
4399
+ // src/adapters/java.ts
4431
4400
  /**
4432
4401
  * Java/JUnit adapter (Upgrade-Plan-v3 Phase 4).
4433
4402
  *
@@ -4593,8 +4562,7 @@ function javaFileTags(file) {
4593
4562
  function readText$1(path) {
4594
4563
  return readFileSync(path, "utf8");
4595
4564
  }
4596
- //#endregion
4597
- //#region src/adapters/csharp.ts
4565
+ // src/adapters/csharp.ts
4598
4566
  /**
4599
4567
  * C#/.NET adapter (Upgrade-Plan-v3 Phase 5).
4600
4568
  *
@@ -4750,8 +4718,7 @@ function csharpFileTags(file) {
4750
4718
  function readText(path) {
4751
4719
  return readFileSync(path, "utf8");
4752
4720
  }
4753
- //#endregion
4754
- //#region src/discovery/yaml-guards.ts
4721
+ // src/discovery/yaml-guards.ts
4755
4722
  /**
4756
4723
  * Shared hostile-YAML guards (product-gap master plan P3 common law —
4757
4724
  * `azure-pipelines.yml` detection rides the same YAML machinery as the
@@ -4820,8 +4787,7 @@ function enforceDepthCap(value, max, depthMessage = GENERIC_LABELS.depthMessage)
4820
4787
  });
4821
4788
  }
4822
4789
  }
4823
- //#endregion
4824
- //#region src/discovery/workflow-parser.ts
4790
+ // src/discovery/workflow-parser.ts
4825
4791
  /**
4826
4792
  * Safe GitHub Actions workflow parser (Phase 0.3, W4-03 debt).
4827
4793
  *
@@ -4867,8 +4833,7 @@ function parseWorkflow(text) {
4867
4833
  }
4868
4834
  return { jobs };
4869
4835
  }
4870
- //#endregion
4871
- //#region src/adapters/github-actions.ts
4836
+ // src/adapters/github-actions.ts
4872
4837
  /**
4873
4838
  * GitHub Actions adapter (R1).
4874
4839
  * Owns workflow discovery and safe-YAML parsing; CI rules consume the
@@ -4953,8 +4918,7 @@ const githubActionsAdapter = {
4953
4918
  };
4954
4919
  var WorkflowParseSkipped = class extends Error {};
4955
4920
  const LIMITS_MAX_BYTES$2 = 1048576;
4956
- //#endregion
4957
- //#region src/discovery/azure-pipeline-parser.ts
4921
+ // src/discovery/azure-pipeline-parser.ts
4958
4922
  /**
4959
4923
  * Safe Azure DevOps pipeline parser (product-gap master plan P3b).
4960
4924
  *
@@ -5188,8 +5152,7 @@ function isAzurePipelineFixture(normalizedPath) {
5188
5152
  const base = normalizedPath.replaceAll("\\", "/").split("/").pop() ?? "";
5189
5153
  return AZURE_PIPELINE_FILENAMES_SET.has(base);
5190
5154
  }
5191
- //#endregion
5192
- //#region src/adapters/azure-pipelines.ts
5155
+ // src/adapters/azure-pipelines.ts
5193
5156
  /**
5194
5157
  * Azure DevOps pipeline adapter (product-gap master plan P3b).
5195
5158
  * Owns `azure-pipelines.yml` discovery (repo root, the platform's
@@ -5269,8 +5232,7 @@ const azurePipelinesAdapter = {
5269
5232
  };
5270
5233
  var AzureParseSkipped = class extends Error {};
5271
5234
  const LIMITS_MAX_BYTES$1 = 1048576;
5272
- //#endregion
5273
- //#region src/adapters/jenkins.ts
5235
+ // src/adapters/jenkins.ts
5274
5236
  /**
5275
5237
  * Jenkins adapter (product-gap master plan P3c).
5276
5238
  * The root `Jenkinsfile` is a TEXT-target file kind: declarative and
@@ -5344,8 +5306,7 @@ const jenkinsAdapter = {
5344
5306
  }
5345
5307
  };
5346
5308
  const LIMITS_MAX_BYTES = 1048576;
5347
- //#endregion
5348
- //#region src/discovery/scan-adapters.ts
5309
+ // src/discovery/scan-adapters.ts
5349
5310
  /**
5350
5311
  * The canonical adapter registry (audits H-6/H-9).
5351
5312
  *
@@ -5519,8 +5480,7 @@ const SEARCHED_FOR = [
5519
5480
  globs: jenkinsAdapter.testFileGlobs
5520
5481
  }
5521
5482
  ];
5522
- //#endregion
5523
- //#region src/scope/changed.ts
5483
+ // src/scope/changed.ts
5524
5484
  /**
5525
5485
  * Changed-scope engine (Sprint-Plan W6, Product-MVP §9 `--scope changed`).
5526
5486
  *
@@ -5763,8 +5723,7 @@ function computeStagedFiles(root) {
5763
5723
  if (raw === null) return null;
5764
5724
  return raw.split("\0").filter((s) => s.length > 0);
5765
5725
  }
5766
- //#endregion
5767
- //#region src/rules/rule.ts
5726
+ // src/rules/rule.ts
5768
5727
  /**
5769
5728
  * Invoke an `astQuery` hook with the mandatory-fallback contract: no
5770
5729
  * hook or no AST on the context resolves to `undefined` (→ regex
@@ -5777,8 +5736,7 @@ function tryAstQuery(hook, ctx) {
5777
5736
  function defineRule(rule) {
5778
5737
  return rule;
5779
5738
  }
5780
- //#endregion
5781
- //#region src/rules/shared/positions.ts
5739
+ // src/rules/shared/positions.ts
5782
5740
  /**
5783
5741
  * Shared position helpers (Phase 6 — Tempering Plan).
5784
5742
  * One lineAt, one colAt, one matchBrace. The other 79 copies are dead.
@@ -5823,8 +5781,7 @@ function lineAt(text, index) {
5823
5781
  function colAt(text, index) {
5824
5782
  return index - (index <= 0 ? -1 : text.lastIndexOf("\n", index - 1));
5825
5783
  }
5826
- //#endregion
5827
- //#region src/rules/test/qa-test-001-focused-test.ts
5784
+ // src/rules/test/qa-test-001-focused-test.ts
5828
5785
  /**
5829
5786
  * QA-TEST-001 — Focused test committed (`fit`/`fdescribe`/`.only`).
5830
5787
  * Severity: error · Confidence: high · deterministic-defect
@@ -5890,8 +5847,7 @@ const focusedTestCommitted = defineRule({
5890
5847
  return findings;
5891
5848
  }
5892
5849
  });
5893
- //#endregion
5894
- //#region src/rules/test/qa-test-002-skipped-test.ts
5850
+ // src/rules/test/qa-test-002-skipped-test.ts
5895
5851
  /**
5896
5852
  * QA-TEST-002 — Skipped test (`it.skip`, `xit`, `test.skip`, `describe.skip`).
5897
5853
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5947,8 +5903,7 @@ const skippedTest = defineRule({
5947
5903
  return findings;
5948
5904
  }
5949
5905
  });
5950
- //#endregion
5951
- //#region src/rules/shared/masking.ts
5906
+ // src/rules/shared/masking.ts
5952
5907
  /** True when the offset was blanked in codeText (inside a string or comment). */
5953
5908
  function isMasked(ctx, index) {
5954
5909
  const mask = ctx.codeText;
@@ -6009,8 +5964,7 @@ function isInsideEmbeddedCode(ctx, index) {
6009
5964
  const hasCallSyntax = /\w\s*\(/.test(inner);
6010
5965
  return hasNestedQuote && hasCallSyntax;
6011
5966
  }
6012
- //#endregion
6013
- //#region src/rules/test/qa-test-003-no-assertions.ts
5967
+ // src/rules/test/qa-test-003-no-assertions.ts
6014
5968
  /**
6015
5969
  * QA-TEST-003 — Test with no assertions.
6016
5970
  * Severity: error · Confidence: high · deterministic-defect
@@ -6088,8 +6042,7 @@ function matchBrace$2(text, open) {
6088
6042
  }
6089
6043
  return -1;
6090
6044
  }
6091
- //#endregion
6092
- //#region src/rules/test/qa-test-004-hard-sleep.ts
6045
+ // src/rules/test/qa-test-004-hard-sleep.ts
6093
6046
  /**
6094
6047
  * QA-TEST-004 — Hard sleep (`sleep()`, `waitForTimeout()`, `setTimeout` as wait).
6095
6048
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6154,8 +6107,7 @@ const hardSleep = defineRule({
6154
6107
  return findings;
6155
6108
  }
6156
6109
  });
6157
- //#endregion
6158
- //#region src/rules/test/qa-test-006-retry-abuse.ts
6110
+ // src/rules/test/qa-test-006-retry-abuse.ts
6159
6111
  /**
6160
6112
  * QA-TEST-006 — Retry abuse in test config.
6161
6113
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6221,8 +6173,7 @@ const retryAbuse = defineRule({
6221
6173
  return findings;
6222
6174
  }
6223
6175
  });
6224
- //#endregion
6225
- //#region src/rules/test/qa-test-010-empty-body.ts
6176
+ // src/rules/test/qa-test-010-empty-body.ts
6226
6177
  /**
6227
6178
  * QA-TEST-010 — Empty test body.
6228
6179
  * Severity: error · Confidence: high · deterministic-defect
@@ -6277,8 +6228,7 @@ const emptyTestBody = defineRule({
6277
6228
  return findings;
6278
6229
  }
6279
6230
  });
6280
- //#endregion
6281
- //#region src/rules/quality/qa-tqual-002-tautological.ts
6231
+ // src/rules/quality/qa-tqual-002-tautological.ts
6282
6232
  /**
6283
6233
  * QA-TQUAL-002 — Tautological assertion.
6284
6234
  * Severity: error · Confidence: high · deterministic-defect
@@ -6355,8 +6305,7 @@ const tautologicalAssertion = defineRule({
6355
6305
  return findings;
6356
6306
  }
6357
6307
  });
6358
- //#endregion
6359
- //#region src/rules/quality/qa-tqual-009-promise-assertion.ts
6308
+ // src/rules/quality/qa-tqual-009-promise-assertion.ts
6360
6309
  /**
6361
6310
  * QA-TQUAL-009 — Unawaited assertion inside a promise chain.
6362
6311
  * Severity: error · Confidence: high · deterministic-defect
@@ -6526,8 +6475,7 @@ function matchParen$2(text, open) {
6526
6475
  }
6527
6476
  return -1;
6528
6477
  }
6529
- //#endregion
6530
- //#region src/rules/quality/qa-tqual-011-commented-out.ts
6478
+ // src/rules/quality/qa-tqual-011-commented-out.ts
6531
6479
  /**
6532
6480
  * QA-TQUAL-011 — Commented-out tests.
6533
6481
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6640,8 +6588,7 @@ function commentRanges(ctx) {
6640
6588
  }
6641
6589
  return ranges;
6642
6590
  }
6643
- //#endregion
6644
- //#region src/rules/playwright/qa-pw-002-unawaited-assertion.ts
6591
+ // src/rules/playwright/qa-pw-002-unawaited-assertion.ts
6645
6592
  /**
6646
6593
  * QA-PW-002 — Missing `await` on a Playwright locator assertion.
6647
6594
  * Severity: error · Confidence: high · deterministic-defect
@@ -6786,8 +6733,7 @@ function runRegexFallback(text, path) {
6786
6733
  });
6787
6734
  return findings;
6788
6735
  }
6789
- //#endregion
6790
- //#region src/rules/playwright/qa-pw-003-debug-artifacts.ts
6736
+ // src/rules/playwright/qa-pw-003-debug-artifacts.ts
6791
6737
  /**
6792
6738
  * QA-PW-003 — Committed `page.pause()` or `test.only()` in e2e specs.
6793
6739
  * Severity: error · Confidence: high · deterministic-defect
@@ -6839,8 +6785,7 @@ const committedDebugArtifacts = defineRule({
6839
6785
  return findings;
6840
6786
  }
6841
6787
  });
6842
- //#endregion
6843
- //#region src/rules/playwright/qa-pw-004-brittle-selectors.ts
6788
+ // src/rules/playwright/qa-pw-004-brittle-selectors.ts
6844
6789
  /**
6845
6790
  * QA-PW-004 — Brittle CSS/XPath selectors vs role-based locators.
6846
6791
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -6932,8 +6877,7 @@ function stripComments(ctx) {
6932
6877
  } catch {}
6933
6878
  return ctx.text.replace(/\/\/[^\n]*/g, (cm) => " ".repeat(cm.length));
6934
6879
  }
6935
- //#endregion
6936
- //#region src/rules/ci/verification-gate.ts
6880
+ // src/rules/ci/verification-gate.ts
6937
6881
  /**
6938
6882
  * Shared "is this shell command a verification gate?" matcher for the QA-CI
6939
6883
  * rules. A gate is a command whose failure MUST fail the job — test runners,
@@ -6973,8 +6917,7 @@ const VERIFICATION_GATE_RE = new RegExp([
6973
6917
  function looksLikeVerificationGate(cmd) {
6974
6918
  return VERIFICATION_GATE_RE.test(cmd);
6975
6919
  }
6976
- //#endregion
6977
- //#region src/rules/ci/azure-gates.ts
6920
+ // src/rules/ci/azure-gates.ts
6978
6921
  /** Discriminator: the Azure parser is the only one emitting `platform`. */
6979
6922
  function isAzurePipelineDoc(ast) {
6980
6923
  return typeof ast === "object" && ast !== null && ast.platform === "azure-pipelines";
@@ -7058,8 +7001,7 @@ function locateAzureJobKey(text, job, keyName) {
7058
7001
  if (m) return lineAt(text, m.index);
7059
7002
  return 1;
7060
7003
  }
7061
- //#endregion
7062
- //#region src/rules/ci/qa-ci-001-continue-on-error.ts
7004
+ // src/rules/ci/qa-ci-001-continue-on-error.ts
7063
7005
  /**
7064
7006
  * QA-CI-001 — `continue-on-error: true` masks a failing job.
7065
7007
  * Severity: error · Confidence: high · deterministic-defect
@@ -7287,8 +7229,7 @@ function describeAzureStep(step) {
7287
7229
  if (step.task) return step.task;
7288
7230
  return "step";
7289
7231
  }
7290
- //#endregion
7291
- //#region src/rules/ci/qa-ci-002-swallowed-exit.ts
7232
+ // src/rules/ci/qa-ci-002-swallowed-exit.ts
7292
7233
  /**
7293
7234
  * QA-CI-002 — `|| true` swallows a command's exit code.
7294
7235
  * Severity: error · Confidence: high · deterministic-defect
@@ -7343,8 +7284,7 @@ const swallowedExitCode = defineRule({
7343
7284
  return findings;
7344
7285
  }
7345
7286
  });
7346
- //#endregion
7347
- //#region src/rules/ci/qa-ci-007-retry-masking.ts
7287
+ // src/rules/ci/qa-ci-007-retry-masking.ts
7348
7288
  /**
7349
7289
  * QA-CI-007 — Retry masking test failures.
7350
7290
  * Severity: warning · Confidence: high · deterministic-defect
@@ -7495,8 +7435,7 @@ function azureArm$1(ctx, doc, findings) {
7495
7435
  });
7496
7436
  }
7497
7437
  }
7498
- //#endregion
7499
- //#region src/rules/ci/qa-ci-005-report-never-generated.ts
7438
+ // src/rules/ci/qa-ci-005-report-never-generated.ts
7500
7439
  /**
7501
7440
  * QA-CI-005 — Report consumed but never generated.
7502
7441
  * Severity: error · Confidence: high · deterministic-defect
@@ -7654,8 +7593,7 @@ function findJobConsumerLine(text, jobName, consumerRe) {
7654
7593
  const anywhere = new RegExp(consumerRe.source, "i").exec(text);
7655
7594
  return anywhere ? lineAt(text, anywhere.index) : 1;
7656
7595
  }
7657
- //#endregion
7658
- //#region src/rules/ci/jenkins-gates.ts
7596
+ // src/rules/ci/jenkins-gates.ts
7659
7597
  /**
7660
7598
  * Jenkinsfile block scanner for the QA-CI Jenkins arms (product-gap
7661
7599
  * master plan P3c). Jenkinsfiles are Groovy text — "no new language
@@ -7901,8 +7839,7 @@ function textIsVerificationGate(text) {
7901
7839
  function lineOfOffset(text, offset) {
7902
7840
  return lineAt(text, offset);
7903
7841
  }
7904
- //#endregion
7905
- //#region src/rules/ci/qa-ci-008-always-success.ts
7842
+ // src/rules/ci/qa-ci-008-always-success.ts
7906
7843
  /**
7907
7844
  * QA-CI-008 — Always-success step at the end of a job.
7908
7845
  * Severity: error · Confidence: high · deterministic-defect
@@ -8076,8 +8013,7 @@ function jenkinsArm$1(ctx, findings) {
8076
8013
  });
8077
8014
  }
8078
8015
  }
8079
- //#endregion
8080
- //#region src/rules/ci/qa-ci-009-exit-code.ts
8016
+ // src/rules/ci/qa-ci-009-exit-code.ts
8081
8017
  /**
8082
8018
  * QA-CI-009 — Test command does not propagate exit code.
8083
8019
  * Severity: error · Confidence: high · deterministic-defect
@@ -8212,8 +8148,7 @@ function jenkinsArm(ctx, findings) {
8212
8148
  });
8213
8149
  }
8214
8150
  }
8215
- //#endregion
8216
- //#region src/rules/ci/qa-ci-010-non-blocking.ts
8151
+ // src/rules/ci/qa-ci-010-non-blocking.ts
8217
8152
  /**
8218
8153
  * QA-CI-010 — Required test job is non-blocking for branch protection.
8219
8154
  * Severity: error · Confidence: medium · deterministic-defect
@@ -8296,8 +8231,7 @@ function findLine(text, re) {
8296
8231
  function escapeRe(s) {
8297
8232
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8298
8233
  }
8299
- //#endregion
8300
- //#region src/rules/ci/qa-ci-013-can-never-fail.ts
8234
+ // src/rules/ci/qa-ci-013-can-never-fail.ts
8301
8235
  /**
8302
8236
  * QA-CI-013 — Verification stage/job conditioned so it can never fail the
8303
8237
  * pipeline (product-gap master plan P3b).
@@ -8401,8 +8335,7 @@ const canNeverFailGate = defineRule({
8401
8335
  function jobHasGate(job) {
8402
8336
  return (job.steps ?? []).some((s) => s !== void 0 && stepIsVerificationGate$1(s));
8403
8337
  }
8404
- //#endregion
8405
- //#region src/rules/ci/qa-ci-014-swallowed-verification.ts
8338
+ // src/rules/ci/qa-ci-014-swallowed-verification.ts
8406
8339
  /**
8407
8340
  * QA-CI-014 — try/catch swallowing a verification-stage failure without
8408
8341
  * rethrow or failure-marking (product-gap master plan P3c).
@@ -8459,8 +8392,7 @@ const swallowedVerificationFailure = defineRule({
8459
8392
  return findings;
8460
8393
  }
8461
8394
  });
8462
- //#endregion
8463
- //#region src/rules/python/qa-py-003-no-assertions.ts
8395
+ // src/rules/python/qa-py-003-no-assertions.ts
8464
8396
  /**
8465
8397
  * QA-PY-003 — Test function with no assertions.
8466
8398
  * Severity: error · Confidence: high · deterministic-defect
@@ -8581,8 +8513,7 @@ function extractBlock$1(text, afterColon) {
8581
8513
  const lineEnd = rest.indexOf("\n", firstIdx);
8582
8514
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
8583
8515
  }
8584
- //#endregion
8585
- //#region src/rules/python/qa-py-005-hard-sleep.ts
8516
+ // src/rules/python/qa-py-005-hard-sleep.ts
8586
8517
  /**
8587
8518
  * QA-PY-005 — time.sleep() in tests.
8588
8519
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8629,8 +8560,7 @@ const pyHardSleep = defineRule({
8629
8560
  return findings;
8630
8561
  }
8631
8562
  });
8632
- //#endregion
8633
- //#region src/rules/python/qa-py-002-skipped-test.ts
8563
+ // src/rules/python/qa-py-002-skipped-test.ts
8634
8564
  /**
8635
8565
  * QA-PY-002 — Skipped test (`@pytest.mark.skip`, `xfail` non-strict).
8636
8566
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8690,8 +8620,7 @@ const pySkippedTest = defineRule({
8690
8620
  return findings;
8691
8621
  }
8692
8622
  });
8693
- //#endregion
8694
- //#region src/rules/python/qa-py-012-tautological.ts
8623
+ // src/rules/python/qa-py-012-tautological.ts
8695
8624
  /**
8696
8625
  * QA-PY-012 — Tautological assertion (`assert True`, `assert x == x`).
8697
8626
  * Severity: error · Confidence: high · deterministic-defect
@@ -8746,8 +8675,7 @@ const pyTautological = defineRule({
8746
8675
  return findings;
8747
8676
  }
8748
8677
  });
8749
- //#endregion
8750
- //#region src/rules/python/qa-py-001-focused-test.ts
8678
+ // src/rules/python/qa-py-001-focused-test.ts
8751
8679
  /**
8752
8680
  * QA-PY-001 — Focused test committed.
8753
8681
  * Severity: error · Confidence: high · deterministic-defect
@@ -8801,8 +8729,7 @@ const pyFocusedTest = defineRule({
8801
8729
  return findings;
8802
8730
  }
8803
8731
  });
8804
- //#endregion
8805
- //#region src/engine/jv-cs-ast.ts
8732
+ // src/engine/jv-cs-ast.ts
8806
8733
  /** Narrow the loose `ast` seam back to a tree-sitter Tree. */
8807
8734
  function getTreeSitterTree(ast) {
8808
8735
  if (!(ast instanceof Object) || !("rootNode" in ast)) return void 0;
@@ -9006,8 +8933,7 @@ function isHelperIdiom(name) {
9006
8933
  }
9007
8934
  return false;
9008
8935
  }
9009
- //#endregion
9010
- //#region src/engine/python-ast.ts
8936
+ // src/engine/python-ast.ts
9011
8937
  function getPythonTree(ast) {
9012
8938
  return getTreeSitterTree(ast);
9013
8939
  }
@@ -9092,8 +9018,7 @@ function pythonWithRaisesBlocks(tree) {
9092
9018
  });
9093
9019
  return out;
9094
9020
  }
9095
- //#endregion
9096
- //#region src/rules/python/qa-py-007-raises-without-match.ts
9021
+ // src/rules/python/qa-py-007-raises-without-match.ts
9097
9022
  /**
9098
9023
  * QA-PY-007 — pytest.raises without match.
9099
9024
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9220,8 +9145,7 @@ function astArm(ctx, tree) {
9220
9145
  }
9221
9146
  return findings;
9222
9147
  }
9223
- //#endregion
9224
- //#region src/rules/python/qa-py-009-commented-out.ts
9148
+ // src/rules/python/qa-py-009-commented-out.ts
9225
9149
  /**
9226
9150
  * QA-PY-009 — Commented-out tests.
9227
9151
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9268,8 +9192,7 @@ const pyCommentedOutTest = defineRule({
9268
9192
  return findings;
9269
9193
  }
9270
9194
  });
9271
- //#endregion
9272
- //#region src/rules/python/qa-py-004-bare-truthiness.ts
9195
+ // src/rules/python/qa-py-004-bare-truthiness.ts
9273
9196
  /**
9274
9197
  * QA-PY-004 — Bare truthiness assert on complex object.
9275
9198
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9348,8 +9271,7 @@ const pyBareTruthinessAssert = defineRule({
9348
9271
  return findings;
9349
9272
  }
9350
9273
  });
9351
- //#endregion
9352
- //#region src/rules/python/qa-py-011-mutable-fixture.ts
9274
+ // src/rules/python/qa-py-011-mutable-fixture.ts
9353
9275
  /**
9354
9276
  * QA-PY-011 — Mutable fixture shared across tests.
9355
9277
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9400,8 +9322,7 @@ const pyMutableFixture = defineRule({
9400
9322
  return findings;
9401
9323
  }
9402
9324
  });
9403
- //#endregion
9404
- //#region src/rules/playwright/qa-pw-101-wait-for-timeout.ts
9325
+ // src/rules/playwright/qa-pw-101-wait-for-timeout.ts
9405
9326
  /**
9406
9327
  * QA-PW-101 — waitForTimeout() anywhere.
9407
9328
  * Severity: error · Confidence: high · deterministic-defect
@@ -9448,8 +9369,7 @@ const pwWaitForTimeout = defineRule({
9448
9369
  return findings;
9449
9370
  }
9450
9371
  });
9451
- //#endregion
9452
- //#region src/rules/playwright/qa-pw-102-wait-load.ts
9372
+ // src/rules/playwright/qa-pw-102-wait-load.ts
9453
9373
  /**
9454
9374
  * QA-PW-102 — waitForEvent('load') / waitForLoadState('load') instead of
9455
9375
  * web-first assertions.
@@ -9503,8 +9423,7 @@ const pwWaitForLoadEvent = defineRule({
9503
9423
  return findings;
9504
9424
  }
9505
9425
  });
9506
- //#endregion
9507
- //#region src/rules/playwright/qa-pw-113-deep-frames.ts
9426
+ // src/rules/playwright/qa-pw-113-deep-frames.ts
9508
9427
  /**
9509
9428
  * QA-PW-113 — frameLocator chains > 2 deep.
9510
9429
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9554,8 +9473,7 @@ const pwDeepFrameLocator = defineRule({
9554
9473
  function count(fragment) {
9555
9474
  return fragment.match(/frameLocator\s*\(/g).length;
9556
9475
  }
9557
- //#endregion
9558
- //#region src/rules/playwright/qa-pw-117-serial.ts
9476
+ // src/rules/playwright/qa-pw-117-serial.ts
9559
9477
  /**
9560
9478
  * QA-PW-117 — test.describe.serial without justification comment.
9561
9479
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9609,8 +9527,7 @@ const pwSerialNoJustification = defineRule({
9609
9527
  return findings;
9610
9528
  }
9611
9529
  });
9612
- //#endregion
9613
- //#region src/rules/playwright/qa-pw-121-config-retries.ts
9530
+ // src/rules/playwright/qa-pw-121-config-retries.ts
9614
9531
  /**
9615
9532
  * QA-PW-121 — playwright.config: retries >= 3 or workers = 0.
9616
9533
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9675,8 +9592,7 @@ const pwConfigRetryAbuse = defineRule({
9675
9592
  return findings;
9676
9593
  }
9677
9594
  });
9678
- //#endregion
9679
- //#region src/rules/playwright/qa-pw-122-no-trace.ts
9595
+ // src/rules/playwright/qa-pw-122-no-trace.ts
9680
9596
  /**
9681
9597
  * QA-PW-122 — No trace capture on retry in playwright.config.
9682
9598
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9725,8 +9641,7 @@ const pwNoTraceOnRetry = defineRule({
9725
9641
  return findings;
9726
9642
  }
9727
9643
  });
9728
- //#endregion
9729
- //#region src/rules/playwright/qa-pw-124-project-split.ts
9644
+ // src/rules/playwright/qa-pw-124-project-split.ts
9730
9645
  /**
9731
9646
  * QA-PW-124 — Projects defined but no smoke/regression split.
9732
9647
  * Severity: info · Confidence: high · deterministic-defect
@@ -9788,8 +9703,7 @@ const pwNoProjectSplit = defineRule({
9788
9703
  return findings;
9789
9704
  }
9790
9705
  });
9791
- //#endregion
9792
- //#region src/rules/playwright/qa-pw-104-trial-click.ts
9706
+ // src/rules/playwright/qa-pw-104-trial-click.ts
9793
9707
  /**
9794
9708
  * QA-PW-104 — `{ trial: true }` misuse — check-then-act race.
9795
9709
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9835,8 +9749,7 @@ const pwTrialMisuse = defineRule({
9835
9749
  return findings;
9836
9750
  }
9837
9751
  });
9838
- //#endregion
9839
- //#region src/rules/playwright/qa-pw-116-storage-state.ts
9752
+ // src/rules/playwright/qa-pw-116-storage-state.ts
9840
9753
  /**
9841
9754
  * QA-PW-116 — storageState reuse without expiry strategy.
9842
9755
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9882,8 +9795,7 @@ const pwStorageStateNoExpiry = defineRule({
9882
9795
  return findings;
9883
9796
  }
9884
9797
  });
9885
- //#endregion
9886
- //#region src/rules/playwright/qa-pw-125-global-setup.ts
9798
+ // src/rules/playwright/qa-pw-125-global-setup.ts
9887
9799
  /**
9888
9800
  * QA-PW-125 — globalSetup mutating shared external state.
9889
9801
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9941,8 +9853,7 @@ const pwGlobalSetupSharedState = defineRule({
9941
9853
  return findings;
9942
9854
  }
9943
9855
  });
9944
- //#endregion
9945
- //#region src/rules/playwright/qa-pw-115-shared-page.ts
9856
+ // src/rules/playwright/qa-pw-115-shared-page.ts
9946
9857
  /**
9947
9858
  * QA-PW-115 — Shared `page` across parallel tests without isolation.
9948
9859
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9990,8 +9901,7 @@ const pwSharedPage = defineRule({
9990
9901
  return findings;
9991
9902
  }
9992
9903
  });
9993
- //#endregion
9994
- //#region src/rules/playwright/qa-pw-140.ts
9904
+ // src/rules/playwright/qa-pw-140.ts
9995
9905
  /**
9996
9906
  * QA-PW-140 — Screenshot without maxDiffPixelRatio.
9997
9907
  *
@@ -10065,8 +9975,7 @@ function matchParen(text, open) {
10065
9975
  }
10066
9976
  return -1;
10067
9977
  }
10068
- //#endregion
10069
- //#region src/rules/playwright/qa-pw-123-hardcoded-url.ts
9978
+ // src/rules/playwright/qa-pw-123-hardcoded-url.ts
10070
9979
  /**
10071
9980
  * QA-PW-123 — Hardcoded baseURL in specs.
10072
9981
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10114,8 +10023,7 @@ const hardcodedBaseUrl = defineRule({
10114
10023
  return findings;
10115
10024
  }
10116
10025
  });
10117
- //#endregion
10118
- //#region src/rules/quality/qa-env-001-env-coupling.ts
10026
+ // src/rules/quality/qa-env-001-env-coupling.ts
10119
10027
  /**
10120
10028
  * QA-ENV-001 — "Works On My Machine" detector core (Tier 5 #24).
10121
10029
  * Environment coupling in test files: fixed ports, OS paths,
@@ -10182,8 +10090,7 @@ const envCoupling = defineRule({
10182
10090
  return findings;
10183
10091
  }
10184
10092
  });
10185
- //#endregion
10186
- //#region src/rules/playwright/qa-pw-141-retry-no-triage.ts
10093
+ // src/rules/playwright/qa-pw-141-retry-no-triage.ts
10187
10094
  /**
10188
10095
  * QA-PW-141 — playwright.config retries set high enough to mask flakes.
10189
10096
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10240,8 +10147,7 @@ const pwRetryMaskingNoForensics = defineRule({
10240
10147
  return findings;
10241
10148
  }
10242
10149
  });
10243
- //#endregion
10244
- //#region src/rules/playwright/qa-pw-142-blanket-route.ts
10150
+ // src/rules/playwright/qa-pw-142-blanket-route.ts
10245
10151
  /**
10246
10152
  * QA-PW-142 — page.route() blanket-mocking instead of scoped intercepts.
10247
10153
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -10296,8 +10202,7 @@ const pwBlanketRouteMock = defineRule({
10296
10202
  return findings;
10297
10203
  }
10298
10204
  });
10299
- //#endregion
10300
- //#region src/rules/playwright/qa-pw-143-no-artifacts.ts
10205
+ // src/rules/playwright/qa-pw-143-no-artifacts.ts
10301
10206
  /**
10302
10207
  * QA-PW-143 — No screenshot/video artifact capture on failure.
10303
10208
  * Severity: info · Confidence: high · deterministic-defect
@@ -10350,8 +10255,7 @@ const pwNoFailureArtifacts = defineRule({
10350
10255
  return findings;
10351
10256
  }
10352
10257
  });
10353
- //#endregion
10354
- //#region src/rules/playwright/qa-pw-144-single-browser.ts
10258
+ // src/rules/playwright/qa-pw-144-single-browser.ts
10355
10259
  /**
10356
10260
  * QA-PW-144 — Single-browser project matrix (no cross-browser coverage).
10357
10261
  * Severity: info · Confidence: high · deterministic-defect
@@ -10413,8 +10317,7 @@ const pwSingleBrowserMatrix = defineRule({
10413
10317
  return findings;
10414
10318
  }
10415
10319
  });
10416
- //#endregion
10417
- //#region src/rules/playwright/qa-pw-146-css-locator.ts
10320
+ // src/rules/playwright/qa-pw-146-css-locator.ts
10418
10321
  /**
10419
10322
  * QA-PW-146 — CSS/XPath string selector where Playwright's locator
10420
10323
  * standard expects a normalized locator (plan §17.3).
@@ -10493,8 +10396,7 @@ const pwLocatorNormalize = defineRule({
10493
10396
  return findings;
10494
10397
  }
10495
10398
  });
10496
- //#endregion
10497
- //#region src/rules/playwright/qa-pw-147-codegen-artifact.ts
10399
+ // src/rules/playwright/qa-pw-147-codegen-artifact.ts
10498
10400
  /**
10499
10401
  * QA-PW-147 — Playwright codegen artifact left unedited (plan §17.1).
10500
10402
  * Severity: info · Confidence: medium · observation
@@ -10553,8 +10455,7 @@ const pwCodegenArtifact = defineRule({
10553
10455
  return findings;
10554
10456
  }
10555
10457
  });
10556
- //#endregion
10557
- //#region src/rules/python/qa-py-101-sync-async-mix.ts
10458
+ // src/rules/python/qa-py-101-sync-async-mix.ts
10558
10459
  /**
10559
10460
  * QA-PY-101 — Playwright-Python: sync API in async test (or vice versa).
10560
10461
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10603,8 +10504,7 @@ const pyPwSyncAsyncMix = defineRule({
10603
10504
  return findings;
10604
10505
  }
10605
10506
  });
10606
- //#endregion
10607
- //#region src/rules/python/qa-py-103-wait-for-timeout.ts
10507
+ // src/rules/python/qa-py-103-wait-for-timeout.ts
10608
10508
  /**
10609
10509
  * QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
10610
10510
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10651,8 +10551,7 @@ const pyPwWaitForTimeout = defineRule({
10651
10551
  return findings;
10652
10552
  }
10653
10553
  });
10654
- //#endregion
10655
- //#region src/rules/python/qa-py-105-pw-no-assertions.ts
10554
+ // src/rules/python/qa-py-105-pw-no-assertions.ts
10656
10555
  /**
10657
10556
  * QA-PY-105 — Playwright-Python: no assertion in Playwright test.
10658
10557
  * Severity: error · Confidence: high · deterministic-defect
@@ -10732,8 +10631,7 @@ function extractBlock(text, afterColon) {
10732
10631
  const lineEnd = rest.indexOf("\n", firstContent.index);
10733
10632
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
10734
10633
  }
10735
- //#endregion
10736
- //#region src/rules/java/qa-jv-101-disabled-test.ts
10634
+ // src/rules/java/qa-jv-101-disabled-test.ts
10737
10635
  /**
10738
10636
  * QA-JV-101 — @Disabled test without justification.
10739
10637
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10787,8 +10685,7 @@ const jvDisabledTest = defineRule({
10787
10685
  return findings;
10788
10686
  }
10789
10687
  });
10790
- //#endregion
10791
- //#region src/rules/java/qa-jv-103-no-assertions.ts
10688
+ // src/rules/java/qa-jv-103-no-assertions.ts
10792
10689
  /**
10793
10690
  * QA-JV-103 — Test method without assertions.
10794
10691
  * Severity: error · Confidence: high · deterministic-defect
@@ -10945,8 +10842,7 @@ function matchBrace$1(text, open) {
10945
10842
  }
10946
10843
  return -1;
10947
10844
  }
10948
- //#endregion
10949
- //#region src/engine/qa-model.ts
10845
+ // src/engine/qa-model.ts
10950
10846
  /**
10951
10847
  * Extract the semantic model for one parsed file. Resolves undefined
10952
10848
  * when the language has no extractor or the parse stage produced no
@@ -11489,8 +11385,7 @@ function extractPythonModel(file) {
11489
11385
  nodes
11490
11386
  };
11491
11387
  }
11492
- //#endregion
11493
- //#region src/rules/java/qa-jv-105-wait-for-timeout.ts
11388
+ // src/rules/java/qa-jv-105-wait-for-timeout.ts
11494
11389
  /**
11495
11390
  * QA-JV-105 — waitForTimeout / hardcoded navigation waits.
11496
11391
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11575,8 +11470,7 @@ const jvWaitForTimeout = defineRule({
11575
11470
  return findings;
11576
11471
  }
11577
11472
  });
11578
- //#endregion
11579
- //#region src/rules/csharp/qa-cs-101-skipped-test.ts
11473
+ // src/rules/csharp/qa-cs-101-skipped-test.ts
11580
11474
  /**
11581
11475
  * QA-CS-101 — [Ignore]/[Skip] test without justification.
11582
11476
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11640,8 +11534,7 @@ const csSkippedTest = defineRule({
11640
11534
  return findings;
11641
11535
  }
11642
11536
  });
11643
- //#endregion
11644
- //#region src/rules/csharp/qa-cs-103-no-assertions.ts
11537
+ // src/rules/csharp/qa-cs-103-no-assertions.ts
11645
11538
  /**
11646
11539
  * QA-CS-103 — Test method without assertions.
11647
11540
  * Severity: error · Confidence: high · deterministic-defect
@@ -11675,7 +11568,6 @@ const csSkippedTest = defineRule({
11675
11568
  * Deep-diffing those requires symbol semantics Mjölnir deliberately does
11676
11569
  * not promise for C# (plan §13.4).
11677
11570
  */
11678
- /** Helper/verification idiom: verify/check/assert-prefixed names. */
11679
11571
  const WHY$5 = "Without an assertion the test can only fail by throwing — it cannot detect behavioral regressions.";
11680
11572
  const FIX$4 = "Add an assertion on the expected outcome, or remove the test.";
11681
11573
  function makeFinding(path, position, name) {
@@ -11839,8 +11731,7 @@ function matchBrace(text, open) {
11839
11731
  }
11840
11732
  return -1;
11841
11733
  }
11842
- //#endregion
11843
- //#region src/rules/csharp/qa-cs-105-wait-for-timeout.ts
11734
+ // src/rules/csharp/qa-cs-105-wait-for-timeout.ts
11844
11735
  /**
11845
11736
  * QA-CS-105 — Playwright-.NET: WaitForTimeoutAsync hard sleep.
11846
11737
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11928,8 +11819,7 @@ const csWaitForTimeout = defineRule({
11928
11819
  return findings;
11929
11820
  }
11930
11821
  });
11931
- //#endregion
11932
- //#region src/rules/cypress/qa-cyp-001-cy-wait.ts
11822
+ // src/rules/cypress/qa-cyp-001-cy-wait.ts
11933
11823
  /**
11934
11824
  * QA-CYP-001 — Fixed `cy.wait(n)` (hard-coded wait).
11935
11825
  * Severity: warning · Confidence: high · deterministic-defect
@@ -12008,8 +11898,7 @@ function isCypressFile(ctx) {
12008
11898
  if (/\.cy\.[jt]sx?$/.test(ctx.path)) return true;
12009
11899
  return /\bcy\s*\./.test(ctx.codeText ?? ctx.text);
12010
11900
  }
12011
- //#endregion
12012
- //#region src/rules/cypress/qa-cyp-002-focused-test.ts
11901
+ // src/rules/cypress/qa-cyp-002-focused-test.ts
12013
11902
  /**
12014
11903
  * QA-CYP-002 — Focused test (`.only`).
12015
11904
  * Severity: error · Confidence: high · deterministic-defect
@@ -12072,8 +11961,7 @@ const cypFocusedTest = defineRule({
12072
11961
  return findings;
12073
11962
  }
12074
11963
  });
12075
- //#endregion
12076
- //#region src/rules/cypress/qa-cyp-003-config-security.ts
11964
+ // src/rules/cypress/qa-cyp-003-config-security.ts
12077
11965
  /**
12078
11966
  * QA-CYP-003 — Cypress config disables `chromeWebSecurity`.
12079
11967
  * Severity: error · Confidence: high · deterministic-defect
@@ -12143,8 +12031,7 @@ const cypConfigSecurity = defineRule({
12143
12031
  return findings;
12144
12032
  }
12145
12033
  });
12146
- //#endregion
12147
- //#region src/rules/selenium/qa-se-sleep-lookup.ts
12034
+ // src/rules/selenium/qa-se-sleep-lookup.ts
12148
12035
  /**
12149
12036
  * Selenium sleep-then-interact rules (Verification Trust Evolution Plan
12150
12037
  * §15.3 — "Selenium implicit-wait abuse / missing WebDriverWait",
@@ -12295,8 +12182,7 @@ const sePythonSleepLookup = defineRule({
12295
12182
  return sleepBeforeLookup(ctx, PY_SLEEP_RE, PY_LOOKUP_RE, "QA-SE-003");
12296
12183
  }
12297
12184
  });
12298
- //#endregion
12299
- //#region src/rules/shared/family.ts
12185
+ // src/rules/shared/family.ts
12300
12186
  /**
12301
12187
  * Cross-language pattern rule family generator (Phase 6 — Tempering Plan).
12302
12188
  *
@@ -12362,8 +12248,7 @@ function definePatternFamily(opts) {
12362
12248
  }
12363
12249
  }));
12364
12250
  }
12365
- //#endregion
12366
- //#region src/rules/families/hard-sleep.ts
12251
+ // src/rules/families/hard-sleep.ts
12367
12252
  /**
12368
12253
  * Hard-sleep family (Phase 6 — Tempering Plan).
12369
12254
  * Java: Thread.sleep(), C#: Thread.Sleep() / Task.Delay()
@@ -12514,8 +12399,7 @@ const hardSleepFamily = definePatternFamily({
12514
12399
  fix: "Use `await Assertions.Expect(locator).ToBeVisibleAsync()` or locator.WaitForAsync()."
12515
12400
  }]
12516
12401
  });
12517
- //#endregion
12518
- //#region src/rules/families/network-idle.ts
12402
+ // src/rules/families/network-idle.ts
12519
12403
  /**
12520
12404
  * Network-idle family (Phase 6 — Tempering Plan).
12521
12405
  * All three language variants of waitForLoadState(networkidle).
@@ -12576,8 +12460,7 @@ const networkIdleFamily = definePatternFamily({
12576
12460
  }
12577
12461
  ]
12578
12462
  });
12579
- //#endregion
12580
- //#region src/rules/families/hardcoded-url.ts
12463
+ // src/rules/families/hardcoded-url.ts
12581
12464
  /**
12582
12465
  * Hardcoded-URL family (Phase 6 — Tempering Plan).
12583
12466
  * Detects absolute URLs in navigation/request calls across languages.
@@ -12627,8 +12510,7 @@ const hardcodedUrlFamily = definePatternFamily({
12627
12510
  fix: "Use `--base-url` / `base_url` fixture, or `os.environ[...]`."
12628
12511
  }]
12629
12512
  });
12630
- //#endregion
12631
- //#region src/rules/families/shared-page.ts
12513
+ // src/rules/families/shared-page.ts
12632
12514
  /**
12633
12515
  * Shared-page family (Phase 6 — Tempering Plan).
12634
12516
  * Detects static/module-level Page/Browser instances shared across tests.
@@ -12689,8 +12571,7 @@ const sharedPageFamily = definePatternFamily({
12689
12571
  }
12690
12572
  ]
12691
12573
  });
12692
- //#endregion
12693
- //#region src/rules/families/brittle-selectors.ts
12574
+ // src/rules/families/brittle-selectors.ts
12694
12575
  /**
12695
12576
  * Brittle-selectors family (Phase 6 — Tempering Plan).
12696
12577
  * Detects XPath/structural CSS selectors across Java, C#, Python.
@@ -12802,8 +12683,7 @@ const brittleSelectorsFamily = [
12802
12683
  }
12803
12684
  ], "Prefer role-based locators (`get_by_role(...)`) or data-testid attributes.")
12804
12685
  ];
12805
- //#endregion
12806
- //#region src/rules/families/retry-masking.ts
12686
+ // src/rules/families/retry-masking.ts
12807
12687
  /**
12808
12688
  * Retry-masking family (Phase 6 — Tempering Plan).
12809
12689
  * Detects retry annotations/attributes that mask flaky tests.
@@ -12923,8 +12803,7 @@ const retryMaskingFamily = [defineRule({
12923
12803
  return findings;
12924
12804
  }
12925
12805
  })];
12926
- //#endregion
12927
- //#region src/rules/index.ts
12806
+ // src/rules/index.ts
12928
12807
  const RULES = [
12929
12808
  focusedTestCommitted,
12930
12809
  skippedTest,
@@ -13046,8 +12925,7 @@ const RETIRED_RULE_IDS = [
13046
12925
  "QA-JV-111",
13047
12926
  "QA-CS-111"
13048
12927
  ];
13049
- //#endregion
13050
- //#region src/lib/wilson.ts
12928
+ // src/lib/wilson.ts
13051
12929
  /**
13052
12930
  * 95% Wilson score interval on a proportion (plan §20.2).
13053
12931
  *
@@ -13170,8 +13048,7 @@ function isProvisional(rule) {
13170
13048
  function isRetiredRule(ruleId) {
13171
13049
  return RETIRED_RULE_IDS.includes(ruleId);
13172
13050
  }
13173
- //#endregion
13174
- //#region src/scorer/scorer.ts
13051
+ // src/scorer/scorer.ts
13175
13052
  /**
13176
13053
  * Transparent scorer (Product-MVP §8).
13177
13054
  * score = 100 − Σ(severity deductions), floor 0.
@@ -13323,9 +13200,9 @@ function countTestDeclarations(text, codeText) {
13323
13200
  const source = codeText ?? text;
13324
13201
  const patterns = [
13325
13202
  /\b(?:it|test)(?:\.\w+)*\s*(?:\(|`)/g,
13326
- /^\s*(?:async\s+)?def\s+test_\w*/gm,
13327
- /^\s*@Test\b/gm,
13328
- /^\s*\[(?:Test|Fact|Theory|TestMethod)\b/gm
13203
+ /^[ \t]*(?:async[ \t]+)?def[ \t]+test_\w*/gm,
13204
+ /^[ \t]*@Test\b/gm,
13205
+ /^[ \t]*\[(?:Test|Fact|Theory|TestMethod)\b/gm
13329
13206
  ];
13330
13207
  let count = 0;
13331
13208
  for (const re of patterns) {
@@ -13334,8 +13211,7 @@ function countTestDeclarations(text, codeText) {
13334
13211
  }
13335
13212
  return count;
13336
13213
  }
13337
- //#endregion
13338
- //#region src/engine/rule-runner.ts
13214
+ // src/engine/rule-runner.ts
13339
13215
  /**
13340
13216
  * Legacy appliesTo → adapter id mapping.
13341
13217
  *
@@ -13374,8 +13250,7 @@ function asUniversal(rule) {
13374
13250
  }
13375
13251
  };
13376
13252
  }
13377
- //#endregion
13378
- //#region src/engine/tier-policy.ts
13253
+ // src/engine/tier-policy.ts
13379
13254
  /**
13380
13255
  * Tier policy (audit H-1): the declared tier is authoritative over a
13381
13256
  * rule's severity and evidence claims.
@@ -13515,8 +13390,7 @@ function enforceTierPolicy(findings, tierByRuleId) {
13515
13390
  if (EVIDENCE_RANK$1[effective] < EVIDENCE_RANK$1[floor.evidenceLevel]) f.evidenceLevel = floor.evidenceLevel;
13516
13391
  }
13517
13392
  }
13518
- //#endregion
13519
- //#region src/engine/evidence-core.ts
13393
+ // src/engine/evidence-core.ts
13520
13394
  /**
13521
13395
  * The core's own version — stamped into every record's provenance so a
13522
13396
  * consumer can tell which normalization contract produced a record.
@@ -13583,8 +13457,7 @@ function countEvidence(records) {
13583
13457
  }
13584
13458
  return counts;
13585
13459
  }
13586
- //#endregion
13587
- //#region src/engine/provenance.ts
13460
+ // src/engine/provenance.ts
13588
13461
  const GENERATED_HEADER_RE = /^\s*(?:\/\/|#|\/\*)\s*(?:auto[- ]?generated|generated by|do not edit)/i;
13589
13462
  /**
13590
13463
  * Codegen fingerprint: the recorder names its first test exactly
@@ -13625,8 +13498,7 @@ function computeAgenticProfile(files, findings) {
13625
13498
  note: "Static provenance markers only (generated-file headers, Playwright codegen default titles). Absence of a marker does not prove human authorship; healed and MCP-edited files without markers are indistinguishable from hand-written ones. Provenance never changes scoring or evidence — authorship is metadata, not trust."
13626
13499
  };
13627
13500
  }
13628
- //#endregion
13629
- //#region src/engine/overlap-dedup.ts
13501
+ // src/engine/overlap-dedup.ts
13630
13502
  const TIER_RANK = {
13631
13503
  core: 0,
13632
13504
  extended: 1,
@@ -13721,8 +13593,7 @@ function dropOrderBefore(a, b, metaByRuleId) {
13721
13593
  if (ranksBefore(b, a, metaByRuleId)) return true;
13722
13594
  return (a.column ?? Number.MAX_SAFE_INTEGER) < (b.column ?? Number.MAX_SAFE_INTEGER);
13723
13595
  }
13724
- //#endregion
13725
- //#region src/engine/finding-identity.ts
13596
+ // src/engine/finding-identity.ts
13726
13597
  /**
13727
13598
  * Finding Identity (ENGINE-002): deterministic, stable fingerprints for
13728
13599
  * deduplication, correlation, and lifecycle tracking across scans.
@@ -13761,8 +13632,7 @@ function codeQualityFingerprint(f) {
13761
13632
  function findingId(f) {
13762
13633
  return `${findingFingerprint(f)}\u0000${f.line}\u0000${f.column}`;
13763
13634
  }
13764
- //#endregion
13765
- //#region src/engine/correlation-engine.ts
13635
+ // src/engine/correlation-engine.ts
13766
13636
  const EVIDENCE_RANK = {
13767
13637
  E0: 0,
13768
13638
  E1: 1,
@@ -13892,8 +13762,7 @@ function correlateFindings(findings) {
13892
13762
  });
13893
13763
  return conclusions;
13894
13764
  }
13895
- //#endregion
13896
- //#region src/engine/dependency-graph.ts
13765
+ // src/engine/dependency-graph.ts
13897
13766
  /**
13898
13767
  * Dependency Graph (ECO-005).
13899
13768
  *
@@ -14066,8 +13935,7 @@ function getReachableFiles(fromFiles, graph) {
14066
13935
  resolvedAny: unresolvedStarts.length < fromFiles.length
14067
13936
  };
14068
13937
  }
14069
- //#endregion
14070
- //#region src/engine/incremental-analysis.ts
13938
+ // src/engine/incremental-analysis.ts
14071
13939
  /**
14072
13940
  * Semantic inputs that affect the full scan beyond individual file content.
14073
13941
  * If any of these changed, an incremental run is NOT safe — a full scan
@@ -14103,8 +13971,7 @@ function isIncrementalSafe(changedFiles) {
14103
13971
  reasons
14104
13972
  };
14105
13973
  }
14106
- //#endregion
14107
- //#region src/brand/tokens.ts
13974
+ // src/brand/tokens.ts
14108
13975
  /**
14109
13976
  * The single source of brand truth.
14110
13977
  *
@@ -14339,8 +14206,7 @@ RUNG_MEANINGS.map((meaning, i) => ({
14339
14206
  runtime: i >= 3,
14340
14207
  color: RUNG_COLORS[i]
14341
14208
  }));
14342
- //#endregion
14343
- //#region src/reporter/presentation.ts
14209
+ // src/reporter/presentation.ts
14344
14210
  /**
14345
14211
  * presentation.ts — the one place a presentation DECISION is made.
14346
14212
  *
@@ -14562,8 +14428,7 @@ function evidenceTag(f) {
14562
14428
  if (runtime !== null) tag += ` · runtime: ${runtime}`;
14563
14429
  return `[${tag}]`;
14564
14430
  }
14565
- //#endregion
14566
- //#region src/engine/monorepo-analysis.ts
14431
+ // src/engine/monorepo-analysis.ts
14567
14432
  /**
14568
14433
  * Package verdict, derived from the one ScoreState band model (BW-104).
14569
14434
  * These two constants used to be a private copy of the band boundaries;
@@ -14670,8 +14535,7 @@ function configurableAggregation(results, config) {
14670
14535
  strategy: config.weightingStrategy
14671
14536
  };
14672
14537
  }
14673
- //#endregion
14674
- //#region src/lib/fs-atomic.ts
14538
+ // src/lib/fs-atomic.ts
14675
14539
  /**
14676
14540
  * Atomic file writes (audit S9).
14677
14541
  *
@@ -14791,8 +14655,7 @@ function pidAlive(pid) {
14791
14655
  return err?.code !== "ESRCH";
14792
14656
  }
14793
14657
  }
14794
- //#endregion
14795
- //#region src/engine/scan-cache.ts
14658
+ // src/engine/scan-cache.ts
14796
14659
  /**
14797
14660
  * Local incremental scan cache (Beta-to-Stable 1.0 plan, M5.2 / A-2).
14798
14661
  *
@@ -15002,8 +14865,7 @@ function createScanCache(root) {
15002
14865
  }
15003
14866
  };
15004
14867
  }
15005
- //#endregion
15006
- //#region src/reporter/theme.ts
14868
+ // src/reporter/theme.ts
15007
14869
  /**
15008
14870
  * Norse-forge theme system. Pure string-in → string-out helpers.
15009
14871
  * Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
@@ -15212,8 +15074,7 @@ function gaugeColorForBand(band, p) {
15212
15074
  if (band === "unmeasured") return p.dim;
15213
15075
  return band === "warning" ? p.warning : p.error;
15214
15076
  }
15215
- //#endregion
15216
- //#region src/reporter/ui.ts
15077
+ // src/reporter/ui.ts
15217
15078
  /**
15218
15079
  * Neutral context for direct calls (tests, library use) and as the
15219
15080
  * safe default when a command runner is invoked without wiring: no
@@ -15302,8 +15163,7 @@ function formatDuration(ms) {
15302
15163
  if (ms === void 0) return "?";
15303
15164
  return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
15304
15165
  }
15305
- //#endregion
15306
- //#region src/forensics/classify.ts
15166
+ // src/forensics/classify.ts
15307
15167
  /**
15308
15168
  * Strong-signal marker tables. Kept deliberately narrow: a marker here
15309
15169
  * is a named family fact, not a vibe. Unknown text yields no signal —
@@ -15423,8 +15283,7 @@ function classifyForensicVerdict(signals) {
15423
15283
  signals: signalCounts
15424
15284
  };
15425
15285
  }
15426
- //#endregion
15427
- //#region src/forensics/analyze.ts
15286
+ // src/forensics/analyze.ts
15428
15287
  const ui$11 = plainContext();
15429
15288
  const MAX_RECORDS = 1e5;
15430
15289
  function analyze(records, source) {
@@ -15587,8 +15446,7 @@ function renderFlakyMd(report) {
15587
15446
  lines.push("");
15588
15447
  return lines.join("\n");
15589
15448
  }
15590
- //#endregion
15591
- //#region src/forensics/evidence-hygiene.ts
15449
+ // src/forensics/evidence-hygiene.ts
15592
15450
  /**
15593
15451
  * Evidence hygiene — secret redaction + control-char sanitization
15594
15452
  * (ENGINE-012).
@@ -15650,8 +15508,7 @@ function sanitizeErrorText(text, options) {
15650
15508
  if (result.length > maxLength) result = result.slice(0, maxLength) + `[truncated at ${maxLength} chars]`;
15651
15509
  return result;
15652
15510
  }
15653
- //#endregion
15654
- //#region src/forensics/trace.ts
15511
+ // src/forensics/trace.ts
15655
15512
  /**
15656
15513
  * Playwright trace ingester (product-gap master plan §9 R5 / WI-17).
15657
15514
  *
@@ -15844,8 +15701,7 @@ function parseTraceArtifact(bytes, artifactName) {
15844
15701
  const { actions } = parseTraceNdjson(text);
15845
15702
  return traceActionsToRecord(actions, artifactName);
15846
15703
  }
15847
- //#endregion
15848
- //#region src/forensics/parse-jest-json.ts
15704
+ // src/forensics/parse-jest-json.ts
15849
15705
  function toStatus$2(raw) {
15850
15706
  switch (raw) {
15851
15707
  case "passed": return "passed";
@@ -15905,8 +15761,7 @@ function parseJestJson(json) {
15905
15761
  }
15906
15762
  return out;
15907
15763
  }
15908
- //#endregion
15909
- //#region src/forensics/parse-junit.ts
15764
+ // src/forensics/parse-junit.ts
15910
15765
  const MAX_INPUT$1 = 20971520;
15911
15766
  function decodeEntities$1(s) {
15912
15767
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -15948,14 +15803,25 @@ function parseJunitXml(xml) {
15948
15803
  const skipped = /<skipped\b/i.test(inner);
15949
15804
  const errors = [];
15950
15805
  if (failed) {
15951
- const failRe = /<(?:failure|error)\b([^>]*)>([\s\S]*?)<\/(?:failure|error)\s*>/gi;
15952
- let failMatch;
15953
- while ((failMatch = failRe.exec(inner)) !== null) {
15954
- const attrsStr = failMatch[1] ?? "";
15806
+ const openRe = /<(?:failure|error)\b/gi;
15807
+ const endRe = /<\/(?:failure|error)\s*>/gi;
15808
+ let open;
15809
+ while ((open = openRe.exec(inner)) !== null) {
15810
+ const gt = inner.indexOf(">", open.index);
15811
+ if (gt === -1) break;
15812
+ const attrsStr = inner.slice(open.index, gt);
15813
+ if (attrsStr.endsWith("/")) {
15814
+ openRe.lastIndex = gt + 1;
15815
+ continue;
15816
+ }
15817
+ endRe.lastIndex = gt + 1;
15818
+ const end = endRe.exec(inner);
15819
+ if (end === null) break;
15955
15820
  const msgAttr = /\smessage\s*=\s*"([^"]*)"/i.exec(attrsStr)?.[1];
15956
15821
  if (msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(msgAttr)));
15957
- const body = failMatch[2]?.trim();
15822
+ const body = inner.slice(gt + 1, end.index).trim();
15958
15823
  if (body && body !== msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(body)));
15824
+ openRe.lastIndex = end.index + end[0].length;
15959
15825
  }
15960
15826
  const selfCloseRe = /<(?:failure|error)\b([^>]*)\/>/gi;
15961
15827
  let scMatch;
@@ -15979,8 +15845,7 @@ function parseJunitXml(xml) {
15979
15845
  }
15980
15846
  return out;
15981
15847
  }
15982
- //#endregion
15983
- //#region src/forensics/parse-trx.ts
15848
+ // src/forensics/parse-trx.ts
15984
15849
  const MAX_INPUT = 20971520;
15985
15850
  function decodeEntities(s) {
15986
15851
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -16091,11 +15956,24 @@ function parseTrxXml(xml) {
16091
15956
  if (id !== void 0 && name !== void 0) byTestId.set(id, name);
16092
15957
  }
16093
15958
  const records = [];
16094
- const resultRe = /<UnitTestResult\b([^>]*)>([\s\S]{0,4000}?)(?=<UnitTestResult\b|<\/Results>)/gi;
16095
- let m;
16096
- while ((m = resultRe.exec(xml)) !== null) {
16097
- const attrs = m[1] ?? "";
16098
- const inner = m[2] ?? "";
15959
+ const startRe = /<UnitTestResult\b/gi;
15960
+ const starts = [];
15961
+ let s;
15962
+ while ((s = startRe.exec(xml)) !== null) {
15963
+ starts.push(s.index);
15964
+ if (starts.length > 2e5) break;
15965
+ }
15966
+ const resultsEnd = xml.search(/<\/Results>/i);
15967
+ for (let i = 0; i < starts.length; i++) {
15968
+ const start = starts[i];
15969
+ const tagEnd = xml.indexOf(">", start);
15970
+ if (tagEnd === -1) break;
15971
+ const selfClosing = xml[tagEnd - 1] === "/";
15972
+ const attrs = xml.slice(start + 15, selfClosing ? tagEnd - 1 : tagEnd);
15973
+ const next = starts[i + 1] ?? -1;
15974
+ let bodyEnd = next === -1 ? xml.length : next;
15975
+ if (resultsEnd > tagEnd && resultsEnd < bodyEnd) bodyEnd = resultsEnd;
15976
+ const inner = selfClosing ? "" : xml.slice(tagEnd + 1, bodyEnd);
16099
15977
  const testName = attr(attrs, "testName") ?? attr(attrs, "testId");
16100
15978
  if (testName === void 0) continue;
16101
15979
  const attempt = {
@@ -16103,7 +15981,8 @@ function parseTrxXml(xml) {
16103
15981
  status: mapTrxOutcome(attr(attrs, "outcome")),
16104
15982
  durationMs: parseDuration(attr(attrs, "duration"))
16105
15983
  };
16106
- const error = messageOf(inner, 0);
15984
+ const messageAt = inner.search(/<Message\b/i);
15985
+ const error = messageAt === -1 ? void 0 : messageOf(inner, messageAt);
16107
15986
  records.push({
16108
15987
  file: "unknown",
16109
15988
  title: testName,
@@ -16115,8 +15994,7 @@ function parseTrxXml(xml) {
16115
15994
  }
16116
15995
  return records;
16117
15996
  }
16118
- //#endregion
16119
- //#region src/forensics/parse-playwright-json.ts
15997
+ // src/forensics/parse-playwright-json.ts
16120
15998
  const MAX_DEPTH = 32;
16121
15999
  function toStatus$1(raw) {
16122
16000
  switch (raw) {
@@ -16181,8 +16059,7 @@ function parsePlaywrightJson(json) {
16181
16059
  walk(root, 0);
16182
16060
  return out;
16183
16061
  }
16184
- //#endregion
16185
- //#region src/forensics/parse-vitest-json.ts
16062
+ // src/forensics/parse-vitest-json.ts
16186
16063
  function toStatus(raw) {
16187
16064
  switch (raw) {
16188
16065
  case "passed": return "passed";
@@ -16236,8 +16113,7 @@ function parseVitestJson(json) {
16236
16113
  }
16237
16114
  return out;
16238
16115
  }
16239
- //#endregion
16240
- //#region src/forensics/parse-har.ts
16116
+ // src/forensics/parse-har.ts
16241
16117
  const MAX_ENTRIES = 2e4;
16242
16118
  function sanitizeTitleUrl(url) {
16243
16119
  try {
@@ -16322,8 +16198,7 @@ function parseHarJsonDetailed(json) {
16322
16198
  truncated
16323
16199
  };
16324
16200
  }
16325
- //#endregion
16326
- //#region src/forensics/run.ts
16201
+ // src/forensics/run.ts
16327
16202
  /**
16328
16203
  * Runtime evidence entry point (R4) — the library behind `explain
16329
16204
  * --evidence`. The `forensics` verb is retired in the v6 carve; this module
@@ -16600,8 +16475,7 @@ function listFiles(dir) {
16600
16475
  walk(dir, 0);
16601
16476
  return out.map((p) => p);
16602
16477
  }
16603
- //#endregion
16604
- //#region src/config/suppressions.ts
16478
+ // src/config/suppressions.ts
16605
16479
  /**
16606
16480
  * Suppression governance (Sprint-Plan W7, Product-MVP §12).
16607
16481
  * `mjolnir scan --suppressions` — every suppression stays visible.
@@ -16638,8 +16512,7 @@ function renderSuppressions(report) {
16638
16512
  lines.push("");
16639
16513
  return lines.join("\n");
16640
16514
  }
16641
- //#endregion
16642
- //#region src/plugins/reserved-prefixes.ts
16515
+ // src/plugins/reserved-prefixes.ts
16643
16516
  /**
16644
16517
  * Core-owned rule-ID prefixes that external rule sources (npm plugins,
16645
16518
  * JS modules, JSON manifests) may never claim — one shared law.
@@ -16669,8 +16542,7 @@ function isReservedPrefix(ruleId) {
16669
16542
  const upper = ruleId.toUpperCase();
16670
16543
  return RESERVED_PREFIXES.some((p) => upper.startsWith(p));
16671
16544
  }
16672
- //#endregion
16673
- //#region src/plugins/load.ts
16545
+ // src/plugins/load.ts
16674
16546
  /**
16675
16547
  * Plugin API (Upgrade-Plan-v3 Phase 6, tier-2 item #1).
16676
16548
  *
@@ -16776,8 +16648,7 @@ function loadPlugins(root, gateOpen = false) {
16776
16648
  }
16777
16649
  return result;
16778
16650
  }
16779
- //#endregion
16780
- //#region src/plugins/local-rules.ts
16651
+ // src/plugins/local-rules.ts
16781
16652
  /**
16782
16653
  * Local plugin/rule contract (Verification Trust Evolution Plan §18 —
16783
16654
  * Local Extensibility).
@@ -17089,8 +16960,7 @@ async function loadModuleRules(path, result) {
17089
16960
  result.rules.push(rule);
17090
16961
  }
17091
16962
  }
17092
- //#endregion
17093
- //#region src/plugins/trust-gate.ts
16963
+ // src/plugins/trust-gate.ts
17094
16964
  /**
17095
16965
  * Plugin trust gate (audit-remediation-master-plan.md, locked decision C2).
17096
16966
  *
@@ -17126,8 +16996,7 @@ function renderGateNotice(skipped) {
17126
16996
  lines.push("To run them, re-run with --enable-plugins (or set MJOLNIR_ENABLE_PLUGINS=1).", "Declarative JSON rule manifests (mjolnir-rules/*.json) are unaffected — they execute no code.");
17127
16997
  return lines.join("\n");
17128
16998
  }
17129
- //#endregion
17130
- //#region src/engine/scan-pipeline.ts
16999
+ // src/engine/scan-pipeline.ts
17131
17000
  /**
17132
17001
  * Canonical scan pipeline (M6 — blueprint §9.2).
17133
17002
  *
@@ -17205,9 +17074,10 @@ async function buildUniversalRules$1(root, strict, opts = {}) {
17205
17074
  for (const p of plugins) for (const r of p.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
17206
17075
  for (const r of local.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
17207
17076
  let rules = [...UNIVERSAL_RULES, ...externalRules];
17208
- const withheldRuleIds = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
17077
+ const quarantined = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
17078
+ const withheldRuleIds = strict ? [] : quarantined;
17209
17079
  if (!strict) rules = rules.filter((r) => tierByRuleId.get(r.id) !== "quarantine");
17210
- const rulesWithheld = strict ? 0 : withheldRuleIds.length;
17080
+ const rulesWithheld = withheldRuleIds.length;
17211
17081
  const rulesApplied = rules.length;
17212
17082
  const pluginMeta = [...plugins.map((p) => ({
17213
17083
  name: p.name,
@@ -17604,12 +17474,28 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
17604
17474
  const runtimeIncomplete = discoveredReport !== void 0 && discoveredReport.report.analysisComplete !== true;
17605
17475
  let forensicVerdicts;
17606
17476
  let evidenceRecords = [];
17607
- if (discoveredReport && discoveredReport.report.analysisComplete === true) try {
17608
- evidenceRecords = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
17609
- stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
17610
- forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
17611
- } catch {
17612
- evidenceRecords = [];
17477
+ if (discoveredReport && discoveredReport.report.analysisComplete === true) {
17478
+ const before = findings.map((f) => ({
17479
+ runtimeCorroboration: f.runtimeCorroboration,
17480
+ trustLevel: f.trustLevel
17481
+ }));
17482
+ try {
17483
+ const records = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
17484
+ stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
17485
+ forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
17486
+ evidenceRecords = records;
17487
+ } catch {
17488
+ findings.forEach((f, i) => {
17489
+ const prev = before[i];
17490
+ if (prev?.runtimeCorroboration === void 0) delete f.runtimeCorroboration;
17491
+ else f.runtimeCorroboration = prev.runtimeCorroboration;
17492
+ if (prev?.trustLevel === void 0) delete f.trustLevel;
17493
+ else f.trustLevel = prev.trustLevel;
17494
+ });
17495
+ forensicVerdicts = void 0;
17496
+ evidenceRecords = [];
17497
+ recordDegradation("runtime-corroboration-failed");
17498
+ }
17613
17499
  }
17614
17500
  return {
17615
17501
  evidenceRecords,
@@ -18042,8 +17928,7 @@ async function runScanInProject(args, hooks = {}) {
18042
17928
  resetTsMorphProject();
18043
17929
  return result;
18044
17930
  }
18045
- //#endregion
18046
- //#region src/commands/help.ts
17931
+ // src/commands/help.ts
18047
17932
  /** Ordered registry — grouped by the same categories the overview uses. */
18048
17933
  const HELP_ENTRIES = [
18049
17934
  {
@@ -18482,8 +18367,7 @@ function renderRootHelp(schemaVersion = 1, options = {}) {
18482
18367
  lines.push(`Docs: ${DOCS_URL} (JSON schemaVersion ${schemaVersion}, additive-only)`);
18483
18368
  return lines.join("\n");
18484
18369
  }
18485
- //#endregion
18486
- //#region src/integrations/sentry.ts
18370
+ // src/integrations/sentry.ts
18487
18371
  /**
18488
18372
  * Sentry crash reporting — opt-in, and inert unless a DSN is configured.
18489
18373
  *
@@ -18609,8 +18493,7 @@ async function flushSentry(timeoutMs = FLUSH_TIMEOUT_MS) {
18609
18493
  return false;
18610
18494
  }
18611
18495
  }
18612
- //#endregion
18613
- //#region src/integrations/ci-install.ts
18496
+ // src/integrations/ci-install.ts
18614
18497
  /**
18615
18498
  * CI integration (Sprint-Plan W7): generates .github/workflows/mjolnir.yml
18616
18499
  * from internal templates ONLY — no user-input interpolation (R3 supply-chain).
@@ -18697,10 +18580,12 @@ function publishedVersionForInstall(startDir = import.meta.dirname) {
18697
18580
  return ENGINE_VERSION;
18698
18581
  }
18699
18582
  /**
18700
- * Template v2 (Terminal + CI UX Overhaul plan, M4): the summary step
18701
- * calls `mjolnir --format github-summary mjolnir.json` — annotations +
18702
- * step summary via
18703
- * ONE emitter — instead of the v1 inline SUMMARY_SCRIPT. The gate
18583
+ * Template v3: the reporting steps render the saved report with
18584
+ * `mjolnir mjolnir.json --format github-summary` (annotations + step summary
18585
+ * via ONE emitter) and `--format pr-comment`. Template v2, generated through
18586
+ * 6.0.0, called `summary` and `pr-comment --from`, the verbs those formats
18587
+ * replaced, so its reporting steps exited 10 and, being `continue-on-error`,
18588
+ * did so silently: no annotations and no comment, on every run. The gate
18704
18589
  * script is unchanged (reads `partial`, `findings[].severity`).
18705
18590
  */
18706
18591
  /** Renders findings into `$GITHUB_STEP_SUMMARY`; tolerates a missing scan result.
@@ -18725,12 +18610,48 @@ const SUMMARY_SCRIPT_V1 = [
18725
18610
  "}",
18726
18611
  "process.stdout.write(lines.length + \" finding(s)\\n\");"
18727
18612
  ].join("\n");
18728
- /** True when a workflow was generated by any Mjölnir template (v1 or v2).
18613
+ /** True when a workflow was generated by any Mjölnir template (v1, v2 or v3).
18614
+ *
18615
+ * The comparison runs on the canonical form, so a workflow generated by an
18616
+ * earlier release still matches. It used to be byte equality with the current
18617
+ * template, and the pinned package version alone made every file from the
18618
+ * previous release read as hand-customised: an upgrade needed `--force`, the
18619
+ * one flag that also overwrites real customisations.
18620
+ *
18729
18621
  * The v1 needle is matched in its INDENTED form: the v1 template embedded
18730
18622
  * the script via indentBlock(…, 10) inside the `run: |` scalar, so the
18731
18623
  * raw unindented substring never appears in a real v1 file. */
18732
18624
  function isKnownTemplate(content) {
18733
- return GATES.some((g) => content === TEMPLATE(g)) || GATES.some((g) => content === ACTION_TEMPLATE(g)) || content.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
18625
+ const file = canonicalWorkflow(content);
18626
+ return GATES.some((g) => {
18627
+ const npx = canonicalWorkflow(TEMPLATE(g));
18628
+ const action = canonicalWorkflow(ACTION_TEMPLATE(g));
18629
+ return file === npx || file === templateV2(npx) || file === action || file === actionTemplateWithoutVersion(action);
18630
+ }) || file.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
18631
+ }
18632
+ /**
18633
+ * A generated workflow with the parts that legitimately change between
18634
+ * releases blanked: line endings, the pinned package version (in the npx
18635
+ * tarball and in the Action's `version:` input) and the pinned Action SHA.
18636
+ */
18637
+ function canonicalWorkflow(text) {
18638
+ return text.replace(/\r\n/g, "\n").replace(/mjolnir-qa-[0-9A-Za-z.+-]+?\.tgz/g, "mjolnir-qa-<version>.tgz").replace(/(\n {10}version: )"[^"\n]*"/g, "$1\"<version>\"").replace(/Sergey-Bar\/Mjolnir@[0-9a-f]{40}/g, "Sergey-Bar/Mjolnir@<sha>");
18639
+ }
18640
+ /**
18641
+ * The action template through 6.0.0, which passed no `version:` and so ran
18642
+ * whatever its pinned Action defaulted to.
18643
+ */
18644
+ function actionTemplateWithoutVersion(canonical) {
18645
+ return canonical.replace("\n version: \"<version>\"", "");
18646
+ }
18647
+ /**
18648
+ * Template v2, the npx template through 6.0.0, rebuilt from v3: the same file
18649
+ * with its two reporting steps calling the verbs the formats replaced. A v2
18650
+ * workflow is recognised because it is broken, so `ci install` has to be able
18651
+ * to replace it without `--force`.
18652
+ */
18653
+ function templateV2(v3) {
18654
+ return v3.replace(".tgz mjolnir.json --format github-summary\n", ".tgz summary mjolnir.json\n").replace(".tgz mjolnir.json --format pr-comment > mjolnir-comment.md\n", ".tgz pr-comment --from mjolnir.json > mjolnir-comment.md\n");
18734
18655
  }
18735
18656
  /** Indents an embedded script so it sits inside a YAML `run: |` block scalar. */
18736
18657
  function indentBlock(text, spaces) {
@@ -18773,11 +18694,11 @@ jobs:
18773
18694
  - name: Annotations + Job Summary
18774
18695
  if: always()
18775
18696
  continue-on-error: true
18776
- run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz summary mjolnir.json
18697
+ run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format github-summary
18777
18698
  - name: Render PR comment
18778
18699
  if: always()
18779
18700
  continue-on-error: true
18780
- run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz pr-comment --from mjolnir.json > mjolnir-comment.md
18701
+ run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format pr-comment > mjolnir-comment.md
18781
18702
  - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
18782
18703
  if: always()
18783
18704
  with:
@@ -18846,12 +18767,22 @@ const GATES = [
18846
18767
  "warning"
18847
18768
  ];
18848
18769
  /**
18849
- * The action-ref the action-based template pins (P1: distribution).
18850
- * The current `v3` major is maintained on every stable release by the
18851
- * dedicated Action tag workflow. The generated workflow pins an immutable
18852
- * Action SHA, never @latest.
18770
+ * The Action the action-based template pins: a commit on `main`, by SHA,
18771
+ * because a generated workflow must never resolve a mutable ref.
18772
+ *
18773
+ * It is the merge commit of #740, the first commit on `main` whose
18774
+ * `action.yml` renders the saved report with `--format pr-comment` and
18775
+ * `--format github-summary`. The pin it replaced, 4a588bc, was a pull-request
18776
+ * branch commit that `main` never contained: its `action.yml` installed
18777
+ * `mjolnir-qa@latest` and called the retired `trust-report` verb, and every
18778
+ * workflow `ci install` generated ran it. A pin can only name a commit that
18779
+ * already exists, so it moves in the change AFTER the Action it points at.
18780
+ *
18781
+ * The template passes `version:` because the Action's own default is the
18782
+ * published stable AT THIS COMMIT, which may be an older CLI than the one that
18783
+ * generated the workflow.
18853
18784
  */
18854
- const ACTION_REF = "Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b";
18785
+ const ACTION_REF = "Sergey-Bar/Mjolnir@6bf46a0e64e2f3ee16266d8456d1b16e5ae16214";
18855
18786
  /**
18856
18787
  * The action-based workflow for one gate level (P1.3): the root
18857
18788
  * action.yml does checkout-independent scanning — setup-node, the scan
@@ -18874,8 +18805,11 @@ const ACTION_REF = "Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b"
18874
18805
  * finish is not a pass — see the exit-code contract in docs/VERSIONING.md.
18875
18806
  * 3. `version:` was pinned to the engine's working version, which while the
18876
18807
  * candidate is a release candidate is not on npm, so the generated
18877
- * workflow could not install what it asked for. The input is omitted now
18878
- * and the Action's own default (the published stable) applies.
18808
+ * workflow could not install what it asked for. It was then omitted,
18809
+ * which handed the choice to the pinned Action's default, the stable that
18810
+ * was published when that commit was made. It now names the published
18811
+ * version the installing package resolves, the same pin the npx template
18812
+ * uses.
18879
18813
  */
18880
18814
  const ACTION_TEMPLATE = (gate) => `name: Mjölnir
18881
18815
 
@@ -18905,6 +18839,7 @@ jobs:
18905
18839
  continue-on-error: true
18906
18840
  uses: ${ACTION_REF}
18907
18841
  with:
18842
+ version: "${INSTALL_VERSION}"
18908
18843
  scope: changed
18909
18844
  format: json
18910
18845
  fail-on: ${gate === "advisory" ? "none" : gate}
@@ -19015,8 +18950,7 @@ function ciInstall(root, gate, options = {}) {
19015
18950
  diffSummary: []
19016
18951
  };
19017
18952
  }
19018
- //#endregion
19019
- //#region src/cli-io.ts
18953
+ // src/cli-io.ts
19020
18954
  const out = (...parts) => console.log(parts.map(String).join(" "));
19021
18955
  const err = (...parts) => console.error(parts.map(String).join(" "));
19022
18956
  function internalErrorMessage(err, emit, debug) {
@@ -19185,8 +19119,7 @@ function buildMachineContract(result) {
19185
19119
  ...result.forensicVerdicts !== void 0 ? { forensicVerdicts: result.forensicVerdicts } : {}
19186
19120
  };
19187
19121
  }
19188
- //#endregion
19189
- //#region src/commands/fixture-example.ts
19122
+ // src/commands/fixture-example.ts
19190
19123
  /**
19191
19124
  * Shared example-fixture selection for `mjolnir explain` (explain.ts)
19192
19125
  * and the generated rule docs (rule-docs.ts) — one chooser so both
@@ -19204,8 +19137,7 @@ function firstFixtureFile(dir) {
19204
19137
  const entries = readdirSync(dir).filter((f) => !f.startsWith(".")).sort();
19205
19138
  return entries.length > 0 ? join(dir, entries[0]) : null;
19206
19139
  }
19207
- //#endregion
19208
- //#region src/commands/explain.ts
19140
+ // src/commands/explain.ts
19209
19141
  /**
19210
19142
  * `mjolnir explain <RULE-ID>` — implements Plan.md Sprint 1.3
19211
19143
  * (Master-Stabilization-Plan Sprint 5, Task 19).
@@ -19546,8 +19478,7 @@ function renderVerdictExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
19546
19478
  function pctOf(v) {
19547
19479
  return `${Math.round(v * 100)}%`;
19548
19480
  }
19549
- //#endregion
19550
- //#region src/engine/resolution.ts
19481
+ // src/engine/resolution.ts
19551
19482
  /** Correlation identity (v1-compatible): ruleId\0file\0message. */
19552
19483
  const fingerprint$1 = findingFingerprint;
19553
19484
  /**
@@ -19612,8 +19543,7 @@ function resolve$1(input) {
19612
19543
  comparedAgainst
19613
19544
  };
19614
19545
  }
19615
- //#endregion
19616
- //#region src/commands/baseline.ts
19546
+ // src/commands/baseline.ts
19617
19547
  /**
19618
19548
  * A shared finding-set READER. Neither verb it used to serve survives the v6 CLI collapse (plan §3): `baseline` and `diff` are both retired, and the capability they shared is now `mjolnir scan --scope changed`.
19619
19549
  * (Master-Stabilization-Plan.md).
@@ -19767,8 +19697,7 @@ function renderBaselineSaved(path, count, replaced) {
19767
19697
  lines.push(nextStep("mjolnir scan --scope changed", ui$8) + " — see only what's new.");
19768
19698
  return lines.join("\n");
19769
19699
  }
19770
- //#endregion
19771
- //#region src/commands/verify.ts
19700
+ // src/commands/verify.ts
19772
19701
  /**
19773
19702
  * `mjolnir ci verify` — the agent-loop digest (product-gap-remediation
19774
19703
  * master plan P7, plan 1788853205786 — flag agent, decision 6).
@@ -19875,8 +19804,7 @@ function renderVerifyDigest(digest) {
19875
19804
  }
19876
19805
  return lines.join("\n").trimEnd();
19877
19806
  }
19878
- //#endregion
19879
- //#region src/forensics/triage.ts
19807
+ // src/forensics/triage.ts
19880
19808
  const ui$7 = plainContext();
19881
19809
  const QUARANTINE_MIN_ATTEMPTS = 2;
19882
19810
  /** Deterministic triage table rows, worst first. */
@@ -20023,8 +19951,7 @@ function renderTriageWorkflowJson(report) {
20023
19951
  rows: workflowRows(report)
20024
19952
  }, null, 2) + "\n";
20025
19953
  }
20026
- //#endregion
20027
- //#region src/commands/pw-report.ts
19954
+ // src/commands/pw-report.ts
20028
19955
  const ui$6 = plainContext();
20029
19956
  function summarizePwRun(report) {
20030
19957
  const slowest = [...report.verdicts].sort((a, b) => b.totalDurationMs - a.totalDurationMs).slice(0, 5).map((v) => ({
@@ -20058,8 +19985,7 @@ function renderPwRunSummary(s) {
20058
19985
  }
20059
19986
  return lines.join("\n");
20060
19987
  }
20061
- //#endregion
20062
- //#region src/mcp/server.ts
19988
+ // src/mcp/server.ts
20063
19989
  /**
20064
19990
  * MCP stdio transport (blueprint §21 — Phase 4, PURE TRANSPORT).
20065
19991
  *
@@ -20077,7 +20003,10 @@ function renderPwRunSummary(s) {
20077
20003
  * - parameter size caps; ONE scan in flight (serialized queue) reusing
20078
20004
  * the pipeline's own budgets; zero network (no fetches anywhere); no
20079
20005
  * shell execution beyond Mjölnir's own verbs.
20080
- * - filesystem boundary = scan target + `.mjolnir/` (same as CLI).
20006
+ * - filesystem boundary: the tools are read-only, and with no further
20007
+ * configuration they accept any path the parent process passes (the
20008
+ * parent is the trust boundary). `MJOLNIR_WORKSPACE_ROOT` confines every
20009
+ * path argument to that directory, symlinks resolved.
20081
20010
  * - the plugin trust gate applies unchanged (--enable-plugins is NOT
20082
20011
  * exposed over MCP: tools run with the gate CLOSED unless the server
20083
20012
  * was started with MJOLNIR_ENABLE_PLUGINS=1 in its own environment).
@@ -20087,12 +20016,27 @@ const MAX_PARAM_BYTES = 65536;
20087
20016
  const MAX_DURATION_MS$1 = 36e5;
20088
20017
  const DEFAULT_MAX_DURATION_MS$1 = 6e5;
20089
20018
  const PROTOCOL_VERSION = "2025-06-18";
20090
- const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? resolve(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
20019
+ /** The real path of `p`, or its lexical resolution when it does not exist. */
20020
+ function realOrResolved(p) {
20021
+ try {
20022
+ return realpathSync.native(resolve(p));
20023
+ } catch {
20024
+ return resolve(p);
20025
+ }
20026
+ }
20027
+ /**
20028
+ * The optional workspace root. When set, every path argument must resolve
20029
+ * INSIDE it after symlinks are followed.
20030
+ *
20031
+ * Unset is open by design and is documented that way in the header: the MCP
20032
+ * tools are read-only and the stdio parent is the trust boundary. Setting
20033
+ * `MJOLNIR_WORKSPACE_ROOT` is how a host confines them.
20034
+ */
20035
+ const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? realOrResolved(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
20091
20036
  function isPathAllowed(path) {
20092
20037
  if (!CONFIGURED_ROOT) return true;
20093
- const candidate = resolve(path);
20094
- const rel = relative(CONFIGURED_ROOT, candidate);
20095
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
20038
+ const rel = relative(CONFIGURED_ROOT, realOrResolved(path));
20039
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
20096
20040
  }
20097
20041
  /** JSON-RPC error codes (subset used here). */
20098
20042
  const MCP_ERRORS = {
@@ -20569,8 +20513,7 @@ async function handleMcpMessage(msg) {
20569
20513
  }
20570
20514
  };
20571
20515
  }
20572
- //#endregion
20573
- //#region src/mcp/transport.ts
20516
+ // src/mcp/transport.ts
20574
20517
  /**
20575
20518
  * MCP stdio transport loop (blueprint §21 — the framing around the
20576
20519
  * pure-transport server).
@@ -20667,8 +20610,7 @@ async function handleStdioLine(line, write) {
20667
20610
  }
20668
20611
  write(`${JSON.stringify(response)}\n`);
20669
20612
  }
20670
- //#endregion
20671
- //#region src/scorer/prioritize.ts
20613
+ // src/scorer/prioritize.ts
20672
20614
  /**
20673
20615
  * "Fix this first" prioritization (Master-Stabilization-Plan Sprint 5,
20674
20616
  * Task 20).
@@ -20718,8 +20660,7 @@ function prioritize(findings) {
20718
20660
  function topFixes(findings, n = 3) {
20719
20661
  return prioritize(findings).filter((p) => p.scoreGain > 0).slice(0, n);
20720
20662
  }
20721
- //#endregion
20722
- //#region src/reporter/art.ts
20663
+ // src/reporter/art.ts
20723
20664
  /**
20724
20665
  * ASCII art assets for the Mjölnir terminal experience.
20725
20666
  * Minimal Nordic / engineering aesthetic — professional, not fantasy.
@@ -20756,8 +20697,7 @@ const TROPHY = String.raw`
20756
20697
  "─".repeat(58);
20757
20698
  /** Wordmark for the 100-state celebration block. */
20758
20699
  const FORGED_WORDMARK = "⚡ F O R G E D ⚡";
20759
- //#endregion
20760
- //#region src/reporter/tone-blunt.ts
20700
+ // src/reporter/tone-blunt.ts
20761
20701
  /**
20762
20702
  * Static blunt messages keyed by rule ID. A simple string lookup
20763
20703
  * (no per-entry closures) so coverage is not artificially fragmented
@@ -20829,8 +20769,7 @@ function bluntMessage(f) {
20829
20769
  if (msg !== void 0) return msg;
20830
20770
  return `${f.message} — fix it or suppress it; ignoring it is worse than either.`;
20831
20771
  }
20832
- //#endregion
20833
- //#region src/reporter/terminal.ts
20772
+ // src/reporter/terminal.ts
20834
20773
  /** Non-verbose finding cards shown before the overflow line. The
20835
20774
  * JSON/SARIF contract always carries ALL findings — this cap is a
20836
20775
  * terminal display concern only. */
@@ -21312,8 +21251,7 @@ function countBySeverity(result) {
21312
21251
  }
21313
21252
  return counts;
21314
21253
  }
21315
- //#endregion
21316
- //#region src/engine/trust-classification.ts
21254
+ // src/engine/trust-classification.ts
21317
21255
  /**
21318
21256
  * Completeness, decided once.
21319
21257
  *
@@ -21419,8 +21357,7 @@ function classifyTrust(result, summary) {
21419
21357
  gaps
21420
21358
  };
21421
21359
  }
21422
- //#endregion
21423
- //#region src/lib/format.ts
21360
+ // src/lib/format.ts
21424
21361
  /**
21425
21362
  * Shared formatting utilities. Single definition site for `pct()` (Task 20)
21426
21363
  * — the two callers (commands/trust-report.ts, reporter/trust-report.ts)
@@ -21429,8 +21366,7 @@ function classifyTrust(result, summary) {
21429
21366
  function pct$1(v) {
21430
21367
  return `${Math.round(v * 100)}%`;
21431
21368
  }
21432
- //#endregion
21433
- //#region src/reporter/trust-report.ts
21369
+ // src/reporter/trust-report.ts
21434
21370
  /** The one label rule, so no renderer grows a second opinion. */
21435
21371
  function gateLabel(f, gate) {
21436
21372
  return isAtGate(f, gate, isAdvisoryFinding) ? "GATE" : "WARN";
@@ -21571,8 +21507,7 @@ function renderTrustReport(result, opts) {
21571
21507
  lines.push("");
21572
21508
  return lines.join("\n");
21573
21509
  }
21574
- //#endregion
21575
- //#region src/reporter/sarif.ts
21510
+ // src/reporter/sarif.ts
21576
21511
  /**
21577
21512
  * SARIF fingerprint values must be printable and stable. The engine's identity
21578
21513
  * strings join their parts with NUL (a separator no repository path or message
@@ -21684,6 +21619,10 @@ function renderSarif(result, repoRootUri) {
21684
21619
  }
21685
21620
  }))
21686
21621
  };
21622
+ const srcRoot = (uri) => {
21623
+ const encoded = uri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/");
21624
+ return encoded.endsWith("/") ? encoded : `${encoded}/`;
21625
+ };
21687
21626
  const sarif = {
21688
21627
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
21689
21628
  version: [
@@ -21693,13 +21632,12 @@ function renderSarif(result, repoRootUri) {
21693
21632
  ].join("."),
21694
21633
  runs: [repoRootUri ? {
21695
21634
  ...run,
21696
- originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
21635
+ originalUriBaseIds: { SRCROOT: { uri: srcRoot(repoRootUri) } }
21697
21636
  } : run]
21698
21637
  };
21699
21638
  return JSON.stringify(sarif, null, 2);
21700
21639
  }
21701
- //#endregion
21702
- //#region src/reporter/codequality.ts
21640
+ // src/reporter/codequality.ts
21703
21641
  function codeQualitySeverity(severity) {
21704
21642
  if (severity === "error") return "major";
21705
21643
  if (severity === "warning") return "minor";
@@ -21718,8 +21656,7 @@ function renderCodeQuality(result) {
21718
21656
  }));
21719
21657
  return JSON.stringify(issues, null, 2) + "\n";
21720
21658
  }
21721
- //#endregion
21722
- //#region src/reporter/mermaid.ts
21659
+ // src/reporter/mermaid.ts
21723
21660
  /**
21724
21661
  * `--format mermaid` — test-architecture diagram (Sprint 9 Task 38,
21725
21662
  * Master-Stabilization-Plan.md). Ranked first among the delight
@@ -21818,8 +21755,7 @@ function renderMermaid(result) {
21818
21755
  for (const { id, cls } of styleAssignments) lines.push(` class ${id} ${cls};`);
21819
21756
  return lines.join("\n");
21820
21757
  }
21821
- //#endregion
21822
- //#region src/reporter/progress.ts
21758
+ // src/reporter/progress.ts
21823
21759
  /**
21824
21760
  * Live scan progress (Terminal + CI UX Overhaul plan, M3).
21825
21761
  *
@@ -21918,8 +21854,7 @@ var ProgressRenderer = class {
21918
21854
  this.erase();
21919
21855
  }
21920
21856
  };
21921
- //#endregion
21922
- //#region src/lib/git-utils.ts
21857
+ // src/lib/git-utils.ts
21923
21858
  /**
21924
21859
  * Shared git utilities (Task 18). The `currentCommit` / `targetCommit`
21925
21860
  * pattern was duplicated between cli.ts and commands/trust-report.ts.
@@ -21952,8 +21887,7 @@ function currentCommit(root, opts = {}) {
21952
21887
  return opts.nullable ? null : "unknown";
21953
21888
  }
21954
21889
  }
21955
- //#endregion
21956
- //#region src/commands/report-io.ts
21890
+ // src/commands/report-io.ts
21957
21891
  /**
21958
21892
  * Shared saved-report loading (agent-handoff plan §9.0).
21959
21893
  *
@@ -22146,8 +22080,7 @@ function loadSavedReport(reportPath) {
22146
22080
  function reportExists(reportPath) {
22147
22081
  return existsSync(reportPath);
22148
22082
  }
22149
- //#endregion
22150
- //#region src/commands/trust-report.ts
22083
+ // src/commands/trust-report.ts
22151
22084
  function buildArtifactIdentity(result, commit) {
22152
22085
  const revisions = /* @__PURE__ */ new Map();
22153
22086
  const levels = /* @__PURE__ */ new Map();
@@ -22173,8 +22106,7 @@ function buildArtifactIdentity(result, commit) {
22173
22106
  }
22174
22107
  };
22175
22108
  }
22176
- //#endregion
22177
- //#region src/reporter/pr-report-actions.ts
22109
+ // src/reporter/pr-report-actions.ts
22178
22110
  /** GitHub-native actions. Social drafts contain only public project information. */
22179
22111
  const PROJECT_URL = "https://github.com/Sergey-Bar/Mjolnir";
22180
22112
  const SHARE_URL = "https://sergey-bar.github.io/Mjolnir/";
@@ -22293,8 +22225,7 @@ const PR_BRAND_CONTRACT = {
22293
22225
  terminology: "trust-engine",
22294
22226
  footerFormat: "scanId + generatedBy"
22295
22227
  };
22296
- //#endregion
22297
- //#region src/reporter/pr-report-shared.ts
22228
+ // src/reporter/pr-report-shared.ts
22298
22229
  const UNIFIED_MARKER = "<!-- mjolnir-report:v2 -->";
22299
22230
  /**
22300
22231
  * The one place a comment asset URL is written down.
@@ -22698,8 +22629,7 @@ function enforceCommentLimit(lines) {
22698
22629
  }
22699
22630
  return render() + notice;
22700
22631
  }
22701
- //#endregion
22702
- //#region src/commands/pr-comment.ts
22632
+ // src/commands/pr-comment.ts
22703
22633
  /**
22704
22634
  * Escape markdown-significant characters (markdown spec).
22705
22635
  * Kept exported for downstream consumer compatibility.
@@ -22721,8 +22651,7 @@ function renderPrComment(result, options = {}) {
22721
22651
  ...options.commit !== void 0 ? { commit: options.commit } : {}
22722
22652
  });
22723
22653
  }
22724
- //#endregion
22725
- //#region src/commands/why.ts
22654
+ // src/commands/why.ts
22726
22655
  /**
22727
22656
  * `mjolnir explain <file:line> <file>:<line>` — occurrence-level evidence/explanation
22728
22657
  * query (agent-handoff plan M2).
@@ -22894,8 +22823,7 @@ async function runWhyCommand(argv, io = {
22894
22823
  }));
22895
22824
  return filtered.length > 0 ? 0 : 1;
22896
22825
  }
22897
- //#endregion
22898
- //#region src/commands/handover.ts
22826
+ // src/commands/handover.ts
22899
22827
  const ui$5 = plainContext();
22900
22828
  const FAKE_GREEN_RULES = /* @__PURE__ */ new Set([
22901
22829
  "QA-TEST-003",
@@ -22983,8 +22911,7 @@ function renderHandover(map) {
22983
22911
  lines.push("Generated by mjolnir explain --plan — re-run after big refactors.");
22984
22912
  return lines.join("\n");
22985
22913
  }
22986
- //#endregion
22987
- //#region src/commands/stats.ts
22914
+ // src/commands/stats.ts
22988
22915
  /**
22989
22916
  * `mjolnir stats` — Sprint 6 Task 26 (Master-Stabilization-Plan.md).
22990
22917
  *
@@ -23104,14 +23031,72 @@ function renderStats(stats) {
23104
23031
  lines.push("different machine — this file is local-only, by design.");
23105
23032
  return lines.join("\n");
23106
23033
  }
23107
- //#endregion
23108
- //#region src/reporter/github.ts
23034
+ // src/reporter/github.ts
23035
+ /** Escape a workflow-command PROPERTY value. */
23036
+ function escapeAnnotationProperty(value) {
23037
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A").replaceAll(":", "%3A").replaceAll(",", "%2C");
23038
+ }
23039
+ /** Escape a workflow-command MESSAGE. */
23040
+ function escapeAnnotationMessage(value) {
23041
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
23042
+ }
23109
23043
  /** ANSI-injection guard for the summary path (belt + braces). */
23110
23044
  function stripAnsiForSummary(value) {
23111
23045
  return value.replace(/\x1b\[[0-9;:?]*[ -/]*[@-~]/g, "");
23112
23046
  }
23113
- //#endregion
23114
- //#region src/commands/summary.ts
23047
+ const SEVERITY_TO_COMMAND = {
23048
+ error: "error",
23049
+ warning: "warning",
23050
+ info: "notice"
23051
+ };
23052
+ /** Render ONE workflow-command annotation line (no trailing newline). */
23053
+ function renderAnnotation(a) {
23054
+ const props = [`file=${escapeAnnotationProperty(a.file)}`, `line=${Math.max(1, a.line ?? 1)}`];
23055
+ if (a.column !== void 0) props.push(`column=${Math.max(1, a.column)}`);
23056
+ props.push(`title=${escapeAnnotationProperty(a.title)}`);
23057
+ return `::${SEVERITY_TO_COMMAND[a.severity]} ${props.join(",")}::${escapeAnnotationMessage(a.message)}`;
23058
+ }
23059
+ /** Render annotations for every finding in a report's findings array. */
23060
+ function renderAnnotations(findings) {
23061
+ return findings.map((f) => {
23062
+ return renderAnnotation({
23063
+ severity: f.severity === "error" || f.severity === "warning" ? f.severity : "info",
23064
+ file: f.file,
23065
+ line: f.line,
23066
+ column: f.column,
23067
+ title: f.ruleId,
23068
+ message: f.message
23069
+ });
23070
+ });
23071
+ }
23072
+ /** GitHub caps annotation messages — keep the head, point at the summary. */
23073
+ function truncateMessage(message, max = 250) {
23074
+ if (message.length <= max) return message;
23075
+ return `${message.slice(0, max)}… (full text in the step summary)`;
23076
+ }
23077
+ // src/commands/summary.ts
23078
+ /**
23079
+ * the `--format github-summary` render — CI annotations + step summary
23080
+ * (Terminal + CI UX Overhaul plan, M4).
23081
+ *
23082
+ * Additive command; default report path `mjolnir.json`. Reads a saved
23083
+ * ScanResult JSON (the `--json` scan output) and emits:
23084
+ * 1. GitHub annotations to stdout, only when GITHUB_ACTIONS=true —
23085
+ * one per finding, via the single github.ts emitter.
23086
+ * 2. A step-summary markdown document to $GITHUB_STEP_SUMMARY when
23087
+ * set (else stdout; --stdout forces stdout): score + band, text
23088
+ * score bar, dimensions table, top deductions, collapsible
23089
+ * per-severity details with fix lines, and an honesty notice for
23090
+ * partial/no-score reports.
23091
+ *
23092
+ * Exit codes (frozen contract): 0 on success — this command NEVER
23093
+ * blocks; the gate step decides. 10 missing file argument. 2
23094
+ * unreadable/invalid JSON (a data problem; the message explains).
23095
+ *
23096
+ * Paths: findings are scan-target-relative. The generated workflow
23097
+ * scans the repo root, so the default output is directly usable;
23098
+ * `--path-prefix <dir>` re-scopes for subdirectory scans.
23099
+ */
23115
23100
  const DETAILS_PER_SEVERITY_CAP = 25;
23116
23101
  /**
23117
23102
  * The score bar. BW-105: the canonical gauge with the inert palette — the
@@ -23205,8 +23190,92 @@ function renderStepSummary(result, options = {}) {
23205
23190
  lines.push("<!-- mjolnir-honesty: scores derive from rules with published evidence levels and measured false-positive rates where available. -->");
23206
23191
  return lines.join("\n");
23207
23192
  }
23208
- //#endregion
23209
- //#region src/commands/milestone-args.ts
23193
+ /** Render one finding as an escaped annotation line. Fields are
23194
+ * sanitized with the same sanitizeData layer the terminal uses
23195
+ * (github.ts's escapers cover %/CR/LF, not OSC/C0), then %/CR/LF-escaped. */
23196
+ function annotationForFinding(f) {
23197
+ return renderAnnotations([{
23198
+ ...f,
23199
+ file: sanitizeData(f.file),
23200
+ ruleId: sanitizeData(f.ruleId),
23201
+ message: truncateMessage(stripAnsiForSummary(sanitizeData(f.message)))
23202
+ }])[0];
23203
+ }
23204
+ const KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
23205
+ "--stdout",
23206
+ "--path-prefix",
23207
+ "--help",
23208
+ "-h"
23209
+ ]);
23210
+ /**
23211
+ * Testable summary command core. Returns the process exit code.
23212
+ * Streams: annotations → stdout (always; GitHub greps them), summary →
23213
+ * step-summary file when set unless --stdout. Unknown flags are a
23214
+ * usage error (exit 10) — a typo'd --stdout must not silently route
23215
+ * the summary to $GITHUB_STEP_SUMMARY.
23216
+ */
23217
+ function runSummaryCommand(argv, io = {
23218
+ out: (line) => console.log(line),
23219
+ err: (line) => console.error(line)
23220
+ }) {
23221
+ for (let i = 0; i < argv.length; i++) {
23222
+ const a = argv[i] ?? "";
23223
+ if (!a.startsWith("-")) continue;
23224
+ if (a === "--path-prefix") {
23225
+ const val = argv[i + 1];
23226
+ if (val === void 0 || val.startsWith("-")) {
23227
+ io.err(usageErrorMessage({
23228
+ flag: "--path-prefix",
23229
+ token: val
23230
+ }));
23231
+ return 10;
23232
+ }
23233
+ i++;
23234
+ continue;
23235
+ }
23236
+ if (KNOWN_SUMMARY_FLAGS.has(a)) continue;
23237
+ io.err(usageErrorMessage({ token: a }));
23238
+ return 10;
23239
+ }
23240
+ const stdout = argv.includes("--stdout");
23241
+ const prefixIdx = argv.indexOf("--path-prefix");
23242
+ const pathPrefix = prefixIdx !== -1 ? argv[prefixIdx + 1] : void 0;
23243
+ const reportPath = argv.filter((a, i) => a !== void 0 && !a.startsWith("-") && (prefixIdx === -1 || i !== prefixIdx + 1))[0] ?? "mjolnir.json";
23244
+ if (!existsSync(reportPath)) {
23245
+ io.err(`mjolnir: report file not found: ${reportPath}`);
23246
+ io.err(" Run the scan with --json first: mjolnir --json > mjolnir.json");
23247
+ return 10;
23248
+ }
23249
+ let result;
23250
+ try {
23251
+ result = loadSavedReport(reportPath);
23252
+ } catch (err) {
23253
+ io.err(`mjolnir: cannot read ${reportPath}: ${errorMessage(err)}`);
23254
+ return 2;
23255
+ }
23256
+ const env = process.env;
23257
+ if (env["GITHUB_ACTIONS"] === "true") for (const f of result.findings) {
23258
+ const file = pathPrefix ? `${pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
23259
+ io.out(annotationForFinding({
23260
+ ...f,
23261
+ file
23262
+ }));
23263
+ }
23264
+ const summary = renderStepSummary(result, {
23265
+ stdout,
23266
+ ...pathPrefix !== void 0 ? { pathPrefix } : {}
23267
+ });
23268
+ const stepSummaryPath = stdout ? void 0 : env["GITHUB_STEP_SUMMARY"];
23269
+ if (stepSummaryPath) try {
23270
+ appendFileSync(stepSummaryPath, `${summary}\n`);
23271
+ } catch (err) {
23272
+ io.err(`mjolnir: could not write $GITHUB_STEP_SUMMARY (${errorMessage(err)}); printing to stdout instead.`);
23273
+ io.out(summary);
23274
+ }
23275
+ else io.out(summary);
23276
+ return 0;
23277
+ }
23278
+ // src/commands/milestone-args.ts
23210
23279
  /**
23211
23280
  * The argument layer every milestone command shares.
23212
23281
  *
@@ -23358,8 +23427,7 @@ function isWorkflowFile(path) {
23358
23427
  const normalized = path.replaceAll("\\", "/");
23359
23428
  return normalized.startsWith(".github/workflows/") || normalized === ".gitlab-ci.yml" || normalized === ".gitlab-ci.yaml" || basename(normalized) === "Jenkinsfile";
23360
23429
  }
23361
- //#endregion
23362
- //#region src/engine/cross-file.ts
23430
+ // src/engine/cross-file.ts
23363
23431
  /**
23364
23432
  * Cross-file analysis (Upgrade-Plan-v3 Phase 6, tier-3 item #2).
23365
23433
  *
@@ -23405,8 +23473,7 @@ function findDuplicateTestNames(files) {
23405
23473
  });
23406
23474
  return out.sort((a, b) => compareCodePoints(a.name, b.name));
23407
23475
  }
23408
- //#endregion
23409
- //#region src/engine/cross-file-analysis.ts
23476
+ // src/engine/cross-file-analysis.ts
23410
23477
  /**
23411
23478
  * Analyze cross-file signals across the scanned test files.
23412
23479
  * Pure function — same input always produces same output.
@@ -23544,8 +23611,7 @@ function renderCrossFileAnalysis(result) {
23544
23611
  }
23545
23612
  return lines.join("\n").trimEnd();
23546
23613
  }
23547
- //#endregion
23548
- //#region src/engine/evidence-graph.ts
23614
+ // src/engine/evidence-graph.ts
23549
23615
  /**
23550
23616
  * Build a complete evidence graph from a scan result.
23551
23617
  * Pure function — deterministic output for the same input.
@@ -23691,8 +23757,7 @@ function renderEvidenceGraphResult(result) {
23691
23757
  lines.push(` ${result.agenticProfile.note}`);
23692
23758
  return lines.join("\n").trimEnd();
23693
23759
  }
23694
- //#endregion
23695
- //#region src/frameworks/scorecard.ts
23760
+ // src/frameworks/scorecard.ts
23696
23761
  const SCORECARD_DIMENSIONS = [
23697
23762
  "discovery",
23698
23763
  "astUsage",
@@ -24802,8 +24867,7 @@ const FRAMEWORK_SCORECARDS = [
24802
24867
  makeScorecard("jenkins", FRAMEWORK_OVERRIDES["jenkins"]),
24803
24868
  makeScorecard("gitlab-ci", FRAMEWORK_OVERRIDES["gitlab-ci"])
24804
24869
  ];
24805
- //#endregion
24806
- //#region src/engine/framework-maturity.ts
24870
+ // src/engine/framework-maturity.ts
24807
24871
  /**
24808
24872
  * M5 — Framework Maturity Tracking for Playwright.
24809
24873
  *
@@ -24892,8 +24956,7 @@ function renderPlaywrightMaturityReport(report) {
24892
24956
  }
24893
24957
  return lines.join("\n").trimEnd();
24894
24958
  }
24895
- //#endregion
24896
- //#region src/engine/suppression-integrity.ts
24959
+ // src/engine/suppression-integrity.ts
24897
24960
  /**
24898
24961
  * ENGINE-006 — Suppression Integrity: detect suppression abuse,
24899
24962
  * unknown rules, expired entries, and compute a full integrity report.
@@ -24987,8 +25050,7 @@ function computeSuppressionIntegrity(suppressions, findings, knownRuleIds, now =
24987
25050
  expiredSuppressions: detectExpiredSuppressions(suppressions, now)
24988
25051
  };
24989
25052
  }
24990
- //#endregion
24991
- //#region src/engine/suppression-governance.ts
25053
+ // src/engine/suppression-governance.ts
24992
25054
  /**
24993
25055
  * M6 — Suppression Policy Governance Gate.
24994
25056
  *
@@ -25051,15 +25113,13 @@ function renderSuppressionGovernanceResult(result) {
25051
25113
  } else lines.push("All suppression policy checks passed.");
25052
25114
  return lines.join("\n").trimEnd();
25053
25115
  }
25054
- //#endregion
25055
- //#region src/adapters/gitlab-ci.ts
25116
+ // src/adapters/gitlab-ci.ts
25056
25117
  function parseGitLabCi(content) {
25057
25118
  const parsed = parse(content);
25058
25119
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("GitLab CI YAML is not a valid object.");
25059
25120
  return parsed;
25060
25121
  }
25061
- //#endregion
25062
- //#region src/engine/cli-command-names.ts
25122
+ // src/engine/cli-command-names.ts
25063
25123
  const CLI_COMMAND_NAMES = [
25064
25124
  "scan",
25065
25125
  "policy",
@@ -25079,8 +25139,7 @@ const CLI_COMMAND_NAMES = [
25079
25139
  "help"
25080
25140
  ];
25081
25141
  const CLI_NON_SCAN_COMMANDS = new Set(CLI_COMMAND_NAMES.filter((command) => command !== "scan"));
25082
- //#endregion
25083
- //#region src/engine/verification-intelligence.ts
25142
+ // src/engine/verification-intelligence.ts
25084
25143
  const CI_WORKFLOW_FILES = [
25085
25144
  ".github/workflows/ci.yml",
25086
25145
  ".github/workflows/test.yml",
@@ -25571,8 +25630,7 @@ function renderCIIntegrityReport(report) {
25571
25630
  }
25572
25631
  return lines.join("\n").trimEnd();
25573
25632
  }
25574
- //#endregion
25575
- //#region src/commands/milestone.ts
25633
+ // src/commands/milestone.ts
25576
25634
  async function runMilestoneScan(args, io, readSources, strict = false) {
25577
25635
  const target = resolve(args.target);
25578
25636
  const valid = validateTarget(target, io);
@@ -25804,8 +25862,7 @@ async function runCIIntegrityCommand(argv, io = {
25804
25862
  return milestoneError(error, io, argv.includes("--debug"));
25805
25863
  }
25806
25864
  }
25807
- //#endregion
25808
- //#region src/scorer/scoring-policy.ts
25865
+ // src/scorer/scoring-policy.ts
25809
25866
  /**
25810
25867
  * `mjolnir scan --policy` — the scoring policy, as the code states it.
25811
25868
  *
@@ -25867,8 +25924,7 @@ function renderScoringPolicy() {
25867
25924
  lines.push("Every number above is imported from src/scorer/scorer.ts at run time. A", "policy that cannot be read cannot be checked, and an uncheckable policy", "is the thing this product exists to catch.");
25868
25925
  return lines.join("\n");
25869
25926
  }
25870
- //#endregion
25871
- //#region src/commands/fix.ts
25927
+ // src/commands/fix.ts
25872
25928
  /**
25873
25929
  * `mjolnir fix` — Safe Auto-Fix with Proof (Tier 1 #3).
25874
25930
  *
@@ -26292,8 +26348,7 @@ function renderFixReport(results, dryRun) {
26292
26348
  lines.push(`${parts.join(" · ")}${dryRun ? " (dry-run)" : ""}. Every applied fix was verified against the rule that flagged it.`);
26293
26349
  return lines.join("\n");
26294
26350
  }
26295
- //#endregion
26296
- //#region src/commands/rules-catalog.ts
26351
+ // src/commands/rules-catalog.ts
26297
26352
  /**
26298
26353
  * `mjolnir explain --list` — machine-readable rule catalog with Trust Metadata.
26299
26354
  *
@@ -26510,8 +26565,7 @@ function countCompleteQuads(ruleIds, root = process.cwd()) {
26510
26565
  total: ruleIds.length
26511
26566
  };
26512
26567
  }
26513
- //#endregion
26514
- //#region src/frameworks/provider-capability-contract.ts
26568
+ // src/frameworks/provider-capability-contract.ts
26515
26569
  const CI_PROVIDER_CAPABILITY_SCHEMA = "m30.ci-provider-capabilities@1";
26516
26570
  const CI_PROVIDER_IDS = [
26517
26571
  "github-actions",
@@ -26757,8 +26811,7 @@ const CI_PROVIDER_CAPABILITY_RECORDS = [
26757
26811
  }
26758
26812
  ];
26759
26813
  new Map(CI_PROVIDER_CAPABILITY_RECORDS.map((record) => [record.provider, record]));
26760
- //#endregion
26761
- //#region src/v6/maturity.ts
26814
+ // src/v6/maturity.ts
26762
26815
  /**
26763
26816
  * v6 capability maturity ladder — `M0`–`M5` (ADR 0001).
26764
26817
  *
@@ -26957,8 +27010,7 @@ function nextLevelGapFromEvidence(evidence, owner, revisitTrigger) {
26957
27010
  question: "How much does it matter? (technical severity stays separate)"
26958
27011
  }
26959
27012
  ].map((axis) => axis.id);
26960
- //#endregion
26961
- //#region src/v6/ecosystem-census.ts
27013
+ // src/v6/ecosystem-census.ts
26962
27014
  /**
26963
27015
  * The leading major of a version or range. `FRAMEWORK_INVENTORY`
26964
27016
  * records *validated versions* (`1.44`, `29`) while a manifest records a
@@ -26971,8 +27023,7 @@ function versionMajor(raw) {
26971
27023
  if (!match) return null;
26972
27024
  return (match[0] ?? "").split(".")[0] ?? null;
26973
27025
  }
26974
- //#endregion
26975
- //#region src/v6/ecosystem-probe.ts
27026
+ // src/v6/ecosystem-probe.ts
26976
27027
  /**
26977
27028
  * Filesystem-backed census evidence + the discovery probe (ADR 0010).
26978
27029
  *
@@ -27224,8 +27275,7 @@ function corpusReposOf(root) {
27224
27275
  }
27225
27276
  });
27226
27277
  }
27227
- //#endregion
27228
- //#region src/v6/capability-registry.ts
27278
+ // src/v6/capability-registry.ts
27229
27279
  /**
27230
27280
 
27231
27281
  }
@@ -27708,8 +27758,7 @@ function validateRegistry(registry) {
27708
27758
  }
27709
27759
  return diagnostics;
27710
27760
  }
27711
- //#endregion
27712
- //#region src/commands/capability.ts
27761
+ // src/commands/capability.ts
27713
27762
  /** Parse the flags. Anything flag-shaped that is not documented is a
27714
27763
  * usage error rather than a silently ignored argument, because
27715
27764
  * `capability --promote-m3` must not read as "show me everything". */
@@ -27829,8 +27878,7 @@ function runCapabilityCommand(argv, io = {
27829
27878
  io.out(json ? capabilityJson(entries, diagnostics) : `${renderCapabilityText(entries, diagnostics)}\n`);
27830
27879
  return diagnostics.length > 0 ? 1 : 0;
27831
27880
  }
27832
- //#endregion
27833
- //#region src/rules/measurement-status.ts
27881
+ // src/rules/measurement-status.ts
27834
27882
  /**
27835
27883
  * Derive the measurement status for a single rule.
27836
27884
  *
@@ -27877,8 +27925,7 @@ function getMeasurementStatus(rules = RULES) {
27877
27925
  };
27878
27926
  });
27879
27927
  }
27880
- //#endregion
27881
- //#region src/commands/rule-health.ts
27928
+ // src/commands/rule-health.ts
27882
27929
  /**
27883
27930
  * Build the per-rule health table from the shipped registry. Deterministic
27884
27931
  * order: the RULES registry order (stable across runs).
@@ -28009,8 +28056,7 @@ function renderRuleHealth(rows, limit) {
28009
28056
  return lines.join("\n");
28010
28057
  }
28011
28058
  Object.keys(MEASURED_FP).length;
28012
- //#endregion
28013
- //#region src/playwright/selector-health-types.ts
28059
+ // src/playwright/selector-health-types.ts
28014
28060
  /** Risk weights per locator shape (0 = resilient, higher = brittle). */
28015
28061
  const LOCATOR_RISK = {
28016
28062
  "role-based": 0,
@@ -28019,8 +28065,7 @@ const LOCATOR_RISK = {
28019
28065
  "css-chain": 60,
28020
28066
  xpath: 90
28021
28067
  };
28022
- //#endregion
28023
- //#region src/playwright/selector-health.ts
28068
+ // src/playwright/selector-health.ts
28024
28069
  /**
28025
28070
  * Selector Health Score engine (Upgrade-Plan-v2 R3, the headline feature).
28026
28071
  *
@@ -28148,8 +28193,7 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
28148
28193
  walk(root);
28149
28194
  return specs.sort((a, b) => a.score - b.score || compareCodePoints(a.file, b.file));
28150
28195
  }
28151
- //#endregion
28152
- //#region src/cli-handlers.ts
28196
+ // src/cli-handlers.ts
28153
28197
  /**
28154
28198
  * Handler functions for every Mjolnir CLI verb.
28155
28199
  *
@@ -28158,6 +28202,22 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
28158
28202
  * (Task 6: renderScanOutput helper).
28159
28203
  */
28160
28204
  /**
28205
+ * The one catch for a scan-backed verb.
28206
+ *
28207
+ * An invalid `mjolnir.config.json` is the USER's error and exits 10 (usage),
28208
+ * as `scan` always did. `fix`, `ci verify` and `explain --plan` each had
28209
+ * their own catch that only knew the internal-error arm, so the same config
28210
+ * typo exited 20 and printed "this is a bug in Mjölnir" on three verbs.
28211
+ */
28212
+ function verbFailure(error, io, debug) {
28213
+ if (error instanceof ConfigValidationError) {
28214
+ io.err(error.message);
28215
+ return 10;
28216
+ }
28217
+ internalErrorMessage(error, io.err, debug);
28218
+ return 20;
28219
+ }
28220
+ /**
28161
28221
  * The one place a run's gate level is resolved from `--blocking`, then
28162
28222
  * `mjolnir.config.json`, then the shipped default.
28163
28223
  *
@@ -28207,9 +28267,11 @@ function renderScanOutput(result, args, target, io, gate) {
28207
28267
  else if (args.format === "pr-comment") {
28208
28268
  const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH));
28209
28269
  const diff = baseline ? diffAgainstBaseline(result, baseline) : void 0;
28270
+ const commit = commitBoundTo(result, target);
28210
28271
  io.out(renderPrComment(result, {
28211
28272
  ...diff ? { diff } : {},
28212
- version: ENGINE_VERSION
28273
+ version: ENGINE_VERSION,
28274
+ ...commit !== void 0 ? { commit } : {}
28213
28275
  }));
28214
28276
  } else if (args.format === "github-summary") io.out(renderStepSummary(result));
28215
28277
  else if (args.json) io.out(JSON.stringify({
@@ -28298,6 +28360,58 @@ async function runSuppressionGate(args, target, io) {
28298
28360
  if (args.base !== void 0) argv.push("--base", args.base);
28299
28361
  return runSuppressionGateCommand(argv, io);
28300
28362
  }
28363
+ /**
28364
+ * The commit a rendered report may claim, or undefined when it cannot claim one.
28365
+ *
28366
+ * The scan records the HEAD it analysed (`runIdentity.commit`). A report states
28367
+ * that commit only when the repository at `dir` is still at it, so a report
28368
+ * rendered from another commit's JSON reads `unknown` instead of borrowing an
28369
+ * identity nothing here can confirm. The retired `pr-comment --from` verb took
28370
+ * the commit from a `--commit` argument; the formats that replaced it check the
28371
+ * checkout instead, which needs no flag and cannot be handed the wrong value.
28372
+ */
28373
+ function commitBoundTo(result, dir) {
28374
+ const recorded = result.runIdentity?.commit;
28375
+ if (recorded === void 0) return void 0;
28376
+ return currentCommit(dir, { nullable: true }) === recorded ? recorded : void 0;
28377
+ }
28378
+ /**
28379
+ * `--format github-summary|pr-comment <report.json>`: render a SAVED report.
28380
+ *
28381
+ * The v6 collapse retired `summary <report>` and `pr-comment --from <report>`
28382
+ * onto these two formats and shipped only the half that scans. Every CI
28383
+ * consumer renders the `--json` report its scan step already wrote: the
28384
+ * Action, both `ci install` templates and this repository's own PR workflow.
28385
+ * All of them kept calling the retired verbs, and all of them exited 10. A
28386
+ * second scan is not a substitute, because the comment has to describe the
28387
+ * result the gate read rather than one that happens to agree with it.
28388
+ *
28389
+ * Delegation, not reimplementation, which is the rule `--suppression-gate`
28390
+ * follows. Each retired verb's command still owns its contract, exit codes
28391
+ * included. The render never gates and exits 0. A report that cannot be read
28392
+ * exits 2 for the summary and 10 for the comment, as those verbs did.
28393
+ *
28394
+ * Returns null when the request is a scan: another format, or a target that
28395
+ * is not a file.
28396
+ */
28397
+ function renderSavedReport(args, target, io) {
28398
+ if (args.format !== "github-summary" && args.format !== "pr-comment") return null;
28399
+ if (!isRegularFile(target)) return null;
28400
+ if (args.format === "github-summary") return runSummaryCommand([target], io);
28401
+ return runPrCommentCommand([
28402
+ "--from",
28403
+ target,
28404
+ "--version",
28405
+ ENGINE_VERSION
28406
+ ], io);
28407
+ }
28408
+ function isRegularFile(path) {
28409
+ try {
28410
+ return statSync(path).isFile();
28411
+ } catch {
28412
+ return false;
28413
+ }
28414
+ }
28301
28415
  async function runScanCommand(argv, io = {
28302
28416
  out,
28303
28417
  err
@@ -28305,6 +28419,8 @@ async function runScanCommand(argv, io = {
28305
28419
  const args = parseArgsOrUsage(argv, io);
28306
28420
  if (!args) return 10;
28307
28421
  const target = resolve(args.target);
28422
+ const savedRender = renderSavedReport(args, target, io);
28423
+ if (savedRender !== null) return savedRender;
28308
28424
  const invalid = validateScanTarget(target, io.err);
28309
28425
  if (invalid !== null) return invalid;
28310
28426
  if (args.suppressions) return printSuppressionLedger(target, io);
@@ -28334,15 +28450,18 @@ async function runScanCommand(argv, io = {
28334
28450
  } } : {}
28335
28451
  });
28336
28452
  progress.done();
28453
+ let baselineFailed = false;
28337
28454
  if (args.saveBaseline) {
28338
28455
  const outPath = join(target, DEFAULT_BASELINE_PATH);
28339
- try {
28456
+ const notice = args.format === "terminal" ? io.out : io.err;
28457
+ if (result.partial) io.err("baseline NOT saved — this scan is partial, and a truncated before-state would report every finding it missed as RESOLVED. Re-run without the time budget.");
28458
+ else try {
28340
28459
  const saved = saveBaseline(result, currentCommit(target) ?? "unknown", outPath);
28341
- io.out(renderBaselineSaved(DEFAULT_BASELINE_PATH, result.findings.length, { ...saved.backupPath !== void 0 ? { backupPath: saved.backupPath } : {} }));
28460
+ notice(renderBaselineSaved(DEFAULT_BASELINE_PATH, result.findings.length, { ...saved.backupPath !== void 0 ? { backupPath: saved.backupPath } : {} }));
28342
28461
  } catch (saveErr) {
28343
28462
  io.err(`baseline save FAILED — ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
28344
28463
  io.err("The scan completed; the snapshot was not written. Fix the path permissions and re-run with `--save-baseline`.");
28345
- return 1;
28464
+ baselineFailed = true;
28346
28465
  }
28347
28466
  }
28348
28467
  if (args.debug && crashLog.length > 0) {
@@ -28378,6 +28497,7 @@ async function runScanCommand(argv, io = {
28378
28497
  }
28379
28498
  if (result.partial) return 2;
28380
28499
  for (const w of warnings) io.err(w);
28500
+ if (baselineFailed) return 2;
28381
28501
  return scanExitCode({
28382
28502
  partial: false,
28383
28503
  findings: result.findings,
@@ -28387,12 +28507,7 @@ async function runScanCommand(argv, io = {
28387
28507
  ...args.requireFullCoverage !== void 0 ? { requireFullCoverage: args.requireFullCoverage } : {}
28388
28508
  });
28389
28509
  } catch (err) {
28390
- if (err instanceof ConfigValidationError) {
28391
- io.err(err.message);
28392
- return 10;
28393
- }
28394
- internalErrorMessage(err, io.err, args?.debug === true);
28395
- return 20;
28510
+ return verbFailure(err, io, args?.debug === true);
28396
28511
  }
28397
28512
  }
28398
28513
  async function runFixCommand(argv, io = {
@@ -28406,16 +28521,17 @@ async function runFixCommand(argv, io = {
28406
28521
  const target = resolve(args.target);
28407
28522
  const invalid = validateScanTarget(target, io.err);
28408
28523
  if (invalid !== null) return invalid;
28409
- const fixes = planAndApplyFixes(await runScan$1({
28524
+ const result = await runScan$1({
28410
28525
  ...args,
28411
28526
  target,
28412
28527
  strict: true
28413
- }), target, { dryRun });
28528
+ });
28529
+ const fixes = planAndApplyFixes(result, target, { dryRun });
28414
28530
  io.out(renderFixReport(fixes, dryRun));
28531
+ if (result.partial) return 2;
28415
28532
  return fixes.some((f) => f.status === "failed") ? 1 : 0;
28416
28533
  } catch (err) {
28417
- internalErrorMessage(err, io.err, args?.debug === true);
28418
- return 20;
28534
+ return verbFailure(err, io, args?.debug === true);
28419
28535
  }
28420
28536
  }
28421
28537
  async function runVerifyCommand(argv, io = {
@@ -28432,14 +28548,20 @@ async function runVerifyCommand(argv, io = {
28432
28548
  ...args,
28433
28549
  target
28434
28550
  });
28435
- const digest = buildVerifyDigest(result, loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w)));
28551
+ const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w));
28552
+ const digest = buildVerifyDigest(result, baseline);
28436
28553
  io.out(renderVerifyDigest(digest));
28437
28554
  if (result.partial) return 2;
28438
28555
  if (!digest.hasBaseline) return 2;
28439
- return digest.new.some((f) => f.severity === "error") ? 1 : 0;
28556
+ const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
28557
+ return scanExitCode({
28558
+ partial: false,
28559
+ findings: diffAgainstBaseline(result, baseline).newFindings,
28560
+ gate: resolveGateLevel(args.blocking, config),
28561
+ isAdvisory: isAdvisoryFinding
28562
+ });
28440
28563
  } catch (err) {
28441
- internalErrorMessage(err, io.err, args?.debug === true);
28442
- return 20;
28564
+ return verbFailure(err, io, args?.debug === true);
28443
28565
  }
28444
28566
  }
28445
28567
  async function runPrCommentCommand(argv, io = {
@@ -28454,13 +28576,14 @@ async function runPrCommentCommand(argv, io = {
28454
28576
  return 10;
28455
28577
  }
28456
28578
  try {
28457
- const saved = loadSavedReport(resolve(fromPath));
28579
+ const reportPath = resolve(fromPath);
28580
+ const saved = loadSavedReport(reportPath);
28458
28581
  const option = (flag) => {
28459
28582
  const index = argv.indexOf(flag);
28460
28583
  return index >= 0 ? argv[index + 1] : void 0;
28461
28584
  };
28462
28585
  const version = option("--version");
28463
- const commit = option("--commit");
28586
+ const commit = option("--commit") ?? commitBoundTo(saved, dirname(reportPath));
28464
28587
  const repoUrl = option("--repo-url");
28465
28588
  io.out(renderPrComment(saved, {
28466
28589
  ...version ? { version } : {},
@@ -28525,15 +28648,19 @@ async function runHandoverCommand(argv, io = {
28525
28648
  target
28526
28649
  });
28527
28650
  let forensics = null;
28651
+ let forensicsFailed = false;
28528
28652
  const resultsDir = join(target, "test-results");
28529
28653
  if (existsSync(resultsDir)) try {
28530
28654
  forensics = runForensics(resultsDir, { writeFlakyMd: false }).report;
28531
- } catch {}
28655
+ } catch (forensicsErr) {
28656
+ io.err(`warning: test-results/ could not be read (${errorMessage(forensicsErr)}) — the map below is static-only.`);
28657
+ forensicsFailed = true;
28658
+ }
28532
28659
  io.out(renderHandover(buildHandover(result, forensics)));
28660
+ if (result.partial || forensicsFailed) return 2;
28533
28661
  return 0;
28534
28662
  } catch (err) {
28535
- internalErrorMessage(err, io.err, args?.debug === true);
28536
- return 20;
28663
+ return verbFailure(err, io, args?.debug === true);
28537
28664
  }
28538
28665
  }
28539
28666
  function runPwReportCommand(argv, io = {
@@ -28667,7 +28794,7 @@ function runEvidenceArm(argv, io) {
28667
28794
  const { report } = runForensics(resolve(target), { writeFlakyMd });
28668
28795
  if (asJson) io.out(JSON.stringify(report, null, 2));
28669
28796
  else io.out(asMd ? renderTriageMd(report) : renderTriage(report));
28670
- return report.analysisComplete === false ? 1 : 0;
28797
+ return report.analysisComplete === false ? 2 : 0;
28671
28798
  } catch (err) {
28672
28799
  internalErrorMessage(err, io.err, argv.includes("--debug"));
28673
28800
  return 20;
@@ -28820,8 +28947,7 @@ async function runDoctorPlaywright(argv, io = { out }) {
28820
28947
  return 20;
28821
28948
  }
28822
28949
  }
28823
- //#endregion
28824
- //#region src/rules/rule-metadata-schema.ts
28950
+ // src/rules/rule-metadata-schema.ts
28825
28951
  const VALID_SEVERITIES = SEVERITY_ORDER$1;
28826
28952
  const VALID_CATEGORIES = RULE_CATEGORIES;
28827
28953
  const VALID_QA_IMPACTS = Object.keys(QA_IMPACT_LABELS);
@@ -28869,8 +28995,7 @@ function validateAllRulesMetadata(rules) {
28869
28995
  for (const rule of rules) all.push(...validateRuleMetadata(rule));
28870
28996
  return all;
28871
28997
  }
28872
- //#endregion
28873
- //#region src/engine/detector-hash.ts
28998
+ // src/engine/detector-hash.ts
28874
28999
  /**
28875
29000
  * Detector revision integrity engine (certification-audit Phase 3,
28876
29001
  * G4/D8v2/D17).
@@ -29138,8 +29263,7 @@ const RULE_ID_RE = new RegExp(`^QA-(?:${[
29138
29263
  appliesTo: "test-files"
29139
29264
  }
29140
29265
  ].map((f) => f.token).join("|")})-\\d{3}$`);
29141
- //#endregion
29142
- //#region src/commands/doctor.ts
29266
+ // src/commands/doctor.ts
29143
29267
  /**
29144
29268
  * `mjolnir doctor` — self-audit of Mjolnir's own rule base.
29145
29269
  *
@@ -30086,8 +30210,7 @@ function stripNonDeterministicFields(json, fields = NON_DETERMINISTIC_FIELDS) {
30086
30210
  for (const field of fields) delete json[field];
30087
30211
  return json;
30088
30212
  }
30089
- //#endregion
30090
- //#region src/commands/doctor-run.ts
30213
+ // src/commands/doctor-run.ts
30091
30214
  /**
30092
30215
  * Doctor CLI entry (certification-audit Phase 5, G6): the command handler
30093
30216
  * lives beside the check implementations instead of in cli.ts — the CLI
@@ -30185,8 +30308,7 @@ function installedPackageRoot() {
30185
30308
  return process.cwd();
30186
30309
  }
30187
30310
  }
30188
- //#endregion
30189
- //#region src/commands/handoff.ts
30311
+ // src/commands/handoff.ts
30190
30312
  /**
30191
30313
  * `mjolnir handoff [mjolnir.json]` — the deterministic fix-handoff
30192
30314
  * artifact (agent-handoff plan M3).
@@ -30496,8 +30618,7 @@ function runHandoffCommand(argv, io = {
30496
30618
  }));
30497
30619
  return 0;
30498
30620
  }
30499
- //#endregion
30500
- //#region src/commands/install-agents.ts
30621
+ // src/commands/install-agents.ts
30501
30622
  /**
30502
30623
  * `mjolnir install` — instruction-surface installer (agent-handoff
30503
30624
  * plan M4).
@@ -30823,8 +30944,7 @@ function executeHookInstall(entry) {
30823
30944
  default: return false;
30824
30945
  }
30825
30946
  }
30826
- //#endregion
30827
- //#region src/commands/release-trust.ts
30947
+ // src/commands/release-trust.ts
30828
30948
  /**
30829
30949
  * `mjolnir ci release-trust` — the Release Trust Verdict (product-gap
30830
30950
  * master plan §5, plan 1789009691197 R4a).
@@ -31535,8 +31655,7 @@ function runReleaseTrustCommand(argv, io = {
31535
31655
  return 20;
31536
31656
  }
31537
31657
  }
31538
- //#endregion
31539
- //#region src/commands/policy.ts
31658
+ // src/commands/policy.ts
31540
31659
  /**
31541
31660
  * `mjolnir policy` — Policy-as-Code (TL-2).
31542
31661
  *
@@ -31660,8 +31779,7 @@ async function runPolicyCommand(argv, io) {
31660
31779
  io.err(`Unknown policy subcommand: ${subcommand}`);
31661
31780
  return 10;
31662
31781
  }
31663
- //#endregion
31664
- //#region src/commands/analyze.ts
31782
+ // src/commands/analyze.ts
31665
31783
  /**
31666
31784
  * `mjolnir analyze --cross-file` — Cross-File Analysis (SDET-7).
31667
31785
  *
@@ -31797,8 +31915,7 @@ function runAnalyzeCommand(argv, io) {
31797
31915
  return 20;
31798
31916
  }
31799
31917
  }
31800
- //#endregion
31801
- //#region src/commands/ci-adapter.ts
31918
+ // src/commands/ci-adapter.ts
31802
31919
  /**
31803
31920
  * `mjolnir ci adapters` — CI Adapters (SDET-4).
31804
31921
  *
@@ -31919,8 +32036,7 @@ function runCiAdapterCommand(argv, io) {
31919
32036
  io.out(output);
31920
32037
  return 0;
31921
32038
  }
31922
- //#endregion
31923
- //#region src/engine/machine-contract-verification.ts
32039
+ // src/engine/machine-contract-verification.ts
31924
32040
  /**
31925
32041
  * M11 — Machine Contract Verification.
31926
32042
  *
@@ -32082,8 +32198,7 @@ function renderContractVerification(result) {
32082
32198
  }
32083
32199
  return lines.join("\n").trimEnd();
32084
32200
  }
32085
- //#endregion
32086
- //#region src/commands/contract-verify.ts
32201
+ // src/commands/contract-verify.ts
32087
32202
  /**
32088
32203
  * `contract-verify` — the machine-contract and exit-code validators, and the
32089
32204
  * command that runs them.
@@ -32240,8 +32355,7 @@ async function runContractVerifyCommand(argv, io = {
32240
32355
  return milestoneError(error, io, argv.includes("--debug"));
32241
32356
  }
32242
32357
  }
32243
- //#endregion
32244
- //#region src/commands/share.ts
32358
+ // src/commands/share.ts
32245
32359
  /**
32246
32360
  * `mjolnir share` — React Doctor's proof loop, without its server.
32247
32361
  *
@@ -32471,8 +32585,7 @@ async function runShareCommand(argv, io) {
32471
32585
  return 20;
32472
32586
  }
32473
32587
  }
32474
- //#endregion
32475
- //#region src/engine/historical-trust.ts
32588
+ // src/engine/historical-trust.ts
32476
32589
  /**
32477
32590
  * Compute a trust snapshot from a scan result and run identity.
32478
32591
  * Pure function — deterministic output for the same input.
@@ -32658,8 +32771,7 @@ function renderTrustTrend(trend) {
32658
32771
  }
32659
32772
  return lines.join("\n").trimEnd();
32660
32773
  }
32661
- //#endregion
32662
- //#region src/commands/release-trend.ts
32774
+ // src/commands/release-trend.ts
32663
32775
  /**
32664
32776
  * `ci release-trend` — how the signed release's trust record has moved across
32665
32777
  * releases.
@@ -32716,8 +32828,7 @@ async function runTrustTrendCommand(argv, io = {
32716
32828
  return milestoneError(error, io, argv.includes("--debug"));
32717
32829
  }
32718
32830
  }
32719
- //#endregion
32720
- //#region src/cli.ts
32831
+ // src/cli.ts
32721
32832
  /**
32722
32833
  * Mjölnir CLI entry point (W1-02).
32723
32834
  * Exit codes (§24.1, frozen): 0 clean · 1 findings ≥ gate · 2 partial ·
@@ -32730,8 +32841,8 @@ async function runTrustTrendCommand(argv, io = {
32730
32841
  * bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
32731
32842
  * where the file happens to sit after install. This follows the same
32732
32843
  * discipline as SARIF's `driver.version` — kept in sync by
32733
- * `scripts/archive/sync-sarif-version.cjs` (archived; superseded by the release
32734
- * workflow's own version stamping) on release and guarded by
32844
+ * `npm run version:surface:sync` (it rewrites every identity surface) on
32845
+ * release and guarded by
32735
32846
  * `tests/contract/version-consistency.spec.ts` locally. R4c moved the literal to
32736
32847
  * src/engine/version.ts (a leaf module) so the scan pipeline's run
32737
32848
  * identity can carry it without a cli.ts import cycle; this re-export
@@ -33085,7 +33196,7 @@ function runCiInstall(argv, io = {
33085
33196
  return 10;
33086
33197
  }
33087
33198
  io.out(`${result.existed ? "Updated" : "Created"} ${result.written}`);
33088
- io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b0 (immutable pin). Gate: ${gate}.`);
33199
+ io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses ${ACTION_REF} (immutable pin). Gate: ${gate}.`);
33089
33200
  io.out(gate === "advisory" ? "Advisory mode reports findings without blocking. Opt in with --gate error or --gate warning." : `Blocking mode fails on ${gate} findings.`);
33090
33201
  io.out("Change with: mjolnir ci install --gate error|warning|advisory");
33091
33202
  if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
@@ -33236,7 +33347,6 @@ if (isEntryPoint()) {
33236
33347
  await flushSentry();
33237
33348
  }
33238
33349
  }
33239
- //#endregion
33240
33350
  export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, MAX_DURATION_MS, 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, renderCiSubcommandHelp, renderScanOutput, runAnalyzeCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCrossFileCommand, runDoctorCommand, runDoctorPlaywright, runEvidenceArm, runEvidenceGraphCommand, runExplainCommand, runFixCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runInstallCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runReleaseTrustCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressionGateCommand, runSuppressions, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
33241
33351
 
33242
33352
  //# sourceMappingURL=cli.mjs.map