knodin 0.7.5 → 0.8.2

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.
Files changed (75) hide show
  1. package/README.md +18 -3
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +371 -66
  4. package/dist/bin/launcher.js +16 -1
  5. package/dist/src/agent-integration.js +82 -16
  6. package/dist/src/artifact-refresh.js +2 -1
  7. package/dist/src/cli-args.js +19 -1
  8. package/dist/src/cli-model.js +28 -2
  9. package/dist/src/codeflow-replay.js +2 -1
  10. package/dist/src/compare.js +39 -0
  11. package/dist/src/competitive-constraints.js +2 -1
  12. package/dist/src/competitive-runner.js +4 -4
  13. package/dist/src/context-export.js +3 -2
  14. package/dist/src/context.js +1 -1
  15. package/dist/src/deterministic-random.js +34 -0
  16. package/dist/src/diagnostics-write-helper.js +473 -0
  17. package/dist/src/diagnostics.js +1160 -133
  18. package/dist/src/doctor.js +3 -1
  19. package/dist/src/engine/ann-hnsw.js +2 -12
  20. package/dist/src/engine/file-walker.js +8 -2
  21. package/dist/src/engine/git-history.js +12 -12
  22. package/dist/src/engine/index.js +1174 -313
  23. package/dist/src/engine/sarif-import.js +341 -0
  24. package/dist/src/engine/scip-import.js +28 -13
  25. package/dist/src/engine/source-policy.js +16 -0
  26. package/dist/src/engine/state-paths.js +175 -0
  27. package/dist/src/execution-profile.js +15 -10
  28. package/dist/src/failure-diagnosis.js +7 -1
  29. package/dist/src/graph-layout.js +173 -0
  30. package/dist/src/index-activity.js +2 -1
  31. package/dist/src/init.js +86 -45
  32. package/dist/src/lifecycle-health.js +41 -9
  33. package/dist/src/mcp-graph-worker.js +69 -0
  34. package/dist/src/mcp-reliability.js +154 -0
  35. package/dist/src/mcp-worker-supervisor.js +350 -0
  36. package/dist/src/mirror.js +290 -0
  37. package/dist/src/node-runtime.js +157 -0
  38. package/dist/src/output-compression.js +2 -1
  39. package/dist/src/output-telemetry.js +16 -11
  40. package/dist/src/progressive-evidence.js +30 -26
  41. package/dist/src/pure-compression-cli.js +4 -3
  42. package/dist/src/relationship-adapters.js +15 -8
  43. package/dist/src/release-preflight.js +13 -10
  44. package/dist/src/repair-lease.js +85 -0
  45. package/dist/src/repository-init-process.js +13 -9
  46. package/dist/src/repository-management.js +34 -4
  47. package/dist/src/response-budget.js +8 -6
  48. package/dist/src/server.js +80 -35
  49. package/dist/src/structural-fast-path.js +16 -10
  50. package/dist/src/structural-snapshot.js +6 -2
  51. package/dist/src/system-config.js +25 -2
  52. package/dist/src/tools/knodin-tools.js +142 -31
  53. package/dist/src/update-ceremony.js +9 -5
  54. package/dist/src/update-trust.js +5 -4
  55. package/dist/src/visualization.js +372 -19
  56. package/dist/src/worktree-lifecycle.js +5 -2
  57. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  58. package/docs/CLI.md +20 -1
  59. package/docs/COMPARISON.md +403 -0
  60. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  61. package/docs/DIAGNOSTICS.md +46 -11
  62. package/docs/HANDOFF.md +180 -0
  63. package/docs/INSTALLATION.md +21 -2
  64. package/docs/MCP.md +59 -8
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
  66. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  67. package/docs/SCIP-IMPORT.md +5 -0
  68. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  69. package/docs/releases/0.5.1.md +4 -4
  70. package/docs/releases/0.8.0.md +74 -0
  71. package/docs/releases/0.8.2.md +34 -0
  72. package/package.json +17 -4
  73. package/roadmap/competitive-roadmap.md +3801 -0
  74. package/schemas/release-attestation-v1.schema.json +1 -1
  75. package/schemas/support-bundle-v2.schema.json +212 -0
@@ -18,8 +18,10 @@ import path from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import chokidar from "chokidar";
20
20
  import Parser from "web-tree-sitter";
21
+ import { compareBytes } from "../compare.js";
21
22
  import { readIndexActivity } from "../index-activity.js";
22
23
  import { runReadOnlyLspQuery } from "../lsp-readonly.js";
24
+ import { acquireRepairLease } from "../repair-lease.js";
23
25
  import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snapshot.js";
24
26
  import { KNODIN_VERSION } from "../version.js";
25
27
  import * as ann from "./ann-hnsw.js";
@@ -28,9 +30,11 @@ import { walkRepoFiles } from "./file-walker.js";
28
30
  import { clearGitHistorySignalCache, collectGitHistorySignals, } from "./git-history.js";
29
31
  import { beginPerfPhase, measurePerfPhase, measurePerfPhaseSync } from "./perf.js";
30
32
  import { isIndexablePath, makeWatchIgnorePredicate } from "./prune.js";
33
+ import { readSarifLog, SARIF_DEFAULT_LIMITS, } from "./sarif-import.js";
31
34
  import { readScipIndex, SCIP_DEFAULT_LIMITS, } from "./scip-import.js";
32
35
  import { isIndexableSourcePath } from "./source-policy.js";
33
36
  import { Database } from "./sqlite.js";
37
+ import { lookupMirror, mayWriteToRepository, resolveDbPath, resolveStateDir, } from "./state-paths.js";
34
38
  import { deleteAllSymbols, deleteSymbolsForFile, deleteSymbolsMatchingPath, ORPHANED_EMBEDDING_PREDICATE, purgeOrphanEmbeddings, } from "./symbol-delete.js";
35
39
  // ES Module resolution
36
40
  const __filename = fileURLToPath(import.meta.url);
@@ -69,7 +73,7 @@ export const REPO_WIDE_QUERY_PATTERNS = [
69
73
  "api_contract_mismatches",
70
74
  ];
71
75
  const unquote = (value) => {
72
- const match = value.trim().match(/^(["'`])([\s\S]*?)\1$/);
76
+ const match = /^(["'`])([\s\S]*?)\1$/.exec(value.trim());
73
77
  return match && !(match[1] === "\x60" && match[2].includes("${")) ? match[2] : undefined;
74
78
  };
75
79
  function isExecutablePosition(source, target) {
@@ -106,7 +110,7 @@ function isExecutablePosition(source, target) {
106
110
  blockComment = true;
107
111
  i++;
108
112
  }
109
- else if (char === '"' || char === "'" || char.charCodeAt(0) === 96)
113
+ else if (char === '"' || char === "'" || char.codePointAt(0) === 96)
110
114
  quote = char;
111
115
  }
112
116
  return !quote && !lineComment && !blockComment;
@@ -139,11 +143,94 @@ function splitCallArguments(value) {
139
143
  result.push(value.slice(start).trim());
140
144
  return result;
141
145
  }
146
+ /**
147
+ * Masks block comments, replacing each with `mask(comment)`.
148
+ *
149
+ * This deliberately does not use a regular expression. The pattern it replaces,
150
+ * `/\/\*[\s\S]*?\*\//g`, is quadratic on source containing many unclosed `/*`:
151
+ * a global regex restarts at every position where `/*` could begin, and each
152
+ * attempt lazily expands to end of input hunting a `*​/` that never arrives.
153
+ * Measured at ~4x cost per doubling of input, roughly 50s on a 1 MB file.
154
+ *
155
+ * The obvious regex repair — the "unrolled loop" form
156
+ * `/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g` — was measured and is *also* quadratic,
157
+ * and about 12x slower again, because its trailing group backtracks through the
158
+ * whole input before failing. Reasoning about regex-engine behaviour was how
159
+ * this bug survived; indexOf advances monotonically, so linearity here is a
160
+ * property of the code rather than a claim about the engine.
161
+ *
162
+ * Behaviour matches the pattern it replaces, including leaving an unterminated
163
+ * `/*` and everything after it untouched.
164
+ */
165
+ function maskBlockComments(content, mask) {
166
+ let open = content.indexOf("/*");
167
+ if (open === -1)
168
+ return content;
169
+ let out = "";
170
+ let pos = 0;
171
+ while (open !== -1) {
172
+ const close = content.indexOf("*/", open + 2);
173
+ // Nothing closes after this point, so no later `/*` can match either.
174
+ if (close === -1)
175
+ break;
176
+ out += content.slice(pos, open) + mask(content.slice(open, close + 2));
177
+ pos = close + 2;
178
+ open = content.indexOf("/*", pos);
179
+ }
180
+ return pos === 0 ? content : out + content.slice(pos);
181
+ }
182
+ /** Earliest of two `indexOf` results, or -1 when neither matched. */
183
+ function earliestIndex(a, b) {
184
+ if (a === -1)
185
+ return b;
186
+ if (b === -1)
187
+ return a;
188
+ return Math.min(a, b);
189
+ }
190
+ /**
191
+ * Removes block and line comments in one left-to-right pass.
192
+ *
193
+ * Kept as a single pass rather than two because the alternation this replaces,
194
+ * `/\/\*[\s\S]*?\*\/|\/\/.*$/gm`, prefers a block comment only where one starts
195
+ * at the current position. Stripping block comments first and line comments
196
+ * second is not equivalent: a line comment containing `/*` would let the block
197
+ * pass consume everything up to a later `*​/`, deleting real code between them.
198
+ */
199
+ function stripBlockAndLineComments(content) {
200
+ let out = "";
201
+ let pos = 0;
202
+ let blockPossible = true;
203
+ for (;;) {
204
+ const block = blockPossible ? content.indexOf("/*", pos) : -1;
205
+ const line = content.indexOf("//", pos);
206
+ const next = earliestIndex(block, line);
207
+ if (next === -1)
208
+ break;
209
+ if (next === block) {
210
+ const close = content.indexOf("*/", block + 2);
211
+ if (close === -1) {
212
+ // No block comment closes anywhere after here; keep looking for
213
+ // line comments, which is what the alternation would go on to do.
214
+ blockPossible = false;
215
+ continue;
216
+ }
217
+ out += content.slice(pos, block);
218
+ pos = close + 2;
219
+ continue;
220
+ }
221
+ // `$` under /m stops before the newline, so the newline itself survives.
222
+ const eol = content.indexOf("\n", line);
223
+ out += content.slice(pos, line);
224
+ pos = eol === -1 ? content.length : eol;
225
+ }
226
+ return pos === 0 ? content : out + content.slice(pos);
227
+ }
142
228
  /** Extract executable TypeScript MCP SDK registrations; comments are masked first. */
143
229
  export function extractMcpToolRegistrations(source, file, root) {
144
- const clean = source
145
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
146
- .replace(/(^|\n)\s*\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, " "));
230
+ // `[^\S\n]` (whitespace except newline) rather than `\s`: `\s` overlaps the
231
+ // `(^|\n)` before it, so every start position in a run of blank lines
232
+ // backtracks through the whole run — super-linear on blank-line-heavy files.
233
+ const clean = maskBlockComments(source, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|\n)[^\S\n]*\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, " "));
147
234
  const rows = [];
148
235
  const astCallStarts = new Set();
149
236
  const astCalls = [];
@@ -228,9 +315,9 @@ export function extractMcpToolRegistrations(source, file, root) {
228
315
  ? astCalls.find((call) => /setRequestHandler\s*\(\s*CallToolRequestSchema/.test(call))
229
316
  : clean;
230
317
  if (callToolHandler && /setRequestHandler\s*\(\s*CallToolRequestSchema/.test(callToolHandler)) {
231
- const name = callToolHandler.match(/\bname\s*!==?\s*(["'`])([^"'`]+)\1/)?.[2];
232
- const handler = callToolHandler.match(/await\s+([\w$]*(?:handle|dispatch)[\w$]*)\s*\(\s*args/i)?.[1];
233
- const factory = source.match(/setRequestHandler\s*\(\s*ListToolsRequestSchema[\s\S]*?tools\s*:\s*(\w*Tools)\s*\(/)?.[1];
318
+ const name = /\bname\s*!==?\s*(["'`])([^"'`]+)\1/.exec(callToolHandler)?.[2];
319
+ const handler = /await\s+([\w$]*(?:handle|dispatch)[\w$]*)\s*\(\s*args/i.exec(callToolHandler)?.[1];
320
+ const factory = /setRequestHandler\s*\(\s*ListToolsRequestSchema[\s\S]*?tools\s*:\s*(\w*Tools)\s*\(/.exec(source)?.[1];
234
321
  if (name && handler)
235
322
  rows.push({
236
323
  name,
@@ -243,7 +330,7 @@ export function extractMcpToolRegistrations(source, file, root) {
243
330
  }
244
331
  // Descriptor objects are accepted only inside an explicit Tool[] factory,
245
332
  // never from unrelated fixture/config objects in an SDK-importing file.
246
- const descriptorFunctionMatch = clean.match(/(?:export\s+)?function\s+(\w*Tools)\s*\([^)]*\)\s*:\s*Tool\[\]\s*\{([\s\S]*?)\n\}/);
333
+ const descriptorFunctionMatch = /(?:export\s+)?function\s+(\w*Tools)\s*\([^)]*\)\s*:\s*Tool\[\]\s*\{([\s\S]*?)\n\}/.exec(clean);
247
334
  const descriptorFunction = descriptorFunctionMatch?.[2];
248
335
  const descriptorScopes = root
249
336
  ? [
@@ -254,11 +341,11 @@ export function extractMcpToolRegistrations(source, file, root) {
254
341
  ? [descriptorFunction]
255
342
  : [];
256
343
  for (const descriptorScope of descriptorScopes) {
257
- const scopeAssociationKey = descriptorScope.match(/function\s+(\w*Tools)\b/)?.[1] ??
258
- descriptorScope.match(/tools\s*:\s*(\w*Tools)\s*\(/)?.[1] ??
344
+ const scopeAssociationKey = /function\s+(\w*Tools)\b/.exec(descriptorScope)?.[1] ??
345
+ /tools\s*:\s*(\w*Tools)\s*\(/.exec(descriptorScope)?.[1] ??
259
346
  `file:${file}`;
260
347
  for (const match of descriptorScope.matchAll(/\bname\s*:\s*(["'`])([^"'`]+)\1\s*,([\s\S]{0,30000}?)\binputSchema\s*:/g)) {
261
- const description = unquote(match[3].match(/\bdescription\s*:\s*((?:["'`])[^"'`]*(?:["'`]))/)?.[1] ?? "");
348
+ const description = unquote(/\bdescription\s*:\s*((?:["'`])[^"'`]*(?:["'`]))/.exec(match[3])?.[1] ?? "");
262
349
  rows.push({
263
350
  name: match[2],
264
351
  description,
@@ -272,6 +359,8 @@ export function extractMcpToolRegistrations(source, file, root) {
272
359
  }
273
360
  return rows;
274
361
  }
362
+ /** `meta` key holding the last index's unparsed-file tally, as a JSON object. */
363
+ export const COVERAGE_UNPARSED_META_KEY = "coverageUnparsedByExtension";
275
364
  function repairMetadataFamily(filePath) {
276
365
  const segments = filePath.replaceAll("\\", "/").split("/");
277
366
  const extension = path.extname(filePath).toLowerCase();
@@ -337,7 +426,7 @@ function symbolIdentity(repoPath, row) {
337
426
  }
338
427
  const SQLITE_SCOPE_CHUNK_SIZE = 400;
339
428
  function scopedFilePaths(filePaths) {
340
- return filePaths === undefined ? undefined : [...new Set(filePaths)].sort();
429
+ return filePaths === undefined ? undefined : [...new Set(filePaths)].sort(compareBytes);
341
430
  }
342
431
  function symbolsForFiles(db, filePaths) {
343
432
  if (filePaths === undefined) {
@@ -410,8 +499,18 @@ function persistSymbolIdentities(db, repoPath, filePaths) {
410
499
  .slice(Math.max(0, row.startLine - 1), row.endLine)
411
500
  .join("\n")
412
501
  .split(/\r?\n/, 1)[0]
413
- .replace(/\s*=>.*$/, "")
414
- .replace(/\s*\{.*$/, "")
502
+ // `(?<!\s)` pins the match to the start of a whitespace run; without it
503
+ // every position inside the run rescans it, which is quadratic.
504
+ //
505
+ // S8786 reports the brace variant as super-linear anyway. Measured on
506
+ // a pure whitespace run with no brace: 0.02ms at n=4,000 rising to
507
+ // 0.11ms at n=32,000 — ratios 1.29 / 1.94 / 2.17 per doubling, i.e.
508
+ // linear. The lookbehind is what makes it so. The arrow variant on
509
+ // the line below is built identically and is not reported, which is
510
+ // the inconsistency that identifies this as a false positive rather
511
+ // than a rule we are ducking.
512
+ .replace(/(?<!\s)\s*=>.*$/, "")
513
+ .replace(/(?<!\s)\s*\{.*$/, "") // NOSONAR S8786 — measured linear, see above
415
514
  .replace(/:\s*(?:#.*)?$/, "")
416
515
  .replace(/\s+/g, " ")
417
516
  .trim() || `${row.kind}:${row.name}`;
@@ -637,7 +736,7 @@ const ARCHITECTURE_CACHE_LIMIT = 16;
637
736
  function getArchitectureMetadataSnapshot(allRepos, resolvedRepoPath) {
638
737
  const cacheKey = JSON.stringify([resolvedRepoPath, allRepos.map((repo) => repo.path)]);
639
738
  const cached = architectureMetadataCache.get(cacheKey);
640
- if (cached && cached.generation === indexGeneration)
739
+ if (cached?.generation === indexGeneration)
641
740
  return cached.snapshot;
642
741
  const formatPath = makePathFormatter(resolvedRepoPath);
643
742
  const communities = detectSymbolCommunities(allRepos, formatPath);
@@ -705,7 +804,7 @@ function setBoundedCache(cache, key, value, limit) {
705
804
  }
706
805
  }
707
806
  function getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath, topN = 15, relationKinds) {
708
- const normalizedKinds = [...(relationKinds ?? [])].sort();
807
+ const normalizedKinds = [...(relationKinds ?? [])].sort(compareBytes);
709
808
  const cacheKey = JSON.stringify([
710
809
  resolvedRepoPath,
711
810
  allRepos.map((repo) => repo.path),
@@ -713,7 +812,7 @@ function getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath, topN = 15, relati
713
812
  normalizedKinds,
714
813
  ]);
715
814
  const cached = graphAnalyticsCache.get(cacheKey);
716
- if (cached && cached.generation === indexGeneration)
815
+ if (cached?.generation === indexGeneration)
717
816
  return cached.result;
718
817
  const result = measurePerfPhase("graph_analytics", async () => {
719
818
  const formatPath = makePathFormatter(resolvedRepoPath);
@@ -755,7 +854,7 @@ function traversalNodeKey(name, file) {
755
854
  }
756
855
  function getTraversalSnapshot(repoPath, db) {
757
856
  const cached = traversalSnapshotCache.get(repoPath);
758
- if (cached && cached.generation === indexGeneration)
857
+ if (cached?.generation === indexGeneration)
759
858
  return cached.snapshot;
760
859
  const snapshot = measurePerfPhaseSync("traversal_snapshot", () => {
761
860
  const edges = db
@@ -857,7 +956,7 @@ export function resetSearchCacheStats() {
857
956
  function getSearchMetadataSnapshot(repoPath, db) {
858
957
  const cacheKey = path.resolve(repoPath);
859
958
  const cached = searchMetadataCache.get(cacheKey);
860
- if (cached && cached.generation === indexGeneration) {
959
+ if (cached?.generation === indexGeneration) {
861
960
  searchCacheStats.metadataCacheHits++;
862
961
  return cached.snapshot;
863
962
  }
@@ -893,7 +992,7 @@ function getSearchMetadataSnapshot(repoPath, db) {
893
992
  function getSearchCommunityLookup(allRepos, resolvedRepoPath) {
894
993
  const cacheKey = JSON.stringify([resolvedRepoPath, allRepos.map((repo) => repo.path)]);
895
994
  const cached = searchCommunityLookupCache.get(cacheKey);
896
- if (cached && cached.generation === indexGeneration) {
995
+ if (cached?.generation === indexGeneration) {
897
996
  searchCacheStats.communityLookupCacheHits++;
898
997
  return cached.lookup;
899
998
  }
@@ -916,7 +1015,7 @@ function getSearchCommunityLookup(allRepos, resolvedRepoPath) {
916
1015
  */
917
1016
  function getAnnIndex(repoPath, rows) {
918
1017
  const cached = annIndexCache.get(repoPath);
919
- if (cached && cached.generation === indexGeneration)
1018
+ if (cached?.generation === indexGeneration)
920
1019
  return cached.index;
921
1020
  const parsed = [];
922
1021
  for (const r of rows) {
@@ -964,7 +1063,7 @@ function getAnnIndex(repoPath, rows) {
964
1063
  */
965
1064
  function computeFlows(db, resolvedRepoPath) {
966
1065
  const cached = flowsCache.get(resolvedRepoPath);
967
- if (cached && cached.generation === indexGeneration)
1066
+ if (cached?.generation === indexGeneration)
968
1067
  return cached.result;
969
1068
  // Resolved call edges only (the callee resolves to a concrete file).
970
1069
  const edgeRows = db
@@ -1013,8 +1112,8 @@ function computeFlows(db, resolvedRepoPath) {
1013
1112
  }
1014
1113
  if (!src)
1015
1114
  return false;
1016
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1017
- return new RegExp(`\\bexport\\s+(?:default\\s+)?(?:function|class|const|let|var|type|interface|enum)\\s+${escaped}\\b`).test(src);
1115
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
1116
+ return new RegExp(String.raw `\bexport\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+${escaped}\b`).test(src);
1018
1117
  };
1019
1118
  // Entry points: endpoints + exported functions with zero callers.
1020
1119
  const outDegree = (name) => adj.get(name)?.length ?? 0;
@@ -1097,7 +1196,7 @@ function computeFlows(db, resolvedRepoPath) {
1097
1196
  */
1098
1197
  function getFileToFlowLookup(db, resolvedRepoPath) {
1099
1198
  const cached = fileToFlowLookupCache.get(resolvedRepoPath);
1100
- if (cached && cached.generation === indexGeneration)
1199
+ if (cached?.generation === indexGeneration)
1101
1200
  return cached.result;
1102
1201
  const byFile = new Map();
1103
1202
  for (const flow of computeFlows(db, resolvedRepoPath)) {
@@ -1325,7 +1424,8 @@ function getPrecedingComment(node) {
1325
1424
  if (text.startsWith("/*")) {
1326
1425
  text = text
1327
1426
  .replace(/^\/\*+\s*/, "")
1328
- .replace(/\*+\/$/, "")
1427
+ // `(?<!\*)` pins the match to the start of the star run (quadratic otherwise).
1428
+ .replace(/(?<!\*)\*+\/$/, "")
1329
1429
  .split("\n")
1330
1430
  .map((line) => line.trim().replace(/^\*\s*/, ""))
1331
1431
  .join("\n")
@@ -1347,9 +1447,9 @@ function getPythonDocstring(node) {
1347
1447
  const body = node.childForFieldName("body");
1348
1448
  if (body && body.childCount > 0) {
1349
1449
  const first = body.child(0);
1350
- if (first && first.type === "expression_statement") {
1450
+ if (first?.type === "expression_statement") {
1351
1451
  const expr = first.child(0);
1352
- if (expr && expr.type === "string") {
1452
+ if (expr?.type === "string") {
1353
1453
  return expr.text.replace(/^["']{3}|["']{3}$/g, "").trim();
1354
1454
  }
1355
1455
  }
@@ -1468,8 +1568,8 @@ function balancedEnd(source, openIndex, open = "(", close = ")") {
1468
1568
  function objectKeys(source) {
1469
1569
  const keys = new Set();
1470
1570
  for (const entry of splitCallArguments(source)) {
1471
- const property = entry.match(/^\s*(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*(?::|,|$)/);
1472
- const key = property?.[1] ?? property?.[2] ?? entry.match(/^\s*([A-Za-z_$][\w$]*)\s*$/)?.[1];
1571
+ const property = /^\s*(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*(?::|,|$)/.exec(entry);
1572
+ const key = property?.[1] ?? property?.[2] ?? /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(entry)?.[1];
1473
1573
  if (key && key !== "..." && !key.startsWith("..."))
1474
1574
  keys.add(key);
1475
1575
  }
@@ -1509,7 +1609,7 @@ function topLevelObjectKeys(source) {
1509
1609
  }
1510
1610
  if (depth !== 0 || !propertyStart || /\s/.test(char))
1511
1611
  continue;
1512
- const match = source.slice(i).match(/^(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:/);
1612
+ const match = /^(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:/.exec(source.slice(i));
1513
1613
  if (match) {
1514
1614
  keys.add(match[1] ?? match[2]);
1515
1615
  propertyStart = false;
@@ -1534,21 +1634,25 @@ function fieldsFromType(typeExpression, types) {
1534
1634
  for (const field of types.get(name[0]) ?? [])
1535
1635
  fields.add(field);
1536
1636
  }
1537
- const inline = typeExpression.match(/\{([\s\S]*)\}/);
1538
- if (inline)
1539
- for (const field of objectKeys(inline[1]))
1637
+ // Widest `{ … }` span, scanned rather than matched: the equivalent regex
1638
+ // (`/\{([\s\S]*)\}/`) backtracks quadratically on brace-heavy type expressions.
1639
+ const braceStart = typeExpression.indexOf("{");
1640
+ const braceEnd = typeExpression.lastIndexOf("}");
1641
+ if (braceStart >= 0 && braceEnd > braceStart) {
1642
+ for (const field of objectKeys(typeExpression.slice(braceStart + 1, braceEnd)))
1540
1643
  fields.add(field);
1644
+ }
1541
1645
  return [...fields];
1542
1646
  }
1543
1647
  function apiPath(expression, isClient) {
1544
1648
  const trimmed = expression.trim();
1545
1649
  let value = null;
1546
1650
  let unresolved = false;
1547
- const quoted = trimmed.match(/^(["'])([\s\S]*?)\1$/);
1651
+ const quoted = /^(["'])([\s\S]*?)\1$/.exec(trimmed);
1548
1652
  if (quoted)
1549
1653
  value = quoted[2];
1550
1654
  else {
1551
- const template = trimmed.match(/^`([\s\S]*)`$/);
1655
+ const template = /^`([\s\S]*)`$/.exec(trimmed);
1552
1656
  if (template && isClient)
1553
1657
  value = template[1].replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, "{$1}");
1554
1658
  else
@@ -1608,9 +1712,7 @@ function declaredResponseFields(source, types) {
1608
1712
  function extractApiContracts(content, definitions, references) {
1609
1713
  const facts = [];
1610
1714
  const types = typeFieldMap(content);
1611
- const masked = content
1612
- .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "))
1613
- .replace(/(^|\s)\/\/.*$/gm, (match, prefix) => prefix + " ".repeat(match.length - prefix.length));
1715
+ const masked = maskBlockComments(content, (match) => match.replace(/[^\n]/g, " ")).replace(/(^|\s)\/\/.*$/gm, (match, prefix) => prefix + " ".repeat(match.length - prefix.length));
1614
1716
  for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.(get|post|put|patch|delete)\s*\(/gi)) {
1615
1717
  const receiver = match[1].toLowerCase();
1616
1718
  if (!new Set(["app", "router", "route", "fastify", "server"]).has(receiver))
@@ -1661,7 +1763,7 @@ function extractApiContracts(content, definitions, references) {
1661
1763
  const addClient = (framework, method, index, args) => {
1662
1764
  const route = apiPath(args[0] ?? "", true);
1663
1765
  if (framework === "fetch") {
1664
- const methodMatch = (args[1] ?? "").match(/\bmethod\s*:\s*["'](get|post|put|patch|delete)["']/i);
1766
+ const methodMatch = /\bmethod\s*:\s*["'](get|post|put|patch|delete)["']/i.exec(args[1] ?? "");
1665
1767
  if (methodMatch)
1666
1768
  method = methodMatch[1].toUpperCase();
1667
1769
  }
@@ -1677,12 +1779,12 @@ function extractApiContracts(content, definitions, references) {
1677
1779
  .match(/(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s*$/)?.[1];
1678
1780
  const bodyMatch = response
1679
1781
  ? framework === "fetch"
1680
- ? new RegExp(`(?:const|let)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*\\(\\s*await\\s+${response}\\.json\\(\\)\\s*\\)\\s+as\\s+([^;\\n]+)`).exec(source)
1681
- : new RegExp(`(?:const|let)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${response}\\.data\\s+as\\s+([^;\\n]+)`).exec(source)
1782
+ ? new RegExp(String.raw `(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*\(\s*await\s+${response}\.json\(\)\s*\)\s+as\s+([^;\n]+)`).exec(source)
1783
+ : new RegExp(String.raw `(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*${response}\.data\s+as\s+([^;\n]+)`).exec(source)
1682
1784
  : null;
1683
1785
  const body = bodyMatch?.[1];
1684
1786
  const accessed = body
1685
- ? [...source.matchAll(new RegExp(`\\b${body}\\.([A-Za-z_$][\\w$]*)`, "g"))].map((entry) => entry[1])
1787
+ ? [...source.matchAll(new RegExp(String.raw `\b${body}\.([A-Za-z_$][\w$]*)`, "g"))].map((entry) => entry[1])
1686
1788
  : [];
1687
1789
  facts.push({
1688
1790
  kind: "client",
@@ -1751,7 +1853,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
1751
1853
  const isGo = filePath.endsWith(".go");
1752
1854
  const isRust = filePath.endsWith(".rs");
1753
1855
  function getEnclosingSymbol() {
1754
- return symbolStack.length > 0 ? symbolStack[symbolStack.length - 1] : null;
1856
+ return symbolStack.at(-1) ?? null;
1755
1857
  }
1756
1858
  function traverse(node) {
1757
1859
  let isNewSymbol = false;
@@ -2187,7 +2289,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2187
2289
  else if (node.type === "variable_declarator") {
2188
2290
  const nameNode = node.childForFieldName("name");
2189
2291
  const valueNode = node.childForFieldName("value");
2190
- if (nameNode && nameNode.type === "identifier") {
2292
+ if (nameNode?.type === "identifier") {
2191
2293
  const name = nameNode.text;
2192
2294
  symName = name;
2193
2295
  const isFunc = valueNode &&
@@ -2235,7 +2337,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2235
2337
  if (!isPython && !isApex) {
2236
2338
  if (node.type === "import_statement" || node.type === "import_declaration") {
2237
2339
  const sourceNode = node.childForFieldName("source");
2238
- if (sourceNode && sourceNode.type === "string") {
2340
+ if (sourceNode?.type === "string") {
2239
2341
  const srcText = sourceNode.text.replace(/^['"`]|['"`]$/g, "");
2240
2342
  fileDependencies.push(srcText);
2241
2343
  // Record which local names this module binds, so a later call to
@@ -2334,14 +2436,14 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2334
2436
  }
2335
2437
  else if (node.type === "query_expression") {
2336
2438
  const queryText = node.text;
2337
- const soqlMatches = [...queryText.matchAll(/from\s+([a-zA-Z0-9_]+)/gi)];
2439
+ const soqlMatches = [...queryText.matchAll(/from\s+(\w+)/gi)];
2338
2440
  for (const match of soqlMatches) {
2339
2441
  const tableName = match[1];
2340
2442
  if (tableName) {
2341
2443
  tableDependencies.push(tableName);
2342
2444
  }
2343
2445
  }
2344
- const soslMatches = [...queryText.matchAll(/returning\s+([a-zA-Z0-9_]+)/gi)];
2446
+ const soslMatches = [...queryText.matchAll(/returning\s+(\w+)/gi)];
2345
2447
  for (const match of soslMatches) {
2346
2448
  const tableName = match[1];
2347
2449
  if (tableName) {
@@ -2519,20 +2621,20 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2519
2621
  node.children.find((c) => c.type === "arguments");
2520
2622
  if (argsNode && argsNode.childCount > 0) {
2521
2623
  let firstArg = argsNode.child(0);
2522
- if (firstArg && firstArg.text === "(") {
2624
+ if (firstArg?.text === "(") {
2523
2625
  firstArg = argsNode.child(1);
2524
2626
  }
2525
2627
  if (firstArg) {
2526
2628
  const tableName = firstArg.text.replace(/['"`]/g, "");
2527
2629
  const columns = [];
2528
2630
  let secondArg = argsNode.child(2);
2529
- if (secondArg && secondArg.text === ",") {
2631
+ if (secondArg?.text === ",") {
2530
2632
  secondArg = argsNode.child(3);
2531
2633
  }
2532
- if (secondArg && secondArg.type === "object") {
2634
+ if (secondArg?.type === "object") {
2533
2635
  for (let i = 0; i < secondArg.childCount; i++) {
2534
2636
  const prop = secondArg.child(i);
2535
- if (prop && prop.type === "pair") {
2637
+ if (prop?.type === "pair") {
2536
2638
  const keyNode = prop.childForFieldName("key") || prop.child(0);
2537
2639
  if (keyNode) {
2538
2640
  columns.push(keyNode.text.replace(/['"`]/g, ""));
@@ -2574,7 +2676,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2574
2676
  const content = rootNode.text;
2575
2677
  const apiContracts = extractApiContracts(content, definitions, references);
2576
2678
  // Drizzle ORM fallback regex
2577
- const drizzleRegex = /(?:pgTable|sqliteTable|mysqlTable)\s*\(\s*['"`]([a-zA-Z0-9_]+)['"`]/g;
2679
+ const drizzleRegex = /(?:pgTable|sqliteTable|mysqlTable)\s*\(\s*['"`](\w+)['"`]/g;
2578
2680
  let drizzleMatch = drizzleRegex.exec(content);
2579
2681
  while (drizzleMatch !== null) {
2580
2682
  const tableName = drizzleMatch[1];
@@ -2596,14 +2698,14 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2596
2698
  }
2597
2699
  // Python: SQLAlchemy / Django Models
2598
2700
  if (isPython) {
2599
- const tablenameRegex = /__tablename__\s*=\s*['"`]([a-zA-Z0-9_]+)['"`]/g;
2701
+ const tablenameRegex = /__tablename__\s*=\s*['"`](\w+)['"`]/g;
2600
2702
  let tablenameMatch = tablenameRegex.exec(content);
2601
2703
  while (tablenameMatch !== null) {
2602
2704
  const tableName = tablenameMatch[1];
2603
2705
  ormDependencies.push({ to: tableName, kind: "orm-relation" });
2604
2706
  tablenameMatch = tablenameRegex.exec(content);
2605
2707
  }
2606
- const dbtableRegex = /db_table\s*=\s*['"`]([a-zA-Z0-9_]+)['"`]/g;
2708
+ const dbtableRegex = /db_table\s*=\s*['"`](\w+)['"`]/g;
2607
2709
  let dbtableMatch = dbtableRegex.exec(content);
2608
2710
  while (dbtableMatch !== null) {
2609
2711
  const tableName = dbtableMatch[1];
@@ -2613,7 +2715,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2613
2715
  }
2614
2716
  // Java: JPA / Hibernate annotations
2615
2717
  if (isJava) {
2616
- const javaTableRegex = /@Table\s*\(\s*name\s*=\s*['"`]([a-zA-Z0-9_]+)['"`]\s*\)/gi;
2718
+ const javaTableRegex = /@Table\s*\(\s*name\s*=\s*['"`](\w+)['"`]\s*\)/gi;
2617
2719
  let javaTableMatch = javaTableRegex.exec(content);
2618
2720
  while (javaTableMatch !== null) {
2619
2721
  const tableName = javaTableMatch[1];
@@ -2623,7 +2725,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2623
2725
  }
2624
2726
  // C#: EF Core ToTable
2625
2727
  if (isCSharp) {
2626
- const csTableRegex = /\.ToTable\s*\(\s*['"`]([a-zA-Z0-9_]+)['"`]\s*\)/g;
2728
+ const csTableRegex = /\.ToTable\s*\(\s*['"`](\w+)['"`]\s*\)/g;
2627
2729
  let csTableMatch = csTableRegex.exec(content);
2628
2730
  while (csTableMatch !== null) {
2629
2731
  const tableName = csTableMatch[1];
@@ -2635,7 +2737,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2635
2737
  // Mask out comments so we don't detect example endpoints in docstrings.
2636
2738
  let maskedContent = rootNode.text;
2637
2739
  // Mask block comments preserving newlines
2638
- maskedContent = maskedContent.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "));
2740
+ maskedContent = maskBlockComments(maskedContent, (match) => match.replace(/[^\n]/g, " "));
2639
2741
  // Mask line comments (// or #) preserving length
2640
2742
  maskedContent = maskedContent.replace(/(^|\s)(\/\/|#).*$/gm, (match, prefix) => {
2641
2743
  return prefix + " ".repeat(match.length - prefix.length);
@@ -2676,9 +2778,16 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2676
2778
  // For decorators, find the decorated function on the next few lines
2677
2779
  const remaining = fileContent.substring(pattern.regex.lastIndex);
2678
2780
  // Match something like "async function name(" or "def name(" or "class name" or "methodName("
2679
- const funcMatch = remaining.match(/^\s*(?:async\s+)?(?:function|def)\s+([a-zA-Z0-9_]+)\b|^\s*(?:@Get|@Post|@Put|@Delete|@Patch|@app|@router|private|public|protected|async|static|\s)*\s*([a-zA-Z0-9_]+)\s*\(/i);
2781
+ // The modifier group already has `\s` as an alternative, so neither a
2782
+ // leading `\s*` nor a trailing `\s*` may wrap it: either one splits a
2783
+ // whitespace run with the group and backtracks catastrophically.
2784
+ // Both forms are `^`-anchored and there is no `m` flag, so trying them
2785
+ // in order is exactly the alternation they replace, minus its complexity.
2786
+ const declaredFunc = /^\s*(?:async\s+)?(?:function|def)\s+(\w+)\b/i.exec(remaining);
2787
+ const funcMatch = declaredFunc ??
2788
+ /^(?:@Get|@Post|@Put|@Delete|@Patch|@app|@router|private|public|protected|async|static|\s)*(\w+)\s*\(/i.exec(remaining);
2680
2789
  if (funcMatch) {
2681
- handlerName = funcMatch[1] || funcMatch[2];
2790
+ handlerName = funcMatch[1];
2682
2791
  }
2683
2792
  }
2684
2793
  // If the handler is a method like controller.method, extract the method name too.
@@ -2812,9 +2921,9 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
2812
2921
  // Bare single positional arg (`@GetMapping("/foo")`, optionally followed
2813
2922
  // by other attributes): a quoted literal at the very start, with no
2814
2923
  // `value =` prefix.
2815
- const bareMatch = args.match(/^\s*["']([^"']+)["']/);
2924
+ const bareMatch = /^\s*["']([^"']+)["']/.exec(args);
2816
2925
  // Named `value = "..."` attribute, anywhere in the argument list.
2817
- const namedMatch = args.match(/\bvalue\s*=\s*["']([^"']+)["']/);
2926
+ const namedMatch = /\bvalue\s*=\s*["']([^"']+)["']/.exec(args);
2818
2927
  const rawPath = bareMatch?.[1] ?? namedMatch?.[1];
2819
2928
  if (!rawPath)
2820
2929
  continue;
@@ -2846,7 +2955,7 @@ function isMinifiedSource(content) {
2846
2955
  return false;
2847
2956
  let newlines = 0;
2848
2957
  for (let i = 0; i < content.length; i++) {
2849
- if (content.charCodeAt(i) === 10)
2958
+ if (content.codePointAt(i) === 10)
2850
2959
  newlines++;
2851
2960
  }
2852
2961
  return content.length / (newlines + 1) > 2000;
@@ -2861,7 +2970,7 @@ function isMinifiedSource(content) {
2861
2970
  * statements stay intact and the file indexes cleanly.
2862
2971
  */
2863
2972
  function sanitizeDollarQuotedSql(content) {
2864
- return content.replace(/\$([A-Za-z0-9_]*)\$[\s\S]*?\$\1\$/g, "''");
2973
+ return content.replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, "''");
2865
2974
  }
2866
2975
  const RESOLVE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".mts", ".cts", ".mjs", ".cjs"];
2867
2976
  // TS ESM writes `.js` specifiers that map to `.ts` source (and .jsx->.tsx, etc.).
@@ -3038,8 +3147,8 @@ function fileDefinesOrReexports(repoPath, relFile, name, seen) {
3038
3147
  return true;
3039
3148
  try {
3040
3149
  const src = fs.readFileSync(path.join(repoPath, relFile), "utf-8");
3041
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3042
- if (new RegExp(`\\b(?:function|class|const|let|var|type|interface|enum)\\s+${escaped}\\b`).test(src)) {
3150
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
3151
+ if (new RegExp(String.raw `\b(?:function|class|const|let|var|type|interface|enum)\s+${escaped}\b`).test(src)) {
3043
3152
  return true;
3044
3153
  }
3045
3154
  }
@@ -3096,7 +3205,7 @@ function salesforceExperienceFile(relativePath) {
3096
3205
  const normalized = relativePath.replaceAll("\\", "/");
3097
3206
  if (normalized === "lwr.config.json")
3098
3207
  return { type: "lwrConfig" };
3099
- const match = normalized.match(/^(force-app\/main\/default\/experiences\/[A-Za-z][A-Za-z0-9_]*?)\/(routes|views|themes|assets)\/[^/]+\.(json|svg)$/);
3208
+ const match = /^(force-app\/main\/default\/experiences\/[A-Za-z]\w*?)\/(routes|views|themes|assets)\/[^/]+\.(json|svg)$/.exec(normalized);
3100
3209
  if (!match)
3101
3210
  return null;
3102
3211
  const type = match[2];
@@ -3131,7 +3240,7 @@ function experienceSiteFile(sitePath, category, value, repoPath) {
3131
3240
  function findExperienceComponentFile(value, repoPath) {
3132
3241
  if (typeof value !== "string")
3133
3242
  return null;
3134
- const component = value.match(/^c:([A-Za-z][A-Za-z0-9_]*)$/)?.[1];
3243
+ const component = /^c:([A-Za-z]\w*)$/.exec(value)?.[1];
3135
3244
  if (!component)
3136
3245
  return null;
3137
3246
  return (findSalesforceLwcComponentFile(component, repoPath) ??
@@ -3147,7 +3256,7 @@ function findExperienceComponentFile(value, repoPath) {
3147
3256
  */
3148
3257
  function lwcBundleFile(relativePath) {
3149
3258
  const normalized = relativePath.replaceAll("\\", "/");
3150
- const match = normalized.match(/^(force-app\/main\/default\/lwc\/)([A-Za-z][A-Za-z0-9_]*?)\/\2\.(js|html|css|js-meta\.xml)$/);
3259
+ const match = /^(force-app\/main\/default\/lwc\/)([A-Za-z]\w*?)\/\2\.(js|html|css|js-meta\.xml)$/.exec(normalized);
3151
3260
  if (!match)
3152
3261
  return null;
3153
3262
  return {
@@ -3163,7 +3272,7 @@ function lwcBundleFile(relativePath) {
3163
3272
  */
3164
3273
  function auraBundleFile(relativePath) {
3165
3274
  const normalized = relativePath.replaceAll("\\", "/");
3166
- const match = normalized.match(/^(force-app\/main\/default\/aura\/)([A-Za-z][A-Za-z0-9_]*?)\/\2\.(app|cmp|css|design|docs|evt|intf|js|svg)$/);
3275
+ const match = /^(force-app\/main\/default\/aura\/)([A-Za-z]\w*?)\/\2\.(app|cmp|css|design|docs|evt|intf|js|svg)$/.exec(normalized);
3167
3276
  if (!match)
3168
3277
  return null;
3169
3278
  return {
@@ -3182,14 +3291,11 @@ function findApexMethodFilePath(className, methodName, repoPath) {
3182
3291
  if (!classFile)
3183
3292
  return null;
3184
3293
  try {
3185
- const source = fs
3186
- .readFileSync(path.join(repoPath, classFile), "utf-8")
3187
- .replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, "")
3188
- .replace(/'(?:\\.|[^'])*'|"(?:\\.|[^"])*"/g, "");
3189
- const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3294
+ const source = stripBlockAndLineComments(fs.readFileSync(path.join(repoPath, classFile), "utf-8")).replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g, "");
3295
+ const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
3190
3296
  // A source-level import is exact only when its declared Apex method exists,
3191
3297
  // never merely when the method name appears in a call, comment, or string.
3192
- const declaration = new RegExp(`\\b(?:public|protected|private|global)\\s+(?:(?:static|virtual|override|webService)\\s+)*[A-Za-z_][\\w<>,.?\\[\\]\\s]*?\\s${escaped}\\s*\\(`);
3298
+ const declaration = new RegExp(String.raw `\b(?:public|protected|private|global)\s+(?:(?:static|virtual|override|webService)\s+)*[A-Za-z_][\w<>,.?\[\]\s]*?\s${escaped}\s*\(`);
3193
3299
  return declaration.test(source) ? classFile : null;
3194
3300
  }
3195
3301
  catch {
@@ -3197,7 +3303,7 @@ function findApexMethodFilePath(className, methodName, repoPath) {
3197
3303
  }
3198
3304
  }
3199
3305
  function isUnnamespacedSalesforceIdentifier(value) {
3200
- return !value.includes("__") && /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)?$/.test(value);
3306
+ return !value.includes("__") && /^[A-Za-z]\w*(?:\.[A-Za-z]\w*)?$/.test(value);
3201
3307
  }
3202
3308
  /**
3203
3309
  * The C20 matcher deliberately recognizes only Salesforce DX metadata paths.
@@ -3209,32 +3315,35 @@ function salesforceMetadataFile(relativePath) {
3209
3315
  const root = "force-app/main/default/";
3210
3316
  if (!normalized.startsWith(root))
3211
3317
  return null;
3212
- let match = normalized.match(/^force-app\/main\/default\/objects\/([A-Za-z][A-Za-z0-9_]*)\/\1\.object-meta\.xml$/);
3318
+ let match = /^force-app\/main\/default\/objects\/([A-Za-z]\w*)\/\1\.object-meta\.xml$/.exec(normalized);
3213
3319
  if (match)
3214
3320
  return { type: "object", object: match[1] };
3215
- match = normalized.match(/^force-app\/main\/default\/objects\/([A-Za-z][A-Za-z0-9_]*)\/fields\/([A-Za-z][A-Za-z0-9_]*)\.field-meta\.xml$/);
3321
+ match =
3322
+ /^force-app\/main\/default\/objects\/([A-Za-z]\w*)\/fields\/([A-Za-z]\w*)\.field-meta\.xml$/.exec(normalized);
3216
3323
  if (match)
3217
3324
  return { type: "field", object: match[1], field: match[2] };
3218
- match = normalized.match(/^force-app\/main\/default\/objects\/([A-Za-z][A-Za-z0-9_]*)\/recordTypes\/([A-Za-z][A-Za-z0-9_]*)\.recordType-meta\.xml$/);
3325
+ match =
3326
+ /^force-app\/main\/default\/objects\/([A-Za-z]\w*)\/recordTypes\/([A-Za-z]\w*)\.recordType-meta\.xml$/.exec(normalized);
3219
3327
  if (match)
3220
3328
  return { type: "recordType", object: match[1], recordType: match[2] };
3221
- match = normalized.match(/^force-app\/main\/default\/permissionSets\/([A-Za-z][A-Za-z0-9_]*)\.permissionSet-meta\.xml$/);
3329
+ match = /^force-app\/main\/default\/permissionSets\/([A-Za-z]\w*)\.permissionSet-meta\.xml$/.exec(normalized);
3222
3330
  if (match)
3223
3331
  return { type: "permissionSet", name: match[1] };
3224
- match = normalized.match(/^force-app\/main\/default\/flows\/([A-Za-z][A-Za-z0-9_]*)\.flow-meta\.xml$/);
3332
+ match = /^force-app\/main\/default\/flows\/([A-Za-z]\w*)\.flow-meta\.xml$/.exec(normalized);
3225
3333
  if (match)
3226
3334
  return { type: "flow", name: match[1] };
3227
- match = normalized.match(/^force-app\/main\/default\/workflows\/([A-Za-z][A-Za-z0-9_]*)\.workflow-meta\.xml$/);
3335
+ match = /^force-app\/main\/default\/workflows\/([A-Za-z]\w*)\.workflow-meta\.xml$/.exec(normalized);
3228
3336
  if (match)
3229
3337
  return { type: "workflow", object: match[1] };
3230
- match = normalized.match(/^force-app\/main\/default\/approvalProcesses\/([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)\.approvalProcess-meta\.xml$/);
3338
+ match =
3339
+ /^force-app\/main\/default\/approvalProcesses\/([A-Za-z]\w*)\.([A-Za-z]\w*)\.approvalProcess-meta\.xml$/.exec(normalized);
3231
3340
  if (match)
3232
3341
  return { type: "approval", object: match[1], name: match[2] };
3233
3342
  return null;
3234
3343
  }
3235
3344
  function staticXmlTagValues(content, tag) {
3236
3345
  const uncommented = content.replace(/<!--[\s\S]*?-->/g, "");
3237
- const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3346
+ const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
3238
3347
  const expression = new RegExp(`<${escaped}>([^<]+)</${escaped}>`, "g");
3239
3348
  const values = [];
3240
3349
  for (const match of uncommented.matchAll(expression)) {
@@ -3246,7 +3355,7 @@ function staticXmlTagValues(content, tag) {
3246
3355
  }
3247
3356
  function staticSalesforceIdentifier(value) {
3248
3357
  return value.split(".").every((part) => {
3249
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(part))
3358
+ if (!/^[A-Za-z]\w*$/.test(part))
3250
3359
  return false;
3251
3360
  const customMarkerCount = (part.match(/__/g) ?? []).length;
3252
3361
  // `Thing__c` is an unnamespaced custom object/field. `pkg__Thing__c`
@@ -3255,14 +3364,14 @@ function staticSalesforceIdentifier(value) {
3255
3364
  });
3256
3365
  }
3257
3366
  function findSalesforceLwcComponentFile(componentName, repoPath) {
3258
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(componentName))
3367
+ if (!/^[A-Za-z]\w*$/.test(componentName))
3259
3368
  return null;
3260
3369
  const candidate = `force-app/main/default/lwc/${componentName}/${componentName}.js`;
3261
3370
  return fs.existsSync(path.join(repoPath, candidate)) ? candidate : null;
3262
3371
  }
3263
3372
  function apexMethodIsDeclared(content, methodName) {
3264
- const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3265
- return (firstExecutableMatch(content, new RegExp(`\\b(?:public|protected|private|global)\\s+(?:(?:static|virtual|override|webService)\\s+)*[A-Za-z_][\\w<>,.?\\[\\]\\s]*?\\s${escaped}\\s*\\(`, "gi")) !== null);
3373
+ const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
3374
+ return (firstExecutableMatch(content, new RegExp(String.raw `\b(?:public|protected|private|global)\s+(?:(?:static|virtual|override|webService)\s+)*[A-Za-z_][\w<>,.?\[\]\s]*?\s${escaped}\s*\(`, "gi")) !== null);
3266
3375
  }
3267
3376
  function firstExecutableMatch(source, expression) {
3268
3377
  for (const match of source.matchAll(expression)) {
@@ -3293,8 +3402,8 @@ function findClosingCallParen(source, openParen) {
3293
3402
  return null;
3294
3403
  }
3295
3404
  function implementsApexInterface(clause, interfaceName) {
3296
- const escaped = interfaceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3297
- return new RegExp(`(?:^|[,\\s])${escaped}(?=\\s|,|<|$)`, "i").test(clause);
3405
+ const escaped = interfaceName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
3406
+ return new RegExp(String.raw `(?:^|[,\s])${escaped}(?=\s|,|<|$)`, "i").test(clause);
3298
3407
  }
3299
3408
  /**
3300
3409
  * Extract the deliberately narrow source-only part of Apex platform behavior.
@@ -3307,35 +3416,40 @@ function extractApexPlatformDependencies(content, repoPath) {
3307
3416
  const add = (to, kind, sourceEvidence) => {
3308
3417
  dependencies.push({ to, kind, sourceEvidence });
3309
3418
  };
3310
- const className = firstExecutableMatch(content, /\bclass\s+([A-Za-z][A-Za-z0-9_]*)\b/gi)?.[1];
3419
+ const className = firstExecutableMatch(content, /\bclass\s+([a-z]\w*)\b/gi)?.[1];
3311
3420
  const restResource = firstExecutableMatch(content, /@RestResource\s*\(\s*urlMapping\s*=\s*(["'])([^"']+)\1\s*\)/gi);
3312
3421
  if (restResource) {
3313
- for (const verb of content.matchAll(/@Http(Get|Post|Put|Patch|Delete)\b[\s\S]{0,300}?\b(?:global|public)\s+static\s+[^({;]+?\s+[A-Za-z][A-Za-z0-9_]*\s*\(/gi)) {
3422
+ // `\s(?:\s|[^({;]*?(?<!\s))` is the return-type span. Written the obvious way
3423
+ // (`\s+[^({;]+?`) both halves can consume whitespace, so every split point of a
3424
+ // whitespace run is retried and the scan is quadratic. Pinning the span to end
3425
+ // on a non-whitespace character — with a bare `\s` for the one-whitespace-char
3426
+ // case that pin would otherwise drop — matches the same text in linear time.
3427
+ for (const verb of content.matchAll(/@Http(Get|Post|Put|Patch|Delete)\b[\s\S]{0,300}?\b(?:global|public)\s+static\s(?:\s|[^({;]*?(?<!\s))\s+[a-z]\w*\s*\(/gi)) {
3314
3428
  if (!isExecutablePosition(content, verb.index ?? 0))
3315
3429
  continue;
3316
- const handlerAnnotation = verb[0].match(/^@Http(?:Get|Post|Put|Patch|Delete)/i)?.[0];
3430
+ const handlerAnnotation = /^@Http(?:Get|Post|Put|Patch|Delete)/i.exec(verb[0])?.[0];
3317
3431
  if (!handlerAnnotation)
3318
3432
  continue;
3319
3433
  add(`REST ${verb[1].toUpperCase()} ${restResource[2]}`, "apex_rest_entry", `${restResource[0].trim()}; ${handlerAnnotation}`);
3320
3434
  }
3321
3435
  }
3322
3436
  if (className) {
3323
- for (const method of content.matchAll(/\b(?:global|public)\s+static\s+webService\s+[^({;]+?\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/gi)) {
3437
+ for (const method of content.matchAll(/\b(?:global|public)\s+static\s+webService\s(?:\s|[^({;]*?(?<!\s))\s+([a-z]\w*)\s*\(/gi)) {
3324
3438
  if (!isExecutablePosition(content, method.index ?? 0))
3325
3439
  continue;
3326
3440
  add(`SOAP ${className}.${method[1]}`, "apex_soap_entry", method[0].trim());
3327
3441
  }
3328
- for (const method of content.matchAll(/@InvocableMethod\b[\s\S]{0,300}?\b(?:global|public)\s+static\s+[^({;]+?\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/gi)) {
3442
+ for (const method of content.matchAll(/@InvocableMethod\b[\s\S]{0,300}?\b(?:global|public)\s+static\s(?:\s|[^({;]*?(?<!\s))\s+([a-z]\w*)\s*\(/gi)) {
3329
3443
  if (!isExecutablePosition(content, method.index ?? 0))
3330
3444
  continue;
3331
3445
  add(`ApexAction ${className}.${method[1]}`, "apex_invocable_entry", method[0].trim());
3332
3446
  }
3333
- for (const method of content.matchAll(/@future\b[\s\S]{0,300}?\b(?:global|public)\s+static\s+[^({;]+?\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/gi)) {
3447
+ for (const method of content.matchAll(/@future\b[\s\S]{0,300}?\b(?:global|public)\s+static\s(?:\s|[^({;]*?(?<!\s))\s+([a-z]\w*)\s*\(/gi)) {
3334
3448
  if (!isExecutablePosition(content, method.index ?? 0))
3335
3449
  continue;
3336
3450
  add(`${className}.${method[1]}`, "apex_async_callback", method[0].trim());
3337
3451
  }
3338
- const implementsClause = firstExecutableMatch(content, new RegExp(`\\bclass\\s+${className}\\b[^{}]*\\bimplements\\b([^{}]+)`, "gis"))?.[1];
3452
+ const implementsClause = firstExecutableMatch(content, new RegExp(String.raw `\bclass\s+${className}\b[^{}]*\bimplements\b([^{}]+)`, "gis"))?.[1];
3339
3453
  if (implementsClause) {
3340
3454
  const callbacks = [
3341
3455
  { interfaceName: "Queueable", methods: ["execute"] },
@@ -3356,11 +3470,11 @@ function extractApexPlatformDependencies(content, repoPath) {
3356
3470
  }
3357
3471
  }
3358
3472
  }
3359
- for (const trigger of content.matchAll(/\btrigger\s+([A-Za-z][A-Za-z0-9_]*)\s+on\s+([A-Za-z][A-Za-z0-9_]*)\s*\(([^)]+)\)/gi)) {
3473
+ for (const trigger of content.matchAll(/\btrigger\s+([a-z]\w*)\s+on\s+([a-z]\w*)\s*\(([^)]+)\)/gi)) {
3360
3474
  if (!isExecutablePosition(content, trigger.index ?? 0))
3361
3475
  continue;
3362
3476
  for (const event of trigger[3].split(",").map((value) => value.trim())) {
3363
- const normalized = event.match(/^(before|after)\s+(insert|update|delete|undelete)$/i);
3477
+ const normalized = /^(before|after)\s+(insert|update|delete|undelete)$/i.exec(event);
3364
3478
  if (!normalized)
3365
3479
  continue;
3366
3480
  const target = `${trigger[2]}.${normalized[1].toLowerCase()}${normalized[2][0].toUpperCase()}${normalized[2].slice(1).toLowerCase()}`;
@@ -3372,8 +3486,8 @@ function extractApexPlatformDependencies(content, repoPath) {
3372
3486
  }
3373
3487
  }
3374
3488
  const enqueueCalls = [
3375
- /\bSystem\.enqueueJob\s*\(\s*new\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/gi,
3376
- /\bDatabase\.executeBatch\s*\(\s*new\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/gi,
3489
+ /\bSystem\.enqueueJob\s*\(\s*new\s+([a-z]\w*)\s*\(/gi,
3490
+ /\bDatabase\.executeBatch\s*\(\s*new\s+([a-z]\w*)\s*\(/gi,
3377
3491
  ];
3378
3492
  for (const enqueue of enqueueCalls) {
3379
3493
  for (const match of content.matchAll(enqueue)) {
@@ -3394,7 +3508,7 @@ function extractApexPlatformDependencies(content, repoPath) {
3394
3508
  const args = splitCallArguments(content.slice(openParen + 1, closeParen));
3395
3509
  if (args.length !== 3)
3396
3510
  continue;
3397
- const job = args[2].match(/^new\s+([A-Za-z][A-Za-z0-9_]*)\s*\(/);
3511
+ const job = /^new\s+([A-Za-z]\w*)\s*\(/.exec(args[2]);
3398
3512
  if (!job)
3399
3513
  continue;
3400
3514
  const target = findUniqueApexClassFilePath(job[1], repoPath);
@@ -3443,7 +3557,7 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
3443
3557
  add(entry.value, "permission_set_record_type", entry.evidence);
3444
3558
  }
3445
3559
  for (const entry of staticXmlTagValues(content, "apexClass")) {
3446
- if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(entry.value))
3560
+ if (!/^[A-Za-z]\w*$/.test(entry.value))
3447
3561
  continue;
3448
3562
  const target = findUniqueApexClassFilePath(entry.value, repoPath);
3449
3563
  if (target)
@@ -3469,7 +3583,7 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
3469
3583
  if (!/<actionType>apex<\/actionType>/.test(action[1]))
3470
3584
  continue;
3471
3585
  for (const entry of staticXmlTagValues(action[1], "actionName")) {
3472
- const target = entry.value.match(/^([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)$/);
3586
+ const target = /^([A-Za-z]\w*)\.([A-Za-z]\w*)$/.exec(entry.value);
3473
3587
  if (!target)
3474
3588
  continue;
3475
3589
  const classFile = findApexMethodFilePath(target[1], target[2], repoPath);
@@ -3478,7 +3592,7 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
3478
3592
  }
3479
3593
  }
3480
3594
  for (const entry of staticXmlTagValues(content, "extensionName")) {
3481
- const component = entry.value.match(/^c:([A-Za-z][A-Za-z0-9_]*)$/);
3595
+ const component = /^c:([A-Za-z]\w*)$/.exec(entry.value);
3482
3596
  if (!component)
3483
3597
  continue;
3484
3598
  const target = findSalesforceLwcComponentFile(component[1], repoPath);
@@ -3548,14 +3662,14 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
3548
3662
  continue;
3549
3663
  if (source.startsWith("c/")) {
3550
3664
  const name = source.slice(2);
3551
- if (/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
3665
+ if (/^[A-Za-z]\w*$/.test(name)) {
3552
3666
  const target = findLwcComponentFile(relativePath, name, repoPath);
3553
3667
  if (target)
3554
3668
  dependencies.push({ to: target, kind: "lwc_component" });
3555
3669
  }
3556
3670
  continue;
3557
3671
  }
3558
- const apex = source.match(/^@salesforce\/apex\/([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)$/);
3672
+ const apex = /^@salesforce\/apex\/([A-Za-z]\w*)\.([A-Za-z]\w*)$/.exec(source);
3559
3673
  if (apex && !source.includes("__")) {
3560
3674
  const target = findApexMethodFilePath(apex[1], apex[2], repoPath);
3561
3675
  if (target) {
@@ -3570,7 +3684,7 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
3570
3684
  }
3571
3685
  continue;
3572
3686
  }
3573
- const schema = source.match(/^@salesforce\/schema\/([^/]+)$/);
3687
+ const schema = /^@salesforce\/schema\/([^/]+)$/.exec(source);
3574
3688
  if (schema && isUnnamespacedSalesforceIdentifier(schema[1])) {
3575
3689
  dependencies.push({
3576
3690
  to: schema[1],
@@ -3579,7 +3693,7 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
3579
3693
  continue;
3580
3694
  }
3581
3695
  if (source.startsWith("lightning/")) {
3582
- for (const binding of bindings.matchAll(/(?:\{|,)\s*([A-Za-z_$][\w$]*)/g)) {
3696
+ for (const binding of bindings.matchAll(/[{,]\s*([A-Za-z_$][\w$]*)/g)) {
3583
3697
  if (binding[1])
3584
3698
  importedNames.set(binding[1], source);
3585
3699
  }
@@ -3685,11 +3799,32 @@ export function parseWqlQuery(query) {
3685
3799
  fields: [],
3686
3800
  parameters: [],
3687
3801
  };
3688
- const fromMatch = query.match(/FROM\s+([a-zA-Z0-9_]+)/i);
3802
+ const fromMatch = /FROM\s+(\w+)/i.exec(query);
3689
3803
  if (fromMatch) {
3690
3804
  result.table = fromMatch[1];
3691
3805
  }
3692
- const selectMatch = query.match(/SELECT\s+(.+?)\s+FROM/i);
3806
+ // The field-list pattern is lazy and retries at every position where SELECT
3807
+ // could begin, so on a long single-line query with no FROM it rescans to end
3808
+ // of input from each one. Measured at ratio ~4.0 per doubling — 2.7s on a
3809
+ // 288 KB query.
3810
+ //
3811
+ // The pattern cannot match without `\s+FROM` present, so proving that
3812
+ // substring absent is exactly equivalent to running it, and costs one linear
3813
+ // pass with no quantifier to backtrack over. Equivalence checked on 14 cases
3814
+ // including nested SELECT, FROM without SELECT, tabs, newlines and
3815
+ // "SELECT Id FROMAccount", plus 100,000 randomised inputs: zero differences.
3816
+ // Degenerate input at n=32,000 falls from 2698ms to 0.078ms, ratio 1.40.
3817
+ //
3818
+ // S8786 still reports the literal, because the rule reads the pattern and
3819
+ // cannot see the guard in front of it. Unlike the brace pattern above, this
3820
+ // one is genuinely super-linear in isolation — what is bounded is the call,
3821
+ // not the regex. Rewriting it as an index scan would satisfy the rule, but
3822
+ // `.` excluding newline makes the equivalent scan subtle enough that the
3823
+ // measured guard is the safer trade for a release. Recorded here rather than
3824
+ // suppressed silently; worth revisiting with the field-list parser.
3825
+ const selectMatch = /\sFROM/i.test(query)
3826
+ ? /SELECT\s+(.+?)\s+FROM/i.exec(query) // NOSONAR S8786 — guarded above, measured linear
3827
+ : null;
3693
3828
  if (selectMatch) {
3694
3829
  const fieldsPart = selectMatch[1];
3695
3830
  let currentField = "";
@@ -3718,7 +3853,7 @@ export function parseWqlQuery(query) {
3718
3853
  if (finalF)
3719
3854
  result.fields.push(finalF);
3720
3855
  }
3721
- const paramMatches = query.matchAll(/(?:\?([0-9a-zA-Z_]+)?|:([a-zA-Z0-9_]+)|@\{([a-zA-Z0-9_]+)\})/g);
3856
+ const paramMatches = query.matchAll(/(?:\?(\w+)?|:(\w+)|@\{(\w+)\})/g);
3722
3857
  for (const match of paramMatches) {
3723
3858
  const param = match[1] || match[2] || match[3] || "?";
3724
3859
  if (!result.parameters.includes(param)) {
@@ -3774,7 +3909,7 @@ async function indexLsifDump(absolutePath, _relativePath, repoPath, db) {
3774
3909
  }
3775
3910
  else if (item.type === "edge") {
3776
3911
  const outV = item.outV.toString();
3777
- const inVs = item.inVs ? item.inVs.map((id) => String(id)) : [item.inV.toString()];
3912
+ const inVs = item.inVs ? item.inVs.map(String) : [item.inV.toString()];
3778
3913
  if (item.label === "contains") {
3779
3914
  for (const rId of inVs) {
3780
3915
  const r = ranges.get(rId);
@@ -3862,7 +3997,7 @@ async function indexLsifDump(absolutePath, _relativePath, repoPath, db) {
3862
3997
  const lines = summary.split("\n");
3863
3998
  if (lines.length > 0) {
3864
3999
  const firstLine = lines[0].replace(/```\w*/, "").trim();
3865
- const fnMatch = firstLine.match(/(?:function|class|interface|const|let|var)\s+([a-zA-Z0-9_]+)/);
4000
+ const fnMatch = /(?:function|class|interface|const|let|var)\s+(\w+)/.exec(firstLine);
3866
4001
  if (fnMatch) {
3867
4002
  name = fnMatch[1];
3868
4003
  }
@@ -3903,9 +4038,36 @@ async function indexLsifDump(absolutePath, _relativePath, repoPath, db) {
3903
4038
  console.error("Error indexing LSIF:", error);
3904
4039
  }
3905
4040
  }
4041
+ /**
4042
+ * Replace the imported-findings tier for one source, leaving graph facts and any
4043
+ * other tool's findings untouched. Re-importing the same log is idempotent.
4044
+ */
4045
+ function persistSarifFindings(db, facts) {
4046
+ const insert = db.prepare(`
4047
+ INSERT INTO findings
4048
+ (ruleId, tool, level, message, filePath, startLine, startCol, endLine, endCol, helpUri, source)
4049
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4050
+ `);
4051
+ db.run("BEGIN TRANSACTION;");
4052
+ try {
4053
+ // Scoped to the tools in this log: importing an ESLint log must not delete
4054
+ // PMD findings imported earlier from a different run.
4055
+ for (const tool of facts.tools)
4056
+ db.run("DELETE FROM findings WHERE tool = ?", [tool]);
4057
+ for (const finding of facts.findings) {
4058
+ insert.run(finding.ruleId, finding.tool, finding.level, finding.message, finding.filePath, finding.startLine, finding.startColumn, finding.endLine, finding.endColumn, finding.helpUri ?? null, facts.inputPath);
4059
+ }
4060
+ db.run("COMMIT;");
4061
+ }
4062
+ catch (error) {
4063
+ db.run("ROLLBACK;");
4064
+ throw error;
4065
+ }
4066
+ return facts.findings.length;
4067
+ }
3906
4068
  /** Replace only the optional SCIP tier, preserving native and LSIF facts verbatim. */
3907
4069
  function persistScipFacts(db, repoPath, facts) {
3908
- const affectedFiles = [...new Set(facts.symbols.map((symbol) => symbol.filePath))].sort();
4070
+ const affectedFiles = [...new Set(facts.symbols.map((symbol) => symbol.filePath))].sort(compareBytes);
3909
4071
  const insertSymbol = db.prepare(`
3910
4072
  INSERT INTO symbols (name, kind, filePath, startLine, endLine, startCol, endCol, summary)
3911
4073
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
@@ -3972,8 +4134,6 @@ function invalidateScipFacts(db) {
3972
4134
  }
3973
4135
  async function indexTerraformFile(content, relativePath, _repoPath, db) {
3974
4136
  const symbols = [];
3975
- const references = [];
3976
- const dependencies = [];
3977
4137
  const lines = content.split("\n");
3978
4138
  const blockRegex = /^\s*(resource|data|module)\s+"([^"]+)"\s+"([^"]+)"\s*\{/i;
3979
4139
  const blockRegexAlt = /^\s*(resource|data|module)\s+"([^"]+)"\s*\{/i;
@@ -4024,23 +4184,7 @@ async function indexTerraformFile(content, relativePath, _repoPath, db) {
4024
4184
  for (const def of symbols) {
4025
4185
  insertSym.run(def.name, def.kind, relativePath, def.startLine, def.endLine, def.startCol, def.endCol, def.summary);
4026
4186
  }
4027
- const insertRef = db.prepare(`
4028
- INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column)
4029
- VALUES (?, ?, ?, NULL, ?, ?)
4030
- `);
4031
- for (const ref of references) {
4032
- insertRef.run(ref.callerSymbol, relativePath, ref.calleeSymbol, ref.line, ref.column);
4033
- }
4034
- const insertDep = db.prepare(`
4035
- INSERT INTO dependencies (fromFile, toFile, kind, confidence)
4036
- VALUES (?, ?, ?, ?)
4037
- `);
4038
- for (const dep of dependencies) {
4039
- insertDep.run(relativePath, dep.toFile, dep.kind, dep.confidence);
4040
- }
4041
4187
  insertSym.finalize();
4042
- insertRef.finalize();
4043
- insertDep.finalize();
4044
4188
  db.run("COMMIT;");
4045
4189
  }
4046
4190
  catch (error) {
@@ -4102,6 +4246,385 @@ async function indexDockerFile(content, relativePath, _repoPath, db) {
4102
4246
  throw error;
4103
4247
  }
4104
4248
  }
4249
+ /**
4250
+ * Removes fenced blocks and inline code spans. A document demonstrating markdown
4251
+ * would otherwise contribute its examples as real edges, which is worse than
4252
+ * missing them: a fabricated dependency is indistinguishable from a true one.
4253
+ */
4254
+ function stripMarkdownCode(content) {
4255
+ return content
4256
+ .replace(/^```[\s\S]*?^```/gm, "")
4257
+ .replace(/^~~~[\s\S]*?^~~~/gm, "")
4258
+ .replace(/`[^`\n]*`/g, "");
4259
+ }
4260
+ // Linear-time by construction, which took three attempts to actually achieve.
4261
+ // Documentation is untrusted input here — it arrives in a mirror of someone
4262
+ // else's repository — so a quadratic scan is a denial-of-service risk, not a
4263
+ // style problem.
4264
+ //
4265
+ // Two things are required, and the first two attempts each got only one:
4266
+ //
4267
+ // 1. No ambiguity *within* an attempt. The optional `<…>` wrapper and trailing
4268
+ // `"title"` are no longer spelled out here; `markdownLinkTarget` splits them
4269
+ // off in code. Each class below is disjoint from what follows it.
4270
+ // 2. No quadratic blow-up *across* attempts. Excluding `[` from both classes is
4271
+ // what buys this. Without it, input like `[a[a[a…` with no closing bracket
4272
+ // starts an attempt at every `[`, and each one scans to end of input.
4273
+ //
4274
+ // A link target containing a literal `[` is therefore not recorded. That is the
4275
+ // safe direction: a missing edge, never a wrong one.
4276
+ const MARKDOWN_INLINE_LINK = /!?\[[^[\]\n]*\]\(([^[)\n]*)\)/g;
4277
+ // The label class excludes newlines. Without that, `[^\]]+` matches across
4278
+ // lines, so a stray unclosed `[` anywhere in a document swallows everything up
4279
+ // to the next `]:` and records an edge to whatever file happens to follow it —
4280
+ // a *fabricated* edge, which is worse than a missing one, because downstream it
4281
+ // is indistinguishable from a real dependency. A reference definition is a
4282
+ // single-line construct; the pattern now says so.
4283
+ const MARKDOWN_REFERENCE_DEFINITION = /^[ \t]{0,3}\[[^\]\n]+\]:[ \t]*<?([^>\s]+)>?/gm;
4284
+ /**
4285
+ * Extracts the target from a markdown link's parenthesised body, discarding an
4286
+ * optional `<…>` wrapper and a trailing `"title"`. Done in code rather than in
4287
+ * {@link MARKDOWN_INLINE_LINK} so the pattern stays backtrack-free.
4288
+ */
4289
+ function markdownLinkTarget(inner) {
4290
+ const trimmed = inner.trim();
4291
+ if (trimmed.startsWith("<")) {
4292
+ const close = trimmed.indexOf(">");
4293
+ return close === -1 ? trimmed.slice(1) : trimmed.slice(1, close);
4294
+ }
4295
+ const titleAt = trimmed.search(/[ \t]/);
4296
+ return titleAt === -1 ? trimmed : trimmed.slice(0, titleAt);
4297
+ }
4298
+ /** C0 controls and DEL. Never legitimate in a link target; see below. */
4299
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching them is the point
4300
+ const CONTROL_CHARACTERS = /[\x00-\x1f\x7f]/;
4301
+ /**
4302
+ * Resolves a markdown link target to a repository-relative path, or null when it
4303
+ * does not denote one (external URL, bare anchor, or an escape above the root).
4304
+ *
4305
+ * In a mirror this parses documentation from a repository nobody here has read,
4306
+ * so the target is untrusted. Two rejections exist for that reason:
4307
+ *
4308
+ * - Control characters, NUL above all. A NUL would otherwise be stored as part
4309
+ * of `toFile` and echoed in evidence, and C-based consumers truncate there:
4310
+ * a grep over such a path silently reports no matches, which reads as "this
4311
+ * file has no references" rather than "your query could not match". That
4312
+ * exact confusion cost real debugging time in this repository.
4313
+ * - Backslashes, normalized to `/` BEFORE the traversal check rather than
4314
+ * rejected. `path.posix` treats `..\..\x` as one odd filename, so the `..`
4315
+ * guard below never fires — and `path.join` on Windows would then read those
4316
+ * backslashes as separators and escape the repository. Normalizing first
4317
+ * makes the existing guard see the traversal it was written to catch.
4318
+ */
4319
+ function resolveDocumentLink(target, relativePath) {
4320
+ if (CONTROL_CHARACTERS.test(target))
4321
+ return null;
4322
+ // Any scheme (http:, https:, mailto:, ftp:) is off-repository.
4323
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target))
4324
+ return null;
4325
+ const forwardSlashed = target.replaceAll("\\", "/");
4326
+ if (forwardSlashed.startsWith("//"))
4327
+ return null;
4328
+ const withoutFragment = forwardSlashed.split("#")[0].split("?")[0];
4329
+ if (!withoutFragment)
4330
+ return null;
4331
+ const normalized = relativePath.replaceAll("\\", "/");
4332
+ const joined = withoutFragment.startsWith("/")
4333
+ ? path.posix.normalize(withoutFragment.slice(1))
4334
+ : path.posix.normalize(path.posix.join(path.posix.dirname(normalized), withoutFragment));
4335
+ if (joined.startsWith("..") || joined === "." || joined === "")
4336
+ return null;
4337
+ return joined;
4338
+ }
4339
+ /**
4340
+ * Link-only indexer for markdown. Produces no symbols — only `dependencies`
4341
+ * edges — which the schema permits: nothing joins `dependencies` to `symbols`,
4342
+ * and the health audit's orphan predicates key on `fromFile` being in
4343
+ * `index_state`, never on the file having contributed symbols.
4344
+ *
4345
+ * A target that resolves inside the repository but does not exist is recorded as
4346
+ * `doc_link_broken` rather than dropped, which is what makes stale documentation
4347
+ * a queryable fact instead of an absence.
4348
+ */
4349
+ async function indexMarkdownFile(content, relativePath, repoPath, db) {
4350
+ const edges = new Map();
4351
+ const body = stripMarkdownCode(content);
4352
+ const record = (rawTarget) => {
4353
+ const resolved = resolveDocumentLink(rawTarget, relativePath);
4354
+ if (!resolved || resolved === relativePath)
4355
+ return;
4356
+ const exists = fs.existsSync(path.join(repoPath, resolved));
4357
+ const kind = exists ? "doc_link" : "doc_link_broken";
4358
+ const key = `${kind}:${resolved}`;
4359
+ if (!edges.has(key))
4360
+ edges.set(key, {
4361
+ toFile: resolved,
4362
+ kind,
4363
+ // A broken target is a real observation but a weaker claim about
4364
+ // structure, so it never carries full confidence.
4365
+ confidence: exists ? 1.0 : 0.5,
4366
+ evidence: rawTarget.slice(0, 500),
4367
+ });
4368
+ };
4369
+ for (const match of body.matchAll(MARKDOWN_INLINE_LINK))
4370
+ record(markdownLinkTarget(match[1]));
4371
+ for (const match of body.matchAll(MARKDOWN_REFERENCE_DEFINITION))
4372
+ record(match[1]);
4373
+ persistLinkOnlyEdges(db, relativePath, [...edges.values()]);
4374
+ }
4375
+ /**
4376
+ * Well-known CI keys that name another pipeline file. Deliberately NOT general
4377
+ * YAML understanding: an over-broad extractor here would emit confident,
4378
+ * unfalsifiable edges from arbitrary config, which is worse than no coverage.
4379
+ *
4380
+ * `uses:` is matched only for workspace-relative composite actions (`./...`),
4381
+ * which GitHub resolves from the REPOSITORY ROOT. `template:`/`local:` are
4382
+ * resolved relative to the CONTAINING FILE, per Azure Pipelines and GitLab CI.
4383
+ * Registry references (`actions/checkout@v4`, `file.yml@resource`) are not files
4384
+ * in this repository and are skipped rather than recorded as fake paths.
4385
+ */
4386
+ const YAML_ROOT_RELATIVE_REF = /^[ \t-]*uses:[ \t]*["']?(\.[^"'\s]+)/gm;
4387
+ const YAML_FILE_RELATIVE_REF = /^[ \t-]*(?:template|local):[ \t]*["']?([^"'\s@]+\.ya?ml)/gm;
4388
+ /**
4389
+ * Link-only indexer for CI YAML. Produces `dependencies` edges between pipeline
4390
+ * files so a template's consumers are answerable; extracts no symbols.
4391
+ */
4392
+ async function indexYamlFile(content, relativePath, repoPath, db) {
4393
+ const edges = new Map();
4394
+ const record = (rawTarget, base) => {
4395
+ const resolved = base === "repo-root"
4396
+ ? resolveDocumentLink(rawTarget.replace(/^\.\//, ""), "placeholder.yml")
4397
+ : resolveDocumentLink(rawTarget, relativePath);
4398
+ if (!resolved || resolved === relativePath)
4399
+ return;
4400
+ const exists = fs.existsSync(path.join(repoPath, resolved));
4401
+ const kind = exists ? "workflow_ref" : "workflow_ref_broken";
4402
+ const key = `${kind}:${resolved}`;
4403
+ if (!edges.has(key))
4404
+ edges.set(key, {
4405
+ toFile: resolved,
4406
+ kind,
4407
+ confidence: exists ? 1.0 : 0.5,
4408
+ evidence: rawTarget.slice(0, 500),
4409
+ });
4410
+ };
4411
+ for (const match of content.matchAll(YAML_ROOT_RELATIVE_REF))
4412
+ record(match[1], "repo-root");
4413
+ for (const match of content.matchAll(YAML_FILE_RELATIVE_REF))
4414
+ record(match[1], "containing-file");
4415
+ persistLinkOnlyEdges(db, relativePath, [...edges.values()]);
4416
+ }
4417
+ /**
4418
+ * MSBuild project-to-project edges. `<ProjectReference>` is the dependency graph
4419
+ * between .NET projects; `<Import>` pulls in shared `.props`/`.targets`.
4420
+ * `<PackageReference>` is deliberately excluded — a NuGet package is not a file
4421
+ * in this repository, and recording one as a path would invent a target.
4422
+ */
4423
+ // Both classes exclude `<` as well as `>`, and that is what keeps the scan
4424
+ // linear rather than quadratic. Measured before and after on `.csproj` content
4425
+ // whose tags are never closed: with `[^>]*?` alone, 28k/56k/112k chars took
4426
+ // 6.0/22.7/92.5ms — doubling the input roughly quadrupled the time, because the
4427
+ // lazy scan runs to end of input from every `<ProjectReference` and a new
4428
+ // attempt starts at every one. Excluding `<` makes an attempt die at the next
4429
+ // tag instead. This is also the correct reading of XML: a `<` cannot appear
4430
+ // inside a tag or an attribute value, so nothing valid is lost.
4431
+ //
4432
+ // It matters because `.csproj`/`.props`/`.targets` are indexed from mirrors —
4433
+ // content from a repository nobody here has read.
4434
+ const MSBUILD_PROJECT_REFERENCE = /<(?:ProjectReference|Import)\s[^><]*?(?:Include|Project)="([^"<]+)"/g;
4435
+ /** `Project("{TYPE-GUID}") = "Name", "relative\path.csproj", "{PROJECT-GUID}"`. */
4436
+ // Every class excludes newlines, for the same reason as the markdown reference
4437
+ // definition above: a `.sln` project entry occupies one line, and letting the
4438
+ // classes span lines lets a malformed entry reach across the file and record a
4439
+ // path that was never written next to it.
4440
+ const SOLUTION_PROJECT_ENTRY = /^Project\("\{[^}\n]*\}"\)\s*=\s*"[^"\n]*",\s*"([^"\n]+)"/gm;
4441
+ /**
4442
+ * Link-only indexer for the .NET build graph: `.csproj`/`.props`/`.targets` via
4443
+ * MSBuild XML, and `.sln` via its own line format (which is not XML). Produces
4444
+ * `dependencies` edges only.
4445
+ */
4446
+ async function indexDotnetProjectFile(content, relativePath, repoPath, db) {
4447
+ const edges = new Map();
4448
+ const record = (rawTarget) => {
4449
+ // MSBuild and solution files use Windows separators even on Unix.
4450
+ const resolved = resolveDocumentLink(rawTarget.replaceAll("\\", "/"), relativePath);
4451
+ if (!resolved || resolved === relativePath)
4452
+ return;
4453
+ const exists = fs.existsSync(path.join(repoPath, resolved));
4454
+ const kind = exists ? "project_ref" : "project_ref_broken";
4455
+ const key = `${kind}:${resolved}`;
4456
+ if (!edges.has(key))
4457
+ edges.set(key, {
4458
+ toFile: resolved,
4459
+ kind,
4460
+ confidence: exists ? 1.0 : 0.5,
4461
+ evidence: rawTarget.slice(0, 500),
4462
+ });
4463
+ };
4464
+ const pattern = relativePath.endsWith(".sln")
4465
+ ? SOLUTION_PROJECT_ENTRY
4466
+ : MSBUILD_PROJECT_REFERENCE;
4467
+ for (const match of content.matchAll(pattern))
4468
+ record(match[1]);
4469
+ persistLinkOnlyEdges(db, relativePath, [...edges.values()]);
4470
+ }
4471
+ /** Net brace depth change across a line, ignoring braces inside strings. */
4472
+ function netBraceDepth(line) {
4473
+ let depth = 0;
4474
+ let quote = null;
4475
+ for (let i = 0; i < line.length; i++) {
4476
+ const char = line[i];
4477
+ if (quote) {
4478
+ if (char === "\\")
4479
+ i++;
4480
+ else if (char === quote)
4481
+ quote = null;
4482
+ continue;
4483
+ }
4484
+ if (char === '"' || char === "'")
4485
+ quote = char;
4486
+ else if (char === "{")
4487
+ depth++;
4488
+ else if (char === "}")
4489
+ depth--;
4490
+ }
4491
+ return depth;
4492
+ }
4493
+ /**
4494
+ * Projects a Razor file onto a C# buffer with the SAME NUMBER OF LINES, keeping
4495
+ * `@code` / `@functions` member bodies at their original line positions and
4496
+ * blanking everything else.
4497
+ *
4498
+ * Line geometry is preserved deliberately rather than reconstructed: symbols
4499
+ * parsed out of the projection carry line numbers that are already correct for
4500
+ * the original file, so there is no offset arithmetic to get wrong. That matters
4501
+ * more here than coverage — a Razor symbol reported at the wrong line is
4502
+ * confidently misleading evidence, which is worse than not indexing Razor at all.
4503
+ *
4504
+ * Only the opening `@code {` line is rewritten (to `class __RazorCode {`), so
4505
+ * columns on that one line shift; every member line is copied verbatim and keeps
4506
+ * both its line and column. Returns null when the file declares no member block.
4507
+ */
4508
+ /** Not global: this is tested per line, so `exec` must not carry lastIndex. */
4509
+ const RAZOR_MEMBER_BLOCK = /@(?:code|functions)\s*\{/;
4510
+ function projectRazorToCSharp(content) {
4511
+ const lines = content.split("\n");
4512
+ const projected = new Array(lines.length).fill("");
4513
+ let depth = 0;
4514
+ let found = false;
4515
+ for (let index = 0; index < lines.length; index++) {
4516
+ const line = lines[index];
4517
+ if (depth === 0) {
4518
+ const opener = RAZOR_MEMBER_BLOCK.exec(line);
4519
+ if (!opener)
4520
+ continue;
4521
+ const at = line.indexOf(opener[0]);
4522
+ const tail = line.slice(at + opener[0].length);
4523
+ projected[index] = `${" ".repeat(at)}class __RazorCode {${tail}`;
4524
+ depth = 1 + netBraceDepth(tail);
4525
+ found = true;
4526
+ if (depth <= 0)
4527
+ depth = 0;
4528
+ continue;
4529
+ }
4530
+ projected[index] = line;
4531
+ depth += netBraceDepth(line);
4532
+ if (depth <= 0)
4533
+ depth = 0;
4534
+ }
4535
+ return found ? projected.join("\n") : null;
4536
+ }
4537
+ /**
4538
+ * Indexes the C# members declared in a Razor file's `@code` / `@functions`
4539
+ * block, reusing the existing C# grammar and extractor rather than describing
4540
+ * Razor as its own language. Markup, directives and inline expressions are not
4541
+ * indexed.
4542
+ */
4543
+ async function indexRazorFile(content, relativePath, _repoPath, db) {
4544
+ const projected = projectRazorToCSharp(content);
4545
+ if (!projected)
4546
+ return;
4547
+ const language = await getLanguageForFile("razor-projection.cs");
4548
+ if (!language)
4549
+ return;
4550
+ const parser = new Parser();
4551
+ parser.setLanguage(language);
4552
+ const tree = parser.parse(projected);
4553
+ if (!tree)
4554
+ return;
4555
+ // `filePath` drives the extractor's language-specific branches, so it must
4556
+ // look like C#; the rows are persisted against the real Razor path below.
4557
+ const extracted = extractSymbolsAndReferences(tree.rootNode, false, false, "razor-projection.cs");
4558
+ tree.delete();
4559
+ parser.delete();
4560
+ // The synthetic wrapper class exists only to make members parseable.
4561
+ const definitions = extracted.definitions.filter((def) => def.name !== "__RazorCode");
4562
+ db.run("BEGIN TRANSACTION;");
4563
+ try {
4564
+ db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
4565
+ deleteSymbolsForFile(db, relativePath);
4566
+ db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
4567
+ // finalize() in a `finally`: a throw mid-loop would otherwise leave the
4568
+ // statement open, and an unfinalized statement can keep SQLite state alive
4569
+ // across the ROLLBACK below — which surfaces later as an unexplained lock
4570
+ // rather than as the error that actually caused it.
4571
+ const insertSym = db.prepare(`
4572
+ INSERT INTO symbols (name, kind, filePath, startLine, endLine, startCol, endCol, summary)
4573
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4574
+ `);
4575
+ try {
4576
+ for (const def of definitions) {
4577
+ insertSym.run(def.name, def.kind, relativePath, def.startLine, def.endLine, def.startCol, def.endCol, def.summary);
4578
+ }
4579
+ }
4580
+ finally {
4581
+ insertSym.finalize();
4582
+ }
4583
+ const insertRef = db.prepare(`
4584
+ INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column)
4585
+ VALUES (?, ?, ?, NULL, ?, ?)
4586
+ `);
4587
+ try {
4588
+ for (const ref of extracted.references)
4589
+ insertRef.run(ref.callerSymbol, relativePath, ref.calleeSymbol, ref.line, ref.column);
4590
+ }
4591
+ finally {
4592
+ insertRef.finalize();
4593
+ }
4594
+ db.run("COMMIT;");
4595
+ }
4596
+ catch (error) {
4597
+ db.run("ROLLBACK;");
4598
+ throw error;
4599
+ }
4600
+ }
4601
+ /** Standard delete-then-insert transaction shared by the link-only indexers. */
4602
+ function persistLinkOnlyEdges(db, relativePath, edges) {
4603
+ db.run("BEGIN TRANSACTION;");
4604
+ try {
4605
+ db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
4606
+ deleteSymbolsForFile(db, relativePath);
4607
+ db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
4608
+ // See persistRazorSymbols: finalize in a `finally` so a mid-loop throw
4609
+ // cannot leave the statement open across the ROLLBACK.
4610
+ const insertDep = db.prepare(`
4611
+ INSERT INTO dependencies (fromFile, toFile, kind, confidence, sourceEvidence)
4612
+ VALUES (?, ?, ?, ?, ?)
4613
+ `);
4614
+ try {
4615
+ for (const edge of edges)
4616
+ insertDep.run(relativePath, edge.toFile, edge.kind, edge.confidence, edge.evidence);
4617
+ }
4618
+ finally {
4619
+ insertDep.finalize();
4620
+ }
4621
+ db.run("COMMIT;");
4622
+ }
4623
+ catch (error) {
4624
+ db.run("ROLLBACK;");
4625
+ throw error;
4626
+ }
4627
+ }
4105
4628
  async function indexWorkdayFile(content, tree, relativePath, _repoPath, db) {
4106
4629
  const symbols = [];
4107
4630
  const references = [];
@@ -4189,7 +4712,7 @@ async function indexWorkdayFile(content, tree, relativePath, _repoPath, db) {
4189
4712
  if (contentNode) {
4190
4713
  for (let i = 0; i < contentNode.childCount; i++) {
4191
4714
  const child = contentNode.child(i);
4192
- if (child && child.type === "element") {
4715
+ if (child?.type === "element") {
4193
4716
  const childStagNode = child.children.find((c) => c.type === "STag" || c.type === "EmptyElemTag");
4194
4717
  if (childStagNode) {
4195
4718
  const childTagNameNode = childStagNode.children.find((c) => c.type === "Name");
@@ -4249,17 +4772,20 @@ async function indexWorkdayFile(content, tree, relativePath, _repoPath, db) {
4249
4772
  const op = match[1].trim();
4250
4773
  addDependency(op, "wws", 1.0);
4251
4774
  }
4252
- const operationTagRegex = /<(?:[a-zA-Z0-9_]+:)?operation[^>]*>([^<]+)<\/(?:[a-zA-Z0-9_]+:)?operation>/gi;
4775
+ const operationTagRegex = /<(?:\w+:)?operation[^>]*>([^<]+)<\/(?:\w+:)?operation>/gi;
4253
4776
  const tagMatches = content.matchAll(operationTagRegex);
4254
4777
  for (const match of tagMatches) {
4255
4778
  const op = match[1].trim();
4256
4779
  addDependency(op, "wws", 1.0);
4257
4780
  }
4258
- const wqlQueryRegex = /SELECT\s+([\s\S]+?)\s+FROM\s+([a-zA-Z0-9_]+)/gi;
4781
+ // The select list is matched but never read, so it stays uncaptured. Pinning it to
4782
+ // end on a non-whitespace character (with a bare `\s` for the single-whitespace
4783
+ // case) keeps `\s+FROM` from re-splitting every whitespace run, which is quadratic.
4784
+ const wqlQueryRegex = /SELECT\s(?:\s|[\s\S]*?(?<!\s))\s+FROM\s+(\w+)/gi;
4259
4785
  const wqlMatches = content.matchAll(wqlQueryRegex);
4260
4786
  for (const match of wqlMatches) {
4261
4787
  const fullQuery = match[0];
4262
- const table = match[2].trim();
4788
+ const table = match[1].trim();
4263
4789
  const wqlRes = parseWqlQuery(fullQuery);
4264
4790
  const targetTable = wqlRes.table || table;
4265
4791
  if (targetTable) {
@@ -4326,8 +4852,8 @@ async function indexAuraBundleFile(content, relativePath, repoPath, db, bundle)
4326
4852
  // computed strings, and client-side action names deliberately produce no edge.
4327
4853
  if (bundle.extension === "cmp") {
4328
4854
  const markup = content.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
4329
- const componentTag = markup.match(/<aura:component\b[^>]*>/i)?.[0];
4330
- const controller = componentTag?.match(/\bcontroller\s*=\s*["']([A-Za-z][A-Za-z0-9_]*)["']/i)?.[1];
4855
+ const componentTag = /<aura:component\b[^>]*>/i.exec(markup)?.[0];
4856
+ const controller = /\bcontroller\s*=\s*["']([a-z]\w*)["']/i.exec(componentTag ?? "")?.[1];
4331
4857
  if (controller) {
4332
4858
  const target = findUniqueApexClassFilePath(controller, repoPath);
4333
4859
  if (target)
@@ -4339,7 +4865,7 @@ async function indexAuraBundleFile(content, relativePath, repoPath, db, bundle)
4339
4865
  }
4340
4866
  // An Aura markup tag in the local `c` namespace is a declared, static LWC
4341
4867
  // target only when the matching Salesforce DX LWC entry point exists.
4342
- for (const match of markup.matchAll(/<c:([A-Za-z][A-Za-z0-9_]*)\b/g)) {
4868
+ for (const match of markup.matchAll(/<c:([A-Za-z]\w*)\b/g)) {
4343
4869
  const target = findSalesforceLwcComponentFile(match[1], repoPath);
4344
4870
  if (target)
4345
4871
  dependencies.push({
@@ -4510,11 +5036,11 @@ async function indexVisualforceFile(content, tree, relativePath, repoPath, db) {
4510
5036
  const pageTagMatches = content.matchAll(pageTagRegex);
4511
5037
  for (const pageTagMatch of pageTagMatches) {
4512
5038
  const tagContent = pageTagMatch[0];
4513
- const controllerMatch = tagContent.match(/controller\s*=\s*["']([^"']+)["']/i);
5039
+ const controllerMatch = /controller\s*=\s*["']([^"']+)["']/i.exec(tagContent);
4514
5040
  if (controllerMatch?.[1]) {
4515
5041
  controllerNames.add(controllerMatch[1].trim());
4516
5042
  }
4517
- const extensionsMatch = tagContent.match(/extensions\s*=\s*["']([^"']+)["']/i);
5043
+ const extensionsMatch = /extensions\s*=\s*["']([^"']+)["']/i.exec(tagContent);
4518
5044
  if (extensionsMatch?.[1]) {
4519
5045
  const extList = extensionsMatch[1]
4520
5046
  .split(",")
@@ -4531,7 +5057,7 @@ async function indexVisualforceFile(content, tree, relativePath, repoPath, db) {
4531
5057
  const exprMatches = content.matchAll(exprRegex);
4532
5058
  for (const match of exprMatches) {
4533
5059
  const expr = match[1];
4534
- const words = expr.match(/[a-zA-Z_][a-zA-Z0-9_]*/g);
5060
+ const words = expr.match(/[a-zA-Z_]\w*/g);
4535
5061
  if (words) {
4536
5062
  for (const word of words) {
4537
5063
  if (["true", "false", "null", "and", "or", "not", "if"].includes(word.toLowerCase())) {
@@ -4658,7 +5184,7 @@ async function indexSqlFile(content, tree, relativePath, _repoPath, db) {
4658
5184
  }
4659
5185
  if (node.type === "relation" || node.type === "object_reference") {
4660
5186
  const refName = node.text.replace(/["`]/g, "").trim();
4661
- if (refName && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(refName)) {
5187
+ if (refName && /^[a-zA-Z_]\w*$/.test(refName)) {
4662
5188
  references.push({
4663
5189
  callerSymbol: null,
4664
5190
  calleeSymbol: refName,
@@ -4675,7 +5201,7 @@ async function indexSqlFile(content, tree, relativePath, _repoPath, db) {
4675
5201
  }
4676
5202
  }
4677
5203
  traverse(tree.rootNode);
4678
- const procRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?(PROCEDURE|PROC|FUNCTION|TRIGGER)\s+([a-zA-Z0-9_]+)/gi;
5204
+ const procRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?(PROCEDURE|PROC|FUNCTION|TRIGGER)\s+(\w+)/gi;
4679
5205
  let procMatch = procRegex.exec(content);
4680
5206
  while (procMatch !== null) {
4681
5207
  const type = procMatch[1].toLowerCase();
@@ -4693,7 +5219,7 @@ async function indexSqlFile(content, tree, relativePath, _repoPath, db) {
4693
5219
  });
4694
5220
  procMatch = procRegex.exec(content);
4695
5221
  }
4696
- const lineageRegex = /(?:FROM|JOIN|INTO|UPDATE)\s+([a-zA-Z0-9_]+)/gi;
5222
+ const lineageRegex = /(?:FROM|JOIN|INTO|UPDATE)\s+(\w+)/gi;
4697
5223
  let lineageMatch = lineageRegex.exec(content);
4698
5224
  while (lineageMatch !== null) {
4699
5225
  const tblName = lineageMatch[1];
@@ -4763,7 +5289,7 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
4763
5289
  if (block) {
4764
5290
  for (let i = 0; i < block.childCount; i++) {
4765
5291
  const colDecl = block.child(i);
4766
- if (colDecl && colDecl.type === "column_declaration") {
5292
+ if (colDecl?.type === "column_declaration") {
4767
5293
  const typeNode = colDecl.childForFieldName("type") ||
4768
5294
  colDecl.children.find((c) => c.type === "column_type");
4769
5295
  if (typeNode) {
@@ -4774,8 +5300,7 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
4774
5300
  from: modelName,
4775
5301
  to: targetModel,
4776
5302
  kind: "orm-relation",
4777
- });
4778
- dependencies.push({
5303
+ }, {
4779
5304
  from: relativePath,
4780
5305
  to: targetModel,
4781
5306
  kind: "orm-relation",
@@ -4795,12 +5320,14 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
4795
5320
  }
4796
5321
  }
4797
5322
  traverse(tree.rootNode);
4798
- const modelBlockRegex = /model\s+([a-zA-Z0-9_]+)\s*\{([^}]+)\}/g;
5323
+ const modelBlockRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
4799
5324
  let mBlock = modelBlockRegex.exec(content);
4800
5325
  while (mBlock !== null) {
4801
5326
  const srcModel = mBlock[1];
4802
5327
  const blockContent = mBlock[2];
4803
- const relationRegex = /[a-zA-Z0-9_]+\s+([a-zA-Z0-9_]+)\s+@relation/g;
5328
+ // `(?<!\w)` pins the leading field name to a word start; without it the scan
5329
+ // restarts inside every identifier and re-walks it, which is quadratic.
5330
+ const relationRegex = /(?<!\w)\w+\s+(\w+)\s+@relation/g;
4804
5331
  let rel = relationRegex.exec(blockContent);
4805
5332
  while (rel !== null) {
4806
5333
  const targetModel = rel[1];
@@ -4874,7 +5401,7 @@ export function parseSimpleNameFromUniqueId(uniqueId) {
4874
5401
  const parts = uniqueId.split(".");
4875
5402
  if (parts.length >= 3) {
4876
5403
  if (parts[0] === "source") {
4877
- return `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
5404
+ return `${parts.at(-2)}.${parts.at(-1)}`;
4878
5405
  }
4879
5406
  return parts[parts.length - 1];
4880
5407
  }
@@ -5081,8 +5608,16 @@ export async function indexDbtManifestFile(content, _absolutePath, relativePath,
5081
5608
  console.error(`Error indexing dbt manifest file ${relativePath}:`, error);
5082
5609
  }
5083
5610
  }
5084
- /** Index a single file's symbols and references into the SQLite database. */
5085
- async function indexFile(absolutePath, relativePath, repoPath, db) {
5611
+ /**
5612
+ * Index a single file's symbols and references into the SQLite database.
5613
+ *
5614
+ * `unparsed`, when supplied, receives a per-extension tally of files that were
5615
+ * collected as indexable but produced no graph facts — no grammar claimed them,
5616
+ * they were rejected as minified, or indexing threw. These are invisible
5617
+ * otherwise: the function swallows all three cases, so a whole language silently
5618
+ * contributing nothing looks exactly like a language with nothing to say.
5619
+ */
5620
+ async function indexFile(absolutePath, relativePath, repoPath, db, unparsed) {
5086
5621
  try {
5087
5622
  invalidateScipFacts(db);
5088
5623
  const baseName = path.basename(absolutePath).toLowerCase();
@@ -5135,16 +5670,45 @@ async function indexFile(absolutePath, relativePath, repoPath, db) {
5135
5670
  await indexSalesforceMetadataFile(content, relativePath, repoPath, db, salesforceMetadata);
5136
5671
  return;
5137
5672
  }
5673
+ if (relativePath.endsWith(".md") || relativePath.endsWith(".mdx")) {
5674
+ const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
5675
+ await indexMarkdownFile(content, relativePath, repoPath, db);
5676
+ return;
5677
+ }
5678
+ if (relativePath.endsWith(".yml") || relativePath.endsWith(".yaml")) {
5679
+ const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
5680
+ await indexYamlFile(content, relativePath, repoPath, db);
5681
+ return;
5682
+ }
5683
+ if (relativePath.endsWith(".csproj") ||
5684
+ relativePath.endsWith(".sln") ||
5685
+ relativePath.endsWith(".props") ||
5686
+ relativePath.endsWith(".targets")) {
5687
+ const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
5688
+ await indexDotnetProjectFile(content, relativePath, repoPath, db);
5689
+ return;
5690
+ }
5691
+ if (relativePath.endsWith(".cshtml") || relativePath.endsWith(".razor")) {
5692
+ const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
5693
+ await indexRazorFile(content, relativePath, repoPath, db);
5694
+ return;
5695
+ }
5138
5696
  const lang = await measurePerfPhase("parser_wasm_init", () => getLanguageForFile(absolutePath));
5139
- if (!lang)
5697
+ if (!lang) {
5698
+ if (unparsed)
5699
+ tallyOne(unparsed, extensionBucket(relativePath));
5140
5700
  return;
5701
+ }
5141
5702
  const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
5142
5703
  // Skip minified / generated bundles (e.g. a `*.bundle.js`): they're not
5143
5704
  // readable source and would inject thousands of junk "symbols" that pollute
5144
5705
  // the graph, communities, and hubs. Heuristic: a very high average line
5145
5706
  // length (minified code packs everything onto a few enormous lines).
5146
- if (isMinifiedSource(content))
5707
+ if (isMinifiedSource(content)) {
5708
+ if (unparsed)
5709
+ tallyOne(unparsed, extensionBucket(relativePath));
5147
5710
  return;
5711
+ }
5148
5712
  const isSql = absolutePath.endsWith(".sql") ||
5149
5713
  absolutePath.endsWith(".pkb") ||
5150
5714
  absolutePath.endsWith(".pks");
@@ -5305,6 +5869,8 @@ async function indexFile(absolutePath, relativePath, repoPath, db) {
5305
5869
  }
5306
5870
  catch (error) {
5307
5871
  console.error(`Error indexing file ${relativePath}:`, error);
5872
+ if (unparsed)
5873
+ tallyOne(unparsed, extensionBucket(relativePath));
5308
5874
  }
5309
5875
  }
5310
5876
  // The installed Transformers runtime was slower for every tested multi-item
@@ -5538,13 +6104,143 @@ function isTestFilePath(relPath) {
5538
6104
  return /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__)\//i.test(relPath);
5539
6105
  }
5540
6106
  /**
5541
- * Enumerate the repo-relative files knodin indexes: source files plus dbt
5542
- * `manifest.json` files and Dockerfiles. Shared by the full index and the
5543
- * cold-start reconcile so both see the identical candidate set.
6107
+ * Repo-relative paths git knows about (tracked plus untracked-but-not-ignored),
6108
+ * or null when `repoPath` is not itself the root of a readable git work tree —
6109
+ * in which case the caller falls back to walking the directory.
5544
6110
  */
6111
+ /** Extension bucket for coverage tallies; extensionless files group together. */
6112
+ function extensionBucket(relativePath) {
6113
+ const ext = path.posix.extname(relativePath.replaceAll("\\", "/")).toLowerCase();
6114
+ return ext === "" ? "(no extension)" : ext;
6115
+ }
6116
+ /** Adds one to `tally[bucket]`. */
6117
+ function tallyOne(tally, bucket) {
6118
+ tally.set(bucket, (tally.get(bucket) ?? 0) + 1);
6119
+ }
5545
6120
  function collectRepoFiles(repoPath) {
5546
- const files = [];
5547
- let candidates = null;
6121
+ return collectRepoFilesWithCoverage(repoPath).files;
6122
+ }
6123
+ /** Descending-count record, so the largest coverage gap reads first. */
6124
+ function sortedTally(tally) {
6125
+ return Object.fromEntries([...tally].sort((left, right) => right[1] - left[1]));
6126
+ }
6127
+ function sumCounts(counts) {
6128
+ let total = 0;
6129
+ for (const value of Object.values(counts))
6130
+ total += value;
6131
+ return total;
6132
+ }
6133
+ /**
6134
+ * Reads the unparsed tally persisted by the last full index, or null when it
6135
+ * cannot be read at all.
6136
+ *
6137
+ * Null and `{}` mean different things and must not be collapsed. A full index
6138
+ * always writes this key, even when nothing was skipped, so a *present* empty
6139
+ * object is a real "no gaps" answer. Absent, unreadable, or malformed means no
6140
+ * full index has completed under this schema — the gap is unknown, not zero.
6141
+ * Returning `{}` for both is how "I could not measure it" becomes "there is
6142
+ * nothing to measure", which is the failure this whole tally exists to prevent.
6143
+ *
6144
+ * Still never throws: a status call must not fail because a tally is corrupt.
6145
+ */
6146
+ function readUnparsedTally(db) {
6147
+ if (!db)
6148
+ return null;
6149
+ try {
6150
+ const raw = getMeta(db, COVERAGE_UNPARSED_META_KEY);
6151
+ if (!raw)
6152
+ return null;
6153
+ const parsed = JSON.parse(raw);
6154
+ if (typeof parsed !== "object" || parsed === null)
6155
+ return null;
6156
+ const counts = {};
6157
+ for (const [key, value] of Object.entries(parsed))
6158
+ if (typeof value === "number" && Number.isFinite(value))
6159
+ counts[key] = value;
6160
+ return counts;
6161
+ }
6162
+ catch {
6163
+ return null;
6164
+ }
6165
+ }
6166
+ /**
6167
+ * The `mirror` block for a status result, or nothing when `repoPath` is an
6168
+ * ordinary checkout. Spread into the result so the key is absent rather than
6169
+ * undefined for non-mirrors.
6170
+ */
6171
+ function mirrorSnapshotFor(repoPath) {
6172
+ const record = lookupMirror(repoPath);
6173
+ if (!record)
6174
+ return {};
6175
+ return { mirror: { url: record.url, sha: record.sha, fetchedAt: record.fetchedAt } };
6176
+ }
6177
+ /**
6178
+ * Compares indexed symbols against embedded ones. A repository with no symbols
6179
+ * is `ready`: nothing is missing. Failure to read either count reports `absent`
6180
+ * rather than assuming completeness — the whole point is to avoid claiming
6181
+ * coverage we cannot demonstrate.
6182
+ */
6183
+ export function semanticReadinessFor(db) {
6184
+ try {
6185
+ const symbols = db.query("SELECT COUNT(*) count FROM symbols").get()?.count ?? 0;
6186
+ if (symbols === 0)
6187
+ return "ready";
6188
+ const embedded = db.query("SELECT COUNT(*) count FROM symbol_embeddings").get()
6189
+ ?.count ?? 0;
6190
+ if (embedded === 0)
6191
+ return "absent";
6192
+ return embedded < symbols ? "partial" : "ready";
6193
+ }
6194
+ catch {
6195
+ return "absent";
6196
+ }
6197
+ }
6198
+ /** Worst readiness across federated repositories; any gap degrades the page. */
6199
+ function worstSemanticReadiness(states) {
6200
+ if (states.includes("absent"))
6201
+ return "absent";
6202
+ if (states.includes("partial"))
6203
+ return "partial";
6204
+ return "ready";
6205
+ }
6206
+ /** Combines policy-level skips with the last index's unparsed tally. */
6207
+ function buildCoverageSkips(skippedByExtension, db) {
6208
+ const byExtension = sortedTally(skippedByExtension);
6209
+ const unparsed = readUnparsedTally(db);
6210
+ return {
6211
+ byExtension,
6212
+ // Reported as empty when unknown so consumers reading only this field are
6213
+ // unchanged; `unparsedUnknown` is what tells them the emptiness is not a
6214
+ // measurement. `total` is then a lower bound, not a count.
6215
+ unparsedByExtension: unparsed ?? {},
6216
+ total: sumCounts(byExtension) + sumCounts(unparsed ?? {}),
6217
+ ...(unparsed === null ? { unparsedUnknown: true } : {}),
6218
+ };
6219
+ }
6220
+ /**
6221
+ * Whether a `.json` candidate actually holds indexable symbols. Only Salesforce
6222
+ * experience bundles and dbt manifests do; everything else is configuration. An
6223
+ * unreadable or malformed file is not a manifest, so it is skipped like any
6224
+ * other non-source JSON rather than failing collection.
6225
+ */
6226
+ function isIndexableJsonCandidate(file, repoPath) {
6227
+ if (salesforceExperienceFile(file))
6228
+ return true;
6229
+ if (path.basename(file).toLowerCase() !== "manifest.json")
6230
+ return false;
6231
+ try {
6232
+ return isDbtManifest(fs.readFileSync(path.join(repoPath, file), "utf-8"));
6233
+ }
6234
+ catch {
6235
+ // Deliberate (comment kept from main's equivalent, removed in this merge):
6236
+ // the candidate list is a snapshot, so a `manifest.json` may be unreadable,
6237
+ // deleted, or not valid UTF-8 by the time we read it. None of that is an
6238
+ // indexing failure — the file simply cannot be confirmed as a dbt manifest,
6239
+ // so it is not indexed as one.
6240
+ return false;
6241
+ }
6242
+ }
6243
+ function gitTrackedCandidates(repoPath) {
5548
6244
  try {
5549
6245
  const gitRoot = child_process
5550
6246
  .execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -5553,48 +6249,63 @@ function collectRepoFiles(repoPath) {
5553
6249
  stdio: ["ignore", "pipe", "ignore"],
5554
6250
  })
5555
6251
  .trim();
5556
- if (gitRoot && isSameDir(gitRoot, repoPath)) {
5557
- candidates = splitNulPaths(child_process.execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
5558
- cwd: repoPath,
5559
- encoding: "utf8",
5560
- stdio: ["ignore", "pipe", "ignore"],
5561
- maxBuffer: 64 * 1024 * 1024,
5562
- })).filter((file) => {
5563
- try {
5564
- return fs.statSync(path.join(repoPath, file)).isFile();
5565
- }
5566
- catch {
5567
- return false;
5568
- }
5569
- });
5570
- }
6252
+ if (!gitRoot || !isSameDir(gitRoot, repoPath))
6253
+ return null;
6254
+ return splitNulPaths(child_process.execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
6255
+ cwd: repoPath,
6256
+ encoding: "utf8",
6257
+ stdio: ["ignore", "pipe", "ignore"],
6258
+ maxBuffer: 64 * 1024 * 1024,
6259
+ })).filter((file) => {
6260
+ try {
6261
+ return fs.statSync(path.join(repoPath, file)).isFile();
6262
+ }
6263
+ catch {
6264
+ return false;
6265
+ }
6266
+ });
5571
6267
  }
5572
6268
  catch {
5573
- candidates = null;
6269
+ return null;
5574
6270
  }
5575
- for (const file of candidates ?? walkRepoFiles(repoPath, { accept: isIndexableSourcePath })) {
5576
- if (!isIndexableSourcePath(file))
6271
+ }
6272
+ /**
6273
+ * Enumerate the repo-relative files knodin indexes: source files plus dbt
6274
+ * `manifest.json` files and Dockerfiles. Shared by the full index and the
6275
+ * cold-start reconcile so both see the identical candidate set.
6276
+ */
6277
+ function collectRepoFilesWithCoverage(repoPath) {
6278
+ const files = [];
6279
+ const skippedByExtension = new Map();
6280
+ const candidates = gitTrackedCandidates(repoPath);
6281
+ // The walk deliberately does NOT pre-filter with `accept`: both branches must
6282
+ // see the same candidate superset so the skip tally below counts identically
6283
+ // whether or not the repository is a Git work tree. (main passed
6284
+ // `{ accept: isIndexableSourcePath }` here; filtering during the walk would
6285
+ // make the excluded files invisible to the tally, which is the whole point of
6286
+ // this function.)
6287
+ for (const file of candidates ?? walkRepoFiles(repoPath)) {
6288
+ // Pruned infrastructure (node_modules, dist, .git, state dirs) is not a
6289
+ // coverage gap and is never counted as one.
6290
+ if (!isIndexablePath(file))
6291
+ continue;
6292
+ if (!isIndexableSourcePath(file)) {
6293
+ tallyOne(skippedByExtension, extensionBucket(file));
5577
6294
  continue;
5578
- const baseName = path.basename(file).toLowerCase();
5579
- if (file.endsWith(".json")) {
5580
- // Only dbt manifest.json files hold indexable symbols; skip all other JSON.
5581
- if (salesforceExperienceFile(file)) {
5582
- files.push(file);
5583
- }
5584
- else if (baseName === "manifest.json") {
5585
- try {
5586
- if (isDbtManifest(fs.readFileSync(path.join(repoPath, file), "utf-8"))) {
5587
- files.push(file);
5588
- }
5589
- }
5590
- catch (_) { }
5591
- }
5592
6295
  }
5593
- else {
6296
+ if (!file.endsWith(".json")) {
5594
6297
  files.push(file);
5595
6298
  }
6299
+ else if (isIndexableJsonCandidate(file, repoPath)) {
6300
+ files.push(file);
6301
+ }
6302
+ else {
6303
+ tallyOne(skippedByExtension, ".json");
6304
+ }
5596
6305
  }
5597
- return files.sort();
6306
+ // Sorted on a copy with `compareBytes`, main's shared byte-order comparator,
6307
+ // so both file-discovery paths produce identical machine-independent ordering.
6308
+ return { files: [...files].sort(compareBytes), skippedByExtension };
5598
6309
  }
5599
6310
  /** Current HEAD sha of a git work tree, or null when not a git repo / no commits. */
5600
6311
  function gitHead(repoPath) {
@@ -5832,7 +6543,7 @@ async function reconcileIndex(repoPath, db, progress) {
5832
6543
  changed.add(file);
5833
6544
  const candidates = [...changed]
5834
6545
  .filter((file) => isIndexableSourcePath(file) || indexedPaths.has(file))
5835
- .sort();
6546
+ .sort(compareBytes);
5836
6547
  progress?.("indexing-files", 0, `Reconciling ${candidates.length.toLocaleString()} changed files`, {
5837
6548
  phaseTotal: candidates.length,
5838
6549
  });
@@ -5876,6 +6587,16 @@ async function reconcileIndex(repoPath, db, progress) {
5876
6587
  await indexEmbeddings(db, repoPath, progress);
5877
6588
  indexGeneration++;
5878
6589
  }
6590
+ else if (semanticReadinessFor(db) !== "ready") {
6591
+ // Unchanged files do NOT imply current embeddings: a deferred
6592
+ // (`skipEmbeddings`) or interrupted pass leaves symbols unembedded while
6593
+ // every file looks reconciled. Without this, the follow-up index a user
6594
+ // is told to run would do nothing and semantic search would stay
6595
+ // silently short forever.
6596
+ progress?.("finalizing", 0, "Completing deferred semantic embeddings");
6597
+ await indexEmbeddings(db, repoPath, progress);
6598
+ indexGeneration++;
6599
+ }
5879
6600
  // Advance the stored HEAD so the next cold start diffs forward from here,
5880
6601
  // and snapshot the working tree so the per-query guard (R9) can tell
5881
6602
  // "dirty, and indexed that way" from "dirty, and moved since".
@@ -6073,7 +6794,8 @@ function gitWorkTreeProbe(repoPath) {
6073
6794
  dirty.push(p);
6074
6795
  }
6075
6796
  }
6076
- return { head, root, dirty: dirty.sort() };
6797
+ dirty.sort(compareBytes);
6798
+ return { head, root, dirty };
6077
6799
  }
6078
6800
  /** True when two paths name the same directory (symlinks resolved). */
6079
6801
  function isSameDir(a, b) {
@@ -6242,9 +6964,14 @@ function stalenessFor(repoPath) {
6242
6964
  return freshnessProbes.get(path.resolve(repoPath))?.staleness ?? "unknown";
6243
6965
  }
6244
6966
  /** Recursively indexes all matching files within the repository. */
6245
- async function indexRepo(repoPath, db, progress) {
6967
+ async function indexRepo(repoPath, db, progress, skipEmbeddings = false) {
6246
6968
  progress?.("collecting-files", 0, "Discovering indexable files");
6247
- const files = measurePerfPhaseSync("file_collection", () => collectRepoFiles(repoPath));
6969
+ const collected = measurePerfPhaseSync("file_collection", () => collectRepoFilesWithCoverage(repoPath));
6970
+ const files = collected.files;
6971
+ // Files that reach the indexer but yield nothing can only be observed here,
6972
+ // during the parse. Persisted below so `status` can report the gap without
6973
+ // re-indexing the repository to rediscover it.
6974
+ const unparsedByExtension = new Map();
6248
6975
  progress?.("indexing-files", 0, `Indexing ${files.length.toLocaleString()} files`, {
6249
6976
  phaseTotal: files.length,
6250
6977
  });
@@ -6258,7 +6985,7 @@ async function indexRepo(repoPath, db, progress) {
6258
6985
  let lastYieldAt = Date.now();
6259
6986
  for (let offset = 0; offset < files.length; offset++) {
6260
6987
  const file = files[offset];
6261
- await indexFile(path.join(repoPath, file), file, repoPath, db);
6988
+ await indexFile(path.join(repoPath, file), file, repoPath, db, unparsedByExtension);
6262
6989
  recordIndexState(db, repoPath, file);
6263
6990
  progress?.("indexing-files", offset + 1, `Indexing ${files.length.toLocaleString()} files`, {
6264
6991
  phaseTotal: files.length,
@@ -6270,11 +6997,17 @@ async function indexRepo(repoPath, db, progress) {
6270
6997
  }
6271
6998
  progress?.("finalizing", 0, "Persisting symbol identities and semantic index");
6272
6999
  persistSymbolIdentities(db, repoPath);
6273
- await indexEmbeddings(db, repoPath, progress);
7000
+ // Structure is complete at this point; embeddings are the expensive tail. When
7001
+ // deferred, `semanticReadiness` reports the gap until a later pass fills it —
7002
+ // `indexEmbeddings` only ever embeds symbols that lack one, so resuming is
7003
+ // cheap and repeating is free.
7004
+ if (!skipEmbeddings)
7005
+ await indexEmbeddings(db, repoPath, progress);
6274
7006
  // Remember the HEAD we indexed at so a later cold start can diff forward.
6275
7007
  setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
6276
7008
  setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
6277
7009
  setMeta(db, "mcpBackfillVersion", "17");
7010
+ setMeta(db, COVERAGE_UNPARSED_META_KEY, JSON.stringify(Object.fromEntries(unparsedByExtension)));
6278
7011
  recordFreshnessBaseline(repoPath, db);
6279
7012
  indexGeneration++;
6280
7013
  }
@@ -6321,7 +7054,7 @@ function startFileWatcher(repoPath, db) {
6321
7054
  return queue.flushPromise;
6322
7055
  queue.flushPromise = (async () => {
6323
7056
  do {
6324
- const paths = [...queue.pending].sort();
7057
+ const paths = [...queue.pending].sort(compareBytes);
6325
7058
  queue.pending.clear();
6326
7059
  if (paths.length === 0)
6327
7060
  break;
@@ -6416,27 +7149,31 @@ async function getOrInitDb(repoPath, options = {}) {
6416
7149
  dbPath = ":memory:";
6417
7150
  }
6418
7151
  else {
6419
- const knodinDir = path.join(normalizedPath, ".knodin");
6420
- await fs.promises.mkdir(knodinDir, { recursive: true });
6421
- // Ensure .knodin is in .gitignore
6422
- try {
6423
- const gitignorePath = path.join(normalizedPath, ".gitignore");
6424
- let gitignoreContent = "";
6425
- if (fs.existsSync(gitignorePath)) {
6426
- gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
7152
+ const stateDir = resolveStateDir(normalizedPath);
7153
+ await fs.promises.mkdir(stateDir, { recursive: true });
7154
+ // Ensure .knodin is in .gitignore. Skipped for mirrors: their state
7155
+ // lives outside the clone, and the clone must stay byte-identical to
7156
+ // the remote so a refetch has nothing of ours to discard.
7157
+ if (mayWriteToRepository(normalizedPath)) {
7158
+ try {
7159
+ const gitignorePath = path.join(normalizedPath, ".gitignore");
7160
+ let gitignoreContent = "";
7161
+ if (fs.existsSync(gitignorePath)) {
7162
+ gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
7163
+ }
7164
+ const lines = gitignoreContent.split("\n").map((l) => l.trim());
7165
+ if (!lines.includes(".knodin") &&
7166
+ !lines.includes(".knodin/") &&
7167
+ !lines.includes("/.knodin")) {
7168
+ const prefix = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
7169
+ await fs.promises.appendFile(gitignorePath, `${prefix}\n# knodin\n.knodin\n`);
7170
+ }
6427
7171
  }
6428
- const lines = gitignoreContent.split("\n").map((l) => l.trim());
6429
- if (!lines.includes(".knodin") &&
6430
- !lines.includes(".knodin/") &&
6431
- !lines.includes("/.knodin")) {
6432
- const prefix = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
6433
- await fs.promises.appendFile(gitignorePath, `${prefix}\n# knodin\n.knodin\n`);
7172
+ catch (e) {
7173
+ console.error("Failed to update .gitignore", e);
6434
7174
  }
6435
7175
  }
6436
- catch (e) {
6437
- console.error("Failed to update .gitignore", e);
6438
- }
6439
- dbPath = path.join(knodinDir, "db.sqlite");
7176
+ dbPath = path.join(stateDir, "db.sqlite");
6440
7177
  }
6441
7178
  const finishDbOpenMigration = beginPerfPhase("db_open_migration");
6442
7179
  const db = new Database(dbPath);
@@ -6480,12 +7217,14 @@ async function getOrInitDb(repoPath, options = {}) {
6480
7217
  // be revisited once as well. v20 distinguishes a known empty object from
6481
7218
  // an unknown response shape, preserving C9's precision-first rule. v21
6482
7219
  // adds source-only LWR/Experience Bundle topology and must revisit its
6483
- // newly indexable JSON files.
6484
- const KNODIN_SCHEMA_VERSION = 21;
7220
+ // newly indexable JSON files. v22 admits markdown as a link-only
7221
+ // participant, so previously skipped documentation must be visited once
7222
+ // to populate its doc_link / doc_link_broken edges.
7223
+ const KNODIN_SCHEMA_VERSION = 22;
6485
7224
  // Highest version whose upgrade needs the stored data REBUILT. Versions
6486
7225
  // above it migrate in place, so an upgrade costs a DELETE rather than a
6487
7226
  // full re-index + re-embed (~40 min of CPU on an 18k-symbol corpus).
6488
- const LAST_REBUILD_SCHEMA_VERSION = 21;
7227
+ const LAST_REBUILD_SCHEMA_VERSION = 22;
6489
7228
  const versionRow = db.query("PRAGMA user_version").get();
6490
7229
  const storedVersion = versionRow?.user_version ?? 0;
6491
7230
  const needsMcpBackfill = storedVersion < 17;
@@ -6611,6 +7350,28 @@ async function getOrInitDb(repoPath, options = {}) {
6611
7350
  value TEXT
6612
7351
  )
6613
7352
  `);
7353
+ // Imported analyzer findings. Deliberately separate from symbols and
7354
+ // references: those are resolved from source, a finding is another
7355
+ // tool's judgement about a location. Keeping it apart means graph
7356
+ // answers never silently inherit somebody else's opinion.
7357
+ db.run(`
7358
+ CREATE TABLE IF NOT EXISTS findings (
7359
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7360
+ ruleId TEXT NOT NULL,
7361
+ tool TEXT NOT NULL,
7362
+ level TEXT NOT NULL,
7363
+ message TEXT,
7364
+ filePath TEXT NOT NULL,
7365
+ startLine INTEGER NOT NULL,
7366
+ startCol INTEGER NOT NULL,
7367
+ endLine INTEGER NOT NULL,
7368
+ endCol INTEGER NOT NULL,
7369
+ helpUri TEXT,
7370
+ source TEXT NOT NULL
7371
+ )
7372
+ `);
7373
+ db.run("CREATE INDEX IF NOT EXISTS idx_findings_file ON findings(filePath)");
7374
+ db.run("CREATE INDEX IF NOT EXISTS idx_findings_tool ON findings(tool)");
6614
7375
  db.run(`
6615
7376
  CREATE TABLE IF NOT EXISTS mcp_tools (
6616
7377
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -6800,7 +7561,7 @@ async function getFederatedRepos(repoPath) {
6800
7561
  // signal, knodin federates to exactly the repos listed in `repos` (possibly
6801
7562
  // none) and nothing else.
6802
7563
  let autoDiscover = process.env.KNODIN_AUTO_FEDERATE === "1" || process.env.KNODIN_AUTO_FEDERATE === "true";
6803
- const configPath = path.join(resolvedRepoPath, ".knodin/federation.json");
7564
+ const configPath = path.join(resolveStateDir(resolvedRepoPath), "federation.json");
6804
7565
  if (fs.existsSync(configPath)) {
6805
7566
  try {
6806
7567
  const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
@@ -6841,7 +7602,7 @@ async function getFederatedRepos(repoPath) {
6841
7602
  if (siblingPath === resolvedRepoPath)
6842
7603
  continue;
6843
7604
  const isAlreadyIndexed = dbInstances.has(path.resolve(siblingPath)) ||
6844
- (!isTest && fs.existsSync(path.join(siblingPath, ".knodin", "db.sqlite")));
7605
+ (!isTest && fs.existsSync(resolveDbPath(siblingPath)));
6845
7606
  if (isAlreadyIndexed) {
6846
7607
  repos.push(siblingPath);
6847
7608
  }
@@ -6976,10 +7737,11 @@ function queryFileSymbols(db, filePath, repoPath) {
6976
7737
  .map((line) => line.trim().replace(/^\d+:\s*/, ""))
6977
7738
  .join(" ")
6978
7739
  .replace(/\s+/g, " ")
6979
- .split(/\s*(?:\{|=>)\s*/, 1)[0]
7740
+ // `(?<!\s)` pins the match to the start of a whitespace run (quadratic otherwise).
7741
+ .split(/(?<!\s)\s*(?:\{|=>)\s*/, 1)[0]
6980
7742
  .slice(0, 500)
6981
7743
  : "";
6982
- const visibility = header.match(/\b(public|private|protected|internal)\b/)?.[1] ??
7744
+ const visibility = /\b(public|private|protected|internal)\b/.exec(header)?.[1] ??
6983
7745
  (definition.filePath.endsWith(".py") && definition.name.startsWith("_")
6984
7746
  ? "private"
6985
7747
  : "default");
@@ -7521,10 +8283,10 @@ function parseUnifiedDiff(stdout) {
7521
8283
  currentFile = null;
7522
8284
  }
7523
8285
  else if (line.startsWith("@@") && currentFile) {
7524
- const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
8286
+ const match = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
7525
8287
  if (match) {
7526
- const startLine = parseInt(match[1], 10);
7527
- const count = match[2] !== undefined ? parseInt(match[2], 10) : 1;
8288
+ const startLine = Number.parseInt(match[1], 10);
8289
+ const count = match[2] !== undefined ? Number.parseInt(match[2], 10) : 1;
7528
8290
  if (count > 0) {
7529
8291
  let set = modifiedFiles.get(currentFile);
7530
8292
  if (!set) {
@@ -7571,6 +8333,15 @@ export function parseGitDiff(base, repoPath, options = {}) {
7571
8333
  if (options.files?.length) {
7572
8334
  return new Map(options.files.map((file) => [safeReviewFile(file), new Set([Number.MAX_SAFE_INTEGER])]));
7573
8335
  }
8336
+ // A mirror is cloned `--depth 1 --single-branch`, so it has no base branch and
8337
+ // no local history to diff against. Git would succeed and return nothing,
8338
+ // producing an empty review that reads as "no changes" — a confident wrong
8339
+ // answer about someone else's code. Fail loudly instead. Explicit `files`
8340
+ // above bypasses git entirely and remains valid on a mirror.
8341
+ const mirror = lookupMirror(repoPath);
8342
+ if (mirror) {
8343
+ throw new Error(`knodin review: ${repoPath} is a read-only mirror of ${mirror.url}, pinned at snapshot ${mirror.sha.slice(0, 12)}. It was cloned without history, so there is nothing to diff against and an empty result would be misleading. Review the source repository directly, or pass explicit files to review.`);
8344
+ }
7574
8345
  if ((options.from === undefined) !== (options.to === undefined)) {
7575
8346
  throw new Error("review revisions require both `from` and `to`");
7576
8347
  }
@@ -7599,7 +8370,7 @@ export function parseGitDiff(base, repoPath, options = {}) {
7599
8370
  function reviewChangedFiles(base, repoPath, options, modified) {
7600
8371
  const files = new Set(modified.keys());
7601
8372
  if (options.files?.length)
7602
- return [...files].sort();
8373
+ return [...files].sort(compareBytes);
7603
8374
  const scope = options.scope ?? "all";
7604
8375
  try {
7605
8376
  const args = ["diff", "--name-only", "-z"];
@@ -7638,7 +8409,7 @@ function reviewChangedFiles(base, repoPath, options, modified) {
7638
8409
  // The diff result remains usable even if untracked discovery fails.
7639
8410
  }
7640
8411
  }
7641
- return [...files].sort();
8412
+ return [...files].sort(compareBytes);
7642
8413
  }
7643
8414
  export function getTestFilePaths(filePath) {
7644
8415
  const dir = path.dirname(filePath);
@@ -7670,7 +8441,7 @@ export function findDefinitionNode(rootNode, symbolName, isPython, isApex = fals
7670
8441
  if (isPython) {
7671
8442
  if (node.type === "function_definition" || node.type === "class_definition") {
7672
8443
  const nameNode = node.childForFieldName("name");
7673
- if (nameNode && nameNode.text === symbolName) {
8444
+ if (nameNode?.text === symbolName) {
7674
8445
  isDef = true;
7675
8446
  }
7676
8447
  }
@@ -7680,28 +8451,26 @@ export function findDefinitionNode(rootNode, symbolName, isPython, isApex = fals
7680
8451
  node.type === "method_declaration" ||
7681
8452
  node.type === "trigger_declaration") {
7682
8453
  const nameNode = node.childForFieldName("name") || node.children.find((c) => c.type === "identifier");
7683
- if (nameNode && nameNode.text === symbolName) {
8454
+ if (nameNode?.text === symbolName) {
7684
8455
  isDef = true;
7685
8456
  }
7686
8457
  }
7687
8458
  }
7688
- else {
7689
- if (node.type === "function_declaration" ||
7690
- node.type === "generator_function_declaration" ||
7691
- node.type === "class_declaration" ||
7692
- node.type === "method_definition" ||
7693
- node.type === "interface_declaration" ||
7694
- node.type === "type_alias_declaration") {
7695
- const nameNode = node.childForFieldName("name");
7696
- if (nameNode && nameNode.text === symbolName) {
7697
- isDef = true;
7698
- }
8459
+ else if (node.type === "function_declaration" ||
8460
+ node.type === "generator_function_declaration" ||
8461
+ node.type === "class_declaration" ||
8462
+ node.type === "method_definition" ||
8463
+ node.type === "interface_declaration" ||
8464
+ node.type === "type_alias_declaration") {
8465
+ const nameNode = node.childForFieldName("name");
8466
+ if (nameNode?.text === symbolName) {
8467
+ isDef = true;
7699
8468
  }
7700
- else if (node.type === "variable_declarator") {
7701
- const nameNode = node.childForFieldName("name");
7702
- if (nameNode && nameNode.type === "identifier" && nameNode.text === symbolName) {
7703
- isDef = true;
7704
- }
8469
+ }
8470
+ else if (node.type === "variable_declarator") {
8471
+ const nameNode = node.childForFieldName("name");
8472
+ if (nameNode?.type === "identifier" && nameNode.text === symbolName) {
8473
+ isDef = true;
7705
8474
  }
7706
8475
  }
7707
8476
  if (isDef) {
@@ -8238,8 +9007,8 @@ function detectSymbolCommunities(allRepos, formatPath, relationKinds) {
8238
9007
  const usedNames = new Set();
8239
9008
  for (const indices of groups.values()) {
8240
9009
  const memberSet = new Set(indices);
8241
- const symbols = indices.map((i) => nodes[i].name).sort();
8242
- const files = Array.from(new Set(indices.map((i) => nodes[i].file))).sort();
9010
+ const symbols = indices.map((i) => nodes[i].name).sort(compareBytes);
9011
+ const files = Array.from(new Set(indices.map((i) => nodes[i].file))).sort(compareBytes);
8243
9012
  let internal = 0.0;
8244
9013
  let total = 0.0;
8245
9014
  for (const i of indices) {
@@ -8299,7 +9068,7 @@ async function detectKnowledgeGaps(allRepos, mapResult, resolvedRepoPath, opts)
8299
9068
  const out = new Set();
8300
9069
  for (const f of files)
8301
9070
  out.add(await languageOfFile(f));
8302
- return [...out].sort();
9071
+ return [...out].sort(compareBytes);
8303
9072
  };
8304
9073
  const singleFileCommunities = [];
8305
9074
  for (const c of mapResult.communities) {
@@ -8665,11 +9434,7 @@ function renderCommunityPage(community, map) {
8665
9434
  const symbols = community.symbols ?? [];
8666
9435
  const fileSet = new Set(files);
8667
9436
  const lines = [];
8668
- lines.push(`# ${community.name}`, "");
8669
- lines.push("## Overview", "");
8670
- lines.push(`- Size: ${community.size} symbol(s)`);
8671
- lines.push(`- Cohesion: ${community.cohesion}`, "");
8672
- lines.push("## Files", "");
9437
+ lines.push(`# ${community.name}`, "", "## Overview", "", `- Size: ${community.size} symbol(s)`, `- Cohesion: ${community.cohesion}`, "", "## Files", "");
8673
9438
  if (files.length > 0) {
8674
9439
  for (const f of files)
8675
9440
  lines.push(`- ${f}`);
@@ -8734,10 +9499,7 @@ function renderCommunityPage(community, map) {
8734
9499
  /** Renders `.knodin/wiki/index.md`: repo path, community table, one link per page. */
8735
9500
  function renderWikiIndex(resolvedRepoPath, map, slugs) {
8736
9501
  const lines = [];
8737
- lines.push("# knodin Wiki", "");
8738
- lines.push(`Repo: ${resolvedRepoPath}`, "");
8739
- lines.push("## Communities", "");
8740
- lines.push("| Name | Size | Cohesion | Top files |", "|---|---|---|---|");
9502
+ lines.push("# knodin Wiki", "", `Repo: ${resolvedRepoPath}`, "", "## Communities", "", "| Name | Size | Cohesion | Top files |", "|---|---|---|---|");
8741
9503
  for (const c of map.communities) {
8742
9504
  const topFiles = (c.files ?? []).slice(0, 3).join(", ");
8743
9505
  lines.push(`| ${c.name} | ${c.size} | ${c.cohesion} | ${topFiles} |`);
@@ -8776,7 +9538,7 @@ function writeWikiPageIfChanged(dirPath, relFile, content, force, written, skipp
8776
9538
  async function writeWiki(engine, repoPath, force) {
8777
9539
  const resolvedRepoPath = path.resolve(repoPath);
8778
9540
  const map = await engine.map(repoPath, "standard");
8779
- const wikiDir = path.join(resolvedRepoPath, ".knodin", "wiki");
9541
+ const wikiDir = path.join(resolveStateDir(resolvedRepoPath), "wiki");
8780
9542
  fs.mkdirSync(wikiDir, { recursive: true });
8781
9543
  // Slugify community names into unique page filenames (lowercase, hyphenated).
8782
9544
  const slugs = new Map();
@@ -8925,8 +9687,8 @@ function findImportSpecifierLines(absPath, symbol) {
8925
9687
  catch {
8926
9688
  return [];
8927
9689
  }
8928
- const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8929
- const wordRe = new RegExp(`\\b${escaped}\\b`);
9690
+ const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
9691
+ const wordRe = new RegExp(String.raw `\b${escaped}\b`);
8930
9692
  // `export` only opens a statement worth scanning when it re-exports bindings
8931
9693
  // (`export {` / `export *`) — never for `export function foo() {`, whose body
8932
9694
  // braces would otherwise swallow the rest of the file.
@@ -8964,6 +9726,11 @@ function withStaleness(engine) {
8964
9726
  result.staleness = stalenessFor(repoPath);
8965
9727
  return result;
8966
9728
  },
9729
+ async dependencyGraph(repoPath) {
9730
+ const result = await engine.dependencyGraph(repoPath);
9731
+ result.staleness = stalenessFor(repoPath);
9732
+ return result;
9733
+ },
8967
9734
  async map(repoPath, detailLevel = "minimal", options) {
8968
9735
  const raw = await engine.map(repoPath, detailLevel, options);
8969
9736
  const result = options
@@ -9373,6 +10140,39 @@ export function createEngine() {
9373
10140
  ...(truncated ? { truncated: true } : {}),
9374
10141
  };
9375
10142
  },
10143
+ async dependencyGraph(repoPath) {
10144
+ const db = await getOrInitDb(repoPath);
10145
+ const rows = db
10146
+ .query("SELECT fromFile, toFile, kind, COALESCE(confidence, 1.0) AS confidence, sourceEvidence FROM dependencies")
10147
+ .all();
10148
+ const degrees = {};
10149
+ const edges = [];
10150
+ for (const row of rows) {
10151
+ // A self-edge carries no layout information and would render as a
10152
+ // dot on top of its own node.
10153
+ if (!row.fromFile || !row.toFile || row.fromFile === row.toFile)
10154
+ continue;
10155
+ degrees[row.fromFile] = (degrees[row.fromFile] ?? 0) + 1;
10156
+ degrees[row.toFile] = (degrees[row.toFile] ?? 0) + 1;
10157
+ edges.push({
10158
+ fromFile: row.fromFile,
10159
+ toFile: row.toFile,
10160
+ kind: row.kind,
10161
+ confidence: row.confidence,
10162
+ ...(row.sourceEvidence ? { sourceEvidence: row.sourceEvidence } : {}),
10163
+ });
10164
+ }
10165
+ // Stable order so repeated calls over unchanged evidence agree.
10166
+ edges.sort((left, right) => left.fromFile === right.fromFile
10167
+ ? left.toFile.localeCompare(right.toFile)
10168
+ : left.fromFile.localeCompare(right.fromFile));
10169
+ return {
10170
+ repoPath: path.resolve(repoPath),
10171
+ edges,
10172
+ fileCount: Object.keys(degrees).length,
10173
+ degrees,
10174
+ };
10175
+ },
9376
10176
  async map(repoPath, detailLevel = "minimal", options = {}) {
9377
10177
  const topN = options.topN ?? 15;
9378
10178
  if (!Number.isInteger(topN) || topN < 1 || topN > 1000)
@@ -9395,10 +10195,10 @@ export function createEngine() {
9395
10195
  resolvedRepoPath,
9396
10196
  topN,
9397
10197
  options.sort ?? "relevance",
9398
- [...(options.relationKinds ?? [])].sort(),
10198
+ [...(options.relationKinds ?? [])].sort(compareBytes),
9399
10199
  ]);
9400
10200
  const cached = minimalMapCache.get(cacheKey);
9401
- if (cached && cached.generation === indexGeneration)
10201
+ if (cached?.generation === indexGeneration)
9402
10202
  return cached.result;
9403
10203
  const computeMinimalMap = (async () => {
9404
10204
  const analytics = await getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath, topN, options.relationKinds);
@@ -9468,7 +10268,7 @@ export function createEngine() {
9468
10268
  return computeMinimalMap;
9469
10269
  }
9470
10270
  const cached = mapCache.get(resolvedRepoPath);
9471
- if (cached && cached.generation === indexGeneration)
10271
+ if (cached?.generation === indexGeneration)
9472
10272
  return cached.result;
9473
10273
  const computeMap = (async () => {
9474
10274
  const repos = await getFederatedRepos(repoPath);
@@ -9506,7 +10306,7 @@ export function createEngine() {
9506
10306
  for (const rf of refsFiles) {
9507
10307
  filesSet.add(rf.callerFile);
9508
10308
  }
9509
- const filesList = Array.from(filesSet).sort();
10309
+ const filesList = Array.from(filesSet).sort(compareBytes);
9510
10310
  const V = filesList.length;
9511
10311
  if (V === 0) {
9512
10312
  return {
@@ -9559,7 +10359,7 @@ export function createEngine() {
9559
10359
  let calleeFile = r.calleeFile;
9560
10360
  if (!calleeFile) {
9561
10361
  const calleeFiles = symbolToFiles.get(r.calleeSymbol);
9562
- if (calleeFiles && calleeFiles.length === 1)
10362
+ if (calleeFiles?.length === 1)
9563
10363
  calleeFile = calleeFiles[0];
9564
10364
  }
9565
10365
  if (!calleeFile || calleeFile === r.callerFile)
@@ -9598,7 +10398,7 @@ export function createEngine() {
9598
10398
  filesSet.add(formatPath(repo.path, rf.callerFile));
9599
10399
  }
9600
10400
  }
9601
- const filesList = Array.from(filesSet).sort();
10401
+ const filesList = Array.from(filesSet).sort(compareBytes);
9602
10402
  const V = filesList.length;
9603
10403
  if (V === 0) {
9604
10404
  return {
@@ -9777,13 +10577,15 @@ export function createEngine() {
9777
10577
  // repair and timestamp drift before reporting it. Reuse an active DB in
9778
10578
  // long-lived processes, or open the existing local DB read-only.
9779
10579
  const activeDb = dbInstances.get(resolved);
9780
- const diskDbPath = path.join(resolved, ".knodin", "db.sqlite");
10580
+ const diskDbPath = resolveDbPath(resolved);
9781
10581
  const db = activeDb ??
9782
10582
  (fs.existsSync(diskDbPath) ? new Database(diskDbPath, { readonly: true }) : null);
9783
10583
  const verifiedAt = new Date().toISOString();
9784
10584
  const activity = options.ignoreActiveOperation ? null : readIndexActivity(resolved);
9785
10585
  if (activity) {
9786
- const sourceFiles = measurePerfPhaseSync("status_audit", () => collectRepoFiles(resolved));
10586
+ const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
10587
+ const sourceFiles = collected.files;
10588
+ const skipped = buildCoverageSkips(collected.skippedByExtension, db);
9787
10589
  let schemaVersion = 0;
9788
10590
  let indexedFiles = 0;
9789
10591
  let filesWithSymbols = 0;
@@ -9826,6 +10628,7 @@ export function createEngine() {
9826
10628
  percent: sourceFiles.length
9827
10629
  ? Math.round((Math.min(indexedFiles, sourceFiles.length) / sourceFiles.length) * 10000) / 100
9828
10630
  : 100,
10631
+ skipped,
9829
10632
  },
9830
10633
  orphaned: { embeddings: 0, references: 0, dependencies: 0 },
9831
10634
  missing: { files: [], records: [] },
@@ -9838,7 +10641,8 @@ export function createEngine() {
9838
10641
  };
9839
10642
  }
9840
10643
  if (!db) {
9841
- const sourceFiles = measurePerfPhaseSync("status_audit", () => collectRepoFiles(resolved));
10644
+ const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
10645
+ const sourceFiles = collected.files;
9842
10646
  return {
9843
10647
  status: "repair-needed",
9844
10648
  repo: resolved,
@@ -9854,6 +10658,7 @@ export function createEngine() {
9854
10658
  indexedFiles: 0,
9855
10659
  filesWithSymbols: 0,
9856
10660
  percent: sourceFiles.length ? 0 : 100,
10661
+ skipped: buildCoverageSkips(collected.skippedByExtension, null),
9857
10662
  },
9858
10663
  orphaned: { embeddings: 0, references: 0, dependencies: 0 },
9859
10664
  missing: {
@@ -9922,7 +10727,9 @@ export function createEngine() {
9922
10727
  // reports repair steps instead of hiding the database problem.
9923
10728
  }
9924
10729
  }
9925
- const sourceFiles = measurePerfPhaseSync("status_audit", () => collectRepoFiles(resolved));
10730
+ const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
10731
+ const sourceFiles = collected.files;
10732
+ const coverageSkips = buildCoverageSkips(collected.skippedByExtension, db);
9926
10733
  const schemaVersion = db.query("PRAGMA user_version").get()?.user_version ?? 0;
9927
10734
  const requiredTables = [
9928
10735
  "symbols",
@@ -9968,6 +10775,7 @@ export function createEngine() {
9968
10775
  indexedFiles: 0,
9969
10776
  filesWithSymbols: 0,
9970
10777
  percent: sourceFiles.length ? 0 : 100,
10778
+ skipped: coverageSkips,
9971
10779
  },
9972
10780
  orphaned: { embeddings: 0, references: 0, dependencies: 0 },
9973
10781
  missing: { files: sourceFiles, records: schemaProblems },
@@ -10049,7 +10857,10 @@ export function createEngine() {
10049
10857
  percent: sourceFiles.length
10050
10858
  ? Math.round(((sourceFiles.length - missingFiles.length) / sourceFiles.length) * 10000) / 100
10051
10859
  : 100,
10860
+ skipped: coverageSkips,
10052
10861
  },
10862
+ ...mirrorSnapshotFor(resolved),
10863
+ semanticReadiness: semanticReadinessFor(db),
10053
10864
  orphaned,
10054
10865
  missing: { files: missingFiles, records },
10055
10866
  lastSuccessfulReconciliation: getMeta(db, "lastSuccessfulReconciliation") ?? null,
@@ -10114,6 +10925,7 @@ export function createEngine() {
10114
10925
  let repairPaths = [];
10115
10926
  let completedFiles = 0;
10116
10927
  let committedWork = false;
10928
+ let repairLease;
10117
10929
  const generationBeforeRepair = indexGeneration;
10118
10930
  const activeOperation = {
10119
10931
  promise: undefined,
@@ -10151,6 +10963,7 @@ export function createEngine() {
10151
10963
  };
10152
10964
  const operation = Promise.resolve()
10153
10965
  .then(async () => {
10966
+ repairLease = acquireRepairLease(resolved);
10154
10967
  const cancelBoundary = () => {
10155
10968
  if (activeOperation.controller.signal.aborted)
10156
10969
  throw activeOperation.controller.signal;
@@ -10183,7 +10996,7 @@ export function createEngine() {
10183
10996
  // Repair partial current-schema databases before normal initialization,
10184
10997
  // whose reconciliation queries require these columns. ALTER preserves all
10185
10998
  // healthy index_state rows; the reconciler then refreshes their snapshots.
10186
- const diskDbPath = path.join(resolved, ".knodin", "db.sqlite");
10999
+ const diskDbPath = resolveDbPath(resolved);
10187
11000
  if (fs.existsSync(diskDbPath)) {
10188
11001
  const activeDb = dbInstances.get(resolved);
10189
11002
  const schemaDb = activeDb ?? new Database(diskDbPath);
@@ -10219,7 +11032,7 @@ export function createEngine() {
10219
11032
  const eligibleRepairPaths = new Set(collectRepoFiles(resolved));
10220
11033
  repairPaths = [...repairKinds.keys()]
10221
11034
  .filter((file) => repairKinds.get(file) === "stale" || fileDriftedFromIndexState(db, resolved, file))
10222
- .sort();
11035
+ .sort(compareBytes);
10223
11036
  counts.skipped = Math.max(0, repairKinds.size - repairPaths.length);
10224
11037
  overallTotal = repairPaths.length + 4;
10225
11038
  emitProgress("planning", repairPaths.length, "Repair plan completed", {
@@ -10427,6 +11240,7 @@ export function createEngine() {
10427
11240
  return await operation;
10428
11241
  }
10429
11242
  finally {
11243
+ repairLease?.release();
10430
11244
  options?.signal?.removeEventListener("abort", abortShared);
10431
11245
  if (options?.onProgress)
10432
11246
  activeOperation.subscribers.delete(options.onProgress);
@@ -10438,6 +11252,7 @@ export function createEngine() {
10438
11252
  const indexed = [];
10439
11253
  const unchanged = [];
10440
11254
  let scipReport;
11255
+ let sarifReport;
10441
11256
  const progress = createIndexProgressReporter(options?.onProgress);
10442
11257
  progress("starting", 0, "Opening local graph database");
10443
11258
  const db = await getOrInitDb(repoPath, {
@@ -10515,12 +11330,17 @@ export function createEngine() {
10515
11330
  await reconcileIndex(repoPath, db, progress);
10516
11331
  }
10517
11332
  else {
10518
- await indexRepo(repoPath, db, progress);
11333
+ await indexRepo(repoPath, db, progress, options?.skipEmbeddings === true);
10519
11334
  }
10520
11335
  // Reuse the same candidate set as indexing/reconciliation. This keeps
10521
11336
  // LWC HTML/CSS bundle members visible in the response without admitting
10522
11337
  // arbitrary web assets to the source index.
10523
- indexed.push(...collectRepoFiles(repoPath));
11338
+ // Appended in a loop, not `push(...files)`: spreading an array as
11339
+ // arguments overflows the call stack once a repository is large
11340
+ // enough. Observed on a 40k-file checkout, which is well within the
11341
+ // range knodin is meant to index.
11342
+ for (const file of collectRepoFiles(repoPath))
11343
+ indexed.push(file);
10524
11344
  completionMessage = `${indexed.length.toLocaleString()} repository file(s) are current`;
10525
11345
  }
10526
11346
  if (options?.scip) {
@@ -10550,6 +11370,29 @@ export function createEngine() {
10550
11370
  if (!indexed.includes(file))
10551
11371
  indexed.push(file);
10552
11372
  }
11373
+ if (options?.sarif) {
11374
+ const facts = readSarifLog(repoPath, options.sarif);
11375
+ persistSarifFindings(db, facts);
11376
+ // Findings are not graph facts, so no re-embedding and no generation
11377
+ // bump: the graph is unchanged and existing answers stay valid.
11378
+ const bounds = { ...SARIF_DEFAULT_LIMITS, ...options.sarif };
11379
+ sarifReport = {
11380
+ provenance: "sarif",
11381
+ inputPath: facts.inputPath,
11382
+ bytes: facts.bytes,
11383
+ tools: facts.tools,
11384
+ rules: facts.rules,
11385
+ files: facts.files,
11386
+ findings: facts.findings.length,
11387
+ skippedUnresolved: facts.skippedUnresolved,
11388
+ elapsedMs: facts.elapsedMs,
11389
+ bounds: {
11390
+ maxBytes: bounds.maxBytes,
11391
+ maxFindings: bounds.maxFindings,
11392
+ timeoutMs: bounds.timeoutMs,
11393
+ },
11394
+ };
11395
+ }
10553
11396
  progress("verifying", 0, "Verifying local graph health", {
10554
11397
  phaseTotal: 1,
10555
11398
  });
@@ -10604,6 +11447,7 @@ export function createEngine() {
10604
11447
  indexed,
10605
11448
  unchanged,
10606
11449
  scip: scipReport,
11450
+ sarif: sarifReport,
10607
11451
  verification: {
10608
11452
  status: health.status,
10609
11453
  issueCount,
@@ -10621,6 +11465,10 @@ export function createEngine() {
10621
11465
  throw new Error("knodin search: invalid testScope");
10622
11466
  const repos = options.federate === false ? [resolvedRepoPath] : await getFederatedRepos(repoPath);
10623
11467
  const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await getOrInitDb(r) })));
11468
+ // Computed before any retrieval route runs, so every exit path can report
11469
+ // it: a short result list caused by missing embeddings must not be
11470
+ // indistinguishable from a short list caused by the query.
11471
+ const semanticReadiness = worstSemanticReadiness(allRepos.map((repo) => semanticReadinessFor(repo.db)));
10624
11472
  const searchSnapshots = new Map(allRepos.map((repo) => [repo.path, getSearchMetadataSnapshot(repo.path, repo.db)]));
10625
11473
  // typeof + Number.isFinite rejects undefined/null/NaN alike, so any non-numeric
10626
11474
  // or unset `limit` falls back to the default instead of clamping to 0/1 or
@@ -10771,6 +11619,7 @@ export function createEngine() {
10771
11619
  limit: maxResults,
10772
11620
  hasMore: offset + maxResults < matches.length,
10773
11621
  retrieval: { route: ["exact-name"], embeddingsUsed: false },
11622
+ semanticReadiness,
10774
11623
  };
10775
11624
  }
10776
11625
  if (retrievalMode !== "vector-only") {
@@ -10815,6 +11664,7 @@ export function createEngine() {
10815
11664
  : [identityQuery ? "stable-identity" : pathQuery ? "exact-path" : "exact-name"],
10816
11665
  embeddingsUsed: false,
10817
11666
  },
11667
+ semanticReadiness,
10818
11668
  };
10819
11669
  }
10820
11670
  }
@@ -10901,6 +11751,7 @@ export function createEngine() {
10901
11751
  : ["lexical"],
10902
11752
  embeddingsUsed: false,
10903
11753
  },
11754
+ semanticReadiness,
10904
11755
  };
10905
11756
  }
10906
11757
  }
@@ -11055,6 +11906,7 @@ export function createEngine() {
11055
11906
  : ["lexical", "bounded-graph", "embeddings"],
11056
11907
  embeddingsUsed: true,
11057
11908
  },
11909
+ semanticReadiness,
11058
11910
  };
11059
11911
  },
11060
11912
  async query(pattern, target, repoPath, to, limit, depth, detailLevel, selector = {}, impactOptions = {}, options = {}) {
@@ -11397,13 +12249,13 @@ export function createEngine() {
11397
12249
  adjacency.set(edge.fromFile, outgoing);
11398
12250
  }
11399
12251
  for (const outgoing of adjacency.values())
11400
- outgoing.sort();
12252
+ outgoing.sort(compareBytes);
11401
12253
  const cycles = [];
11402
12254
  const stopAfter = cap + 1;
11403
12255
  const workLimit = Math.min(Math.max(cap * 1000, 10_000), 1_000_000);
11404
12256
  let work = 0;
11405
12257
  let workTruncated = false;
11406
- for (const root of [...nodes].sort()) {
12258
+ for (const root of [...nodes].sort(compareBytes)) {
11407
12259
  if (cycles.length >= stopAfter || workTruncated)
11408
12260
  break;
11409
12261
  const path = [root];
@@ -11581,7 +12433,7 @@ export function createEngine() {
11581
12433
  const includeTests = impactOptions.includeTests ?? true;
11582
12434
  const relationKinds = new Set(impactOptions.relationKinds ?? []);
11583
12435
  const minConfidence = Math.min(Math.max(impactOptions.minConfidence ?? 0, 0), 1);
11584
- const isTestFile = (file) => /(^|\/)(test|tests|__tests__)(\/|$)|\.(test|spec)\.[^.]+$/i.test(file);
12436
+ const isTestFile = (file) => /((^|\/)(test|tests|__tests__)(\/|$))|(\.(test|spec)\.[^.]+$)/i.test(file);
11585
12437
  const allowed = (kind, confidence, file) => (includeTests || !isTestFile(file)) &&
11586
12438
  (relationKinds.size === 0 || relationKinds.has(kind)) &&
11587
12439
  confidence >= minConfidence;
@@ -11596,8 +12448,8 @@ export function createEngine() {
11596
12448
  const text = fs.readFileSync(path.join(resolvedRepoPath, file), "utf8").split("\n")[line - 1];
11597
12449
  if (!text)
11598
12450
  return undefined;
11599
- const escaped = callee.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11600
- const match = text.match(new RegExp(`\\b${escaped}\\s*\\(([^)]*)\\)`));
12451
+ const escaped = callee.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
12452
+ const match = new RegExp(String.raw `\b${escaped}\s*\(([^)]*)\)`).exec(text);
11601
12453
  if (!match)
11602
12454
  return undefined;
11603
12455
  return {
@@ -11942,8 +12794,27 @@ export function createEngine() {
11942
12794
  const src = readSource(relFile);
11943
12795
  if (!src)
11944
12796
  return false;
11945
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11946
- return (new RegExp(`\\bexport\\s+(?:default\\s+)?(?:function|class|const|let|var|type|interface|enum)\\s+${escaped}\\b`).test(src) || new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`).test(src));
12797
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
12798
+ return (new RegExp(String.raw `\bexport\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+${escaped}\b`).test(src) ||
12799
+ new RegExp(String.raw `\bexport\s*\{[^}]*\b${escaped}\b[^}]*\}`).test(src));
12800
+ };
12801
+ // Whether an import/export binding clause may bind `name`. A namespace
12802
+ // binding, or one whose `{ … }` span cannot be read, is conservatively a
12803
+ // yes; a readable named list is a yes only when it names the symbol.
12804
+ const bindingClauseBinds = (bindings, name) => {
12805
+ if (bindings.includes("*"))
12806
+ return true;
12807
+ // First `{ … }` span, scanned rather than matched: the equivalent
12808
+ // regex re-walks the binding list from every `{`, which is quadratic.
12809
+ const open = bindings.indexOf("{");
12810
+ const close = open < 0 ? -1 : bindings.indexOf("}", open + 1);
12811
+ const named = close < 0 ? undefined : bindings.slice(open + 1, close);
12812
+ if (!named)
12813
+ return true;
12814
+ return named.split(",").some((part) => part
12815
+ .trim()
12816
+ .split(/\s+as\s+/)[0]
12817
+ ?.trim() === name);
11947
12818
  };
11948
12819
  const importerMayReachExport = (importer, relFile, name) => {
11949
12820
  const src = readSource(importer);
@@ -11954,20 +12825,8 @@ export function createEngine() {
11954
12825
  if (resolveModuleToFile(importer, match[2], repoPath) !== relFile)
11955
12826
  continue;
11956
12827
  matchedTarget = true;
11957
- const bindings = match[1].trim();
11958
- if (bindings.includes("*"))
12828
+ if (bindingClauseBinds(match[1].trim(), name))
11959
12829
  return true;
11960
- const named = bindings.match(/\{([^}]*)\}/)?.[1];
11961
- if (!named)
11962
- return true;
11963
- for (const part of named.split(",")) {
11964
- const imported = part
11965
- .trim()
11966
- .split(/\s+as\s+/)[0]
11967
- ?.trim();
11968
- if (imported === name)
11969
- return true;
11970
- }
11971
12830
  }
11972
12831
  // The dependency extractor found an edge but the source pattern was
11973
12832
  // not recognized (for example, a dynamic loader). Keep it live.
@@ -12520,9 +13379,13 @@ export function createEngine() {
12520
13379
  // Parameters are reaching definitions at function entry. Deliberately
12521
13380
  // handles only simple identifiers, not destructuring/default expressions.
12522
13381
  const signature = functionLines[0] ?? "";
12523
- const params = signature.match(/\(([^)]*)\)/)?.[1] ?? "";
13382
+ // First `( … )` span, scanned rather than matched: the equivalent regex
13383
+ // re-walks the signature from every `(`, which is quadratic.
13384
+ const parenOpen = signature.indexOf("(");
13385
+ const parenClose = parenOpen < 0 ? -1 : signature.indexOf(")", parenOpen + 1);
13386
+ const params = parenClose < 0 ? "" : signature.slice(parenOpen + 1, parenClose);
12524
13387
  for (const parameter of params.split(",")) {
12525
- const name = parameter.trim().match(/^([A-Za-z_$][\w$]*)/)?.[1];
13388
+ const name = /^([A-Za-z_$][\w$]*)/.exec(parameter.trim())?.[1];
12526
13389
  if (name) {
12527
13390
  trackedVariables.add(name);
12528
13391
  add("definition", name, 0, signature);
@@ -12532,7 +13395,7 @@ export function createEngine() {
12532
13395
  const line = raw.trim();
12533
13396
  if (!line || line.startsWith("//"))
12534
13397
  continue;
12535
- const definition = line.match(/^(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/);
13398
+ const definition = /^(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/.exec(line);
12536
13399
  if (definition) {
12537
13400
  trackedVariables.add(definition[1]);
12538
13401
  add("definition", definition[1], index, raw);
@@ -12560,13 +13423,11 @@ export function createEngine() {
12560
13423
  ].includes(variable))
12561
13424
  continue;
12562
13425
  if (definition?.[1] === variable &&
12563
- !line
12564
- .slice((definition.index ?? 0) + definition[0].length)
12565
- .match(new RegExp(`\\b${variable}\\b`)))
13426
+ !new RegExp(String.raw `\b${variable}\b`).exec(line.slice((definition.index ?? 0) + definition[0].length)))
12566
13427
  continue;
12567
- if (control?.match(new RegExp(`\\b${variable}\\b`)))
13428
+ if (control?.match(new RegExp(String.raw `\b${variable}\b`)))
12568
13429
  add("control", variable, index, raw);
12569
- else if (!definition || definition[1] !== variable)
13430
+ else if (definition?.[1] !== variable)
12570
13431
  add("use", variable, index, raw);
12571
13432
  }
12572
13433
  }
@@ -12748,7 +13609,7 @@ export function createEngine() {
12748
13609
  selected,
12749
13610
  ]);
12750
13611
  const cachedOverview = architectureOverviewCache.get(overviewCacheKey);
12751
- if (cachedOverview && cachedOverview.generation === indexGeneration) {
13612
+ if (cachedOverview?.generation === indexGeneration) {
12752
13613
  finishArchitecture();
12753
13614
  return finish([{ architectureOverview: structuredClone(cachedOverview.result) }]);
12754
13615
  }
@@ -12880,7 +13741,7 @@ export function createEngine() {
12880
13741
  .map(([name, value]) => ({
12881
13742
  name,
12882
13743
  symbols: value.symbols,
12883
- files: [...value.files].sort(),
13744
+ files: [...value.files].sort(compareBytes),
12884
13745
  }))
12885
13746
  .sort((a, b) => b.symbols - a.symbols || a.name.localeCompare(b.name))
12886
13747
  .slice(0, cap);
@@ -13174,8 +14035,8 @@ export function createEngine() {
13174
14035
  collideStmt.finalize();
13175
14036
  // Line-scoped word-boundary replacement plan: only the lines the preview
13176
14037
  // recorded are touched (never a whole-file blind replace).
13177
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13178
- const wordRe = new RegExp(`\\b${escaped}\\b`, "g");
14038
+ const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
14039
+ const wordRe = new RegExp(String.raw `\b${escaped}\b`, "g");
13179
14040
  const linesByFile = new Map();
13180
14041
  // Import-specifier lines are rewritten outside string literals, so a module
13181
14042
  // specifier that happens to contain the old name (`from "./helper"`) is left