mjolnir-qa 2.1.0 → 3.0.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.
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { createRequire } from "node:module";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
4
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { createHash } from "node:crypto";
6
+ import { createHash, randomBytes } from "node:crypto";
7
7
  import * as ts$2 from "ts-morph";
8
8
  import ts, { Project, SyntaxKind, ts as ts$1 } from "ts-morph";
9
9
  import { Language, Parser } from "web-tree-sitter";
@@ -97,6 +97,38 @@ const DEDUCTIONS = {
97
97
  info: 1
98
98
  };
99
99
  //#endregion
100
+ //#region src/engine/completion.ts
101
+ function deriveCompletion(input) {
102
+ const truncationReasons = [...new Set(input.truncationReasons)].sort();
103
+ const reasons = /* @__PURE__ */ new Set();
104
+ if (input.discoveryTruncated) reasons.add("discovery-truncated");
105
+ if (input.rulesPartial) reasons.add("rules-partial");
106
+ if (input.skippedFiles > 0) reasons.add(`skipped-files:${input.skippedFiles}`);
107
+ if (input.rulesCrashed > 0) reasons.add(`rules-crashed:${input.rulesCrashed}`);
108
+ if (input.scopeIgnored > 0) reasons.add(`scope-ignored:${input.scopeIgnored}`);
109
+ if (input.scopeUnrecognized > 0) reasons.add(`scope-unrecognized:${input.scopeUnrecognized}`);
110
+ if (input.parseFailed > 0) reasons.add(`parse-failed:${input.parseFailed}`);
111
+ if (input.parseFallbacks && input.parseFallbacks > 0) reasons.add(`parse-fallbacks:${input.parseFallbacks}`);
112
+ if (input.scopeDegraded) reasons.add(`scope-degraded:${input.scopeDegraded}`);
113
+ if (input.runtimeIncomplete) reasons.add("runtime-incomplete");
114
+ if (input.identityIncomplete) reasons.add("identity-incomplete");
115
+ for (const reason of truncationReasons) reasons.add(`truncated:${reason}`);
116
+ const discoveryPartial = input.discoveryTruncated || input.scopeIgnored > 0 || input.scopeUnrecognized > 0 || input.scopeDegraded !== void 0;
117
+ const rulesPartial = input.rulesPartial || input.rulesCrashed > 0 || input.parseFailed > 0;
118
+ return {
119
+ partial: discoveryPartial || rulesPartial || input.skippedFiles > 0 || input.runtimeIncomplete === true || input.identityIncomplete === true || truncationReasons.length > 0,
120
+ analysisStatus: {
121
+ discovery: discoveryPartial ? "partial" : "complete",
122
+ rules: rulesPartial ? "partial" : "complete",
123
+ skippedFiles: input.skippedFiles,
124
+ rulesCrashed: input.rulesCrashed,
125
+ parseFallbacks: input.parseFallbacks ?? 0,
126
+ ...truncationReasons.length > 0 ? { truncationReasons } : {},
127
+ reasons: [...reasons].sort()
128
+ }
129
+ };
130
+ }
131
+ //#endregion
100
132
  //#region src/engine/runtime-corroboration.ts
101
133
  /**
102
134
  * Stamp runtime corroboration + trust levels onto findings (mutates in
@@ -863,7 +895,10 @@ function sha256$1(text) {
863
895
  return createHash("sha256").update(text).digest("hex");
864
896
  }
865
897
  function canonical(value) {
866
- return JSON.stringify(value);
898
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
899
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
900
+ const record = value;
901
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(",")}}`;
867
902
  }
868
903
  function buildRunIdentity(input) {
869
904
  const inputFingerprint = sha256$1([...input.files].map((f) => f.hash ? `${f.path}:${f.hash}` : `${f.path}:${f.size}`).sort().join("\n"));
@@ -945,7 +980,7 @@ function buildEvidenceGraph(parts) {
945
980
  * scripts/sync-sarif-version.cjs and guarded by the version-consistency
946
981
  * spec. cli.ts re-exports this as CLI_VERSION.
947
982
  */
948
- const ENGINE_VERSION = "2.1.0";
983
+ const ENGINE_VERSION = "3.0.0";
949
984
  //#endregion
950
985
  //#region src/engine/contract-versions.ts
951
986
  /**
@@ -1106,7 +1141,9 @@ function basename$1(p) {
1106
1141
  * Internal files (cache, hashes) use `isRecord`; user-facing files
1107
1142
  * (config, plugins) use stricter checks that throw descriptive errors.
1108
1143
  */
1144
+ const MAX_JSON_BYTES = 16777216;
1109
1145
  function parseJsonFile(text, source, validate) {
1146
+ if (Buffer.byteLength(text, "utf8") > MAX_JSON_BYTES) throw new Error(`JSON source exceeds the ${MAX_JSON_BYTES}-byte limit: ${source}`);
1110
1147
  let parsed;
1111
1148
  try {
1112
1149
  parsed = JSON.parse(text);
@@ -1125,6 +1162,59 @@ function isRecord(v) {
1125
1162
  return typeof v === "object" && v !== null && !Array.isArray(v);
1126
1163
  }
1127
1164
  //#endregion
1165
+ //#region src/lib/fs-bounded.ts
1166
+ function closeQuiet(fd) {
1167
+ try {
1168
+ closeSync(fd);
1169
+ } catch {
1170
+ return;
1171
+ }
1172
+ }
1173
+ function readFileBounded(path, maxBytes) {
1174
+ let fd;
1175
+ try {
1176
+ const pathStat = lstatSync(path);
1177
+ if (pathStat.isSymbolicLink()) return {
1178
+ ok: false,
1179
+ reason: "symlink"
1180
+ };
1181
+ if (!pathStat.isFile()) return {
1182
+ ok: false,
1183
+ reason: "unreadable"
1184
+ };
1185
+ fd = openSync(path, 0);
1186
+ const fdStat = fstatSync(fd);
1187
+ if (!fdStat.isFile() || fdStat.size > maxBytes) return {
1188
+ ok: false,
1189
+ reason: fdStat.size > maxBytes ? "too-large" : "unreadable"
1190
+ };
1191
+ const chunks = [];
1192
+ let total = 0;
1193
+ const buffer = Buffer.allocUnsafe(Math.min(65536, maxBytes));
1194
+ while (true) {
1195
+ const read = readSync(fd, buffer, 0, buffer.length, null);
1196
+ if (read === 0) break;
1197
+ total += read;
1198
+ if (total > maxBytes) return {
1199
+ ok: false,
1200
+ reason: "too-large"
1201
+ };
1202
+ chunks.push(Buffer.from(buffer.subarray(0, read)));
1203
+ }
1204
+ return {
1205
+ ok: true,
1206
+ data: Buffer.concat(chunks)
1207
+ };
1208
+ } catch {
1209
+ return {
1210
+ ok: false,
1211
+ reason: "unreadable"
1212
+ };
1213
+ } finally {
1214
+ if (fd !== void 0) closeQuiet(fd);
1215
+ }
1216
+ }
1217
+ //#endregion
1128
1218
  //#region src/config/config-schema.ts
1129
1219
  const VALID_GATES = /* @__PURE__ */ new Set([
1130
1220
  "advisory",
@@ -1223,7 +1313,11 @@ const CONFIG_NAMES = ["mjolnir.config.json", ".mjolnir.json"];
1223
1313
  function findConfigPath(root) {
1224
1314
  for (const name of CONFIG_NAMES) {
1225
1315
  const p = join(root, name);
1226
- if (existsSync(p)) return p;
1316
+ try {
1317
+ if (existsSync(p) && !lstatSync(p).isSymbolicLink()) return p;
1318
+ } catch {
1319
+ continue;
1320
+ }
1227
1321
  }
1228
1322
  return null;
1229
1323
  }
@@ -1243,7 +1337,10 @@ function loadConfig(root, options = {}) {
1243
1337
  const p = join(root, name);
1244
1338
  if (!existsSync(p)) continue;
1245
1339
  try {
1246
- const parsed = parseJsonFile(readFileSync(p, "utf8"), p, (v) => isRecord(v));
1340
+ if (!existsSync(p) || lstatSync(p).isSymbolicLink()) continue;
1341
+ const read = readFileBounded(p, 1048576);
1342
+ if (!read.ok) throw new Error("config source could not be read safely");
1343
+ const parsed = parseJsonFile(read.data.toString("utf8"), p, (v) => isRecord(v));
1247
1344
  return {
1248
1345
  config: parsed,
1249
1346
  path: p,
@@ -1542,8 +1639,9 @@ function sharedWalk(options) {
1542
1639
  }
1543
1640
  const full = join(dir, entry.name);
1544
1641
  const rel = relative(options.root, full).replaceAll("\\", "/");
1642
+ if (entry.isDirectory() && options.skipDirs.includes(entry.name)) continue;
1545
1643
  if (options.ignoreMatcher.isIgnored(rel)) {
1546
- options.onIgnored?.();
1644
+ if (!rel.startsWith(".mjolnir/")) options.onIgnored?.(rel);
1547
1645
  continue;
1548
1646
  }
1549
1647
  if (entry.isSymbolicLink()) {
@@ -1551,7 +1649,6 @@ function sharedWalk(options) {
1551
1649
  continue;
1552
1650
  }
1553
1651
  if (entry.isDirectory()) {
1554
- if (options.skipDirs.includes(entry.name)) continue;
1555
1652
  if (rel.split("/").length > LIMITS$1.maxDepth) {
1556
1653
  options.onSkipped("max-depth");
1557
1654
  continue;
@@ -1569,7 +1666,7 @@ function sharedWalk(options) {
1569
1666
  } catch {
1570
1667
  options.onSkipped("stat-failed");
1571
1668
  }
1572
- else if (entry.isFile()) options.onUnrecognized?.();
1669
+ else if (entry.isFile()) options.onUnrecognized?.(rel);
1573
1670
  }
1574
1671
  };
1575
1672
  walk(options.root);
@@ -3671,7 +3768,7 @@ const SCAN_ADAPTERS = [
3671
3768
  * the adapter that claims it; per-adapter caps still apply.
3672
3769
  */
3673
3770
  function discoverAllTestFiles(ctx, languageAdapters, buckets, fixtureDirMemo) {
3674
- const walkSkips = languageAdapters.reduce((common, a) => common.filter((name) => a.dirSkips.includes(name)), languageAdapters[0]?.dirSkips ?? []);
3771
+ const walkSkips = [.../* @__PURE__ */ new Set([...languageAdapters.reduce((common, a) => common.filter((name) => a.dirSkips.includes(name)), languageAdapters[0]?.dirSkips ?? []), ".mjolnir"])];
3675
3772
  sharedWalk({
3676
3773
  root: ctx.workspace.root,
3677
3774
  deadline: ctx.deadline,
@@ -3697,10 +3794,35 @@ function discoverAllTestFiles(ctx, languageAdapters, buckets, fixtureDirMemo) {
3697
3794
  },
3698
3795
  isFull: () => languageAdapters.every((a) => (buckets.get(a.id)?.length ?? 0) >= ctx.maxFiles),
3699
3796
  fixtureDirMemo,
3700
- onIgnored: ctx.onIgnored,
3701
- onUnrecognized: ctx.onUnrecognized
3797
+ onIgnored: (path) => {
3798
+ if (isScopeRelevantIgnored(path)) ctx.onIgnored?.(path);
3799
+ },
3800
+ onUnrecognized: (path) => {
3801
+ if (typeof path === "string" && isUnrecognizedSourceCandidate(path)) ctx.onUnrecognized?.(path);
3802
+ }
3702
3803
  });
3703
3804
  }
3805
+ function isScopeRelevantIgnored(path) {
3806
+ const name = path.replaceAll("\\", "/").split("/").pop() ?? path;
3807
+ return /\.(?:[cm]?[jt]sx?|py|java|cs|ya?ml|min\.js)$/i.test(name);
3808
+ }
3809
+ function isUnrecognizedSourceCandidate(path) {
3810
+ const normalized = path.replaceAll("\\", "/");
3811
+ const name = normalized.split("/").pop() ?? normalized;
3812
+ if (normalized.includes(".github/workflows/") || name === "azure-pipelines.yml" || name === "Jenkinsfile") return false;
3813
+ const extensionIndex = name.lastIndexOf(".");
3814
+ const stem = extensionIndex > 0 ? name.slice(0, extensionIndex) : name;
3815
+ if (name === "mjolnir.config.json" || name === ".mjolnir.json" || name === "package.json" || name === "package-lock.json" || name === "pnpm-lock.yaml" || name === "yarn.lock" || [
3816
+ "playwright",
3817
+ "vitest",
3818
+ "vite",
3819
+ "eslint",
3820
+ "tsconfig",
3821
+ "jsconfig"
3822
+ ].some((prefix) => stem === prefix || stem.startsWith(`${prefix}.`))) return false;
3823
+ const testLike = /(?:^|\/)__tests__\//i.test(normalized) || /\.(?:spec|test)\.[cm]?[jt]sx?$/i.test(name);
3824
+ return /\.(?:[cm]?[jt]sx?|py|java|cs|ya?ml)$/i.test(name) && testLike;
3825
+ }
3704
3826
  /** Whether ANY shipped adapter would discover this path as a test file. */
3705
3827
  function isKnownTestFile(path) {
3706
3828
  return SCAN_ADAPTERS.some((a) => a.isTestFile(path));
@@ -3764,17 +3886,14 @@ const SEARCHED_FOR = [
3764
3886
  * The fix: resolve git ONCE per process to an ABSOLUTE path by walking
3765
3887
  * the PATH directly (never consulting the CWD), verify the candidate is
3766
3888
  * an existing file, and pass that path to execFileSync. Resolution is
3767
- * memoized; a failure to find a real git anywhere on PATH degrades to
3768
- * the plain name (previous behavior) with the resolution error recorded
3769
- * — degraded git data already means full-file attribution, never a
3770
- * crash.
3889
+ * memoized; a failure to find a real git anywhere on PATH is recorded
3890
+ * and callers degrade without invoking a bare name.
3771
3891
  */
3772
3892
  let resolvedGit = void 0;
3773
3893
  /**
3774
3894
  * The absolute path of the git binary Mjölnir will invoke, or null when
3775
- * PATH carries no executable `git` at all (S1 degradation: callers fall
3776
- * back to the bare name and their own try/catch — same honest degrade
3777
- * as before, minus the CWD-hijack surface).
3895
+ * PATH carries no executable `git` at all. Callers must degrade without
3896
+ * invoking a bare name.
3778
3897
  */
3779
3898
  function resolveGitPath() {
3780
3899
  if (resolvedGit !== void 0) return resolvedGit;
@@ -3807,7 +3926,8 @@ function resolveGitPath() {
3807
3926
  * the previous inline `git()` helpers, with the hijack surface closed.
3808
3927
  */
3809
3928
  function runGit(root, args) {
3810
- const exe = resolveGitPath() ?? "git";
3929
+ const exe = resolveGitPath();
3930
+ if (!exe) return null;
3811
3931
  try {
3812
3932
  return execFileSync(exe, [
3813
3933
  "-C",
@@ -3943,7 +4063,7 @@ function computeChangedScope(root, baseBranch) {
3943
4063
  "--",
3944
4064
  ...chunk
3945
4065
  ]);
3946
- if (output === null) continue;
4066
+ if (output === null) return null;
3947
4067
  const sections = output.split("\ndiff --git ");
3948
4068
  for (const section of sections) {
3949
4069
  const body = section.startsWith("diff --git ") ? section : `diff --git ${section}`;
@@ -3971,6 +4091,11 @@ function computeChangedScope(root, baseBranch) {
3971
4091
  "--unified=0",
3972
4092
  "HEAD"
3973
4093
  ], changedFiles);
4094
+ if (committedDiffs === null || workingDiffs === null) return {
4095
+ changed: {},
4096
+ degraded: true,
4097
+ reason: "diff-failed"
4098
+ };
3974
4099
  for (const file of changedFiles) {
3975
4100
  const committedDiff = committedDiffs.get(file);
3976
4101
  const workingDiff = workingDiffs.get(file);
@@ -3996,8 +4121,11 @@ const LIMITS_MAX_LINES = 1e6;
3996
4121
  function allLinesOf(root, file) {
3997
4122
  const full = join(root, file);
3998
4123
  try {
3999
- if (statSync(full).size > LIMITS$1.maxFileBytes) return null;
4000
- const lineCount = readFileSync(full, "utf8").split("\n").length;
4124
+ const stat = lstatSync(full);
4125
+ if (stat.isSymbolicLink() || !stat.isFile()) return null;
4126
+ const read = readFileBounded(full, LIMITS$1.maxFileBytes);
4127
+ if (!read.ok) return null;
4128
+ const lineCount = read.data.toString("utf8").split("\n").length;
4001
4129
  return Array.from({ length: lineCount }, (_, i) => i + 1);
4002
4130
  } catch {
4003
4131
  return null;
@@ -12107,6 +12235,122 @@ function configurableAggregation(results, config) {
12107
12235
  };
12108
12236
  }
12109
12237
  //#endregion
12238
+ //#region src/lib/fs-atomic.ts
12239
+ /**
12240
+ * Atomic file writes (audit S9).
12241
+ *
12242
+ * Every durability-critical write in Mjölnir (baseline, stats, badge,
12243
+ * TRIAGE.md, scaffolded rule files) used to hand-roll
12244
+ * `writeFileSync(path, data)` — a crash mid-write left a TRUNCATED file
12245
+ * at the real path, and a subsequent read (diff, badge endpoint) served
12246
+ * confident nonsense from it.
12247
+ *
12248
+ * `writeFileAtomic` writes to a temp sibling, then RENAMES. On the same
12249
+ * volume rename is atomic: readers see either the complete old file or
12250
+ * the complete new file, never a half-written one. The temp name is
12251
+ * created with `wx` (exclusive) so concurrent writers cannot interleave,
12252
+ * stale temps are cleaned up on failure, and on Windows the rename is
12253
+ * retried briefly because a concurrent reader can hold the destination
12254
+ * open (EBUSY/EPERM).
12255
+ */
12256
+ function atomicTempPath(path) {
12257
+ return `${path}.mjolnir-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.tmp`;
12258
+ }
12259
+ /**
12260
+ * Atomically replace `path` with `data`.
12261
+ */
12262
+ function writeFileAtomic(path, data, opts = {}) {
12263
+ const dir = dirname(path);
12264
+ if (opts.mkdirs !== false && !existsSync(dir)) mkdirSync(dir, { recursive: true });
12265
+ const tmp = atomicTempPath(path);
12266
+ let fd;
12267
+ try {
12268
+ fd = openSync(tmp, "wx", opts.mode ?? 420);
12269
+ writeSync(fd, data, null, opts.encoding ?? "utf8");
12270
+ } finally {
12271
+ if (fd !== void 0) closeSync(fd);
12272
+ }
12273
+ try {
12274
+ renameWithWindowsRetry(tmp, path);
12275
+ } catch (err) {
12276
+ try {
12277
+ if (existsSync(tmp)) unlinkSync(tmp);
12278
+ } catch {}
12279
+ throw err;
12280
+ }
12281
+ }
12282
+ /**
12283
+ * renameSync retry loop for Windows: a concurrent reader (another scan,
12284
+ * a badge endpoint, an editor) holding the destination open makes
12285
+ * rename fail with EBUSY/EPERM. A short bounded retry closes the race
12286
+ * without turning an atomic swap into a partial write.
12287
+ *
12288
+ * Internal contract test hook: the platform check keeps this loop off
12289
+ * the POSIX hot path; on win32 the EBUSY/EPERM arms are exercised by
12290
+ * the fs-atomic-retry spec (mocked renameSync).
12291
+ */
12292
+ const RENAME_RETRIES = 8;
12293
+ const RENAME_RETRY_DELAY_MS = 25;
12294
+ /** Synchronous sleep that does not spin the CPU. */
12295
+ function sleepSync(ms) {
12296
+ try {
12297
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12298
+ } catch {
12299
+ const until = Date.now() + ms;
12300
+ while (Date.now() < until);
12301
+ }
12302
+ }
12303
+ function renameWithWindowsRetry(from, to) {
12304
+ for (let attempt = 0;; attempt++) try {
12305
+ renameSync(from, to);
12306
+ return;
12307
+ } catch (err) {
12308
+ const code = err?.code;
12309
+ if (process.platform === "win32" && (code === "EBUSY" || code === "EPERM") && attempt < RENAME_RETRIES) {
12310
+ sleepSync(RENAME_RETRY_DELAY_MS);
12311
+ continue;
12312
+ }
12313
+ throw err;
12314
+ }
12315
+ }
12316
+ const STALE_TEMP_AGE_MS = 864e5;
12317
+ const TEMP_PID_RE = /^.+\.mjolnir-([1-9]\d{0,9})-(\d{13})-[0-9a-f]{8}\.tmp$/;
12318
+ function sweepStaleTempFiles(dir) {
12319
+ let swept = 0;
12320
+ let entries;
12321
+ try {
12322
+ entries = readdirSync(dir);
12323
+ } catch {
12324
+ return 0;
12325
+ }
12326
+ const cutoff = Date.now() - STALE_TEMP_AGE_MS;
12327
+ for (const entry of entries) {
12328
+ const match = TEMP_PID_RE.exec(entry);
12329
+ if (!match || Number(match[2]) > cutoff) continue;
12330
+ const path = join(dir, entry);
12331
+ try {
12332
+ const stat = lstatSync(path);
12333
+ if (!stat.isFile() || stat.mtimeMs > cutoff) continue;
12334
+ if (pidAlive(Number(match[1]))) continue;
12335
+ unlinkSync(path);
12336
+ swept++;
12337
+ } catch {
12338
+ continue;
12339
+ }
12340
+ }
12341
+ return swept;
12342
+ }
12343
+ function pidAlive(pid) {
12344
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 2147483647) return true;
12345
+ if (pid === process.pid) return true;
12346
+ try {
12347
+ process.kill(pid, 0);
12348
+ return true;
12349
+ } catch (err) {
12350
+ return err?.code !== "ESRCH";
12351
+ }
12352
+ }
12353
+ //#endregion
12110
12354
  //#region src/engine/scan-cache.ts
12111
12355
  /**
12112
12356
  * Local incremental scan cache (Beta-to-Stable 1.0 plan, M5.2 / A-2).
@@ -12142,6 +12386,17 @@ const MAX_ENTRIES$1 = 4096;
12142
12386
  * serialized size; the newest entries win (real LRU-by-use).
12143
12387
  */
12144
12388
  const MAX_TOTAL_BYTES = 33554432;
12389
+ const MAX_FINDINGS_PER_ENTRY = 1e4;
12390
+ function isCachedFinding(value) {
12391
+ if (!isRecord(value)) return false;
12392
+ return typeof value["ruleId"] === "string" && typeof value["category"] === "string" && typeof value["severity"] === "string" && typeof value["confidence"] === "string" && typeof value["findingType"] === "string" && typeof value["file"] === "string" && Number.isSafeInteger(value["line"]) && Number.isSafeInteger(value["column"]) && typeof value["message"] === "string" && typeof value["why"] === "string" && typeof value["fix"] === "string";
12393
+ }
12394
+ function isCacheFile(value) {
12395
+ if (!isRecord(value) || value["version"] !== CACHE_VERSION) return false;
12396
+ const entries = value["entries"];
12397
+ if (!isRecord(entries) || Object.keys(entries).length > MAX_ENTRIES$1) return false;
12398
+ return Object.values(entries).every((entry) => isRecord(entry) && Array.isArray(entry["findings"]) && entry["findings"].length <= MAX_FINDINGS_PER_ENTRY && entry["findings"].every(isCachedFinding));
12399
+ }
12145
12400
  /** sha256 hex of a string — the only hash this module needs. */
12146
12401
  function sha256(text) {
12147
12402
  return createHash("sha256").update(text).digest("hex");
@@ -12240,12 +12495,15 @@ function createScanCache(root) {
12240
12495
  let totalBytes = 0;
12241
12496
  try {
12242
12497
  if (existsSync(file)) {
12243
- entries = parseJsonFile(readFileSync(file, "utf8"), file, (v) => isRecord(v) && v["version"] === CACHE_VERSION && isRecord(v["entries"])).entries;
12498
+ const stat = lstatSync(file);
12499
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_TOTAL_BYTES) throw new Error("invalid cache file");
12500
+ entries = parseJsonFile(readFileSync(file, "utf8"), file, isCacheFile).entries;
12244
12501
  for (const [k, v] of Object.entries(entries)) {
12245
- const size = JSON.stringify(v).length + k.length + 4;
12502
+ const size = Buffer.byteLength(JSON.stringify(v), "utf8") + k.length + 4;
12246
12503
  entryBytes.set(k, size);
12247
12504
  totalBytes += size;
12248
12505
  }
12506
+ if (totalBytes > MAX_TOTAL_BYTES) throw new Error("cache byte budget exceeded");
12249
12507
  }
12250
12508
  } catch {
12251
12509
  entries = {};
@@ -12271,11 +12529,13 @@ function createScanCache(root) {
12271
12529
  },
12272
12530
  store(key, findings, fileBudgetExceeded) {
12273
12531
  if (fileBudgetExceeded) return;
12532
+ if (findings.length > MAX_FINDINGS_PER_ENTRY) return;
12533
+ const entryJson = JSON.stringify(findings);
12534
+ const newBytes = Buffer.byteLength(entryJson, "utf8") + key.length + 4;
12535
+ if (newBytes > MAX_TOTAL_BYTES) return;
12274
12536
  const replacedBytes = entryBytes.get(key) ?? 0;
12275
12537
  delete entries[key];
12276
- const entryJson = JSON.stringify(findings);
12277
12538
  entries[key] = { findings: structuredClone(findings) };
12278
- const newBytes = entryJson.length + key.length + 4;
12279
12539
  entryBytes.set(key, newBytes);
12280
12540
  totalBytes = totalBytes - replacedBytes + newBytes;
12281
12541
  let count = Object.keys(entries).length;
@@ -12292,10 +12552,10 @@ function createScanCache(root) {
12292
12552
  if (!dirty) return;
12293
12553
  try {
12294
12554
  mkdirSync(dir, { recursive: true });
12295
- writeFileSync(file, JSON.stringify({
12555
+ writeFileAtomic(file, JSON.stringify({
12296
12556
  version: CACHE_VERSION,
12297
12557
  entries
12298
- }), "utf8");
12558
+ }), { encoding: "utf8" });
12299
12559
  } catch {}
12300
12560
  }
12301
12561
  };
@@ -14276,6 +14536,31 @@ function loadPlugins(root, gateOpen = false) {
14276
14536
  * next catalog render (locked by tests/local-rules.spec.ts).
14277
14537
  */
14278
14538
  const LOCAL_RULES_DIR = "mjolnir-rules";
14539
+ const MAX_EXTERNAL_RULES = 1e3;
14540
+ const MAX_RULE_FILE_BYTES = 1048576;
14541
+ const MAX_PATTERNS_PER_RULE = 100;
14542
+ function hasNestedQuantifier(pattern) {
14543
+ for (let i = 0; i < pattern.length; i++) {
14544
+ if (pattern.charAt(i) !== "(") continue;
14545
+ let bodyHasQuantifier = false;
14546
+ let j = i + 1;
14547
+ for (; j < pattern.length && pattern.charAt(j) !== ")"; j++) {
14548
+ const bodyChar = pattern.charAt(j);
14549
+ if (bodyChar === "+" || bodyChar === "*" || bodyChar === "?") {
14550
+ bodyHasQuantifier = true;
14551
+ break;
14552
+ }
14553
+ }
14554
+ if (bodyHasQuantifier) {
14555
+ while (j < pattern.length && pattern.charAt(j) !== ")") j++;
14556
+ if (j + 1 < pattern.length) {
14557
+ const next = pattern.charAt(j + 1);
14558
+ if (next === "+" || next === "*" || next === "{") return true;
14559
+ }
14560
+ }
14561
+ }
14562
+ return false;
14563
+ }
14279
14564
  const ALLOWED_CATEGORIES = /* @__PURE__ */ new Set([
14280
14565
  "QA-TEST",
14281
14566
  "QA-TQUAL",
@@ -14305,7 +14590,7 @@ const ALLOWED_QA_IMPACTS = /* @__PURE__ */ new Set([
14305
14590
  * Missing directory → empty result (not an error — most workspaces
14306
14591
  * carry none).
14307
14592
  */
14308
- async function loadLocalRules(root, gateOpen = true) {
14593
+ async function loadLocalRules(root, gateOpen = false) {
14309
14594
  const result = {
14310
14595
  rules: [],
14311
14596
  errors: [],
@@ -14313,6 +14598,15 @@ async function loadLocalRules(root, gateOpen = true) {
14313
14598
  };
14314
14599
  const dir = join(root, LOCAL_RULES_DIR);
14315
14600
  if (!existsSync(dir)) return result;
14601
+ try {
14602
+ if (lstatSync(dir).isSymbolicLink()) {
14603
+ result.errors.push(`external rules directory "${LOCAL_RULES_DIR}/" is a symlink — skipped`);
14604
+ return result;
14605
+ }
14606
+ } catch (err) {
14607
+ result.errors.push(`external rules directory "${LOCAL_RULES_DIR}/" could not be inspected: ${err instanceof Error ? err.message : String(err)}`);
14608
+ return result;
14609
+ }
14316
14610
  let entries;
14317
14611
  try {
14318
14612
  entries = readdirSync(dir);
@@ -14321,7 +14615,20 @@ async function loadLocalRules(root, gateOpen = true) {
14321
14615
  return result;
14322
14616
  }
14323
14617
  for (const entry of entries.sort()) {
14618
+ if (result.rules.length >= MAX_EXTERNAL_RULES) {
14619
+ result.errors.push(`external rule budget exceeded (${MAX_EXTERNAL_RULES})`);
14620
+ break;
14621
+ }
14324
14622
  const path = join(dir, entry);
14623
+ try {
14624
+ if (lstatSync(path).isSymbolicLink()) {
14625
+ result.errors.push(`external rule "${LOCAL_RULES_DIR}/${entry}" is a symlink — skipped`);
14626
+ continue;
14627
+ }
14628
+ } catch (err) {
14629
+ result.errors.push(`external rule "${LOCAL_RULES_DIR}/${entry}" could not be inspected: ${err instanceof Error ? err.message : String(err)}`);
14630
+ continue;
14631
+ }
14325
14632
  if (entry.endsWith(".json")) loadJsonRule(path, result);
14326
14633
  else if (entry.endsWith(".mjs") || entry.endsWith(".js")) {
14327
14634
  if (!gateOpen) result.skipped.push(`${LOCAL_RULES_DIR}/${entry}`);
@@ -14334,6 +14641,10 @@ function loadJsonRule(path, result) {
14334
14641
  const name = `${LOCAL_RULES_DIR}/${path.split(/[\\/]/).pop()}`;
14335
14642
  let raw;
14336
14643
  try {
14644
+ if (lstatSync(path).size > MAX_RULE_FILE_BYTES) {
14645
+ result.errors.push(`external rule "${name}" exceeds the file size budget`);
14646
+ return;
14647
+ }
14337
14648
  raw = JSON.parse(readFileSync(path, "utf8"));
14338
14649
  } catch (err) {
14339
14650
  result.errors.push(`external rule "${name}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
@@ -14354,7 +14665,7 @@ function loadJsonRule(path, result) {
14354
14665
  return;
14355
14666
  }
14356
14667
  const patterns = decl["patterns"];
14357
- if (!Array.isArray(patterns) || patterns.length === 0 || !patterns.every((p) => typeof p === "string" && p.length > 0)) {
14668
+ if (!Array.isArray(patterns) || patterns.length === 0 || patterns.length > MAX_PATTERNS_PER_RULE || !patterns.every((p) => typeof p === "string" && p.length > 0)) {
14358
14669
  result.errors.push(`external rule ${id} must declare a non-empty "patterns" array of regex source strings.`);
14359
14670
  return;
14360
14671
  }
@@ -14370,6 +14681,10 @@ function loadJsonRule(path, result) {
14370
14681
  result.errors.push(`external rule ${id} has a pattern longer than 512 chars — likely a copy-paste error.`);
14371
14682
  return;
14372
14683
  }
14684
+ if (hasNestedQuantifier(p)) {
14685
+ result.errors.push(`external rule ${id} has a nested quantifier that may cause catastrophic backtracking`);
14686
+ return;
14687
+ }
14373
14688
  const quantifiers = p.match(/[+*?]|\{\d/g);
14374
14689
  if (quantifiers && quantifiers.length > 64) {
14375
14690
  result.errors.push(`external rule ${id} has a pattern with more than 64 quantifier/wildcard tokens — likely catastrophic backtracking.`);
@@ -14567,6 +14882,8 @@ var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
14567
14882
  summarizeForensicVerdicts: () => summarizeForensicVerdicts
14568
14883
  });
14569
14884
  const UNIVERSAL_RULES = RULES.map(asUniversal);
14885
+ const DEFAULT_MAX_DURATION_MS = 6e5;
14886
+ const MAX_DURATION_MS = 36e5;
14570
14887
  /** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
14571
14888
  const KNOWN_RULE_IDS = new Set(RULES.map((r) => r.id));
14572
14889
  /**
@@ -14783,7 +15100,9 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14783
15100
  let testDeclarationCount = 0;
14784
15101
  let rulesPartial = false;
14785
15102
  let parseFailed = 0;
15103
+ let parseFallbacks = 0;
14786
15104
  let scanned = 0;
15105
+ let analyzed = 0;
14787
15106
  for (const path of testFiles) {
14788
15107
  if (Date.now() > deadline) {
14789
15108
  rulesPartial = true;
@@ -14797,7 +15116,12 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14797
15116
  if (!isCiAdapter) testFileCount++;
14798
15117
  let text;
14799
15118
  try {
14800
- text = readFileSync(path, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
15119
+ const readResult = readFileBounded(path, LIMITS$1.maxFileBytes);
15120
+ if (!readResult.ok) {
15121
+ skippedFiles++;
15122
+ continue;
15123
+ }
15124
+ text = readResult.data.toString("utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
14801
15125
  } catch {
14802
15126
  skippedFiles++;
14803
15127
  continue;
@@ -14831,6 +15155,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14831
15155
  const cachedFindings = cache.lookup(cacheKey);
14832
15156
  if (cachedFindings) {
14833
15157
  for (const f of cachedFindings) findings.push(f);
15158
+ analyzed++;
14834
15159
  continue;
14835
15160
  }
14836
15161
  hooks.onProgress?.({
@@ -14857,10 +15182,12 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14857
15182
  }
14858
15183
  const actualMode = parsed ? "ast" : "regex";
14859
15184
  if (wantsAst && actualMode === "regex") {
15185
+ parseFallbacks++;
14860
15186
  cacheKey = fileCacheKey(rulesDigest, text, identity(actualMode));
14861
15187
  const fallbackFindings = cache.lookup(cacheKey);
14862
15188
  if (fallbackFindings) {
14863
15189
  for (const f of fallbackFindings) findings.push(f);
15190
+ analyzed++;
14864
15191
  continue;
14865
15192
  }
14866
15193
  }
@@ -14891,8 +15218,10 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14891
15218
  fileBudgetExceeded = true;
14892
15219
  }
14893
15220
  });
15221
+ if (!fileRuleFailed && !fileBudgetExceeded) analyzed++;
14894
15222
  if (!fileRuleFailed) cache.store(cacheKey, findings.slice(findingsStart), fileBudgetExceeded);
14895
15223
  } catch {
15224
+ if (wantsAst) parseFallbacks++;
14896
15225
  skippedFiles++;
14897
15226
  parseFailed++;
14898
15227
  } finally {
@@ -14905,7 +15234,9 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
14905
15234
  testDeclarationCount,
14906
15235
  rulesPartial,
14907
15236
  parseFailed,
14908
- scanned
15237
+ parseFallbacks,
15238
+ scanned,
15239
+ analyzed
14909
15240
  };
14910
15241
  }
14911
15242
  /**
@@ -14983,6 +15314,7 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
14983
15314
  for (const f of findings) f.fixGroupId = f.ruleId;
14984
15315
  const discoveredReport = discoverAndParseRuntimeReport(scanRoot.root);
14985
15316
  const runtimeReportPath = discoveredReport?.path;
15317
+ const runtimeIncomplete = discoveredReport !== void 0 && discoveredReport.report.analysisComplete !== true;
14986
15318
  let forensicVerdicts;
14987
15319
  if (discoveredReport && discoveredReport.report.analysisComplete === true) try {
14988
15320
  buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
@@ -14995,6 +15327,7 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
14995
15327
  suppressionCount,
14996
15328
  frameworks,
14997
15329
  runtimeReportPath,
15330
+ runtimeIncomplete,
14998
15331
  forensicVerdicts,
14999
15332
  config
15000
15333
  };
@@ -15063,10 +15396,13 @@ function assembleScanResult(o) {
15063
15396
  if (o.scopeIgnored > 0) scopeReasons.push(`ignored:${o.scopeIgnored}`);
15064
15397
  if (o.scopeUnrecognized > 0) scopeReasons.push(`unrecognized:${o.scopeUnrecognized}`);
15065
15398
  if (o.parseFailed > 0) scopeReasons.push(`parseFailed:${o.parseFailed}`);
15399
+ if (o.skippedFiles > 0) scopeReasons.push(`skipped:${o.skippedFiles}`);
15400
+ if (o.scopeInfo.degraded) scopeReasons.push(`degraded:${o.scopeInfo.degraded}`);
15401
+ if (o.runtimeIncomplete) scopeReasons.push("runtime-incomplete");
15066
15402
  for (const reason of o.truncationReasons) scopeReasons.push(`truncated:${reason}`);
15067
15403
  const scopeIntegrity = {
15068
15404
  discovered: o.testFiles.length,
15069
- analyzed: Math.max(0, o.scanned),
15405
+ analyzed: Math.max(0, o.analyzed ?? o.scanned),
15070
15406
  ignored: o.scopeIgnored,
15071
15407
  unrecognized: o.scopeUnrecognized,
15072
15408
  parseFailed: o.parseFailed,
@@ -15075,28 +15411,59 @@ function assembleScanResult(o) {
15075
15411
  ...scopeReasons.length > 0 ? { reasons: scopeReasons } : {}
15076
15412
  };
15077
15413
  const scopeAdjustedTotal = scopeReasons.length > 0 && total >= 100 ? 99 : total;
15414
+ let identityIncomplete = false;
15078
15415
  const inputSnapshot = o.testFiles.map((p) => {
15079
15416
  const relPath = relative(o.workspace.root, p).replaceAll("\\", "/");
15080
15417
  try {
15081
- const content = readFileSync(p);
15082
- const hash = createHash("sha256").update(content).digest("hex").slice(0, 16);
15418
+ const read = readFileBounded(resolve(o.workspace.root, p), LIMITS$1.maxFileBytes);
15419
+ if (!read.ok) {
15420
+ if (existsSync(o.workspace.root)) identityIncomplete = true;
15421
+ return {
15422
+ path: relPath,
15423
+ size: 0,
15424
+ hash: "UNAVAILABLE"
15425
+ };
15426
+ }
15427
+ const hash = createHash("sha256").update(read.data).digest("hex");
15083
15428
  return {
15084
15429
  path: relPath,
15085
- size: content.length,
15430
+ size: read.data.length,
15086
15431
  hash
15087
15432
  };
15088
15433
  } catch {
15434
+ if (existsSync(o.workspace.root)) identityIncomplete = true;
15089
15435
  return {
15090
15436
  path: relPath,
15091
- size: 0
15437
+ size: 0,
15438
+ hash: "UNAVAILABLE"
15092
15439
  };
15093
15440
  }
15094
15441
  });
15095
15442
  let reportDigest;
15096
15443
  if (o.runtimeReportPath) try {
15097
- const reportBytes = readFileSync(o.runtimeReportPath);
15098
- reportDigest = createHash("sha256").update(reportBytes).digest("hex").slice(0, 16);
15099
- } catch {}
15444
+ const reportPath = resolve(o.workspace.root, o.runtimeReportPath);
15445
+ if (lstatSync(reportPath).isFile()) {
15446
+ const read = readFileBounded(reportPath, LIMITS$1.maxFileBytes);
15447
+ if (!read.ok) identityIncomplete = true;
15448
+ else reportDigest = createHash("sha256").update(read.data).digest("hex");
15449
+ }
15450
+ } catch {
15451
+ identityIncomplete = true;
15452
+ }
15453
+ const completion = deriveCompletion({
15454
+ discoveryTruncated: o.discoveryTruncated,
15455
+ rulesPartial: o.rulesPartial,
15456
+ skippedFiles: o.skippedFiles,
15457
+ rulesCrashed: o.rulesCrashed,
15458
+ truncationReasons: o.truncationReasons,
15459
+ scopeIgnored: o.scopeIgnored,
15460
+ scopeUnrecognized: o.scopeUnrecognized,
15461
+ parseFailed: o.parseFailed,
15462
+ parseFallbacks: o.parseFallbacks ?? 0,
15463
+ ...o.scopeInfo.degraded !== void 0 ? { scopeDegraded: o.scopeInfo.degraded } : {},
15464
+ ...o.runtimeIncomplete !== void 0 ? { runtimeIncomplete: o.runtimeIncomplete } : {},
15465
+ identityIncomplete
15466
+ });
15100
15467
  const runIdentity = buildRunIdentity({
15101
15468
  files: inputSnapshot,
15102
15469
  rules: [...o.REVISION_BY_RULE_ID.entries()].map(([id, detectorRevision]) => ({
@@ -15114,13 +15481,14 @@ function assembleScanResult(o) {
15114
15481
  const evidenceGraph = buildEvidenceGraph({ runId: runIdentity });
15115
15482
  const hasTests = o.testFileCount > 0 && o.testDeclarationCount > 0;
15116
15483
  const suiteInvalidatedBy = [...new Set(o.findings.filter((f) => SUITE_INVALIDATING_RULE_IDS.has(f.ruleId)).map((f) => f.ruleId))].sort();
15484
+ const finalScore = hasTests ? completion.partial && scopeAdjustedTotal >= 100 ? 99 : scopeAdjustedTotal : null;
15117
15485
  const result = {
15118
15486
  schemaVersion: 1,
15119
- partial: o.discoveryTruncated || o.rulesPartial || o.skippedFiles > 0 || o.rulesCrashed > 0 || o.scopeInfo.degraded !== void 0,
15487
+ partial: completion.partial,
15120
15488
  scopeIntegrity,
15121
15489
  runIdentity,
15122
15490
  evidenceGraph,
15123
- score: hasTests ? scopeAdjustedTotal : null,
15491
+ score: finalScore,
15124
15492
  ...hasTests ? {} : { reason: "no-tests-found" },
15125
15493
  frameworks: o.frameworks.frameworks,
15126
15494
  frameworkDetectionUnknown: o.frameworks.unknown,
@@ -15146,12 +15514,8 @@ function assembleScanResult(o) {
15146
15514
  file: o.cache.stats.file
15147
15515
  } } : {},
15148
15516
  analysisStatus: {
15149
- discovery: o.discoveryTruncated ? "partial" : "complete",
15150
- rules: o.rulesPartial ? "partial" : "complete",
15151
- skippedFiles: o.skippedFiles,
15152
- durationMs: elapsed,
15153
- rulesCrashed: o.rulesCrashed,
15154
- ...o.truncationReasons.size > 0 ? { truncationReasons: [...o.truncationReasons].sort() } : {}
15517
+ ...completion.analysisStatus,
15518
+ durationMs: elapsed
15155
15519
  },
15156
15520
  scoringModelVersion: SCORING_MODEL_VERSION
15157
15521
  };
@@ -15199,7 +15563,8 @@ function assembleScanResult(o) {
15199
15563
  */
15200
15564
  async function runScan(args, hooks = {}) {
15201
15565
  const started = Date.now();
15202
- const deadline = started + args.maxDurationMs;
15566
+ const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS;
15567
+ const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS);
15203
15568
  const discovered = discoverWorkspace(args.target);
15204
15569
  const targetAbs = resolve(args.target);
15205
15570
  const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
@@ -15288,7 +15653,9 @@ async function runScan(args, hooks = {}) {
15288
15653
  testDeclarationCount = analysis.testDeclarationCount;
15289
15654
  rulesPartial = analysis.rulesPartial;
15290
15655
  parseFailed = analysis.parseFailed;
15656
+ const parseFallbacks = analysis.parseFallbacks;
15291
15657
  const scanned = analysis.scanned;
15658
+ const analyzed = analysis.analyzed;
15292
15659
  const postScan = applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, declarationsByFile, testDeclarationCount, tierByRuleId, REVISION_BY_RULE_ID);
15293
15660
  testDeclarationCount = postScan.testDeclarationCount;
15294
15661
  const result = assembleScanResult({
@@ -15304,7 +15671,9 @@ async function runScan(args, hooks = {}) {
15304
15671
  scopeIgnored,
15305
15672
  scopeUnrecognized,
15306
15673
  parseFailed,
15674
+ parseFallbacks,
15307
15675
  scanned,
15676
+ analyzed,
15308
15677
  testFiles,
15309
15678
  workspace,
15310
15679
  scanRoot,
@@ -15317,6 +15686,7 @@ async function runScan(args, hooks = {}) {
15317
15686
  suppressionCount: postScan.suppressionCount,
15318
15687
  frameworks: postScan.frameworks,
15319
15688
  runtimeReportPath: postScan.runtimeReportPath,
15689
+ runtimeIncomplete: postScan.runtimeIncomplete,
15320
15690
  forensicVerdicts: postScan.forensicVerdicts,
15321
15691
  config: postScan.config,
15322
15692
  fileProvenance,
@@ -15329,4 +15699,4 @@ async function runScan(args, hooks = {}) {
15329
15699
  return result;
15330
15700
  }
15331
15701
  //#endregion
15332
- export { massCeiling as $, box as A, isAdvisoryFinding as At, headlineFor as B, buildFooter as C, buildEvidenceGraph as Ct, plainContext as D, RULE_CATEGORIES as Dt, panel as E, QA_IMPACT_LABELS as Et, scoreGauge as F, buildDependencyGraph as G, EVIDENCE as H, shouldColorize as I, classifyProvenance as J, getReachableFiles as K, shouldUseAscii as L, padTo as M, palette as N, sectionHeader as O, SEVERITY_ORDER as Ot, sanitizeData as P, deductionFor as Q, wrapText as R, FLAKE_GLYPH as S, ENGINE_VERSION as St, okIcon as T, DEDUCTIONS as Tt, TINT as U, BADGE_BAND as V, TRUST as W, capForTier as X, computeAgenticProfile as Y, computeDimensions as Z, summarizeForensicVerdicts as _, ConfigValidationError as _t, applyPostScanProcessing as a, SEARCHED_FOR as at, renderSuppressions as b, isRecord as bt, discoverAndParseRuntimeReport as c, parseWorkflow as ct, isValidFindingRecord as d, parseTsFile as dt, RETIRED_RULE_IDS as et, pathMatchesGlob as f, detectFrameworks as ft, selectAdapter as g, isLintFixtureDir as gt, scan_pipeline_exports as h, globToRegExp as ht, SUITE_INVALIDATING_RULE_IDS as i, SCAN_ADAPTERS as it, measure as j, isValidCategory as jt, severityIcon as k, deriveEvidenceLevel as kt, discoverTestFilesPhase as l, parseYamlGuarded as lt, runScan as m, createIgnoreMatcher as mt, KNOWN_RULE_IDS as n, getRule as nt, assembleScanResult as o, isAzurePipelineFixture as ot, runFileAnalysisPhase as p, DEFAULT_IGNORE_MATCHER as pt, correlateFindings as q, OVERLAP_META_BY_RULE_ID as r, resolveGitPath as rt, buildUniversalRules as s, parseAzurePipeline as st, EVIDENCE_OVERRIDES as t, RULES as tt, fallbackWorkspace as u, computeCodeText as ut, loadLocalRules as v, isSuppressionActive as vt, nextStep as w, MEASURED_FP as wt, runForensics as x, parseJsonFile as xt, loadSuppressions as y, loadConfig as yt, deriveScoreState as z };
15702
+ export { correlateFindings as $, plainContext as A, parseJsonFile as At, shouldUseAscii as B, isValidCategory as Bt, runForensics as C, globToRegExp as Ct, nextStep as D, loadConfig as Dt, buildFooter as E, isSuppressionActive as Et, padTo as F, QA_IMPACT_LABELS as Ft, EVIDENCE as G, deriveScoreState as H, palette as I, RULE_CATEGORIES as It, atomicTempPath as J, TINT as K, sanitizeData as L, SEVERITY_ORDER as Lt, severityIcon as M, buildEvidenceGraph as Mt, box as N, MEASURED_FP as Nt, okIcon as O, readFileBounded as Ot, measure as P, DEDUCTIONS as Pt, getReachableFiles as Q, scoreGauge as R, deriveEvidenceLevel as Rt, renderSuppressions as S, createIgnoreMatcher as St, FLAKE_GLYPH as T, ConfigValidationError as Tt, headlineFor as U, wrapText as V, BADGE_BAND as W, writeFileAtomic as X, sweepStaleTempFiles as Y, buildDependencyGraph as Z, summarizeForensicVerdicts as _, computeCodeText as _t, applyPostScanProcessing as a, massCeiling as at, loadLocalRules as b, DEFAULT_IGNORE_MATCHER as bt, discoverAndParseRuntimeReport as c, getRule as ct, isValidFindingRecord as d, SCAN_ADAPTERS as dt, classifyProvenance as et, pathMatchesGlob as f, SEARCHED_FOR as ft, selectAdapter as g, parseYamlGuarded as gt, scan_pipeline_exports as h, parseWorkflow as ht, SUITE_INVALIDATING_RULE_IDS as i, deductionFor as it, sectionHeader as j, ENGINE_VERSION as jt, panel as k, isRecord as kt, discoverTestFilesPhase as l, resolveGitPath as lt, runScan as m, parseAzurePipeline as mt, KNOWN_RULE_IDS as n, capForTier as nt, assembleScanResult as o, RETIRED_RULE_IDS as ot, runFileAnalysisPhase as p, isAzurePipelineFixture as pt, TRUST as q, OVERLAP_META_BY_RULE_ID as r, computeDimensions as rt, buildUniversalRules as s, RULES as st, EVIDENCE_OVERRIDES as t, computeAgenticProfile as tt, fallbackWorkspace as u, runGit as ut, pluginsGateOpen as v, parseTsFile as vt, sanitizeErrorText as w, isLintFixtureDir as wt, loadSuppressions as x, LIMITS$1 as xt, renderGateNotice as y, detectFrameworks as yt, shouldColorize as z, isAdvisoryFinding as zt };