mjolnir-qa 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,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
@@ -301,7 +307,8 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
301
307
  let n = p.replace(/\\/g, "/");
302
308
  if (scanRoot) {
303
309
  const root = scanRoot.replace(/\\/g, "/").replace(/\/?$/, "/");
304
- if (n.startsWith(root)) n = n.slice(root.length);
310
+ const head = n.slice(0, root.length);
311
+ if (/^[a-z]:\//i.test(root) ? head.toLowerCase() === root.toLowerCase() : head === root) n = n.slice(root.length);
305
312
  }
306
313
  return n.replace(/^\.\//, "").replace(/^\/+/, "");
307
314
  };
@@ -367,8 +374,7 @@ function deriveTrustLevel(finding, corroboration) {
367
374
  if (corroboration.level === "test") return "L4";
368
375
  return "L3";
369
376
  }
370
- //#endregion
371
- //#region src/rules/measured-fp.generated.ts
377
+ // src/rules/measured-fp.generated.ts
372
378
  const MEASURED_FP = {
373
379
  "QA-CI-001": {
374
380
  fpRate: .105,
@@ -1429,8 +1435,7 @@ const MEASURED_FP_RAW = {
1429
1435
  ciHigh: .4343
1430
1436
  }
1431
1437
  };
1432
- //#endregion
1433
- //#region src/engine/trust-summary.ts
1438
+ // src/engine/trust-summary.ts
1434
1439
  /**
1435
1440
  * Trust level of the summary — the BEST trust level any finding in the
1436
1441
  * scan reached. Static-only findings settle at L2 (deriveTrustLevel's
@@ -1554,8 +1559,7 @@ function buildTrustSummary(result, declarationsByFile) {
1554
1559
  ceilingReasons: reasons
1555
1560
  };
1556
1561
  }
1557
- //#endregion
1558
- //#region src/governance/run-mode.ts
1562
+ // src/governance/run-mode.ts
1559
1563
  /**
1560
1564
  * Which mode a run is in, and how the other one is reached.
1561
1565
  *
@@ -1606,8 +1610,7 @@ function resolveRunMode(input) {
1606
1610
  if (declared === "local-only" || declared === "hosted") return declared;
1607
1611
  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
1612
  }
1609
- //#endregion
1610
- //#region src/engine/run-identity.ts
1613
+ // src/engine/run-identity.ts
1611
1614
  /**
1612
1615
  * Run Identity (product-gap master plan §7, plan 1789009691197 R4c) —
1613
1616
  * the deterministic binding that anchors every trust claim to its
@@ -1736,8 +1739,7 @@ function buildEvidenceGraph(parts) {
1736
1739
  } } : {}
1737
1740
  };
1738
1741
  }
1739
- //#endregion
1740
- //#region src/engine/file-executor.ts
1742
+ // src/engine/file-executor.ts
1741
1743
  function cacheHitOutcome(job) {
1742
1744
  return {
1743
1745
  path: job.path,
@@ -1828,8 +1830,7 @@ function createExecutor(name, concurrency, runOne) {
1828
1830
  execute: runOne
1829
1831
  };
1830
1832
  }
1831
- //#endregion
1832
- //#region src/scope/git-resolve.ts
1833
+ // src/scope/git-resolve.ts
1833
1834
  /**
1834
1835
  * Absolute-path git resolution (audit S1).
1835
1836
  *
@@ -1904,8 +1905,7 @@ function runGit(root, args) {
1904
1905
  return null;
1905
1906
  }
1906
1907
  }
1907
- //#endregion
1908
- //#region src/lib/safe-json.ts
1908
+ // src/lib/safe-json.ts
1909
1909
  /**
1910
1910
  * Reusable safe-JSON parse-and-validate helper.
1911
1911
  *
@@ -1942,8 +1942,7 @@ function isRecord(v) {
1942
1942
  return false;
1943
1943
  }
1944
1944
  }
1945
- //#endregion
1946
- //#region src/engine/candidate-binding.ts
1945
+ // src/engine/candidate-binding.ts
1947
1946
  /**
1948
1947
  * Candidate binding (plan V5-010).
1949
1948
  *
@@ -2076,18 +2075,17 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
2076
2075
  return null;
2077
2076
  }
2078
2077
  }
2079
- //#endregion
2080
- //#region src/engine/version.ts
2078
+ // src/engine/version.ts
2081
2079
  /**
2082
2080
  * Engine version, as a leaf module: `runIdentity` (R4c) needs the engine
2083
2081
  * version inside scan-pipeline WITHOUT importing cli.ts (a cycle — cli
2084
2082
  * imports the pipeline). The literal follows the same discipline as
2085
2083
  * 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
2084
+ * `npm run version:surface:sync` (this file is an identity surface) and
2085
+ * guarded by the version-consistency
2088
2086
  * spec. cli.ts re-exports this as CLI_VERSION.
2089
2087
  */
2090
- const ENGINE_VERSION = "6.0.0";
2088
+ const ENGINE_VERSION = "6.1.0";
2091
2089
  /**
2092
2090
  * Build identity — WHICH BUILD IS RUNNING, as distinct from WHICH RELEASE IS
2093
2091
  * PUBLISHED.
@@ -2148,16 +2146,7 @@ function resolveBuildId() {
2148
2146
  };
2149
2147
  return buildIdFrom(runGit);
2150
2148
  }
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
- */
2149
+ // src/engine/contract-versions.ts
2161
2150
  const TRUST_MODEL_VERSION = "1.0.0";
2162
2151
  /**
2163
2152
  * 2.0.0 (ADR 0014): the core tier carries an evidence FLOOR. A core rule's
@@ -2169,8 +2158,7 @@ const TRUST_MODEL_VERSION = "1.0.0";
2169
2158
  */
2170
2159
  const SCORING_MODEL_VERSION = "2.0.0";
2171
2160
  const FRAMEWORK_SUPPORT_MATRIX_VERSION = "1.0.0";
2172
- //#endregion
2173
- //#region src/discovery/evidence-discovery.ts
2161
+ // src/discovery/evidence-discovery.ts
2174
2162
  /**
2175
2163
  * Zero-config evidence discovery (Mega MVP Master Plan v3.1 §26 WI-11).
2176
2164
  *
@@ -2262,8 +2250,7 @@ function discoverEvidenceCandidates(scanRoot) {
2262
2250
  }
2263
2251
  return out.sort((a, b) => a.depth - b.depth || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
2264
2252
  }
2265
- //#endregion
2266
- //#region src/discovery/workspace.ts
2253
+ // src/discovery/workspace.ts
2267
2254
  /**
2268
2255
  * Repository discovery (Sprint-Plan W1-03).
2269
2256
  * Finds project root, parses package.json, detects npm/yarn/pnpm workspaces.
@@ -2310,8 +2297,7 @@ function basename$1(p) {
2310
2297
  const parts = p.split(/[\\/]/);
2311
2298
  return parts[parts.length - 1];
2312
2299
  }
2313
- //#endregion
2314
- //#region src/lib/fs-bounded.ts
2300
+ // src/lib/fs-bounded.ts
2315
2301
  function closeQuiet(fd) {
2316
2302
  try {
2317
2303
  closeSync(fd);
@@ -2363,8 +2349,7 @@ function readFileBounded(path, maxBytes) {
2363
2349
  if (fd !== void 0) closeQuiet(fd);
2364
2350
  }
2365
2351
  }
2366
- //#endregion
2367
- //#region src/config/config-schema.ts
2352
+ // src/config/config-schema.ts
2368
2353
  const VALID_GATES = /* @__PURE__ */ new Set([
2369
2354
  "advisory",
2370
2355
  "error",
@@ -2427,15 +2412,18 @@ function validateConfigSchema(config) {
2427
2412
  }
2428
2413
  }
2429
2414
  if (cfg["plugins"] !== void 0) {
2430
- if (cfg["plugins"] === null || typeof cfg["plugins"] !== "object") errors.push("plugins must be an object or array");
2415
+ if (!Array.isArray(cfg["plugins"])) errors.push(`plugins must be an array of package names or { "package": "<name>" } objects, got ${typeof cfg["plugins"]}`);
2416
+ else for (let i = 0; i < cfg["plugins"].length; i++) {
2417
+ const entry = cfg["plugins"][i];
2418
+ if (!(typeof entry === "string" || entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry["package"] === "string")) errors.push(`plugins[${i}] must be a package name or { "package": "<name>" }, got ${JSON.stringify(entry)}`);
2419
+ }
2431
2420
  }
2432
2421
  return {
2433
2422
  valid: errors.length === 0,
2434
2423
  errors
2435
2424
  };
2436
2425
  }
2437
- //#endregion
2438
- //#region src/config/config.ts
2426
+ // src/config/config.ts
2439
2427
  /**
2440
2428
  * Config file support (Sprint-Plan W7, Product-MVP §27 + GAP-F).
2441
2429
  * Zero-config preserved: config presence never changes detection
@@ -2493,7 +2481,7 @@ function loadConfig(root, options = {}) {
2493
2481
  return {
2494
2482
  config: parsed,
2495
2483
  path: p,
2496
- warnings: validate(parsed, options.knownRuleIds)
2484
+ warnings: validate(parsed, options.knownRuleIds, options.now, name)
2497
2485
  };
2498
2486
  } catch (err) {
2499
2487
  const msg = err.message;
@@ -2506,7 +2494,7 @@ function loadConfig(root, options = {}) {
2506
2494
  warnings: []
2507
2495
  };
2508
2496
  }
2509
- function validate(cfg, knownRuleIds) {
2497
+ function validate(cfg, knownRuleIds, now = /* @__PURE__ */ new Date(), fileName = "mjolnir.config.json") {
2510
2498
  const warnings = [];
2511
2499
  const schemaResult = validateConfigSchema(cfg);
2512
2500
  if (!schemaResult.valid) throw new Error(schemaResult.errors.join("; "));
@@ -2526,10 +2514,10 @@ function validate(cfg, knownRuleIds) {
2526
2514
  if (!Array.isArray(ign.files)) throw new Error(`ignore for ${ign.ruleId}: "files" must be an array of glob strings, got ${typeof ign.files}`);
2527
2515
  for (const g of ign.files) if (typeof g !== "string") throw new Error(`ignore for ${ign.ruleId}: "files" entries must be strings, got ${typeof g} (${JSON.stringify(g)})`);
2528
2516
  }
2529
- if (ign.expires !== void 0 && Number.isNaN(new Date(ign.expires).getTime())) throw new Error(`ignore for ${ign.ruleId}: "expires" must be an ISO date, got "${ign.expires}"`);
2530
- if (ign.expires !== void 0 && !Number.isNaN(new Date(ign.expires).getTime()) && new Date(ign.expires).getTime() < Date.now()) warnings.push(`warning: ignore for ${ign.ruleId} has expired (expires "${ign.expires}") — the entry is kept but currently inactive.`);
2517
+ if (ign.expires !== void 0 && Number.isNaN(utcMidnight(ign.expires))) throw new Error(`ignore for ${ign.ruleId}: "expires" must be an ISO date (YYYY-MM-DD), got "${ign.expires}"`);
2518
+ if (ign.expires !== void 0 && !isSuppressionActive(ign, now)) warnings.push(`warning: ignore for ${ign.ruleId} has expired (expires "${ign.expires}") — the entry is kept but currently inactive.`);
2531
2519
  }
2532
- for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning: mjolnir.config.json has an unknown top-level key "${key}" — ignored (typo? see README §Configuration).`);
2520
+ for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning: ${fileName} has an unknown top-level key "${key}" — ignored (typo? see README §Configuration).`);
2533
2521
  if (cfg.exclude !== void 0) {
2534
2522
  if (!Array.isArray(cfg.exclude)) throw new Error(`exclude must be an array of strings, got ${typeof cfg.exclude}`);
2535
2523
  for (const p of cfg.exclude) if (typeof p !== "string") throw new Error(`exclude entries must be strings, got ${typeof p} (${JSON.stringify(p)})`);
@@ -2551,12 +2539,22 @@ function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
2551
2539
  if (!ign.expires) return true;
2552
2540
  return utcMidnight(ign.expires) > utcMidnightOf(now);
2553
2541
  }
2554
- /** UTC-midnight timestamp of an ISO date string (YYYY-MM-DD). */
2542
+ /**
2543
+ * UTC-midnight timestamp of an ISO calendar date (YYYY-MM-DD), or NaN.
2544
+ *
2545
+ * NaN for anything that is not a real calendar day, so a load-time check and
2546
+ * the active/expired verdict share one parser and cannot disagree.
2547
+ */
2555
2548
  function utcMidnight(isoDate) {
2556
- if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return NaN;
2557
2549
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
2558
2550
  if (!m) return NaN;
2559
- return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
2551
+ const year = Number(m[1]);
2552
+ const month = Number(m[2]) - 1;
2553
+ const day = Number(m[3]);
2554
+ const t = Date.UTC(year, month, day);
2555
+ const back = new Date(t);
2556
+ if (back.getUTCFullYear() !== year || back.getUTCMonth() !== month || back.getUTCDate() !== day) return NaN;
2557
+ return t;
2560
2558
  }
2561
2559
  /** UTC-midnight timestamp of a Date (calendar day, timezone-independent). */
2562
2560
  function utcMidnightOf(d) {
@@ -2569,8 +2567,7 @@ function applySeverityOverrides(findings, cfg) {
2569
2567
  if (override && SEVERITY_ORDER$1.includes(override)) f.severity = override;
2570
2568
  }
2571
2569
  }
2572
- //#endregion
2573
- //#region src/lib/compare.ts
2570
+ // src/lib/compare.ts
2574
2571
  /**
2575
2572
  * Order that does not depend on who is asking.
2576
2573
  *
@@ -2616,8 +2613,7 @@ function compareCodePoints(a, b) {
2616
2613
  function compareLocalized(locale) {
2617
2614
  return (a, b) => a.localeCompare(b, locale);
2618
2615
  }
2619
- //#endregion
2620
- //#region src/lib/glob.ts
2616
+ // src/lib/glob.ts
2621
2617
  /**
2622
2618
  * The shared segment compiler: a glob to a regex BODY, with no anchors.
2623
2619
  *
@@ -2671,8 +2667,7 @@ function anyDepthGlobRegExp(glob) {
2671
2667
  function pathMatchesGlob$1(path, glob) {
2672
2668
  return anchoredGlobRegExp(glob).test(path.replaceAll("\\", "/"));
2673
2669
  }
2674
- //#endregion
2675
- //#region src/discovery/ignores.ts
2670
+ // src/discovery/ignores.ts
2676
2671
  /**
2677
2672
  * Ignore resolution (Product-MVP.txt R1 — Tier-1 launch blocker).
2678
2673
  *
@@ -2829,8 +2824,7 @@ function isLintFixtureDir(absoluteDir) {
2829
2824
  return false;
2830
2825
  }
2831
2826
  }
2832
- //#endregion
2833
- //#region src/discovery/shared-walk.ts
2827
+ // src/discovery/shared-walk.ts
2834
2828
  /**
2835
2829
  * Shared directory walker (audit P-2).
2836
2830
  *
@@ -2893,8 +2887,7 @@ function sharedWalk(options) {
2893
2887
  };
2894
2888
  walk(options.root);
2895
2889
  }
2896
- //#endregion
2897
- //#region src/frameworks/framework-inventory.ts
2890
+ // src/frameworks/framework-inventory.ts
2898
2891
  const FRAMEWORK_INVENTORY = [
2899
2892
  {
2900
2893
  frameworkId: "playwright",
@@ -3095,8 +3088,7 @@ const FRAMEWORK_INVENTORY = [
3095
3088
  function getFrameworkById(frameworkId) {
3096
3089
  return FRAMEWORK_INVENTORY.find((f) => f.frameworkId === frameworkId);
3097
3090
  }
3098
- //#endregion
3099
- //#region src/discovery/frameworks.ts
3091
+ // src/discovery/frameworks.ts
3100
3092
  /**
3101
3093
  * Framework detection via config resolution (Sprint-Plan W3-01, R8).
3102
3094
  *
@@ -3179,8 +3171,7 @@ function detectFrameworks(ws) {
3179
3171
  unknown: false
3180
3172
  };
3181
3173
  }
3182
- //#endregion
3183
- //#region src/engine/ts-ast.ts
3174
+ // src/engine/ts-ast.ts
3184
3175
  /**
3185
3176
  * AST precision layer (Upgrade-Plan-v3 Phase 3).
3186
3177
  *
@@ -3379,8 +3370,7 @@ function getCodeOnlyText(file) {
3379
3370
  function resetTsMorphProject() {
3380
3371
  looseProject = null;
3381
3372
  }
3382
- //#endregion
3383
- //#region src/engine/code-text.ts
3373
+ // src/engine/code-text.ts
3384
3374
  /**
3385
3375
  * Code-only text masking (Phase 1 — Tempering Plan).
3386
3376
  *
@@ -3740,8 +3730,7 @@ function getCodeTextForCounting(text, language) {
3740
3730
  function blankRange(chars, start, end) {
3741
3731
  for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
3742
3732
  }
3743
- //#endregion
3744
- //#region src/engine/adapter.ts
3733
+ // src/engine/adapter.ts
3745
3734
  /**
3746
3735
  * The §15.1 framework filter, shared by every language adapter: a rule
3747
3736
  * that declares `frameworks` runs on a file only when the file carries
@@ -3755,8 +3744,7 @@ function frameworkFilterApplies(rule, file) {
3755
3744
  if (tags === void 0 || tags.length === 0) return true;
3756
3745
  return rule.frameworks.some((f) => tags.includes(f));
3757
3746
  }
3758
- //#endregion
3759
- //#region src/engine/shared-run-rules.ts
3747
+ // src/engine/shared-run-rules.ts
3760
3748
  /**
3761
3749
  * Run ONE rule with crash isolation, and return what it produced.
3762
3750
  *
@@ -3793,8 +3781,7 @@ function runRuleIsolated(rule, file, onCrash) {
3793
3781
  return null;
3794
3782
  }
3795
3783
  }
3796
- //#endregion
3797
- //#region src/adapters/typescript.ts
3784
+ // src/adapters/typescript.ts
3798
3785
  /**
3799
3786
  * TypeScript/JavaScript adapter (R1).
3800
3787
  * Wraps the proven regex-based engine behind the LanguageAdapter
@@ -4047,8 +4034,7 @@ function loadWorkspaceShim(root) {
4047
4034
  return null;
4048
4035
  }
4049
4036
  }
4050
- //#endregion
4051
- //#region src/engine/tree-sitter-ast.ts
4037
+ // src/engine/tree-sitter-ast.ts
4052
4038
  /**
4053
4039
  * Tree-sitter WASM AST layer for Java and C# — Sprint 8 Task 36
4054
4040
  * (Master-Stabilization-Plan.md).
@@ -4267,8 +4253,7 @@ async function releaseTreeSitterResources() {
4267
4253
  } catch {}
4268
4254
  }
4269
4255
  }
4270
- //#endregion
4271
- //#region src/adapters/python.ts
4256
+ // src/adapters/python.ts
4272
4257
  /**
4273
4258
  * Python/pytest adapter (Upgrade-Plan-v2 R2).
4274
4259
  *
@@ -4426,8 +4411,7 @@ const pythonAdapter = {
4426
4411
  function readText$2(p) {
4427
4412
  return readFileSync(p, "utf8");
4428
4413
  }
4429
- //#endregion
4430
- //#region src/adapters/java.ts
4414
+ // src/adapters/java.ts
4431
4415
  /**
4432
4416
  * Java/JUnit adapter (Upgrade-Plan-v3 Phase 4).
4433
4417
  *
@@ -4495,7 +4479,9 @@ const javaAdapter = {
4495
4479
  const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(block[0])?.[1] ?? "";
4496
4480
  for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (re.test(artifact)) frameworks.add(tag);
4497
4481
  }
4498
- } catch {}
4482
+ } catch {
4483
+ recordDegradation("java-manifest-unreadable");
4484
+ }
4499
4485
  for (const gradle of ["build.gradle", "build.gradle.kts"]) {
4500
4486
  const path = join(root, gradle);
4501
4487
  if (!existsSync(path)) continue;
@@ -4505,7 +4491,9 @@ const javaAdapter = {
4505
4491
  const artifact = m[1].split(":")[1];
4506
4492
  for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (artifact !== void 0 && re.test(artifact)) frameworks.add(tag);
4507
4493
  }
4508
- } catch {}
4494
+ } catch {
4495
+ recordDegradation("java-manifest-unreadable");
4496
+ }
4509
4497
  }
4510
4498
  if (frameworks.size === 0) return {
4511
4499
  frameworks: [],
@@ -4593,8 +4581,7 @@ function javaFileTags(file) {
4593
4581
  function readText$1(path) {
4594
4582
  return readFileSync(path, "utf8");
4595
4583
  }
4596
- //#endregion
4597
- //#region src/adapters/csharp.ts
4584
+ // src/adapters/csharp.ts
4598
4585
  /**
4599
4586
  * C#/.NET adapter (Upgrade-Plan-v3 Phase 5).
4600
4587
  *
@@ -4664,9 +4651,13 @@ const csharpAdapter = {
4664
4651
  const pkg = m[1];
4665
4652
  for (const { re, tag } of CS_PACKAGE_TAGS) if (re.test(pkg)) frameworks.add(tag);
4666
4653
  }
4667
- } catch {}
4654
+ } catch {
4655
+ recordDegradation("csharp-project-unreadable");
4656
+ }
4668
4657
  }
4669
- } catch {}
4658
+ } catch {
4659
+ recordDegradation("csharp-project-unreadable");
4660
+ }
4670
4661
  if (frameworks.size === 0) return {
4671
4662
  frameworks: [],
4672
4663
  unknown: true
@@ -4750,8 +4741,7 @@ function csharpFileTags(file) {
4750
4741
  function readText(path) {
4751
4742
  return readFileSync(path, "utf8");
4752
4743
  }
4753
- //#endregion
4754
- //#region src/discovery/yaml-guards.ts
4744
+ // src/discovery/yaml-guards.ts
4755
4745
  /**
4756
4746
  * Shared hostile-YAML guards (product-gap master plan P3 common law —
4757
4747
  * `azure-pipelines.yml` detection rides the same YAML machinery as the
@@ -4820,8 +4810,7 @@ function enforceDepthCap(value, max, depthMessage = GENERIC_LABELS.depthMessage)
4820
4810
  });
4821
4811
  }
4822
4812
  }
4823
- //#endregion
4824
- //#region src/discovery/workflow-parser.ts
4813
+ // src/discovery/workflow-parser.ts
4825
4814
  /**
4826
4815
  * Safe GitHub Actions workflow parser (Phase 0.3, W4-03 debt).
4827
4816
  *
@@ -4867,8 +4856,7 @@ function parseWorkflow(text) {
4867
4856
  }
4868
4857
  return { jobs };
4869
4858
  }
4870
- //#endregion
4871
- //#region src/adapters/github-actions.ts
4859
+ // src/adapters/github-actions.ts
4872
4860
  /**
4873
4861
  * GitHub Actions adapter (R1).
4874
4862
  * Owns workflow discovery and safe-YAML parsing; CI rules consume the
@@ -4953,8 +4941,7 @@ const githubActionsAdapter = {
4953
4941
  };
4954
4942
  var WorkflowParseSkipped = class extends Error {};
4955
4943
  const LIMITS_MAX_BYTES$2 = 1048576;
4956
- //#endregion
4957
- //#region src/discovery/azure-pipeline-parser.ts
4944
+ // src/discovery/azure-pipeline-parser.ts
4958
4945
  /**
4959
4946
  * Safe Azure DevOps pipeline parser (product-gap master plan P3b).
4960
4947
  *
@@ -5188,8 +5175,7 @@ function isAzurePipelineFixture(normalizedPath) {
5188
5175
  const base = normalizedPath.replaceAll("\\", "/").split("/").pop() ?? "";
5189
5176
  return AZURE_PIPELINE_FILENAMES_SET.has(base);
5190
5177
  }
5191
- //#endregion
5192
- //#region src/adapters/azure-pipelines.ts
5178
+ // src/adapters/azure-pipelines.ts
5193
5179
  /**
5194
5180
  * Azure DevOps pipeline adapter (product-gap master plan P3b).
5195
5181
  * Owns `azure-pipelines.yml` discovery (repo root, the platform's
@@ -5269,8 +5255,7 @@ const azurePipelinesAdapter = {
5269
5255
  };
5270
5256
  var AzureParseSkipped = class extends Error {};
5271
5257
  const LIMITS_MAX_BYTES$1 = 1048576;
5272
- //#endregion
5273
- //#region src/adapters/jenkins.ts
5258
+ // src/adapters/jenkins.ts
5274
5259
  /**
5275
5260
  * Jenkins adapter (product-gap master plan P3c).
5276
5261
  * The root `Jenkinsfile` is a TEXT-target file kind: declarative and
@@ -5344,8 +5329,7 @@ const jenkinsAdapter = {
5344
5329
  }
5345
5330
  };
5346
5331
  const LIMITS_MAX_BYTES = 1048576;
5347
- //#endregion
5348
- //#region src/discovery/scan-adapters.ts
5332
+ // src/discovery/scan-adapters.ts
5349
5333
  /**
5350
5334
  * The canonical adapter registry (audits H-6/H-9).
5351
5335
  *
@@ -5519,8 +5503,7 @@ const SEARCHED_FOR = [
5519
5503
  globs: jenkinsAdapter.testFileGlobs
5520
5504
  }
5521
5505
  ];
5522
- //#endregion
5523
- //#region src/scope/changed.ts
5506
+ // src/scope/changed.ts
5524
5507
  /**
5525
5508
  * Changed-scope engine (Sprint-Plan W6, Product-MVP §9 `--scope changed`).
5526
5509
  *
@@ -5625,6 +5608,7 @@ function computeChangedScope(root, baseBranch) {
5625
5608
  ...untrackedSet
5626
5609
  ])].sort();
5627
5610
  const changed = {};
5611
+ const fullyChanged = /* @__PURE__ */ new Set();
5628
5612
  const batchDiff = (args, files) => {
5629
5613
  const perFile = /* @__PURE__ */ new Map();
5630
5614
  if (files.length === 0) return perFile;
@@ -5676,7 +5660,8 @@ function computeChangedScope(root, baseBranch) {
5676
5660
  if (untrackedSet.has(file)) {
5677
5661
  const all = allLinesOf(root, file);
5678
5662
  if (all === null) {
5679
- changed[file] = new Set(Array.from({ length: LIMITS_MAX_LINES }, (_, i) => i + 1));
5663
+ fullyChanged.add(file);
5664
+ changed[file] = lines;
5680
5665
  continue;
5681
5666
  }
5682
5667
  for (const l of all) lines.add(l);
@@ -5685,11 +5670,10 @@ function computeChangedScope(root, baseBranch) {
5685
5670
  }
5686
5671
  return {
5687
5672
  changed,
5673
+ ...fullyChanged.size > 0 ? { fullyChanged } : {},
5688
5674
  degraded: false
5689
5675
  };
5690
5676
  }
5691
- /** Sentinel size for "fully changed" — bounded, mirrors LIMITS.maxFileBytes scale. */
5692
- const LIMITS_MAX_LINES = 1e6;
5693
5677
  /** Every 1-based line number of an untracked file (it is all new). */
5694
5678
  function allLinesOf(root, file) {
5695
5679
  const full = join(root, file);
@@ -5739,6 +5723,7 @@ function parseChangedLines(diff) {
5739
5723
  function filterToChanged(findings, diff) {
5740
5724
  if (diff.degraded) return findings;
5741
5725
  return findings.filter((f) => {
5726
+ if (diff.fullyChanged?.has(f.file)) return true;
5742
5727
  const lines = diff.changed[f.file];
5743
5728
  if (!lines) return false;
5744
5729
  for (let l = Math.max(1, f.line - 3); l <= f.line; l++) if (lines.has(l)) return true;
@@ -5763,8 +5748,7 @@ function computeStagedFiles(root) {
5763
5748
  if (raw === null) return null;
5764
5749
  return raw.split("\0").filter((s) => s.length > 0);
5765
5750
  }
5766
- //#endregion
5767
- //#region src/rules/rule.ts
5751
+ // src/rules/rule.ts
5768
5752
  /**
5769
5753
  * Invoke an `astQuery` hook with the mandatory-fallback contract: no
5770
5754
  * hook or no AST on the context resolves to `undefined` (→ regex
@@ -5777,8 +5761,7 @@ function tryAstQuery(hook, ctx) {
5777
5761
  function defineRule(rule) {
5778
5762
  return rule;
5779
5763
  }
5780
- //#endregion
5781
- //#region src/rules/shared/positions.ts
5764
+ // src/rules/shared/positions.ts
5782
5765
  /**
5783
5766
  * Shared position helpers (Phase 6 — Tempering Plan).
5784
5767
  * One lineAt, one colAt, one matchBrace. The other 79 copies are dead.
@@ -5823,8 +5806,7 @@ function lineAt(text, index) {
5823
5806
  function colAt(text, index) {
5824
5807
  return index - (index <= 0 ? -1 : text.lastIndexOf("\n", index - 1));
5825
5808
  }
5826
- //#endregion
5827
- //#region src/rules/test/qa-test-001-focused-test.ts
5809
+ // src/rules/test/qa-test-001-focused-test.ts
5828
5810
  /**
5829
5811
  * QA-TEST-001 — Focused test committed (`fit`/`fdescribe`/`.only`).
5830
5812
  * Severity: error · Confidence: high · deterministic-defect
@@ -5890,8 +5872,7 @@ const focusedTestCommitted = defineRule({
5890
5872
  return findings;
5891
5873
  }
5892
5874
  });
5893
- //#endregion
5894
- //#region src/rules/test/qa-test-002-skipped-test.ts
5875
+ // src/rules/test/qa-test-002-skipped-test.ts
5895
5876
  /**
5896
5877
  * QA-TEST-002 — Skipped test (`it.skip`, `xit`, `test.skip`, `describe.skip`).
5897
5878
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5947,8 +5928,7 @@ const skippedTest = defineRule({
5947
5928
  return findings;
5948
5929
  }
5949
5930
  });
5950
- //#endregion
5951
- //#region src/rules/shared/masking.ts
5931
+ // src/rules/shared/masking.ts
5952
5932
  /** True when the offset was blanked in codeText (inside a string or comment). */
5953
5933
  function isMasked(ctx, index) {
5954
5934
  const mask = ctx.codeText;
@@ -6009,8 +5989,7 @@ function isInsideEmbeddedCode(ctx, index) {
6009
5989
  const hasCallSyntax = /\w\s*\(/.test(inner);
6010
5990
  return hasNestedQuote && hasCallSyntax;
6011
5991
  }
6012
- //#endregion
6013
- //#region src/rules/test/qa-test-003-no-assertions.ts
5992
+ // src/rules/test/qa-test-003-no-assertions.ts
6014
5993
  /**
6015
5994
  * QA-TEST-003 — Test with no assertions.
6016
5995
  * Severity: error · Confidence: high · deterministic-defect
@@ -6088,8 +6067,7 @@ function matchBrace$2(text, open) {
6088
6067
  }
6089
6068
  return -1;
6090
6069
  }
6091
- //#endregion
6092
- //#region src/rules/test/qa-test-004-hard-sleep.ts
6070
+ // src/rules/test/qa-test-004-hard-sleep.ts
6093
6071
  /**
6094
6072
  * QA-TEST-004 — Hard sleep (`sleep()`, `waitForTimeout()`, `setTimeout` as wait).
6095
6073
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6154,8 +6132,7 @@ const hardSleep = defineRule({
6154
6132
  return findings;
6155
6133
  }
6156
6134
  });
6157
- //#endregion
6158
- //#region src/rules/test/qa-test-006-retry-abuse.ts
6135
+ // src/rules/test/qa-test-006-retry-abuse.ts
6159
6136
  /**
6160
6137
  * QA-TEST-006 — Retry abuse in test config.
6161
6138
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6221,8 +6198,7 @@ const retryAbuse = defineRule({
6221
6198
  return findings;
6222
6199
  }
6223
6200
  });
6224
- //#endregion
6225
- //#region src/rules/test/qa-test-010-empty-body.ts
6201
+ // src/rules/test/qa-test-010-empty-body.ts
6226
6202
  /**
6227
6203
  * QA-TEST-010 — Empty test body.
6228
6204
  * Severity: error · Confidence: high · deterministic-defect
@@ -6277,8 +6253,7 @@ const emptyTestBody = defineRule({
6277
6253
  return findings;
6278
6254
  }
6279
6255
  });
6280
- //#endregion
6281
- //#region src/rules/quality/qa-tqual-002-tautological.ts
6256
+ // src/rules/quality/qa-tqual-002-tautological.ts
6282
6257
  /**
6283
6258
  * QA-TQUAL-002 — Tautological assertion.
6284
6259
  * Severity: error · Confidence: high · deterministic-defect
@@ -6355,8 +6330,7 @@ const tautologicalAssertion = defineRule({
6355
6330
  return findings;
6356
6331
  }
6357
6332
  });
6358
- //#endregion
6359
- //#region src/rules/quality/qa-tqual-009-promise-assertion.ts
6333
+ // src/rules/quality/qa-tqual-009-promise-assertion.ts
6360
6334
  /**
6361
6335
  * QA-TQUAL-009 — Unawaited assertion inside a promise chain.
6362
6336
  * Severity: error · Confidence: high · deterministic-defect
@@ -6526,8 +6500,7 @@ function matchParen$2(text, open) {
6526
6500
  }
6527
6501
  return -1;
6528
6502
  }
6529
- //#endregion
6530
- //#region src/rules/quality/qa-tqual-011-commented-out.ts
6503
+ // src/rules/quality/qa-tqual-011-commented-out.ts
6531
6504
  /**
6532
6505
  * QA-TQUAL-011 — Commented-out tests.
6533
6506
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6640,8 +6613,7 @@ function commentRanges(ctx) {
6640
6613
  }
6641
6614
  return ranges;
6642
6615
  }
6643
- //#endregion
6644
- //#region src/rules/playwright/qa-pw-002-unawaited-assertion.ts
6616
+ // src/rules/playwright/qa-pw-002-unawaited-assertion.ts
6645
6617
  /**
6646
6618
  * QA-PW-002 — Missing `await` on a Playwright locator assertion.
6647
6619
  * Severity: error · Confidence: high · deterministic-defect
@@ -6786,8 +6758,7 @@ function runRegexFallback(text, path) {
6786
6758
  });
6787
6759
  return findings;
6788
6760
  }
6789
- //#endregion
6790
- //#region src/rules/playwright/qa-pw-003-debug-artifacts.ts
6761
+ // src/rules/playwright/qa-pw-003-debug-artifacts.ts
6791
6762
  /**
6792
6763
  * QA-PW-003 — Committed `page.pause()` or `test.only()` in e2e specs.
6793
6764
  * Severity: error · Confidence: high · deterministic-defect
@@ -6839,8 +6810,7 @@ const committedDebugArtifacts = defineRule({
6839
6810
  return findings;
6840
6811
  }
6841
6812
  });
6842
- //#endregion
6843
- //#region src/rules/playwright/qa-pw-004-brittle-selectors.ts
6813
+ // src/rules/playwright/qa-pw-004-brittle-selectors.ts
6844
6814
  /**
6845
6815
  * QA-PW-004 — Brittle CSS/XPath selectors vs role-based locators.
6846
6816
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -6932,8 +6902,7 @@ function stripComments(ctx) {
6932
6902
  } catch {}
6933
6903
  return ctx.text.replace(/\/\/[^\n]*/g, (cm) => " ".repeat(cm.length));
6934
6904
  }
6935
- //#endregion
6936
- //#region src/rules/ci/verification-gate.ts
6905
+ // src/rules/ci/verification-gate.ts
6937
6906
  /**
6938
6907
  * Shared "is this shell command a verification gate?" matcher for the QA-CI
6939
6908
  * rules. A gate is a command whose failure MUST fail the job — test runners,
@@ -6973,8 +6942,7 @@ const VERIFICATION_GATE_RE = new RegExp([
6973
6942
  function looksLikeVerificationGate(cmd) {
6974
6943
  return VERIFICATION_GATE_RE.test(cmd);
6975
6944
  }
6976
- //#endregion
6977
- //#region src/rules/ci/azure-gates.ts
6945
+ // src/rules/ci/azure-gates.ts
6978
6946
  /** Discriminator: the Azure parser is the only one emitting `platform`. */
6979
6947
  function isAzurePipelineDoc(ast) {
6980
6948
  return typeof ast === "object" && ast !== null && ast.platform === "azure-pipelines";
@@ -7058,8 +7026,7 @@ function locateAzureJobKey(text, job, keyName) {
7058
7026
  if (m) return lineAt(text, m.index);
7059
7027
  return 1;
7060
7028
  }
7061
- //#endregion
7062
- //#region src/rules/ci/qa-ci-001-continue-on-error.ts
7029
+ // src/rules/ci/qa-ci-001-continue-on-error.ts
7063
7030
  /**
7064
7031
  * QA-CI-001 — `continue-on-error: true` masks a failing job.
7065
7032
  * Severity: error · Confidence: high · deterministic-defect
@@ -7287,8 +7254,7 @@ function describeAzureStep(step) {
7287
7254
  if (step.task) return step.task;
7288
7255
  return "step";
7289
7256
  }
7290
- //#endregion
7291
- //#region src/rules/ci/qa-ci-002-swallowed-exit.ts
7257
+ // src/rules/ci/qa-ci-002-swallowed-exit.ts
7292
7258
  /**
7293
7259
  * QA-CI-002 — `|| true` swallows a command's exit code.
7294
7260
  * Severity: error · Confidence: high · deterministic-defect
@@ -7343,8 +7309,7 @@ const swallowedExitCode = defineRule({
7343
7309
  return findings;
7344
7310
  }
7345
7311
  });
7346
- //#endregion
7347
- //#region src/rules/ci/qa-ci-007-retry-masking.ts
7312
+ // src/rules/ci/qa-ci-007-retry-masking.ts
7348
7313
  /**
7349
7314
  * QA-CI-007 — Retry masking test failures.
7350
7315
  * Severity: warning · Confidence: high · deterministic-defect
@@ -7495,8 +7460,7 @@ function azureArm$1(ctx, doc, findings) {
7495
7460
  });
7496
7461
  }
7497
7462
  }
7498
- //#endregion
7499
- //#region src/rules/ci/qa-ci-005-report-never-generated.ts
7463
+ // src/rules/ci/qa-ci-005-report-never-generated.ts
7500
7464
  /**
7501
7465
  * QA-CI-005 — Report consumed but never generated.
7502
7466
  * Severity: error · Confidence: high · deterministic-defect
@@ -7654,8 +7618,7 @@ function findJobConsumerLine(text, jobName, consumerRe) {
7654
7618
  const anywhere = new RegExp(consumerRe.source, "i").exec(text);
7655
7619
  return anywhere ? lineAt(text, anywhere.index) : 1;
7656
7620
  }
7657
- //#endregion
7658
- //#region src/rules/ci/jenkins-gates.ts
7621
+ // src/rules/ci/jenkins-gates.ts
7659
7622
  /**
7660
7623
  * Jenkinsfile block scanner for the QA-CI Jenkins arms (product-gap
7661
7624
  * master plan P3c). Jenkinsfiles are Groovy text — "no new language
@@ -7901,8 +7864,7 @@ function textIsVerificationGate(text) {
7901
7864
  function lineOfOffset(text, offset) {
7902
7865
  return lineAt(text, offset);
7903
7866
  }
7904
- //#endregion
7905
- //#region src/rules/ci/qa-ci-008-always-success.ts
7867
+ // src/rules/ci/qa-ci-008-always-success.ts
7906
7868
  /**
7907
7869
  * QA-CI-008 — Always-success step at the end of a job.
7908
7870
  * Severity: error · Confidence: high · deterministic-defect
@@ -8076,8 +8038,7 @@ function jenkinsArm$1(ctx, findings) {
8076
8038
  });
8077
8039
  }
8078
8040
  }
8079
- //#endregion
8080
- //#region src/rules/ci/qa-ci-009-exit-code.ts
8041
+ // src/rules/ci/qa-ci-009-exit-code.ts
8081
8042
  /**
8082
8043
  * QA-CI-009 — Test command does not propagate exit code.
8083
8044
  * Severity: error · Confidence: high · deterministic-defect
@@ -8212,8 +8173,7 @@ function jenkinsArm(ctx, findings) {
8212
8173
  });
8213
8174
  }
8214
8175
  }
8215
- //#endregion
8216
- //#region src/rules/ci/qa-ci-010-non-blocking.ts
8176
+ // src/rules/ci/qa-ci-010-non-blocking.ts
8217
8177
  /**
8218
8178
  * QA-CI-010 — Required test job is non-blocking for branch protection.
8219
8179
  * Severity: error · Confidence: medium · deterministic-defect
@@ -8296,8 +8256,7 @@ function findLine(text, re) {
8296
8256
  function escapeRe(s) {
8297
8257
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8298
8258
  }
8299
- //#endregion
8300
- //#region src/rules/ci/qa-ci-013-can-never-fail.ts
8259
+ // src/rules/ci/qa-ci-013-can-never-fail.ts
8301
8260
  /**
8302
8261
  * QA-CI-013 — Verification stage/job conditioned so it can never fail the
8303
8262
  * pipeline (product-gap master plan P3b).
@@ -8401,8 +8360,7 @@ const canNeverFailGate = defineRule({
8401
8360
  function jobHasGate(job) {
8402
8361
  return (job.steps ?? []).some((s) => s !== void 0 && stepIsVerificationGate$1(s));
8403
8362
  }
8404
- //#endregion
8405
- //#region src/rules/ci/qa-ci-014-swallowed-verification.ts
8363
+ // src/rules/ci/qa-ci-014-swallowed-verification.ts
8406
8364
  /**
8407
8365
  * QA-CI-014 — try/catch swallowing a verification-stage failure without
8408
8366
  * rethrow or failure-marking (product-gap master plan P3c).
@@ -8459,8 +8417,7 @@ const swallowedVerificationFailure = defineRule({
8459
8417
  return findings;
8460
8418
  }
8461
8419
  });
8462
- //#endregion
8463
- //#region src/rules/python/qa-py-003-no-assertions.ts
8420
+ // src/rules/python/qa-py-003-no-assertions.ts
8464
8421
  /**
8465
8422
  * QA-PY-003 — Test function with no assertions.
8466
8423
  * Severity: error · Confidence: high · deterministic-defect
@@ -8581,8 +8538,7 @@ function extractBlock$1(text, afterColon) {
8581
8538
  const lineEnd = rest.indexOf("\n", firstIdx);
8582
8539
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
8583
8540
  }
8584
- //#endregion
8585
- //#region src/rules/python/qa-py-005-hard-sleep.ts
8541
+ // src/rules/python/qa-py-005-hard-sleep.ts
8586
8542
  /**
8587
8543
  * QA-PY-005 — time.sleep() in tests.
8588
8544
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8629,8 +8585,7 @@ const pyHardSleep = defineRule({
8629
8585
  return findings;
8630
8586
  }
8631
8587
  });
8632
- //#endregion
8633
- //#region src/rules/python/qa-py-002-skipped-test.ts
8588
+ // src/rules/python/qa-py-002-skipped-test.ts
8634
8589
  /**
8635
8590
  * QA-PY-002 — Skipped test (`@pytest.mark.skip`, `xfail` non-strict).
8636
8591
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8690,8 +8645,7 @@ const pySkippedTest = defineRule({
8690
8645
  return findings;
8691
8646
  }
8692
8647
  });
8693
- //#endregion
8694
- //#region src/rules/python/qa-py-012-tautological.ts
8648
+ // src/rules/python/qa-py-012-tautological.ts
8695
8649
  /**
8696
8650
  * QA-PY-012 — Tautological assertion (`assert True`, `assert x == x`).
8697
8651
  * Severity: error · Confidence: high · deterministic-defect
@@ -8746,8 +8700,7 @@ const pyTautological = defineRule({
8746
8700
  return findings;
8747
8701
  }
8748
8702
  });
8749
- //#endregion
8750
- //#region src/rules/python/qa-py-001-focused-test.ts
8703
+ // src/rules/python/qa-py-001-focused-test.ts
8751
8704
  /**
8752
8705
  * QA-PY-001 — Focused test committed.
8753
8706
  * Severity: error · Confidence: high · deterministic-defect
@@ -8801,8 +8754,7 @@ const pyFocusedTest = defineRule({
8801
8754
  return findings;
8802
8755
  }
8803
8756
  });
8804
- //#endregion
8805
- //#region src/engine/jv-cs-ast.ts
8757
+ // src/engine/jv-cs-ast.ts
8806
8758
  /** Narrow the loose `ast` seam back to a tree-sitter Tree. */
8807
8759
  function getTreeSitterTree(ast) {
8808
8760
  if (!(ast instanceof Object) || !("rootNode" in ast)) return void 0;
@@ -9006,8 +8958,7 @@ function isHelperIdiom(name) {
9006
8958
  }
9007
8959
  return false;
9008
8960
  }
9009
- //#endregion
9010
- //#region src/engine/python-ast.ts
8961
+ // src/engine/python-ast.ts
9011
8962
  function getPythonTree(ast) {
9012
8963
  return getTreeSitterTree(ast);
9013
8964
  }
@@ -9092,8 +9043,7 @@ function pythonWithRaisesBlocks(tree) {
9092
9043
  });
9093
9044
  return out;
9094
9045
  }
9095
- //#endregion
9096
- //#region src/rules/python/qa-py-007-raises-without-match.ts
9046
+ // src/rules/python/qa-py-007-raises-without-match.ts
9097
9047
  /**
9098
9048
  * QA-PY-007 — pytest.raises without match.
9099
9049
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9220,8 +9170,7 @@ function astArm(ctx, tree) {
9220
9170
  }
9221
9171
  return findings;
9222
9172
  }
9223
- //#endregion
9224
- //#region src/rules/python/qa-py-009-commented-out.ts
9173
+ // src/rules/python/qa-py-009-commented-out.ts
9225
9174
  /**
9226
9175
  * QA-PY-009 — Commented-out tests.
9227
9176
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9268,8 +9217,7 @@ const pyCommentedOutTest = defineRule({
9268
9217
  return findings;
9269
9218
  }
9270
9219
  });
9271
- //#endregion
9272
- //#region src/rules/python/qa-py-004-bare-truthiness.ts
9220
+ // src/rules/python/qa-py-004-bare-truthiness.ts
9273
9221
  /**
9274
9222
  * QA-PY-004 — Bare truthiness assert on complex object.
9275
9223
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9348,8 +9296,7 @@ const pyBareTruthinessAssert = defineRule({
9348
9296
  return findings;
9349
9297
  }
9350
9298
  });
9351
- //#endregion
9352
- //#region src/rules/python/qa-py-011-mutable-fixture.ts
9299
+ // src/rules/python/qa-py-011-mutable-fixture.ts
9353
9300
  /**
9354
9301
  * QA-PY-011 — Mutable fixture shared across tests.
9355
9302
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9400,8 +9347,7 @@ const pyMutableFixture = defineRule({
9400
9347
  return findings;
9401
9348
  }
9402
9349
  });
9403
- //#endregion
9404
- //#region src/rules/playwright/qa-pw-101-wait-for-timeout.ts
9350
+ // src/rules/playwright/qa-pw-101-wait-for-timeout.ts
9405
9351
  /**
9406
9352
  * QA-PW-101 — waitForTimeout() anywhere.
9407
9353
  * Severity: error · Confidence: high · deterministic-defect
@@ -9448,8 +9394,7 @@ const pwWaitForTimeout = defineRule({
9448
9394
  return findings;
9449
9395
  }
9450
9396
  });
9451
- //#endregion
9452
- //#region src/rules/playwright/qa-pw-102-wait-load.ts
9397
+ // src/rules/playwright/qa-pw-102-wait-load.ts
9453
9398
  /**
9454
9399
  * QA-PW-102 — waitForEvent('load') / waitForLoadState('load') instead of
9455
9400
  * web-first assertions.
@@ -9503,8 +9448,7 @@ const pwWaitForLoadEvent = defineRule({
9503
9448
  return findings;
9504
9449
  }
9505
9450
  });
9506
- //#endregion
9507
- //#region src/rules/playwright/qa-pw-113-deep-frames.ts
9451
+ // src/rules/playwright/qa-pw-113-deep-frames.ts
9508
9452
  /**
9509
9453
  * QA-PW-113 — frameLocator chains > 2 deep.
9510
9454
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9554,8 +9498,7 @@ const pwDeepFrameLocator = defineRule({
9554
9498
  function count(fragment) {
9555
9499
  return fragment.match(/frameLocator\s*\(/g).length;
9556
9500
  }
9557
- //#endregion
9558
- //#region src/rules/playwright/qa-pw-117-serial.ts
9501
+ // src/rules/playwright/qa-pw-117-serial.ts
9559
9502
  /**
9560
9503
  * QA-PW-117 — test.describe.serial without justification comment.
9561
9504
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9609,8 +9552,7 @@ const pwSerialNoJustification = defineRule({
9609
9552
  return findings;
9610
9553
  }
9611
9554
  });
9612
- //#endregion
9613
- //#region src/rules/playwright/qa-pw-121-config-retries.ts
9555
+ // src/rules/playwright/qa-pw-121-config-retries.ts
9614
9556
  /**
9615
9557
  * QA-PW-121 — playwright.config: retries >= 3 or workers = 0.
9616
9558
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9675,8 +9617,7 @@ const pwConfigRetryAbuse = defineRule({
9675
9617
  return findings;
9676
9618
  }
9677
9619
  });
9678
- //#endregion
9679
- //#region src/rules/playwright/qa-pw-122-no-trace.ts
9620
+ // src/rules/playwright/qa-pw-122-no-trace.ts
9680
9621
  /**
9681
9622
  * QA-PW-122 — No trace capture on retry in playwright.config.
9682
9623
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9725,8 +9666,7 @@ const pwNoTraceOnRetry = defineRule({
9725
9666
  return findings;
9726
9667
  }
9727
9668
  });
9728
- //#endregion
9729
- //#region src/rules/playwright/qa-pw-124-project-split.ts
9669
+ // src/rules/playwright/qa-pw-124-project-split.ts
9730
9670
  /**
9731
9671
  * QA-PW-124 — Projects defined but no smoke/regression split.
9732
9672
  * Severity: info · Confidence: high · deterministic-defect
@@ -9788,8 +9728,7 @@ const pwNoProjectSplit = defineRule({
9788
9728
  return findings;
9789
9729
  }
9790
9730
  });
9791
- //#endregion
9792
- //#region src/rules/playwright/qa-pw-104-trial-click.ts
9731
+ // src/rules/playwright/qa-pw-104-trial-click.ts
9793
9732
  /**
9794
9733
  * QA-PW-104 — `{ trial: true }` misuse — check-then-act race.
9795
9734
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9835,8 +9774,7 @@ const pwTrialMisuse = defineRule({
9835
9774
  return findings;
9836
9775
  }
9837
9776
  });
9838
- //#endregion
9839
- //#region src/rules/playwright/qa-pw-116-storage-state.ts
9777
+ // src/rules/playwright/qa-pw-116-storage-state.ts
9840
9778
  /**
9841
9779
  * QA-PW-116 — storageState reuse without expiry strategy.
9842
9780
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9882,8 +9820,7 @@ const pwStorageStateNoExpiry = defineRule({
9882
9820
  return findings;
9883
9821
  }
9884
9822
  });
9885
- //#endregion
9886
- //#region src/rules/playwright/qa-pw-125-global-setup.ts
9823
+ // src/rules/playwright/qa-pw-125-global-setup.ts
9887
9824
  /**
9888
9825
  * QA-PW-125 — globalSetup mutating shared external state.
9889
9826
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9941,8 +9878,7 @@ const pwGlobalSetupSharedState = defineRule({
9941
9878
  return findings;
9942
9879
  }
9943
9880
  });
9944
- //#endregion
9945
- //#region src/rules/playwright/qa-pw-115-shared-page.ts
9881
+ // src/rules/playwright/qa-pw-115-shared-page.ts
9946
9882
  /**
9947
9883
  * QA-PW-115 — Shared `page` across parallel tests without isolation.
9948
9884
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9990,8 +9926,7 @@ const pwSharedPage = defineRule({
9990
9926
  return findings;
9991
9927
  }
9992
9928
  });
9993
- //#endregion
9994
- //#region src/rules/playwright/qa-pw-140.ts
9929
+ // src/rules/playwright/qa-pw-140.ts
9995
9930
  /**
9996
9931
  * QA-PW-140 — Screenshot without maxDiffPixelRatio.
9997
9932
  *
@@ -10065,8 +10000,7 @@ function matchParen(text, open) {
10065
10000
  }
10066
10001
  return -1;
10067
10002
  }
10068
- //#endregion
10069
- //#region src/rules/playwright/qa-pw-123-hardcoded-url.ts
10003
+ // src/rules/playwright/qa-pw-123-hardcoded-url.ts
10070
10004
  /**
10071
10005
  * QA-PW-123 — Hardcoded baseURL in specs.
10072
10006
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10114,8 +10048,7 @@ const hardcodedBaseUrl = defineRule({
10114
10048
  return findings;
10115
10049
  }
10116
10050
  });
10117
- //#endregion
10118
- //#region src/rules/quality/qa-env-001-env-coupling.ts
10051
+ // src/rules/quality/qa-env-001-env-coupling.ts
10119
10052
  /**
10120
10053
  * QA-ENV-001 — "Works On My Machine" detector core (Tier 5 #24).
10121
10054
  * Environment coupling in test files: fixed ports, OS paths,
@@ -10182,8 +10115,7 @@ const envCoupling = defineRule({
10182
10115
  return findings;
10183
10116
  }
10184
10117
  });
10185
- //#endregion
10186
- //#region src/rules/playwright/qa-pw-141-retry-no-triage.ts
10118
+ // src/rules/playwright/qa-pw-141-retry-no-triage.ts
10187
10119
  /**
10188
10120
  * QA-PW-141 — playwright.config retries set high enough to mask flakes.
10189
10121
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10240,8 +10172,7 @@ const pwRetryMaskingNoForensics = defineRule({
10240
10172
  return findings;
10241
10173
  }
10242
10174
  });
10243
- //#endregion
10244
- //#region src/rules/playwright/qa-pw-142-blanket-route.ts
10175
+ // src/rules/playwright/qa-pw-142-blanket-route.ts
10245
10176
  /**
10246
10177
  * QA-PW-142 — page.route() blanket-mocking instead of scoped intercepts.
10247
10178
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -10296,8 +10227,7 @@ const pwBlanketRouteMock = defineRule({
10296
10227
  return findings;
10297
10228
  }
10298
10229
  });
10299
- //#endregion
10300
- //#region src/rules/playwright/qa-pw-143-no-artifacts.ts
10230
+ // src/rules/playwright/qa-pw-143-no-artifacts.ts
10301
10231
  /**
10302
10232
  * QA-PW-143 — No screenshot/video artifact capture on failure.
10303
10233
  * Severity: info · Confidence: high · deterministic-defect
@@ -10350,8 +10280,7 @@ const pwNoFailureArtifacts = defineRule({
10350
10280
  return findings;
10351
10281
  }
10352
10282
  });
10353
- //#endregion
10354
- //#region src/rules/playwright/qa-pw-144-single-browser.ts
10283
+ // src/rules/playwright/qa-pw-144-single-browser.ts
10355
10284
  /**
10356
10285
  * QA-PW-144 — Single-browser project matrix (no cross-browser coverage).
10357
10286
  * Severity: info · Confidence: high · deterministic-defect
@@ -10413,8 +10342,7 @@ const pwSingleBrowserMatrix = defineRule({
10413
10342
  return findings;
10414
10343
  }
10415
10344
  });
10416
- //#endregion
10417
- //#region src/rules/playwright/qa-pw-146-css-locator.ts
10345
+ // src/rules/playwright/qa-pw-146-css-locator.ts
10418
10346
  /**
10419
10347
  * QA-PW-146 — CSS/XPath string selector where Playwright's locator
10420
10348
  * standard expects a normalized locator (plan §17.3).
@@ -10493,8 +10421,7 @@ const pwLocatorNormalize = defineRule({
10493
10421
  return findings;
10494
10422
  }
10495
10423
  });
10496
- //#endregion
10497
- //#region src/rules/playwright/qa-pw-147-codegen-artifact.ts
10424
+ // src/rules/playwright/qa-pw-147-codegen-artifact.ts
10498
10425
  /**
10499
10426
  * QA-PW-147 — Playwright codegen artifact left unedited (plan §17.1).
10500
10427
  * Severity: info · Confidence: medium · observation
@@ -10553,8 +10480,7 @@ const pwCodegenArtifact = defineRule({
10553
10480
  return findings;
10554
10481
  }
10555
10482
  });
10556
- //#endregion
10557
- //#region src/rules/python/qa-py-101-sync-async-mix.ts
10483
+ // src/rules/python/qa-py-101-sync-async-mix.ts
10558
10484
  /**
10559
10485
  * QA-PY-101 — Playwright-Python: sync API in async test (or vice versa).
10560
10486
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10603,8 +10529,7 @@ const pyPwSyncAsyncMix = defineRule({
10603
10529
  return findings;
10604
10530
  }
10605
10531
  });
10606
- //#endregion
10607
- //#region src/rules/python/qa-py-103-wait-for-timeout.ts
10532
+ // src/rules/python/qa-py-103-wait-for-timeout.ts
10608
10533
  /**
10609
10534
  * QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
10610
10535
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10651,8 +10576,7 @@ const pyPwWaitForTimeout = defineRule({
10651
10576
  return findings;
10652
10577
  }
10653
10578
  });
10654
- //#endregion
10655
- //#region src/rules/python/qa-py-105-pw-no-assertions.ts
10579
+ // src/rules/python/qa-py-105-pw-no-assertions.ts
10656
10580
  /**
10657
10581
  * QA-PY-105 — Playwright-Python: no assertion in Playwright test.
10658
10582
  * Severity: error · Confidence: high · deterministic-defect
@@ -10732,8 +10656,7 @@ function extractBlock(text, afterColon) {
10732
10656
  const lineEnd = rest.indexOf("\n", firstContent.index);
10733
10657
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
10734
10658
  }
10735
- //#endregion
10736
- //#region src/rules/java/qa-jv-101-disabled-test.ts
10659
+ // src/rules/java/qa-jv-101-disabled-test.ts
10737
10660
  /**
10738
10661
  * QA-JV-101 — @Disabled test without justification.
10739
10662
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10787,8 +10710,7 @@ const jvDisabledTest = defineRule({
10787
10710
  return findings;
10788
10711
  }
10789
10712
  });
10790
- //#endregion
10791
- //#region src/rules/java/qa-jv-103-no-assertions.ts
10713
+ // src/rules/java/qa-jv-103-no-assertions.ts
10792
10714
  /**
10793
10715
  * QA-JV-103 — Test method without assertions.
10794
10716
  * Severity: error · Confidence: high · deterministic-defect
@@ -10945,8 +10867,7 @@ function matchBrace$1(text, open) {
10945
10867
  }
10946
10868
  return -1;
10947
10869
  }
10948
- //#endregion
10949
- //#region src/engine/qa-model.ts
10870
+ // src/engine/qa-model.ts
10950
10871
  /**
10951
10872
  * Extract the semantic model for one parsed file. Resolves undefined
10952
10873
  * when the language has no extractor or the parse stage produced no
@@ -11489,8 +11410,7 @@ function extractPythonModel(file) {
11489
11410
  nodes
11490
11411
  };
11491
11412
  }
11492
- //#endregion
11493
- //#region src/rules/java/qa-jv-105-wait-for-timeout.ts
11413
+ // src/rules/java/qa-jv-105-wait-for-timeout.ts
11494
11414
  /**
11495
11415
  * QA-JV-105 — waitForTimeout / hardcoded navigation waits.
11496
11416
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11575,8 +11495,7 @@ const jvWaitForTimeout = defineRule({
11575
11495
  return findings;
11576
11496
  }
11577
11497
  });
11578
- //#endregion
11579
- //#region src/rules/csharp/qa-cs-101-skipped-test.ts
11498
+ // src/rules/csharp/qa-cs-101-skipped-test.ts
11580
11499
  /**
11581
11500
  * QA-CS-101 — [Ignore]/[Skip] test without justification.
11582
11501
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11640,8 +11559,7 @@ const csSkippedTest = defineRule({
11640
11559
  return findings;
11641
11560
  }
11642
11561
  });
11643
- //#endregion
11644
- //#region src/rules/csharp/qa-cs-103-no-assertions.ts
11562
+ // src/rules/csharp/qa-cs-103-no-assertions.ts
11645
11563
  /**
11646
11564
  * QA-CS-103 — Test method without assertions.
11647
11565
  * Severity: error · Confidence: high · deterministic-defect
@@ -11675,7 +11593,6 @@ const csSkippedTest = defineRule({
11675
11593
  * Deep-diffing those requires symbol semantics Mjölnir deliberately does
11676
11594
  * not promise for C# (plan §13.4).
11677
11595
  */
11678
- /** Helper/verification idiom: verify/check/assert-prefixed names. */
11679
11596
  const WHY$5 = "Without an assertion the test can only fail by throwing — it cannot detect behavioral regressions.";
11680
11597
  const FIX$4 = "Add an assertion on the expected outcome, or remove the test.";
11681
11598
  function makeFinding(path, position, name) {
@@ -11839,8 +11756,7 @@ function matchBrace(text, open) {
11839
11756
  }
11840
11757
  return -1;
11841
11758
  }
11842
- //#endregion
11843
- //#region src/rules/csharp/qa-cs-105-wait-for-timeout.ts
11759
+ // src/rules/csharp/qa-cs-105-wait-for-timeout.ts
11844
11760
  /**
11845
11761
  * QA-CS-105 — Playwright-.NET: WaitForTimeoutAsync hard sleep.
11846
11762
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11928,8 +11844,7 @@ const csWaitForTimeout = defineRule({
11928
11844
  return findings;
11929
11845
  }
11930
11846
  });
11931
- //#endregion
11932
- //#region src/rules/cypress/qa-cyp-001-cy-wait.ts
11847
+ // src/rules/cypress/qa-cyp-001-cy-wait.ts
11933
11848
  /**
11934
11849
  * QA-CYP-001 — Fixed `cy.wait(n)` (hard-coded wait).
11935
11850
  * Severity: warning · Confidence: high · deterministic-defect
@@ -12008,8 +11923,7 @@ function isCypressFile(ctx) {
12008
11923
  if (/\.cy\.[jt]sx?$/.test(ctx.path)) return true;
12009
11924
  return /\bcy\s*\./.test(ctx.codeText ?? ctx.text);
12010
11925
  }
12011
- //#endregion
12012
- //#region src/rules/cypress/qa-cyp-002-focused-test.ts
11926
+ // src/rules/cypress/qa-cyp-002-focused-test.ts
12013
11927
  /**
12014
11928
  * QA-CYP-002 — Focused test (`.only`).
12015
11929
  * Severity: error · Confidence: high · deterministic-defect
@@ -12072,8 +11986,7 @@ const cypFocusedTest = defineRule({
12072
11986
  return findings;
12073
11987
  }
12074
11988
  });
12075
- //#endregion
12076
- //#region src/rules/cypress/qa-cyp-003-config-security.ts
11989
+ // src/rules/cypress/qa-cyp-003-config-security.ts
12077
11990
  /**
12078
11991
  * QA-CYP-003 — Cypress config disables `chromeWebSecurity`.
12079
11992
  * Severity: error · Confidence: high · deterministic-defect
@@ -12143,8 +12056,7 @@ const cypConfigSecurity = defineRule({
12143
12056
  return findings;
12144
12057
  }
12145
12058
  });
12146
- //#endregion
12147
- //#region src/rules/selenium/qa-se-sleep-lookup.ts
12059
+ // src/rules/selenium/qa-se-sleep-lookup.ts
12148
12060
  /**
12149
12061
  * Selenium sleep-then-interact rules (Verification Trust Evolution Plan
12150
12062
  * §15.3 — "Selenium implicit-wait abuse / missing WebDriverWait",
@@ -12295,8 +12207,7 @@ const sePythonSleepLookup = defineRule({
12295
12207
  return sleepBeforeLookup(ctx, PY_SLEEP_RE, PY_LOOKUP_RE, "QA-SE-003");
12296
12208
  }
12297
12209
  });
12298
- //#endregion
12299
- //#region src/rules/shared/family.ts
12210
+ // src/rules/shared/family.ts
12300
12211
  /**
12301
12212
  * Cross-language pattern rule family generator (Phase 6 — Tempering Plan).
12302
12213
  *
@@ -12362,8 +12273,7 @@ function definePatternFamily(opts) {
12362
12273
  }
12363
12274
  }));
12364
12275
  }
12365
- //#endregion
12366
- //#region src/rules/families/hard-sleep.ts
12276
+ // src/rules/families/hard-sleep.ts
12367
12277
  /**
12368
12278
  * Hard-sleep family (Phase 6 — Tempering Plan).
12369
12279
  * Java: Thread.sleep(), C#: Thread.Sleep() / Task.Delay()
@@ -12514,8 +12424,7 @@ const hardSleepFamily = definePatternFamily({
12514
12424
  fix: "Use `await Assertions.Expect(locator).ToBeVisibleAsync()` or locator.WaitForAsync()."
12515
12425
  }]
12516
12426
  });
12517
- //#endregion
12518
- //#region src/rules/families/network-idle.ts
12427
+ // src/rules/families/network-idle.ts
12519
12428
  /**
12520
12429
  * Network-idle family (Phase 6 — Tempering Plan).
12521
12430
  * All three language variants of waitForLoadState(networkidle).
@@ -12576,8 +12485,7 @@ const networkIdleFamily = definePatternFamily({
12576
12485
  }
12577
12486
  ]
12578
12487
  });
12579
- //#endregion
12580
- //#region src/rules/families/hardcoded-url.ts
12488
+ // src/rules/families/hardcoded-url.ts
12581
12489
  /**
12582
12490
  * Hardcoded-URL family (Phase 6 — Tempering Plan).
12583
12491
  * Detects absolute URLs in navigation/request calls across languages.
@@ -12627,8 +12535,7 @@ const hardcodedUrlFamily = definePatternFamily({
12627
12535
  fix: "Use `--base-url` / `base_url` fixture, or `os.environ[...]`."
12628
12536
  }]
12629
12537
  });
12630
- //#endregion
12631
- //#region src/rules/families/shared-page.ts
12538
+ // src/rules/families/shared-page.ts
12632
12539
  /**
12633
12540
  * Shared-page family (Phase 6 — Tempering Plan).
12634
12541
  * Detects static/module-level Page/Browser instances shared across tests.
@@ -12689,8 +12596,7 @@ const sharedPageFamily = definePatternFamily({
12689
12596
  }
12690
12597
  ]
12691
12598
  });
12692
- //#endregion
12693
- //#region src/rules/families/brittle-selectors.ts
12599
+ // src/rules/families/brittle-selectors.ts
12694
12600
  /**
12695
12601
  * Brittle-selectors family (Phase 6 — Tempering Plan).
12696
12602
  * Detects XPath/structural CSS selectors across Java, C#, Python.
@@ -12802,8 +12708,7 @@ const brittleSelectorsFamily = [
12802
12708
  }
12803
12709
  ], "Prefer role-based locators (`get_by_role(...)`) or data-testid attributes.")
12804
12710
  ];
12805
- //#endregion
12806
- //#region src/rules/families/retry-masking.ts
12711
+ // src/rules/families/retry-masking.ts
12807
12712
  /**
12808
12713
  * Retry-masking family (Phase 6 — Tempering Plan).
12809
12714
  * Detects retry annotations/attributes that mask flaky tests.
@@ -12923,8 +12828,7 @@ const retryMaskingFamily = [defineRule({
12923
12828
  return findings;
12924
12829
  }
12925
12830
  })];
12926
- //#endregion
12927
- //#region src/rules/index.ts
12831
+ // src/rules/index.ts
12928
12832
  const RULES = [
12929
12833
  focusedTestCommitted,
12930
12834
  skippedTest,
@@ -13046,8 +12950,7 @@ const RETIRED_RULE_IDS = [
13046
12950
  "QA-JV-111",
13047
12951
  "QA-CS-111"
13048
12952
  ];
13049
- //#endregion
13050
- //#region src/lib/wilson.ts
12953
+ // src/lib/wilson.ts
13051
12954
  /**
13052
12955
  * 95% Wilson score interval on a proportion (plan §20.2).
13053
12956
  *
@@ -13170,8 +13073,7 @@ function isProvisional(rule) {
13170
13073
  function isRetiredRule(ruleId) {
13171
13074
  return RETIRED_RULE_IDS.includes(ruleId);
13172
13075
  }
13173
- //#endregion
13174
- //#region src/scorer/scorer.ts
13076
+ // src/scorer/scorer.ts
13175
13077
  /**
13176
13078
  * Transparent scorer (Product-MVP §8).
13177
13079
  * score = 100 − Σ(severity deductions), floor 0.
@@ -13323,9 +13225,9 @@ function countTestDeclarations(text, codeText) {
13323
13225
  const source = codeText ?? text;
13324
13226
  const patterns = [
13325
13227
  /\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
13228
+ /^[ \t]*(?:async[ \t]+)?def[ \t]+test_\w*/gm,
13229
+ /^[ \t]*@Test\b/gm,
13230
+ /^[ \t]*\[(?:Test|Fact|Theory|TestMethod)\b/gm
13329
13231
  ];
13330
13232
  let count = 0;
13331
13233
  for (const re of patterns) {
@@ -13334,8 +13236,7 @@ function countTestDeclarations(text, codeText) {
13334
13236
  }
13335
13237
  return count;
13336
13238
  }
13337
- //#endregion
13338
- //#region src/engine/rule-runner.ts
13239
+ // src/engine/rule-runner.ts
13339
13240
  /**
13340
13241
  * Legacy appliesTo → adapter id mapping.
13341
13242
  *
@@ -13374,8 +13275,7 @@ function asUniversal(rule) {
13374
13275
  }
13375
13276
  };
13376
13277
  }
13377
- //#endregion
13378
- //#region src/engine/tier-policy.ts
13278
+ // src/engine/tier-policy.ts
13379
13279
  /**
13380
13280
  * Tier policy (audit H-1): the declared tier is authoritative over a
13381
13281
  * rule's severity and evidence claims.
@@ -13515,8 +13415,7 @@ function enforceTierPolicy(findings, tierByRuleId) {
13515
13415
  if (EVIDENCE_RANK$1[effective] < EVIDENCE_RANK$1[floor.evidenceLevel]) f.evidenceLevel = floor.evidenceLevel;
13516
13416
  }
13517
13417
  }
13518
- //#endregion
13519
- //#region src/engine/evidence-core.ts
13418
+ // src/engine/evidence-core.ts
13520
13419
  /**
13521
13420
  * The core's own version — stamped into every record's provenance so a
13522
13421
  * consumer can tell which normalization contract produced a record.
@@ -13583,8 +13482,7 @@ function countEvidence(records) {
13583
13482
  }
13584
13483
  return counts;
13585
13484
  }
13586
- //#endregion
13587
- //#region src/engine/provenance.ts
13485
+ // src/engine/provenance.ts
13588
13486
  const GENERATED_HEADER_RE = /^\s*(?:\/\/|#|\/\*)\s*(?:auto[- ]?generated|generated by|do not edit)/i;
13589
13487
  /**
13590
13488
  * Codegen fingerprint: the recorder names its first test exactly
@@ -13625,8 +13523,7 @@ function computeAgenticProfile(files, findings) {
13625
13523
  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
13524
  };
13627
13525
  }
13628
- //#endregion
13629
- //#region src/engine/overlap-dedup.ts
13526
+ // src/engine/overlap-dedup.ts
13630
13527
  const TIER_RANK = {
13631
13528
  core: 0,
13632
13529
  extended: 1,
@@ -13721,8 +13618,7 @@ function dropOrderBefore(a, b, metaByRuleId) {
13721
13618
  if (ranksBefore(b, a, metaByRuleId)) return true;
13722
13619
  return (a.column ?? Number.MAX_SAFE_INTEGER) < (b.column ?? Number.MAX_SAFE_INTEGER);
13723
13620
  }
13724
- //#endregion
13725
- //#region src/engine/finding-identity.ts
13621
+ // src/engine/finding-identity.ts
13726
13622
  /**
13727
13623
  * Finding Identity (ENGINE-002): deterministic, stable fingerprints for
13728
13624
  * deduplication, correlation, and lifecycle tracking across scans.
@@ -13761,8 +13657,7 @@ function codeQualityFingerprint(f) {
13761
13657
  function findingId(f) {
13762
13658
  return `${findingFingerprint(f)}\u0000${f.line}\u0000${f.column}`;
13763
13659
  }
13764
- //#endregion
13765
- //#region src/engine/correlation-engine.ts
13660
+ // src/engine/correlation-engine.ts
13766
13661
  const EVIDENCE_RANK = {
13767
13662
  E0: 0,
13768
13663
  E1: 1,
@@ -13892,8 +13787,7 @@ function correlateFindings(findings) {
13892
13787
  });
13893
13788
  return conclusions;
13894
13789
  }
13895
- //#endregion
13896
- //#region src/engine/dependency-graph.ts
13790
+ // src/engine/dependency-graph.ts
13897
13791
  /**
13898
13792
  * Dependency Graph (ECO-005).
13899
13793
  *
@@ -14066,8 +13960,7 @@ function getReachableFiles(fromFiles, graph) {
14066
13960
  resolvedAny: unresolvedStarts.length < fromFiles.length
14067
13961
  };
14068
13962
  }
14069
- //#endregion
14070
- //#region src/engine/incremental-analysis.ts
13963
+ // src/engine/incremental-analysis.ts
14071
13964
  /**
14072
13965
  * Semantic inputs that affect the full scan beyond individual file content.
14073
13966
  * If any of these changed, an incremental run is NOT safe — a full scan
@@ -14103,8 +13996,7 @@ function isIncrementalSafe(changedFiles) {
14103
13996
  reasons
14104
13997
  };
14105
13998
  }
14106
- //#endregion
14107
- //#region src/brand/tokens.ts
13999
+ // src/brand/tokens.ts
14108
14000
  /**
14109
14001
  * The single source of brand truth.
14110
14002
  *
@@ -14339,8 +14231,7 @@ RUNG_MEANINGS.map((meaning, i) => ({
14339
14231
  runtime: i >= 3,
14340
14232
  color: RUNG_COLORS[i]
14341
14233
  }));
14342
- //#endregion
14343
- //#region src/reporter/presentation.ts
14234
+ // src/reporter/presentation.ts
14344
14235
  /**
14345
14236
  * presentation.ts — the one place a presentation DECISION is made.
14346
14237
  *
@@ -14562,8 +14453,7 @@ function evidenceTag(f) {
14562
14453
  if (runtime !== null) tag += ` · runtime: ${runtime}`;
14563
14454
  return `[${tag}]`;
14564
14455
  }
14565
- //#endregion
14566
- //#region src/engine/monorepo-analysis.ts
14456
+ // src/engine/monorepo-analysis.ts
14567
14457
  /**
14568
14458
  * Package verdict, derived from the one ScoreState band model (BW-104).
14569
14459
  * These two constants used to be a private copy of the band boundaries;
@@ -14670,8 +14560,7 @@ function configurableAggregation(results, config) {
14670
14560
  strategy: config.weightingStrategy
14671
14561
  };
14672
14562
  }
14673
- //#endregion
14674
- //#region src/lib/fs-atomic.ts
14563
+ // src/lib/fs-atomic.ts
14675
14564
  /**
14676
14565
  * Atomic file writes (audit S9).
14677
14566
  *
@@ -14791,8 +14680,7 @@ function pidAlive(pid) {
14791
14680
  return err?.code !== "ESRCH";
14792
14681
  }
14793
14682
  }
14794
- //#endregion
14795
- //#region src/engine/scan-cache.ts
14683
+ // src/engine/scan-cache.ts
14796
14684
  /**
14797
14685
  * Local incremental scan cache (Beta-to-Stable 1.0 plan, M5.2 / A-2).
14798
14686
  *
@@ -14892,7 +14780,9 @@ function hashDir(dir, hash, depth) {
14892
14780
  else if (entry.isFile() && /\.tsx?$/.test(entry.name)) try {
14893
14781
  hash.update(entry.name);
14894
14782
  hash.update(readFileSync(full));
14895
- } catch {}
14783
+ } catch {
14784
+ recordDegradation("rules-tree-file-unreadable");
14785
+ }
14896
14786
  }
14897
14787
  }
14898
14788
  /**
@@ -15002,8 +14892,7 @@ function createScanCache(root) {
15002
14892
  }
15003
14893
  };
15004
14894
  }
15005
- //#endregion
15006
- //#region src/reporter/theme.ts
14895
+ // src/reporter/theme.ts
15007
14896
  /**
15008
14897
  * Norse-forge theme system. Pure string-in → string-out helpers.
15009
14898
  * Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
@@ -15212,8 +15101,7 @@ function gaugeColorForBand(band, p) {
15212
15101
  if (band === "unmeasured") return p.dim;
15213
15102
  return band === "warning" ? p.warning : p.error;
15214
15103
  }
15215
- //#endregion
15216
- //#region src/reporter/ui.ts
15104
+ // src/reporter/ui.ts
15217
15105
  /**
15218
15106
  * Neutral context for direct calls (tests, library use) and as the
15219
15107
  * safe default when a command runner is invoked without wiring: no
@@ -15302,8 +15190,7 @@ function formatDuration(ms) {
15302
15190
  if (ms === void 0) return "?";
15303
15191
  return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
15304
15192
  }
15305
- //#endregion
15306
- //#region src/forensics/classify.ts
15193
+ // src/forensics/classify.ts
15307
15194
  /**
15308
15195
  * Strong-signal marker tables. Kept deliberately narrow: a marker here
15309
15196
  * is a named family fact, not a vibe. Unknown text yields no signal —
@@ -15423,8 +15310,7 @@ function classifyForensicVerdict(signals) {
15423
15310
  signals: signalCounts
15424
15311
  };
15425
15312
  }
15426
- //#endregion
15427
- //#region src/forensics/analyze.ts
15313
+ // src/forensics/analyze.ts
15428
15314
  const ui$11 = plainContext();
15429
15315
  const MAX_RECORDS = 1e5;
15430
15316
  function analyze(records, source) {
@@ -15587,8 +15473,7 @@ function renderFlakyMd(report) {
15587
15473
  lines.push("");
15588
15474
  return lines.join("\n");
15589
15475
  }
15590
- //#endregion
15591
- //#region src/forensics/evidence-hygiene.ts
15476
+ // src/forensics/evidence-hygiene.ts
15592
15477
  /**
15593
15478
  * Evidence hygiene — secret redaction + control-char sanitization
15594
15479
  * (ENGINE-012).
@@ -15650,8 +15535,7 @@ function sanitizeErrorText(text, options) {
15650
15535
  if (result.length > maxLength) result = result.slice(0, maxLength) + `[truncated at ${maxLength} chars]`;
15651
15536
  return result;
15652
15537
  }
15653
- //#endregion
15654
- //#region src/forensics/trace.ts
15538
+ // src/forensics/trace.ts
15655
15539
  /**
15656
15540
  * Playwright trace ingester (product-gap master plan §9 R5 / WI-17).
15657
15541
  *
@@ -15844,8 +15728,7 @@ function parseTraceArtifact(bytes, artifactName) {
15844
15728
  const { actions } = parseTraceNdjson(text);
15845
15729
  return traceActionsToRecord(actions, artifactName);
15846
15730
  }
15847
- //#endregion
15848
- //#region src/forensics/parse-jest-json.ts
15731
+ // src/forensics/parse-jest-json.ts
15849
15732
  function toStatus$2(raw) {
15850
15733
  switch (raw) {
15851
15734
  case "passed": return "passed";
@@ -15905,8 +15788,7 @@ function parseJestJson(json) {
15905
15788
  }
15906
15789
  return out;
15907
15790
  }
15908
- //#endregion
15909
- //#region src/forensics/parse-junit.ts
15791
+ // src/forensics/parse-junit.ts
15910
15792
  const MAX_INPUT$1 = 20971520;
15911
15793
  function decodeEntities$1(s) {
15912
15794
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -15948,14 +15830,25 @@ function parseJunitXml(xml) {
15948
15830
  const skipped = /<skipped\b/i.test(inner);
15949
15831
  const errors = [];
15950
15832
  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] ?? "";
15833
+ const openRe = /<(?:failure|error)\b/gi;
15834
+ const endRe = /<\/(?:failure|error)\s*>/gi;
15835
+ let open;
15836
+ while ((open = openRe.exec(inner)) !== null) {
15837
+ const gt = inner.indexOf(">", open.index);
15838
+ if (gt === -1) break;
15839
+ const attrsStr = inner.slice(open.index, gt);
15840
+ if (attrsStr.endsWith("/")) {
15841
+ openRe.lastIndex = gt + 1;
15842
+ continue;
15843
+ }
15844
+ endRe.lastIndex = gt + 1;
15845
+ const end = endRe.exec(inner);
15846
+ if (end === null) break;
15955
15847
  const msgAttr = /\smessage\s*=\s*"([^"]*)"/i.exec(attrsStr)?.[1];
15956
15848
  if (msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(msgAttr)));
15957
- const body = failMatch[2]?.trim();
15849
+ const body = inner.slice(gt + 1, end.index).trim();
15958
15850
  if (body && body !== msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(body)));
15851
+ openRe.lastIndex = end.index + end[0].length;
15959
15852
  }
15960
15853
  const selfCloseRe = /<(?:failure|error)\b([^>]*)\/>/gi;
15961
15854
  let scMatch;
@@ -15979,8 +15872,7 @@ function parseJunitXml(xml) {
15979
15872
  }
15980
15873
  return out;
15981
15874
  }
15982
- //#endregion
15983
- //#region src/forensics/parse-trx.ts
15875
+ // src/forensics/parse-trx.ts
15984
15876
  const MAX_INPUT = 20971520;
15985
15877
  function decodeEntities(s) {
15986
15878
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -16091,11 +15983,24 @@ function parseTrxXml(xml) {
16091
15983
  if (id !== void 0 && name !== void 0) byTestId.set(id, name);
16092
15984
  }
16093
15985
  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] ?? "";
15986
+ const startRe = /<UnitTestResult\b/gi;
15987
+ const starts = [];
15988
+ let s;
15989
+ while ((s = startRe.exec(xml)) !== null) {
15990
+ starts.push(s.index);
15991
+ if (starts.length > 2e5) break;
15992
+ }
15993
+ const resultsEnd = xml.search(/<\/Results>/i);
15994
+ for (let i = 0; i < starts.length; i++) {
15995
+ const start = starts[i];
15996
+ const tagEnd = xml.indexOf(">", start);
15997
+ if (tagEnd === -1) break;
15998
+ const selfClosing = xml[tagEnd - 1] === "/";
15999
+ const attrs = xml.slice(start + 15, selfClosing ? tagEnd - 1 : tagEnd);
16000
+ const next = starts[i + 1] ?? -1;
16001
+ let bodyEnd = next === -1 ? xml.length : next;
16002
+ if (resultsEnd > tagEnd && resultsEnd < bodyEnd) bodyEnd = resultsEnd;
16003
+ const inner = selfClosing ? "" : xml.slice(tagEnd + 1, bodyEnd);
16099
16004
  const testName = attr(attrs, "testName") ?? attr(attrs, "testId");
16100
16005
  if (testName === void 0) continue;
16101
16006
  const attempt = {
@@ -16103,7 +16008,8 @@ function parseTrxXml(xml) {
16103
16008
  status: mapTrxOutcome(attr(attrs, "outcome")),
16104
16009
  durationMs: parseDuration(attr(attrs, "duration"))
16105
16010
  };
16106
- const error = messageOf(inner, 0);
16011
+ const messageAt = inner.search(/<Message\b/i);
16012
+ const error = messageAt === -1 ? void 0 : messageOf(inner, messageAt);
16107
16013
  records.push({
16108
16014
  file: "unknown",
16109
16015
  title: testName,
@@ -16115,8 +16021,7 @@ function parseTrxXml(xml) {
16115
16021
  }
16116
16022
  return records;
16117
16023
  }
16118
- //#endregion
16119
- //#region src/forensics/parse-playwright-json.ts
16024
+ // src/forensics/parse-playwright-json.ts
16120
16025
  const MAX_DEPTH = 32;
16121
16026
  function toStatus$1(raw) {
16122
16027
  switch (raw) {
@@ -16181,8 +16086,7 @@ function parsePlaywrightJson(json) {
16181
16086
  walk(root, 0);
16182
16087
  return out;
16183
16088
  }
16184
- //#endregion
16185
- //#region src/forensics/parse-vitest-json.ts
16089
+ // src/forensics/parse-vitest-json.ts
16186
16090
  function toStatus(raw) {
16187
16091
  switch (raw) {
16188
16092
  case "passed": return "passed";
@@ -16236,8 +16140,7 @@ function parseVitestJson(json) {
16236
16140
  }
16237
16141
  return out;
16238
16142
  }
16239
- //#endregion
16240
- //#region src/forensics/parse-har.ts
16143
+ // src/forensics/parse-har.ts
16241
16144
  const MAX_ENTRIES = 2e4;
16242
16145
  function sanitizeTitleUrl(url) {
16243
16146
  try {
@@ -16322,8 +16225,7 @@ function parseHarJsonDetailed(json) {
16322
16225
  truncated
16323
16226
  };
16324
16227
  }
16325
- //#endregion
16326
- //#region src/forensics/run.ts
16228
+ // src/forensics/run.ts
16327
16229
  /**
16328
16230
  * Runtime evidence entry point (R4) — the library behind `explain
16329
16231
  * --evidence`. The `forensics` verb is retired in the v6 carve; this module
@@ -16488,13 +16390,15 @@ function runForensics(target, options = {}) {
16488
16390
  report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
16489
16391
  }
16490
16392
  let flakyMdPath;
16393
+ let flakyMdWriteError;
16491
16394
  if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
16492
16395
  const base = stat.isFile() ? dirname(target) : target;
16493
16396
  flakyMdPath = join(base, "FLAKY.md");
16494
16397
  try {
16495
16398
  writeFileAtomic(flakyMdPath, renderFlakyMd(report));
16496
- } catch {
16399
+ } catch (e) {
16497
16400
  flakyMdPath = void 0;
16401
+ flakyMdWriteError = e instanceof Error ? e.message : String(e);
16498
16402
  }
16499
16403
  }
16500
16404
  return {
@@ -16502,7 +16406,7 @@ function runForensics(target, options = {}) {
16502
16406
  output: [
16503
16407
  renderLeaderboard(report),
16504
16408
  "",
16505
- flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options)
16409
+ flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options, flakyMdWriteError)
16506
16410
  ].join("\n"),
16507
16411
  flakyMdPath
16508
16412
  };
@@ -16511,9 +16415,10 @@ function renderFlakyMdHint() {
16511
16415
  return "Full details in FLAKY.md (committed artifact).";
16512
16416
  }
16513
16417
  /** Honest fallback for every case where FLAKY.md was NOT written (M2). */
16514
- function renderFlakyMdNotWritten(options) {
16418
+ function renderFlakyMdNotWritten(options, writeError) {
16515
16419
  if (options.writeFlakyMd === false) return "FLAKY.md not written (--no-flaky-md).";
16516
- return "FLAKY.md was not written — nothing recognized to report (or the target directory is not writable).";
16420
+ if (writeError !== void 0) return `FLAKY.md could not be written: ${writeError}`;
16421
+ return "FLAKY.md was not written — nothing recognized to report.";
16517
16422
  }
16518
16423
  /**
16519
16424
  * R5: the trace artifact's test identity. The per-test trace convention
@@ -16600,8 +16505,7 @@ function listFiles(dir) {
16600
16505
  walk(dir, 0);
16601
16506
  return out.map((p) => p);
16602
16507
  }
16603
- //#endregion
16604
- //#region src/config/suppressions.ts
16508
+ // src/config/suppressions.ts
16605
16509
  /**
16606
16510
  * Suppression governance (Sprint-Plan W7, Product-MVP §12).
16607
16511
  * `mjolnir scan --suppressions` — every suppression stays visible.
@@ -16638,8 +16542,7 @@ function renderSuppressions(report) {
16638
16542
  lines.push("");
16639
16543
  return lines.join("\n");
16640
16544
  }
16641
- //#endregion
16642
- //#region src/plugins/reserved-prefixes.ts
16545
+ // src/plugins/reserved-prefixes.ts
16643
16546
  /**
16644
16547
  * Core-owned rule-ID prefixes that external rule sources (npm plugins,
16645
16548
  * JS modules, JSON manifests) may never claim — one shared law.
@@ -16669,8 +16572,7 @@ function isReservedPrefix(ruleId) {
16669
16572
  const upper = ruleId.toUpperCase();
16670
16573
  return RESERVED_PREFIXES.some((p) => upper.startsWith(p));
16671
16574
  }
16672
- //#endregion
16673
- //#region src/plugins/load.ts
16575
+ // src/plugins/load.ts
16674
16576
  /**
16675
16577
  * Plugin API (Upgrade-Plan-v3 Phase 6, tier-2 item #1).
16676
16578
  *
@@ -16776,8 +16678,7 @@ function loadPlugins(root, gateOpen = false) {
16776
16678
  }
16777
16679
  return result;
16778
16680
  }
16779
- //#endregion
16780
- //#region src/plugins/local-rules.ts
16681
+ // src/plugins/local-rules.ts
16781
16682
  /**
16782
16683
  * Local plugin/rule contract (Verification Trust Evolution Plan §18 —
16783
16684
  * Local Extensibility).
@@ -17089,8 +16990,7 @@ async function loadModuleRules(path, result) {
17089
16990
  result.rules.push(rule);
17090
16991
  }
17091
16992
  }
17092
- //#endregion
17093
- //#region src/plugins/trust-gate.ts
16993
+ // src/plugins/trust-gate.ts
17094
16994
  /**
17095
16995
  * Plugin trust gate (audit-remediation-master-plan.md, locked decision C2).
17096
16996
  *
@@ -17126,8 +17026,7 @@ function renderGateNotice(skipped) {
17126
17026
  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
17027
  return lines.join("\n");
17128
17028
  }
17129
- //#endregion
17130
- //#region src/engine/scan-pipeline.ts
17029
+ // src/engine/scan-pipeline.ts
17131
17030
  /**
17132
17031
  * Canonical scan pipeline (M6 — blueprint §9.2).
17133
17032
  *
@@ -17143,8 +17042,10 @@ function renderGateNotice(skipped) {
17143
17042
  * module moved. Re-exports in cli.ts keep the historical import surface.
17144
17043
  */
17145
17044
  var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
17045
+ DEFAULT_MAX_DURATION_MS: () => DEFAULT_MAX_DURATION_MS$1,
17146
17046
  EVIDENCE_OVERRIDES: () => EVIDENCE_OVERRIDES$1,
17147
17047
  KNOWN_RULE_IDS: () => KNOWN_RULE_IDS$1,
17048
+ MAX_DURATION_MS: () => MAX_DURATION_MS$1,
17148
17049
  OVERLAP_META_BY_RULE_ID: () => OVERLAP_META_BY_RULE_ID$1,
17149
17050
  SUITE_INVALIDATING_RULE_IDS: () => SUITE_INVALIDATING_RULE_IDS$1,
17150
17051
  applyPostScanProcessing: () => applyPostScanProcessing,
@@ -17161,8 +17062,13 @@ var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
17161
17062
  summarizeForensicVerdicts: () => summarizeForensicVerdicts
17162
17063
  });
17163
17064
  const UNIVERSAL_RULES = RULES.map(asUniversal);
17164
- const DEFAULT_MAX_DURATION_MS$2 = 6e5;
17165
- const MAX_DURATION_MS$2 = 36e5;
17065
+ /**
17066
+ * The scan time budget: the default, and the ceiling every surface enforces.
17067
+ * Owned here, where the clamp is applied; `cli.ts` and the MCP server import
17068
+ * these rather than keeping copies that only agree by coincidence.
17069
+ */
17070
+ const DEFAULT_MAX_DURATION_MS$1 = 6e5;
17071
+ const MAX_DURATION_MS$1 = 36e5;
17166
17072
  /** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
17167
17073
  const KNOWN_RULE_IDS$1 = new Set(RULES.map((r) => r.id));
17168
17074
  /**
@@ -17205,9 +17111,10 @@ async function buildUniversalRules$1(root, strict, opts = {}) {
17205
17111
  for (const p of plugins) for (const r of p.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
17206
17112
  for (const r of local.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
17207
17113
  let rules = [...UNIVERSAL_RULES, ...externalRules];
17208
- const withheldRuleIds = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
17114
+ const quarantined = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
17115
+ const withheldRuleIds = strict ? [] : quarantined;
17209
17116
  if (!strict) rules = rules.filter((r) => tierByRuleId.get(r.id) !== "quarantine");
17210
- const rulesWithheld = strict ? 0 : withheldRuleIds.length;
17117
+ const rulesWithheld = withheldRuleIds.length;
17211
17118
  const rulesApplied = rules.length;
17212
17119
  const pluginMeta = [...plugins.map((p) => ({
17213
17120
  name: p.name,
@@ -17604,12 +17511,28 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
17604
17511
  const runtimeIncomplete = discoveredReport !== void 0 && discoveredReport.report.analysisComplete !== true;
17605
17512
  let forensicVerdicts;
17606
17513
  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 = [];
17514
+ if (discoveredReport && discoveredReport.report.analysisComplete === true) {
17515
+ const before = findings.map((f) => ({
17516
+ runtimeCorroboration: f.runtimeCorroboration,
17517
+ trustLevel: f.trustLevel
17518
+ }));
17519
+ try {
17520
+ const records = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
17521
+ stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
17522
+ forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
17523
+ evidenceRecords = records;
17524
+ } catch {
17525
+ findings.forEach((f, i) => {
17526
+ const prev = before[i];
17527
+ if (prev?.runtimeCorroboration === void 0) delete f.runtimeCorroboration;
17528
+ else f.runtimeCorroboration = prev.runtimeCorroboration;
17529
+ if (prev?.trustLevel === void 0) delete f.trustLevel;
17530
+ else f.trustLevel = prev.trustLevel;
17531
+ });
17532
+ forensicVerdicts = void 0;
17533
+ evidenceRecords = [];
17534
+ recordDegradation("runtime-corroboration-failed");
17535
+ }
17613
17536
  }
17614
17537
  return {
17615
17538
  evidenceRecords,
@@ -17900,8 +17823,8 @@ async function runScan$1(args, hooks = {}) {
17900
17823
  async function runScanInProject(args, hooks = {}) {
17901
17824
  const started = Date.now();
17902
17825
  const degradationWindow = beginDegradationWindow();
17903
- const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$2;
17904
- const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$2);
17826
+ const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$1;
17827
+ const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$1);
17905
17828
  const discovered = discoverWorkspace(args.target);
17906
17829
  const targetAbs = resolve(args.target);
17907
17830
  const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
@@ -18042,8 +17965,7 @@ async function runScanInProject(args, hooks = {}) {
18042
17965
  resetTsMorphProject();
18043
17966
  return result;
18044
17967
  }
18045
- //#endregion
18046
- //#region src/commands/help.ts
17968
+ // src/commands/help.ts
18047
17969
  /** Ordered registry — grouped by the same categories the overview uses. */
18048
17970
  const HELP_ENTRIES = [
18049
17971
  {
@@ -18482,8 +18404,7 @@ function renderRootHelp(schemaVersion = 1, options = {}) {
18482
18404
  lines.push(`Docs: ${DOCS_URL} (JSON schemaVersion ${schemaVersion}, additive-only)`);
18483
18405
  return lines.join("\n");
18484
18406
  }
18485
- //#endregion
18486
- //#region src/integrations/sentry.ts
18407
+ // src/integrations/sentry.ts
18487
18408
  /**
18488
18409
  * Sentry crash reporting — opt-in, and inert unless a DSN is configured.
18489
18410
  *
@@ -18609,8 +18530,7 @@ async function flushSentry(timeoutMs = FLUSH_TIMEOUT_MS) {
18609
18530
  return false;
18610
18531
  }
18611
18532
  }
18612
- //#endregion
18613
- //#region src/integrations/ci-install.ts
18533
+ // src/integrations/ci-install.ts
18614
18534
  /**
18615
18535
  * CI integration (Sprint-Plan W7): generates .github/workflows/mjolnir.yml
18616
18536
  * from internal templates ONLY — no user-input interpolation (R3 supply-chain).
@@ -18697,10 +18617,12 @@ function publishedVersionForInstall(startDir = import.meta.dirname) {
18697
18617
  return ENGINE_VERSION;
18698
18618
  }
18699
18619
  /**
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
18620
+ * Template v3: the reporting steps render the saved report with
18621
+ * `mjolnir mjolnir.json --format github-summary` (annotations + step summary
18622
+ * via ONE emitter) and `--format pr-comment`. Template v2, generated through
18623
+ * 6.0.0, called `summary` and `pr-comment --from`, the verbs those formats
18624
+ * replaced, so its reporting steps exited 10 and, being `continue-on-error`,
18625
+ * did so silently: no annotations and no comment, on every run. The gate
18704
18626
  * script is unchanged (reads `partial`, `findings[].severity`).
18705
18627
  */
18706
18628
  /** Renders findings into `$GITHUB_STEP_SUMMARY`; tolerates a missing scan result.
@@ -18725,12 +18647,52 @@ const SUMMARY_SCRIPT_V1 = [
18725
18647
  "}",
18726
18648
  "process.stdout.write(lines.length + \" finding(s)\\n\");"
18727
18649
  ].join("\n");
18728
- /** True when a workflow was generated by any Mjölnir template (v1 or v2).
18650
+ /** True when a workflow was generated by any Mjölnir template (v1, v2 or v3).
18651
+ *
18652
+ * The comparison runs on the canonical form, so a workflow generated by an
18653
+ * earlier release still matches. It used to be byte equality with the current
18654
+ * template, and the pinned package version alone made every file from the
18655
+ * previous release read as hand-customised: an upgrade needed `--force`, the
18656
+ * one flag that also overwrites real customisations.
18657
+ *
18729
18658
  * The v1 needle is matched in its INDENTED form: the v1 template embedded
18730
18659
  * the script via indentBlock(…, 10) inside the `run: |` scalar, so the
18731
18660
  * raw unindented substring never appears in a real v1 file. */
18732
18661
  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));
18662
+ const file = canonicalWorkflow(content);
18663
+ return GATES.some((g) => {
18664
+ const npx = canonicalWorkflow(TEMPLATE(g));
18665
+ const action = canonicalWorkflow(ACTION_TEMPLATE(g));
18666
+ return file === npx || file === templateV2(npx) || file === action || file === actionTemplateWithoutVersion(action);
18667
+ }) || file.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
18668
+ }
18669
+ /**
18670
+ * A generated workflow with the parts that legitimately change between
18671
+ * releases blanked: line endings, the pinned package version (in the npx
18672
+ * tarball and in the Action's `version:` input) and every pinned action SHA
18673
+ * with its version comment. Dependabot moves the `actions/*` pins between
18674
+ * releases, so blanking only the Mjölnir Action's SHA made each of those bumps
18675
+ * turn every workflow the previous release generated into one that reads as
18676
+ * hand-customised.
18677
+ */
18678
+ function canonicalWorkflow(text) {
18679
+ 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(/(uses: [^\s@]+)@[0-9a-f]{40}[^\n]*/g, "$1@<sha>");
18680
+ }
18681
+ /**
18682
+ * The action template through 6.0.0, which passed no `version:` and so ran
18683
+ * whatever its pinned Action defaulted to.
18684
+ */
18685
+ function actionTemplateWithoutVersion(canonical) {
18686
+ return canonical.replace("\n version: \"<version>\"", "");
18687
+ }
18688
+ /**
18689
+ * Template v2, the npx template through 6.0.0, rebuilt from v3: the same file
18690
+ * with its two reporting steps calling the verbs the formats replaced. A v2
18691
+ * workflow is recognised because it is broken, so `ci install` has to be able
18692
+ * to replace it without `--force`.
18693
+ */
18694
+ function templateV2(v3) {
18695
+ 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
18696
  }
18735
18697
  /** Indents an embedded script so it sits inside a YAML `run: |` block scalar. */
18736
18698
  function indentBlock(text, spaces) {
@@ -18773,12 +18735,12 @@ jobs:
18773
18735
  - name: Annotations + Job Summary
18774
18736
  if: always()
18775
18737
  continue-on-error: true
18776
- run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz summary mjolnir.json
18738
+ run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format github-summary
18777
18739
  - name: Render PR comment
18778
18740
  if: always()
18779
18741
  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
18781
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
18742
+ run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format pr-comment > mjolnir-comment.md
18743
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
18782
18744
  if: always()
18783
18745
  with:
18784
18746
  name: mjolnir-report
@@ -18803,7 +18765,7 @@ ${indentBlock(gateScript(gate), 10)}
18803
18765
  contents: read
18804
18766
  pull-requests: write
18805
18767
  steps:
18806
- - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
18768
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
18807
18769
  with:
18808
18770
  name: mjolnir-report
18809
18771
  - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -18846,12 +18808,22 @@ const GATES = [
18846
18808
  "warning"
18847
18809
  ];
18848
18810
  /**
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.
18811
+ * The Action the action-based template pins: a commit on `main`, by SHA,
18812
+ * because a generated workflow must never resolve a mutable ref.
18813
+ *
18814
+ * It is the merge commit of #740, the first commit on `main` whose
18815
+ * `action.yml` renders the saved report with `--format pr-comment` and
18816
+ * `--format github-summary`. The pin it replaced, 4a588bc, was a pull-request
18817
+ * branch commit that `main` never contained: its `action.yml` installed
18818
+ * `mjolnir-qa@latest` and called the retired `trust-report` verb, and every
18819
+ * workflow `ci install` generated ran it. A pin can only name a commit that
18820
+ * already exists, so it moves in the change AFTER the Action it points at.
18821
+ *
18822
+ * The template passes `version:` because the Action's own default is the
18823
+ * published stable AT THIS COMMIT, which may be an older CLI than the one that
18824
+ * generated the workflow.
18853
18825
  */
18854
- const ACTION_REF = "Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b";
18826
+ const ACTION_REF = "Sergey-Bar/Mjolnir@6bf46a0e64e2f3ee16266d8456d1b16e5ae16214";
18855
18827
  /**
18856
18828
  * The action-based workflow for one gate level (P1.3): the root
18857
18829
  * action.yml does checkout-independent scanning — setup-node, the scan
@@ -18874,8 +18846,11 @@ const ACTION_REF = "Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b"
18874
18846
  * finish is not a pass — see the exit-code contract in docs/VERSIONING.md.
18875
18847
  * 3. `version:` was pinned to the engine's working version, which while the
18876
18848
  * 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.
18849
+ * workflow could not install what it asked for. It was then omitted,
18850
+ * which handed the choice to the pinned Action's default, the stable that
18851
+ * was published when that commit was made. It now names the published
18852
+ * version the installing package resolves, the same pin the npx template
18853
+ * uses.
18879
18854
  */
18880
18855
  const ACTION_TEMPLATE = (gate) => `name: Mjölnir
18881
18856
 
@@ -18905,13 +18880,14 @@ jobs:
18905
18880
  continue-on-error: true
18906
18881
  uses: ${ACTION_REF}
18907
18882
  with:
18883
+ version: "${INSTALL_VERSION}"
18908
18884
  scope: changed
18909
18885
  format: json
18910
18886
  fail-on: ${gate === "advisory" ? "none" : gate}
18911
18887
  pr-comment: "false"
18912
18888
  trust-artifact: "false"
18913
18889
  annotations: "true"
18914
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
18890
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
18915
18891
  if: always()
18916
18892
  with:
18917
18893
  name: mjolnir-report
@@ -18942,7 +18918,7 @@ jobs:
18942
18918
  contents: read
18943
18919
  pull-requests: write
18944
18920
  steps:
18945
- - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
18921
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
18946
18922
  with:
18947
18923
  name: mjolnir-report
18948
18924
  - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -19015,8 +18991,7 @@ function ciInstall(root, gate, options = {}) {
19015
18991
  diffSummary: []
19016
18992
  };
19017
18993
  }
19018
- //#endregion
19019
- //#region src/cli-io.ts
18994
+ // src/cli-io.ts
19020
18995
  const out = (...parts) => console.log(parts.map(String).join(" "));
19021
18996
  const err = (...parts) => console.error(parts.map(String).join(" "));
19022
18997
  function internalErrorMessage(err, emit, debug) {
@@ -19185,8 +19160,7 @@ function buildMachineContract(result) {
19185
19160
  ...result.forensicVerdicts !== void 0 ? { forensicVerdicts: result.forensicVerdicts } : {}
19186
19161
  };
19187
19162
  }
19188
- //#endregion
19189
- //#region src/commands/fixture-example.ts
19163
+ // src/commands/fixture-example.ts
19190
19164
  /**
19191
19165
  * Shared example-fixture selection for `mjolnir explain` (explain.ts)
19192
19166
  * and the generated rule docs (rule-docs.ts) — one chooser so both
@@ -19204,8 +19178,7 @@ function firstFixtureFile(dir) {
19204
19178
  const entries = readdirSync(dir).filter((f) => !f.startsWith(".")).sort();
19205
19179
  return entries.length > 0 ? join(dir, entries[0]) : null;
19206
19180
  }
19207
- //#endregion
19208
- //#region src/commands/explain.ts
19181
+ // src/commands/explain.ts
19209
19182
  /**
19210
19183
  * `mjolnir explain <RULE-ID>` — implements Plan.md Sprint 1.3
19211
19184
  * (Master-Stabilization-Plan Sprint 5, Task 19).
@@ -19546,8 +19519,7 @@ function renderVerdictExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
19546
19519
  function pctOf(v) {
19547
19520
  return `${Math.round(v * 100)}%`;
19548
19521
  }
19549
- //#endregion
19550
- //#region src/engine/resolution.ts
19522
+ // src/engine/resolution.ts
19551
19523
  /** Correlation identity (v1-compatible): ruleId\0file\0message. */
19552
19524
  const fingerprint$1 = findingFingerprint;
19553
19525
  /**
@@ -19612,8 +19584,7 @@ function resolve$1(input) {
19612
19584
  comparedAgainst
19613
19585
  };
19614
19586
  }
19615
- //#endregion
19616
- //#region src/commands/baseline.ts
19587
+ // src/commands/baseline.ts
19617
19588
  /**
19618
19589
  * 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
19590
  * (Master-Stabilization-Plan.md).
@@ -19767,8 +19738,7 @@ function renderBaselineSaved(path, count, replaced) {
19767
19738
  lines.push(nextStep("mjolnir scan --scope changed", ui$8) + " — see only what's new.");
19768
19739
  return lines.join("\n");
19769
19740
  }
19770
- //#endregion
19771
- //#region src/commands/verify.ts
19741
+ // src/commands/verify.ts
19772
19742
  /**
19773
19743
  * `mjolnir ci verify` — the agent-loop digest (product-gap-remediation
19774
19744
  * master plan P7, plan 1788853205786 — flag agent, decision 6).
@@ -19875,8 +19845,7 @@ function renderVerifyDigest(digest) {
19875
19845
  }
19876
19846
  return lines.join("\n").trimEnd();
19877
19847
  }
19878
- //#endregion
19879
- //#region src/forensics/triage.ts
19848
+ // src/forensics/triage.ts
19880
19849
  const ui$7 = plainContext();
19881
19850
  const QUARANTINE_MIN_ATTEMPTS = 2;
19882
19851
  /** Deterministic triage table rows, worst first. */
@@ -20023,8 +19992,7 @@ function renderTriageWorkflowJson(report) {
20023
19992
  rows: workflowRows(report)
20024
19993
  }, null, 2) + "\n";
20025
19994
  }
20026
- //#endregion
20027
- //#region src/commands/pw-report.ts
19995
+ // src/commands/pw-report.ts
20028
19996
  const ui$6 = plainContext();
20029
19997
  function summarizePwRun(report) {
20030
19998
  const slowest = [...report.verdicts].sort((a, b) => b.totalDurationMs - a.totalDurationMs).slice(0, 5).map((v) => ({
@@ -20058,8 +20026,7 @@ function renderPwRunSummary(s) {
20058
20026
  }
20059
20027
  return lines.join("\n");
20060
20028
  }
20061
- //#endregion
20062
- //#region src/mcp/server.ts
20029
+ // src/mcp/server.ts
20063
20030
  /**
20064
20031
  * MCP stdio transport (blueprint §21 — Phase 4, PURE TRANSPORT).
20065
20032
  *
@@ -20077,22 +20044,38 @@ function renderPwRunSummary(s) {
20077
20044
  * - parameter size caps; ONE scan in flight (serialized queue) reusing
20078
20045
  * the pipeline's own budgets; zero network (no fetches anywhere); no
20079
20046
  * shell execution beyond Mjölnir's own verbs.
20080
- * - filesystem boundary = scan target + `.mjolnir/` (same as CLI).
20047
+ * - filesystem boundary: the tools are read-only, and with no further
20048
+ * configuration they accept any path the parent process passes (the
20049
+ * parent is the trust boundary). `MJOLNIR_WORKSPACE_ROOT` confines every
20050
+ * path argument to that directory, symlinks resolved.
20081
20051
  * - the plugin trust gate applies unchanged (--enable-plugins is NOT
20082
20052
  * exposed over MCP: tools run with the gate CLOSED unless the server
20083
20053
  * was started with MJOLNIR_ENABLE_PLUGINS=1 in its own environment).
20084
20054
  */
20085
20055
  /** Hard caps (§21 threat model: parameter size + resource bounds). */
20086
20056
  const MAX_PARAM_BYTES = 65536;
20087
- const MAX_DURATION_MS$1 = 36e5;
20088
- const DEFAULT_MAX_DURATION_MS$1 = 6e5;
20089
20057
  const PROTOCOL_VERSION = "2025-06-18";
20090
- const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? resolve(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
20058
+ /** The real path of `p`, or its lexical resolution when it does not exist. */
20059
+ function realOrResolved(p) {
20060
+ try {
20061
+ return realpathSync.native(resolve(p));
20062
+ } catch {
20063
+ return resolve(p);
20064
+ }
20065
+ }
20066
+ /**
20067
+ * The optional workspace root. When set, every path argument must resolve
20068
+ * INSIDE it after symlinks are followed.
20069
+ *
20070
+ * Unset is open by design and is documented that way in the header: the MCP
20071
+ * tools are read-only and the stdio parent is the trust boundary. Setting
20072
+ * `MJOLNIR_WORKSPACE_ROOT` is how a host confines them.
20073
+ */
20074
+ const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? realOrResolved(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
20091
20075
  function isPathAllowed(path) {
20092
20076
  if (!CONFIGURED_ROOT) return true;
20093
- const candidate = resolve(path);
20094
- const rel = relative(CONFIGURED_ROOT, candidate);
20095
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
20077
+ const rel = relative(CONFIGURED_ROOT, realOrResolved(path));
20078
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
20096
20079
  }
20097
20080
  /** JSON-RPC error codes (subset used here). */
20098
20081
  const MCP_ERRORS = {
@@ -20134,7 +20117,7 @@ const MCP_TOOLS = [
20134
20117
  },
20135
20118
  fixturesRoot: {
20136
20119
  type: "string",
20137
- description: "Root directory for fixture lookups (defaults to cwd)."
20120
+ description: "Root directory for fixture lookups (defaults to <cwd>/tests/fixtures, as for `mjolnir explain`)."
20138
20121
  }
20139
20122
  },
20140
20123
  required: ["ruleId"]
@@ -20214,7 +20197,7 @@ function validateParams(name, args) {
20214
20197
  if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
20215
20198
  if (args["maxDurationMs"] !== void 0) {
20216
20199
  const duration = args["maxDurationMs"];
20217
- if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > MAX_DURATION_MS$1) return "`maxDurationMs` must be a finite positive number within the server budget";
20200
+ if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > 36e5) return "`maxDurationMs` must be a finite positive number within the server budget";
20218
20201
  }
20219
20202
  }
20220
20203
  if (name === "explain") {
@@ -20318,7 +20301,7 @@ async function handleToolCall(call) {
20318
20301
  message: "fixturesRoot must be a string"
20319
20302
  }
20320
20303
  };
20321
- const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? process.cwd());
20304
+ const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? join(process.cwd(), "tests", "fixtures"));
20322
20305
  if (!explanation.ok) return {
20323
20306
  jsonrpc: "2.0",
20324
20307
  id: call.id,
@@ -20569,8 +20552,7 @@ async function handleMcpMessage(msg) {
20569
20552
  }
20570
20553
  };
20571
20554
  }
20572
- //#endregion
20573
- //#region src/mcp/transport.ts
20555
+ // src/mcp/transport.ts
20574
20556
  /**
20575
20557
  * MCP stdio transport loop (blueprint §21 — the framing around the
20576
20558
  * pure-transport server).
@@ -20667,8 +20649,7 @@ async function handleStdioLine(line, write) {
20667
20649
  }
20668
20650
  write(`${JSON.stringify(response)}\n`);
20669
20651
  }
20670
- //#endregion
20671
- //#region src/scorer/prioritize.ts
20652
+ // src/scorer/prioritize.ts
20672
20653
  /**
20673
20654
  * "Fix this first" prioritization (Master-Stabilization-Plan Sprint 5,
20674
20655
  * Task 20).
@@ -20718,8 +20699,7 @@ function prioritize(findings) {
20718
20699
  function topFixes(findings, n = 3) {
20719
20700
  return prioritize(findings).filter((p) => p.scoreGain > 0).slice(0, n);
20720
20701
  }
20721
- //#endregion
20722
- //#region src/reporter/art.ts
20702
+ // src/reporter/art.ts
20723
20703
  /**
20724
20704
  * ASCII art assets for the Mjölnir terminal experience.
20725
20705
  * Minimal Nordic / engineering aesthetic — professional, not fantasy.
@@ -20756,8 +20736,7 @@ const TROPHY = String.raw`
20756
20736
  "─".repeat(58);
20757
20737
  /** Wordmark for the 100-state celebration block. */
20758
20738
  const FORGED_WORDMARK = "⚡ F O R G E D ⚡";
20759
- //#endregion
20760
- //#region src/reporter/tone-blunt.ts
20739
+ // src/reporter/tone-blunt.ts
20761
20740
  /**
20762
20741
  * Static blunt messages keyed by rule ID. A simple string lookup
20763
20742
  * (no per-entry closures) so coverage is not artificially fragmented
@@ -20829,8 +20808,7 @@ function bluntMessage(f) {
20829
20808
  if (msg !== void 0) return msg;
20830
20809
  return `${f.message} — fix it or suppress it; ignoring it is worse than either.`;
20831
20810
  }
20832
- //#endregion
20833
- //#region src/reporter/terminal.ts
20811
+ // src/reporter/terminal.ts
20834
20812
  /** Non-verbose finding cards shown before the overflow line. The
20835
20813
  * JSON/SARIF contract always carries ALL findings — this cap is a
20836
20814
  * terminal display concern only. */
@@ -21312,8 +21290,7 @@ function countBySeverity(result) {
21312
21290
  }
21313
21291
  return counts;
21314
21292
  }
21315
- //#endregion
21316
- //#region src/engine/trust-classification.ts
21293
+ // src/engine/trust-classification.ts
21317
21294
  /**
21318
21295
  * Completeness, decided once.
21319
21296
  *
@@ -21419,8 +21396,7 @@ function classifyTrust(result, summary) {
21419
21396
  gaps
21420
21397
  };
21421
21398
  }
21422
- //#endregion
21423
- //#region src/lib/format.ts
21399
+ // src/lib/format.ts
21424
21400
  /**
21425
21401
  * Shared formatting utilities. Single definition site for `pct()` (Task 20)
21426
21402
  * — the two callers (commands/trust-report.ts, reporter/trust-report.ts)
@@ -21429,8 +21405,7 @@ function classifyTrust(result, summary) {
21429
21405
  function pct$1(v) {
21430
21406
  return `${Math.round(v * 100)}%`;
21431
21407
  }
21432
- //#endregion
21433
- //#region src/reporter/trust-report.ts
21408
+ // src/reporter/trust-report.ts
21434
21409
  /** The one label rule, so no renderer grows a second opinion. */
21435
21410
  function gateLabel(f, gate) {
21436
21411
  return isAtGate(f, gate, isAdvisoryFinding) ? "GATE" : "WARN";
@@ -21571,8 +21546,7 @@ function renderTrustReport(result, opts) {
21571
21546
  lines.push("");
21572
21547
  return lines.join("\n");
21573
21548
  }
21574
- //#endregion
21575
- //#region src/reporter/sarif.ts
21549
+ // src/reporter/sarif.ts
21576
21550
  /**
21577
21551
  * SARIF fingerprint values must be printable and stable. The engine's identity
21578
21552
  * strings join their parts with NUL (a separator no repository path or message
@@ -21684,6 +21658,10 @@ function renderSarif(result, repoRootUri) {
21684
21658
  }
21685
21659
  }))
21686
21660
  };
21661
+ const srcRoot = (uri) => {
21662
+ const encoded = uri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/");
21663
+ return encoded.endsWith("/") ? encoded : `${encoded}/`;
21664
+ };
21687
21665
  const sarif = {
21688
21666
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
21689
21667
  version: [
@@ -21693,13 +21671,12 @@ function renderSarif(result, repoRootUri) {
21693
21671
  ].join("."),
21694
21672
  runs: [repoRootUri ? {
21695
21673
  ...run,
21696
- originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
21674
+ originalUriBaseIds: { SRCROOT: { uri: srcRoot(repoRootUri) } }
21697
21675
  } : run]
21698
21676
  };
21699
21677
  return JSON.stringify(sarif, null, 2);
21700
21678
  }
21701
- //#endregion
21702
- //#region src/reporter/codequality.ts
21679
+ // src/reporter/codequality.ts
21703
21680
  function codeQualitySeverity(severity) {
21704
21681
  if (severity === "error") return "major";
21705
21682
  if (severity === "warning") return "minor";
@@ -21718,8 +21695,7 @@ function renderCodeQuality(result) {
21718
21695
  }));
21719
21696
  return JSON.stringify(issues, null, 2) + "\n";
21720
21697
  }
21721
- //#endregion
21722
- //#region src/reporter/mermaid.ts
21698
+ // src/reporter/mermaid.ts
21723
21699
  /**
21724
21700
  * `--format mermaid` — test-architecture diagram (Sprint 9 Task 38,
21725
21701
  * Master-Stabilization-Plan.md). Ranked first among the delight
@@ -21818,8 +21794,7 @@ function renderMermaid(result) {
21818
21794
  for (const { id, cls } of styleAssignments) lines.push(` class ${id} ${cls};`);
21819
21795
  return lines.join("\n");
21820
21796
  }
21821
- //#endregion
21822
- //#region src/reporter/progress.ts
21797
+ // src/reporter/progress.ts
21823
21798
  /**
21824
21799
  * Live scan progress (Terminal + CI UX Overhaul plan, M3).
21825
21800
  *
@@ -21918,8 +21893,7 @@ var ProgressRenderer = class {
21918
21893
  this.erase();
21919
21894
  }
21920
21895
  };
21921
- //#endregion
21922
- //#region src/lib/git-utils.ts
21896
+ // src/lib/git-utils.ts
21923
21897
  /**
21924
21898
  * Shared git utilities (Task 18). The `currentCommit` / `targetCommit`
21925
21899
  * pattern was duplicated between cli.ts and commands/trust-report.ts.
@@ -21952,8 +21926,7 @@ function currentCommit(root, opts = {}) {
21952
21926
  return opts.nullable ? null : "unknown";
21953
21927
  }
21954
21928
  }
21955
- //#endregion
21956
- //#region src/commands/report-io.ts
21929
+ // src/commands/report-io.ts
21957
21930
  /**
21958
21931
  * Shared saved-report loading (agent-handoff plan §9.0).
21959
21932
  *
@@ -22146,8 +22119,7 @@ function loadSavedReport(reportPath) {
22146
22119
  function reportExists(reportPath) {
22147
22120
  return existsSync(reportPath);
22148
22121
  }
22149
- //#endregion
22150
- //#region src/commands/trust-report.ts
22122
+ // src/commands/trust-report.ts
22151
22123
  function buildArtifactIdentity(result, commit) {
22152
22124
  const revisions = /* @__PURE__ */ new Map();
22153
22125
  const levels = /* @__PURE__ */ new Map();
@@ -22173,8 +22145,7 @@ function buildArtifactIdentity(result, commit) {
22173
22145
  }
22174
22146
  };
22175
22147
  }
22176
- //#endregion
22177
- //#region src/reporter/pr-report-actions.ts
22148
+ // src/reporter/pr-report-actions.ts
22178
22149
  /** GitHub-native actions. Social drafts contain only public project information. */
22179
22150
  const PROJECT_URL = "https://github.com/Sergey-Bar/Mjolnir";
22180
22151
  const SHARE_URL = "https://sergey-bar.github.io/Mjolnir/";
@@ -22293,8 +22264,7 @@ const PR_BRAND_CONTRACT = {
22293
22264
  terminology: "trust-engine",
22294
22265
  footerFormat: "scanId + generatedBy"
22295
22266
  };
22296
- //#endregion
22297
- //#region src/reporter/pr-report-shared.ts
22267
+ // src/reporter/pr-report-shared.ts
22298
22268
  const UNIFIED_MARKER = "<!-- mjolnir-report:v2 -->";
22299
22269
  /**
22300
22270
  * The one place a comment asset URL is written down.
@@ -22321,6 +22291,24 @@ const MAX_FINDINGS_PER_GROUP = 25;
22321
22291
  function escMd(s) {
22322
22292
  return sanitizeData(s).replace(/([\\`*_{}[\]()#+!|<>])/g, "\\$1");
22323
22293
  }
22294
+ /**
22295
+ * A Markdown code span for untrusted text.
22296
+ *
22297
+ * Backslash escapes are literal inside a code span, so `escMd` there rendered
22298
+ * `__snapshots__` as `\_\_snapshots\_\_` and `app/(auth)` as `app/\(auth\)`.
22299
+ * A span cannot be broken out of with formatting characters, only with a
22300
+ * backtick run, so it is fenced with one backtick more than the longest run
22301
+ * inside (CommonMark 6.1) and padded where the text would otherwise touch the
22302
+ * fence. Kept to one line, because a newline would end the list item.
22303
+ */
22304
+ function mdCodeSpan(raw) {
22305
+ const text = sanitizeData(raw).replace(/[\r\n]+/g, " ");
22306
+ if (text.trim() === "") return "` `";
22307
+ const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
22308
+ const fence = "`".repeat(longest + 1);
22309
+ const pad = text.startsWith("`") || text.endsWith("`") || text.startsWith(" ") && text.endsWith(" ") ? " " : "";
22310
+ return `${fence}${pad}${text}${pad}${fence}`;
22311
+ }
22324
22312
  function looksLikeCode(s) {
22325
22313
  return /[(;={]|await |expect\(/.test(s);
22326
22314
  }
@@ -22335,10 +22323,8 @@ function evidenceTagLine(f) {
22335
22323
  }
22336
22324
  function findingLineMd(f) {
22337
22325
  const icon = f.severity === "error" ? "🔴" : f.severity === "warning" ? "🟡" : "🔵";
22338
- const fix = escMd(f.fix);
22339
- const fixBody = looksLikeCode(f.fix) ? `\`${fix}\`` : `_${fix}_`;
22340
- const safePath = escMd(f.file).replace(/[\r\n]+/g, " ");
22341
- return `${icon} **${escMd(f.ruleId)}** \`${safePath}:${f.line}\` — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
22326
+ const fixBody = looksLikeCode(f.fix) ? mdCodeSpan(f.fix) : `_${escMd(f.fix)}_`;
22327
+ return `${icon} **${escMd(f.ruleId)}** ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
22342
22328
  }
22343
22329
  /**
22344
22330
  * Reader-facing sentences for the `analysisStatus.reasons` tokens.
@@ -22369,7 +22355,8 @@ const REASON_PHRASES = [
22369
22355
  ["runtime-incomplete", "the runtime evidence set is incomplete"],
22370
22356
  ["identity-incomplete", "the run could not be bound to the tree it analysed"],
22371
22357
  ["degraded:", "capability was lost to a swallowed error"],
22372
- ["truncated:", "the scan was cut short"]
22358
+ ["truncated:", "the scan was cut short"],
22359
+ ["coverage:quarantine:", "quarantine-tier rule(s) withheld, as in every default scan (`--include-warn` runs them)"]
22373
22360
  ];
22374
22361
  /**
22375
22362
  * `scope-ignored:1` -> "1 path was ignored by the scope filter".
@@ -22516,7 +22503,9 @@ function renderUnifiedReport(result, options = {}) {
22516
22503
  provisionalRuleIds: [],
22517
22504
  ceilingReasons: []
22518
22505
  };
22519
- const incomplete = isIncompleteResult(result) || !result.trustSummary || summary.level === "L0" && summary.evidenceCoverage === 0;
22506
+ const analysisIncomplete = isIncompleteResult(result) || !result.trustSummary;
22507
+ const noEvidence = summary.level === "L0" && summary.evidenceCoverage === 0;
22508
+ const incomplete = analysisIncomplete || noEvidence;
22520
22509
  lines.push(`## <img width="16" height="16" alt="" src="${MARK_URL}"/> Mjölnir Verification Report`);
22521
22510
  lines.push("");
22522
22511
  lines.push("> Tests tell you what passed. Mjölnir tells you what you can trust.");
@@ -22527,8 +22516,9 @@ function renderUnifiedReport(result, options = {}) {
22527
22516
  }
22528
22517
  if (incomplete) {
22529
22518
  const reasons = result.analysisStatus?.reasons ?? [];
22519
+ const explained = [...reasons.length > 0 ? [humanizeReasons(reasons)] : [], ...!analysisIncomplete && noEvidence ? ["no runtime evidence corroborates this run, so an empty result is an observation, not a verdict"] : []];
22530
22520
  lines.push(`> [!WARNING]
22531
- > **Analysis status: INCOMPLETE.** ${reasons.length > 0 ? humanizeReasons(reasons) : "The scan did not complete a trustworthy analysis."}`);
22521
+ > **Analysis status: INCOMPLETE.** ${explained.length > 0 ? explained.join("; ") : "The scan did not complete a trustworthy analysis."}`);
22532
22522
  lines.push("");
22533
22523
  }
22534
22524
  const diff = options.diff;
@@ -22698,8 +22688,7 @@ function enforceCommentLimit(lines) {
22698
22688
  }
22699
22689
  return render() + notice;
22700
22690
  }
22701
- //#endregion
22702
- //#region src/commands/pr-comment.ts
22691
+ // src/commands/pr-comment.ts
22703
22692
  /**
22704
22693
  * Escape markdown-significant characters (markdown spec).
22705
22694
  * Kept exported for downstream consumer compatibility.
@@ -22721,8 +22710,7 @@ function renderPrComment(result, options = {}) {
22721
22710
  ...options.commit !== void 0 ? { commit: options.commit } : {}
22722
22711
  });
22723
22712
  }
22724
- //#endregion
22725
- //#region src/commands/why.ts
22713
+ // src/commands/why.ts
22726
22714
  /**
22727
22715
  * `mjolnir explain <file:line> <file>:<line>` — occurrence-level evidence/explanation
22728
22716
  * query (agent-handoff plan M2).
@@ -22894,8 +22882,7 @@ async function runWhyCommand(argv, io = {
22894
22882
  }));
22895
22883
  return filtered.length > 0 ? 0 : 1;
22896
22884
  }
22897
- //#endregion
22898
- //#region src/commands/handover.ts
22885
+ // src/commands/handover.ts
22899
22886
  const ui$5 = plainContext();
22900
22887
  const FAKE_GREEN_RULES = /* @__PURE__ */ new Set([
22901
22888
  "QA-TEST-003",
@@ -22983,8 +22970,7 @@ function renderHandover(map) {
22983
22970
  lines.push("Generated by mjolnir explain --plan — re-run after big refactors.");
22984
22971
  return lines.join("\n");
22985
22972
  }
22986
- //#endregion
22987
- //#region src/commands/stats.ts
22973
+ // src/commands/stats.ts
22988
22974
  /**
22989
22975
  * `mjolnir stats` — Sprint 6 Task 26 (Master-Stabilization-Plan.md).
22990
22976
  *
@@ -23104,14 +23090,72 @@ function renderStats(stats) {
23104
23090
  lines.push("different machine — this file is local-only, by design.");
23105
23091
  return lines.join("\n");
23106
23092
  }
23107
- //#endregion
23108
- //#region src/reporter/github.ts
23093
+ // src/reporter/github.ts
23094
+ /** Escape a workflow-command PROPERTY value. */
23095
+ function escapeAnnotationProperty(value) {
23096
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A").replaceAll(":", "%3A").replaceAll(",", "%2C");
23097
+ }
23098
+ /** Escape a workflow-command MESSAGE. */
23099
+ function escapeAnnotationMessage(value) {
23100
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
23101
+ }
23109
23102
  /** ANSI-injection guard for the summary path (belt + braces). */
23110
23103
  function stripAnsiForSummary(value) {
23111
23104
  return value.replace(/\x1b\[[0-9;:?]*[ -/]*[@-~]/g, "");
23112
23105
  }
23113
- //#endregion
23114
- //#region src/commands/summary.ts
23106
+ const SEVERITY_TO_COMMAND = {
23107
+ error: "error",
23108
+ warning: "warning",
23109
+ info: "notice"
23110
+ };
23111
+ /** Render ONE workflow-command annotation line (no trailing newline). */
23112
+ function renderAnnotation(a) {
23113
+ const props = [`file=${escapeAnnotationProperty(a.file)}`, `line=${Math.max(1, a.line ?? 1)}`];
23114
+ if (a.column !== void 0) props.push(`column=${Math.max(1, a.column)}`);
23115
+ props.push(`title=${escapeAnnotationProperty(a.title)}`);
23116
+ return `::${SEVERITY_TO_COMMAND[a.severity]} ${props.join(",")}::${escapeAnnotationMessage(a.message)}`;
23117
+ }
23118
+ /** Render annotations for every finding in a report's findings array. */
23119
+ function renderAnnotations(findings) {
23120
+ return findings.map((f) => {
23121
+ return renderAnnotation({
23122
+ severity: f.severity === "error" || f.severity === "warning" ? f.severity : "info",
23123
+ file: f.file,
23124
+ line: f.line,
23125
+ column: f.column,
23126
+ title: f.ruleId,
23127
+ message: f.message
23128
+ });
23129
+ });
23130
+ }
23131
+ /** GitHub caps annotation messages — keep the head, point at the summary. */
23132
+ function truncateMessage(message, max = 250) {
23133
+ if (message.length <= max) return message;
23134
+ return `${message.slice(0, max)}… (full text in the step summary)`;
23135
+ }
23136
+ // src/commands/summary.ts
23137
+ /**
23138
+ * the `--format github-summary` render — CI annotations + step summary
23139
+ * (Terminal + CI UX Overhaul plan, M4).
23140
+ *
23141
+ * Additive command; default report path `mjolnir.json`. Reads a saved
23142
+ * ScanResult JSON (the `--json` scan output) and emits:
23143
+ * 1. GitHub annotations to stdout, only when GITHUB_ACTIONS=true —
23144
+ * one per finding, via the single github.ts emitter.
23145
+ * 2. A step-summary markdown document to $GITHUB_STEP_SUMMARY when
23146
+ * set (else stdout; --stdout forces stdout): score + band, text
23147
+ * score bar, dimensions table, top deductions, collapsible
23148
+ * per-severity details with fix lines, and an honesty notice for
23149
+ * partial/no-score reports.
23150
+ *
23151
+ * Exit codes (frozen contract): 0 on success — this command NEVER
23152
+ * blocks; the gate step decides. 10 missing file argument. 2
23153
+ * unreadable/invalid JSON (a data problem; the message explains).
23154
+ *
23155
+ * Paths: findings are scan-target-relative. The generated workflow
23156
+ * scans the repo root, so the default output is directly usable;
23157
+ * `--path-prefix <dir>` re-scopes for subdirectory scans.
23158
+ */
23115
23159
  const DETAILS_PER_SEVERITY_CAP = 25;
23116
23160
  /**
23117
23161
  * The score bar. BW-105: the canonical gauge with the inert palette — the
@@ -23188,7 +23232,7 @@ function renderStepSummary(result, options = {}) {
23188
23232
  lines.push(`<details${open}>`, `<summary>${icons[sev]} ${list.length} ${sev}${list.length === 1 ? "" : "s"}</summary>`, "");
23189
23233
  for (const f of list.slice(0, DETAILS_PER_SEVERITY_CAP)) {
23190
23234
  const path = options.pathPrefix ? `${options.pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
23191
- lines.push(`- **${escapeMarkdown(f.ruleId)}** \`${escapeMarkdown(path)}:${f.line}\` — ${escapeMarkdown(stripAnsiForSummary(f.message))}`);
23235
+ lines.push(`- **${escapeMarkdown(f.ruleId)}** ${mdCodeSpan(`${path}:${f.line}`)} — ${escapeMarkdown(stripAnsiForSummary(f.message))}`);
23192
23236
  lines.push(` - Fix: ${escapeMarkdown(stripAnsiForSummary(f.fix))}`);
23193
23237
  }
23194
23238
  if (list.length > DETAILS_PER_SEVERITY_CAP) lines.push(`- … and ${list.length - DETAILS_PER_SEVERITY_CAP} more — see the full JSON artifact.`);
@@ -23205,8 +23249,92 @@ function renderStepSummary(result, options = {}) {
23205
23249
  lines.push("<!-- mjolnir-honesty: scores derive from rules with published evidence levels and measured false-positive rates where available. -->");
23206
23250
  return lines.join("\n");
23207
23251
  }
23208
- //#endregion
23209
- //#region src/commands/milestone-args.ts
23252
+ /** Render one finding as an escaped annotation line. Fields are
23253
+ * sanitized with the same sanitizeData layer the terminal uses
23254
+ * (github.ts's escapers cover %/CR/LF, not OSC/C0), then %/CR/LF-escaped. */
23255
+ function annotationForFinding(f) {
23256
+ return renderAnnotations([{
23257
+ ...f,
23258
+ file: sanitizeData(f.file),
23259
+ ruleId: sanitizeData(f.ruleId),
23260
+ message: truncateMessage(stripAnsiForSummary(sanitizeData(f.message)))
23261
+ }])[0];
23262
+ }
23263
+ const KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
23264
+ "--stdout",
23265
+ "--path-prefix",
23266
+ "--help",
23267
+ "-h"
23268
+ ]);
23269
+ /**
23270
+ * Testable summary command core. Returns the process exit code.
23271
+ * Streams: annotations → stdout (always; GitHub greps them), summary →
23272
+ * step-summary file when set unless --stdout. Unknown flags are a
23273
+ * usage error (exit 10) — a typo'd --stdout must not silently route
23274
+ * the summary to $GITHUB_STEP_SUMMARY.
23275
+ */
23276
+ function runSummaryCommand(argv, io = {
23277
+ out: (line) => console.log(line),
23278
+ err: (line) => console.error(line)
23279
+ }) {
23280
+ for (let i = 0; i < argv.length; i++) {
23281
+ const a = argv[i] ?? "";
23282
+ if (!a.startsWith("-")) continue;
23283
+ if (a === "--path-prefix") {
23284
+ const val = argv[i + 1];
23285
+ if (val === void 0 || val.startsWith("-")) {
23286
+ io.err(usageErrorMessage({
23287
+ flag: "--path-prefix",
23288
+ token: val
23289
+ }));
23290
+ return 10;
23291
+ }
23292
+ i++;
23293
+ continue;
23294
+ }
23295
+ if (KNOWN_SUMMARY_FLAGS.has(a)) continue;
23296
+ io.err(usageErrorMessage({ token: a }));
23297
+ return 10;
23298
+ }
23299
+ const stdout = argv.includes("--stdout");
23300
+ const prefixIdx = argv.indexOf("--path-prefix");
23301
+ const pathPrefix = prefixIdx !== -1 ? argv[prefixIdx + 1] : void 0;
23302
+ const reportPath = argv.filter((a, i) => a !== void 0 && !a.startsWith("-") && (prefixIdx === -1 || i !== prefixIdx + 1))[0] ?? "mjolnir.json";
23303
+ if (!existsSync(reportPath)) {
23304
+ io.err(`mjolnir: report file not found: ${reportPath}`);
23305
+ io.err(" Run the scan with --json first: mjolnir --json > mjolnir.json");
23306
+ return 10;
23307
+ }
23308
+ let result;
23309
+ try {
23310
+ result = loadSavedReport(reportPath);
23311
+ } catch (err) {
23312
+ io.err(`mjolnir: cannot read ${reportPath}: ${errorMessage(err)}`);
23313
+ return 2;
23314
+ }
23315
+ const env = process.env;
23316
+ if (env["GITHUB_ACTIONS"] === "true") for (const f of result.findings) {
23317
+ const file = pathPrefix ? `${pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
23318
+ io.out(annotationForFinding({
23319
+ ...f,
23320
+ file
23321
+ }));
23322
+ }
23323
+ const summary = renderStepSummary(result, {
23324
+ stdout,
23325
+ ...pathPrefix !== void 0 ? { pathPrefix } : {}
23326
+ });
23327
+ const stepSummaryPath = stdout ? void 0 : env["GITHUB_STEP_SUMMARY"];
23328
+ if (stepSummaryPath) try {
23329
+ appendFileSync(stepSummaryPath, `${summary}\n`);
23330
+ } catch (err) {
23331
+ io.err(`mjolnir: could not write $GITHUB_STEP_SUMMARY (${errorMessage(err)}); printing to stdout instead.`);
23332
+ io.out(summary);
23333
+ }
23334
+ else io.out(summary);
23335
+ return 0;
23336
+ }
23337
+ // src/commands/milestone-args.ts
23210
23338
  /**
23211
23339
  * The argument layer every milestone command shares.
23212
23340
  *
@@ -23358,8 +23486,7 @@ function isWorkflowFile(path) {
23358
23486
  const normalized = path.replaceAll("\\", "/");
23359
23487
  return normalized.startsWith(".github/workflows/") || normalized === ".gitlab-ci.yml" || normalized === ".gitlab-ci.yaml" || basename(normalized) === "Jenkinsfile";
23360
23488
  }
23361
- //#endregion
23362
- //#region src/engine/cross-file.ts
23489
+ // src/engine/cross-file.ts
23363
23490
  /**
23364
23491
  * Cross-file analysis (Upgrade-Plan-v3 Phase 6, tier-3 item #2).
23365
23492
  *
@@ -23405,8 +23532,7 @@ function findDuplicateTestNames(files) {
23405
23532
  });
23406
23533
  return out.sort((a, b) => compareCodePoints(a.name, b.name));
23407
23534
  }
23408
- //#endregion
23409
- //#region src/engine/cross-file-analysis.ts
23535
+ // src/engine/cross-file-analysis.ts
23410
23536
  /**
23411
23537
  * Analyze cross-file signals across the scanned test files.
23412
23538
  * Pure function — same input always produces same output.
@@ -23544,8 +23670,7 @@ function renderCrossFileAnalysis(result) {
23544
23670
  }
23545
23671
  return lines.join("\n").trimEnd();
23546
23672
  }
23547
- //#endregion
23548
- //#region src/engine/evidence-graph.ts
23673
+ // src/engine/evidence-graph.ts
23549
23674
  /**
23550
23675
  * Build a complete evidence graph from a scan result.
23551
23676
  * Pure function — deterministic output for the same input.
@@ -23691,8 +23816,7 @@ function renderEvidenceGraphResult(result) {
23691
23816
  lines.push(` ${result.agenticProfile.note}`);
23692
23817
  return lines.join("\n").trimEnd();
23693
23818
  }
23694
- //#endregion
23695
- //#region src/frameworks/scorecard.ts
23819
+ // src/frameworks/scorecard.ts
23696
23820
  const SCORECARD_DIMENSIONS = [
23697
23821
  "discovery",
23698
23822
  "astUsage",
@@ -24802,8 +24926,7 @@ const FRAMEWORK_SCORECARDS = [
24802
24926
  makeScorecard("jenkins", FRAMEWORK_OVERRIDES["jenkins"]),
24803
24927
  makeScorecard("gitlab-ci", FRAMEWORK_OVERRIDES["gitlab-ci"])
24804
24928
  ];
24805
- //#endregion
24806
- //#region src/engine/framework-maturity.ts
24929
+ // src/engine/framework-maturity.ts
24807
24930
  /**
24808
24931
  * M5 — Framework Maturity Tracking for Playwright.
24809
24932
  *
@@ -24892,8 +25015,7 @@ function renderPlaywrightMaturityReport(report) {
24892
25015
  }
24893
25016
  return lines.join("\n").trimEnd();
24894
25017
  }
24895
- //#endregion
24896
- //#region src/engine/suppression-integrity.ts
25018
+ // src/engine/suppression-integrity.ts
24897
25019
  /**
24898
25020
  * ENGINE-006 — Suppression Integrity: detect suppression abuse,
24899
25021
  * unknown rules, expired entries, and compute a full integrity report.
@@ -24987,8 +25109,7 @@ function computeSuppressionIntegrity(suppressions, findings, knownRuleIds, now =
24987
25109
  expiredSuppressions: detectExpiredSuppressions(suppressions, now)
24988
25110
  };
24989
25111
  }
24990
- //#endregion
24991
- //#region src/engine/suppression-governance.ts
25112
+ // src/engine/suppression-governance.ts
24992
25113
  /**
24993
25114
  * M6 — Suppression Policy Governance Gate.
24994
25115
  *
@@ -25051,15 +25172,13 @@ function renderSuppressionGovernanceResult(result) {
25051
25172
  } else lines.push("All suppression policy checks passed.");
25052
25173
  return lines.join("\n").trimEnd();
25053
25174
  }
25054
- //#endregion
25055
- //#region src/adapters/gitlab-ci.ts
25175
+ // src/adapters/gitlab-ci.ts
25056
25176
  function parseGitLabCi(content) {
25057
25177
  const parsed = parse(content);
25058
25178
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("GitLab CI YAML is not a valid object.");
25059
25179
  return parsed;
25060
25180
  }
25061
- //#endregion
25062
- //#region src/engine/cli-command-names.ts
25181
+ // src/engine/cli-command-names.ts
25063
25182
  const CLI_COMMAND_NAMES = [
25064
25183
  "scan",
25065
25184
  "policy",
@@ -25079,8 +25198,7 @@ const CLI_COMMAND_NAMES = [
25079
25198
  "help"
25080
25199
  ];
25081
25200
  const CLI_NON_SCAN_COMMANDS = new Set(CLI_COMMAND_NAMES.filter((command) => command !== "scan"));
25082
- //#endregion
25083
- //#region src/engine/verification-intelligence.ts
25201
+ // src/engine/verification-intelligence.ts
25084
25202
  const CI_WORKFLOW_FILES = [
25085
25203
  ".github/workflows/ci.yml",
25086
25204
  ".github/workflows/test.yml",
@@ -25571,8 +25689,7 @@ function renderCIIntegrityReport(report) {
25571
25689
  }
25572
25690
  return lines.join("\n").trimEnd();
25573
25691
  }
25574
- //#endregion
25575
- //#region src/commands/milestone.ts
25692
+ // src/commands/milestone.ts
25576
25693
  async function runMilestoneScan(args, io, readSources, strict = false) {
25577
25694
  const target = resolve(args.target);
25578
25695
  const valid = validateTarget(target, io);
@@ -25804,8 +25921,7 @@ async function runCIIntegrityCommand(argv, io = {
25804
25921
  return milestoneError(error, io, argv.includes("--debug"));
25805
25922
  }
25806
25923
  }
25807
- //#endregion
25808
- //#region src/scorer/scoring-policy.ts
25924
+ // src/scorer/scoring-policy.ts
25809
25925
  /**
25810
25926
  * `mjolnir scan --policy` — the scoring policy, as the code states it.
25811
25927
  *
@@ -25867,8 +25983,7 @@ function renderScoringPolicy() {
25867
25983
  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
25984
  return lines.join("\n");
25869
25985
  }
25870
- //#endregion
25871
- //#region src/commands/fix.ts
25986
+ // src/commands/fix.ts
25872
25987
  /**
25873
25988
  * `mjolnir fix` — Safe Auto-Fix with Proof (Tier 1 #3).
25874
25989
  *
@@ -26292,8 +26407,7 @@ function renderFixReport(results, dryRun) {
26292
26407
  lines.push(`${parts.join(" · ")}${dryRun ? " (dry-run)" : ""}. Every applied fix was verified against the rule that flagged it.`);
26293
26408
  return lines.join("\n");
26294
26409
  }
26295
- //#endregion
26296
- //#region src/commands/rules-catalog.ts
26410
+ // src/commands/rules-catalog.ts
26297
26411
  /**
26298
26412
  * `mjolnir explain --list` — machine-readable rule catalog with Trust Metadata.
26299
26413
  *
@@ -26510,8 +26624,7 @@ function countCompleteQuads(ruleIds, root = process.cwd()) {
26510
26624
  total: ruleIds.length
26511
26625
  };
26512
26626
  }
26513
- //#endregion
26514
- //#region src/frameworks/provider-capability-contract.ts
26627
+ // src/frameworks/provider-capability-contract.ts
26515
26628
  const CI_PROVIDER_CAPABILITY_SCHEMA = "m30.ci-provider-capabilities@1";
26516
26629
  const CI_PROVIDER_IDS = [
26517
26630
  "github-actions",
@@ -26757,8 +26870,7 @@ const CI_PROVIDER_CAPABILITY_RECORDS = [
26757
26870
  }
26758
26871
  ];
26759
26872
  new Map(CI_PROVIDER_CAPABILITY_RECORDS.map((record) => [record.provider, record]));
26760
- //#endregion
26761
- //#region src/v6/maturity.ts
26873
+ // src/v6/maturity.ts
26762
26874
  /**
26763
26875
  * v6 capability maturity ladder — `M0`–`M5` (ADR 0001).
26764
26876
  *
@@ -26957,8 +27069,7 @@ function nextLevelGapFromEvidence(evidence, owner, revisitTrigger) {
26957
27069
  question: "How much does it matter? (technical severity stays separate)"
26958
27070
  }
26959
27071
  ].map((axis) => axis.id);
26960
- //#endregion
26961
- //#region src/v6/ecosystem-census.ts
27072
+ // src/v6/ecosystem-census.ts
26962
27073
  /**
26963
27074
  * The leading major of a version or range. `FRAMEWORK_INVENTORY`
26964
27075
  * records *validated versions* (`1.44`, `29`) while a manifest records a
@@ -26971,8 +27082,7 @@ function versionMajor(raw) {
26971
27082
  if (!match) return null;
26972
27083
  return (match[0] ?? "").split(".")[0] ?? null;
26973
27084
  }
26974
- //#endregion
26975
- //#region src/v6/ecosystem-probe.ts
27085
+ // src/v6/ecosystem-probe.ts
26976
27086
  /**
26977
27087
  * Filesystem-backed census evidence + the discovery probe (ADR 0010).
26978
27088
  *
@@ -27224,8 +27334,7 @@ function corpusReposOf(root) {
27224
27334
  }
27225
27335
  });
27226
27336
  }
27227
- //#endregion
27228
- //#region src/v6/capability-registry.ts
27337
+ // src/v6/capability-registry.ts
27229
27338
  /**
27230
27339
 
27231
27340
  }
@@ -27708,8 +27817,7 @@ function validateRegistry(registry) {
27708
27817
  }
27709
27818
  return diagnostics;
27710
27819
  }
27711
- //#endregion
27712
- //#region src/commands/capability.ts
27820
+ // src/commands/capability.ts
27713
27821
  /** Parse the flags. Anything flag-shaped that is not documented is a
27714
27822
  * usage error rather than a silently ignored argument, because
27715
27823
  * `capability --promote-m3` must not read as "show me everything". */
@@ -27829,8 +27937,7 @@ function runCapabilityCommand(argv, io = {
27829
27937
  io.out(json ? capabilityJson(entries, diagnostics) : `${renderCapabilityText(entries, diagnostics)}\n`);
27830
27938
  return diagnostics.length > 0 ? 1 : 0;
27831
27939
  }
27832
- //#endregion
27833
- //#region src/rules/measurement-status.ts
27940
+ // src/rules/measurement-status.ts
27834
27941
  /**
27835
27942
  * Derive the measurement status for a single rule.
27836
27943
  *
@@ -27877,8 +27984,7 @@ function getMeasurementStatus(rules = RULES) {
27877
27984
  };
27878
27985
  });
27879
27986
  }
27880
- //#endregion
27881
- //#region src/commands/rule-health.ts
27987
+ // src/commands/rule-health.ts
27882
27988
  /**
27883
27989
  * Build the per-rule health table from the shipped registry. Deterministic
27884
27990
  * order: the RULES registry order (stable across runs).
@@ -28009,8 +28115,7 @@ function renderRuleHealth(rows, limit) {
28009
28115
  return lines.join("\n");
28010
28116
  }
28011
28117
  Object.keys(MEASURED_FP).length;
28012
- //#endregion
28013
- //#region src/playwright/selector-health-types.ts
28118
+ // src/playwright/selector-health-types.ts
28014
28119
  /** Risk weights per locator shape (0 = resilient, higher = brittle). */
28015
28120
  const LOCATOR_RISK = {
28016
28121
  "role-based": 0,
@@ -28019,8 +28124,7 @@ const LOCATOR_RISK = {
28019
28124
  "css-chain": 60,
28020
28125
  xpath: 90
28021
28126
  };
28022
- //#endregion
28023
- //#region src/playwright/selector-health.ts
28127
+ // src/playwright/selector-health.ts
28024
28128
  /**
28025
28129
  * Selector Health Score engine (Upgrade-Plan-v2 R3, the headline feature).
28026
28130
  *
@@ -28148,8 +28252,7 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
28148
28252
  walk(root);
28149
28253
  return specs.sort((a, b) => a.score - b.score || compareCodePoints(a.file, b.file));
28150
28254
  }
28151
- //#endregion
28152
- //#region src/cli-handlers.ts
28255
+ // src/cli-handlers.ts
28153
28256
  /**
28154
28257
  * Handler functions for every Mjolnir CLI verb.
28155
28258
  *
@@ -28158,6 +28261,35 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
28158
28261
  * (Task 6: renderScanOutput helper).
28159
28262
  */
28160
28263
  /**
28264
+ * The one catch for a scan-backed verb.
28265
+ *
28266
+ * An invalid `mjolnir.config.json` is the USER's error and exits 10 (usage),
28267
+ * as `scan` always did. `fix`, `ci verify` and `explain --plan` each had
28268
+ * their own catch that only knew the internal-error arm, so the same config
28269
+ * typo exited 20 and printed "this is a bug in Mjölnir" on three verbs.
28270
+ */
28271
+ function verbFailure(error, io, debug) {
28272
+ if (error instanceof ConfigValidationError) {
28273
+ io.err(error.message);
28274
+ return 10;
28275
+ }
28276
+ internalErrorMessage(error, io.err, debug);
28277
+ return 20;
28278
+ }
28279
+ /**
28280
+ * The notices a scan reports about itself, sent to the verb's own stderr.
28281
+ *
28282
+ * `fix`, `ci verify` and `handover` called `runScan` with no hooks, so a
28283
+ * config warning (an unknown key, an expired suppression) never reached the
28284
+ * user on those verbs, and the plugin gate notice bypassed `io.err`.
28285
+ */
28286
+ function scanNotices(io) {
28287
+ return {
28288
+ onConfigWarning: (message) => io.err(message),
28289
+ onGateNotice: (notice) => io.err(notice)
28290
+ };
28291
+ }
28292
+ /**
28161
28293
  * The one place a run's gate level is resolved from `--blocking`, then
28162
28294
  * `mjolnir.config.json`, then the shipped default.
28163
28295
  *
@@ -28207,9 +28339,11 @@ function renderScanOutput(result, args, target, io, gate) {
28207
28339
  else if (args.format === "pr-comment") {
28208
28340
  const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH));
28209
28341
  const diff = baseline ? diffAgainstBaseline(result, baseline) : void 0;
28342
+ const commit = commitBoundTo(result, target);
28210
28343
  io.out(renderPrComment(result, {
28211
28344
  ...diff ? { diff } : {},
28212
- version: ENGINE_VERSION
28345
+ version: ENGINE_VERSION,
28346
+ ...commit !== void 0 ? { commit } : {}
28213
28347
  }));
28214
28348
  } else if (args.format === "github-summary") io.out(renderStepSummary(result));
28215
28349
  else if (args.json) io.out(JSON.stringify({
@@ -28298,6 +28432,58 @@ async function runSuppressionGate(args, target, io) {
28298
28432
  if (args.base !== void 0) argv.push("--base", args.base);
28299
28433
  return runSuppressionGateCommand(argv, io);
28300
28434
  }
28435
+ /**
28436
+ * The commit a rendered report may claim, or undefined when it cannot claim one.
28437
+ *
28438
+ * The scan records the HEAD it analysed (`runIdentity.commit`). A report states
28439
+ * that commit only when the repository at `dir` is still at it, so a report
28440
+ * rendered from another commit's JSON reads `unknown` instead of borrowing an
28441
+ * identity nothing here can confirm. The retired `pr-comment --from` verb took
28442
+ * the commit from a `--commit` argument; the formats that replaced it check the
28443
+ * checkout instead, which needs no flag and cannot be handed the wrong value.
28444
+ */
28445
+ function commitBoundTo(result, dir) {
28446
+ const recorded = result.runIdentity?.commit;
28447
+ if (recorded === void 0) return void 0;
28448
+ return currentCommit(dir, { nullable: true }) === recorded ? recorded : void 0;
28449
+ }
28450
+ /**
28451
+ * `--format github-summary|pr-comment <report.json>`: render a SAVED report.
28452
+ *
28453
+ * The v6 collapse retired `summary <report>` and `pr-comment --from <report>`
28454
+ * onto these two formats and shipped only the half that scans. Every CI
28455
+ * consumer renders the `--json` report its scan step already wrote: the
28456
+ * Action, both `ci install` templates and this repository's own PR workflow.
28457
+ * All of them kept calling the retired verbs, and all of them exited 10. A
28458
+ * second scan is not a substitute, because the comment has to describe the
28459
+ * result the gate read rather than one that happens to agree with it.
28460
+ *
28461
+ * Delegation, not reimplementation, which is the rule `--suppression-gate`
28462
+ * follows. Each retired verb's command still owns its contract, exit codes
28463
+ * included. The render never gates and exits 0. A report that cannot be read
28464
+ * exits 2 for the summary and 10 for the comment, as those verbs did.
28465
+ *
28466
+ * Returns null when the request is a scan: another format, or a target that
28467
+ * is not a file.
28468
+ */
28469
+ function renderSavedReport(args, target, io) {
28470
+ if (args.format !== "github-summary" && args.format !== "pr-comment") return null;
28471
+ if (!isRegularFile(target)) return null;
28472
+ if (args.format === "github-summary") return runSummaryCommand([target], io);
28473
+ return runPrCommentCommand([
28474
+ "--from",
28475
+ target,
28476
+ "--version",
28477
+ ENGINE_VERSION
28478
+ ], io);
28479
+ }
28480
+ function isRegularFile(path) {
28481
+ try {
28482
+ return statSync(path).isFile();
28483
+ } catch {
28484
+ return false;
28485
+ }
28486
+ }
28301
28487
  async function runScanCommand(argv, io = {
28302
28488
  out,
28303
28489
  err
@@ -28305,6 +28491,8 @@ async function runScanCommand(argv, io = {
28305
28491
  const args = parseArgsOrUsage(argv, io);
28306
28492
  if (!args) return 10;
28307
28493
  const target = resolve(args.target);
28494
+ const savedRender = renderSavedReport(args, target, io);
28495
+ if (savedRender !== null) return savedRender;
28308
28496
  const invalid = validateScanTarget(target, io.err);
28309
28497
  if (invalid !== null) return invalid;
28310
28498
  if (args.suppressions) return printSuppressionLedger(target, io);
@@ -28325,24 +28513,26 @@ async function runScanCommand(argv, io = {
28325
28513
  ...args,
28326
28514
  target
28327
28515
  }, {
28328
- onConfigWarning: (message) => io.err(message),
28516
+ ...scanNotices(io),
28329
28517
  onProgress: (e) => progress.onEvent(e),
28330
- onGateNotice: (notice) => io.err(notice),
28331
28518
  ...args.debug ? { onRuleCrash: (ruleId, file, error) => {
28332
28519
  const detail = error instanceof Error ? [error.message, error.stack].filter((value) => Boolean(value)).join("\n") : String(error);
28333
28520
  crashLog.push(`${ruleId} crashed on ${file}: ${detail}`);
28334
28521
  } } : {}
28335
28522
  });
28336
28523
  progress.done();
28524
+ let baselineFailed = false;
28337
28525
  if (args.saveBaseline) {
28338
28526
  const outPath = join(target, DEFAULT_BASELINE_PATH);
28339
- try {
28527
+ const notice = args.format === "terminal" ? io.out : io.err;
28528
+ 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.");
28529
+ else try {
28340
28530
  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 } : {} }));
28531
+ notice(renderBaselineSaved(DEFAULT_BASELINE_PATH, result.findings.length, { ...saved.backupPath !== void 0 ? { backupPath: saved.backupPath } : {} }));
28342
28532
  } catch (saveErr) {
28343
28533
  io.err(`baseline save FAILED — ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
28344
28534
  io.err("The scan completed; the snapshot was not written. Fix the path permissions and re-run with `--save-baseline`.");
28345
- return 1;
28535
+ baselineFailed = true;
28346
28536
  }
28347
28537
  }
28348
28538
  if (args.debug && crashLog.length > 0) {
@@ -28350,7 +28540,7 @@ async function runScanCommand(argv, io = {
28350
28540
  for (const line of crashLog.slice(0, 50)) io.err(` ${line}`);
28351
28541
  if (crashLog.length > 50) io.err(` … and ${crashLog.length - 50} more`);
28352
28542
  }
28353
- const { config, warnings } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
28543
+ const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
28354
28544
  for (const d of args.deprecatedFlags ?? []) io.err(`warning: ${d.flag} is deprecated and will be removed in 7.0 — use ${d.replacement}. It still does exactly what it did; nothing about your scan changes this release.`);
28355
28545
  const gate = resolveGateLevel(args.blocking, config);
28356
28546
  if (args.scoreOnly) {
@@ -28377,7 +28567,7 @@ async function runScanCommand(argv, io = {
28377
28567
  }
28378
28568
  }
28379
28569
  if (result.partial) return 2;
28380
- for (const w of warnings) io.err(w);
28570
+ if (baselineFailed) return 2;
28381
28571
  return scanExitCode({
28382
28572
  partial: false,
28383
28573
  findings: result.findings,
@@ -28387,12 +28577,7 @@ async function runScanCommand(argv, io = {
28387
28577
  ...args.requireFullCoverage !== void 0 ? { requireFullCoverage: args.requireFullCoverage } : {}
28388
28578
  });
28389
28579
  } 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;
28580
+ return verbFailure(err, io, args?.debug === true);
28396
28581
  }
28397
28582
  }
28398
28583
  async function runFixCommand(argv, io = {
@@ -28406,16 +28591,17 @@ async function runFixCommand(argv, io = {
28406
28591
  const target = resolve(args.target);
28407
28592
  const invalid = validateScanTarget(target, io.err);
28408
28593
  if (invalid !== null) return invalid;
28409
- const fixes = planAndApplyFixes(await runScan$1({
28594
+ const result = await runScan$1({
28410
28595
  ...args,
28411
28596
  target,
28412
28597
  strict: true
28413
- }), target, { dryRun });
28598
+ }, scanNotices(io));
28599
+ const fixes = planAndApplyFixes(result, target, { dryRun });
28414
28600
  io.out(renderFixReport(fixes, dryRun));
28601
+ if (result.partial) return 2;
28415
28602
  return fixes.some((f) => f.status === "failed") ? 1 : 0;
28416
28603
  } catch (err) {
28417
- internalErrorMessage(err, io.err, args?.debug === true);
28418
- return 20;
28604
+ return verbFailure(err, io, args?.debug === true);
28419
28605
  }
28420
28606
  }
28421
28607
  async function runVerifyCommand(argv, io = {
@@ -28431,15 +28617,21 @@ async function runVerifyCommand(argv, io = {
28431
28617
  const result = await runScan$1({
28432
28618
  ...args,
28433
28619
  target
28434
- });
28435
- const digest = buildVerifyDigest(result, loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w)));
28620
+ }, scanNotices(io));
28621
+ const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w));
28622
+ const digest = buildVerifyDigest(result, baseline);
28436
28623
  io.out(renderVerifyDigest(digest));
28437
28624
  if (result.partial) return 2;
28438
28625
  if (!digest.hasBaseline) return 2;
28439
- return digest.new.some((f) => f.severity === "error") ? 1 : 0;
28626
+ const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
28627
+ return scanExitCode({
28628
+ partial: false,
28629
+ findings: diffAgainstBaseline(result, baseline).newFindings,
28630
+ gate: resolveGateLevel(args.blocking, config),
28631
+ isAdvisory: isAdvisoryFinding
28632
+ });
28440
28633
  } catch (err) {
28441
- internalErrorMessage(err, io.err, args?.debug === true);
28442
- return 20;
28634
+ return verbFailure(err, io, args?.debug === true);
28443
28635
  }
28444
28636
  }
28445
28637
  async function runPrCommentCommand(argv, io = {
@@ -28454,13 +28646,14 @@ async function runPrCommentCommand(argv, io = {
28454
28646
  return 10;
28455
28647
  }
28456
28648
  try {
28457
- const saved = loadSavedReport(resolve(fromPath));
28649
+ const reportPath = resolve(fromPath);
28650
+ const saved = loadSavedReport(reportPath);
28458
28651
  const option = (flag) => {
28459
28652
  const index = argv.indexOf(flag);
28460
28653
  return index >= 0 ? argv[index + 1] : void 0;
28461
28654
  };
28462
28655
  const version = option("--version");
28463
- const commit = option("--commit");
28656
+ const commit = option("--commit") ?? commitBoundTo(saved, dirname(reportPath));
28464
28657
  const repoUrl = option("--repo-url");
28465
28658
  io.out(renderPrComment(saved, {
28466
28659
  ...version ? { version } : {},
@@ -28523,17 +28716,21 @@ async function runHandoverCommand(argv, io = {
28523
28716
  const result = await runScan$1({
28524
28717
  ...args,
28525
28718
  target
28526
- });
28719
+ }, scanNotices(io));
28527
28720
  let forensics = null;
28721
+ let forensicsFailed = false;
28528
28722
  const resultsDir = join(target, "test-results");
28529
28723
  if (existsSync(resultsDir)) try {
28530
28724
  forensics = runForensics(resultsDir, { writeFlakyMd: false }).report;
28531
- } catch {}
28725
+ } catch (forensicsErr) {
28726
+ io.err(`warning: test-results/ could not be read (${errorMessage(forensicsErr)}) — the map below is static-only.`);
28727
+ forensicsFailed = true;
28728
+ }
28532
28729
  io.out(renderHandover(buildHandover(result, forensics)));
28730
+ if (result.partial || forensicsFailed) return 2;
28533
28731
  return 0;
28534
28732
  } catch (err) {
28535
- internalErrorMessage(err, io.err, args?.debug === true);
28536
- return 20;
28733
+ return verbFailure(err, io, args?.debug === true);
28537
28734
  }
28538
28735
  }
28539
28736
  function runPwReportCommand(argv, io = {
@@ -28667,7 +28864,7 @@ function runEvidenceArm(argv, io) {
28667
28864
  const { report } = runForensics(resolve(target), { writeFlakyMd });
28668
28865
  if (asJson) io.out(JSON.stringify(report, null, 2));
28669
28866
  else io.out(asMd ? renderTriageMd(report) : renderTriage(report));
28670
- return report.analysisComplete === false ? 1 : 0;
28867
+ return report.analysisComplete === false ? 2 : 0;
28671
28868
  } catch (err) {
28672
28869
  internalErrorMessage(err, io.err, argv.includes("--debug"));
28673
28870
  return 20;
@@ -28820,8 +29017,7 @@ async function runDoctorPlaywright(argv, io = { out }) {
28820
29017
  return 20;
28821
29018
  }
28822
29019
  }
28823
- //#endregion
28824
- //#region src/rules/rule-metadata-schema.ts
29020
+ // src/rules/rule-metadata-schema.ts
28825
29021
  const VALID_SEVERITIES = SEVERITY_ORDER$1;
28826
29022
  const VALID_CATEGORIES = RULE_CATEGORIES;
28827
29023
  const VALID_QA_IMPACTS = Object.keys(QA_IMPACT_LABELS);
@@ -28869,8 +29065,7 @@ function validateAllRulesMetadata(rules) {
28869
29065
  for (const rule of rules) all.push(...validateRuleMetadata(rule));
28870
29066
  return all;
28871
29067
  }
28872
- //#endregion
28873
- //#region src/engine/detector-hash.ts
29068
+ // src/engine/detector-hash.ts
28874
29069
  /**
28875
29070
  * Detector revision integrity engine (certification-audit Phase 3,
28876
29071
  * G4/D8v2/D17).
@@ -29138,8 +29333,7 @@ const RULE_ID_RE = new RegExp(`^QA-(?:${[
29138
29333
  appliesTo: "test-files"
29139
29334
  }
29140
29335
  ].map((f) => f.token).join("|")})-\\d{3}$`);
29141
- //#endregion
29142
- //#region src/commands/doctor.ts
29336
+ // src/commands/doctor.ts
29143
29337
  /**
29144
29338
  * `mjolnir doctor` — self-audit of Mjolnir's own rule base.
29145
29339
  *
@@ -30086,8 +30280,7 @@ function stripNonDeterministicFields(json, fields = NON_DETERMINISTIC_FIELDS) {
30086
30280
  for (const field of fields) delete json[field];
30087
30281
  return json;
30088
30282
  }
30089
- //#endregion
30090
- //#region src/commands/doctor-run.ts
30283
+ // src/commands/doctor-run.ts
30091
30284
  /**
30092
30285
  * Doctor CLI entry (certification-audit Phase 5, G6): the command handler
30093
30286
  * lives beside the check implementations instead of in cli.ts — the CLI
@@ -30185,8 +30378,7 @@ function installedPackageRoot() {
30185
30378
  return process.cwd();
30186
30379
  }
30187
30380
  }
30188
- //#endregion
30189
- //#region src/commands/handoff.ts
30381
+ // src/commands/handoff.ts
30190
30382
  /**
30191
30383
  * `mjolnir handoff [mjolnir.json]` — the deterministic fix-handoff
30192
30384
  * artifact (agent-handoff plan M3).
@@ -30272,7 +30464,7 @@ function verificationBlock(version) {
30272
30464
  /** The per-rule remediation copy block (fenced, self-contained). */
30273
30465
  function ruleCopyBlock(group, version) {
30274
30466
  const f = group.findings[0];
30275
- const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `- \`${escapeMarkdown(x.file)}:${x.line}\``).join("\n");
30467
+ const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `- ${mdCodeSpan(`${x.file}:${x.line}`)}`).join("\n");
30276
30468
  const overflow = group.findings.length > OCCURRENCE_CAP ? `\n- … and ${group.findings.length - OCCURRENCE_CAP} more — see the JSON report.` : "";
30277
30469
  return [
30278
30470
  "```text",
@@ -30398,7 +30590,7 @@ function renderHandoff(result, options = {}, version = ENGINE_VERSION) {
30398
30590
  lines.push("");
30399
30591
  lines.push(`**Occurrences (${g.findings.length}):**`);
30400
30592
  lines.push("");
30401
- for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`- \`${escapeMarkdown(f.file)}:${f.line}\` — ${escapeMarkdown(f.message)}`);
30593
+ for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`- ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escapeMarkdown(f.message)}`);
30402
30594
  if (g.findings.length > OCCURRENCE_CAP) lines.push(`- … and ${g.findings.length - OCCURRENCE_CAP} more — see the JSON report.`);
30403
30595
  lines.push("");
30404
30596
  lines.push(ruleCopyBlock(g, version));
@@ -30496,8 +30688,7 @@ function runHandoffCommand(argv, io = {
30496
30688
  }));
30497
30689
  return 0;
30498
30690
  }
30499
- //#endregion
30500
- //#region src/commands/install-agents.ts
30691
+ // src/commands/install-agents.ts
30501
30692
  /**
30502
30693
  * `mjolnir install` — instruction-surface installer (agent-handoff
30503
30694
  * plan M4).
@@ -30823,8 +31014,7 @@ function executeHookInstall(entry) {
30823
31014
  default: return false;
30824
31015
  }
30825
31016
  }
30826
- //#endregion
30827
- //#region src/commands/release-trust.ts
31017
+ // src/commands/release-trust.ts
30828
31018
  /**
30829
31019
  * `mjolnir ci release-trust` — the Release Trust Verdict (product-gap
30830
31020
  * master plan §5, plan 1789009691197 R4a).
@@ -31535,8 +31725,7 @@ function runReleaseTrustCommand(argv, io = {
31535
31725
  return 20;
31536
31726
  }
31537
31727
  }
31538
- //#endregion
31539
- //#region src/commands/policy.ts
31728
+ // src/commands/policy.ts
31540
31729
  /**
31541
31730
  * `mjolnir policy` — Policy-as-Code (TL-2).
31542
31731
  *
@@ -31660,8 +31849,7 @@ async function runPolicyCommand(argv, io) {
31660
31849
  io.err(`Unknown policy subcommand: ${subcommand}`);
31661
31850
  return 10;
31662
31851
  }
31663
- //#endregion
31664
- //#region src/commands/analyze.ts
31852
+ // src/commands/analyze.ts
31665
31853
  /**
31666
31854
  * `mjolnir analyze --cross-file` — Cross-File Analysis (SDET-7).
31667
31855
  *
@@ -31797,8 +31985,7 @@ function runAnalyzeCommand(argv, io) {
31797
31985
  return 20;
31798
31986
  }
31799
31987
  }
31800
- //#endregion
31801
- //#region src/commands/ci-adapter.ts
31988
+ // src/commands/ci-adapter.ts
31802
31989
  /**
31803
31990
  * `mjolnir ci adapters` — CI Adapters (SDET-4).
31804
31991
  *
@@ -31919,8 +32106,7 @@ function runCiAdapterCommand(argv, io) {
31919
32106
  io.out(output);
31920
32107
  return 0;
31921
32108
  }
31922
- //#endregion
31923
- //#region src/engine/machine-contract-verification.ts
32109
+ // src/engine/machine-contract-verification.ts
31924
32110
  /**
31925
32111
  * M11 — Machine Contract Verification.
31926
32112
  *
@@ -32082,8 +32268,7 @@ function renderContractVerification(result) {
32082
32268
  }
32083
32269
  return lines.join("\n").trimEnd();
32084
32270
  }
32085
- //#endregion
32086
- //#region src/commands/contract-verify.ts
32271
+ // src/commands/contract-verify.ts
32087
32272
  /**
32088
32273
  * `contract-verify` — the machine-contract and exit-code validators, and the
32089
32274
  * command that runs them.
@@ -32240,8 +32425,7 @@ async function runContractVerifyCommand(argv, io = {
32240
32425
  return milestoneError(error, io, argv.includes("--debug"));
32241
32426
  }
32242
32427
  }
32243
- //#endregion
32244
- //#region src/commands/share.ts
32428
+ // src/commands/share.ts
32245
32429
  /**
32246
32430
  * `mjolnir share` — React Doctor's proof loop, without its server.
32247
32431
  *
@@ -32471,8 +32655,7 @@ async function runShareCommand(argv, io) {
32471
32655
  return 20;
32472
32656
  }
32473
32657
  }
32474
- //#endregion
32475
- //#region src/engine/historical-trust.ts
32658
+ // src/engine/historical-trust.ts
32476
32659
  /**
32477
32660
  * Compute a trust snapshot from a scan result and run identity.
32478
32661
  * Pure function — deterministic output for the same input.
@@ -32658,8 +32841,7 @@ function renderTrustTrend(trend) {
32658
32841
  }
32659
32842
  return lines.join("\n").trimEnd();
32660
32843
  }
32661
- //#endregion
32662
- //#region src/commands/release-trend.ts
32844
+ // src/commands/release-trend.ts
32663
32845
  /**
32664
32846
  * `ci release-trend` — how the signed release's trust record has moved across
32665
32847
  * releases.
@@ -32716,8 +32898,7 @@ async function runTrustTrendCommand(argv, io = {
32716
32898
  return milestoneError(error, io, argv.includes("--debug"));
32717
32899
  }
32718
32900
  }
32719
- //#endregion
32720
- //#region src/cli.ts
32901
+ // src/cli.ts
32721
32902
  /**
32722
32903
  * Mjölnir CLI entry point (W1-02).
32723
32904
  * Exit codes (§24.1, frozen): 0 clean · 1 findings ≥ gate · 2 partial ·
@@ -32730,16 +32911,14 @@ async function runTrustTrendCommand(argv, io = {
32730
32911
  * bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
32731
32912
  * where the file happens to sit after install. This follows the same
32732
32913
  * 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
32914
+ * `npm run version:surface:sync` (it rewrites every identity surface) on
32915
+ * release and guarded by
32735
32916
  * `tests/contract/version-consistency.spec.ts` locally. R4c moved the literal to
32736
32917
  * src/engine/version.ts (a leaf module) so the scan pipeline's run
32737
32918
  * identity can carry it without a cli.ts import cycle; this re-export
32738
32919
  * keeps every existing consumer stable.
32739
32920
  */
32740
- const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
32741
- const DEFAULT_MAX_DURATION_MS = 6e5;
32742
- const MAX_DURATION_MS = 36e5;
32921
+ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS, DEFAULT_MAX_DURATION_MS, MAX_DURATION_MS } = scan_pipeline_exports;
32743
32922
  function parseArgs(argv, onError) {
32744
32923
  const args = {
32745
32924
  target: ".",
@@ -32798,7 +32977,7 @@ function parseArgs(argv, onError) {
32798
32977
  args.base = ref;
32799
32978
  } else if (a === "--max-duration") {
32800
32979
  const v = Number(argv[++i]);
32801
- if (!Number.isFinite(v) || v <= 0 || v * 1e3 > 36e5) return reject({
32980
+ if (!Number.isFinite(v) || v <= 0 || v * 1e3 > MAX_DURATION_MS) return reject({
32802
32981
  flag: "--max-duration",
32803
32982
  token: argv[i]
32804
32983
  });
@@ -32857,11 +33036,22 @@ function parseArgs(argv, onError) {
32857
33036
  }
32858
33037
  return args;
32859
33038
  }
32860
- /** Scan flags that exist — the "did you mean" candidate pool. */
33039
+ /**
33040
+ * Scan flags that exist — the "did you mean" candidate pool.
33041
+ *
33042
+ * Every flag `parseArgs` accepts, and nothing it rejects. The pool had
33043
+ * drifted both ways: ten accepted flags were missing, and `--dry-run` and
33044
+ * `--version` were in it although a scan rejects them, so `mjolnir scan
33045
+ * --dry-run` answered `unknown flag "--dry-run"` and then suggested it.
33046
+ * tests/cli/help.spec.ts reads `parseArgs` to hold the two lists together.
33047
+ */
32861
33048
  const KNOWN_SCAN_FLAGS = [
32862
33049
  "--json",
32863
33050
  "--format",
32864
33051
  "--verbose",
33052
+ "--suppressions",
33053
+ "--suppression-gate",
33054
+ "--policy",
32865
33055
  "--scope",
32866
33056
  "--base",
32867
33057
  "--max-duration",
@@ -32876,12 +33066,16 @@ const KNOWN_SCAN_FLAGS = [
32876
33066
  "--record-milestones",
32877
33067
  "--save-baseline",
32878
33068
  "--cache",
33069
+ "--no-progress",
33070
+ "--category",
33071
+ "--score",
33072
+ "--require-full-coverage",
33073
+ "--staged",
33074
+ "--blocking",
33075
+ "--enable-plugins",
32879
33076
  "--monorepo",
32880
33077
  "--help",
32881
- "-h",
32882
- "--version",
32883
- "-v",
32884
- "--dry-run"
33078
+ "-h"
32885
33079
  ];
32886
33080
  /** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
32887
33081
  function levenshtein(a, b) {
@@ -33085,7 +33279,7 @@ function runCiInstall(argv, io = {
33085
33279
  return 10;
33086
33280
  }
33087
33281
  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}.`);
33282
+ io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses ${ACTION_REF} (immutable pin). Gate: ${gate}.`);
33089
33283
  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
33284
  io.out("Change with: mjolnir ci install --gate error|warning|advisory");
33091
33285
  if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
@@ -33138,13 +33332,8 @@ async function main(argv = process$1.argv.slice(2), io = {
33138
33332
  io.out(renderCiSubcommandHelp());
33139
33333
  return 10;
33140
33334
  }
33141
- if (argv[0] === "ci" && argv[1] === "install") return runCiInstall(argv.slice(2), io);
33142
33335
  if (argv[0] === "ci") {
33143
33336
  const sub = argv[1];
33144
- if (sub === void 0 || sub === "--help" || sub === "-h") {
33145
- io.out(renderCiSubcommandHelp());
33146
- return sub === void 0 ? 10 : 0;
33147
- }
33148
33337
  const entry = CI_SUBCOMMANDS[sub];
33149
33338
  if (entry === void 0) {
33150
33339
  io.err(`mjolnir ci: unknown subcommand "${sub}".`);
@@ -33236,7 +33425,6 @@ if (isEntryPoint()) {
33236
33425
  await flushSentry();
33237
33426
  }
33238
33427
  }
33239
- //#endregion
33240
- 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 };
33428
+ export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, KNOWN_SCAN_FLAGS, 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
33429
 
33242
33430
  //# sourceMappingURL=cli.mjs.map