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.
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { createInterface } from "node:readline";
3
3
  import { Buffer as Buffer$1 } from "node:buffer";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
- import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
5
+ import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
6
6
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -12,7 +12,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { Language, Parser } from "web-tree-sitter";
13
13
  import { parse } from "yaml";
14
14
  import { inflateRawSync } from "node:zlib";
15
- //#region src/types.ts
15
+ // src/types.ts
16
16
  /** Severity ladder. Order matters for sorting and gating. */
17
17
  const SEVERITY_ORDER = [
18
18
  "error",
@@ -99,8 +99,7 @@ const DEDUCTIONS = {
99
99
  warning: 3,
100
100
  info: 1
101
101
  };
102
- //#endregion
103
- //#region src/engine/completion.ts
102
+ // src/engine/completion.ts
104
103
  function deriveCompletion(input) {
105
104
  const truncationReasons = [...new Set(input.truncationReasons)].sort();
106
105
  const degradations = [...input.degradations ?? []].sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
@@ -142,8 +141,7 @@ function deriveCompletion(input) {
142
141
  }
143
142
  };
144
143
  }
145
- //#endregion
146
- //#region src/engine/degradation-ledger.ts
144
+ // src/engine/degradation-ledger.ts
147
145
  /**
148
146
  * Append-only. Records are never edited and never removed by a recording
149
147
  * site; a reader takes a window, and the window is defined by index, so
@@ -179,8 +177,7 @@ function summarizeDegradations(window) {
179
177
  count
180
178
  })).sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
181
179
  }
182
- //#endregion
183
- //#region src/engine/runtime-corroboration.ts
180
+ // src/engine/runtime-corroboration.ts
184
181
  /**
185
182
  * Stamp runtime corroboration + trust levels onto findings (mutates in
186
183
  * place, the same contract as stampEvidenceLevels). Returns the number
@@ -196,7 +193,8 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
196
193
  let n = p.replace(/\\/g, "/");
197
194
  if (scanRoot) {
198
195
  const root = scanRoot.replace(/\\/g, "/").replace(/\/?$/, "/");
199
- if (n.startsWith(root)) n = n.slice(root.length);
196
+ const head = n.slice(0, root.length);
197
+ if (/^[a-z]:\//i.test(root) ? head.toLowerCase() === root.toLowerCase() : head === root) n = n.slice(root.length);
200
198
  }
201
199
  return n.replace(/^\.\//, "").replace(/^\/+/, "");
202
200
  };
@@ -262,8 +260,7 @@ function deriveTrustLevel(finding, corroboration) {
262
260
  if (corroboration.level === "test") return "L4";
263
261
  return "L3";
264
262
  }
265
- //#endregion
266
- //#region src/rules/measured-fp.generated.ts
263
+ // src/rules/measured-fp.generated.ts
267
264
  const MEASURED_FP = {
268
265
  "QA-CI-001": {
269
266
  fpRate: .105,
@@ -777,8 +774,7 @@ const MEASURED_FP = {
777
774
  ciHigh: .4343
778
775
  }
779
776
  };
780
- //#endregion
781
- //#region src/engine/trust-summary.ts
777
+ // src/engine/trust-summary.ts
782
778
  /**
783
779
  * Trust level of the summary — the BEST trust level any finding in the
784
780
  * scan reached. Static-only findings settle at L2 (deriveTrustLevel's
@@ -902,8 +898,7 @@ function buildTrustSummary(result, declarationsByFile) {
902
898
  ceilingReasons: reasons
903
899
  };
904
900
  }
905
- //#endregion
906
- //#region src/governance/run-mode.ts
901
+ // src/governance/run-mode.ts
907
902
  /**
908
903
  * Which mode a run is in, and how the other one is reached.
909
904
  *
@@ -954,8 +949,7 @@ function resolveRunMode(input) {
954
949
  if (declared === "local-only" || declared === "hosted") return declared;
955
950
  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.`);
956
951
  }
957
- //#endregion
958
- //#region src/engine/run-identity.ts
952
+ // src/engine/run-identity.ts
959
953
  /**
960
954
  * Run Identity (product-gap master plan §7, plan 1789009691197 R4c) —
961
955
  * the deterministic binding that anchors every trust claim to its
@@ -1084,8 +1078,7 @@ function buildEvidenceGraph(parts) {
1084
1078
  } } : {}
1085
1079
  };
1086
1080
  }
1087
- //#endregion
1088
- //#region src/engine/file-executor.ts
1081
+ // src/engine/file-executor.ts
1089
1082
  function cacheHitOutcome(job) {
1090
1083
  return {
1091
1084
  path: job.path,
@@ -1176,8 +1169,7 @@ function createExecutor(name, concurrency, runOne) {
1176
1169
  execute: runOne
1177
1170
  };
1178
1171
  }
1179
- //#endregion
1180
- //#region src/scope/git-resolve.ts
1172
+ // src/scope/git-resolve.ts
1181
1173
  /**
1182
1174
  * Absolute-path git resolution (audit S1).
1183
1175
  *
@@ -1252,8 +1244,7 @@ function runGit(root, args) {
1252
1244
  return null;
1253
1245
  }
1254
1246
  }
1255
- //#endregion
1256
- //#region src/lib/safe-json.ts
1247
+ // src/lib/safe-json.ts
1257
1248
  /**
1258
1249
  * Reusable safe-JSON parse-and-validate helper.
1259
1250
  *
@@ -1290,8 +1281,7 @@ function isRecord(v) {
1290
1281
  return false;
1291
1282
  }
1292
1283
  }
1293
- //#endregion
1294
- //#region src/engine/candidate-binding.ts
1284
+ // src/engine/candidate-binding.ts
1295
1285
  /**
1296
1286
  * Candidate binding (plan V5-010).
1297
1287
  *
@@ -1424,18 +1414,17 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
1424
1414
  return null;
1425
1415
  }
1426
1416
  }
1427
- //#endregion
1428
- //#region src/engine/version.ts
1417
+ // src/engine/version.ts
1429
1418
  /**
1430
1419
  * Engine version, as a leaf module: `runIdentity` (R4c) needs the engine
1431
1420
  * version inside scan-pipeline WITHOUT importing cli.ts (a cycle — cli
1432
1421
  * imports the pipeline). The literal follows the same discipline as
1433
1422
  * CLI_VERSION and SARIF's driver.version: kept in sync on release by
1434
- * scripts/archive/sync-sarif-version.cjs (archived; superseded by the release
1435
- * workflow's own version stamping) and guarded by the version-consistency
1423
+ * `npm run version:surface:sync` (this file is an identity surface) and
1424
+ * guarded by the version-consistency
1436
1425
  * spec. cli.ts re-exports this as CLI_VERSION.
1437
1426
  */
1438
- const ENGINE_VERSION = "6.0.0";
1427
+ const ENGINE_VERSION = "6.1.0";
1439
1428
  resolveBuildId();
1440
1429
  /**
1441
1430
  * The decision, with the process boundary already behind `runGit`.
@@ -1475,16 +1464,7 @@ function resolveBuildId() {
1475
1464
  };
1476
1465
  return buildIdFrom(runGit);
1477
1466
  }
1478
- //#endregion
1479
- //#region src/engine/contract-versions.ts
1480
- /**
1481
- * Versioned contract registry (VERSION-001).
1482
- *
1483
- * Single source of truth for every version constant the engine exposes
1484
- * to consumers (run identity, evidence graph, machine contract).
1485
- * Constants are `as const` — literal types flow into the identity
1486
- * surface without widening.
1487
- */
1467
+ // src/engine/contract-versions.ts
1488
1468
  const TRUST_MODEL_VERSION = "1.0.0";
1489
1469
  /**
1490
1470
  * 2.0.0 (ADR 0014): the core tier carries an evidence FLOOR. A core rule's
@@ -1496,8 +1476,7 @@ const TRUST_MODEL_VERSION = "1.0.0";
1496
1476
  */
1497
1477
  const SCORING_MODEL_VERSION = "2.0.0";
1498
1478
  const FRAMEWORK_SUPPORT_MATRIX_VERSION = "1.0.0";
1499
- //#endregion
1500
- //#region src/discovery/evidence-discovery.ts
1479
+ // src/discovery/evidence-discovery.ts
1501
1480
  /**
1502
1481
  * Zero-config evidence discovery (Mega MVP Master Plan v3.1 §26 WI-11).
1503
1482
  *
@@ -1589,8 +1568,7 @@ function discoverEvidenceCandidates(scanRoot) {
1589
1568
  }
1590
1569
  return out.sort((a, b) => a.depth - b.depth || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
1591
1570
  }
1592
- //#endregion
1593
- //#region src/discovery/workspace.ts
1571
+ // src/discovery/workspace.ts
1594
1572
  /**
1595
1573
  * Repository discovery (Sprint-Plan W1-03).
1596
1574
  * Finds project root, parses package.json, detects npm/yarn/pnpm workspaces.
@@ -1637,8 +1615,7 @@ function basename$1(p) {
1637
1615
  const parts = p.split(/[\\/]/);
1638
1616
  return parts[parts.length - 1];
1639
1617
  }
1640
- //#endregion
1641
- //#region src/lib/fs-bounded.ts
1618
+ // src/lib/fs-bounded.ts
1642
1619
  function closeQuiet(fd) {
1643
1620
  try {
1644
1621
  closeSync(fd);
@@ -1690,8 +1667,7 @@ function readFileBounded(path, maxBytes) {
1690
1667
  if (fd !== void 0) closeQuiet(fd);
1691
1668
  }
1692
1669
  }
1693
- //#endregion
1694
- //#region src/config/config-schema.ts
1670
+ // src/config/config-schema.ts
1695
1671
  const VALID_GATES = /* @__PURE__ */ new Set([
1696
1672
  "advisory",
1697
1673
  "error",
@@ -1754,15 +1730,18 @@ function validateConfigSchema(config) {
1754
1730
  }
1755
1731
  }
1756
1732
  if (cfg["plugins"] !== void 0) {
1757
- if (cfg["plugins"] === null || typeof cfg["plugins"] !== "object") errors.push("plugins must be an object or array");
1733
+ if (!Array.isArray(cfg["plugins"])) errors.push(`plugins must be an array of package names or { "package": "<name>" } objects, got ${typeof cfg["plugins"]}`);
1734
+ else for (let i = 0; i < cfg["plugins"].length; i++) {
1735
+ const entry = cfg["plugins"][i];
1736
+ 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)}`);
1737
+ }
1758
1738
  }
1759
1739
  return {
1760
1740
  valid: errors.length === 0,
1761
1741
  errors
1762
1742
  };
1763
1743
  }
1764
- //#endregion
1765
- //#region src/config/config.ts
1744
+ // src/config/config.ts
1766
1745
  /**
1767
1746
  * Config file support (Sprint-Plan W7, Product-MVP §27 + GAP-F).
1768
1747
  * Zero-config preserved: config presence never changes detection
@@ -1820,7 +1799,7 @@ function loadConfig(root, options = {}) {
1820
1799
  return {
1821
1800
  config: parsed,
1822
1801
  path: p,
1823
- warnings: validate(parsed, options.knownRuleIds)
1802
+ warnings: validate(parsed, options.knownRuleIds, options.now, name)
1824
1803
  };
1825
1804
  } catch (err) {
1826
1805
  const msg = err.message;
@@ -1833,7 +1812,7 @@ function loadConfig(root, options = {}) {
1833
1812
  warnings: []
1834
1813
  };
1835
1814
  }
1836
- function validate(cfg, knownRuleIds) {
1815
+ function validate(cfg, knownRuleIds, now = /* @__PURE__ */ new Date(), fileName = "mjolnir.config.json") {
1837
1816
  const warnings = [];
1838
1817
  const schemaResult = validateConfigSchema(cfg);
1839
1818
  if (!schemaResult.valid) throw new Error(schemaResult.errors.join("; "));
@@ -1853,10 +1832,10 @@ function validate(cfg, knownRuleIds) {
1853
1832
  if (!Array.isArray(ign.files)) throw new Error(`ignore for ${ign.ruleId}: "files" must be an array of glob strings, got ${typeof ign.files}`);
1854
1833
  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)})`);
1855
1834
  }
1856
- 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}"`);
1857
- 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.`);
1835
+ 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}"`);
1836
+ 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.`);
1858
1837
  }
1859
- 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).`);
1838
+ 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).`);
1860
1839
  if (cfg.exclude !== void 0) {
1861
1840
  if (!Array.isArray(cfg.exclude)) throw new Error(`exclude must be an array of strings, got ${typeof cfg.exclude}`);
1862
1841
  for (const p of cfg.exclude) if (typeof p !== "string") throw new Error(`exclude entries must be strings, got ${typeof p} (${JSON.stringify(p)})`);
@@ -1878,12 +1857,22 @@ function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
1878
1857
  if (!ign.expires) return true;
1879
1858
  return utcMidnight(ign.expires) > utcMidnightOf(now);
1880
1859
  }
1881
- /** UTC-midnight timestamp of an ISO date string (YYYY-MM-DD). */
1860
+ /**
1861
+ * UTC-midnight timestamp of an ISO calendar date (YYYY-MM-DD), or NaN.
1862
+ *
1863
+ * NaN for anything that is not a real calendar day, so a load-time check and
1864
+ * the active/expired verdict share one parser and cannot disagree.
1865
+ */
1882
1866
  function utcMidnight(isoDate) {
1883
- if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return NaN;
1884
1867
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
1885
1868
  if (!m) return NaN;
1886
- return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
1869
+ const year = Number(m[1]);
1870
+ const month = Number(m[2]) - 1;
1871
+ const day = Number(m[3]);
1872
+ const t = Date.UTC(year, month, day);
1873
+ const back = new Date(t);
1874
+ if (back.getUTCFullYear() !== year || back.getUTCMonth() !== month || back.getUTCDate() !== day) return NaN;
1875
+ return t;
1887
1876
  }
1888
1877
  /** UTC-midnight timestamp of a Date (calendar day, timezone-independent). */
1889
1878
  function utcMidnightOf(d) {
@@ -1896,8 +1885,7 @@ function applySeverityOverrides(findings, cfg) {
1896
1885
  if (override && SEVERITY_ORDER.includes(override)) f.severity = override;
1897
1886
  }
1898
1887
  }
1899
- //#endregion
1900
- //#region src/lib/compare.ts
1888
+ // src/lib/compare.ts
1901
1889
  /**
1902
1890
  * Order that does not depend on who is asking.
1903
1891
  *
@@ -1943,8 +1931,7 @@ function compareCodePoints(a, b) {
1943
1931
  function compareLocalized(locale) {
1944
1932
  return (a, b) => a.localeCompare(b, locale);
1945
1933
  }
1946
- //#endregion
1947
- //#region src/lib/glob.ts
1934
+ // src/lib/glob.ts
1948
1935
  /**
1949
1936
  * The shared segment compiler: a glob to a regex BODY, with no anchors.
1950
1937
  *
@@ -1998,8 +1985,7 @@ function anyDepthGlobRegExp(glob) {
1998
1985
  function pathMatchesGlob(path, glob) {
1999
1986
  return anchoredGlobRegExp(glob).test(path.replaceAll("\\", "/"));
2000
1987
  }
2001
- //#endregion
2002
- //#region src/discovery/ignores.ts
1988
+ // src/discovery/ignores.ts
2003
1989
  /**
2004
1990
  * Ignore resolution (Product-MVP.txt R1 — Tier-1 launch blocker).
2005
1991
  *
@@ -2155,8 +2141,7 @@ function isLintFixtureDir(absoluteDir) {
2155
2141
  return false;
2156
2142
  }
2157
2143
  }
2158
- //#endregion
2159
- //#region src/discovery/shared-walk.ts
2144
+ // src/discovery/shared-walk.ts
2160
2145
  /**
2161
2146
  * Shared directory walker (audit P-2).
2162
2147
  *
@@ -2219,8 +2204,7 @@ function sharedWalk(options) {
2219
2204
  };
2220
2205
  walk(options.root);
2221
2206
  }
2222
- //#endregion
2223
- //#region src/discovery/frameworks.ts
2207
+ // src/discovery/frameworks.ts
2224
2208
  /**
2225
2209
  * Framework detection via config resolution (Sprint-Plan W3-01, R8).
2226
2210
  *
@@ -2303,8 +2287,7 @@ function detectFrameworks(ws) {
2303
2287
  unknown: false
2304
2288
  };
2305
2289
  }
2306
- //#endregion
2307
- //#region src/engine/ts-ast.ts
2290
+ // src/engine/ts-ast.ts
2308
2291
  /**
2309
2292
  * AST precision layer (Upgrade-Plan-v3 Phase 3).
2310
2293
  *
@@ -2503,8 +2486,7 @@ function getCodeOnlyText(file) {
2503
2486
  function resetTsMorphProject() {
2504
2487
  looseProject = null;
2505
2488
  }
2506
- //#endregion
2507
- //#region src/engine/code-text.ts
2489
+ // src/engine/code-text.ts
2508
2490
  /**
2509
2491
  * Code-only text masking (Phase 1 — Tempering Plan).
2510
2492
  *
@@ -2864,8 +2846,7 @@ function getCodeTextForCounting(text, language) {
2864
2846
  function blankRange(chars, start, end) {
2865
2847
  for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
2866
2848
  }
2867
- //#endregion
2868
- //#region src/engine/adapter.ts
2849
+ // src/engine/adapter.ts
2869
2850
  /**
2870
2851
  * The §15.1 framework filter, shared by every language adapter: a rule
2871
2852
  * that declares `frameworks` runs on a file only when the file carries
@@ -2879,8 +2860,7 @@ function frameworkFilterApplies(rule, file) {
2879
2860
  if (tags === void 0 || tags.length === 0) return true;
2880
2861
  return rule.frameworks.some((f) => tags.includes(f));
2881
2862
  }
2882
- //#endregion
2883
- //#region src/engine/shared-run-rules.ts
2863
+ // src/engine/shared-run-rules.ts
2884
2864
  /**
2885
2865
  * Run ONE rule with crash isolation, and return what it produced.
2886
2866
  *
@@ -2917,8 +2897,7 @@ function runRuleIsolated(rule, file, onCrash) {
2917
2897
  return null;
2918
2898
  }
2919
2899
  }
2920
- //#endregion
2921
- //#region src/adapters/typescript.ts
2900
+ // src/adapters/typescript.ts
2922
2901
  /**
2923
2902
  * TypeScript/JavaScript adapter (R1).
2924
2903
  * Wraps the proven regex-based engine behind the LanguageAdapter
@@ -3171,8 +3150,7 @@ function loadWorkspaceShim(root) {
3171
3150
  return null;
3172
3151
  }
3173
3152
  }
3174
- //#endregion
3175
- //#region src/engine/tree-sitter-ast.ts
3153
+ // src/engine/tree-sitter-ast.ts
3176
3154
  /**
3177
3155
  * Tree-sitter WASM AST layer for Java and C# — Sprint 8 Task 36
3178
3156
  * (Master-Stabilization-Plan.md).
@@ -3391,8 +3369,7 @@ async function releaseTreeSitterResources() {
3391
3369
  } catch {}
3392
3370
  }
3393
3371
  }
3394
- //#endregion
3395
- //#region src/adapters/python.ts
3372
+ // src/adapters/python.ts
3396
3373
  /**
3397
3374
  * Python/pytest adapter (Upgrade-Plan-v2 R2).
3398
3375
  *
@@ -3550,8 +3527,7 @@ const pythonAdapter = {
3550
3527
  function readText$2(p) {
3551
3528
  return readFileSync(p, "utf8");
3552
3529
  }
3553
- //#endregion
3554
- //#region src/adapters/java.ts
3530
+ // src/adapters/java.ts
3555
3531
  /**
3556
3532
  * Java/JUnit adapter (Upgrade-Plan-v3 Phase 4).
3557
3533
  *
@@ -3619,7 +3595,9 @@ const javaAdapter = {
3619
3595
  const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(block[0])?.[1] ?? "";
3620
3596
  for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (re.test(artifact)) frameworks.add(tag);
3621
3597
  }
3622
- } catch {}
3598
+ } catch {
3599
+ recordDegradation("java-manifest-unreadable");
3600
+ }
3623
3601
  for (const gradle of ["build.gradle", "build.gradle.kts"]) {
3624
3602
  const path = join(root, gradle);
3625
3603
  if (!existsSync(path)) continue;
@@ -3629,7 +3607,9 @@ const javaAdapter = {
3629
3607
  const artifact = m[1].split(":")[1];
3630
3608
  for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (artifact !== void 0 && re.test(artifact)) frameworks.add(tag);
3631
3609
  }
3632
- } catch {}
3610
+ } catch {
3611
+ recordDegradation("java-manifest-unreadable");
3612
+ }
3633
3613
  }
3634
3614
  if (frameworks.size === 0) return {
3635
3615
  frameworks: [],
@@ -3717,8 +3697,7 @@ function javaFileTags(file) {
3717
3697
  function readText$1(path) {
3718
3698
  return readFileSync(path, "utf8");
3719
3699
  }
3720
- //#endregion
3721
- //#region src/adapters/csharp.ts
3700
+ // src/adapters/csharp.ts
3722
3701
  /**
3723
3702
  * C#/.NET adapter (Upgrade-Plan-v3 Phase 5).
3724
3703
  *
@@ -3788,9 +3767,13 @@ const csharpAdapter = {
3788
3767
  const pkg = m[1];
3789
3768
  for (const { re, tag } of CS_PACKAGE_TAGS) if (re.test(pkg)) frameworks.add(tag);
3790
3769
  }
3791
- } catch {}
3770
+ } catch {
3771
+ recordDegradation("csharp-project-unreadable");
3772
+ }
3792
3773
  }
3793
- } catch {}
3774
+ } catch {
3775
+ recordDegradation("csharp-project-unreadable");
3776
+ }
3794
3777
  if (frameworks.size === 0) return {
3795
3778
  frameworks: [],
3796
3779
  unknown: true
@@ -3874,8 +3857,7 @@ function csharpFileTags(file) {
3874
3857
  function readText(path) {
3875
3858
  return readFileSync(path, "utf8");
3876
3859
  }
3877
- //#endregion
3878
- //#region src/discovery/yaml-guards.ts
3860
+ // src/discovery/yaml-guards.ts
3879
3861
  /**
3880
3862
  * Shared hostile-YAML guards (product-gap master plan P3 common law —
3881
3863
  * `azure-pipelines.yml` detection rides the same YAML machinery as the
@@ -3944,8 +3926,7 @@ function enforceDepthCap(value, max, depthMessage = GENERIC_LABELS.depthMessage)
3944
3926
  });
3945
3927
  }
3946
3928
  }
3947
- //#endregion
3948
- //#region src/discovery/workflow-parser.ts
3929
+ // src/discovery/workflow-parser.ts
3949
3930
  /**
3950
3931
  * Safe GitHub Actions workflow parser (Phase 0.3, W4-03 debt).
3951
3932
  *
@@ -3991,8 +3972,7 @@ function parseWorkflow(text) {
3991
3972
  }
3992
3973
  return { jobs };
3993
3974
  }
3994
- //#endregion
3995
- //#region src/adapters/github-actions.ts
3975
+ // src/adapters/github-actions.ts
3996
3976
  /**
3997
3977
  * GitHub Actions adapter (R1).
3998
3978
  * Owns workflow discovery and safe-YAML parsing; CI rules consume the
@@ -4077,8 +4057,7 @@ const githubActionsAdapter = {
4077
4057
  };
4078
4058
  var WorkflowParseSkipped = class extends Error {};
4079
4059
  const LIMITS_MAX_BYTES$2 = 1048576;
4080
- //#endregion
4081
- //#region src/discovery/azure-pipeline-parser.ts
4060
+ // src/discovery/azure-pipeline-parser.ts
4082
4061
  /**
4083
4062
  * Safe Azure DevOps pipeline parser (product-gap master plan P3b).
4084
4063
  *
@@ -4312,8 +4291,7 @@ function isAzurePipelineFixture(normalizedPath) {
4312
4291
  const base = normalizedPath.replaceAll("\\", "/").split("/").pop() ?? "";
4313
4292
  return AZURE_PIPELINE_FILENAMES_SET.has(base);
4314
4293
  }
4315
- //#endregion
4316
- //#region src/adapters/azure-pipelines.ts
4294
+ // src/adapters/azure-pipelines.ts
4317
4295
  /**
4318
4296
  * Azure DevOps pipeline adapter (product-gap master plan P3b).
4319
4297
  * Owns `azure-pipelines.yml` discovery (repo root, the platform's
@@ -4393,8 +4371,7 @@ const azurePipelinesAdapter = {
4393
4371
  };
4394
4372
  var AzureParseSkipped = class extends Error {};
4395
4373
  const LIMITS_MAX_BYTES$1 = 1048576;
4396
- //#endregion
4397
- //#region src/adapters/jenkins.ts
4374
+ // src/adapters/jenkins.ts
4398
4375
  /**
4399
4376
  * Jenkins adapter (product-gap master plan P3c).
4400
4377
  * The root `Jenkinsfile` is a TEXT-target file kind: declarative and
@@ -4468,8 +4445,7 @@ const jenkinsAdapter = {
4468
4445
  }
4469
4446
  };
4470
4447
  const LIMITS_MAX_BYTES = 1048576;
4471
- //#endregion
4472
- //#region src/discovery/scan-adapters.ts
4448
+ // src/discovery/scan-adapters.ts
4473
4449
  /**
4474
4450
  * The canonical adapter registry (audits H-6/H-9).
4475
4451
  *
@@ -4606,8 +4582,7 @@ function isInsideSkippedDir(root, absPath, skipNames) {
4606
4582
  return relative(root, absPath).replaceAll("\\", "/").split("/").slice(0, -1).some((seg) => skipNames.includes(seg));
4607
4583
  }
4608
4584
  typescriptAdapter.testFileGlobs, pythonAdapter.testFileGlobs, javaAdapter.testFileGlobs, csharpAdapter.testFileGlobs, githubActionsAdapter.testFileGlobs, azurePipelinesAdapter.testFileGlobs, jenkinsAdapter.testFileGlobs;
4609
- //#endregion
4610
- //#region src/scope/changed.ts
4585
+ // src/scope/changed.ts
4611
4586
  /**
4612
4587
  * Changed-scope engine (Sprint-Plan W6, Product-MVP §9 `--scope changed`).
4613
4588
  *
@@ -4712,6 +4687,7 @@ function computeChangedScope(root, baseBranch) {
4712
4687
  ...untrackedSet
4713
4688
  ])].sort();
4714
4689
  const changed = {};
4690
+ const fullyChanged = /* @__PURE__ */ new Set();
4715
4691
  const batchDiff = (args, files) => {
4716
4692
  const perFile = /* @__PURE__ */ new Map();
4717
4693
  if (files.length === 0) return perFile;
@@ -4763,7 +4739,8 @@ function computeChangedScope(root, baseBranch) {
4763
4739
  if (untrackedSet.has(file)) {
4764
4740
  const all = allLinesOf(root, file);
4765
4741
  if (all === null) {
4766
- changed[file] = new Set(Array.from({ length: LIMITS_MAX_LINES }, (_, i) => i + 1));
4742
+ fullyChanged.add(file);
4743
+ changed[file] = lines;
4767
4744
  continue;
4768
4745
  }
4769
4746
  for (const l of all) lines.add(l);
@@ -4772,11 +4749,10 @@ function computeChangedScope(root, baseBranch) {
4772
4749
  }
4773
4750
  return {
4774
4751
  changed,
4752
+ ...fullyChanged.size > 0 ? { fullyChanged } : {},
4775
4753
  degraded: false
4776
4754
  };
4777
4755
  }
4778
- /** Sentinel size for "fully changed" — bounded, mirrors LIMITS.maxFileBytes scale. */
4779
- const LIMITS_MAX_LINES = 1e6;
4780
4756
  /** Every 1-based line number of an untracked file (it is all new). */
4781
4757
  function allLinesOf(root, file) {
4782
4758
  const full = join(root, file);
@@ -4826,6 +4802,7 @@ function parseChangedLines(diff) {
4826
4802
  function filterToChanged(findings, diff) {
4827
4803
  if (diff.degraded) return findings;
4828
4804
  return findings.filter((f) => {
4805
+ if (diff.fullyChanged?.has(f.file)) return true;
4829
4806
  const lines = diff.changed[f.file];
4830
4807
  if (!lines) return false;
4831
4808
  for (let l = Math.max(1, f.line - 3); l <= f.line; l++) if (lines.has(l)) return true;
@@ -4850,8 +4827,7 @@ function computeStagedFiles(root) {
4850
4827
  if (raw === null) return null;
4851
4828
  return raw.split("\0").filter((s) => s.length > 0);
4852
4829
  }
4853
- //#endregion
4854
- //#region src/rules/rule.ts
4830
+ // src/rules/rule.ts
4855
4831
  /**
4856
4832
  * Invoke an `astQuery` hook with the mandatory-fallback contract: no
4857
4833
  * hook or no AST on the context resolves to `undefined` (→ regex
@@ -4864,8 +4840,7 @@ function tryAstQuery(hook, ctx) {
4864
4840
  function defineRule(rule) {
4865
4841
  return rule;
4866
4842
  }
4867
- //#endregion
4868
- //#region src/rules/shared/positions.ts
4843
+ // src/rules/shared/positions.ts
4869
4844
  /**
4870
4845
  * Shared position helpers (Phase 6 — Tempering Plan).
4871
4846
  * One lineAt, one colAt, one matchBrace. The other 79 copies are dead.
@@ -4910,8 +4885,7 @@ function lineAt(text, index) {
4910
4885
  function colAt(text, index) {
4911
4886
  return index - (index <= 0 ? -1 : text.lastIndexOf("\n", index - 1));
4912
4887
  }
4913
- //#endregion
4914
- //#region src/rules/test/qa-test-001-focused-test.ts
4888
+ // src/rules/test/qa-test-001-focused-test.ts
4915
4889
  /**
4916
4890
  * QA-TEST-001 — Focused test committed (`fit`/`fdescribe`/`.only`).
4917
4891
  * Severity: error · Confidence: high · deterministic-defect
@@ -4977,8 +4951,7 @@ const focusedTestCommitted = defineRule({
4977
4951
  return findings;
4978
4952
  }
4979
4953
  });
4980
- //#endregion
4981
- //#region src/rules/test/qa-test-002-skipped-test.ts
4954
+ // src/rules/test/qa-test-002-skipped-test.ts
4982
4955
  /**
4983
4956
  * QA-TEST-002 — Skipped test (`it.skip`, `xit`, `test.skip`, `describe.skip`).
4984
4957
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5034,8 +5007,7 @@ const skippedTest = defineRule({
5034
5007
  return findings;
5035
5008
  }
5036
5009
  });
5037
- //#endregion
5038
- //#region src/rules/shared/masking.ts
5010
+ // src/rules/shared/masking.ts
5039
5011
  /** True when the offset was blanked in codeText (inside a string or comment). */
5040
5012
  function isMasked(ctx, index) {
5041
5013
  const mask = ctx.codeText;
@@ -5096,8 +5068,7 @@ function isInsideEmbeddedCode(ctx, index) {
5096
5068
  const hasCallSyntax = /\w\s*\(/.test(inner);
5097
5069
  return hasNestedQuote && hasCallSyntax;
5098
5070
  }
5099
- //#endregion
5100
- //#region src/rules/test/qa-test-003-no-assertions.ts
5071
+ // src/rules/test/qa-test-003-no-assertions.ts
5101
5072
  /**
5102
5073
  * QA-TEST-003 — Test with no assertions.
5103
5074
  * Severity: error · Confidence: high · deterministic-defect
@@ -5175,8 +5146,7 @@ function matchBrace$2(text, open) {
5175
5146
  }
5176
5147
  return -1;
5177
5148
  }
5178
- //#endregion
5179
- //#region src/rules/test/qa-test-004-hard-sleep.ts
5149
+ // src/rules/test/qa-test-004-hard-sleep.ts
5180
5150
  /**
5181
5151
  * QA-TEST-004 — Hard sleep (`sleep()`, `waitForTimeout()`, `setTimeout` as wait).
5182
5152
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5241,8 +5211,7 @@ const hardSleep = defineRule({
5241
5211
  return findings;
5242
5212
  }
5243
5213
  });
5244
- //#endregion
5245
- //#region src/rules/test/qa-test-006-retry-abuse.ts
5214
+ // src/rules/test/qa-test-006-retry-abuse.ts
5246
5215
  /**
5247
5216
  * QA-TEST-006 — Retry abuse in test config.
5248
5217
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5308,8 +5277,7 @@ const retryAbuse = defineRule({
5308
5277
  return findings;
5309
5278
  }
5310
5279
  });
5311
- //#endregion
5312
- //#region src/rules/test/qa-test-010-empty-body.ts
5280
+ // src/rules/test/qa-test-010-empty-body.ts
5313
5281
  /**
5314
5282
  * QA-TEST-010 — Empty test body.
5315
5283
  * Severity: error · Confidence: high · deterministic-defect
@@ -5364,8 +5332,7 @@ const emptyTestBody = defineRule({
5364
5332
  return findings;
5365
5333
  }
5366
5334
  });
5367
- //#endregion
5368
- //#region src/rules/quality/qa-tqual-002-tautological.ts
5335
+ // src/rules/quality/qa-tqual-002-tautological.ts
5369
5336
  /**
5370
5337
  * QA-TQUAL-002 — Tautological assertion.
5371
5338
  * Severity: error · Confidence: high · deterministic-defect
@@ -5442,8 +5409,7 @@ const tautologicalAssertion = defineRule({
5442
5409
  return findings;
5443
5410
  }
5444
5411
  });
5445
- //#endregion
5446
- //#region src/rules/quality/qa-tqual-009-promise-assertion.ts
5412
+ // src/rules/quality/qa-tqual-009-promise-assertion.ts
5447
5413
  /**
5448
5414
  * QA-TQUAL-009 — Unawaited assertion inside a promise chain.
5449
5415
  * Severity: error · Confidence: high · deterministic-defect
@@ -5613,8 +5579,7 @@ function matchParen$2(text, open) {
5613
5579
  }
5614
5580
  return -1;
5615
5581
  }
5616
- //#endregion
5617
- //#region src/rules/quality/qa-tqual-011-commented-out.ts
5582
+ // src/rules/quality/qa-tqual-011-commented-out.ts
5618
5583
  /**
5619
5584
  * QA-TQUAL-011 — Commented-out tests.
5620
5585
  * Severity: warning · Confidence: high · deterministic-defect
@@ -5727,8 +5692,7 @@ function commentRanges(ctx) {
5727
5692
  }
5728
5693
  return ranges;
5729
5694
  }
5730
- //#endregion
5731
- //#region src/rules/playwright/qa-pw-002-unawaited-assertion.ts
5695
+ // src/rules/playwright/qa-pw-002-unawaited-assertion.ts
5732
5696
  /**
5733
5697
  * QA-PW-002 — Missing `await` on a Playwright locator assertion.
5734
5698
  * Severity: error · Confidence: high · deterministic-defect
@@ -5873,8 +5837,7 @@ function runRegexFallback(text, path) {
5873
5837
  });
5874
5838
  return findings;
5875
5839
  }
5876
- //#endregion
5877
- //#region src/rules/playwright/qa-pw-003-debug-artifacts.ts
5840
+ // src/rules/playwright/qa-pw-003-debug-artifacts.ts
5878
5841
  /**
5879
5842
  * QA-PW-003 — Committed `page.pause()` or `test.only()` in e2e specs.
5880
5843
  * Severity: error · Confidence: high · deterministic-defect
@@ -5926,8 +5889,7 @@ const committedDebugArtifacts = defineRule({
5926
5889
  return findings;
5927
5890
  }
5928
5891
  });
5929
- //#endregion
5930
- //#region src/rules/playwright/qa-pw-004-brittle-selectors.ts
5892
+ // src/rules/playwright/qa-pw-004-brittle-selectors.ts
5931
5893
  /**
5932
5894
  * QA-PW-004 — Brittle CSS/XPath selectors vs role-based locators.
5933
5895
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -6019,8 +5981,7 @@ function stripComments(ctx) {
6019
5981
  } catch {}
6020
5982
  return ctx.text.replace(/\/\/[^\n]*/g, (cm) => " ".repeat(cm.length));
6021
5983
  }
6022
- //#endregion
6023
- //#region src/rules/ci/verification-gate.ts
5984
+ // src/rules/ci/verification-gate.ts
6024
5985
  /**
6025
5986
  * Shared "is this shell command a verification gate?" matcher for the QA-CI
6026
5987
  * rules. A gate is a command whose failure MUST fail the job — test runners,
@@ -6060,8 +6021,7 @@ const VERIFICATION_GATE_RE = new RegExp([
6060
6021
  function looksLikeVerificationGate(cmd) {
6061
6022
  return VERIFICATION_GATE_RE.test(cmd);
6062
6023
  }
6063
- //#endregion
6064
- //#region src/rules/ci/azure-gates.ts
6024
+ // src/rules/ci/azure-gates.ts
6065
6025
  /** Discriminator: the Azure parser is the only one emitting `platform`. */
6066
6026
  function isAzurePipelineDoc(ast) {
6067
6027
  return typeof ast === "object" && ast !== null && ast.platform === "azure-pipelines";
@@ -6145,8 +6105,7 @@ function locateAzureJobKey(text, job, keyName) {
6145
6105
  if (m) return lineAt(text, m.index);
6146
6106
  return 1;
6147
6107
  }
6148
- //#endregion
6149
- //#region src/rules/ci/qa-ci-001-continue-on-error.ts
6108
+ // src/rules/ci/qa-ci-001-continue-on-error.ts
6150
6109
  /**
6151
6110
  * QA-CI-001 — `continue-on-error: true` masks a failing job.
6152
6111
  * Severity: error · Confidence: high · deterministic-defect
@@ -6374,8 +6333,7 @@ function describeAzureStep(step) {
6374
6333
  if (step.task) return step.task;
6375
6334
  return "step";
6376
6335
  }
6377
- //#endregion
6378
- //#region src/rules/ci/qa-ci-002-swallowed-exit.ts
6336
+ // src/rules/ci/qa-ci-002-swallowed-exit.ts
6379
6337
  /**
6380
6338
  * QA-CI-002 — `|| true` swallows a command's exit code.
6381
6339
  * Severity: error · Confidence: high · deterministic-defect
@@ -6430,8 +6388,7 @@ const swallowedExitCode = defineRule({
6430
6388
  return findings;
6431
6389
  }
6432
6390
  });
6433
- //#endregion
6434
- //#region src/rules/ci/qa-ci-007-retry-masking.ts
6391
+ // src/rules/ci/qa-ci-007-retry-masking.ts
6435
6392
  /**
6436
6393
  * QA-CI-007 — Retry masking test failures.
6437
6394
  * Severity: warning · Confidence: high · deterministic-defect
@@ -6582,8 +6539,7 @@ function azureArm$1(ctx, doc, findings) {
6582
6539
  });
6583
6540
  }
6584
6541
  }
6585
- //#endregion
6586
- //#region src/rules/ci/qa-ci-005-report-never-generated.ts
6542
+ // src/rules/ci/qa-ci-005-report-never-generated.ts
6587
6543
  /**
6588
6544
  * QA-CI-005 — Report consumed but never generated.
6589
6545
  * Severity: error · Confidence: high · deterministic-defect
@@ -6741,8 +6697,7 @@ function findJobConsumerLine(text, jobName, consumerRe) {
6741
6697
  const anywhere = new RegExp(consumerRe.source, "i").exec(text);
6742
6698
  return anywhere ? lineAt(text, anywhere.index) : 1;
6743
6699
  }
6744
- //#endregion
6745
- //#region src/rules/ci/jenkins-gates.ts
6700
+ // src/rules/ci/jenkins-gates.ts
6746
6701
  /**
6747
6702
  * Jenkinsfile block scanner for the QA-CI Jenkins arms (product-gap
6748
6703
  * master plan P3c). Jenkinsfiles are Groovy text — "no new language
@@ -6988,8 +6943,7 @@ function textIsVerificationGate(text) {
6988
6943
  function lineOfOffset(text, offset) {
6989
6944
  return lineAt(text, offset);
6990
6945
  }
6991
- //#endregion
6992
- //#region src/rules/ci/qa-ci-008-always-success.ts
6946
+ // src/rules/ci/qa-ci-008-always-success.ts
6993
6947
  /**
6994
6948
  * QA-CI-008 — Always-success step at the end of a job.
6995
6949
  * Severity: error · Confidence: high · deterministic-defect
@@ -7163,8 +7117,7 @@ function jenkinsArm$1(ctx, findings) {
7163
7117
  });
7164
7118
  }
7165
7119
  }
7166
- //#endregion
7167
- //#region src/rules/ci/qa-ci-009-exit-code.ts
7120
+ // src/rules/ci/qa-ci-009-exit-code.ts
7168
7121
  /**
7169
7122
  * QA-CI-009 — Test command does not propagate exit code.
7170
7123
  * Severity: error · Confidence: high · deterministic-defect
@@ -7299,8 +7252,7 @@ function jenkinsArm(ctx, findings) {
7299
7252
  });
7300
7253
  }
7301
7254
  }
7302
- //#endregion
7303
- //#region src/rules/ci/qa-ci-010-non-blocking.ts
7255
+ // src/rules/ci/qa-ci-010-non-blocking.ts
7304
7256
  /**
7305
7257
  * QA-CI-010 — Required test job is non-blocking for branch protection.
7306
7258
  * Severity: error · Confidence: medium · deterministic-defect
@@ -7383,8 +7335,7 @@ function findLine(text, re) {
7383
7335
  function escapeRe(s) {
7384
7336
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7385
7337
  }
7386
- //#endregion
7387
- //#region src/rules/ci/qa-ci-013-can-never-fail.ts
7338
+ // src/rules/ci/qa-ci-013-can-never-fail.ts
7388
7339
  /**
7389
7340
  * QA-CI-013 — Verification stage/job conditioned so it can never fail the
7390
7341
  * pipeline (product-gap master plan P3b).
@@ -7488,8 +7439,7 @@ const canNeverFailGate = defineRule({
7488
7439
  function jobHasGate(job) {
7489
7440
  return (job.steps ?? []).some((s) => s !== void 0 && stepIsVerificationGate$1(s));
7490
7441
  }
7491
- //#endregion
7492
- //#region src/rules/ci/qa-ci-014-swallowed-verification.ts
7442
+ // src/rules/ci/qa-ci-014-swallowed-verification.ts
7493
7443
  /**
7494
7444
  * QA-CI-014 — try/catch swallowing a verification-stage failure without
7495
7445
  * rethrow or failure-marking (product-gap master plan P3c).
@@ -7546,8 +7496,7 @@ const swallowedVerificationFailure = defineRule({
7546
7496
  return findings;
7547
7497
  }
7548
7498
  });
7549
- //#endregion
7550
- //#region src/rules/python/qa-py-003-no-assertions.ts
7499
+ // src/rules/python/qa-py-003-no-assertions.ts
7551
7500
  /**
7552
7501
  * QA-PY-003 — Test function with no assertions.
7553
7502
  * Severity: error · Confidence: high · deterministic-defect
@@ -7668,8 +7617,7 @@ function extractBlock$1(text, afterColon) {
7668
7617
  const lineEnd = rest.indexOf("\n", firstIdx);
7669
7618
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
7670
7619
  }
7671
- //#endregion
7672
- //#region src/rules/python/qa-py-005-hard-sleep.ts
7620
+ // src/rules/python/qa-py-005-hard-sleep.ts
7673
7621
  /**
7674
7622
  * QA-PY-005 — time.sleep() in tests.
7675
7623
  * Severity: warning · Confidence: high · deterministic-defect
@@ -7716,8 +7664,7 @@ const pyHardSleep = defineRule({
7716
7664
  return findings;
7717
7665
  }
7718
7666
  });
7719
- //#endregion
7720
- //#region src/rules/python/qa-py-002-skipped-test.ts
7667
+ // src/rules/python/qa-py-002-skipped-test.ts
7721
7668
  /**
7722
7669
  * QA-PY-002 — Skipped test (`@pytest.mark.skip`, `xfail` non-strict).
7723
7670
  * Severity: warning · Confidence: high · deterministic-defect
@@ -7777,8 +7724,7 @@ const pySkippedTest = defineRule({
7777
7724
  return findings;
7778
7725
  }
7779
7726
  });
7780
- //#endregion
7781
- //#region src/rules/python/qa-py-012-tautological.ts
7727
+ // src/rules/python/qa-py-012-tautological.ts
7782
7728
  /**
7783
7729
  * QA-PY-012 — Tautological assertion (`assert True`, `assert x == x`).
7784
7730
  * Severity: error · Confidence: high · deterministic-defect
@@ -7833,8 +7779,7 @@ const pyTautological = defineRule({
7833
7779
  return findings;
7834
7780
  }
7835
7781
  });
7836
- //#endregion
7837
- //#region src/rules/python/qa-py-001-focused-test.ts
7782
+ // src/rules/python/qa-py-001-focused-test.ts
7838
7783
  /**
7839
7784
  * QA-PY-001 — Focused test committed.
7840
7785
  * Severity: error · Confidence: high · deterministic-defect
@@ -7888,8 +7833,7 @@ const pyFocusedTest = defineRule({
7888
7833
  return findings;
7889
7834
  }
7890
7835
  });
7891
- //#endregion
7892
- //#region src/engine/jv-cs-ast.ts
7836
+ // src/engine/jv-cs-ast.ts
7893
7837
  /** Narrow the loose `ast` seam back to a tree-sitter Tree. */
7894
7838
  function getTreeSitterTree(ast) {
7895
7839
  if (!(ast instanceof Object) || !("rootNode" in ast)) return void 0;
@@ -8093,8 +8037,7 @@ function isHelperIdiom(name) {
8093
8037
  }
8094
8038
  return false;
8095
8039
  }
8096
- //#endregion
8097
- //#region src/engine/python-ast.ts
8040
+ // src/engine/python-ast.ts
8098
8041
  function getPythonTree(ast) {
8099
8042
  return getTreeSitterTree(ast);
8100
8043
  }
@@ -8179,8 +8122,7 @@ function pythonWithRaisesBlocks(tree) {
8179
8122
  });
8180
8123
  return out;
8181
8124
  }
8182
- //#endregion
8183
- //#region src/rules/python/qa-py-007-raises-without-match.ts
8125
+ // src/rules/python/qa-py-007-raises-without-match.ts
8184
8126
  /**
8185
8127
  * QA-PY-007 — pytest.raises without match.
8186
8128
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8307,8 +8249,7 @@ function astArm(ctx, tree) {
8307
8249
  }
8308
8250
  return findings;
8309
8251
  }
8310
- //#endregion
8311
- //#region src/rules/python/qa-py-009-commented-out.ts
8252
+ // src/rules/python/qa-py-009-commented-out.ts
8312
8253
  /**
8313
8254
  * QA-PY-009 — Commented-out tests.
8314
8255
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8355,8 +8296,7 @@ const pyCommentedOutTest = defineRule({
8355
8296
  return findings;
8356
8297
  }
8357
8298
  });
8358
- //#endregion
8359
- //#region src/rules/python/qa-py-004-bare-truthiness.ts
8299
+ // src/rules/python/qa-py-004-bare-truthiness.ts
8360
8300
  /**
8361
8301
  * QA-PY-004 — Bare truthiness assert on complex object.
8362
8302
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8435,8 +8375,7 @@ const pyBareTruthinessAssert = defineRule({
8435
8375
  return findings;
8436
8376
  }
8437
8377
  });
8438
- //#endregion
8439
- //#region src/rules/python/qa-py-011-mutable-fixture.ts
8378
+ // src/rules/python/qa-py-011-mutable-fixture.ts
8440
8379
  /**
8441
8380
  * QA-PY-011 — Mutable fixture shared across tests.
8442
8381
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8487,8 +8426,7 @@ const pyMutableFixture = defineRule({
8487
8426
  return findings;
8488
8427
  }
8489
8428
  });
8490
- //#endregion
8491
- //#region src/rules/playwright/qa-pw-101-wait-for-timeout.ts
8429
+ // src/rules/playwright/qa-pw-101-wait-for-timeout.ts
8492
8430
  /**
8493
8431
  * QA-PW-101 — waitForTimeout() anywhere.
8494
8432
  * Severity: error · Confidence: high · deterministic-defect
@@ -8535,8 +8473,7 @@ const pwWaitForTimeout = defineRule({
8535
8473
  return findings;
8536
8474
  }
8537
8475
  });
8538
- //#endregion
8539
- //#region src/rules/playwright/qa-pw-102-wait-load.ts
8476
+ // src/rules/playwright/qa-pw-102-wait-load.ts
8540
8477
  /**
8541
8478
  * QA-PW-102 — waitForEvent('load') / waitForLoadState('load') instead of
8542
8479
  * web-first assertions.
@@ -8590,8 +8527,7 @@ const pwWaitForLoadEvent = defineRule({
8590
8527
  return findings;
8591
8528
  }
8592
8529
  });
8593
- //#endregion
8594
- //#region src/rules/playwright/qa-pw-113-deep-frames.ts
8530
+ // src/rules/playwright/qa-pw-113-deep-frames.ts
8595
8531
  /**
8596
8532
  * QA-PW-113 — frameLocator chains > 2 deep.
8597
8533
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8641,8 +8577,7 @@ const pwDeepFrameLocator = defineRule({
8641
8577
  function count(fragment) {
8642
8578
  return fragment.match(/frameLocator\s*\(/g).length;
8643
8579
  }
8644
- //#endregion
8645
- //#region src/rules/playwright/qa-pw-117-serial.ts
8580
+ // src/rules/playwright/qa-pw-117-serial.ts
8646
8581
  /**
8647
8582
  * QA-PW-117 — test.describe.serial without justification comment.
8648
8583
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8696,8 +8631,7 @@ const pwSerialNoJustification = defineRule({
8696
8631
  return findings;
8697
8632
  }
8698
8633
  });
8699
- //#endregion
8700
- //#region src/rules/playwright/qa-pw-121-config-retries.ts
8634
+ // src/rules/playwright/qa-pw-121-config-retries.ts
8701
8635
  /**
8702
8636
  * QA-PW-121 — playwright.config: retries >= 3 or workers = 0.
8703
8637
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8762,8 +8696,7 @@ const pwConfigRetryAbuse = defineRule({
8762
8696
  return findings;
8763
8697
  }
8764
8698
  });
8765
- //#endregion
8766
- //#region src/rules/playwright/qa-pw-122-no-trace.ts
8699
+ // src/rules/playwright/qa-pw-122-no-trace.ts
8767
8700
  /**
8768
8701
  * QA-PW-122 — No trace capture on retry in playwright.config.
8769
8702
  * Severity: warning · Confidence: high · deterministic-defect
@@ -8812,8 +8745,7 @@ const pwNoTraceOnRetry = defineRule({
8812
8745
  return findings;
8813
8746
  }
8814
8747
  });
8815
- //#endregion
8816
- //#region src/rules/playwright/qa-pw-124-project-split.ts
8748
+ // src/rules/playwright/qa-pw-124-project-split.ts
8817
8749
  /**
8818
8750
  * QA-PW-124 — Projects defined but no smoke/regression split.
8819
8751
  * Severity: info · Confidence: high · deterministic-defect
@@ -8875,8 +8807,7 @@ const pwNoProjectSplit = defineRule({
8875
8807
  return findings;
8876
8808
  }
8877
8809
  });
8878
- //#endregion
8879
- //#region src/rules/playwright/qa-pw-104-trial-click.ts
8810
+ // src/rules/playwright/qa-pw-104-trial-click.ts
8880
8811
  /**
8881
8812
  * QA-PW-104 — `{ trial: true }` misuse — check-then-act race.
8882
8813
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8922,8 +8853,7 @@ const pwTrialMisuse = defineRule({
8922
8853
  return findings;
8923
8854
  }
8924
8855
  });
8925
- //#endregion
8926
- //#region src/rules/playwright/qa-pw-116-storage-state.ts
8856
+ // src/rules/playwright/qa-pw-116-storage-state.ts
8927
8857
  /**
8928
8858
  * QA-PW-116 — storageState reuse without expiry strategy.
8929
8859
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -8969,8 +8899,7 @@ const pwStorageStateNoExpiry = defineRule({
8969
8899
  return findings;
8970
8900
  }
8971
8901
  });
8972
- //#endregion
8973
- //#region src/rules/playwright/qa-pw-125-global-setup.ts
8902
+ // src/rules/playwright/qa-pw-125-global-setup.ts
8974
8903
  /**
8975
8904
  * QA-PW-125 — globalSetup mutating shared external state.
8976
8905
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9028,8 +8957,7 @@ const pwGlobalSetupSharedState = defineRule({
9028
8957
  return findings;
9029
8958
  }
9030
8959
  });
9031
- //#endregion
9032
- //#region src/rules/playwright/qa-pw-115-shared-page.ts
8960
+ // src/rules/playwright/qa-pw-115-shared-page.ts
9033
8961
  /**
9034
8962
  * QA-PW-115 — Shared `page` across parallel tests without isolation.
9035
8963
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9077,8 +9005,7 @@ const pwSharedPage = defineRule({
9077
9005
  return findings;
9078
9006
  }
9079
9007
  });
9080
- //#endregion
9081
- //#region src/rules/playwright/qa-pw-140.ts
9008
+ // src/rules/playwright/qa-pw-140.ts
9082
9009
  /**
9083
9010
  * QA-PW-140 — Screenshot without maxDiffPixelRatio.
9084
9011
  *
@@ -9152,8 +9079,7 @@ function matchParen(text, open) {
9152
9079
  }
9153
9080
  return -1;
9154
9081
  }
9155
- //#endregion
9156
- //#region src/rules/playwright/qa-pw-123-hardcoded-url.ts
9082
+ // src/rules/playwright/qa-pw-123-hardcoded-url.ts
9157
9083
  /**
9158
9084
  * QA-PW-123 — Hardcoded baseURL in specs.
9159
9085
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9201,8 +9127,7 @@ const hardcodedBaseUrl = defineRule({
9201
9127
  return findings;
9202
9128
  }
9203
9129
  });
9204
- //#endregion
9205
- //#region src/rules/quality/qa-env-001-env-coupling.ts
9130
+ // src/rules/quality/qa-env-001-env-coupling.ts
9206
9131
  /**
9207
9132
  * QA-ENV-001 — "Works On My Machine" detector core (Tier 5 #24).
9208
9133
  * Environment coupling in test files: fixed ports, OS paths,
@@ -9269,8 +9194,7 @@ const envCoupling = defineRule({
9269
9194
  return findings;
9270
9195
  }
9271
9196
  });
9272
- //#endregion
9273
- //#region src/rules/playwright/qa-pw-141-retry-no-triage.ts
9197
+ // src/rules/playwright/qa-pw-141-retry-no-triage.ts
9274
9198
  /**
9275
9199
  * QA-PW-141 — playwright.config retries set high enough to mask flakes.
9276
9200
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9327,8 +9251,7 @@ const pwRetryMaskingNoForensics = defineRule({
9327
9251
  return findings;
9328
9252
  }
9329
9253
  });
9330
- //#endregion
9331
- //#region src/rules/playwright/qa-pw-142-blanket-route.ts
9254
+ // src/rules/playwright/qa-pw-142-blanket-route.ts
9332
9255
  /**
9333
9256
  * QA-PW-142 — page.route() blanket-mocking instead of scoped intercepts.
9334
9257
  * Severity: warning · Confidence: medium · heuristic-risk
@@ -9383,8 +9306,7 @@ const pwBlanketRouteMock = defineRule({
9383
9306
  return findings;
9384
9307
  }
9385
9308
  });
9386
- //#endregion
9387
- //#region src/rules/playwright/qa-pw-143-no-artifacts.ts
9309
+ // src/rules/playwright/qa-pw-143-no-artifacts.ts
9388
9310
  /**
9389
9311
  * QA-PW-143 — No screenshot/video artifact capture on failure.
9390
9312
  * Severity: info · Confidence: high · deterministic-defect
@@ -9437,8 +9359,7 @@ const pwNoFailureArtifacts = defineRule({
9437
9359
  return findings;
9438
9360
  }
9439
9361
  });
9440
- //#endregion
9441
- //#region src/rules/playwright/qa-pw-144-single-browser.ts
9362
+ // src/rules/playwright/qa-pw-144-single-browser.ts
9442
9363
  /**
9443
9364
  * QA-PW-144 — Single-browser project matrix (no cross-browser coverage).
9444
9365
  * Severity: info · Confidence: high · deterministic-defect
@@ -9500,8 +9421,7 @@ const pwSingleBrowserMatrix = defineRule({
9500
9421
  return findings;
9501
9422
  }
9502
9423
  });
9503
- //#endregion
9504
- //#region src/rules/playwright/qa-pw-146-css-locator.ts
9424
+ // src/rules/playwright/qa-pw-146-css-locator.ts
9505
9425
  /**
9506
9426
  * QA-PW-146 — CSS/XPath string selector where Playwright's locator
9507
9427
  * standard expects a normalized locator (plan §17.3).
@@ -9580,8 +9500,7 @@ const pwLocatorNormalize = defineRule({
9580
9500
  return findings;
9581
9501
  }
9582
9502
  });
9583
- //#endregion
9584
- //#region src/rules/playwright/qa-pw-147-codegen-artifact.ts
9503
+ // src/rules/playwright/qa-pw-147-codegen-artifact.ts
9585
9504
  /**
9586
9505
  * QA-PW-147 — Playwright codegen artifact left unedited (plan §17.1).
9587
9506
  * Severity: info · Confidence: medium · observation
@@ -9640,8 +9559,7 @@ const pwCodegenArtifact = defineRule({
9640
9559
  return findings;
9641
9560
  }
9642
9561
  });
9643
- //#endregion
9644
- //#region src/rules/python/qa-py-101-sync-async-mix.ts
9562
+ // src/rules/python/qa-py-101-sync-async-mix.ts
9645
9563
  /**
9646
9564
  * QA-PY-101 — Playwright-Python: sync API in async test (or vice versa).
9647
9565
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9690,8 +9608,7 @@ const pyPwSyncAsyncMix = defineRule({
9690
9608
  return findings;
9691
9609
  }
9692
9610
  });
9693
- //#endregion
9694
- //#region src/rules/python/qa-py-103-wait-for-timeout.ts
9611
+ // src/rules/python/qa-py-103-wait-for-timeout.ts
9695
9612
  /**
9696
9613
  * QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
9697
9614
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9738,8 +9655,7 @@ const pyPwWaitForTimeout = defineRule({
9738
9655
  return findings;
9739
9656
  }
9740
9657
  });
9741
- //#endregion
9742
- //#region src/rules/python/qa-py-105-pw-no-assertions.ts
9658
+ // src/rules/python/qa-py-105-pw-no-assertions.ts
9743
9659
  /**
9744
9660
  * QA-PY-105 — Playwright-Python: no assertion in Playwright test.
9745
9661
  * Severity: error · Confidence: high · deterministic-defect
@@ -9819,8 +9735,7 @@ function extractBlock(text, afterColon) {
9819
9735
  const lineEnd = rest.indexOf("\n", firstContent.index);
9820
9736
  return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
9821
9737
  }
9822
- //#endregion
9823
- //#region src/rules/java/qa-jv-101-disabled-test.ts
9738
+ // src/rules/java/qa-jv-101-disabled-test.ts
9824
9739
  /**
9825
9740
  * QA-JV-101 — @Disabled test without justification.
9826
9741
  * Severity: warning · Confidence: high · deterministic-defect
@@ -9874,8 +9789,7 @@ const jvDisabledTest = defineRule({
9874
9789
  return findings;
9875
9790
  }
9876
9791
  });
9877
- //#endregion
9878
- //#region src/rules/java/qa-jv-103-no-assertions.ts
9792
+ // src/rules/java/qa-jv-103-no-assertions.ts
9879
9793
  /**
9880
9794
  * QA-JV-103 — Test method without assertions.
9881
9795
  * Severity: error · Confidence: high · deterministic-defect
@@ -10032,8 +9946,7 @@ function matchBrace$1(text, open) {
10032
9946
  }
10033
9947
  return -1;
10034
9948
  }
10035
- //#endregion
10036
- //#region src/engine/qa-model.ts
9949
+ // src/engine/qa-model.ts
10037
9950
  /**
10038
9951
  * Extract the semantic model for one parsed file. Resolves undefined
10039
9952
  * when the language has no extractor or the parse stage produced no
@@ -10576,8 +10489,7 @@ function extractPythonModel(file) {
10576
10489
  nodes
10577
10490
  };
10578
10491
  }
10579
- //#endregion
10580
- //#region src/rules/java/qa-jv-105-wait-for-timeout.ts
10492
+ // src/rules/java/qa-jv-105-wait-for-timeout.ts
10581
10493
  /**
10582
10494
  * QA-JV-105 — waitForTimeout / hardcoded navigation waits.
10583
10495
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10662,8 +10574,7 @@ const jvWaitForTimeout = defineRule({
10662
10574
  return findings;
10663
10575
  }
10664
10576
  });
10665
- //#endregion
10666
- //#region src/rules/csharp/qa-cs-101-skipped-test.ts
10577
+ // src/rules/csharp/qa-cs-101-skipped-test.ts
10667
10578
  /**
10668
10579
  * QA-CS-101 — [Ignore]/[Skip] test without justification.
10669
10580
  * Severity: warning · Confidence: high · deterministic-defect
@@ -10727,8 +10638,7 @@ const csSkippedTest = defineRule({
10727
10638
  return findings;
10728
10639
  }
10729
10640
  });
10730
- //#endregion
10731
- //#region src/rules/csharp/qa-cs-103-no-assertions.ts
10641
+ // src/rules/csharp/qa-cs-103-no-assertions.ts
10732
10642
  /**
10733
10643
  * QA-CS-103 — Test method without assertions.
10734
10644
  * Severity: error · Confidence: high · deterministic-defect
@@ -10762,7 +10672,6 @@ const csSkippedTest = defineRule({
10762
10672
  * Deep-diffing those requires symbol semantics Mjölnir deliberately does
10763
10673
  * not promise for C# (plan §13.4).
10764
10674
  */
10765
- /** Helper/verification idiom: verify/check/assert-prefixed names. */
10766
10675
  const WHY$5 = "Without an assertion the test can only fail by throwing — it cannot detect behavioral regressions.";
10767
10676
  const FIX$4 = "Add an assertion on the expected outcome, or remove the test.";
10768
10677
  function makeFinding(path, position, name) {
@@ -10926,8 +10835,7 @@ function matchBrace(text, open) {
10926
10835
  }
10927
10836
  return -1;
10928
10837
  }
10929
- //#endregion
10930
- //#region src/rules/csharp/qa-cs-105-wait-for-timeout.ts
10838
+ // src/rules/csharp/qa-cs-105-wait-for-timeout.ts
10931
10839
  /**
10932
10840
  * QA-CS-105 — Playwright-.NET: WaitForTimeoutAsync hard sleep.
10933
10841
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11015,8 +10923,7 @@ const csWaitForTimeout = defineRule({
11015
10923
  return findings;
11016
10924
  }
11017
10925
  });
11018
- //#endregion
11019
- //#region src/rules/cypress/qa-cyp-001-cy-wait.ts
10926
+ // src/rules/cypress/qa-cyp-001-cy-wait.ts
11020
10927
  /**
11021
10928
  * QA-CYP-001 — Fixed `cy.wait(n)` (hard-coded wait).
11022
10929
  * Severity: warning · Confidence: high · deterministic-defect
@@ -11095,8 +11002,7 @@ function isCypressFile(ctx) {
11095
11002
  if (/\.cy\.[jt]sx?$/.test(ctx.path)) return true;
11096
11003
  return /\bcy\s*\./.test(ctx.codeText ?? ctx.text);
11097
11004
  }
11098
- //#endregion
11099
- //#region src/rules/cypress/qa-cyp-002-focused-test.ts
11005
+ // src/rules/cypress/qa-cyp-002-focused-test.ts
11100
11006
  /**
11101
11007
  * QA-CYP-002 — Focused test (`.only`).
11102
11008
  * Severity: error · Confidence: high · deterministic-defect
@@ -11159,8 +11065,7 @@ const cypFocusedTest = defineRule({
11159
11065
  return findings;
11160
11066
  }
11161
11067
  });
11162
- //#endregion
11163
- //#region src/rules/cypress/qa-cyp-003-config-security.ts
11068
+ // src/rules/cypress/qa-cyp-003-config-security.ts
11164
11069
  /**
11165
11070
  * QA-CYP-003 — Cypress config disables `chromeWebSecurity`.
11166
11071
  * Severity: error · Confidence: high · deterministic-defect
@@ -11230,8 +11135,7 @@ const cypConfigSecurity = defineRule({
11230
11135
  return findings;
11231
11136
  }
11232
11137
  });
11233
- //#endregion
11234
- //#region src/rules/selenium/qa-se-sleep-lookup.ts
11138
+ // src/rules/selenium/qa-se-sleep-lookup.ts
11235
11139
  /**
11236
11140
  * Selenium sleep-then-interact rules (Verification Trust Evolution Plan
11237
11141
  * §15.3 — "Selenium implicit-wait abuse / missing WebDriverWait",
@@ -11382,8 +11286,7 @@ const sePythonSleepLookup = defineRule({
11382
11286
  return sleepBeforeLookup(ctx, PY_SLEEP_RE, PY_LOOKUP_RE, "QA-SE-003");
11383
11287
  }
11384
11288
  });
11385
- //#endregion
11386
- //#region src/rules/shared/family.ts
11289
+ // src/rules/shared/family.ts
11387
11290
  /**
11388
11291
  * Cross-language pattern rule family generator (Phase 6 — Tempering Plan).
11389
11292
  *
@@ -11449,8 +11352,7 @@ function definePatternFamily(opts) {
11449
11352
  }
11450
11353
  }));
11451
11354
  }
11452
- //#endregion
11453
- //#region src/rules/families/hard-sleep.ts
11355
+ // src/rules/families/hard-sleep.ts
11454
11356
  /**
11455
11357
  * Hard-sleep family (Phase 6 — Tempering Plan).
11456
11358
  * Java: Thread.sleep(), C#: Thread.Sleep() / Task.Delay()
@@ -11601,8 +11503,7 @@ const hardSleepFamily = definePatternFamily({
11601
11503
  fix: "Use `await Assertions.Expect(locator).ToBeVisibleAsync()` or locator.WaitForAsync()."
11602
11504
  }]
11603
11505
  });
11604
- //#endregion
11605
- //#region src/rules/families/network-idle.ts
11506
+ // src/rules/families/network-idle.ts
11606
11507
  /**
11607
11508
  * Network-idle family (Phase 6 — Tempering Plan).
11608
11509
  * All three language variants of waitForLoadState(networkidle).
@@ -11663,8 +11564,7 @@ const networkIdleFamily = definePatternFamily({
11663
11564
  }
11664
11565
  ]
11665
11566
  });
11666
- //#endregion
11667
- //#region src/rules/families/hardcoded-url.ts
11567
+ // src/rules/families/hardcoded-url.ts
11668
11568
  /**
11669
11569
  * Hardcoded-URL family (Phase 6 — Tempering Plan).
11670
11570
  * Detects absolute URLs in navigation/request calls across languages.
@@ -11714,8 +11614,7 @@ const hardcodedUrlFamily = definePatternFamily({
11714
11614
  fix: "Use `--base-url` / `base_url` fixture, or `os.environ[...]`."
11715
11615
  }]
11716
11616
  });
11717
- //#endregion
11718
- //#region src/rules/families/shared-page.ts
11617
+ // src/rules/families/shared-page.ts
11719
11618
  /**
11720
11619
  * Shared-page family (Phase 6 — Tempering Plan).
11721
11620
  * Detects static/module-level Page/Browser instances shared across tests.
@@ -11776,8 +11675,7 @@ const sharedPageFamily = definePatternFamily({
11776
11675
  }
11777
11676
  ]
11778
11677
  });
11779
- //#endregion
11780
- //#region src/rules/families/brittle-selectors.ts
11678
+ // src/rules/families/brittle-selectors.ts
11781
11679
  /**
11782
11680
  * Brittle-selectors family (Phase 6 — Tempering Plan).
11783
11681
  * Detects XPath/structural CSS selectors across Java, C#, Python.
@@ -11889,8 +11787,7 @@ const brittleSelectorsFamily = [
11889
11787
  }
11890
11788
  ], "Prefer role-based locators (`get_by_role(...)`) or data-testid attributes.")
11891
11789
  ];
11892
- //#endregion
11893
- //#region src/rules/families/retry-masking.ts
11790
+ // src/rules/families/retry-masking.ts
11894
11791
  /**
11895
11792
  * Retry-masking family (Phase 6 — Tempering Plan).
11896
11793
  * Detects retry annotations/attributes that mask flaky tests.
@@ -12010,8 +11907,7 @@ const retryMaskingFamily = [defineRule({
12010
11907
  return findings;
12011
11908
  }
12012
11909
  })];
12013
- //#endregion
12014
- //#region src/rules/index.ts
11910
+ // src/rules/index.ts
12015
11911
  const RULES = [
12016
11912
  focusedTestCommitted,
12017
11913
  skippedTest,
@@ -12092,8 +11988,7 @@ const RULES = [
12092
11988
  function getRule(id) {
12093
11989
  return RULES.find((r) => r.id === id);
12094
11990
  }
12095
- //#endregion
12096
- //#region src/lib/wilson.ts
11991
+ // src/lib/wilson.ts
12097
11992
  /**
12098
11993
  * 95% Wilson score interval on a proportion (plan §20.2).
12099
11994
  *
@@ -12203,8 +12098,7 @@ function effectiveTier(rule) {
12203
12098
  if (rule.tier !== void 0) return rule.tier;
12204
12099
  return measurementTier(rule);
12205
12100
  }
12206
- //#endregion
12207
- //#region src/scorer/scorer.ts
12101
+ // src/scorer/scorer.ts
12208
12102
  /**
12209
12103
  * Transparent scorer (Product-MVP §8).
12210
12104
  * score = 100 − Σ(severity deductions), floor 0.
@@ -12356,9 +12250,9 @@ function countTestDeclarations(text, codeText) {
12356
12250
  const source = codeText ?? text;
12357
12251
  const patterns = [
12358
12252
  /\b(?:it|test)(?:\.\w+)*\s*(?:\(|`)/g,
12359
- /^\s*(?:async\s+)?def\s+test_\w*/gm,
12360
- /^\s*@Test\b/gm,
12361
- /^\s*\[(?:Test|Fact|Theory|TestMethod)\b/gm
12253
+ /^[ \t]*(?:async[ \t]+)?def[ \t]+test_\w*/gm,
12254
+ /^[ \t]*@Test\b/gm,
12255
+ /^[ \t]*\[(?:Test|Fact|Theory|TestMethod)\b/gm
12362
12256
  ];
12363
12257
  let count = 0;
12364
12258
  for (const re of patterns) {
@@ -12367,8 +12261,7 @@ function countTestDeclarations(text, codeText) {
12367
12261
  }
12368
12262
  return count;
12369
12263
  }
12370
- //#endregion
12371
- //#region src/engine/rule-runner.ts
12264
+ // src/engine/rule-runner.ts
12372
12265
  /**
12373
12266
  * Legacy appliesTo → adapter id mapping.
12374
12267
  *
@@ -12407,8 +12300,7 @@ function asUniversal(rule) {
12407
12300
  }
12408
12301
  };
12409
12302
  }
12410
- //#endregion
12411
- //#region src/engine/tier-policy.ts
12303
+ // src/engine/tier-policy.ts
12412
12304
  /**
12413
12305
  * Tier policy (audit H-1): the declared tier is authoritative over a
12414
12306
  * rule's severity and evidence claims.
@@ -12510,8 +12402,7 @@ function enforceTierPolicy(findings, tierByRuleId) {
12510
12402
  if (EVIDENCE_RANK$1[effective] < EVIDENCE_RANK$1[floor.evidenceLevel]) f.evidenceLevel = floor.evidenceLevel;
12511
12403
  }
12512
12404
  }
12513
- //#endregion
12514
- //#region src/engine/evidence-core.ts
12405
+ // src/engine/evidence-core.ts
12515
12406
  /**
12516
12407
  * The core's own version — stamped into every record's provenance so a
12517
12408
  * consumer can tell which normalization contract produced a record.
@@ -12578,8 +12469,7 @@ function countEvidence(records) {
12578
12469
  }
12579
12470
  return counts;
12580
12471
  }
12581
- //#endregion
12582
- //#region src/engine/provenance.ts
12472
+ // src/engine/provenance.ts
12583
12473
  const GENERATED_HEADER_RE = /^\s*(?:\/\/|#|\/\*)\s*(?:auto[- ]?generated|generated by|do not edit)/i;
12584
12474
  /**
12585
12475
  * Codegen fingerprint: the recorder names its first test exactly
@@ -12620,8 +12510,7 @@ function computeAgenticProfile(files, findings) {
12620
12510
  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."
12621
12511
  };
12622
12512
  }
12623
- //#endregion
12624
- //#region src/engine/overlap-dedup.ts
12513
+ // src/engine/overlap-dedup.ts
12625
12514
  const TIER_RANK = {
12626
12515
  core: 0,
12627
12516
  extended: 1,
@@ -12716,8 +12605,7 @@ function dropOrderBefore(a, b, metaByRuleId) {
12716
12605
  if (ranksBefore(b, a, metaByRuleId)) return true;
12717
12606
  return (a.column ?? Number.MAX_SAFE_INTEGER) < (b.column ?? Number.MAX_SAFE_INTEGER);
12718
12607
  }
12719
- //#endregion
12720
- //#region src/engine/finding-identity.ts
12608
+ // src/engine/finding-identity.ts
12721
12609
  /**
12722
12610
  * Correlation identity for before/after comparison: ruleId + file +
12723
12611
  * message, deliberately EXCLUDING line — a source edit that shifts a
@@ -12726,8 +12614,7 @@ function dropOrderBefore(a, b, metaByRuleId) {
12726
12614
  function findingFingerprint(f) {
12727
12615
  return `${f.ruleId}\u0000${f.file}\u0000${f.message}`;
12728
12616
  }
12729
- //#endregion
12730
- //#region src/engine/correlation-engine.ts
12617
+ // src/engine/correlation-engine.ts
12731
12618
  const EVIDENCE_RANK = {
12732
12619
  E0: 0,
12733
12620
  E1: 1,
@@ -12857,8 +12744,7 @@ function correlateFindings(findings) {
12857
12744
  });
12858
12745
  return conclusions;
12859
12746
  }
12860
- //#endregion
12861
- //#region src/engine/dependency-graph.ts
12747
+ // src/engine/dependency-graph.ts
12862
12748
  /**
12863
12749
  * Dependency Graph (ECO-005).
12864
12750
  *
@@ -12995,8 +12881,7 @@ function parseManifest(filePath) {
12995
12881
  if (filePath.endsWith("pyproject.toml")) return parsePyprojectToml(filePath);
12996
12882
  if (filePath.endsWith("pom.xml")) return parsePomXml(filePath);
12997
12883
  }
12998
- //#endregion
12999
- //#region src/engine/incremental-analysis.ts
12884
+ // src/engine/incremental-analysis.ts
13000
12885
  /**
13001
12886
  * Semantic inputs that affect the full scan beyond individual file content.
13002
12887
  * If any of these changed, an incremental run is NOT safe — a full scan
@@ -13032,8 +12917,7 @@ function isIncrementalSafe(changedFiles) {
13032
12917
  reasons
13033
12918
  };
13034
12919
  }
13035
- //#endregion
13036
- //#region src/brand/tokens.ts
12920
+ // src/brand/tokens.ts
13037
12921
  /**
13038
12922
  * The single source of brand truth.
13039
12923
  *
@@ -13319,8 +13203,7 @@ function deriveScoreState(score) {
13319
13203
  rune: RUNES.critical
13320
13204
  };
13321
13205
  }
13322
- //#endregion
13323
- //#region src/engine/monorepo-analysis.ts
13206
+ // src/engine/monorepo-analysis.ts
13324
13207
  /**
13325
13208
  * Package verdict, derived from the one ScoreState band model (BW-104).
13326
13209
  * These two constants used to be a private copy of the band boundaries;
@@ -13427,8 +13310,7 @@ function configurableAggregation(results, config) {
13427
13310
  strategy: config.weightingStrategy
13428
13311
  };
13429
13312
  }
13430
- //#endregion
13431
- //#region src/lib/fs-atomic.ts
13313
+ // src/lib/fs-atomic.ts
13432
13314
  /**
13433
13315
  * Atomic file writes (audit S9).
13434
13316
  *
@@ -13511,8 +13393,7 @@ function renameWithWindowsRetry(from, to) {
13511
13393
  throw err;
13512
13394
  }
13513
13395
  }
13514
- //#endregion
13515
- //#region src/engine/scan-cache.ts
13396
+ // src/engine/scan-cache.ts
13516
13397
  /**
13517
13398
  * Local incremental scan cache (Beta-to-Stable 1.0 plan, M5.2 / A-2).
13518
13399
  *
@@ -13612,7 +13493,9 @@ function hashDir(dir, hash, depth) {
13612
13493
  else if (entry.isFile() && /\.tsx?$/.test(entry.name)) try {
13613
13494
  hash.update(entry.name);
13614
13495
  hash.update(readFileSync(full));
13615
- } catch {}
13496
+ } catch {
13497
+ recordDegradation("rules-tree-file-unreadable");
13498
+ }
13616
13499
  }
13617
13500
  }
13618
13501
  /**
@@ -13722,8 +13605,7 @@ function createScanCache(root) {
13722
13605
  }
13723
13606
  };
13724
13607
  }
13725
- //#endregion
13726
- //#region src/reporter/theme.ts
13608
+ // src/reporter/theme.ts
13727
13609
  /**
13728
13610
  * Norse-forge theme system. Pure string-in → string-out helpers.
13729
13611
  * Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
@@ -13823,8 +13705,7 @@ function rgb([r, g, b]) {
13823
13705
  function palette(enabled) {
13824
13706
  return enabled ? on : off;
13825
13707
  }
13826
- //#endregion
13827
- //#region src/reporter/ui.ts
13708
+ // src/reporter/ui.ts
13828
13709
  /**
13829
13710
  * Neutral context for direct calls (tests, library use) and as the
13830
13711
  * safe default when a command runner is invoked without wiring: no
@@ -13849,8 +13730,7 @@ function sectionHeader(title, ui) {
13849
13730
  const glyph = ui.ascii ? "=" : "▍";
13850
13731
  return ` ${ui.p.accent(`${glyph} ${title}`)}`;
13851
13732
  }
13852
- //#endregion
13853
- //#region src/forensics/classify.ts
13733
+ // src/forensics/classify.ts
13854
13734
  /**
13855
13735
  * Strong-signal marker tables. Kept deliberately narrow: a marker here
13856
13736
  * is a named family fact, not a vibe. Unknown text yields no signal —
@@ -13970,8 +13850,7 @@ function classifyForensicVerdict(signals) {
13970
13850
  signals: signalCounts
13971
13851
  };
13972
13852
  }
13973
- //#endregion
13974
- //#region src/forensics/analyze.ts
13853
+ // src/forensics/analyze.ts
13975
13854
  const ui$5 = plainContext();
13976
13855
  const MAX_RECORDS = 1e5;
13977
13856
  function analyze(records, source) {
@@ -14134,8 +14013,7 @@ function renderFlakyMd(report) {
14134
14013
  lines.push("");
14135
14014
  return lines.join("\n");
14136
14015
  }
14137
- //#endregion
14138
- //#region src/forensics/evidence-hygiene.ts
14016
+ // src/forensics/evidence-hygiene.ts
14139
14017
  /**
14140
14018
  * Evidence hygiene — secret redaction + control-char sanitization
14141
14019
  * (ENGINE-012).
@@ -14197,8 +14075,7 @@ function sanitizeErrorText(text, options) {
14197
14075
  if (result.length > maxLength) result = result.slice(0, maxLength) + `[truncated at ${maxLength} chars]`;
14198
14076
  return result;
14199
14077
  }
14200
- //#endregion
14201
- //#region src/forensics/trace.ts
14078
+ // src/forensics/trace.ts
14202
14079
  /**
14203
14080
  * Playwright trace ingester (product-gap master plan §9 R5 / WI-17).
14204
14081
  *
@@ -14391,8 +14268,7 @@ function parseTraceArtifact(bytes, artifactName) {
14391
14268
  const { actions } = parseTraceNdjson(text);
14392
14269
  return traceActionsToRecord(actions, artifactName);
14393
14270
  }
14394
- //#endregion
14395
- //#region src/forensics/parse-jest-json.ts
14271
+ // src/forensics/parse-jest-json.ts
14396
14272
  function toStatus$2(raw) {
14397
14273
  switch (raw) {
14398
14274
  case "passed": return "passed";
@@ -14452,8 +14328,7 @@ function parseJestJson(json) {
14452
14328
  }
14453
14329
  return out;
14454
14330
  }
14455
- //#endregion
14456
- //#region src/forensics/parse-junit.ts
14331
+ // src/forensics/parse-junit.ts
14457
14332
  const MAX_INPUT$1 = 20971520;
14458
14333
  function decodeEntities$1(s) {
14459
14334
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -14495,14 +14370,25 @@ function parseJunitXml(xml) {
14495
14370
  const skipped = /<skipped\b/i.test(inner);
14496
14371
  const errors = [];
14497
14372
  if (failed) {
14498
- const failRe = /<(?:failure|error)\b([^>]*)>([\s\S]*?)<\/(?:failure|error)\s*>/gi;
14499
- let failMatch;
14500
- while ((failMatch = failRe.exec(inner)) !== null) {
14501
- const attrsStr = failMatch[1] ?? "";
14373
+ const openRe = /<(?:failure|error)\b/gi;
14374
+ const endRe = /<\/(?:failure|error)\s*>/gi;
14375
+ let open;
14376
+ while ((open = openRe.exec(inner)) !== null) {
14377
+ const gt = inner.indexOf(">", open.index);
14378
+ if (gt === -1) break;
14379
+ const attrsStr = inner.slice(open.index, gt);
14380
+ if (attrsStr.endsWith("/")) {
14381
+ openRe.lastIndex = gt + 1;
14382
+ continue;
14383
+ }
14384
+ endRe.lastIndex = gt + 1;
14385
+ const end = endRe.exec(inner);
14386
+ if (end === null) break;
14502
14387
  const msgAttr = /\smessage\s*=\s*"([^"]*)"/i.exec(attrsStr)?.[1];
14503
14388
  if (msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(msgAttr)));
14504
- const body = failMatch[2]?.trim();
14389
+ const body = inner.slice(gt + 1, end.index).trim();
14505
14390
  if (body && body !== msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(body)));
14391
+ openRe.lastIndex = end.index + end[0].length;
14506
14392
  }
14507
14393
  const selfCloseRe = /<(?:failure|error)\b([^>]*)\/>/gi;
14508
14394
  let scMatch;
@@ -14526,8 +14412,7 @@ function parseJunitXml(xml) {
14526
14412
  }
14527
14413
  return out;
14528
14414
  }
14529
- //#endregion
14530
- //#region src/forensics/parse-trx.ts
14415
+ // src/forensics/parse-trx.ts
14531
14416
  const MAX_INPUT = 20971520;
14532
14417
  function decodeEntities(s) {
14533
14418
  return s.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&#39;", "'").replaceAll("&amp;", "&");
@@ -14638,11 +14523,24 @@ function parseTrxXml(xml) {
14638
14523
  if (id !== void 0 && name !== void 0) byTestId.set(id, name);
14639
14524
  }
14640
14525
  const records = [];
14641
- const resultRe = /<UnitTestResult\b([^>]*)>([\s\S]{0,4000}?)(?=<UnitTestResult\b|<\/Results>)/gi;
14642
- let m;
14643
- while ((m = resultRe.exec(xml)) !== null) {
14644
- const attrs = m[1] ?? "";
14645
- const inner = m[2] ?? "";
14526
+ const startRe = /<UnitTestResult\b/gi;
14527
+ const starts = [];
14528
+ let s;
14529
+ while ((s = startRe.exec(xml)) !== null) {
14530
+ starts.push(s.index);
14531
+ if (starts.length > 2e5) break;
14532
+ }
14533
+ const resultsEnd = xml.search(/<\/Results>/i);
14534
+ for (let i = 0; i < starts.length; i++) {
14535
+ const start = starts[i];
14536
+ const tagEnd = xml.indexOf(">", start);
14537
+ if (tagEnd === -1) break;
14538
+ const selfClosing = xml[tagEnd - 1] === "/";
14539
+ const attrs = xml.slice(start + 15, selfClosing ? tagEnd - 1 : tagEnd);
14540
+ const next = starts[i + 1] ?? -1;
14541
+ let bodyEnd = next === -1 ? xml.length : next;
14542
+ if (resultsEnd > tagEnd && resultsEnd < bodyEnd) bodyEnd = resultsEnd;
14543
+ const inner = selfClosing ? "" : xml.slice(tagEnd + 1, bodyEnd);
14646
14544
  const testName = attr(attrs, "testName") ?? attr(attrs, "testId");
14647
14545
  if (testName === void 0) continue;
14648
14546
  const attempt = {
@@ -14650,7 +14548,8 @@ function parseTrxXml(xml) {
14650
14548
  status: mapTrxOutcome(attr(attrs, "outcome")),
14651
14549
  durationMs: parseDuration(attr(attrs, "duration"))
14652
14550
  };
14653
- const error = messageOf(inner, 0);
14551
+ const messageAt = inner.search(/<Message\b/i);
14552
+ const error = messageAt === -1 ? void 0 : messageOf(inner, messageAt);
14654
14553
  records.push({
14655
14554
  file: "unknown",
14656
14555
  title: testName,
@@ -14662,8 +14561,7 @@ function parseTrxXml(xml) {
14662
14561
  }
14663
14562
  return records;
14664
14563
  }
14665
- //#endregion
14666
- //#region src/forensics/parse-playwright-json.ts
14564
+ // src/forensics/parse-playwright-json.ts
14667
14565
  const MAX_DEPTH = 32;
14668
14566
  function toStatus$1(raw) {
14669
14567
  switch (raw) {
@@ -14728,8 +14626,7 @@ function parsePlaywrightJson(json) {
14728
14626
  walk(root, 0);
14729
14627
  return out;
14730
14628
  }
14731
- //#endregion
14732
- //#region src/forensics/parse-vitest-json.ts
14629
+ // src/forensics/parse-vitest-json.ts
14733
14630
  function toStatus(raw) {
14734
14631
  switch (raw) {
14735
14632
  case "passed": return "passed";
@@ -14783,8 +14680,7 @@ function parseVitestJson(json) {
14783
14680
  }
14784
14681
  return out;
14785
14682
  }
14786
- //#endregion
14787
- //#region src/forensics/parse-har.ts
14683
+ // src/forensics/parse-har.ts
14788
14684
  const MAX_ENTRIES = 2e4;
14789
14685
  function sanitizeTitleUrl(url) {
14790
14686
  try {
@@ -14869,8 +14765,7 @@ function parseHarJsonDetailed(json) {
14869
14765
  truncated
14870
14766
  };
14871
14767
  }
14872
- //#endregion
14873
- //#region src/forensics/run.ts
14768
+ // src/forensics/run.ts
14874
14769
  /**
14875
14770
  * Runtime evidence entry point (R4) — the library behind `explain
14876
14771
  * --evidence`. The `forensics` verb is retired in the v6 carve; this module
@@ -15035,13 +14930,15 @@ function runForensics(target, options = {}) {
15035
14930
  report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
15036
14931
  }
15037
14932
  let flakyMdPath;
14933
+ let flakyMdWriteError;
15038
14934
  if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
15039
14935
  const base = stat.isFile() ? dirname(target) : target;
15040
14936
  flakyMdPath = join(base, "FLAKY.md");
15041
14937
  try {
15042
14938
  writeFileAtomic(flakyMdPath, renderFlakyMd(report));
15043
- } catch {
14939
+ } catch (e) {
15044
14940
  flakyMdPath = void 0;
14941
+ flakyMdWriteError = e instanceof Error ? e.message : String(e);
15045
14942
  }
15046
14943
  }
15047
14944
  return {
@@ -15049,7 +14946,7 @@ function runForensics(target, options = {}) {
15049
14946
  output: [
15050
14947
  renderLeaderboard(report),
15051
14948
  "",
15052
- 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)
14949
+ 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)
15053
14950
  ].join("\n"),
15054
14951
  flakyMdPath
15055
14952
  };
@@ -15058,9 +14955,10 @@ function renderFlakyMdHint() {
15058
14955
  return "Full details in FLAKY.md (committed artifact).";
15059
14956
  }
15060
14957
  /** Honest fallback for every case where FLAKY.md was NOT written (M2). */
15061
- function renderFlakyMdNotWritten(options) {
14958
+ function renderFlakyMdNotWritten(options, writeError) {
15062
14959
  if (options.writeFlakyMd === false) return "FLAKY.md not written (--no-flaky-md).";
15063
- return "FLAKY.md was not written — nothing recognized to report (or the target directory is not writable).";
14960
+ if (writeError !== void 0) return `FLAKY.md could not be written: ${writeError}`;
14961
+ return "FLAKY.md was not written — nothing recognized to report.";
15064
14962
  }
15065
14963
  /**
15066
14964
  * R5: the trace artifact's test identity. The per-test trace convention
@@ -15161,8 +15059,7 @@ function loadSuppressions(root, now = /* @__PURE__ */ new Date()) {
15161
15059
  entries
15162
15060
  };
15163
15061
  }
15164
- //#endregion
15165
- //#region src/plugins/reserved-prefixes.ts
15062
+ // src/plugins/reserved-prefixes.ts
15166
15063
  /**
15167
15064
  * Core-owned rule-ID prefixes that external rule sources (npm plugins,
15168
15065
  * JS modules, JSON manifests) may never claim — one shared law.
@@ -15192,8 +15089,7 @@ function isReservedPrefix(ruleId) {
15192
15089
  const upper = ruleId.toUpperCase();
15193
15090
  return RESERVED_PREFIXES.some((p) => upper.startsWith(p));
15194
15091
  }
15195
- //#endregion
15196
- //#region src/plugins/load.ts
15092
+ // src/plugins/load.ts
15197
15093
  /**
15198
15094
  * Plugin API (Upgrade-Plan-v3 Phase 6, tier-2 item #1).
15199
15095
  *
@@ -15299,8 +15195,7 @@ function loadPlugins(root, gateOpen = false) {
15299
15195
  }
15300
15196
  return result;
15301
15197
  }
15302
- //#endregion
15303
- //#region src/plugins/local-rules.ts
15198
+ // src/plugins/local-rules.ts
15304
15199
  /**
15305
15200
  * Local plugin/rule contract (Verification Trust Evolution Plan §18 —
15306
15201
  * Local Extensibility).
@@ -15612,8 +15507,7 @@ async function loadModuleRules(path, result) {
15612
15507
  result.rules.push(rule);
15613
15508
  }
15614
15509
  }
15615
- //#endregion
15616
- //#region src/plugins/trust-gate.ts
15510
+ // src/plugins/trust-gate.ts
15617
15511
  /**
15618
15512
  * Plugin trust gate (audit-remediation-master-plan.md, locked decision C2).
15619
15513
  *
@@ -15649,8 +15543,7 @@ function renderGateNotice(skipped) {
15649
15543
  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.");
15650
15544
  return lines.join("\n");
15651
15545
  }
15652
- //#endregion
15653
- //#region src/engine/scan-pipeline.ts
15546
+ // src/engine/scan-pipeline.ts
15654
15547
  /**
15655
15548
  * Canonical scan pipeline (M6 — blueprint §9.2).
15656
15549
  *
@@ -15666,8 +15559,13 @@ function renderGateNotice(skipped) {
15666
15559
  * module moved. Re-exports in cli.ts keep the historical import surface.
15667
15560
  */
15668
15561
  const UNIVERSAL_RULES = RULES.map(asUniversal);
15669
- const DEFAULT_MAX_DURATION_MS$1 = 6e5;
15670
- const MAX_DURATION_MS$1 = 36e5;
15562
+ /**
15563
+ * The scan time budget: the default, and the ceiling every surface enforces.
15564
+ * Owned here, where the clamp is applied; `cli.ts` and the MCP server import
15565
+ * these rather than keeping copies that only agree by coincidence.
15566
+ */
15567
+ const DEFAULT_MAX_DURATION_MS = 6e5;
15568
+ const MAX_DURATION_MS = 36e5;
15671
15569
  /** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
15672
15570
  const KNOWN_RULE_IDS = new Set(RULES.map((r) => r.id));
15673
15571
  /**
@@ -15710,9 +15608,10 @@ async function buildUniversalRules(root, strict, opts = {}) {
15710
15608
  for (const p of plugins) for (const r of p.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
15711
15609
  for (const r of local.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
15712
15610
  let rules = [...UNIVERSAL_RULES, ...externalRules];
15713
- const withheldRuleIds = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
15611
+ const quarantined = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
15612
+ const withheldRuleIds = strict ? [] : quarantined;
15714
15613
  if (!strict) rules = rules.filter((r) => tierByRuleId.get(r.id) !== "quarantine");
15715
- const rulesWithheld = strict ? 0 : withheldRuleIds.length;
15614
+ const rulesWithheld = withheldRuleIds.length;
15716
15615
  const rulesApplied = rules.length;
15717
15616
  const pluginMeta = [...plugins.map((p) => ({
15718
15617
  name: p.name,
@@ -16109,12 +16008,28 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
16109
16008
  const runtimeIncomplete = discoveredReport !== void 0 && discoveredReport.report.analysisComplete !== true;
16110
16009
  let forensicVerdicts;
16111
16010
  let evidenceRecords = [];
16112
- if (discoveredReport && discoveredReport.report.analysisComplete === true) try {
16113
- evidenceRecords = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
16114
- stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
16115
- forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
16116
- } catch {
16117
- evidenceRecords = [];
16011
+ if (discoveredReport && discoveredReport.report.analysisComplete === true) {
16012
+ const before = findings.map((f) => ({
16013
+ runtimeCorroboration: f.runtimeCorroboration,
16014
+ trustLevel: f.trustLevel
16015
+ }));
16016
+ try {
16017
+ const records = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
16018
+ stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
16019
+ forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
16020
+ evidenceRecords = records;
16021
+ } catch {
16022
+ findings.forEach((f, i) => {
16023
+ const prev = before[i];
16024
+ if (prev?.runtimeCorroboration === void 0) delete f.runtimeCorroboration;
16025
+ else f.runtimeCorroboration = prev.runtimeCorroboration;
16026
+ if (prev?.trustLevel === void 0) delete f.trustLevel;
16027
+ else f.trustLevel = prev.trustLevel;
16028
+ });
16029
+ forensicVerdicts = void 0;
16030
+ evidenceRecords = [];
16031
+ recordDegradation("runtime-corroboration-failed");
16032
+ }
16118
16033
  }
16119
16034
  return {
16120
16035
  evidenceRecords,
@@ -16405,8 +16320,8 @@ async function runScan(args, hooks = {}) {
16405
16320
  async function runScanInProject(args, hooks = {}) {
16406
16321
  const started = Date.now();
16407
16322
  const degradationWindow = beginDegradationWindow();
16408
- const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$1;
16409
- const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$1);
16323
+ const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS;
16324
+ const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS);
16410
16325
  const discovered = discoverWorkspace(args.target);
16411
16326
  const targetAbs = resolve(args.target);
16412
16327
  const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
@@ -16547,8 +16462,7 @@ async function runScanInProject(args, hooks = {}) {
16547
16462
  resetTsMorphProject();
16548
16463
  return result;
16549
16464
  }
16550
- //#endregion
16551
- //#region src/cli-io.ts
16465
+ // src/cli-io.ts
16552
16466
  /**
16553
16467
  * The ONE error-to-message derivation in `src/`.
16554
16468
  *
@@ -16707,8 +16621,7 @@ function buildMachineContract(result) {
16707
16621
  ...result.forensicVerdicts !== void 0 ? { forensicVerdicts: result.forensicVerdicts } : {}
16708
16622
  };
16709
16623
  }
16710
- //#endregion
16711
- //#region src/commands/fixture-example.ts
16624
+ // src/commands/fixture-example.ts
16712
16625
  /**
16713
16626
  * Shared example-fixture selection for `mjolnir explain` (explain.ts)
16714
16627
  * and the generated rule docs (rule-docs.ts) — one chooser so both
@@ -16809,8 +16722,7 @@ function explainRule(ruleId, fixturesRoot) {
16809
16722
  exampleFixtureRelPath: relative(fixturesRoot, fixturePath).replaceAll("\\", "/")
16810
16723
  };
16811
16724
  }
16812
- //#endregion
16813
- //#region src/engine/resolution.ts
16725
+ // src/engine/resolution.ts
16814
16726
  /** Correlation identity (v1-compatible): ruleId\0file\0message. */
16815
16727
  const fingerprint$1 = findingFingerprint;
16816
16728
  /**
@@ -16961,8 +16873,7 @@ function diffAgainstBaseline(result, baseline) {
16961
16873
  unchangedCount
16962
16874
  };
16963
16875
  }
16964
- //#endregion
16965
- //#region src/commands/verify.ts
16876
+ // src/commands/verify.ts
16966
16877
  /**
16967
16878
  * `mjolnir ci verify` — the agent-loop digest (product-gap-remediation
16968
16879
  * master plan P7, plan 1788853205786 — flag agent, decision 6).
@@ -17109,8 +17020,7 @@ function renderTriageWorkflowJson(report) {
17109
17020
  rows: workflowRows(report)
17110
17021
  }, null, 2) + "\n";
17111
17022
  }
17112
- //#endregion
17113
- //#region src/commands/pw-report.ts
17023
+ // src/commands/pw-report.ts
17114
17024
  const ui = plainContext();
17115
17025
  function summarizePwRun(report) {
17116
17026
  const slowest = [...report.verdicts].sort((a, b) => b.totalDurationMs - a.totalDurationMs).slice(0, 5).map((v) => ({
@@ -17144,8 +17054,7 @@ function renderPwRunSummary(s) {
17144
17054
  }
17145
17055
  return lines.join("\n");
17146
17056
  }
17147
- //#endregion
17148
- //#region src/mcp/server.ts
17057
+ // src/mcp/server.ts
17149
17058
  /**
17150
17059
  * MCP stdio transport (blueprint §21 — Phase 4, PURE TRANSPORT).
17151
17060
  *
@@ -17163,22 +17072,38 @@ function renderPwRunSummary(s) {
17163
17072
  * - parameter size caps; ONE scan in flight (serialized queue) reusing
17164
17073
  * the pipeline's own budgets; zero network (no fetches anywhere); no
17165
17074
  * shell execution beyond Mjölnir's own verbs.
17166
- * - filesystem boundary = scan target + `.mjolnir/` (same as CLI).
17075
+ * - filesystem boundary: the tools are read-only, and with no further
17076
+ * configuration they accept any path the parent process passes (the
17077
+ * parent is the trust boundary). `MJOLNIR_WORKSPACE_ROOT` confines every
17078
+ * path argument to that directory, symlinks resolved.
17167
17079
  * - the plugin trust gate applies unchanged (--enable-plugins is NOT
17168
17080
  * exposed over MCP: tools run with the gate CLOSED unless the server
17169
17081
  * was started with MJOLNIR_ENABLE_PLUGINS=1 in its own environment).
17170
17082
  */
17171
17083
  /** Hard caps (§21 threat model: parameter size + resource bounds). */
17172
17084
  const MAX_PARAM_BYTES = 65536;
17173
- const MAX_DURATION_MS = 36e5;
17174
- const DEFAULT_MAX_DURATION_MS = 6e5;
17175
17085
  const PROTOCOL_VERSION = "2025-06-18";
17176
- const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? resolve(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
17086
+ /** The real path of `p`, or its lexical resolution when it does not exist. */
17087
+ function realOrResolved(p) {
17088
+ try {
17089
+ return realpathSync.native(resolve(p));
17090
+ } catch {
17091
+ return resolve(p);
17092
+ }
17093
+ }
17094
+ /**
17095
+ * The optional workspace root. When set, every path argument must resolve
17096
+ * INSIDE it after symlinks are followed.
17097
+ *
17098
+ * Unset is open by design and is documented that way in the header: the MCP
17099
+ * tools are read-only and the stdio parent is the trust boundary. Setting
17100
+ * `MJOLNIR_WORKSPACE_ROOT` is how a host confines them.
17101
+ */
17102
+ const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? realOrResolved(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
17177
17103
  function isPathAllowed(path) {
17178
17104
  if (!CONFIGURED_ROOT) return true;
17179
- const candidate = resolve(path);
17180
- const rel = relative(CONFIGURED_ROOT, candidate);
17181
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
17105
+ const rel = relative(CONFIGURED_ROOT, realOrResolved(path));
17106
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
17182
17107
  }
17183
17108
  /** JSON-RPC error codes (subset used here). */
17184
17109
  const MCP_ERRORS = {
@@ -17220,7 +17145,7 @@ const MCP_TOOLS = [
17220
17145
  },
17221
17146
  fixturesRoot: {
17222
17147
  type: "string",
17223
- description: "Root directory for fixture lookups (defaults to cwd)."
17148
+ description: "Root directory for fixture lookups (defaults to <cwd>/tests/fixtures, as for `mjolnir explain`)."
17224
17149
  }
17225
17150
  },
17226
17151
  required: ["ruleId"]
@@ -17300,7 +17225,7 @@ function validateParams(name, args) {
17300
17225
  if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
17301
17226
  if (args["maxDurationMs"] !== void 0) {
17302
17227
  const duration = args["maxDurationMs"];
17303
- if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > MAX_DURATION_MS) return "`maxDurationMs` must be a finite positive number within the server budget";
17228
+ if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > 36e5) return "`maxDurationMs` must be a finite positive number within the server budget";
17304
17229
  }
17305
17230
  }
17306
17231
  if (name === "explain") {
@@ -17404,7 +17329,7 @@ async function handleToolCall(call) {
17404
17329
  message: "fixturesRoot must be a string"
17405
17330
  }
17406
17331
  };
17407
- const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? process.cwd());
17332
+ const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? join(process.cwd(), "tests", "fixtures"));
17408
17333
  if (!explanation.ok) return {
17409
17334
  jsonrpc: "2.0",
17410
17335
  id: call.id,
@@ -17655,8 +17580,7 @@ async function handleMcpMessage(msg) {
17655
17580
  }
17656
17581
  };
17657
17582
  }
17658
- //#endregion
17659
- //#region src/mcp/transport.ts
17583
+ // src/mcp/transport.ts
17660
17584
  /**
17661
17585
  * MCP stdio transport loop (blueprint §21 — the framing around the
17662
17586
  * pure-transport server).
@@ -17753,8 +17677,7 @@ async function handleStdioLine(line, write) {
17753
17677
  }
17754
17678
  write(`${JSON.stringify(response)}\n`);
17755
17679
  }
17756
- //#endregion
17757
- //#region src/integrations/sentry.ts
17680
+ // src/integrations/sentry.ts
17758
17681
  /**
17759
17682
  * Sentry crash reporting — opt-in, and inert unless a DSN is configured.
17760
17683
  *
@@ -17880,8 +17803,7 @@ async function flushSentry(timeoutMs = FLUSH_TIMEOUT_MS) {
17880
17803
  return false;
17881
17804
  }
17882
17805
  }
17883
- //#endregion
17884
- //#region src/mcp/stdio.ts
17806
+ // src/mcp/stdio.ts
17885
17807
  /**
17886
17808
  * The standalone MCP stdio binary (blueprint §21), built to
17887
17809
  * `dist/mcp/stdio.mjs` and run by `npm run mcp`.
@@ -17906,7 +17828,6 @@ runStdioTransport(process.stdin, process.stdout).then(() => process.exit(0)).cat
17906
17828
  process.stderr.write(`mjolnir mcp fatal: ${err instanceof Error ? err.message : err}\n`);
17907
17829
  process.exitCode = 20;
17908
17830
  });
17909
- //#endregion
17910
17831
  export {};
17911
17832
 
17912
17833
  //# sourceMappingURL=stdio.mjs.map