@vibgrate/cli 2026.722.2 → 2026.727.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 (145) hide show
  1. package/DOCS.md +418 -213
  2. package/README.md +34 -8
  3. package/dist/baseline-TG4ATZGZ.js +8 -0
  4. package/dist/{baseline-BD3A7EXD.js.map → baseline-TG4ATZGZ.js.map} +1 -1
  5. package/dist/chunk-2MBL43EJ.js +82 -0
  6. package/dist/chunk-2MBL43EJ.js.map +1 -0
  7. package/dist/chunk-2XQDMLKB.js +465 -0
  8. package/dist/chunk-2XQDMLKB.js.map +1 -0
  9. package/dist/{chunk-PY3DNX5H.js → chunk-3Q7QBWFW.js} +13 -2
  10. package/dist/chunk-3Q7QBWFW.js.map +1 -0
  11. package/dist/chunk-3V24O47U.js +478 -0
  12. package/dist/chunk-3V24O47U.js.map +1 -0
  13. package/dist/chunk-5I4VSBWQ.js +764 -0
  14. package/dist/chunk-5I4VSBWQ.js.map +1 -0
  15. package/dist/{chunk-WNIIKCNF.js → chunk-6ABXHZRN.js} +59 -311
  16. package/dist/chunk-6ABXHZRN.js.map +1 -0
  17. package/dist/chunk-6AYDFFP5.js +136 -0
  18. package/dist/chunk-6AYDFFP5.js.map +1 -0
  19. package/dist/chunk-6UQMXIAG.js +122 -0
  20. package/dist/chunk-6UQMXIAG.js.map +1 -0
  21. package/dist/chunk-7N4Y3E47.js +108 -0
  22. package/dist/chunk-7N4Y3E47.js.map +1 -0
  23. package/dist/chunk-BPF3YX2U.js +64 -0
  24. package/dist/chunk-BPF3YX2U.js.map +1 -0
  25. package/dist/chunk-BWPBB45M.js +88 -0
  26. package/dist/chunk-BWPBB45M.js.map +1 -0
  27. package/dist/{chunk-6CXTPC74.js → chunk-CY4WKXB2.js} +27 -6
  28. package/dist/chunk-CY4WKXB2.js.map +1 -0
  29. package/dist/chunk-DGUM43GV.js +10 -0
  30. package/dist/chunk-DGUM43GV.js.map +1 -0
  31. package/dist/{chunk-NONYJLOJ.js → chunk-EX4PQT6W.js} +3 -3
  32. package/dist/{chunk-NONYJLOJ.js.map → chunk-EX4PQT6W.js.map} +1 -1
  33. package/dist/chunk-IS7VTH2Q.js +627 -0
  34. package/dist/chunk-IS7VTH2Q.js.map +1 -0
  35. package/dist/chunk-IZDH67DX.js +289 -0
  36. package/dist/chunk-IZDH67DX.js.map +1 -0
  37. package/dist/chunk-J4RFRKZH.js +2496 -0
  38. package/dist/chunk-J4RFRKZH.js.map +1 -0
  39. package/dist/chunk-J7LKEB2R.js +51 -0
  40. package/dist/chunk-J7LKEB2R.js.map +1 -0
  41. package/dist/chunk-JIBOJTGS.js +313 -0
  42. package/dist/chunk-JIBOJTGS.js.map +1 -0
  43. package/dist/chunk-K3MZIVZW.js +88 -0
  44. package/dist/chunk-K3MZIVZW.js.map +1 -0
  45. package/dist/chunk-K7REHDQS.js +776 -0
  46. package/dist/chunk-K7REHDQS.js.map +1 -0
  47. package/dist/{chunk-2O5YZVZV.js → chunk-KCKYKDMS.js} +23 -19
  48. package/dist/chunk-KCKYKDMS.js.map +1 -0
  49. package/dist/{chunk-3MXNNBLI.js → chunk-KNELIQJO.js} +1647 -2042
  50. package/dist/chunk-KNELIQJO.js.map +1 -0
  51. package/dist/chunk-KSBHGWRV.js +613 -0
  52. package/dist/chunk-KSBHGWRV.js.map +1 -0
  53. package/dist/chunk-LKVMI67M.js +1695 -0
  54. package/dist/chunk-LKVMI67M.js.map +1 -0
  55. package/dist/chunk-NDMDSPFX.js +69 -0
  56. package/dist/chunk-NDMDSPFX.js.map +1 -0
  57. package/dist/{chunk-CS37OBE3.js → chunk-OFUC6RYN.js} +791 -783
  58. package/dist/chunk-OFUC6RYN.js.map +1 -0
  59. package/dist/chunk-OOIG3RUM.js +276 -0
  60. package/dist/chunk-OOIG3RUM.js.map +1 -0
  61. package/dist/chunk-PMYWZRRS.js +62 -0
  62. package/dist/chunk-PMYWZRRS.js.map +1 -0
  63. package/dist/chunk-PXHNTC6D.js +382 -0
  64. package/dist/chunk-PXHNTC6D.js.map +1 -0
  65. package/dist/{chunk-I7GY7MVN.js → chunk-RNP3SMTV.js} +60 -12
  66. package/dist/chunk-RNP3SMTV.js.map +1 -0
  67. package/dist/chunk-TCDAZZNP.js +843 -0
  68. package/dist/chunk-TCDAZZNP.js.map +1 -0
  69. package/dist/{chunk-5BDHQ7DD.js → chunk-WGUL45AV.js} +24 -17
  70. package/dist/chunk-WGUL45AV.js.map +1 -0
  71. package/dist/cli.d.ts +3 -0
  72. package/dist/cli.js +3373 -617
  73. package/dist/cli.js.map +1 -1
  74. package/dist/execution-env-ZU45JZ4G.js +4 -0
  75. package/dist/execution-env-ZU45JZ4G.js.map +1 -0
  76. package/dist/federation-SZWZIFZ7.js +7 -0
  77. package/dist/federation-SZWZIFZ7.js.map +1 -0
  78. package/dist/{fs-IPY4FJKH.js → fs-B5HZ2MIU.js} +3 -2
  79. package/dist/fs-B5HZ2MIU.js.map +1 -0
  80. package/dist/{fs-KCABDURV.js → fs-O5IQYXHO.js} +3 -2
  81. package/dist/fs-O5IQYXHO.js.map +1 -0
  82. package/dist/git-ref-XDXB6JP5.js +4 -0
  83. package/dist/git-ref-XDXB6JP5.js.map +1 -0
  84. package/dist/graph-backend-73URGTFY.js +11 -0
  85. package/dist/graph-backend-73URGTFY.js.map +1 -0
  86. package/dist/index.d.ts +472 -137
  87. package/dist/index.js +15 -7
  88. package/dist/index.js.map +1 -1
  89. package/dist/{interactive-A4DISSIW.js → interactive-372NLI5C.js} +186 -226
  90. package/dist/interactive-372NLI5C.js.map +1 -0
  91. package/dist/llm-host-7GWDO6IX.js +9 -0
  92. package/dist/llm-host-7GWDO6IX.js.map +1 -0
  93. package/dist/load-I333TGNI.js +8 -0
  94. package/dist/load-I333TGNI.js.map +1 -0
  95. package/dist/local-runtime-5O2NN3GA.js +5 -0
  96. package/dist/local-runtime-5O2NN3GA.js.map +1 -0
  97. package/dist/mcp-tools-I7JK3S2M.js +4 -0
  98. package/dist/{mcp-tools-I6PME3AV.js.map → mcp-tools-I7JK3S2M.js.map} +1 -1
  99. package/dist/model-execution-profile-TFBKCNJG.js +4 -0
  100. package/dist/model-execution-profile-TFBKCNJG.js.map +1 -0
  101. package/dist/model-orchestrator-TVONAUPS.js +9 -0
  102. package/dist/model-orchestrator-TVONAUPS.js.map +1 -0
  103. package/dist/parse-worker.d.ts +1 -1
  104. package/dist/parse-worker.js +2 -1
  105. package/dist/parse-worker.js.map +1 -1
  106. package/dist/paths-2RAH3GME.js +6 -0
  107. package/dist/paths-2RAH3GME.js.map +1 -0
  108. package/dist/resolve-gguf-VPL4YQ6J.js +8 -0
  109. package/dist/resolve-gguf-VPL4YQ6J.js.map +1 -0
  110. package/dist/runtime-session-JRKCQUHY.js +18 -0
  111. package/dist/runtime-session-JRKCQUHY.js.map +1 -0
  112. package/dist/session-ITPYQ5XI.js +11 -0
  113. package/dist/{session-POJUDFPH.js.map → session-ITPYQ5XI.js.map} +1 -1
  114. package/dist/session-store-WFBX7HXL.js +4 -0
  115. package/dist/{session-store-W5RZZHA3.js.map → session-store-WFBX7HXL.js.map} +1 -1
  116. package/dist/{stream-json-Q2JGWPHB.js → stream-json-SLMDKLXP.js} +17 -6
  117. package/dist/stream-json-SLMDKLXP.js.map +1 -0
  118. package/dist/{types-Jl6RJ175.d.ts → types-7AHBUbFm.d.ts} +8 -2
  119. package/dist/ui-Q6DJ3GCU.js +5 -0
  120. package/dist/ui-Q6DJ3GCU.js.map +1 -0
  121. package/dist/vgd-7JV5KCNJ.js +17 -0
  122. package/dist/vgd-7JV5KCNJ.js.map +1 -0
  123. package/package.json +9 -6
  124. package/dist/baseline-BD3A7EXD.js +0 -7
  125. package/dist/chunk-2O5YZVZV.js.map +0 -1
  126. package/dist/chunk-3MXNNBLI.js.map +0 -1
  127. package/dist/chunk-5BDHQ7DD.js.map +0 -1
  128. package/dist/chunk-6CXTPC74.js.map +0 -1
  129. package/dist/chunk-CS37OBE3.js.map +0 -1
  130. package/dist/chunk-GGJZA3Q6.js +0 -961
  131. package/dist/chunk-GGJZA3Q6.js.map +0 -1
  132. package/dist/chunk-I7GY7MVN.js.map +0 -1
  133. package/dist/chunk-JBXNQCGE.js +0 -484
  134. package/dist/chunk-JBXNQCGE.js.map +0 -1
  135. package/dist/chunk-PY3DNX5H.js.map +0 -1
  136. package/dist/chunk-WNIIKCNF.js.map +0 -1
  137. package/dist/fs-IPY4FJKH.js.map +0 -1
  138. package/dist/fs-KCABDURV.js.map +0 -1
  139. package/dist/interactive-A4DISSIW.js.map +0 -1
  140. package/dist/mcp-tools-I6PME3AV.js +0 -3
  141. package/dist/session-POJUDFPH.js +0 -6
  142. package/dist/session-store-W5RZZHA3.js +0 -3
  143. package/dist/stream-json-Q2JGWPHB.js.map +0 -1
  144. package/dist/ui-HOVOPWMX.js +0 -4
  145. package/dist/ui-HOVOPWMX.js.map +0 -1
@@ -1,12 +1,17 @@
1
- import { resolveOne, resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, findNodes, inventory, localApiSurface } from './chunk-GGJZA3Q6.js';
2
- import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-5BDHQ7DD.js';
3
- import { langById, grammarSetVersion, langForExtension, setGrammarsOverride, parseSource } from './chunk-PY3DNX5H.js';
4
- import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
5
- import { impactOf, indexFor, withTimeout, getNodeEmbeddings, queryGraphSemantic, queryGraph, loadEmbedder, isModelReady, cacheDir, loadCache, stableStringify, serializeGraph, parseGraph, acquireLock, releaseLock } from './chunk-WNIIKCNF.js';
1
+ import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-WGUL45AV.js';
2
+ import { resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, inventory, localApiSurface, edgeId, buildModuleResolver, resolve, applyStaticTestLinkage, nodeId } from './chunk-LKVMI67M.js';
3
+ import { resolveOne, findNodes } from './chunk-J7LKEB2R.js';
4
+ import { discoverModels } from './chunk-OOIG3RUM.js';
5
+ import { impactOf, indexFor, withTimeout, getNodeEmbeddings, queryGraphSemantic, queryGraph, loadEmbedder, isModelReady, acquireLock, releaseLock } from './chunk-6ABXHZRN.js';
6
+ import { langById, grammarSetVersion, langForExtension, setGrammarsOverride, parseSource } from './chunk-3Q7QBWFW.js';
7
+ import { redactSecrets } from './chunk-RXP66R2E.js';
8
+ import { writeGraphIndex, SCHEMA_VERSION, loadGraph, ensureVibgrateGitignore, vibgrateDir, writeArtifacts } from './chunk-2XQDMLKB.js';
9
+ import { cacheDir, loadCache, serializeGraph, stableStringify } from './chunk-K3MZIVZW.js';
6
10
  import { shortId, canonicalize, hashString, hashBytes } from './chunk-JFGYT6BI.js';
7
- import * as fs18 from 'fs';
11
+ import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
12
+ import * as fs16 from 'fs';
8
13
  import { spawnSync, execFileSync } from 'child_process';
9
- import * as path2 from 'path';
14
+ import * as path18 from 'path';
10
15
  import ignore from 'ignore';
11
16
  import * as v8 from 'v8';
12
17
  import Graph from 'graphology';
@@ -24,8 +29,8 @@ import { Command } from 'commander';
24
29
  import chalk from 'chalk';
25
30
  import * as zlib from 'zlib';
26
31
  import { promisify } from 'util';
27
- import { encode, decode } from 'gpt-tokenizer/encoding/cl100k_base';
28
32
  import { Worker } from 'worker_threads';
33
+ import { encode, decode } from 'gpt-tokenizer/encoding/cl100k_base';
29
34
 
30
35
  var NPX_INVOCATION = "npx @vibgrate/cli";
31
36
  function whichOnPath(cmd) {
@@ -41,9 +46,9 @@ function whichOnPath(cmd) {
41
46
  }
42
47
  function isOwnBinary(binPath) {
43
48
  try {
44
- const real = fs18.realpathSync(binPath);
49
+ const real = fs16.realpathSync(binPath);
45
50
  if (/[\\/]@vibgrate[\\/]cli[\\/]/.test(real)) return true;
46
- const head = fs18.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
51
+ const head = fs16.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
47
52
  return head.includes("@vibgrate/cli") || head.includes("vibgrate");
48
53
  } catch {
49
54
  return false;
@@ -53,7 +58,7 @@ function isEphemeralNpxBinary(binPath) {
53
58
  const NPX_SEGMENT = /[\\/]_npx[\\/]/;
54
59
  if (NPX_SEGMENT.test(binPath)) return true;
55
60
  try {
56
- return NPX_SEGMENT.test(fs18.realpathSync(binPath));
61
+ return NPX_SEGMENT.test(fs16.realpathSync(binPath));
57
62
  } catch {
58
63
  return false;
59
64
  }
@@ -88,6 +93,14 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
88
93
  ".wrangler",
89
94
  ".turbo",
90
95
  ".cache",
96
+ // Migration / tool backup trees (NativeScript, codemods, agent worktrees)
97
+ ".migration_backup",
98
+ ".update_backup",
99
+ ".backup",
100
+ ".bak",
101
+ ".history",
102
+ ".snapshots",
103
+ ".snapshots_bak",
91
104
  // JavaScript / Node — installed dependency trees
92
105
  "node_modules",
93
106
  "bower_components",
@@ -112,6 +125,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
112
125
  "build",
113
126
  "out",
114
127
  "coverage",
128
+ // Native mobile platform build trees (NativeScript / Cordova / Capacitor)
129
+ "platforms",
130
+ "App_Resources",
115
131
  // Vendored third-party source trees
116
132
  "vendor",
117
133
  "Pods",
@@ -219,19 +235,19 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
219
235
  "flake.lock"
220
236
  ]);
221
237
  function toPosix(p) {
222
- return p.split(path2.sep).join("/");
238
+ return p.split(path18.sep).join("/");
223
239
  }
224
240
  function buildRootIgnore(root, exclude) {
225
241
  const ig = ignore();
226
- const gitignorePath = path2.join(root, ".gitignore");
227
- if (fs18.existsSync(gitignorePath)) {
228
- ig.add(fs18.readFileSync(gitignorePath, "utf8"));
242
+ const gitignorePath = path18.join(root, ".gitignore");
243
+ if (fs16.existsSync(gitignorePath)) {
244
+ ig.add(fs16.readFileSync(gitignorePath, "utf8"));
229
245
  }
230
246
  if (exclude.length) ig.add(exclude);
231
247
  return ig;
232
248
  }
233
249
  function discover(options) {
234
- const root = path2.resolve(options.root);
250
+ const root = path18.resolve(options.root);
235
251
  const onlyLangs = (options.only ?? []).filter(Boolean);
236
252
  const allowLang = (lang) => onlyLangs.length === 0 || onlyLangs.includes(lang.id);
237
253
  for (const id of onlyLangs) {
@@ -240,39 +256,39 @@ function discover(options) {
240
256
  }
241
257
  }
242
258
  const rootIg = buildRootIgnore(root, options.exclude ?? []);
243
- const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path2.resolve(root, p)) : [root]).filter((p) => fs18.existsSync(p));
259
+ const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path18.resolve(root, p)) : [root]).filter((p) => fs16.existsSync(p));
244
260
  const found = /* @__PURE__ */ new Map();
245
261
  const considerFile = (abs) => {
246
- const rel2 = toPosix(path2.relative(root, abs));
262
+ const rel2 = toPosix(path18.relative(root, abs));
247
263
  if (rel2.startsWith("..")) return;
248
264
  if (rel2 === "" || rootIg.ignores(rel2)) return;
249
- if (SKIP_FILES.has(path2.basename(abs).toLowerCase())) return;
250
- const lang = langForExtension(path2.extname(abs));
265
+ if (SKIP_FILES.has(path18.basename(abs).toLowerCase())) return;
266
+ const lang = langForExtension(path18.extname(abs));
251
267
  if (!lang || !allowLang(lang)) return;
252
268
  found.set(rel2, { rel: rel2, abs, lang });
253
269
  };
254
- const walk2 = (dir) => {
270
+ const walk = (dir) => {
255
271
  let entries;
256
272
  try {
257
- entries = fs18.readdirSync(dir, { withFileTypes: true });
273
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
258
274
  } catch {
259
275
  return;
260
276
  }
261
277
  for (const entry of entries) {
262
- const abs = path2.join(dir, entry.name);
263
- const rel2 = toPosix(path2.relative(root, abs));
278
+ const abs = path18.join(dir, entry.name);
279
+ const rel2 = toPosix(path18.relative(root, abs));
264
280
  if (entry.isDirectory()) {
265
281
  if (SKIP_DIRS.has(entry.name)) continue;
266
282
  if (rel2 && rootIg.ignores(`${rel2}/`)) continue;
267
- walk2(abs);
283
+ walk(abs);
268
284
  } else if (entry.isFile()) {
269
285
  considerFile(abs);
270
286
  }
271
287
  }
272
288
  };
273
289
  for (const scope of scopeAbs) {
274
- const stat = fs18.statSync(scope);
275
- if (stat.isDirectory()) walk2(scope);
290
+ const stat = fs16.statSync(scope);
291
+ if (stat.isDirectory()) walk(scope);
276
292
  else if (stat.isFile()) considerFile(scope);
277
293
  }
278
294
  return [...found.values()].sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
@@ -336,458 +352,6 @@ function formatBytes(n) {
336
352
  if (n >= 1024) return `${(n / 1024).toFixed(1)} KiB`;
337
353
  return `${n} B`;
338
354
  }
339
- var JS_TS_EXT = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte", ".astro"];
340
- var OTHER_EXT = [
341
- ".py",
342
- ".pyi",
343
- ".go",
344
- ".rs",
345
- ".rb",
346
- ".java",
347
- ".cs",
348
- ".php",
349
- ".kt",
350
- ".kts",
351
- ".swift",
352
- ".scala",
353
- ".sc",
354
- ".dart",
355
- ".lua",
356
- ".ex",
357
- ".exs",
358
- ".sh",
359
- ".bash",
360
- ".zig",
361
- ".c",
362
- ".cpp",
363
- ".cc",
364
- ".cxx",
365
- ".hpp",
366
- ".hh",
367
- ".hxx",
368
- ".h",
369
- ".m",
370
- ".mm",
371
- ".ml",
372
- ".mli",
373
- ".res",
374
- ".sol"
375
- ];
376
- var EXT_GROUPS = {
377
- ".py": [".py", ".pyi"],
378
- ".pyi": [".py", ".pyi"],
379
- ".java": [".java"],
380
- ".cs": [".cs"],
381
- ".go": [".go"],
382
- ".rs": [".rs"],
383
- ".rb": [".rb"]
384
- };
385
- for (const e of JS_TS_EXT) EXT_GROUPS[e] = JS_TS_EXT;
386
- var CPP_EXT = [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h"];
387
- Object.assign(EXT_GROUPS, {
388
- ".php": [".php"],
389
- ".kt": [".kt", ".kts"],
390
- ".kts": [".kt", ".kts"],
391
- ".swift": [".swift"],
392
- ".scala": [".scala", ".sc"],
393
- ".sc": [".scala", ".sc"],
394
- ".dart": [".dart"],
395
- ".lua": [".lua"],
396
- ".ex": [".ex", ".exs"],
397
- ".exs": [".ex", ".exs"],
398
- ".sh": [".sh", ".bash"],
399
- ".bash": [".sh", ".bash"],
400
- ".zig": [".zig"],
401
- ".c": [".c", ".h"],
402
- ".cpp": CPP_EXT,
403
- ".cc": CPP_EXT,
404
- ".cxx": CPP_EXT,
405
- ".hpp": CPP_EXT,
406
- ".hh": CPP_EXT,
407
- ".hxx": CPP_EXT,
408
- ".h": [...CPP_EXT, ".c"],
409
- // ObjC #imports headers; ObjC++ additionally reaches C++ headers.
410
- ".m": [".h", ".m", ".mm"],
411
- ".mm": [".h", ".m", ".mm", ...CPP_EXT],
412
- ".ml": [".ml", ".mli"],
413
- ".mli": [".ml", ".mli"],
414
- ".res": [".res"],
415
- ".sol": [".sol"],
416
- // Template containers: ERB fragments are Ruby; HTML/EJS script is JS.
417
- ".erb": [".rb", ".erb"],
418
- ".html": JS_TS_EXT,
419
- ".htm": JS_TS_EXT,
420
- ".ejs": JS_TS_EXT
421
- });
422
- function buildModuleResolver(root, relSet) {
423
- const probe = makeProbe(relSet);
424
- const suffixIndex = buildSuffixIndex(relSet);
425
- const tsPaths = loadTsconfigPaths(root);
426
- const workspaces = loadWorkspacePackages(root);
427
- function resolveAlias(source) {
428
- if (!tsPaths) return null;
429
- for (const [pattern, targets] of tsPaths.paths) {
430
- const m = matchPattern(pattern, source);
431
- if (m === null) continue;
432
- for (const target of targets) {
433
- const sub = target.replace("*", m);
434
- const rel2 = posixJoin(tsPaths.baseUrlRel, sub);
435
- const hit = probe(rel2);
436
- if (hit) return hit;
437
- }
438
- }
439
- return null;
440
- }
441
- function resolveWorkspace(source) {
442
- if (!workspaces.size) return null;
443
- let best = null;
444
- for (const [name, dirRel] of workspaces) {
445
- if (source === name || source.startsWith(`${name}/`)) {
446
- const rest = source === name ? "" : source.slice(name.length + 1);
447
- if (!best || name.length > best.dirRel.length) best = { dirRel, rest };
448
- }
449
- }
450
- if (!best) return null;
451
- const base = best.rest ? posixJoin(best.dirRel, best.rest) : best.dirRel;
452
- return probe(base) ?? probe(posixJoin(base, "src/index")) ?? probe(posixJoin(base, "index"));
453
- }
454
- return {
455
- resolve(fromRel, source) {
456
- if (source.includes("\\")) source = source.split("\\").join(".");
457
- if (source.startsWith("./") || source.startsWith("../")) {
458
- return probe(posixJoin(path2.posix.dirname(fromRel), source));
459
- }
460
- if (/\.[A-Za-z0-9]+$/.test(source) && !source.startsWith("<")) {
461
- const hit = probe(posixJoin(path2.posix.dirname(fromRel), source));
462
- if (hit) return hit;
463
- }
464
- if (source.startsWith(".")) {
465
- return resolvePyRelative(probe, fromRel, source);
466
- }
467
- return resolveAlias(source) ?? resolveWorkspace(source) ?? resolveSuffix(suffixIndex, fromRel, source) ?? resolveDotted(probe, source);
468
- }
469
- };
470
- }
471
- function buildSuffixIndex(relSet) {
472
- const byLast = /* @__PURE__ */ new Map();
473
- for (const rel2 of relSet) {
474
- const dot = rel2.lastIndexOf(".");
475
- if (dot < 0) continue;
476
- const ext = rel2.slice(dot);
477
- if (!EXT_GROUPS[ext]) continue;
478
- let key = rel2.slice(0, dot);
479
- const base = key.slice(key.lastIndexOf("/") + 1);
480
- if (base === "__init__" || base === "index") {
481
- key = key.slice(0, Math.max(0, key.length - base.length - 1));
482
- }
483
- if (!key) continue;
484
- const last = key.slice(key.lastIndexOf("/") + 1);
485
- const list = byLast.get(last);
486
- if (list) list.push({ rel: rel2, key, ext });
487
- else byLast.set(last, [{ rel: rel2, key, ext }]);
488
- }
489
- return byLast;
490
- }
491
- function resolveSuffix(index, fromRel, source) {
492
- if (!/^[A-Za-z_][\w.]*$/.test(source) || !source.includes(".")) return null;
493
- const fromDot = fromRel.lastIndexOf(".");
494
- const fromExt = fromDot >= 0 ? fromRel.slice(fromDot) : "";
495
- const allowed = EXT_GROUPS[fromExt];
496
- if (!allowed) return null;
497
- const q = source.replace(/\./g, "/");
498
- const last = q.slice(q.lastIndexOf("/") + 1);
499
- const cands = index.get(last);
500
- if (!cands) return null;
501
- const matches = cands.filter(
502
- (c) => allowed.includes(c.ext) && (c.key === q || c.key.endsWith("/" + q))
503
- );
504
- if (matches.length === 0) return null;
505
- if (matches.length === 1) return matches[0].rel;
506
- return pickClosest(matches, fromRel).rel;
507
- }
508
- function pickClosest(matches, fromRel) {
509
- const fromSegs = fromRel.split("/");
510
- const score = (rel2) => {
511
- const segs = rel2.split("/");
512
- let i = 0;
513
- while (i < segs.length && i < fromSegs.length && segs[i] === fromSegs[i]) i++;
514
- return i;
515
- };
516
- return [...matches].sort((a, b) => {
517
- const d = score(b.rel) - score(a.rel);
518
- if (d) return d;
519
- if (a.rel.length !== b.rel.length) return a.rel.length - b.rel.length;
520
- return a.rel < b.rel ? -1 : 1;
521
- })[0];
522
- }
523
- function resolveDotted(probe, source) {
524
- if (!/^[A-Za-z_][\w.]*$/.test(source) || !source.includes(".")) return null;
525
- const slashed = source.replace(/\./g, "/");
526
- return probe(slashed) ?? probe(posixJoin("src", slashed));
527
- }
528
- function resolvePyRelative(probe, fromRel, source) {
529
- let dots = 0;
530
- while (source[dots] === ".") dots++;
531
- const rest = source.slice(dots);
532
- let dir = path2.posix.dirname(fromRel);
533
- for (let i = 1; i < dots; i++) dir = path2.posix.dirname(dir);
534
- const sub = rest.replace(/\./g, "/");
535
- return probe(sub ? posixJoin(dir, sub) : dir);
536
- }
537
- function relativeResolver(relSet) {
538
- const probe = makeProbe(relSet);
539
- return {
540
- resolve(fromRel, source) {
541
- if (source.startsWith(".")) return probe(posixJoin(path2.posix.dirname(fromRel), source));
542
- return resolveDotted(probe, source);
543
- }
544
- };
545
- }
546
- function makeProbe(relSet) {
547
- return (base) => {
548
- const norm = normalizeRel(base);
549
- if (!norm) return null;
550
- if (relSet.has(norm)) return norm;
551
- for (const ext of JS_TS_EXT) if (relSet.has(norm + ext)) return norm + ext;
552
- for (const ext of JS_TS_EXT) if (relSet.has(`${norm}/index${ext}`)) return `${norm}/index${ext}`;
553
- for (const ext of OTHER_EXT) if (relSet.has(norm + ext)) return norm + ext;
554
- if (relSet.has(`${norm}/__init__.py`)) return `${norm}/__init__.py`;
555
- return null;
556
- };
557
- }
558
- function normalizeRel(p) {
559
- const norm = path2.posix.normalize(p);
560
- const stripped = norm.startsWith("./") ? norm.slice(2) : norm;
561
- return stripped.startsWith("..") ? "" : stripped;
562
- }
563
- function posixJoin(a, b) {
564
- return path2.posix.normalize(path2.posix.join(a, b));
565
- }
566
- function matchPattern(pattern, source) {
567
- const star = pattern.indexOf("*");
568
- if (star === -1) return source === pattern ? "" : null;
569
- const prefix = pattern.slice(0, star);
570
- const suffix = pattern.slice(star + 1);
571
- if (!source.startsWith(prefix) || !source.endsWith(suffix)) return null;
572
- if (source.length < prefix.length + suffix.length) return null;
573
- return source.slice(prefix.length, source.length - suffix.length);
574
- }
575
- function loadTsconfigPaths(root) {
576
- for (const name of ["tsconfig.base.json", "tsconfig.json"]) {
577
- const file = path2.join(root, name);
578
- if (fs18.existsSync(file)) {
579
- const merged = readTsconfigChain(root, file, /* @__PURE__ */ new Set());
580
- if (merged && merged.paths.length) return merged;
581
- }
582
- }
583
- return null;
584
- }
585
- function readTsconfigChain(root, file, seen) {
586
- if (seen.has(file) || seen.size > 6) return null;
587
- seen.add(file);
588
- let cfg;
589
- try {
590
- cfg = parseJsonc(fs18.readFileSync(file, "utf8"));
591
- } catch {
592
- return null;
593
- }
594
- const dir = path2.dirname(file);
595
- let base = { baseUrlRel: "", paths: [] };
596
- if (cfg.extends && cfg.extends.startsWith(".")) {
597
- const extPath = path2.resolve(dir, cfg.extends.endsWith(".json") ? cfg.extends : `${cfg.extends}.json`);
598
- if (fs18.existsSync(extPath)) {
599
- const inherited = readTsconfigChain(root, extPath, seen);
600
- if (inherited) base = inherited;
601
- }
602
- }
603
- const co = cfg.compilerOptions ?? {};
604
- const baseUrlRel = co.baseUrl !== void 0 ? path2.relative(root, path2.resolve(dir, co.baseUrl)).split(path2.sep).join("/") : base.baseUrlRel;
605
- const paths = [...base.paths];
606
- if (co.paths) for (const [k, v] of Object.entries(co.paths)) paths.push([k, v]);
607
- return { baseUrlRel, paths };
608
- }
609
- function loadWorkspacePackages(root) {
610
- const map = /* @__PURE__ */ new Map();
611
- const globs = [];
612
- const rootPkg = path2.join(root, "package.json");
613
- if (fs18.existsSync(rootPkg)) {
614
- try {
615
- const pkg = JSON.parse(fs18.readFileSync(rootPkg, "utf8"));
616
- const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
617
- if (ws) globs.push(...ws);
618
- } catch {
619
- }
620
- }
621
- const pnpmWs = path2.join(root, "pnpm-workspace.yaml");
622
- if (fs18.existsSync(pnpmWs)) {
623
- for (const line of fs18.readFileSync(pnpmWs, "utf8").split("\n")) {
624
- const m = /^\s*-\s*['"]?([^'"\n]+)['"]?\s*$/.exec(line);
625
- if (m) globs.push(m[1].trim());
626
- }
627
- }
628
- for (const dir of expandWorkspaceGlobs(root, globs)) {
629
- const pj = path2.join(dir, "package.json");
630
- try {
631
- const name = JSON.parse(fs18.readFileSync(pj, "utf8")).name;
632
- if (name) map.set(name, path2.relative(root, dir).split(path2.sep).join("/"));
633
- } catch {
634
- }
635
- }
636
- return map;
637
- }
638
- function expandWorkspaceGlobs(root, globs) {
639
- const out = /* @__PURE__ */ new Set();
640
- for (const glob of globs) {
641
- const clean = glob.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
642
- const baseDir = path2.join(root, clean);
643
- const recurse = glob.endsWith("**");
644
- if (glob === clean) {
645
- if (fs18.existsSync(path2.join(baseDir, "package.json"))) out.add(baseDir);
646
- continue;
647
- }
648
- collectPackageDirs(baseDir, recurse ? 4 : 1, out);
649
- }
650
- return [...out];
651
- }
652
- function collectPackageDirs(dir, depth, out) {
653
- if (depth < 0) return;
654
- let entries;
655
- try {
656
- entries = fs18.readdirSync(dir, { withFileTypes: true });
657
- } catch {
658
- return;
659
- }
660
- for (const e of entries) {
661
- if (!e.isDirectory() || e.name === "node_modules" || e.name.startsWith(".")) continue;
662
- const child = path2.join(dir, e.name);
663
- if (fs18.existsSync(path2.join(child, "package.json"))) out.add(child);
664
- else collectPackageDirs(child, depth - 1, out);
665
- }
666
- }
667
- function parseJsonc(text) {
668
- return JSON.parse(stripJsonc(text));
669
- }
670
- function stripJsonc(text) {
671
- let out = "";
672
- let inStr = false;
673
- let strCh = "";
674
- for (let i = 0; i < text.length; i++) {
675
- const ch = text[i];
676
- const next = text[i + 1];
677
- if (inStr) {
678
- out += ch;
679
- if (ch === "\\") {
680
- out += next ?? "";
681
- i++;
682
- } else if (ch === strCh) {
683
- inStr = false;
684
- }
685
- continue;
686
- }
687
- if (ch === '"' || ch === "'") {
688
- inStr = true;
689
- strCh = ch;
690
- out += ch;
691
- continue;
692
- }
693
- if (ch === "/" && next === "/") {
694
- while (i < text.length && text[i] !== "\n") i++;
695
- out += "\n";
696
- continue;
697
- }
698
- if (ch === "/" && next === "*") {
699
- i += 2;
700
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
701
- i++;
702
- continue;
703
- }
704
- out += ch;
705
- }
706
- return out.replace(/,(\s*[}\]])/g, "$1");
707
- }
708
-
709
- // src/engine/ids.ts
710
- function nodeId(parts) {
711
- return shortId(
712
- canonicalize({
713
- t: "node",
714
- kind: parts.kind,
715
- qn: parts.qualifiedName,
716
- file: parts.file,
717
- sig: parts.signature ?? ""
718
- })
719
- );
720
- }
721
- function edgeId(kind, src, dst) {
722
- return shortId(canonicalize({ t: "edge", kind, src, dst }));
723
- }
724
-
725
- // src/engine/tests.ts
726
- function isTestFile(rel2) {
727
- const lower = rel2.toLowerCase();
728
- const base = rel2.split("/").pop() ?? rel2;
729
- const lbase = base.toLowerCase();
730
- if (/(^|\/)(__tests__|__test__)\//.test(lower)) return true;
731
- if (/(^|\/)(tests?|spec|specs)\//.test(lower)) return true;
732
- if (/(^|\/)src\/test\//.test(lower)) return true;
733
- if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(lbase)) return true;
734
- if (/^test_.*\.py$/.test(lbase) || /_test\.py$/.test(lbase)) return true;
735
- if (/_test\.go$/.test(lbase)) return true;
736
- if (/[A-Za-z0-9](Test|Tests|IT)\.java$/.test(base)) return true;
737
- if (/_spec\.rb$/.test(lbase) || /_test\.rb$/.test(lbase)) return true;
738
- if (/[A-Za-z0-9](Test|Tests)\.cs$/.test(base)) return true;
739
- if (/[A-Za-z0-9](Test|Tests)\.swift$/.test(base)) return true;
740
- return false;
741
- }
742
- function applyStaticTestLinkage(nodes, edges) {
743
- const byId = new Map(nodes.map((n) => [n.id, n]));
744
- const testFiles = /* @__PURE__ */ new Set();
745
- for (const n of nodes) if (n.kind === "file" && isTestFile(n.file)) testFiles.add(n.file);
746
- const fileNodeIdByRel = /* @__PURE__ */ new Map();
747
- for (const n of nodes) if (n.kind === "file") fileNodeIdByRel.set(n.file, n.id);
748
- const covered = /* @__PURE__ */ new Set();
749
- const newEdges = /* @__PURE__ */ new Map();
750
- for (const e of edges) {
751
- if (e.kind !== "call") continue;
752
- const src = byId.get(e.src);
753
- const dst = byId.get(e.dst);
754
- if (!src || !dst) continue;
755
- if (testFiles.has(src.file) && !testFiles.has(dst.file) && dst.kind !== "external") {
756
- const testFileId = fileNodeIdByRel.get(src.file);
757
- if (!testFileId) continue;
758
- const id = edgeId("test", testFileId, dst.id);
759
- if (!newEdges.has(id)) {
760
- newEdges.set(id, {
761
- id,
762
- kind: "test",
763
- src: testFileId,
764
- dst: dst.id,
765
- resolution: e.resolution,
766
- confidence: e.confidence
767
- });
768
- }
769
- covered.add(dst.id);
770
- }
771
- }
772
- const outNodes = nodes.map((n) => ({ ...n, tested: testedFlag(n, covered.has(n.id), testFiles) }));
773
- const outEdges = [...edges, ...newEdges.values()].sort(
774
- (a, b) => a.kind.localeCompare(b.kind) || a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst)
775
- );
776
- return {
777
- nodes: outNodes,
778
- edges: outEdges,
779
- testFiles: [...testFiles].sort(),
780
- testEdgeCount: newEdges.size
781
- };
782
- }
783
- function testedFlag(node, covered, testFiles) {
784
- if (node.kind === "external" || node.kind === "file" || node.kind === "package" || node.kind === "module") {
785
- return null;
786
- }
787
- if (testFiles.has(node.file)) return null;
788
- if (node.kind === "function" || node.kind === "method") return covered;
789
- return covered ? true : node.tested ?? null;
790
- }
791
355
 
792
356
  // src/engine/scip.ts
793
357
  var ROLE_DEFINITION = 1;
@@ -976,25 +540,36 @@ function mulberry32(seed) {
976
540
  var CLUSTER_SEED = 42;
977
541
 
978
542
  // src/engine/analyze.ts
543
+ var TIER_LARGE_NODES = 5e3;
544
+ var TIER_XL_NODES = 5e4;
545
+ function pickAnalysisTier(nodeCount, forced) {
546
+ if (forced) return forced;
547
+ if (nodeCount > TIER_XL_NODES) return "xl";
548
+ if (nodeCount > TIER_LARGE_NODES) return "large";
549
+ return "full";
550
+ }
979
551
  var DEP_EDGES = /* @__PURE__ */ new Set(["call", "import", "extends", "implements", "references"]);
980
552
  var CLUSTER_EDGE_KINDS = /* @__PURE__ */ new Set([...DEP_EDGES, "contains"]);
981
553
  var IMPORTANCE_WEIGHTS = { pagerank: 0.4, betweenness: 0.25, eigenvector: 0.2, degree: 0.15 };
982
554
  var DEFAULT_BETWEENNESS_LIMIT = 5e3;
983
555
  function analyze(nodes, edges, options = {}) {
984
- const mode = options.cluster ?? "louvain";
556
+ const tier = pickAnalysisTier(nodes.length, options.tier);
557
+ const mode = options.cluster ?? (tier === "xl" ? "louvain" : "louvain");
985
558
  const ids = [...nodes.map((n) => n.id)].sort((a, b) => a.localeCompare(b));
986
559
  const dg = simpleGraph(ids, edges, DEP_EDGES, "directed");
987
560
  const pr = safeMetric(() => pagerank(dg), ids);
988
- const bw = dg.order <= (options.betweennessLimit ?? DEFAULT_BETWEENNESS_LIMIT) ? safeMetric(() => betweenness(dg), ids) : zeros(ids);
989
- const ev = safeMetric(() => eigenvector(dg), ids);
561
+ const bwLimit = options.betweennessLimit ?? DEFAULT_BETWEENNESS_LIMIT;
562
+ const bw = tier === "full" && dg.order <= bwLimit ? safeMetric(() => betweenness(dg), ids) : zeros(ids);
563
+ const ev = tier === "full" || tier === "large" ? safeMetric(() => eigenvector(dg), ids) : zeros(ids);
990
564
  const deg = degreeMap(dg, ids);
991
565
  const prN = normalize(pr);
992
566
  const bwN = normalize(bw);
993
567
  const evN = normalize(ev);
994
568
  const degN = normalize(deg);
569
+ const weights = tier === "xl" ? { pagerank: 0.7, betweenness: 0, eigenvector: 0, degree: 0.3 } : tier === "large" ? { pagerank: 0.5, betweenness: 0, eigenvector: 0.25, degree: 0.25 } : IMPORTANCE_WEIGHTS;
995
570
  const importance = /* @__PURE__ */ new Map();
996
571
  for (const id of ids) {
997
- const score = IMPORTANCE_WEIGHTS.pagerank * (prN.get(id) ?? 0) + IMPORTANCE_WEIGHTS.betweenness * (bwN.get(id) ?? 0) + IMPORTANCE_WEIGHTS.eigenvector * (evN.get(id) ?? 0) + IMPORTANCE_WEIGHTS.degree * (degN.get(id) ?? 0);
572
+ const score = weights.pagerank * (prN.get(id) ?? 0) + weights.betweenness * (bwN.get(id) ?? 0) + weights.eigenvector * (evN.get(id) ?? 0) + weights.degree * (degN.get(id) ?? 0);
998
573
  importance.set(id, score);
999
574
  }
1000
575
  const importanceN = normalize(importance);
@@ -1003,7 +578,7 @@ function analyze(nodes, edges, options = {}) {
1003
578
  const variance = vals.reduce((a, b) => a + (b - mean) ** 2, 0) / (vals.length || 1);
1004
579
  const std = Math.sqrt(variance);
1005
580
  const hubThreshold = mean + 2 * std;
1006
- const { areaByNode, mode: actualMode } = cluster(ids, edges, mode);
581
+ const { areaByNode, mode: actualMode } = tier === "xl" ? clusterFileLevel(nodes, edges, mode) : cluster(ids, edges, mode);
1007
582
  const outNodes = nodes.map((n) => ({
1008
583
  ...n,
1009
584
  centrality: {
@@ -1018,7 +593,42 @@ function analyze(nodes, edges, options = {}) {
1018
593
  })).sort((a, b) => a.id.localeCompare(b.id));
1019
594
  const areas = buildAreas(outNodes, edges, areaByNode);
1020
595
  const outEdges = scoreSurprise(edges, areaByNode);
1021
- return { nodes: outNodes, edges: outEdges, areas, cluster: actualMode };
596
+ return { nodes: outNodes, edges: outEdges, areas, cluster: actualMode, tier };
597
+ }
598
+ function clusterFileLevel(nodes, edges, mode) {
599
+ const fileNodes = nodes.filter((n) => n.kind === "file");
600
+ const fileIds = fileNodes.map((n) => n.id).sort((a, b) => a.localeCompare(b));
601
+ if (fileIds.length === 0) {
602
+ return { areaByNode: new Map(nodes.map((n) => [n.id, -1])), mode: "none" };
603
+ }
604
+ const fileIdByPath = new Map(fileNodes.map((n) => [n.file, n.id]));
605
+ const fileOf = /* @__PURE__ */ new Map();
606
+ for (const n of nodes) {
607
+ if (n.kind === "file") fileOf.set(n.id, n.id);
608
+ else if (n.file) {
609
+ const fid = fileIdByPath.get(n.file);
610
+ if (fid) fileOf.set(n.id, fid);
611
+ }
612
+ }
613
+ const fileEdges = [];
614
+ const seen = /* @__PURE__ */ new Set();
615
+ for (const e of edges) {
616
+ if (!DEP_EDGES.has(e.kind)) continue;
617
+ const sf = fileOf.get(e.src);
618
+ const df = fileOf.get(e.dst);
619
+ if (!sf || !df || sf === df) continue;
620
+ const key = `${e.kind}|${sf}|${df}`;
621
+ if (seen.has(key)) continue;
622
+ seen.add(key);
623
+ fileEdges.push({ ...e, src: sf, dst: df, id: key });
624
+ }
625
+ const { areaByNode: fileAreas, mode: used } = cluster(fileIds, fileEdges, mode);
626
+ const areaByNode = /* @__PURE__ */ new Map();
627
+ for (const n of nodes) {
628
+ const fid = fileOf.get(n.id);
629
+ areaByNode.set(n.id, fid ? fileAreas.get(fid) ?? -1 : -1);
630
+ }
631
+ return { areaByNode, mode: used };
1022
632
  }
1023
633
  function simpleGraph(ids, edges, kinds, type) {
1024
634
  const g = new Graph({ type, multi: false, allowSelfLoops: false });
@@ -1159,14 +769,14 @@ var DEFAULT_PATHS = [
1159
769
  ];
1160
770
  function loadCoverage(root, explicit) {
1161
771
  const candidates = (explicit && explicit.length ? explicit : DEFAULT_PATHS).map(
1162
- (p) => path2.resolve(root, p)
772
+ (p) => path18.resolve(root, p)
1163
773
  );
1164
- const found = candidates.filter((p) => fs18.existsSync(p));
774
+ const found = candidates.filter((p) => fs16.existsSync(p));
1165
775
  if (found.length === 0) return null;
1166
776
  const map = /* @__PURE__ */ new Map();
1167
777
  for (const file of found) {
1168
778
  try {
1169
- const text = fs18.readFileSync(file, "utf8");
779
+ const text = fs16.readFileSync(file, "utf8");
1170
780
  if (file.endsWith(".json")) mergeIstanbul(map, text, root);
1171
781
  else mergeLcov(map, text, root);
1172
782
  } catch {
@@ -1175,8 +785,8 @@ function loadCoverage(root, explicit) {
1175
785
  return map.size ? map : null;
1176
786
  }
1177
787
  function rel(root, p) {
1178
- const abs = path2.isAbsolute(p) ? p : path2.resolve(root, p);
1179
- return path2.relative(root, abs).split(path2.sep).join("/");
788
+ const abs = path18.isAbsolute(p) ? p : path18.resolve(root, p);
789
+ return path18.relative(root, abs).split(path18.sep).join("/");
1180
790
  }
1181
791
  function bump(map, file, line, hits) {
1182
792
  let lh = map.get(file);
@@ -1463,9 +1073,6 @@ function matchConfidence(entry, nameParts, imports, callees) {
1463
1073
  }
1464
1074
  return conf;
1465
1075
  }
1466
-
1467
- // src/schema.ts
1468
- var SCHEMA_VERSION = "vg-graph/1.0";
1469
1076
  var DEFAULT_INLINE_THRESHOLD = 24;
1470
1077
  async function parseFiles(files, options = {}) {
1471
1078
  const threshold = options.inlineThreshold ?? DEFAULT_INLINE_THRESHOLD;
@@ -1486,7 +1093,7 @@ async function parseInline(files, options) {
1486
1093
  onProgress?.(0, files.length);
1487
1094
  for (const file of files) {
1488
1095
  try {
1489
- const source = fs18.readFileSync(file.abs, "utf8");
1096
+ const source = fs16.readFileSync(file.abs, "utf8");
1490
1097
  out.push(await parseSource(file.rel, file.lang.id, source));
1491
1098
  } catch (err) {
1492
1099
  out.push(emptyParse(file, `parse failed: ${err.message}`));
@@ -1541,9 +1148,9 @@ function isWorkerOom(err) {
1541
1148
  return e?.code === "ERR_WORKER_OUT_OF_MEMORY" || /out of memory/i.test(e?.message ?? "");
1542
1149
  }
1543
1150
  function resolveWorkerFile() {
1544
- const here = path2.dirname(fileURLToPath(import.meta.url));
1545
- const candidate = path2.join(here, "parse-worker.js");
1546
- return fs18.existsSync(candidate) ? candidate : null;
1151
+ const here = path18.dirname(fileURLToPath(import.meta.url));
1152
+ const candidate = path18.join(here, "parse-worker.js");
1153
+ return fs16.existsSync(candidate) ? candidate : null;
1547
1154
  }
1548
1155
  function chunk(items, buckets) {
1549
1156
  const out = Array.from({ length: Math.min(buckets, items.length || 1) }, () => []);
@@ -1569,335 +1176,6 @@ function emptyParse(file, warning) {
1569
1176
  warnings: [warning]
1570
1177
  };
1571
1178
  }
1572
- function resolve4(parses, resolver) {
1573
- const baseNodes = /* @__PURE__ */ new Map();
1574
- const relSet = new Set(parses.map((p) => p.rel));
1575
- const mod = resolver ?? relativeResolver(relSet);
1576
- const defsByName = /* @__PURE__ */ new Map();
1577
- const defsByFile = /* @__PURE__ */ new Map();
1578
- const fileNodeIdByRel = /* @__PURE__ */ new Map();
1579
- for (const p of parses) {
1580
- const fileId = nodeId({ kind: "file", qualifiedName: p.rel, file: p.rel });
1581
- fileNodeIdByRel.set(p.rel, fileId);
1582
- addNode(baseNodes, {
1583
- id: fileId,
1584
- kind: "file",
1585
- name: path2.posix.basename(p.rel),
1586
- qualifiedName: p.rel,
1587
- file: p.rel,
1588
- span: { start: 1, end: 1 },
1589
- lang: p.lang
1590
- });
1591
- for (const d of p.defs) {
1592
- const id = nodeId({
1593
- kind: d.kind,
1594
- qualifiedName: d.qualifiedName,
1595
- file: p.rel,
1596
- signature: d.signature
1597
- });
1598
- addNode(baseNodes, {
1599
- id,
1600
- kind: d.kind,
1601
- name: d.name,
1602
- qualifiedName: d.qualifiedName,
1603
- file: p.rel,
1604
- span: { start: d.startLine, end: d.endLine },
1605
- lang: p.lang,
1606
- signature: d.signature,
1607
- doc: d.doc
1608
- });
1609
- const ref = {
1610
- id,
1611
- name: d.name,
1612
- qualifiedName: d.qualifiedName,
1613
- rel: p.rel,
1614
- lang: p.lang,
1615
- kind: d.kind,
1616
- startByte: d.startByte,
1617
- endByte: d.endByte
1618
- };
1619
- push(defsByName, d.name, ref);
1620
- push(defsByFile, p.rel, ref);
1621
- }
1622
- }
1623
- const filesByNamespace = /* @__PURE__ */ new Map();
1624
- for (const p of parses) for (const ns of p.namespaces ?? []) addToSet(filesByNamespace, ns, p.rel);
1625
- const relsByLang = /* @__PURE__ */ new Map();
1626
- for (const p of parses) push(relsByLang, p.lang, p.rel);
1627
- const nsReachableByRel = /* @__PURE__ */ new Map();
1628
- const moduleReachableByRel = /* @__PURE__ */ new Map();
1629
- for (const p of parses) {
1630
- if (MODULE_SCOPED_LANGS.has(p.lang)) {
1631
- const mod2 = /* @__PURE__ */ new Set();
1632
- for (const rel2 of relsByLang.get(p.lang) ?? []) if (rel2 !== p.rel) mod2.add(rel2);
1633
- moduleReachableByRel.set(p.rel, mod2);
1634
- }
1635
- const reach = /* @__PURE__ */ new Set();
1636
- const namespaces = new Set(p.namespaces ?? []);
1637
- for (const imp of p.imports) {
1638
- namespaces.add(imp.source);
1639
- if (IMPORT_NAMES_A_TYPE.has(p.lang)) {
1640
- const sep9 = Math.max(imp.source.lastIndexOf("."), imp.source.lastIndexOf("\\"));
1641
- if (sep9 > 0) namespaces.add(imp.source.slice(0, sep9));
1642
- }
1643
- }
1644
- for (const ns of namespaces) for (const rel2 of filesByNamespace.get(ns) ?? []) if (rel2 !== p.rel) reach.add(rel2);
1645
- nsReachableByRel.set(p.rel, reach);
1646
- }
1647
- const superTypesByType = /* @__PURE__ */ new Map();
1648
- for (const p of parses) {
1649
- const localDefs = defsByFile.get(p.rel) ?? [];
1650
- for (const h of p.heritage) {
1651
- const encl = enclosingDefRef(localDefs, h.byte);
1652
- if (encl && (encl.kind === "class" || encl.kind === "interface")) addToSet(superTypesByType, encl.name, h.superName);
1653
- }
1654
- }
1655
- const edges = new EdgeSet();
1656
- const unresolved2 = /* @__PURE__ */ new Map();
1657
- const stats = {
1658
- callsResolved: 0,
1659
- callsUnresolved: 0,
1660
- importsResolvedToFile: 0,
1661
- importsExternal: 0,
1662
- resolvers: ["heuristic"]
1663
- };
1664
- for (const p of parses) {
1665
- const fileId = fileNodeIdByRel.get(p.rel);
1666
- const localByQn = /* @__PURE__ */ new Map();
1667
- for (const ref of defsByFile.get(p.rel) ?? []) localByQn.set(ref.qualifiedName, ref.id);
1668
- for (const d of p.defs) {
1669
- const id = localByQn.get(d.qualifiedName);
1670
- const parentQn = parentQualifiedName(d.qualifiedName);
1671
- const parentId = parentQn ? localByQn.get(parentQn) : void 0;
1672
- edges.add("contains", parentId ?? fileId, id, "heuristic", 1);
1673
- }
1674
- }
1675
- const importedFilesByRel = /* @__PURE__ */ new Map();
1676
- for (const p of parses) {
1677
- const fileId = fileNodeIdByRel.get(p.rel);
1678
- const importedRels = /* @__PURE__ */ new Set();
1679
- for (const imp of p.imports) {
1680
- const targetRel = mod.resolve(p.rel, imp.source);
1681
- if (targetRel && targetRel !== p.rel) {
1682
- importedRels.add(targetRel);
1683
- edges.add("import", fileId, fileNodeIdByRel.get(targetRel), "heuristic", 0.9);
1684
- stats.importsResolvedToFile++;
1685
- } else {
1686
- const extId = ensureExternalNode(baseNodes, imp.source);
1687
- edges.add("import", fileId, extId, "heuristic", 1);
1688
- stats.importsExternal++;
1689
- }
1690
- }
1691
- importedFilesByRel.set(p.rel, importedRels);
1692
- }
1693
- for (const p of parses) {
1694
- const fileId = fileNodeIdByRel.get(p.rel);
1695
- const localDefs = defsByFile.get(p.rel) ?? [];
1696
- const imported = importedFilesByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1697
- const testCaller = isTestFile(p.rel);
1698
- const nsReach = nsReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1699
- const modReach = moduleReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1700
- for (const call of p.calls) {
1701
- const srcId = enclosingDefId(localDefs, call.byte) ?? fileId;
1702
- const resolved = resolveCall(call, p.rel, p.lang, imported, defsByName, srcId, testCaller, nsReach, superTypesByType, modReach);
1703
- if (resolved) {
1704
- edges.add("call", srcId, resolved.id, "heuristic", resolved.confidence);
1705
- stats.callsResolved++;
1706
- } else {
1707
- stats.callsUnresolved++;
1708
- bumpUnresolved(unresolved2, srcId, call.callee, "call", p.rel);
1709
- }
1710
- }
1711
- }
1712
- for (const p of parses) {
1713
- const fileId = fileNodeIdByRel.get(p.rel);
1714
- const localDefs = defsByFile.get(p.rel) ?? [];
1715
- const imported = importedFilesByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1716
- const testCaller = isTestFile(p.rel);
1717
- const nsReach = nsReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1718
- const modReach = moduleReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1719
- for (const ref of p.typeRefs) {
1720
- const srcId = enclosingDefId(localDefs, ref.byte) ?? fileId;
1721
- const target = resolveType(ref.name, p.rel, p.lang, imported, defsByName, testCaller, nsReach, modReach);
1722
- if (target) edges.add("references", srcId, target.id, "heuristic", 0.7);
1723
- }
1724
- }
1725
- for (const p of parses) {
1726
- const localDefs = defsByFile.get(p.rel) ?? [];
1727
- const imported = importedFilesByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1728
- const testCaller = isTestFile(p.rel);
1729
- for (const h of p.heritage) {
1730
- const srcId = enclosingDefId(localDefs, h.byte);
1731
- if (!srcId) continue;
1732
- const nsReach = nsReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1733
- const modReach = moduleReachableByRel.get(p.rel) ?? /* @__PURE__ */ new Set();
1734
- const target = resolveType(h.superName, p.rel, p.lang, imported, defsByName, testCaller, nsReach, modReach);
1735
- if (target) edges.add(h.kind, srcId, target.id, "heuristic", 0.85);
1736
- else bumpUnresolved(unresolved2, srcId, h.superName, h.kind, p.rel);
1737
- }
1738
- }
1739
- const nodes = [...baseNodes.values()].map((n) => ({
1740
- ...n,
1741
- importance: 0,
1742
- centrality: { degree: 0, pagerank: 0, betweenness: 0, eigenvector: 0 },
1743
- area: 0,
1744
- isHub: false,
1745
- tested: null
1746
- }));
1747
- const unresolvedList = [...unresolved2.values()].sort(
1748
- (a, b) => a.from.localeCompare(b.from) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
1749
- );
1750
- return { nodes, edges: edges.toArray(), unresolved: unresolvedList, stats };
1751
- }
1752
- function bumpUnresolved(map, from, name, kind, fromRel) {
1753
- const key = `${from}\0${kind}\0${name}`;
1754
- const existing = map.get(key);
1755
- if (existing) existing.count++;
1756
- else map.set(key, { from, name, kind, count: 1, fromRel });
1757
- }
1758
- function addNode(map, n) {
1759
- if (!map.has(n.id)) map.set(n.id, n);
1760
- }
1761
- function ensureExternalNode(map, source) {
1762
- const id = nodeId({ kind: "external", qualifiedName: source, file: "" });
1763
- if (!map.has(id)) {
1764
- map.set(id, {
1765
- id,
1766
- kind: "external",
1767
- name: source,
1768
- qualifiedName: source,
1769
- file: "",
1770
- span: { start: 1, end: 1 },
1771
- lang: "external"
1772
- });
1773
- }
1774
- return id;
1775
- }
1776
- function push(map, key, value) {
1777
- const list = map.get(key);
1778
- if (list) list.push(value);
1779
- else map.set(key, [value]);
1780
- }
1781
- function parentQualifiedName(qn) {
1782
- const idx = qn.lastIndexOf(".");
1783
- return idx > 0 ? qn.slice(0, idx) : null;
1784
- }
1785
- function enclosingDefRef(defs, byte) {
1786
- let best;
1787
- for (const d of defs) {
1788
- if (d.startByte <= byte && d.endByte >= byte) {
1789
- if (!best || d.endByte - d.startByte < best.endByte - best.startByte) best = d;
1790
- }
1791
- }
1792
- return best;
1793
- }
1794
- function enclosingDefId(defs, byte) {
1795
- return enclosingDefRef(defs, byte)?.id;
1796
- }
1797
- function addToSet(map, key, value) {
1798
- const s = map.get(key);
1799
- if (s) s.add(value);
1800
- else map.set(key, /* @__PURE__ */ new Set([value]));
1801
- }
1802
- function parentTypeName(qualifiedName) {
1803
- const i = qualifiedName.lastIndexOf(".");
1804
- return i > 0 ? qualifiedName.slice(0, i) : null;
1805
- }
1806
- function disambiguateInterfaceImpl(pool, superTypesByType) {
1807
- const withParent = pool.map((c) => ({ c, parent: parentTypeName(c.qualifiedName) })).filter((x) => x.parent !== null);
1808
- if (withParent.length < 2) return null;
1809
- const parents = new Set(withParent.map((x) => x.parent));
1810
- const impls = withParent.filter(
1811
- (x) => [...parents].some((other) => other !== x.parent && superTypesByType.get(x.parent)?.has(other))
1812
- );
1813
- if (impls.length === 0) return null;
1814
- const interfaceParents = /* @__PURE__ */ new Set();
1815
- for (const impl of impls) for (const s of superTypesByType.get(impl.parent) ?? []) interfaceParents.add(s);
1816
- const interfaces = withParent.filter((x) => interfaceParents.has(x.parent));
1817
- const uniqueImplIds = new Set(impls.map((x) => x.c.id));
1818
- if (uniqueImplIds.size === 1) return impls[0].c;
1819
- const uniqueIfaceIds = new Set(interfaces.map((x) => x.c.id));
1820
- if (uniqueIfaceIds.size === 1) return interfaces[0].c;
1821
- return null;
1822
- }
1823
- var PACKAGE_SCOPED_LANGS = /* @__PURE__ */ new Set(["go", "java", "cs", "kotlin", "scala", "swift"]);
1824
- var IMPORT_NAMES_A_TYPE = /* @__PURE__ */ new Set(["java", "kotlin", "scala", "php"]);
1825
- var MODULE_SCOPED_LANGS = /* @__PURE__ */ new Set(["swift"]);
1826
- function dirOf(rel2) {
1827
- const i = rel2.lastIndexOf("/");
1828
- return i >= 0 ? rel2.slice(0, i) : "";
1829
- }
1830
- function resolveCall(call, fromRel, fromLang, importedRels, defsByName, enclosingId, fromIsTestFile = false, nsReachableRels = /* @__PURE__ */ new Set(), superTypesByType = /* @__PURE__ */ new Map(), moduleReachableRels = /* @__PURE__ */ new Set()) {
1831
- const candidates = defsByName.get(call.callee);
1832
- if (!candidates || candidates.length === 0) return null;
1833
- const callable = candidates.filter((c) => c.kind === "function" || c.kind === "method");
1834
- const pool = callable.length ? callable : candidates;
1835
- let sameFile = pool.filter((c) => c.rel === fromRel);
1836
- if (call.qualified && enclosingId) sameFile = sameFile.filter((c) => c.id !== enclosingId);
1837
- if (sameFile.length === 1) return { id: sameFile[0].id, confidence: 0.85 };
1838
- if (sameFile.length > 1) {
1839
- const pick = [...sameFile].sort((a, b) => a.id.localeCompare(b.id))[0];
1840
- return { id: pick.id, confidence: 0.4 };
1841
- }
1842
- const dir = dirOf(fromRel);
1843
- const notSelf = (c) => !(call.qualified && enclosingId && c.id === enclosingId);
1844
- const strict = pool.filter(
1845
- (c) => notSelf(c) && (importedRels.has(c.rel) || PACKAGE_SCOPED_LANGS.has(fromLang) && c.lang === fromLang && dirOf(c.rel) === dir || nsReachableRels.has(c.rel))
1846
- );
1847
- if (strict.length === 1) return { id: strict[0].id, confidence: 0.75 };
1848
- const broad = pool.filter((c) => notSelf(c) && (moduleReachableRels.has(c.rel) || strict.includes(c)));
1849
- if (broad.length > 1) {
1850
- const dedup = disambiguateInterfaceImpl(broad, superTypesByType);
1851
- if (dedup) return { id: dedup.id, confidence: 0.6 };
1852
- }
1853
- if (strict.length > 1) return null;
1854
- if (fromLang === "swift" && fromIsTestFile && pool.length === 1) {
1855
- return { id: pool[0].id, confidence: 0.5 };
1856
- }
1857
- return null;
1858
- }
1859
- function resolveType(name, fromRel, fromLang, importedRels, defsByName, fromIsTestFile = false, nsReachableRels = /* @__PURE__ */ new Set(), moduleReachableRels = /* @__PURE__ */ new Set()) {
1860
- const candidates = (defsByName.get(name) ?? []).filter(
1861
- (c) => c.kind === "class" || c.kind === "interface"
1862
- );
1863
- if (candidates.length === 0) return null;
1864
- const sameFile = candidates.filter((c) => c.rel === fromRel);
1865
- if (sameFile.length === 1) return { id: sameFile[0].id };
1866
- const imported = candidates.filter((c) => importedRels.has(c.rel));
1867
- if (imported.length === 1) return { id: imported[0].id };
1868
- if (imported.length > 1) return null;
1869
- if (PACKAGE_SCOPED_LANGS.has(fromLang)) {
1870
- const dir = dirOf(fromRel);
1871
- const samePkg = candidates.filter((c) => c.lang === fromLang && dirOf(c.rel) === dir);
1872
- if (samePkg.length === 1) return { id: samePkg[0].id };
1873
- }
1874
- if (nsReachableRels.size || moduleReachableRels.size) {
1875
- const reach = candidates.filter((c) => nsReachableRels.has(c.rel) || moduleReachableRels.has(c.rel));
1876
- if (reach.length === 1) return { id: reach[0].id };
1877
- }
1878
- if (fromLang === "swift" && fromIsTestFile && candidates.length === 1) {
1879
- return { id: candidates[0].id };
1880
- }
1881
- return null;
1882
- }
1883
- var EdgeSet = class {
1884
- map = /* @__PURE__ */ new Map();
1885
- add(kind, src, dst, resolution, confidence) {
1886
- const id = edgeId(kind, src, dst);
1887
- const existing = this.map.get(id);
1888
- if (existing) {
1889
- existing.count = (existing.count ?? 1) + 1;
1890
- if (confidence > existing.confidence) existing.confidence = confidence;
1891
- return;
1892
- }
1893
- this.map.set(id, { id, kind, src, dst, resolution, confidence, count: 1 });
1894
- }
1895
- toArray() {
1896
- return [...this.map.values()].sort(
1897
- (a, b) => a.kind.localeCompare(b.kind) || a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst)
1898
- );
1899
- }
1900
- };
1901
1179
  var PRECISE_KIND = "tsc";
1902
1180
  function tsResolveEdges(root, tsFiles, nodes) {
1903
1181
  const empty = {
@@ -2121,7 +1399,7 @@ function enclosing3(nodes, line) {
2121
1399
  return best;
2122
1400
  }
2123
1401
  function normalize2(p) {
2124
- return path2.resolve(p).split(path2.sep).join("/");
1402
+ return path18.resolve(p).split(path18.sep).join("/");
2125
1403
  }
2126
1404
 
2127
1405
  // src/engine/epistemic.ts
@@ -2147,66 +1425,565 @@ function epistemicBreakdown(edges) {
2147
1425
  }
2148
1426
  return out;
2149
1427
  }
2150
-
2151
- // src/engine/build.ts
2152
- async function buildGraph(options) {
2153
- const start = nowMs();
2154
- const root = path2.resolve(options.root);
2155
- const files = discover({
2156
- root,
2157
- only: options.only,
2158
- exclude: options.exclude,
2159
- paths: options.paths
2160
- });
2161
- const limits = resolveLimits(options.limits);
2162
- if (limits.maxFiles > 0 && files.length > limits.maxFiles) {
2163
- throw new ResourceLimitError(
2164
- `graph build stopped: ${files.length.toLocaleString()} files exceed the ${limits.maxFiles.toLocaleString()}-file limit. Scope the build (pass sub-paths, add --exclude globs, or --only <langs>), or set VG_MAX_FILES to raise the limit (0 disables it).`
2165
- );
2166
- }
2167
- checkMemoryBudget("discovery", limits.memoryBudgetMb);
2168
- const grammars = grammarSetVersion();
2169
- const cache = loadCache(root, {
2170
- toolVersion: VERSION,
2171
- grammars,
2172
- disabled: options.noCache
2173
- });
2174
- const hashes = /* @__PURE__ */ new Map();
2175
- const fileStats = [];
2176
- const toParse = [];
2177
- const reused = [];
2178
- const buildWarnings = [];
2179
- for (const file of files) {
2180
- let stat;
1428
+ var DOC_BODY_MAX_CHARS = 8e3;
1429
+ var DOC_FILE_MAX_BYTES = 512 * 1024;
1430
+ var DOC_MAX_FILES = 600;
1431
+ var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".txt", ".rst", ".adoc"]);
1432
+ var ENV_EXAMPLE_NAMES = /* @__PURE__ */ new Set([
1433
+ "example.env",
1434
+ "env.example",
1435
+ ".env.example",
1436
+ ".env.sample",
1437
+ ".env.template",
1438
+ ".env.development.example",
1439
+ ".env.local.example",
1440
+ "dotenv.example"
1441
+ ]);
1442
+ var EXACT_BASENAME = {
1443
+ // Manifests / package managers
1444
+ "package.json": "manifest",
1445
+ "package.yaml": "manifest",
1446
+ "package.yml": "manifest",
1447
+ "pyproject.toml": "manifest",
1448
+ "setup.cfg": "manifest",
1449
+ "requirements.txt": "manifest",
1450
+ "requirements-dev.txt": "manifest",
1451
+ "pipfile": "manifest",
1452
+ "go.mod": "manifest",
1453
+ "go.work": "manifest",
1454
+ "cargo.toml": "manifest",
1455
+ "composer.json": "manifest",
1456
+ "gemfile": "manifest",
1457
+ "podfile": "manifest",
1458
+ "pubspec.yaml": "manifest",
1459
+ "mix.exs": "manifest",
1460
+ "build.gradle": "manifest",
1461
+ "build.gradle.kts": "manifest",
1462
+ "settings.gradle": "manifest",
1463
+ "settings.gradle.kts": "manifest",
1464
+ "pom.xml": "manifest",
1465
+ "project.clj": "manifest",
1466
+ "stack.yaml": "manifest",
1467
+ "package.swift": "manifest",
1468
+ // Workspace / monorepo
1469
+ "pnpm-workspace.yaml": "workspace",
1470
+ "pnpm-workspace.yml": "workspace",
1471
+ "lerna.json": "workspace",
1472
+ "nx.json": "workspace",
1473
+ "turbo.json": "workspace",
1474
+ "rush.json": "workspace",
1475
+ "workspace.json": "workspace",
1476
+ // Docker
1477
+ dockerfile: "docker",
1478
+ "dockerfile.dev": "docker",
1479
+ "dockerfile.prod": "docker",
1480
+ "dockerfile.local": "docker",
1481
+ containerfile: "docker",
1482
+ // Compose
1483
+ "docker-compose.yml": "compose",
1484
+ "docker-compose.yaml": "compose",
1485
+ "compose.yml": "compose",
1486
+ "compose.yaml": "compose",
1487
+ "docker-compose.dev.yml": "compose",
1488
+ "docker-compose.prod.yml": "compose",
1489
+ "docker-compose.override.yml": "compose",
1490
+ // Task runners
1491
+ makefile: "task-runner",
1492
+ gnumakefile: "task-runner",
1493
+ justfile: "task-runner",
1494
+ "taskfile.yml": "task-runner",
1495
+ "taskfile.yaml": "task-runner",
1496
+ rakefile: "task-runner",
1497
+ // Build / tool config (exact)
1498
+ "tsconfig.json": "build-config",
1499
+ "jsconfig.json": "build-config",
1500
+ "vite.config.ts": "build-config",
1501
+ "vite.config.js": "build-config",
1502
+ "vite.config.mjs": "build-config",
1503
+ "vitest.config.ts": "build-config",
1504
+ "vitest.config.js": "build-config",
1505
+ "jest.config.js": "build-config",
1506
+ "jest.config.ts": "build-config",
1507
+ "jest.config.cjs": "build-config",
1508
+ "webpack.config.js": "build-config",
1509
+ "webpack.config.ts": "build-config",
1510
+ "rollup.config.js": "build-config",
1511
+ "esbuild.config.js": "build-config",
1512
+ "next.config.js": "build-config",
1513
+ "next.config.mjs": "build-config",
1514
+ "next.config.ts": "build-config",
1515
+ "nuxt.config.ts": "build-config",
1516
+ "nuxt.config.js": "build-config",
1517
+ "astro.config.mjs": "build-config",
1518
+ "svelte.config.js": "build-config",
1519
+ "remix.config.js": "build-config",
1520
+ "angular.json": "build-config",
1521
+ "babel.config.js": "build-config",
1522
+ "babel.config.json": "build-config",
1523
+ ".babelrc": "build-config",
1524
+ ".babelrc.js": "build-config",
1525
+ ".babelrc.json": "build-config",
1526
+ "eslint.config.js": "build-config",
1527
+ "eslint.config.mjs": "build-config",
1528
+ "eslint.config.cjs": "build-config",
1529
+ "eslint.config.ts": "build-config",
1530
+ ".eslintrc": "build-config",
1531
+ ".eslintrc.js": "build-config",
1532
+ ".eslintrc.cjs": "build-config",
1533
+ ".eslintrc.json": "build-config",
1534
+ ".eslintrc.yml": "build-config",
1535
+ ".prettierrc": "build-config",
1536
+ ".prettierrc.js": "build-config",
1537
+ ".prettierrc.json": "build-config",
1538
+ ".prettierrc.yml": "build-config",
1539
+ "prettier.config.js": "build-config",
1540
+ "prettier.config.cjs": "build-config",
1541
+ "ruff.toml": "build-config",
1542
+ ".ruff.toml": "build-config",
1543
+ "pytest.ini": "build-config",
1544
+ "tox.ini": "build-config",
1545
+ "mypy.ini": "build-config",
1546
+ ".mypy.ini": "build-config",
1547
+ "phpunit.xml": "build-config",
1548
+ "phpunit.xml.dist": "build-config",
1549
+ // CI exact
1550
+ "jenkinsfile": "ci",
1551
+ ".gitlab-ci.yml": "ci",
1552
+ ".travis.yml": "ci",
1553
+ "azure-pipelines.yml": "ci",
1554
+ "buildkite.yml": "ci",
1555
+ "cloudbuild.yaml": "ci",
1556
+ "appveyor.yml": "ci",
1557
+ "circle.yml": "ci",
1558
+ // Deploy / infra (root-level common names)
1559
+ "vercel.json": "infra",
1560
+ "netlify.toml": "infra",
1561
+ "wrangler.toml": "infra",
1562
+ "wrangler.json": "infra",
1563
+ "wrangler.jsonc": "infra",
1564
+ "fly.toml": "infra",
1565
+ "railway.toml": "infra",
1566
+ "render.yaml": "infra",
1567
+ "app.yaml": "infra",
1568
+ "Procfile": "infra",
1569
+ "procfile": "infra",
1570
+ "heroku.yml": "infra",
1571
+ "serverless.yml": "infra",
1572
+ "serverless.yaml": "infra",
1573
+ "sam.yaml": "infra",
1574
+ "samconfig.toml": "infra",
1575
+ "cdk.json": "infra",
1576
+ "pulumi.yaml": "infra",
1577
+ "terraform.tf": "infra",
1578
+ // Agent / project meta (also covered by .md extension)
1579
+ "agents.md": "markdown",
1580
+ "claude.md": "markdown",
1581
+ "codeowners": "other-config",
1582
+ ".nvmrc": "other-config",
1583
+ ".node-version": "other-config",
1584
+ ".python-version": "other-config",
1585
+ "runtime.txt": "other-config",
1586
+ ".tool-versions": "other-config",
1587
+ "editorconfig": "other-config",
1588
+ ".editorconfig": "other-config"
1589
+ };
1590
+ var BASENAME_PREFIX = [
1591
+ { re: /^dockerfile(\.|$)/i, category: "docker" },
1592
+ { re: /^docker-compose(\.|$)/i, category: "compose" },
1593
+ { re: /^compose\.(ya?ml)$/i, category: "compose" },
1594
+ { re: /^tsconfig(\..+)?\.json$/i, category: "build-config" },
1595
+ { re: /^jsconfig(\..+)?\.json$/i, category: "build-config" },
1596
+ { re: /^vite\.config\./i, category: "build-config" },
1597
+ { re: /^vitest\.config\./i, category: "build-config" },
1598
+ { re: /^jest\.config\./i, category: "build-config" },
1599
+ { re: /^webpack\.config\./i, category: "build-config" },
1600
+ { re: /^rollup\.config\./i, category: "build-config" },
1601
+ { re: /^next\.config\./i, category: "build-config" },
1602
+ { re: /^nuxt\.config\./i, category: "build-config" },
1603
+ { re: /^astro\.config\./i, category: "build-config" },
1604
+ { re: /^eslint\.config\./i, category: "build-config" },
1605
+ { re: /^\.eslintrc/i, category: "build-config" },
1606
+ { re: /^openapi\./i, category: "api-contract" },
1607
+ { re: /^swagger\./i, category: "api-contract" },
1608
+ { re: /^requirements(-\w+)?\.txt$/i, category: "manifest" },
1609
+ { re: /^dockerfile/i, category: "docker" }
1610
+ ];
1611
+ var PATH_PATTERNS = [
1612
+ // GitHub Actions / workflows
1613
+ { re: /^\.github\/workflows\/.+\.ya?ml$/i, category: "ci" },
1614
+ { re: /^\.github\/actions\/.+/i, category: "ci" },
1615
+ { re: /^\.circleci\/.+\.ya?ml$/i, category: "ci" },
1616
+ { re: /^\.buildkite\/.+/i, category: "ci" },
1617
+ // Helm / k8s (values + charts — not every template blob)
1618
+ { re: /^charts\/[^/]+\/(chart\.ya?ml|values.*\.ya?ml)$/i, category: "infra" },
1619
+ { re: /^deploy\/.+\.ya?ml$/i, category: "infra" },
1620
+ { re: /^k8s\/.+\.ya?ml$/i, category: "infra" },
1621
+ { re: /^kubernetes\/.+\.ya?ml$/i, category: "infra" },
1622
+ { re: /^infra\/.+\.ya?ml$/i, category: "infra" },
1623
+ { re: /^terraform\/.+\.tf$/i, category: "infra" },
1624
+ // API contracts by path
1625
+ { re: /(^|\/)(openapi|swagger)(\.|\/)/i, category: "api-contract" },
1626
+ { re: /(^|\/)api(-)?(spec|schema|contracts?)\//i, category: "api-contract" }
1627
+ ];
1628
+ var API_EXTENSIONS = /* @__PURE__ */ new Set([".graphql", ".gql", ".proto", ".avsc", ".thrift"]);
1629
+ var IMPORTANCE = {
1630
+ markdown: 0.1,
1631
+ text: 0.06,
1632
+ "env-example": 0.12,
1633
+ manifest: 0.16,
1634
+ docker: 0.14,
1635
+ compose: 0.15,
1636
+ ci: 0.14,
1637
+ "build-config": 0.11,
1638
+ "api-contract": 0.13,
1639
+ "task-runner": 0.13,
1640
+ workspace: 0.14,
1641
+ infra: 0.12,
1642
+ "other-config": 0.08
1643
+ };
1644
+ function toPosix2(p) {
1645
+ return p.split(path18.sep).join("/");
1646
+ }
1647
+ function buildRootIgnore2(root, exclude) {
1648
+ const ig = ignore();
1649
+ const gitignorePath = path18.join(root, ".gitignore");
1650
+ if (fs16.existsSync(gitignorePath)) {
2181
1651
  try {
2182
- stat = fs18.statSync(file.abs);
1652
+ ig.add(fs16.readFileSync(gitignorePath, "utf8"));
2183
1653
  } catch {
2184
- continue;
2185
- }
2186
- if (limits.maxFileBytes > 0 && stat.size > limits.maxFileBytes) {
2187
- fileStats.push({
2188
- rel: file.rel,
2189
- size: stat.size,
2190
- mtimeMs: stat.mtimeMs,
2191
- hash: hashString(`vg:oversize:${stat.size}`)
2192
- });
2193
- buildWarnings.push(
2194
- `${file.rel}: skipped \u2014 ${formatBytes(stat.size)} exceeds the ${formatBytes(limits.maxFileBytes)} per-file limit (set VG_MAX_FILE_BYTES to raise it, 0 to disable)`
2195
- );
2196
- continue;
2197
1654
  }
2198
- let hash = "";
1655
+ }
1656
+ if (exclude.length) ig.add(exclude);
1657
+ return ig;
1658
+ }
1659
+ function classifyProjectContext(relOrName) {
1660
+ const rel2 = relOrName.replace(/\\/g, "/");
1661
+ const base = path18.basename(rel2);
1662
+ const baseLower = base.toLowerCase();
1663
+ if (/^\.env($|\.)/i.test(base) && !isEnvExampleName(baseLower)) return null;
1664
+ if (SKIP_FILES.has(baseLower)) return null;
1665
+ if (isEnvExampleName(baseLower)) return "env-example";
1666
+ const exact = EXACT_BASENAME[baseLower] ?? EXACT_BASENAME[base];
1667
+ if (exact) return exact;
1668
+ for (const { re, category } of BASENAME_PREFIX) {
1669
+ if (re.test(base)) return category;
1670
+ }
1671
+ for (const { re, category } of PATH_PATTERNS) {
1672
+ if (re.test(rel2)) return category;
1673
+ }
1674
+ const ext = path18.extname(baseLower);
1675
+ if (DOC_EXTENSIONS.has(ext)) return ext === ".txt" ? "text" : "markdown";
1676
+ if (API_EXTENSIONS.has(ext)) return "api-contract";
1677
+ if ((ext === ".yaml" || ext === ".yml" || ext === ".json") && /openapi|swagger|asyncapi/i.test(base)) {
1678
+ return "api-contract";
1679
+ }
1680
+ if (/^docker-compose\..+\.ya?ml$/i.test(base) || /^compose\..+\.ya?ml$/i.test(base)) {
1681
+ return "compose";
1682
+ }
1683
+ return null;
1684
+ }
1685
+ function isEnvExampleName(baseLower) {
1686
+ if (ENV_EXAMPLE_NAMES.has(baseLower)) return true;
1687
+ if (/^(\.)?env\.(example|sample|template)(\.|$)/i.test(baseLower)) return true;
1688
+ if (/^example\.env/i.test(baseLower)) return true;
1689
+ return false;
1690
+ }
1691
+ function discoverDocs(options) {
1692
+ const root = path18.resolve(options.root);
1693
+ const maxFiles = options.maxFiles ?? DOC_MAX_FILES;
1694
+ const rootIg = buildRootIgnore2(root, options.exclude ?? []);
1695
+ const scopeAbs = (options.paths?.length ? options.paths.map((p) => path18.resolve(root, p)) : [root]).filter(
1696
+ (p) => fs16.existsSync(p)
1697
+ );
1698
+ const found = /* @__PURE__ */ new Map();
1699
+ const consider = (abs) => {
1700
+ if (found.size >= maxFiles) return;
1701
+ const rel2 = toPosix2(path18.relative(root, abs));
1702
+ if (!rel2 || rel2.startsWith("..")) return;
1703
+ if (rootIg.ignores(rel2)) return;
1704
+ const base = path18.basename(abs);
1705
+ if (SKIP_FILES.has(base.toLowerCase())) return;
1706
+ const category = classifyProjectContext(rel2);
1707
+ if (!category) return;
2199
1708
  try {
2200
- hash = hashBytes(fs18.readFileSync(file.abs));
1709
+ const st = fs16.statSync(abs);
1710
+ if (!st.isFile() || st.size > DOC_FILE_MAX_BYTES) return;
1711
+ if (st.size === 0) return;
2201
1712
  } catch {
2202
- continue;
1713
+ return;
1714
+ }
1715
+ found.set(rel2, { rel: rel2, abs, kind: category, category });
1716
+ };
1717
+ const walk = (dir, depth) => {
1718
+ if (found.size >= maxFiles || depth > 12) return;
1719
+ let entries;
1720
+ try {
1721
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
1722
+ } catch {
1723
+ return;
1724
+ }
1725
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1726
+ if (found.size >= maxFiles) return;
1727
+ const abs = path18.join(dir, entry.name);
1728
+ const rel2 = toPosix2(path18.relative(root, abs));
1729
+ if (entry.isDirectory()) {
1730
+ if (SKIP_DIRS.has(entry.name)) continue;
1731
+ if (rel2 && rootIg.ignores(`${rel2}/`) && !rel2.startsWith(".github")) continue;
1732
+ walk(abs, depth + 1);
1733
+ } else if (entry.isFile()) {
1734
+ consider(abs);
1735
+ }
1736
+ }
1737
+ };
1738
+ for (const scope of scopeAbs) {
1739
+ try {
1740
+ const st = fs16.statSync(scope);
1741
+ if (st.isDirectory()) walk(scope, 0);
1742
+ else if (st.isFile()) consider(scope);
1743
+ } catch {
1744
+ }
1745
+ }
1746
+ return [...found.values()].sort((a, b) => a.rel.localeCompare(b.rel));
1747
+ }
1748
+ function prepareDocBody(rel2, raw, category) {
1749
+ const scrubbed = redactSecrets(raw);
1750
+ if (scrubbed.length <= DOC_BODY_MAX_CHARS) return scrubbed;
1751
+ if (category === "manifest" && path18.basename(rel2).toLowerCase() === "package.json") {
1752
+ return summarizePackageJson(scrubbed);
1753
+ }
1754
+ if (category === "api-contract" && /\.(ya?ml|json)$/i.test(rel2)) {
1755
+ return scrubbed.slice(0, DOC_BODY_MAX_CHARS);
1756
+ }
1757
+ if (category === "ci") {
1758
+ return scrubbed.slice(0, DOC_BODY_MAX_CHARS);
1759
+ }
1760
+ return scrubbed.slice(0, DOC_BODY_MAX_CHARS);
1761
+ }
1762
+ function summarizePackageJson(raw) {
1763
+ try {
1764
+ const o = JSON.parse(raw);
1765
+ const keep = {};
1766
+ for (const k of [
1767
+ "name",
1768
+ "version",
1769
+ "description",
1770
+ "private",
1771
+ "type",
1772
+ "packageManager",
1773
+ "engines",
1774
+ "workspaces",
1775
+ "scripts",
1776
+ "dependencies",
1777
+ "devDependencies",
1778
+ "peerDependencies",
1779
+ "optionalDependencies",
1780
+ "main",
1781
+ "bin",
1782
+ "exports"
1783
+ ]) {
1784
+ if (o[k] !== void 0) keep[k] = o[k];
1785
+ }
1786
+ const s = JSON.stringify(keep, null, 2);
1787
+ return s.length <= DOC_BODY_MAX_CHARS ? s : s.slice(0, DOC_BODY_MAX_CHARS);
1788
+ } catch {
1789
+ return raw.slice(0, DOC_BODY_MAX_CHARS);
1790
+ }
1791
+ }
1792
+ function documentNodesFromDocs(docs) {
1793
+ const nodes = [];
1794
+ for (const d of docs) {
1795
+ let raw = "";
1796
+ try {
1797
+ raw = fs16.readFileSync(d.abs, "utf8");
1798
+ } catch {
1799
+ continue;
1800
+ }
1801
+ const category = d.category ?? d.kind;
1802
+ const scrubbed = prepareDocBody(d.rel, raw, category);
1803
+ if (!scrubbed.trim()) continue;
1804
+ const name = path18.basename(d.rel);
1805
+ const qualifiedName = d.rel.replace(/\\/g, "/");
1806
+ const id = nodeId({
1807
+ kind: "document",
1808
+ qualifiedName,
1809
+ file: qualifiedName,
1810
+ signature: category
1811
+ });
1812
+ const lineCount = Math.max(1, scrubbed.split("\n").length);
1813
+ const langTag = category === "markdown" ? "md" : category === "env-example" ? "env" : category === "manifest" ? "manifest" : category === "docker" || category === "compose" ? "docker" : category === "ci" ? "ci" : category === "api-contract" ? "api" : category === "build-config" ? "config" : category === "task-runner" ? "task" : category === "workspace" ? "workspace" : category === "infra" ? "infra" : "txt";
1814
+ nodes.push({
1815
+ id,
1816
+ kind: "document",
1817
+ name,
1818
+ qualifiedName,
1819
+ file: qualifiedName,
1820
+ span: { start: 1, end: lineCount },
1821
+ lang: langTag,
1822
+ signature: `document:${category}`,
1823
+ doc: scrubbed,
1824
+ importance: IMPORTANCE[category] ?? 0.05,
1825
+ centrality: { degree: 0, pagerank: 0, betweenness: 0, eigenvector: 0 },
1826
+ area: -1,
1827
+ isHub: false,
1828
+ tested: null
1829
+ });
1830
+ }
1831
+ return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id));
1832
+ }
1833
+ function shardTsProjects(root, files) {
1834
+ if (files.length === 0) return [];
1835
+ const cache = /* @__PURE__ */ new Map();
1836
+ const byKey = /* @__PURE__ */ new Map();
1837
+ for (const f of files) {
1838
+ const dir = path18.dirname(f.abs);
1839
+ const key = nearestProjectKey(root, dir, cache);
1840
+ const list = byKey.get(key);
1841
+ if (list) list.push(f);
1842
+ else byKey.set(key, [f]);
1843
+ }
1844
+ return [...byKey.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([key, shardFiles]) => ({
1845
+ key,
1846
+ files: [...shardFiles].sort((a, b) => a.rel.localeCompare(b.rel))
1847
+ }));
1848
+ }
1849
+ function nearestProjectKey(root, startDir, cache) {
1850
+ const hit = cache.get(startDir);
1851
+ if (hit !== void 0) return hit;
1852
+ let dir = startDir;
1853
+ const rootAbs = path18.resolve(root);
1854
+ while (true) {
1855
+ if (fs16.existsSync(path18.join(dir, "tsconfig.json")) || fs16.existsSync(path18.join(dir, "tsconfig.base.json")) || fs16.existsSync(path18.join(dir, "package.json"))) {
1856
+ const key = toPosix3(path18.relative(rootAbs, dir)) || ".";
1857
+ cache.set(startDir, key);
1858
+ return key;
1859
+ }
1860
+ if (dir === rootAbs || path18.dirname(dir) === dir) {
1861
+ cache.set(startDir, ".");
1862
+ return ".";
1863
+ }
1864
+ dir = path18.dirname(dir);
1865
+ }
1866
+ }
1867
+ function toPosix3(p) {
1868
+ return p.split(path18.sep).join("/");
1869
+ }
1870
+
1871
+ // src/engine/timing.ts
1872
+ var StageTimer = class {
1873
+ marks = /* @__PURE__ */ new Map();
1874
+ durations = {};
1875
+ start(name) {
1876
+ this.marks.set(name, nowMs());
1877
+ }
1878
+ end(name) {
1879
+ const t0 = this.marks.get(name);
1880
+ if (t0 === void 0) return 0;
1881
+ const ms = Math.round((nowMs() - t0) * 1e3) / 1e3;
1882
+ this.durations[name] = ms;
1883
+ this.marks.delete(name);
1884
+ return ms;
1885
+ }
1886
+ /** Run `fn` under a named stage. */
1887
+ async measure(name, fn) {
1888
+ this.start(name);
1889
+ try {
1890
+ return await fn();
1891
+ } finally {
1892
+ this.end(name);
1893
+ }
1894
+ }
1895
+ snapshot() {
1896
+ return { ...this.durations };
1897
+ }
1898
+ };
1899
+ function nowMs() {
1900
+ return Number(process.hrtime.bigint() / 1000n) / 1e3;
1901
+ }
1902
+
1903
+ // src/engine/build.ts
1904
+ async function buildGraph(options) {
1905
+ const timer = new StageTimer();
1906
+ timer.start("total");
1907
+ const root = path18.resolve(options.root);
1908
+ timer.start("discover");
1909
+ const files = discover({
1910
+ root,
1911
+ only: options.only,
1912
+ exclude: options.exclude,
1913
+ paths: options.paths
1914
+ });
1915
+ timer.end("discover");
1916
+ const limits = resolveLimits(options.limits);
1917
+ if (limits.maxFiles > 0 && files.length > limits.maxFiles) {
1918
+ throw new ResourceLimitError(
1919
+ `graph build stopped: ${files.length.toLocaleString()} files exceed the ${limits.maxFiles.toLocaleString()}-file limit. Scope the build (pass sub-paths, add --exclude globs, or --only <langs>), or set VG_MAX_FILES to raise the limit (0 disables it).`
1920
+ );
1921
+ }
1922
+ checkMemoryBudget("discovery", limits.memoryBudgetMb);
1923
+ const grammars = grammarSetVersion();
1924
+ const cache = loadCache(root, {
1925
+ toolVersion: VERSION,
1926
+ grammars,
1927
+ disabled: options.noCache
1928
+ });
1929
+ timer.start("hash");
1930
+ const hashes = /* @__PURE__ */ new Map();
1931
+ const fileStats = [];
1932
+ const toParse = [];
1933
+ const reused = [];
1934
+ const buildWarnings = [];
1935
+ let statHits = 0;
1936
+ for (const file of files) {
1937
+ let stat;
1938
+ try {
1939
+ stat = fs16.statSync(file.abs);
1940
+ } catch {
1941
+ continue;
1942
+ }
1943
+ if (limits.maxFileBytes > 0 && stat.size > limits.maxFileBytes) {
1944
+ fileStats.push({
1945
+ rel: file.rel,
1946
+ size: stat.size,
1947
+ mtimeMs: stat.mtimeMs,
1948
+ hash: hashString(`vg:oversize:${stat.size}`)
1949
+ });
1950
+ buildWarnings.push(
1951
+ `${file.rel}: skipped \u2014 ${formatBytes(stat.size)} exceeds the ${formatBytes(limits.maxFileBytes)} per-file limit (set VG_MAX_FILE_BYTES to raise it, 0 to disable)`
1952
+ );
1953
+ continue;
1954
+ }
1955
+ const byStat = options.noCache ? void 0 : cache.getByStat(file.rel, stat.mtimeMs, stat.size);
1956
+ if (byStat) {
1957
+ hashes.set(file.rel, byStat.hash);
1958
+ if (byStat.parse.hash !== byStat.hash) byStat.parse.hash = byStat.hash;
1959
+ fileStats.push({
1960
+ rel: file.rel,
1961
+ size: stat.size,
1962
+ mtimeMs: stat.mtimeMs,
1963
+ hash: byStat.hash
1964
+ });
1965
+ reused.push(byStat.parse);
1966
+ statHits++;
1967
+ continue;
1968
+ }
1969
+ let hash = "";
1970
+ try {
1971
+ hash = hashBytes(fs16.readFileSync(file.abs));
1972
+ } catch {
1973
+ continue;
2203
1974
  }
2204
1975
  hashes.set(file.rel, hash);
2205
1976
  fileStats.push({ rel: file.rel, size: stat.size, mtimeMs: stat.mtimeMs, hash });
2206
1977
  const cached2 = cache.get(file.rel, hash);
2207
- if (cached2) reused.push(cached2);
2208
- else toParse.push(file);
1978
+ if (cached2) {
1979
+ reused.push(cached2);
1980
+ cache.set(file.rel, cached2, { mtimeMs: stat.mtimeMs, size: stat.size });
1981
+ } else {
1982
+ toParse.push(file);
1983
+ }
2209
1984
  }
1985
+ timer.end("hash");
1986
+ timer.start("parse");
2210
1987
  const parsedNew = await parseFiles(toParse, {
2211
1988
  jobs: options.jobs,
2212
1989
  inline: options.inline,
@@ -2214,8 +1991,12 @@ async function buildGraph(options) {
2214
1991
  grammarsDir: options.grammarsDir,
2215
1992
  memoryBudgetMb: limits.memoryBudgetMb
2216
1993
  });
1994
+ timer.end("parse");
2217
1995
  checkMemoryBudget("parse", limits.memoryBudgetMb);
2218
- for (const p of parsedNew) cache.set(p.rel, p);
1996
+ for (const p of parsedNew) {
1997
+ const st = fileStats.find((f) => f.rel === p.rel);
1998
+ cache.set(p.rel, p, st ? { mtimeMs: st.mtimeMs, size: st.size } : void 0);
1999
+ }
2219
2000
  const currentRels = new Set(files.map((f) => f.rel));
2220
2001
  cache.prune(currentRels);
2221
2002
  cache.save();
@@ -2223,31 +2004,61 @@ async function buildGraph(options) {
2223
2004
  (a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0
2224
2005
  );
2225
2006
  const warnings = [...buildWarnings, ...parses.flatMap((p) => p.warnings ?? [])];
2007
+ timer.start("resolve");
2226
2008
  const moduleResolver = buildModuleResolver(root, new Set(parses.map((p) => p.rel)));
2227
- const resolved = resolve4(parses, moduleResolver);
2009
+ const resolved = resolve(parses, moduleResolver);
2010
+ timer.end("resolve");
2228
2011
  checkMemoryBudget("resolve", limits.memoryBudgetMb);
2229
2012
  const nodeFileById = new Map(resolved.nodes.map((n) => [n.id, n.file]));
2230
2013
  let edges = resolved.edges;
2231
2014
  const resolvers = [...resolved.stats.resolvers];
2232
2015
  const preciseCoveredFiles = /* @__PURE__ */ new Set();
2233
2016
  let tscStats;
2234
- let tsFiles = options.noTsc ? [] : files.filter((f) => (f.lang.id === "ts" || f.lang.id === "tsx" || f.lang.id === "js") && hashes.has(f.rel)).map((f) => ({ rel: f.rel, abs: f.abs }));
2017
+ const skipTsc = options.noTsc || options.fast;
2018
+ let tsFiles = skipTsc ? [] : files.filter((f) => (f.lang.id === "ts" || f.lang.id === "tsx" || f.lang.id === "js") && hashes.has(f.rel)).map((f) => ({ rel: f.rel, abs: f.abs }));
2235
2019
  if (limits.tscMaxFiles > 0 && tsFiles.length > limits.tscMaxFiles) {
2236
2020
  warnings.push(
2237
2021
  `typescript resolver skipped \u2014 ${tsFiles.length.toLocaleString()} TS/JS files exceed the ${limits.tscMaxFiles.toLocaleString()}-file limit; calls use the heuristic resolver (set VG_TSC_MAX_FILES to raise it, 0 to disable)`
2238
2022
  );
2239
2023
  tsFiles = [];
2240
2024
  }
2025
+ timer.start("tsc");
2241
2026
  if (tsFiles.length) {
2242
- const res = tsResolveEdges(root, tsFiles, resolved.nodes);
2243
- if (res.stats.files > 0) {
2244
- edges = mergePreciseEdges(edges, res.edges, res.coveredFiles, nodeFileById);
2245
- for (const f of res.coveredFiles) preciseCoveredFiles.add(f);
2027
+ const shards = shardTsProjects(root, tsFiles);
2028
+ let filesCovered = 0;
2029
+ let calls = 0;
2030
+ let jsx = 0;
2031
+ let heritage = 0;
2032
+ let resolvedCount = 0;
2033
+ const coveredAll = /* @__PURE__ */ new Set();
2034
+ const preciseAll = [];
2035
+ for (const shard of shards) {
2036
+ const res = tsResolveEdges(root, shard.files, resolved.nodes);
2037
+ if (res.stats.files === 0) continue;
2038
+ filesCovered += res.stats.files;
2039
+ calls += res.stats.calls;
2040
+ jsx += res.stats.jsx;
2041
+ heritage += res.stats.heritage;
2042
+ resolvedCount += res.stats.resolved;
2043
+ for (const f of res.coveredFiles) coveredAll.add(f);
2044
+ preciseAll.push(...res.edges);
2045
+ }
2046
+ if (filesCovered > 0) {
2047
+ edges = mergePreciseEdges(edges, preciseAll, coveredAll, nodeFileById);
2048
+ for (const f of coveredAll) preciseCoveredFiles.add(f);
2246
2049
  if (!resolvers.includes("tsc")) resolvers.unshift("tsc");
2247
- tscStats = res.stats;
2050
+ tscStats = {
2051
+ files: filesCovered,
2052
+ calls,
2053
+ jsx,
2054
+ heritage,
2055
+ resolved: resolvedCount,
2056
+ shards: shards.length
2057
+ };
2248
2058
  }
2249
2059
  checkMemoryBudget("typescript resolution", limits.memoryBudgetMb);
2250
2060
  }
2061
+ timer.end("tsc");
2251
2062
  const scip = options.noScip ? null : loadScipIndex(root, options.scip);
2252
2063
  let scipStats;
2253
2064
  if (scip) {
@@ -2261,9 +2072,30 @@ async function buildGraph(options) {
2261
2072
  let nodes = linked.nodes;
2262
2073
  const coverage = options.noCoverage ? null : loadCoverage(root, options.coverage);
2263
2074
  if (coverage) nodes = applyCoverage(nodes, coverage);
2264
- const analysis = analyze(nodes, linked.edges, { cluster: options.cluster });
2075
+ const docs = discoverDocs({
2076
+ root,
2077
+ exclude: options.exclude,
2078
+ paths: options.paths
2079
+ });
2080
+ for (const d of docs) {
2081
+ try {
2082
+ const st = fs16.statSync(d.abs);
2083
+ const hash = hashBytes(fs16.readFileSync(d.abs));
2084
+ hashes.set(d.rel, hash);
2085
+ fileStats.push({ rel: d.rel, size: st.size, mtimeMs: st.mtimeMs, hash });
2086
+ } catch {
2087
+ }
2088
+ }
2089
+ const docNodes = documentNodesFromDocs(docs);
2090
+ if (docNodes.length) nodes = [...nodes, ...docNodes];
2091
+ timer.start("analyze");
2092
+ const analysis = analyze(nodes, linked.edges, {
2093
+ cluster: options.cluster,
2094
+ tier: options.analysisTier
2095
+ });
2096
+ timer.end("analyze");
2265
2097
  checkMemoryBudget("analysis", limits.memoryBudgetMb);
2266
- const languages = [...new Set(parses.map((p) => p.lang))].sort();
2098
+ const languages = [.../* @__PURE__ */ new Set([...parses.map((p) => p.lang), ...docNodes.map((n) => n.lang)])].sort();
2267
2099
  const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
2268
2100
  const corpusHash = computeCorpusHash(parses, hashes);
2269
2101
  const nodeKindById = new Map(analysis.nodes.map((n) => [n.id, n.kind]));
@@ -2289,8 +2121,9 @@ async function buildGraph(options) {
2289
2121
  toolchain
2290
2122
  },
2291
2123
  meta: {
2292
- root: path2.basename(root) === "" ? "." : ".",
2124
+ root: path18.basename(root) === "" ? "." : ".",
2293
2125
  languages,
2126
+ analysisTier: analysis.tier,
2294
2127
  counts: {
2295
2128
  nodes: analysis.nodes.length,
2296
2129
  edges: analysis.edges.length,
@@ -2316,16 +2149,27 @@ async function buildGraph(options) {
2316
2149
  const grounding = groundGraph(analysis.nodes, analysis.edges, parses);
2317
2150
  if (grounding.length) graph.grounding = grounding;
2318
2151
  }
2152
+ let indexResult;
2153
+ if (!options.noIndex) {
2154
+ timer.start("index");
2155
+ const wr = writeGraphIndex(root, graph);
2156
+ indexResult = { ok: wr.ok, path: wr.ok ? wr.path : void 0, reason: wr.reason };
2157
+ timer.end("index");
2158
+ }
2159
+ timer.end("total");
2160
+ const stages = timer.snapshot();
2319
2161
  return {
2320
2162
  graph,
2321
- timing: { totalMs: Math.round((nowMs() - start) * 1e3) / 1e3 },
2163
+ timing: { totalMs: stages.total ?? 0, stages },
2322
2164
  reparsed: parsedNew.length,
2323
2165
  reused: reused.length,
2166
+ statHits,
2324
2167
  totalFiles: files.length,
2325
2168
  fileStats,
2326
2169
  resolveStats: resolved.stats,
2327
2170
  tsc: tscStats,
2328
2171
  scip: scipStats,
2172
+ index: indexResult,
2329
2173
  warnings
2330
2174
  };
2331
2175
  }
@@ -2335,14 +2179,14 @@ function toRepoRel(p) {
2335
2179
  function loadScipIndex(root, explicit) {
2336
2180
  const candidates = [
2337
2181
  explicit,
2338
- path2.join(root, "index.scip"),
2339
- path2.join(root, ".vibgrate", "index.scip")
2182
+ path18.join(root, "index.scip"),
2183
+ path18.join(root, ".vibgrate", "index.scip")
2340
2184
  ].filter((p) => Boolean(p));
2341
2185
  for (const file of candidates) {
2342
- const abs = path2.isAbsolute(file) ? file : path2.resolve(root, file);
2343
- if (!fs18.existsSync(abs)) continue;
2186
+ const abs = path18.isAbsolute(file) ? file : path18.resolve(root, file);
2187
+ if (!fs16.existsSync(abs)) continue;
2344
2188
  try {
2345
- const index = decodeScipIndex(new Uint8Array(fs18.readFileSync(abs)));
2189
+ const index = decodeScipIndex(new Uint8Array(fs16.readFileSync(abs)));
2346
2190
  if (index.documents.length) {
2347
2191
  return { index, tool: index.toolVersion ? `${index.toolName} ${index.toolVersion}` : index.toolName };
2348
2192
  }
@@ -2384,187 +2228,13 @@ function computeToolchain(grammars, resolvers) {
2384
2228
  };
2385
2229
  }
2386
2230
  function computeCorpusHash(parses, hashes) {
2387
- const list = parses.map((p) => [p.rel, hashes.get(p.rel) ?? p.hash]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2231
+ const fromParses = parses.map((p) => [p.rel, hashes.get(p.rel) ?? p.hash]);
2232
+ const fromHashes = [...hashes.entries()].map(([rel2, hash]) => [rel2, hash]);
2233
+ const byRel = /* @__PURE__ */ new Map();
2234
+ for (const [rel2, hash] of [...fromParses, ...fromHashes]) byRel.set(rel2, hash);
2235
+ const list = [...byRel.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2388
2236
  return hashString(canonicalize(list));
2389
2237
  }
2390
- function nowMs() {
2391
- return Number(process.hrtime.bigint() / 1000n) / 1e3;
2392
- }
2393
-
2394
- // src/engine/report.ts
2395
- function renderReport(graph) {
2396
- const { meta, provenance, nodes, edges } = graph;
2397
- const lines = [];
2398
- lines.push("# Code Graph Report");
2399
- lines.push("");
2400
- lines.push(`Generated by \`vg\` ${provenance.version} \xB7 schema \`${graph.schemaVersion}\``);
2401
- lines.push("");
2402
- lines.push("## Summary");
2403
- lines.push("");
2404
- lines.push(`- **Nodes:** ${meta.counts.nodes.toLocaleString("en-US")}`);
2405
- lines.push(`- **Edges:** ${meta.counts.edges.toLocaleString("en-US")}`);
2406
- lines.push(`- **Areas:** ${meta.counts.areas.toLocaleString("en-US")}`);
2407
- lines.push(`- **Languages:** ${meta.languages.join(", ") || "\u2014"}`);
2408
- lines.push(`- **Clustering:** ${meta.cluster}`);
2409
- lines.push(`- **Resolvers:** ${provenance.resolver.join(", ")}`);
2410
- lines.push(`- **Edge kinds:** ${meta.edgeKinds.join(", ") || "\u2014"}`);
2411
- lines.push("");
2412
- const fileNodes = nodes.filter((n) => n.kind === "file");
2413
- const defsByFile = /* @__PURE__ */ new Map();
2414
- for (const n of nodes) {
2415
- if (n.kind === "file" || n.kind === "external") continue;
2416
- defsByFile.set(n.file, (defsByFile.get(n.file) ?? 0) + 1);
2417
- }
2418
- const topFiles = [...defsByFile.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 15);
2419
- if (topFiles.length) {
2420
- lines.push("## Largest files (by definitions)");
2421
- lines.push("");
2422
- lines.push("| File | Definitions |");
2423
- lines.push("| --- | ---: |");
2424
- for (const [file, count] of topFiles) lines.push(`| \`${file}\` | ${count} |`);
2425
- lines.push("");
2426
- }
2427
- const topNodes = nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, 15);
2428
- if (topNodes.length) {
2429
- lines.push("## Most-connected definitions");
2430
- lines.push("");
2431
- lines.push("| Symbol | Kind | File | Importance |");
2432
- lines.push("| --- | --- | --- | ---: |");
2433
- for (const n of topNodes) {
2434
- lines.push(
2435
- `| \`${n.qualifiedName}\` | ${n.kind} | \`${n.file}\`:${n.span.start} | ${n.importance.toFixed(3)} |`
2436
- );
2437
- }
2438
- lines.push("");
2439
- }
2440
- const byKind = /* @__PURE__ */ new Map();
2441
- for (const e of edges) byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + 1);
2442
- if (byKind.size) {
2443
- lines.push("## Edges by kind");
2444
- lines.push("");
2445
- lines.push("| Kind | Count |");
2446
- lines.push("| --- | ---: |");
2447
- for (const [kind, count] of [...byKind.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
2448
- lines.push(`| ${kind} | ${count} |`);
2449
- }
2450
- lines.push("");
2451
- }
2452
- lines.push("---");
2453
- lines.push("");
2454
- lines.push(`_Files: ${fileNodes.length} \xB7 Corpus: \`${provenance.corpusHash.slice(0, 16)}\u2026\`_`);
2455
- lines.push("");
2456
- return lines.join("\n");
2457
- }
2458
-
2459
- // src/engine/html.ts
2460
- function renderHtml(graph) {
2461
- const top = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, 50);
2462
- const rows = top.map(
2463
- (n) => `<tr><td><code>${esc(n.qualifiedName)}</code></td><td>${esc(n.kind)}</td><td><code>${esc(n.file)}</code>:${n.span.start}</td><td>${n.importance.toFixed(3)}</td></tr>`
2464
- ).join("\n");
2465
- const embedded = stableStringify(graph, 0);
2466
- return `<!doctype html>
2467
- <html lang="en">
2468
- <head>
2469
- <meta charset="utf-8">
2470
- <meta name="viewport" content="width=device-width, initial-scale=1">
2471
- <title>vg \xB7 code graph</title>
2472
- <style>
2473
- :root { color-scheme: light dark; }
2474
- body { font: 14px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }
2475
- h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; }
2476
- table { border-collapse: collapse; width: 100%; }
2477
- th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #8884; }
2478
- td:last-child, th:last-child { text-align: right; }
2479
- .meta { color: #888; } code { font-size: 0.9em; }
2480
- </style>
2481
- </head>
2482
- <body>
2483
- <h1>vg \xB7 code graph</h1>
2484
- <p class="meta">${graph.meta.counts.nodes} nodes \xB7 ${graph.meta.counts.edges} edges \xB7
2485
- ${graph.meta.counts.areas} areas \xB7 ${esc(graph.meta.languages.join(", ") || "\u2014")} \xB7
2486
- clustering: ${esc(graph.meta.cluster)} \xB7 vg ${esc(graph.provenance.version)}</p>
2487
- <h2>Most-connected definitions</h2>
2488
- <table>
2489
- <thead><tr><th>Symbol</th><th>Kind</th><th>Location</th><th>Importance</th></tr></thead>
2490
- <tbody>
2491
- ${rows}
2492
- </tbody>
2493
- </table>
2494
- <p class="meta">Interactive WebGL view arrives in Phase 1. The full graph is embedded below for tooling.</p>
2495
- <script type="application/json" id="vg-graph">
2496
- ${embedded}
2497
- </script>
2498
- </body>
2499
- </html>
2500
- `;
2501
- }
2502
- function esc(s) {
2503
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2504
- }
2505
- function vibgrateDir(root) {
2506
- return path2.join(root, ".vibgrate");
2507
- }
2508
- var DEFAULT_GITIGNORE = [
2509
- "# Created once by vg \u2014 local graph artifacts stay out of git by default.",
2510
- "# Run `vg share` to commit the map for your team (it rewrites this file).",
2511
- "# vg never touches an existing .vibgrate/.gitignore: edit it (or leave it",
2512
- "# empty) to manage these ignores yourself.",
2513
- ".gitignore",
2514
- "cache/",
2515
- "graph.json",
2516
- "graph.html",
2517
- "GRAPH_REPORT.md",
2518
- "facts.jsonl",
2519
- "mcp-navigation.json"
2520
- ];
2521
- function ensureVibgrateGitignore(root) {
2522
- const file = path2.join(vibgrateDir(root), ".gitignore");
2523
- if (fs18.existsSync(file)) return;
2524
- fs18.mkdirSync(vibgrateDir(root), { recursive: true });
2525
- fs18.writeFileSync(file, `${DEFAULT_GITIGNORE.join("\n")}
2526
- `);
2527
- }
2528
- function defaultGraphPath(root) {
2529
- return path2.join(vibgrateDir(root), "graph.json");
2530
- }
2531
- function writeArtifacts(graph, options) {
2532
- const dir = vibgrateDir(options.root);
2533
- fs18.mkdirSync(dir, { recursive: true });
2534
- ensureVibgrateGitignore(options.root);
2535
- const graphPath = options.graphPath ?? defaultGraphPath(options.root);
2536
- fs18.mkdirSync(path2.dirname(graphPath), { recursive: true });
2537
- const tmp = `${graphPath}.${process.pid}.tmp`;
2538
- try {
2539
- fs18.writeFileSync(tmp, serializeGraph(graph));
2540
- fs18.renameSync(tmp, graphPath);
2541
- } catch (err) {
2542
- fs18.rmSync(tmp, { force: true });
2543
- throw err;
2544
- }
2545
- const written = { graphPath };
2546
- if (options.report !== false) {
2547
- const reportPath = path2.join(dir, "GRAPH_REPORT.md");
2548
- fs18.writeFileSync(reportPath, renderReport(graph));
2549
- written.reportPath = reportPath;
2550
- }
2551
- if (options.html !== false) {
2552
- const htmlPath = path2.join(dir, "graph.html");
2553
- fs18.writeFileSync(htmlPath, renderHtml(graph));
2554
- written.htmlPath = htmlPath;
2555
- }
2556
- if (graph.facts && graph.facts.length) {
2557
- const factsPath = path2.join(dir, "facts.jsonl");
2558
- fs18.writeFileSync(factsPath, graph.facts.map((f) => JSON.stringify(f)).join("\n") + "\n");
2559
- written.factsPath = factsPath;
2560
- }
2561
- return written;
2562
- }
2563
- function loadGraph(root, graphPath) {
2564
- const file = graphPath ?? defaultGraphPath(root);
2565
- if (!fs18.existsSync(file)) return null;
2566
- return parseGraph(fs18.readFileSync(file, "utf8"));
2567
- }
2568
2238
 
2569
2239
  // src/engine/verify.ts
2570
2240
  var PINNED = "1970-01-01T00:00:00.000Z";
@@ -2631,21 +2301,21 @@ function usageError(message) {
2631
2301
  }
2632
2302
  var SNAPSHOT_VERSION = "vg-freshness/1";
2633
2303
  function snapshotPath(root) {
2634
- return path2.join(cacheDir(root), "freshness.json");
2304
+ return path18.join(cacheDir(root), "freshness.json");
2635
2305
  }
2636
2306
  function writeSnapshot(root, corpusHash, fileStats, scope = {}) {
2637
2307
  const files = {};
2638
2308
  for (const f of fileStats) files[f.rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: f.hash };
2639
2309
  const snapshot = { version: SNAPSHOT_VERSION, corpusHash, scope: pruneScope(scope), files };
2640
2310
  try {
2641
- fs18.mkdirSync(cacheDir(root), { recursive: true });
2642
- fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2311
+ fs16.mkdirSync(cacheDir(root), { recursive: true });
2312
+ fs16.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2643
2313
  } catch {
2644
2314
  }
2645
2315
  }
2646
2316
  function loadSnapshot(root) {
2647
2317
  try {
2648
- const loaded = JSON.parse(fs18.readFileSync(snapshotPath(root), "utf8"));
2318
+ const loaded = JSON.parse(fs16.readFileSync(snapshotPath(root), "utf8"));
2649
2319
  if (loaded.version === SNAPSHOT_VERSION && loaded.files && typeof loaded.corpusHash === "string") {
2650
2320
  return { ...loaded, scope: loaded.scope ?? {} };
2651
2321
  }
@@ -2681,7 +2351,7 @@ function probeFreshness(root) {
2681
2351
  }
2682
2352
  let stat;
2683
2353
  try {
2684
- stat = fs18.statSync(file.abs);
2354
+ stat = fs16.statSync(file.abs);
2685
2355
  } catch {
2686
2356
  drift.removed.push(file.rel);
2687
2357
  continue;
@@ -2689,7 +2359,7 @@ function probeFreshness(root) {
2689
2359
  if (stat.size === entry.size && stat.mtimeMs === entry.mtimeMs) continue;
2690
2360
  let hash = "";
2691
2361
  try {
2692
- hash = hashBytes(fs18.readFileSync(file.abs));
2362
+ hash = hashBytes(fs16.readFileSync(file.abs));
2693
2363
  } catch {
2694
2364
  drift.removed.push(file.rel);
2695
2365
  continue;
@@ -2706,7 +2376,7 @@ function probeFreshness(root) {
2706
2376
  }
2707
2377
  if (absorbed && !hasDrift(drift)) {
2708
2378
  try {
2709
- fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2379
+ fs16.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2710
2380
  } catch {
2711
2381
  }
2712
2382
  }
@@ -2787,10 +2457,10 @@ function testsToRun(graph, rootId, depth = 4) {
2787
2457
  };
2788
2458
  }
2789
2459
  function detectRunner(root, lang) {
2790
- const pkgPath = path2.join(root, "package.json");
2791
- if (fs18.existsSync(pkgPath)) {
2460
+ const pkgPath = path18.join(root, "package.json");
2461
+ if (fs16.existsSync(pkgPath)) {
2792
2462
  try {
2793
- const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf8"));
2463
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
2794
2464
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2795
2465
  if (deps.vitest) return { name: "vitest", command: (f) => `npx vitest run ${f.join(" ")}`.trim() };
2796
2466
  if (deps.jest) return { name: "jest", command: (f) => `npx jest ${f.join(" ")}`.trim() };
@@ -2798,10 +2468,10 @@ function detectRunner(root, lang) {
2798
2468
  } catch {
2799
2469
  }
2800
2470
  }
2801
- if (lang === "py" || fs18.existsSync(path2.join(root, "pyproject.toml")) || fs18.existsSync(path2.join(root, "pytest.ini"))) {
2471
+ if (lang === "py" || fs16.existsSync(path18.join(root, "pyproject.toml")) || fs16.existsSync(path18.join(root, "pytest.ini"))) {
2802
2472
  return { name: "pytest", command: (f) => `pytest ${f.join(" ")}`.trim() };
2803
2473
  }
2804
- if (lang === "go" || fs18.existsSync(path2.join(root, "go.mod"))) {
2474
+ if (lang === "go" || fs16.existsSync(path18.join(root, "go.mod"))) {
2805
2475
  return { name: "go test", command: () => "go test ./..." };
2806
2476
  }
2807
2477
  if (lang === "cs" || hasFile(root, /\.csproj$/) || hasFile(root, /\.sln$/)) {
@@ -2812,82 +2482,16 @@ function detectRunner(root, lang) {
2812
2482
  }
2813
2483
  function hasFile(root, re) {
2814
2484
  try {
2815
- return fs18.readdirSync(root).some((f) => re.test(f));
2485
+ return fs16.readdirSync(root).some((f) => re.test(f));
2816
2486
  } catch {
2817
2487
  return false;
2818
2488
  }
2819
2489
  }
2820
- function discoverModels(home = os.homedir()) {
2821
- return [...ollama(home), ...lmStudio(home), ...looseGguf(home)].sort(
2822
- (a, b) => a.runtime.localeCompare(b.runtime) || a.name.localeCompare(b.name)
2823
- );
2824
- }
2825
- function ollama(home) {
2826
- const base = path2.join(home, ".ollama", "models", "manifests");
2827
- const out = [];
2828
- walk(base, 4, (file) => {
2829
- const rel2 = path2.relative(base, file).split(path2.sep);
2830
- if (rel2.length >= 2) {
2831
- const tag = rel2[rel2.length - 1];
2832
- const model = rel2[rel2.length - 2];
2833
- out.push({ runtime: "ollama", name: `${model}:${tag}`, path: file });
2834
- }
2835
- });
2836
- return dedupe(out);
2837
- }
2838
- function lmStudio(home) {
2839
- const bases = [path2.join(home, ".lmstudio", "models"), path2.join(home, ".cache", "lm-studio", "models")];
2840
- const out = [];
2841
- for (const base of bases) {
2842
- walk(base, 5, (file) => {
2843
- if (!file.endsWith(".gguf")) return;
2844
- const rel2 = path2.relative(base, file).replace(/\\/g, "/");
2845
- out.push({ runtime: "lm-studio", name: rel2, path: file });
2846
- });
2847
- }
2848
- return dedupe(out);
2849
- }
2850
- function looseGguf(home) {
2851
- const bases = [path2.join(home, "models"), path2.join(home, ".cache", "huggingface")];
2852
- const out = [];
2853
- for (const base of bases) {
2854
- walk(base, 4, (file) => {
2855
- if (!file.endsWith(".gguf")) return;
2856
- out.push({ runtime: "gguf", name: path2.basename(file), path: file });
2857
- });
2858
- }
2859
- return dedupe(out);
2860
- }
2861
- function walk(dir, depth, onFile) {
2862
- if (depth < 0) return;
2863
- let entries;
2864
- try {
2865
- entries = fs18.readdirSync(dir, { withFileTypes: true });
2866
- } catch {
2867
- return;
2868
- }
2869
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2870
- const abs = path2.join(dir, e.name);
2871
- if (e.isDirectory()) walk(abs, depth - 1, onFile);
2872
- else if (e.isFile()) onFile(abs);
2873
- }
2874
- }
2875
- function dedupe(models) {
2876
- const seen = /* @__PURE__ */ new Set();
2877
- const out = [];
2878
- for (const m of models) {
2879
- const key = `${m.runtime}:${m.name}`;
2880
- if (seen.has(key)) continue;
2881
- seen.add(key);
2882
- out.push(m);
2883
- }
2884
- return out;
2885
- }
2886
2490
  function findGitRoot(startDir = process.cwd()) {
2887
- let dir = path2.resolve(startDir);
2491
+ let dir = path18.resolve(startDir);
2888
2492
  for (; ; ) {
2889
- if (fs18.existsSync(path2.join(dir, ".git"))) return dir;
2890
- const parent = path2.dirname(dir);
2493
+ if (fs16.existsSync(path18.join(dir, ".git"))) return dir;
2494
+ const parent = path18.dirname(dir);
2891
2495
  if (parent === dir) return null;
2892
2496
  dir = parent;
2893
2497
  }
@@ -2898,19 +2502,19 @@ function normalizeEntry(line) {
2898
2502
  function ensureGitignored(entry, startDir = process.cwd()) {
2899
2503
  const root = findGitRoot(startDir);
2900
2504
  if (!root) return { status: "not-a-repo", entry };
2901
- const gitignorePath = path2.join(root, ".gitignore");
2505
+ const gitignorePath = path18.join(root, ".gitignore");
2902
2506
  const target = normalizeEntry(entry);
2903
2507
  let existing = "";
2904
2508
  let fileExisted = true;
2905
2509
  try {
2906
- existing = fs18.readFileSync(gitignorePath, "utf8");
2510
+ existing = fs16.readFileSync(gitignorePath, "utf8");
2907
2511
  } catch {
2908
2512
  fileExisted = false;
2909
2513
  }
2910
2514
  const alreadyPresent = existing.split(/\r?\n/).some((line) => normalizeEntry(line) === target);
2911
2515
  if (alreadyPresent) return { status: "present", entry, gitignorePath };
2912
2516
  const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
2913
- fs18.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
2517
+ fs16.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
2914
2518
  `, "utf8");
2915
2519
  return { status: fileExisted ? "added" : "created", entry, gitignorePath };
2916
2520
  }
@@ -3064,14 +2668,14 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
3064
2668
  console.log(chalk.dim("The secret must be registered on your Vibgrate ingest API."));
3065
2669
  }
3066
2670
  if (opts.write) {
3067
- const writePath = path2.resolve(opts.write);
2671
+ const writePath = path18.resolve(opts.write);
3068
2672
  await writeTextFile(writePath, dsn + "\n");
3069
2673
  console.log("");
3070
2674
  console.log(chalk.green("\u2714") + ` DSN written to ${opts.write}`);
3071
- const root = findGitRoot(path2.dirname(writePath));
3072
- const rel2 = root ? path2.relative(root, writePath).split(path2.sep).join("/") : "";
2675
+ const root = findGitRoot(path18.dirname(writePath));
2676
+ const rel2 = root ? path18.relative(root, writePath).split(path18.sep).join("/") : "";
3073
2677
  if (root && rel2 && !rel2.startsWith("..")) {
3074
- const res = ensureGitignored(rel2, path2.dirname(writePath));
2678
+ const res = ensureGitignored(rel2, path18.dirname(writePath));
3075
2679
  if (res.status === "created") {
3076
2680
  console.log(chalk.green("\u2714") + ` Created .gitignore and ignored ${rel2}`);
3077
2681
  } else if (res.status === "added") {
@@ -3089,30 +2693,30 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
3089
2693
  var STORE_DIRNAME = ".vibgrate";
3090
2694
  var STORE_FILENAME = "credentials.json";
3091
2695
  function homeCredentialsPath() {
3092
- return path2.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
2696
+ return path18.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
3093
2697
  }
3094
2698
  function projectCredentialsPath(cwd = process.cwd()) {
3095
2699
  const root = findGitRoot(cwd) ?? cwd;
3096
- return path2.join(root, STORE_DIRNAME, STORE_FILENAME);
2700
+ return path18.join(root, STORE_DIRNAME, STORE_FILENAME);
3097
2701
  }
3098
2702
  function credentialsPath(opts = {}) {
3099
2703
  const override = process.env.VIBGRATE_CREDENTIALS?.trim();
3100
- if (override) return path2.resolve(override);
2704
+ if (override) return path18.resolve(override);
3101
2705
  const local = projectCredentialsPath(opts.cwd);
3102
2706
  if (opts.local) return local;
3103
- if (fs18.existsSync(local)) return local;
2707
+ if (fs16.existsSync(local)) return local;
3104
2708
  return homeCredentialsPath();
3105
2709
  }
3106
2710
  function gitignoreEntryForCredentials(repoRoot, credsFile = credentialsPath()) {
3107
- const rel2 = path2.relative(repoRoot, credsFile);
3108
- if (rel2 && !rel2.startsWith("..") && !path2.isAbsolute(rel2)) {
3109
- return rel2.split(path2.sep).join("/");
2711
+ const rel2 = path18.relative(repoRoot, credsFile);
2712
+ if (rel2 && !rel2.startsWith("..") && !path18.isAbsolute(rel2)) {
2713
+ return rel2.split(path18.sep).join("/");
3110
2714
  }
3111
2715
  return `${STORE_DIRNAME}/${STORE_FILENAME}`;
3112
2716
  }
3113
2717
  function readStoredCredentials(opts = {}) {
3114
2718
  try {
3115
- const raw = fs18.readFileSync(credentialsPath(opts), "utf8");
2719
+ const raw = fs16.readFileSync(credentialsPath(opts), "utf8");
3116
2720
  const parsed = JSON.parse(raw);
3117
2721
  return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
3118
2722
  } catch {
@@ -3121,16 +2725,16 @@ function readStoredCredentials(opts = {}) {
3121
2725
  }
3122
2726
  function writeStoredCredentials(creds, opts = {}) {
3123
2727
  const file = credentialsPath(opts);
3124
- fs18.mkdirSync(path2.dirname(file), { recursive: true });
3125
- fs18.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
2728
+ fs16.mkdirSync(path18.dirname(file), { recursive: true });
2729
+ fs16.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
3126
2730
  try {
3127
- fs18.chmodSync(file, 384);
2731
+ fs16.chmodSync(file, 384);
3128
2732
  } catch {
3129
2733
  }
3130
2734
  }
3131
2735
  function clearStoredCredentials(opts = {}) {
3132
2736
  try {
3133
- fs18.rmSync(credentialsPath(opts));
2737
+ fs16.rmSync(credentialsPath(opts));
3134
2738
  return true;
3135
2739
  } catch {
3136
2740
  return false;
@@ -3501,7 +3105,7 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
3501
3105
  if (opts.strict) process.exit(1);
3502
3106
  return;
3503
3107
  }
3504
- const filePath = path2.resolve(opts.file);
3108
+ const filePath = path18.resolve(opts.file);
3505
3109
  if (!await pathExists(filePath)) {
3506
3110
  console.error(chalk.red(`Scan artifact not found: ${filePath}`));
3507
3111
  console.error(chalk.dim('Run "vibgrate scan" first.'));
@@ -3562,935 +3166,935 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
3562
3166
  if (opts.strict) process.exit(1);
3563
3167
  }
3564
3168
  });
3565
- function readScanArtifact(root) {
3566
- try {
3567
- const file = path2.join(root, ".vibgrate", "scan_result.json");
3568
- return JSON.parse(fs18.readFileSync(file, "utf8"));
3569
- } catch {
3570
- return null;
3169
+ var MAX_FILE_BYTES = 1e6;
3170
+ var PREVIEW_CHARS = 120;
3171
+ var NUL = "\0";
3172
+ var PARALLEL_MIN_BYTES = 64e6;
3173
+ var PARALLEL_MIN_FILES = 2e3;
3174
+ var ROW_CAP = 1e4;
3175
+ async function scanCandidates(root, files, needle, opts) {
3176
+ const needleLower = needle.toLowerCase();
3177
+ if (!needleLower || files.length === 0) return { hits: [], total: 0, truncated: false };
3178
+ if (files.length < PARALLEL_MIN_FILES && !process.env.VG_PARALLEL_MIN_BYTES) {
3179
+ return opts.collectAll ? scanInline(root, files, needleLower, ROW_CAP, false, void 0) : scanInline(root, files, needleLower, opts.budget, true, void 0);
3180
+ }
3181
+ const { over, sizes } = measure(root, files);
3182
+ if (!over) {
3183
+ return opts.collectAll ? scanInline(root, files, needleLower, ROW_CAP, false, sizes) : scanInline(root, files, needleLower, opts.budget, true, sizes);
3184
+ }
3185
+ const out = await scanParallel(root, files, needleLower);
3186
+ if (!opts.collectAll && out.hits.length > opts.budget) {
3187
+ out.hits.length = opts.budget;
3188
+ out.truncated = true;
3571
3189
  }
3190
+ return out;
3572
3191
  }
3573
- function loadVulnerabilities(root) {
3574
- return readScanArtifact(root)?.extended?.vulnerabilities ?? null;
3192
+ function parallelMinBytes() {
3193
+ const v = Number(process.env.VG_PARALLEL_MIN_BYTES);
3194
+ return Number.isFinite(v) && v >= 0 ? v : PARALLEL_MIN_BYTES;
3575
3195
  }
3576
- function resolvePackageTarget(root, pkg) {
3577
- const artifact = readScanArtifact(root);
3578
- for (const project of artifact?.projects ?? []) {
3579
- const dep = project.dependencies?.find((d) => d.package === pkg);
3580
- if (dep) {
3581
- return {
3582
- ecosystem: projectTypeToVulnEcosystem(project.type) ?? "unknown",
3583
- currentVersion: dep.resolvedVersion,
3584
- latestVersion: dep.latestStable,
3585
- majorsBehind: dep.majorsBehind
3586
- };
3196
+ function measure(root, files) {
3197
+ const threshold = parallelMinBytes();
3198
+ const sizes = /* @__PURE__ */ new Map();
3199
+ let total = 0;
3200
+ for (const rel2 of files) {
3201
+ let size;
3202
+ try {
3203
+ size = fs16.statSync(path18.join(root, rel2)).size;
3204
+ } catch {
3205
+ continue;
3587
3206
  }
3207
+ sizes.set(rel2, size);
3208
+ if (size <= MAX_FILE_BYTES) total += size;
3209
+ if (total >= threshold) return { over: true, sizes };
3588
3210
  }
3589
- const rec = inventory(root).records.find((r) => r.name === pkg);
3590
- if (rec) {
3591
- const ecosystem = rec.ecosystem === "npm" ? "npm" : rec.ecosystem === "pypi" ? "pypi" : "unknown";
3592
- return { ecosystem, currentVersion: rec.installed ?? null, latestVersion: rec.latest ?? null, majorsBehind: null };
3593
- }
3594
- return { ecosystem: "unknown", currentVersion: null, latestVersion: null, majorsBehind: null };
3211
+ return { over: false, sizes };
3595
3212
  }
3596
- function openFixableAdvisories(root, pkg) {
3597
- const vulns = loadVulnerabilities(root);
3598
- const match = vulns?.packages.find((p) => p.package === pkg);
3599
- return (match?.advisories ?? []).filter((a) => a.fixedVersions.length).map((a) => a.id);
3213
+ function matchLines(text, needleLower) {
3214
+ const low = text.toLowerCase();
3215
+ if (!low.includes(needleLower)) return [];
3216
+ const lowLines = low.split("\n");
3217
+ const rawLines = text.split("\n");
3218
+ const hits = [];
3219
+ for (let i = 0; i < lowLines.length; i++) {
3220
+ if (lowLines[i].includes(needleLower)) hits.push({ line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW_CHARS) });
3221
+ }
3222
+ return hits;
3600
3223
  }
3601
- function filterBySeverity(result, minSeverity) {
3602
- if (!minSeverity || minSeverity === "unknown") return result;
3603
- const floor = severityRank(minSeverity);
3604
- const packages = result.packages.map((p) => ({ ...p, advisories: p.advisories.filter((a) => severityRank(a.severity) >= floor) })).filter((p) => p.advisories.length > 0);
3605
- const totalAdvisories = packages.reduce((n, p) => n + p.advisories.length, 0);
3606
- return { ...result, packages, totalAdvisories };
3224
+ function readCandidate(abs, knownSize) {
3225
+ try {
3226
+ const size = knownSize ?? fs16.statSync(abs).size;
3227
+ if (size > MAX_FILE_BYTES) return null;
3228
+ const text = fs16.readFileSync(abs, "utf8");
3229
+ return text.includes(NUL) ? null : text;
3230
+ } catch {
3231
+ return null;
3232
+ }
3607
3233
  }
3608
-
3609
- // src/mcp/attribution.ts
3610
- var MCP_MAX_COMMITS = 300;
3611
- var ECOSYSTEM_MAP = {
3612
- npm: "npm",
3613
- pypi: "pypi",
3614
- go: "go",
3615
- rust: "cargo",
3616
- ruby: "rubygems",
3617
- php: "composer",
3618
- dotnet: "nuget",
3619
- dart: "pub"
3620
- // swift (no advisory ecosystem) and java (no resolved lockfile) have no timeline.
3621
- };
3622
- function brief(commit) {
3623
- return { sha: commit.shortSha, author: commit.authorName, date: commit.date, subject: commit.subject };
3234
+ function scanInline(root, files, needleLower, cap, stopAtCap, sizes) {
3235
+ const hits = [];
3236
+ let total = 0;
3237
+ let truncated = false;
3238
+ for (const rel2 of files) {
3239
+ const text = readCandidate(path18.join(root, rel2), sizes?.get(rel2));
3240
+ if (text === null) continue;
3241
+ for (const h of matchLines(text, needleLower)) {
3242
+ total++;
3243
+ if (hits.length < cap) hits.push({ file: rel2, line: h.line, preview: h.preview });
3244
+ else {
3245
+ truncated = true;
3246
+ if (stopAtCap) return { hits, total, truncated };
3247
+ }
3248
+ }
3249
+ }
3250
+ return { hits, total, truncated };
3624
3251
  }
3625
- async function attributedInventory(root, opts = {}) {
3626
- const inv = inventory(root);
3627
- if (!opts.attribute) return { ...inv, attribution: "off" };
3628
- if (!await gitHistoryAvailable(root)) return { ...inv, attribution: "unavailable" };
3629
- const timelines = await buildVersionTimelines(root, { maxCommits: MCP_MAX_COMMITS });
3630
- if (!timelines) return { ...inv, attribution: "unavailable" };
3631
- const records = inv.records.map((r) => {
3632
- const ecosystem = ECOSYSTEM_MAP[r.ecosystem];
3633
- if (!ecosystem) return r;
3634
- const pt = findPackageTimeline(timelines, ecosystem, r.name);
3635
- if (!pt || !pt.changes.length) return r;
3636
- return {
3637
- ...r,
3638
- addedBy: brief(pt.changes[0].commit),
3639
- currentVersionBy: brief(pt.changes[pt.changes.length - 1].commit)
3640
- };
3252
+ async function scanParallel(root, files, needleLower) {
3253
+ const n = Math.min(files.length, Math.max(2, Math.min(8, os.cpus().length - 1)));
3254
+ const shards = Array.from({ length: n }, () => []);
3255
+ files.forEach((f, i) => shards[i % n].push(f));
3256
+ let results;
3257
+ try {
3258
+ results = await Promise.all(shards.map((shard) => runWorker(root, shard, needleLower)));
3259
+ } catch {
3260
+ return scanInline(root, files, needleLower, ROW_CAP, false, void 0);
3261
+ }
3262
+ const hits = [];
3263
+ let total = 0;
3264
+ let truncated = false;
3265
+ for (const r of results) {
3266
+ total += r.total;
3267
+ truncated = truncated || r.truncated;
3268
+ for (const h of r.hits) hits.push(h);
3269
+ }
3270
+ hits.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line);
3271
+ if (hits.length > ROW_CAP) {
3272
+ hits.length = ROW_CAP;
3273
+ truncated = true;
3274
+ }
3275
+ return { hits, total, truncated };
3276
+ }
3277
+ var WORKER_SOURCE = [
3278
+ "const { parentPort, workerData } = require('worker_threads');",
3279
+ "const fs = require('fs');",
3280
+ "const path = require('path');",
3281
+ `const MAX = ${MAX_FILE_BYTES};`,
3282
+ `const PREVIEW = ${PREVIEW_CHARS};`,
3283
+ `const CAP = ${ROW_CAP};`,
3284
+ "const NUL = String.fromCharCode(0);",
3285
+ "const NL = String.fromCharCode(10);",
3286
+ "const { root, files, needleLower } = workerData;",
3287
+ "const hits = []; let total = 0, truncated = false;",
3288
+ "for (const rel of files) {",
3289
+ " const abs = path.join(root, rel);",
3290
+ " let text;",
3291
+ ' try { if (fs.statSync(abs).size > MAX) continue; text = fs.readFileSync(abs, "utf8"); } catch { continue; }',
3292
+ " if (text.includes(NUL)) continue;",
3293
+ " const low = text.toLowerCase();",
3294
+ " if (!low.includes(needleLower)) continue;",
3295
+ " const lowLines = low.split(NL); const rawLines = text.split(NL);",
3296
+ " for (let i = 0; i < lowLines.length; i++) {",
3297
+ " if (!lowLines[i].includes(needleLower)) continue;",
3298
+ " total++;",
3299
+ " if (hits.length < CAP) hits.push({ file: rel, line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW) });",
3300
+ " else truncated = true;",
3301
+ " }",
3302
+ "}",
3303
+ "parentPort.postMessage({ hits, total, truncated });"
3304
+ ].join("\n");
3305
+ function runWorker(root, files, needleLower) {
3306
+ return new Promise((resolve12, reject) => {
3307
+ const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { root, files, needleLower } });
3308
+ let settled = false;
3309
+ w.once("message", (m) => {
3310
+ settled = true;
3311
+ resolve12(m);
3312
+ });
3313
+ w.once("error", reject);
3314
+ w.once("exit", (code) => {
3315
+ if (!settled && code !== 0) reject(new Error(`literal-scan worker exited with code ${code}`));
3316
+ });
3641
3317
  });
3642
- return { ...inv, records, attribution: "git" };
3643
3318
  }
3644
- function countTokens(text) {
3645
- return encode(text).length;
3319
+
3320
+ // src/engine/search.ts
3321
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
3322
+ ".git",
3323
+ ".vibgrate",
3324
+ "node_modules",
3325
+ "dist",
3326
+ "build",
3327
+ "out",
3328
+ "target",
3329
+ "vendor",
3330
+ "__pycache__",
3331
+ // .NET intermediate output and coverage reports: purely generated trees that a
3332
+ // C#/coverage-heavy repo pays for on every literal sweep. `bin` is deliberately
3333
+ // NOT here — plenty of repos keep real scripts in bin/ (rails, node CLIs), and
3334
+ // completeness beats speed; the binary-extension skip below keeps a .NET bin/
3335
+ // cheap anyway.
3336
+ "obj",
3337
+ "coverage"
3338
+ ]);
3339
+ var MAX_FILES_SCANNED = 2e4;
3340
+ var BINARY_EXTS = /* @__PURE__ */ new Set([
3341
+ ".dll",
3342
+ ".pdb",
3343
+ ".exe",
3344
+ ".so",
3345
+ ".dylib",
3346
+ ".a",
3347
+ ".lib",
3348
+ ".o",
3349
+ ".class",
3350
+ ".jar",
3351
+ ".wasm",
3352
+ ".node",
3353
+ ".png",
3354
+ ".jpg",
3355
+ ".jpeg",
3356
+ ".gif",
3357
+ ".webp",
3358
+ ".ico",
3359
+ ".icns",
3360
+ ".bmp",
3361
+ ".zip",
3362
+ ".gz",
3363
+ ".tgz",
3364
+ ".bz2",
3365
+ ".xz",
3366
+ ".7z",
3367
+ ".rar",
3368
+ ".woff",
3369
+ ".woff2",
3370
+ ".ttf",
3371
+ ".otf",
3372
+ ".eot",
3373
+ ".mp3",
3374
+ ".mp4",
3375
+ ".mov",
3376
+ ".avi",
3377
+ ".pdf"
3378
+ ]);
3379
+ function hasBinaryExt(rel2) {
3380
+ return BINARY_EXTS.has(path18.extname(rel2).toLowerCase());
3646
3381
  }
3647
- function truncateToTokens(text, budget, marker = "\n\u2026(truncated)") {
3648
- if (!Number.isFinite(budget) || budget <= 0) return { text, truncated: false, tokens: countTokens(text) };
3649
- const tokens = encode(text);
3650
- if (tokens.length <= budget) return { text, truncated: false, tokens: tokens.length };
3651
- const markerTokens = encode(marker).length;
3652
- const keep = Math.max(0, budget - markerTokens);
3653
- let slice = decode(tokens.slice(0, keep));
3654
- const lastNl = slice.lastIndexOf("\n");
3655
- if (lastNl > 0) slice = slice.slice(0, lastNl);
3656
- const out = slice + marker;
3657
- return { text: out, truncated: true, tokens: countTokens(out) };
3382
+ function toPosix4(rel2) {
3383
+ return path18.sep === "\\" ? rel2.replaceAll("\\", "/") : rel2;
3658
3384
  }
3659
-
3660
- // src/engine/selection-weights.ts
3661
- var DEFAULT_SELECTION_WEIGHTS = {
3662
- headingUsage: 4,
3663
- headingPreamble: -6,
3664
- hasCode: 3,
3665
- codeDensity: 0.5,
3666
- queryOverlap: 5,
3667
- symbolMatch: 2,
3668
- position: -0.1,
3669
- linkDensity: -3
3670
- };
3671
-
3672
- // src/engine/select.ts
3673
- var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "to", "of", "in", "for", "and", "or", "is", "how", "do", "i", "use", "using", "with", "on", "my"]);
3674
- var USAGE_RE = /\b(usage|example|examples|quick ?start|getting started|api|guide|how to|reference|tutorial|basic)\b/i;
3675
- var PREAMBLE_RE = /\b(install|installation|license|licence|contributing|contributors?|sponsors?|table of contents|toc|changelog|funding|code of conduct|security|acknowledge)\b/i;
3676
- function splitSections(md) {
3677
- const out = [];
3678
- let cur = { heading: "", level: 0};
3679
- let buf = [];
3680
- const flush = () => {
3681
- const body = buf.join("\n").trim();
3682
- if (cur.heading || body) out.push({ heading: cur.heading, level: cur.level, body });
3683
- buf = [];
3684
- };
3685
- for (const line of md.split("\n")) {
3686
- const h = /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
3687
- if (h) {
3688
- flush();
3689
- cur = { heading: h[2].trim(), level: h[1].length, body: "" };
3690
- } else {
3691
- buf.push(line);
3692
- }
3385
+ async function searchSymbols(graph, root, query, limit) {
3386
+ let q = query.trim();
3387
+ if (!q) return { matches: [], moreAvailable: false, hint: "query is required" };
3388
+ const quoted = /^(["'`])(.+)\1$/.exec(q);
3389
+ if (quoted && quoted[2].trim()) q = quoted[2].trim();
3390
+ const isPhrase = Boolean(quoted) || /\s/.test(q) || /[/\\]/.test(q) && !q.includes(":") && !/[*?]/.test(q);
3391
+ let nodes = findNodes(graph, q).filter((n) => n.kind !== "file");
3392
+ if (nodes.length === 0) {
3393
+ nodes = fileSymbolNodes(graph, q);
3394
+ }
3395
+ if (nodes.length === 0 && isPhrase) {
3396
+ nodes = reconstructedIdentifierNodes(graph, q);
3397
+ }
3398
+ if (nodes.length === 0) {
3399
+ const tokens = queryTokens(q);
3400
+ if (tokens.length >= 2) nodes = multiTokenNodes(graph, tokens);
3401
+ }
3402
+ const symbolCap = isPhrase ? Math.max(1, Math.floor(limit / 3)) : limit;
3403
+ const symbolHits = nodes.slice(0, symbolCap).map((n, i) => ({
3404
+ kind: n.kind,
3405
+ name: n.qualifiedName,
3406
+ file: n.file,
3407
+ line: n.span.start,
3408
+ score: Math.round((1 - i / Math.max(nodes.length, 1)) * 100) / 100
3409
+ }));
3410
+ const spare = limit - symbolHits.length;
3411
+ const textHits = [];
3412
+ let truncatedScan = false;
3413
+ let totalTextMatches;
3414
+ const skipScan = !isPhrase && hasExactSymbolMatch(nodes, q);
3415
+ if (spare > 0 && !skipScan) {
3416
+ const seen = new Set(symbolHits.map((h) => `${h.file}:${h.line}`));
3417
+ const scan = await scanFiles(root, q, spare, seen, textHits, isPhrase);
3418
+ truncatedScan = scan.truncated;
3419
+ if (isPhrase) totalTextMatches = scan.total;
3693
3420
  }
3694
- flush();
3695
- return out;
3696
- }
3697
- function chunkBody(body) {
3698
- const chunks = [];
3699
- let cur = [];
3700
- let inFence = false;
3701
- const flush = () => {
3702
- const c = cur.join("\n").trim();
3703
- if (c) chunks.push(c);
3704
- cur = [];
3705
- };
3706
- for (const line of body.split("\n")) {
3707
- if (/^\s*```/.test(line)) inFence = !inFence;
3708
- if (!inFence && line.trim() === "") {
3709
- flush();
3710
- continue;
3711
- }
3712
- cur.push(line);
3421
+ const matches = [...symbolHits, ...textHits];
3422
+ if (matches.length === 0) {
3423
+ return {
3424
+ matches,
3425
+ moreAvailable: false,
3426
+ hint: "no symbol or text match \u2014 for meaning-level questions (symptoms, relationships, what-breaks-if) use query_graph"
3427
+ };
3713
3428
  }
3714
- flush();
3715
- return chunks;
3429
+ const literalTruncated = totalTextMatches !== void 0 && totalTextMatches > textHits.length;
3430
+ const moreAvailable = isPhrase ? literalTruncated || truncatedScan : nodes.length > symbolHits.length || truncatedScan;
3431
+ const result = { matches, moreAvailable };
3432
+ if (totalTextMatches !== void 0) result.totalTextMatches = totalTextMatches;
3433
+ if (literalTruncated) {
3434
+ result.hint = `showing ${textHits.length} of ${totalTextMatches}${truncatedScan ? "+" : ""} literal matches \u2014 raise limit or narrow the query to get them all`;
3435
+ }
3436
+ return result;
3716
3437
  }
3717
- function stripNoise(body) {
3718
- return body.split("\n").filter((l) => !/^\s*(?:!\[[^\]]*\]\([^)]*\)\s*)+$/.test(l)).join("\n").trim();
3438
+ function fileSymbolNodes(graph, q) {
3439
+ if (!/[/\\]/.test(q) && !/\.\w+$/.test(q)) return [];
3440
+ const norm = q.replaceAll("\\", "/").replace(/^\.\//, "");
3441
+ const inFile = graph.nodes.filter(
3442
+ (n) => n.kind !== "file" && (n.file === norm || n.file.endsWith(`/${norm}`))
3443
+ );
3444
+ return [...inFile].sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName));
3719
3445
  }
3720
- function tokenizeQuery(q) {
3721
- if (!q) return [];
3722
- return [...new Set(q.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1 && !STOPWORDS.has(w)))];
3446
+ function hasExactSymbolMatch(nodes, q) {
3447
+ if (nodes.length === 0) return false;
3448
+ if (/^(.*):(\d+)$/.test(q)) return true;
3449
+ const lower = q.toLowerCase();
3450
+ return nodes.some((n) => n.id === q || n.qualifiedName.toLowerCase() === lower || n.name.toLowerCase() === lower);
3723
3451
  }
3724
- function symbolsFromApi(api) {
3725
- if (!api) return [];
3726
- const out = /* @__PURE__ */ new Set();
3727
- const re = /\b(?:function|class|interface|type|const|enum|namespace)\s+([A-Za-z_$][\w$]*)/g;
3728
- let m;
3729
- while (m = re.exec(api)) out.add(m[1]);
3730
- return [...out];
3452
+ function queryTokens(q) {
3453
+ return [...new Set(q.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
3731
3454
  }
3732
- function scoreSection(s, idx, queryTerms, symbols, w) {
3733
- const text = `${s.heading}
3734
- ${s.body}`.toLowerCase();
3735
- let score = 0;
3736
- if (USAGE_RE.test(s.heading)) score += w.headingUsage;
3737
- if (PREAMBLE_RE.test(s.heading)) score += w.headingPreamble;
3738
- const codeBlocks = Math.floor((s.body.match(/```/g) ?? []).length / 2);
3739
- if (codeBlocks > 0) score += w.hasCode + Math.min(codeBlocks, 5) * w.codeDensity;
3740
- for (const t of queryTerms) if (text.includes(t)) score += w.queryOverlap;
3741
- for (const sym of symbols) if (sym.length > 2 && text.includes(sym.toLowerCase())) score += w.symbolMatch;
3742
- score += idx * w.position;
3743
- const links = (s.body.match(/\]\(|https?:\/\//g) ?? []).length;
3744
- const lineCount = Math.max(1, s.body.split("\n").filter((l) => l.trim()).length);
3745
- if (links / lineCount > 0.5) score += w.linkDensity;
3746
- return score;
3455
+ function reconstructedIdentifierNodes(graph, q) {
3456
+ const words = q.trim().split(/\s+/).filter(Boolean);
3457
+ if (words.length < 2) return [];
3458
+ const concat = findNodes(graph, words.join(""));
3459
+ if (concat.length) return concat.filter((n) => n.kind !== "file");
3460
+ const snake = findNodes(graph, words.join("_"));
3461
+ return snake.filter((n) => n.kind !== "file");
3747
3462
  }
3748
- var apiBlock = (api) => api ? `## API (types)
3749
-
3750
- \`\`\`ts
3751
- ${api}
3752
- \`\`\`` : "";
3753
- function selectForBudget(input) {
3754
- const w = input.weights ?? DEFAULT_SELECTION_WEIGHTS;
3755
- const api = apiBlock(input.apiSurface);
3756
- const readme = input.readme.trim();
3757
- if (!input.budget || !Number.isFinite(input.budget)) {
3758
- const full2 = [readme, api].filter(Boolean).join("\n\n");
3759
- return { text: full2, tokens: countTokens(full2), truncated: false };
3760
- }
3761
- const budget = input.budget;
3762
- const queryTerms = tokenizeQuery(input.query);
3763
- const symbols = symbolsFromApi(input.apiSurface);
3764
- const render = (s) => {
3765
- const body = stripNoise(s.body);
3766
- if (!body) return "";
3767
- return `${s.heading ? `## ${s.heading}
3768
- ` : ""}${body}`.trim();
3769
- };
3770
- const sections = splitSections(readme).filter((s) => !PREAMBLE_RE.test(s.heading));
3771
- const full = [api, ...sections.map(render)].filter(Boolean).join("\n\n").trim();
3772
- if (countTokens(full) <= budget) {
3773
- return { text: full, tokens: countTokens(full), truncated: false };
3774
- }
3775
- const units = [];
3776
- for (const s of sections) {
3777
- if (countTokens(s.body) > budget) {
3778
- chunkBody(s.body).forEach((p, i) => units.push({ heading: i === 0 ? s.heading : "", level: s.level, body: p }));
3779
- } else {
3780
- units.push(s);
3463
+ function multiTokenNodes(graph, tokens) {
3464
+ const scored = [];
3465
+ for (const n of graph.nodes) {
3466
+ if (n.kind === "file") continue;
3467
+ const name = n.name.toLowerCase();
3468
+ const qn = n.qualifiedName.toLowerCase();
3469
+ let hits = 0;
3470
+ let exact = false;
3471
+ for (const t of tokens) {
3472
+ if (name === t || qn === t) {
3473
+ hits++;
3474
+ exact = true;
3475
+ } else if (qn.includes(t)) hits++;
3781
3476
  }
3477
+ if (exact || hits >= 2) scored.push({ node: n, hits, exact });
3782
3478
  }
3783
- const ranked = units.map((s, idx) => ({ s, idx, score: scoreSection(s, idx, queryTerms, symbols, w) })).sort((a, b) => b.score - a.score || a.idx - b.idx);
3784
- const out = [];
3785
- let used = 0;
3786
- const add3 = (chunk2) => {
3787
- const t = countTokens(chunk2);
3788
- if (used + t <= budget) {
3789
- out.push(chunk2);
3790
- used += t;
3791
- return true;
3479
+ return scored.sort((a, b) => Number(b.exact) - Number(a.exact) || b.hits - a.hits || b.node.importance - a.node.importance || a.node.qualifiedName.localeCompare(b.node.qualifiedName)).map((e) => e.node);
3480
+ }
3481
+ async function scanFiles(root, needle, budget, seen, out, countAll) {
3482
+ const listing = listCandidateFiles(root, needle);
3483
+ const outcome = await scanCandidates(root, listing.files, needle, { collectAll: countAll, budget });
3484
+ let overlap = 0;
3485
+ for (const h of outcome.hits) {
3486
+ if (seen.has(`${h.file}:${h.line}`)) {
3487
+ overlap++;
3488
+ continue;
3792
3489
  }
3793
- return false;
3794
- };
3795
- if (api && !add3(api) && used < budget) {
3796
- out.push(truncateToTokens(api, budget - used).text);
3797
- used = budget;
3490
+ seen.add(`${h.file}:${h.line}`);
3491
+ if (out.length < budget) out.push({ kind: "text", file: h.file, line: h.line, preview: h.preview });
3798
3492
  }
3799
- const picked = [];
3800
- for (const { s, idx } of ranked) {
3801
- const chunk2 = render(s);
3802
- if (!chunk2) continue;
3803
- if (used + countTokens(`
3804
- ${chunk2}`) <= budget) {
3805
- picked.push(idx);
3806
- used += countTokens(`
3807
- ${chunk2}`);
3808
- }
3493
+ return { total: outcome.total - overlap, truncated: listing.truncated || outcome.truncated };
3494
+ }
3495
+ function listCandidateFiles(root, needle) {
3496
+ return ripgrepCandidates(root, needle) ?? walkCandidatesCached(root);
3497
+ }
3498
+ var LISTING_TTL_MS = 2e3;
3499
+ var listingCache = /* @__PURE__ */ new Map();
3500
+ function listingTtlMs() {
3501
+ const v = Number(process.env.VG_LISTING_TTL_MS);
3502
+ return Number.isFinite(v) && v >= 0 ? v : LISTING_TTL_MS;
3503
+ }
3504
+ function walkCandidatesCached(root) {
3505
+ const ttl = listingTtlMs();
3506
+ if (ttl > 0) {
3507
+ const hit = listingCache.get(root);
3508
+ if (hit && Date.now() - hit.at < ttl) return hit.listing;
3809
3509
  }
3810
- picked.sort((a, b) => a - b);
3811
- for (const idx of picked) out.push(render(units[idx]));
3812
- if (used < budget - 20) {
3813
- const next = ranked.find((r) => !picked.includes(r.idx) && render(r.s));
3814
- if (next) {
3815
- const head = truncateToTokens(render(next.s), budget - used).text;
3816
- if (head.trim()) out.push(head);
3510
+ const listing = walkCandidates(root);
3511
+ if (ttl > 0) listingCache.set(root, { listing, at: Date.now() });
3512
+ return listing;
3513
+ }
3514
+ function walkCandidates(root) {
3515
+ const files = [];
3516
+ let truncated = false;
3517
+ const stack = [root];
3518
+ while (stack.length) {
3519
+ const dir = stack.pop();
3520
+ let entries;
3521
+ try {
3522
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
3523
+ } catch {
3524
+ continue;
3525
+ }
3526
+ for (const e of entries) {
3527
+ if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
3528
+ const abs = path18.join(dir, e.name);
3529
+ if (e.isDirectory()) {
3530
+ stack.push(abs);
3531
+ continue;
3532
+ }
3533
+ if (hasBinaryExt(e.name)) continue;
3534
+ if (files.length >= MAX_FILES_SCANNED) {
3535
+ truncated = true;
3536
+ continue;
3537
+ }
3538
+ files.push(toPosix4(path18.relative(root, abs)));
3817
3539
  }
3818
3540
  }
3819
- const text = out.join("\n\n").trim();
3820
- return { text, tokens: countTokens(text), truncated: true };
3541
+ files.sort();
3542
+ return { files, truncated };
3821
3543
  }
3822
-
3823
- // src/engine/quality.ts
3824
- function assessDocQuality(content, opts = {}) {
3825
- const minTokens = opts.minTokens ?? 60;
3826
- const lc = content.toLowerCase();
3827
- const reasons = [];
3828
- let score = 0;
3829
- const hasCode = /```/.test(content) || /(^|\n)( {4}|\t)\S/.test(content);
3830
- if (hasCode) score += 2;
3831
- else reasons.push("no code example");
3832
- const tokens = countTokens(content);
3833
- if (tokens >= minTokens) score += 1;
3834
- else reasons.push(`thin (${tokens} tokens)`);
3835
- const bareName = opts.name?.toLowerCase().replace(/^@[^/]+\//, "");
3836
- if (bareName && bareName.length > 1) {
3837
- if (lc.includes(bareName)) score += 1;
3838
- else reasons.push("library name absent");
3839
- }
3840
- const qt = tokenizeQuery(opts.query);
3841
- let onTopic = qt.length === 0;
3842
- if (qt.length) {
3843
- const hits = qt.filter((t) => lc.includes(t)).length;
3844
- if (hits) {
3845
- score += Math.min(hits, 2);
3846
- onTopic = true;
3847
- } else {
3848
- reasons.push("query terms absent");
3544
+ var rgAvailable;
3545
+ function hasRipgrep() {
3546
+ if (process.env.VG_DISABLE_RIPGREP) return false;
3547
+ if (rgAvailable === void 0) {
3548
+ try {
3549
+ rgAvailable = spawnSync("rg", ["--version"], { stdio: "ignore", timeout: 3e3 }).status === 0;
3550
+ } catch {
3551
+ rgAvailable = false;
3849
3552
  }
3850
3553
  }
3851
- if (opts.symbols?.length) {
3852
- if (opts.symbols.some((s) => s.length > 2 && lc.includes(s.toLowerCase()))) score += 1;
3853
- else reasons.push("no API symbols present");
3854
- }
3855
- const sufficient = hasCode && tokens >= minTokens && onTopic;
3856
- return { score, sufficient, reasons };
3554
+ return rgAvailable;
3857
3555
  }
3858
-
3859
- // src/engine/hosted.ts
3860
- function hostedBase(opts = {}) {
3861
- const explicit = opts.base || process.env.VIBGRATE_LIB_HOST;
3862
- if (explicit) return explicit.replace(/\/+$/, "");
3863
- let host;
3864
- try {
3865
- host = resolveIngestHost(opts.region, opts.ingest);
3866
- } catch {
3867
- host = "us.ingest.vibgrate.com";
3868
- }
3869
- return `https://${host}`;
3556
+ function ripgrepCandidates(root, needle) {
3557
+ if (!hasRipgrep()) return null;
3558
+ const globs = [...IGNORE_DIRS].flatMap((d) => ["--glob", `!**/${d}/**`, "--glob", `!${d}/**`]);
3559
+ const res = spawnSync(
3560
+ "rg",
3561
+ ["--files-with-matches", "--fixed-strings", "--ignore-case", "--no-ignore", "--no-messages", ...globs, "--", needle, "."],
3562
+ { cwd: root, encoding: "utf8", timeout: 15e3, maxBuffer: 32 * 1024 * 1024 }
3563
+ );
3564
+ if (res.error || res.status !== 0 && res.status !== 1 || res.signal) return null;
3565
+ const files = parseRgFileList(res.stdout);
3566
+ if (files.length > MAX_FILES_SCANNED) return { files: files.slice(0, MAX_FILES_SCANNED), truncated: true };
3567
+ return { files, truncated: false };
3870
3568
  }
3871
- async function fetchHostedDocs(req, opts = {}) {
3872
- const f = opts.fetchImpl ?? globalThis.fetch;
3873
- if (!f) return null;
3874
- const url = `${hostedBase(opts)}/v1/lib/docs`;
3875
- const ctrl = new AbortController();
3876
- const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 4e3);
3877
- const headers = { "content-type": "application/json" };
3878
- if (opts.auth) {
3879
- headers["Authorization"] = `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`;
3880
- headers["X-Vibgrate-Timestamp"] = String(Date.now());
3569
+ function parseRgFileList(stdout, sep9 = path18.sep) {
3570
+ const seen = /* @__PURE__ */ new Set();
3571
+ for (const raw of stdout.split("\n")) {
3572
+ if (!raw) continue;
3573
+ const posix = sep9 === "\\" ? raw.replaceAll("\\", "/") : raw;
3574
+ const rel2 = posix.startsWith("./") ? posix.slice(2) : posix;
3575
+ if (!rel2) continue;
3576
+ const segs = rel2.split("/");
3577
+ if (segs.some((s) => s.startsWith(".") || IGNORE_DIRS.has(s))) continue;
3578
+ if (hasBinaryExt(rel2)) continue;
3579
+ seen.add(rel2);
3881
3580
  }
3581
+ return [...seen].sort();
3582
+ }
3583
+ function readScanArtifact(root) {
3882
3584
  try {
3883
- const res = await f(url, {
3884
- method: "POST",
3885
- headers,
3886
- body: JSON.stringify({
3887
- name: req.name,
3888
- targetId: req.targetId,
3889
- query: req.query,
3890
- verbosity: req.verbosity,
3891
- max_tokens: req.maxTokens
3892
- }),
3893
- signal: ctrl.signal
3894
- });
3895
- if (!res.ok) return null;
3896
- const data = await res.json();
3897
- const content = typeof data.content === "string" ? data.content : typeof data.docs === "string" ? data.docs : "";
3898
- if (!content.trim()) return null;
3899
- return {
3900
- content,
3901
- version: typeof data.version === "string" ? data.version : null,
3902
- source: "hosted",
3903
- metadata: data.metadata ?? void 0
3904
- };
3585
+ const file = path18.join(root, ".vibgrate", "scan_result.json");
3586
+ return JSON.parse(fs16.readFileSync(file, "utf8"));
3905
3587
  } catch {
3906
3588
  return null;
3907
- } finally {
3908
- clearTimeout(timer);
3909
3589
  }
3910
3590
  }
3911
- async function publishPrivateLibrary(req, opts = {}) {
3912
- const f = opts.fetchImpl ?? globalThis.fetch;
3913
- if (!f) return { ok: false, status: 0, message: "no network available" };
3914
- if (!opts.auth) return { ok: false, status: 401, message: "a DSN is required to publish a private library (run `vibgrate login` or set VIBGRATE_DSN)" };
3915
- const url = `${hostedBase(opts)}/v1/lib/private`;
3916
- const ctrl = new AbortController();
3917
- const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
3918
- try {
3919
- const res = await f(url, {
3920
- method: "POST",
3921
- headers: {
3922
- "content-type": "application/json",
3923
- Authorization: `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`,
3924
- "X-Vibgrate-Timestamp": String(Date.now())
3925
- },
3926
- body: JSON.stringify({
3927
- name: req.name,
3928
- version: req.version,
3929
- readme: req.readme,
3930
- dts: req.dts,
3931
- compliance: req.compliance,
3932
- language: req.language,
3933
- sourceUrl: req.sourceUrl
3934
- }),
3935
- signal: ctrl.signal
3936
- });
3937
- const data = await res.json().catch(() => ({}));
3938
- if (!res.ok) return { ok: false, status: res.status, error: typeof data.error === "string" ? data.error : void 0, message: typeof data.message === "string" ? data.message : void 0 };
3939
- return {
3940
- ok: true,
3941
- targetId: String(data.targetId ?? ""),
3942
- ecosystem: String(data.ecosystem ?? ""),
3943
- entities: Number(data.entities) || 0,
3944
- indexedTokens: Number(data.indexedTokens) || 0
3945
- };
3946
- } catch (err) {
3947
- return { ok: false, status: 0, message: err instanceof Error ? err.message : "request failed" };
3948
- } finally {
3949
- clearTimeout(timer);
3591
+ function loadVulnerabilities(root) {
3592
+ return readScanArtifact(root)?.extended?.vulnerabilities ?? null;
3593
+ }
3594
+ function resolvePackageTarget(root, pkg) {
3595
+ const artifact = readScanArtifact(root);
3596
+ for (const project of artifact?.projects ?? []) {
3597
+ const dep = project.dependencies?.find((d) => d.package === pkg);
3598
+ if (dep) {
3599
+ return {
3600
+ ecosystem: projectTypeToVulnEcosystem(project.type) ?? "unknown",
3601
+ currentVersion: dep.resolvedVersion,
3602
+ latestVersion: dep.latestStable,
3603
+ majorsBehind: dep.majorsBehind
3604
+ };
3605
+ }
3606
+ }
3607
+ const rec = inventory(root).records.find((r) => r.name === pkg);
3608
+ if (rec) {
3609
+ const ecosystem = rec.ecosystem === "npm" ? "npm" : rec.ecosystem === "pypi" ? "pypi" : "unknown";
3610
+ return { ecosystem, currentVersion: rec.installed ?? null, latestVersion: rec.latest ?? null, majorsBehind: null };
3950
3611
  }
3612
+ return { ecosystem: "unknown", currentVersion: null, latestVersion: null, majorsBehind: null };
3613
+ }
3614
+ function openFixableAdvisories(root, pkg) {
3615
+ const vulns = loadVulnerabilities(root);
3616
+ const match = vulns?.packages.find((p) => p.package === pkg);
3617
+ return (match?.advisories ?? []).filter((a) => a.fixedVersions.length).map((a) => a.id);
3618
+ }
3619
+ function filterBySeverity(result, minSeverity) {
3620
+ if (!minSeverity || minSeverity === "unknown") return result;
3621
+ const floor = severityRank(minSeverity);
3622
+ const packages = result.packages.map((p) => ({ ...p, advisories: p.advisories.filter((a) => severityRank(a.severity) >= floor) })).filter((p) => p.advisories.length > 0);
3623
+ const totalAdvisories = packages.reduce((n, p) => n + p.advisories.length, 0);
3624
+ return { ...result, packages, totalAdvisories };
3951
3625
  }
3952
3626
 
3953
- // src/engine/hosted-cache.ts
3954
- var CACHE_VERSION = "vg-hosted-docs/1";
3955
- var HOSTED_DOCS_TTL_MS = 24 * 60 * 60 * 1e3;
3956
- var MAX_ENTRIES = 64;
3957
- function cachePath(root) {
3958
- return path2.join(cacheDir(root), "hosted-docs.json");
3627
+ // src/mcp/attribution.ts
3628
+ var MCP_MAX_COMMITS = 300;
3629
+ var ECOSYSTEM_MAP = {
3630
+ npm: "npm",
3631
+ pypi: "pypi",
3632
+ go: "go",
3633
+ rust: "cargo",
3634
+ ruby: "rubygems",
3635
+ php: "composer",
3636
+ dotnet: "nuget",
3637
+ dart: "pub"
3638
+ // swift (no advisory ecosystem) and java (no resolved lockfile) have no timeline.
3639
+ };
3640
+ function brief(commit) {
3641
+ return { sha: commit.shortSha, author: commit.authorName, date: commit.date, subject: commit.subject };
3959
3642
  }
3960
- function hostedDocsCacheKey(req, opts = {}) {
3961
- const material = JSON.stringify([
3962
- req.name ?? "",
3963
- req.targetId ?? "",
3964
- req.query ?? "",
3965
- req.verbosity ?? "",
3966
- req.maxTokens ?? 0,
3967
- hostedBase(opts),
3968
- opts.auth?.keyId ?? "anon"
3969
- ]);
3970
- return createHash("sha256").update(material).digest("hex");
3643
+ async function attributedInventory(root, opts = {}) {
3644
+ const inv = inventory(root);
3645
+ if (!opts.attribute) return { ...inv, attribution: "off" };
3646
+ if (!await gitHistoryAvailable(root)) return { ...inv, attribution: "unavailable" };
3647
+ const timelines = await buildVersionTimelines(root, { maxCommits: MCP_MAX_COMMITS });
3648
+ if (!timelines) return { ...inv, attribution: "unavailable" };
3649
+ const records = inv.records.map((r) => {
3650
+ const ecosystem = ECOSYSTEM_MAP[r.ecosystem];
3651
+ if (!ecosystem) return r;
3652
+ const pt = findPackageTimeline(timelines, ecosystem, r.name);
3653
+ if (!pt || !pt.changes.length) return r;
3654
+ return {
3655
+ ...r,
3656
+ addedBy: brief(pt.changes[0].commit),
3657
+ currentVersionBy: brief(pt.changes[pt.changes.length - 1].commit)
3658
+ };
3659
+ });
3660
+ return { ...inv, records, attribution: "git" };
3971
3661
  }
3972
- function loadFile(root) {
3973
- try {
3974
- const parsed = JSON.parse(fs18.readFileSync(cachePath(root), "utf8"));
3975
- if (parsed && parsed.version === CACHE_VERSION && parsed.entries && typeof parsed.entries === "object") return parsed;
3976
- } catch {
3977
- }
3978
- return { version: CACHE_VERSION, entries: {} };
3662
+ function countTokens(text) {
3663
+ return encode(text).length;
3979
3664
  }
3980
- function saveFile(root, file) {
3981
- try {
3982
- const keys = Object.keys(file.entries);
3983
- if (keys.length > MAX_ENTRIES) {
3984
- for (const k of keys.sort((a, b) => file.entries[a].at - file.entries[b].at).slice(0, keys.length - MAX_ENTRIES)) {
3985
- delete file.entries[k];
3986
- }
3987
- }
3988
- fs18.mkdirSync(cacheDir(root), { recursive: true });
3989
- fs18.writeFileSync(cachePath(root), JSON.stringify(file));
3990
- } catch {
3991
- }
3665
+ function truncateToTokens(text, budget, marker = "\n\u2026(truncated)") {
3666
+ if (!Number.isFinite(budget) || budget <= 0) return { text, truncated: false, tokens: countTokens(text) };
3667
+ const tokens = encode(text);
3668
+ if (tokens.length <= budget) return { text, truncated: false, tokens: tokens.length };
3669
+ const markerTokens = encode(marker).length;
3670
+ const keep = Math.max(0, budget - markerTokens);
3671
+ let slice = decode(tokens.slice(0, keep));
3672
+ const lastNl = slice.lastIndexOf("\n");
3673
+ if (lastNl > 0) slice = slice.slice(0, lastNl);
3674
+ const out = slice + marker;
3675
+ return { text: out, truncated: true, tokens: countTokens(out) };
3992
3676
  }
3993
- async function fetchHostedDocsCached(root, req, opts = {}) {
3994
- const now = opts.now ?? Date.now;
3995
- const ttl = opts.ttlMs ?? HOSTED_DOCS_TTL_MS;
3996
- const key = hostedDocsCacheKey(req, opts);
3997
- const file = loadFile(root);
3998
- if (!opts.bypass) {
3999
- const hit = file.entries[key];
4000
- if (hit && now() - hit.at < ttl && hit.result?.content) {
4001
- return { ...hit.result, metadata: { ...hit.result.metadata ?? {}, cached: true } };
3677
+
3678
+ // src/engine/selection-weights.ts
3679
+ var DEFAULT_SELECTION_WEIGHTS = {
3680
+ headingUsage: 4,
3681
+ headingPreamble: -6,
3682
+ hasCode: 3,
3683
+ codeDensity: 0.5,
3684
+ queryOverlap: 5,
3685
+ symbolMatch: 2,
3686
+ position: -0.1,
3687
+ linkDensity: -3
3688
+ };
3689
+
3690
+ // src/engine/select.ts
3691
+ var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "to", "of", "in", "for", "and", "or", "is", "how", "do", "i", "use", "using", "with", "on", "my"]);
3692
+ var USAGE_RE = /\b(usage|example|examples|quick ?start|getting started|api|guide|how to|reference|tutorial|basic)\b/i;
3693
+ var PREAMBLE_RE = /\b(install|installation|license|licence|contributing|contributors?|sponsors?|table of contents|toc|changelog|funding|code of conduct|security|acknowledge)\b/i;
3694
+ function splitSections(md) {
3695
+ const out = [];
3696
+ let cur = { heading: "", level: 0};
3697
+ let buf = [];
3698
+ const flush = () => {
3699
+ const body = buf.join("\n").trim();
3700
+ if (cur.heading || body) out.push({ heading: cur.heading, level: cur.level, body });
3701
+ buf = [];
3702
+ };
3703
+ for (const line of md.split("\n")) {
3704
+ const h = /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
3705
+ if (h) {
3706
+ flush();
3707
+ cur = { heading: h[2].trim(), level: h[1].length, body: "" };
3708
+ } else {
3709
+ buf.push(line);
4002
3710
  }
4003
3711
  }
4004
- const result = await fetchHostedDocs(req, opts);
4005
- if (result) {
4006
- file.entries[key] = { at: now(), result };
4007
- saveFile(root, file);
4008
- }
4009
- return result;
4010
- }
4011
-
4012
- // src/mcp/response.ts
4013
- var MAX_RESULT_TOKENS = 25e3;
4014
- function renderToolResult(result, maxTokens = MAX_RESULT_TOKENS) {
4015
- return toolResult(clampToBudget(compactResult(result), maxTokens));
3712
+ flush();
3713
+ return out;
4016
3714
  }
4017
- function toolResult(result) {
4018
- return {
4019
- content: [{ type: "text", text: JSON.stringify(result) }],
4020
- structuredContent: asStructured(result)
3715
+ function chunkBody(body) {
3716
+ const chunks = [];
3717
+ let cur = [];
3718
+ let inFence = false;
3719
+ const flush = () => {
3720
+ const c = cur.join("\n").trim();
3721
+ if (c) chunks.push(c);
3722
+ cur = [];
4021
3723
  };
4022
- }
4023
- function compactResult(value) {
4024
- return prune(value);
4025
- }
4026
- function prune(v) {
4027
- if (Array.isArray(v)) return v.map(prune);
4028
- if (v && typeof v === "object") {
4029
- const out = {};
4030
- for (const [k, raw] of Object.entries(v)) {
4031
- const pv = prune(raw);
4032
- if (!isEmpty(pv)) out[k] = pv;
3724
+ for (const line of body.split("\n")) {
3725
+ if (/^\s*```/.test(line)) inFence = !inFence;
3726
+ if (!inFence && line.trim() === "") {
3727
+ flush();
3728
+ continue;
4033
3729
  }
4034
- return out;
3730
+ cur.push(line);
4035
3731
  }
4036
- return v;
3732
+ flush();
3733
+ return chunks;
4037
3734
  }
4038
- function isEmpty(v) {
4039
- if (v === null || v === void 0 || v === "") return true;
4040
- if (Array.isArray(v)) return v.length === 0;
4041
- if (typeof v === "object") return Object.keys(v).length === 0;
4042
- return false;
3735
+ function stripNoise(body) {
3736
+ return body.split("\n").filter((l) => !/^\s*(?:!\[[^\]]*\]\([^)]*\)\s*)+$/.test(l)).join("\n").trim();
4043
3737
  }
4044
- function clampToBudget(value, maxTokens = MAX_RESULT_TOKENS) {
4045
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
4046
- if (tokensOf(value) <= maxTokens) return value;
4047
- const obj2 = { ...value };
4048
- const truncated = {};
4049
- for (let guard = 0; guard < 64 && tokensOf(obj2) > maxTokens; guard++) {
4050
- const arrays = Object.entries(obj2).filter(
4051
- (e) => Array.isArray(e[1]) && e[1].length > 0
4052
- );
4053
- if (arrays.length === 0) break;
4054
- arrays.sort((a, b) => tokensOf(b[1]) - tokensOf(a[1]));
4055
- const [field, arr] = arrays[0];
4056
- const total = truncated[field]?.total ?? arr.length;
4057
- const shown = Math.max(1, Math.floor(arr.length / 2));
4058
- obj2[field] = arr.slice(0, shown);
4059
- truncated[field] = { shown, total };
4060
- }
4061
- if (Object.keys(truncated).length) obj2._truncated = truncated;
4062
- return obj2;
3738
+ function tokenizeQuery(q) {
3739
+ if (!q) return [];
3740
+ return [...new Set(q.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1 && !STOPWORDS.has(w)))];
4063
3741
  }
4064
- function tokensOf(v) {
4065
- return countTokens(JSON.stringify(v));
3742
+ function symbolsFromApi(api) {
3743
+ if (!api) return [];
3744
+ const out = /* @__PURE__ */ new Set();
3745
+ const re = /\b(?:function|class|interface|type|const|enum|namespace)\s+([A-Za-z_$][\w$]*)/g;
3746
+ let m;
3747
+ while (m = re.exec(api)) out.add(m[1]);
3748
+ return [...out];
4066
3749
  }
4067
- function asStructured(result) {
4068
- if (result && typeof result === "object" && !Array.isArray(result)) {
4069
- return result;
4070
- }
4071
- return { result };
3750
+ function scoreSection(s, idx, queryTerms, symbols, w) {
3751
+ const text = `${s.heading}
3752
+ ${s.body}`.toLowerCase();
3753
+ let score = 0;
3754
+ if (USAGE_RE.test(s.heading)) score += w.headingUsage;
3755
+ if (PREAMBLE_RE.test(s.heading)) score += w.headingPreamble;
3756
+ const codeBlocks = Math.floor((s.body.match(/```/g) ?? []).length / 2);
3757
+ if (codeBlocks > 0) score += w.hasCode + Math.min(codeBlocks, 5) * w.codeDensity;
3758
+ for (const t of queryTerms) if (text.includes(t)) score += w.queryOverlap;
3759
+ for (const sym of symbols) if (sym.length > 2 && text.includes(sym.toLowerCase())) score += w.symbolMatch;
3760
+ score += idx * w.position;
3761
+ const links = (s.body.match(/\]\(|https?:\/\//g) ?? []).length;
3762
+ const lineCount = Math.max(1, s.body.split("\n").filter((l) => l.trim()).length);
3763
+ if (links / lineCount > 0.5) score += w.linkDensity;
3764
+ return score;
4072
3765
  }
4073
- function boundList(items, max) {
4074
- if (!Number.isFinite(max) || max < 0 || items.length <= max) {
4075
- return { items, total: items.length };
3766
+ var apiBlock = (api) => api ? `## API (types)
3767
+
3768
+ \`\`\`ts
3769
+ ${api}
3770
+ \`\`\`` : "";
3771
+ function selectForBudget(input) {
3772
+ const w = input.weights ?? DEFAULT_SELECTION_WEIGHTS;
3773
+ const api = apiBlock(input.apiSurface);
3774
+ const readme = input.readme.trim();
3775
+ if (!input.budget || !Number.isFinite(input.budget)) {
3776
+ const full2 = [readme, api].filter(Boolean).join("\n\n");
3777
+ return { text: full2, tokens: countTokens(full2), truncated: false };
4076
3778
  }
4077
- return { items: items.slice(0, max), total: items.length };
4078
- }
4079
- var NODE_EDGE_CAP = 50;
4080
- var MAX_FILE_BYTES = 1e6;
4081
- var PREVIEW_CHARS = 120;
4082
- var NUL = "\0";
4083
- var PARALLEL_MIN_BYTES = 64e6;
4084
- var PARALLEL_MIN_FILES = 2e3;
4085
- var ROW_CAP = 1e4;
4086
- async function scanCandidates(root, files, needle, opts) {
4087
- const needleLower = needle.toLowerCase();
4088
- if (!needleLower || files.length === 0) return { hits: [], total: 0, truncated: false };
4089
- if (files.length < PARALLEL_MIN_FILES && !process.env.VG_PARALLEL_MIN_BYTES) {
4090
- return opts.collectAll ? scanInline(root, files, needleLower, ROW_CAP, false, void 0) : scanInline(root, files, needleLower, opts.budget, true, void 0);
3779
+ const budget = input.budget;
3780
+ const queryTerms = tokenizeQuery(input.query);
3781
+ const symbols = symbolsFromApi(input.apiSurface);
3782
+ const render = (s) => {
3783
+ const body = stripNoise(s.body);
3784
+ if (!body) return "";
3785
+ return `${s.heading ? `## ${s.heading}
3786
+ ` : ""}${body}`.trim();
3787
+ };
3788
+ const sections = splitSections(readme).filter((s) => !PREAMBLE_RE.test(s.heading));
3789
+ const full = [api, ...sections.map(render)].filter(Boolean).join("\n\n").trim();
3790
+ if (countTokens(full) <= budget) {
3791
+ return { text: full, tokens: countTokens(full), truncated: false };
4091
3792
  }
4092
- const { over, sizes } = measure(root, files);
4093
- if (!over) {
4094
- return opts.collectAll ? scanInline(root, files, needleLower, ROW_CAP, false, sizes) : scanInline(root, files, needleLower, opts.budget, true, sizes);
3793
+ const units = [];
3794
+ for (const s of sections) {
3795
+ if (countTokens(s.body) > budget) {
3796
+ chunkBody(s.body).forEach((p, i) => units.push({ heading: i === 0 ? s.heading : "", level: s.level, body: p }));
3797
+ } else {
3798
+ units.push(s);
3799
+ }
4095
3800
  }
4096
- const out = await scanParallel(root, files, needleLower);
4097
- if (!opts.collectAll && out.hits.length > opts.budget) {
4098
- out.hits.length = opts.budget;
4099
- out.truncated = true;
3801
+ const ranked = units.map((s, idx) => ({ s, idx, score: scoreSection(s, idx, queryTerms, symbols, w) })).sort((a, b) => b.score - a.score || a.idx - b.idx);
3802
+ const out = [];
3803
+ let used = 0;
3804
+ const add3 = (chunk2) => {
3805
+ const t = countTokens(chunk2);
3806
+ if (used + t <= budget) {
3807
+ out.push(chunk2);
3808
+ used += t;
3809
+ return true;
3810
+ }
3811
+ return false;
3812
+ };
3813
+ if (api && !add3(api) && used < budget) {
3814
+ out.push(truncateToTokens(api, budget - used).text);
3815
+ used = budget;
4100
3816
  }
4101
- return out;
4102
- }
4103
- function parallelMinBytes() {
4104
- const v = Number(process.env.VG_PARALLEL_MIN_BYTES);
4105
- return Number.isFinite(v) && v >= 0 ? v : PARALLEL_MIN_BYTES;
4106
- }
4107
- function measure(root, files) {
4108
- const threshold = parallelMinBytes();
4109
- const sizes = /* @__PURE__ */ new Map();
4110
- let total = 0;
4111
- for (const rel2 of files) {
4112
- let size;
4113
- try {
4114
- size = fs18.statSync(path2.join(root, rel2)).size;
4115
- } catch {
4116
- continue;
3817
+ const picked = [];
3818
+ for (const { s, idx } of ranked) {
3819
+ const chunk2 = render(s);
3820
+ if (!chunk2) continue;
3821
+ if (used + countTokens(`
3822
+ ${chunk2}`) <= budget) {
3823
+ picked.push(idx);
3824
+ used += countTokens(`
3825
+ ${chunk2}`);
4117
3826
  }
4118
- sizes.set(rel2, size);
4119
- if (size <= MAX_FILE_BYTES) total += size;
4120
- if (total >= threshold) return { over: true, sizes };
4121
3827
  }
4122
- return { over: false, sizes };
3828
+ picked.sort((a, b) => a - b);
3829
+ for (const idx of picked) out.push(render(units[idx]));
3830
+ if (used < budget - 20) {
3831
+ const next = ranked.find((r) => !picked.includes(r.idx) && render(r.s));
3832
+ if (next) {
3833
+ const head = truncateToTokens(render(next.s), budget - used).text;
3834
+ if (head.trim()) out.push(head);
3835
+ }
3836
+ }
3837
+ const text = out.join("\n\n").trim();
3838
+ return { text, tokens: countTokens(text), truncated: true };
4123
3839
  }
4124
- function matchLines(text, needleLower) {
4125
- const low = text.toLowerCase();
4126
- if (!low.includes(needleLower)) return [];
4127
- const lowLines = low.split("\n");
4128
- const rawLines = text.split("\n");
4129
- const hits = [];
4130
- for (let i = 0; i < lowLines.length; i++) {
4131
- if (lowLines[i].includes(needleLower)) hits.push({ line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW_CHARS) });
3840
+
3841
+ // src/engine/quality.ts
3842
+ function assessDocQuality(content, opts = {}) {
3843
+ const minTokens = opts.minTokens ?? 60;
3844
+ const lc = content.toLowerCase();
3845
+ const reasons = [];
3846
+ let score = 0;
3847
+ const hasCode = /```/.test(content) || /(^|\n)( {4}|\t)\S/.test(content);
3848
+ if (hasCode) score += 2;
3849
+ else reasons.push("no code example");
3850
+ const tokens = countTokens(content);
3851
+ if (tokens >= minTokens) score += 1;
3852
+ else reasons.push(`thin (${tokens} tokens)`);
3853
+ const bareName = opts.name?.toLowerCase().replace(/^@[^/]+\//, "");
3854
+ if (bareName && bareName.length > 1) {
3855
+ if (lc.includes(bareName)) score += 1;
3856
+ else reasons.push("library name absent");
3857
+ }
3858
+ const qt = tokenizeQuery(opts.query);
3859
+ let onTopic = qt.length === 0;
3860
+ if (qt.length) {
3861
+ const hits = qt.filter((t) => lc.includes(t)).length;
3862
+ if (hits) {
3863
+ score += Math.min(hits, 2);
3864
+ onTopic = true;
3865
+ } else {
3866
+ reasons.push("query terms absent");
3867
+ }
4132
3868
  }
4133
- return hits;
3869
+ if (opts.symbols?.length) {
3870
+ if (opts.symbols.some((s) => s.length > 2 && lc.includes(s.toLowerCase()))) score += 1;
3871
+ else reasons.push("no API symbols present");
3872
+ }
3873
+ const sufficient = hasCode && tokens >= minTokens && onTopic;
3874
+ return { score, sufficient, reasons };
4134
3875
  }
4135
- function readCandidate(abs, knownSize) {
3876
+
3877
+ // src/engine/hosted.ts
3878
+ function hostedBase(opts = {}) {
3879
+ const explicit = opts.base || process.env.VIBGRATE_LIB_HOST;
3880
+ if (explicit) return explicit.replace(/\/+$/, "");
3881
+ let host;
4136
3882
  try {
4137
- const size = knownSize ?? fs18.statSync(abs).size;
4138
- if (size > MAX_FILE_BYTES) return null;
4139
- const text = fs18.readFileSync(abs, "utf8");
4140
- return text.includes(NUL) ? null : text;
3883
+ host = resolveIngestHost(opts.region, opts.ingest);
4141
3884
  } catch {
4142
- return null;
3885
+ host = "us.ingest.vibgrate.com";
4143
3886
  }
3887
+ return `https://${host}`;
4144
3888
  }
4145
- function scanInline(root, files, needleLower, cap, stopAtCap, sizes) {
4146
- const hits = [];
4147
- let total = 0;
4148
- let truncated = false;
4149
- for (const rel2 of files) {
4150
- const text = readCandidate(path2.join(root, rel2), sizes?.get(rel2));
4151
- if (text === null) continue;
4152
- for (const h of matchLines(text, needleLower)) {
4153
- total++;
4154
- if (hits.length < cap) hits.push({ file: rel2, line: h.line, preview: h.preview });
4155
- else {
4156
- truncated = true;
4157
- if (stopAtCap) return { hits, total, truncated };
4158
- }
4159
- }
3889
+ async function fetchHostedDocs(req, opts = {}) {
3890
+ const f = opts.fetchImpl ?? globalThis.fetch;
3891
+ if (!f) return null;
3892
+ const url = `${hostedBase(opts)}/v1/lib/docs`;
3893
+ const ctrl = new AbortController();
3894
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 4e3);
3895
+ const headers = { "content-type": "application/json" };
3896
+ if (opts.auth) {
3897
+ headers["Authorization"] = `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`;
3898
+ headers["X-Vibgrate-Timestamp"] = String(Date.now());
4160
3899
  }
4161
- return { hits, total, truncated };
4162
- }
4163
- async function scanParallel(root, files, needleLower) {
4164
- const n = Math.min(files.length, Math.max(2, Math.min(8, os.cpus().length - 1)));
4165
- const shards = Array.from({ length: n }, () => []);
4166
- files.forEach((f, i) => shards[i % n].push(f));
4167
- let results;
4168
3900
  try {
4169
- results = await Promise.all(shards.map((shard) => runWorker(root, shard, needleLower)));
3901
+ const res = await f(url, {
3902
+ method: "POST",
3903
+ headers,
3904
+ body: JSON.stringify({
3905
+ name: req.name,
3906
+ targetId: req.targetId,
3907
+ query: req.query,
3908
+ verbosity: req.verbosity,
3909
+ max_tokens: req.maxTokens
3910
+ }),
3911
+ signal: ctrl.signal
3912
+ });
3913
+ if (!res.ok) return null;
3914
+ const data = await res.json();
3915
+ const content = typeof data.content === "string" ? data.content : typeof data.docs === "string" ? data.docs : "";
3916
+ if (!content.trim()) return null;
3917
+ return {
3918
+ content,
3919
+ version: typeof data.version === "string" ? data.version : null,
3920
+ source: "hosted",
3921
+ metadata: data.metadata ?? void 0
3922
+ };
4170
3923
  } catch {
4171
- return scanInline(root, files, needleLower, ROW_CAP, false, void 0);
4172
- }
4173
- const hits = [];
4174
- let total = 0;
4175
- let truncated = false;
4176
- for (const r of results) {
4177
- total += r.total;
4178
- truncated = truncated || r.truncated;
4179
- for (const h of r.hits) hits.push(h);
4180
- }
4181
- hits.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line);
4182
- if (hits.length > ROW_CAP) {
4183
- hits.length = ROW_CAP;
4184
- truncated = true;
3924
+ return null;
3925
+ } finally {
3926
+ clearTimeout(timer);
4185
3927
  }
4186
- return { hits, total, truncated };
4187
- }
4188
- var WORKER_SOURCE = [
4189
- "const { parentPort, workerData } = require('worker_threads');",
4190
- "const fs = require('fs');",
4191
- "const path = require('path');",
4192
- `const MAX = ${MAX_FILE_BYTES};`,
4193
- `const PREVIEW = ${PREVIEW_CHARS};`,
4194
- `const CAP = ${ROW_CAP};`,
4195
- "const NUL = String.fromCharCode(0);",
4196
- "const NL = String.fromCharCode(10);",
4197
- "const { root, files, needleLower } = workerData;",
4198
- "const hits = []; let total = 0, truncated = false;",
4199
- "for (const rel of files) {",
4200
- " const abs = path.join(root, rel);",
4201
- " let text;",
4202
- ' try { if (fs.statSync(abs).size > MAX) continue; text = fs.readFileSync(abs, "utf8"); } catch { continue; }',
4203
- " if (text.includes(NUL)) continue;",
4204
- " const low = text.toLowerCase();",
4205
- " if (!low.includes(needleLower)) continue;",
4206
- " const lowLines = low.split(NL); const rawLines = text.split(NL);",
4207
- " for (let i = 0; i < lowLines.length; i++) {",
4208
- " if (!lowLines[i].includes(needleLower)) continue;",
4209
- " total++;",
4210
- " if (hits.length < CAP) hits.push({ file: rel, line: i + 1, preview: rawLines[i].trim().slice(0, PREVIEW) });",
4211
- " else truncated = true;",
4212
- " }",
4213
- "}",
4214
- "parentPort.postMessage({ hits, total, truncated });"
4215
- ].join("\n");
4216
- function runWorker(root, files, needleLower) {
4217
- return new Promise((resolve11, reject) => {
4218
- const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { root, files, needleLower } });
4219
- let settled = false;
4220
- w.once("message", (m) => {
4221
- settled = true;
4222
- resolve11(m);
4223
- });
4224
- w.once("error", reject);
4225
- w.once("exit", (code) => {
4226
- if (!settled && code !== 0) reject(new Error(`literal-scan worker exited with code ${code}`));
4227
- });
4228
- });
4229
- }
4230
-
4231
- // src/engine/search.ts
4232
- var IGNORE_DIRS = /* @__PURE__ */ new Set([
4233
- ".git",
4234
- ".vibgrate",
4235
- "node_modules",
4236
- "dist",
4237
- "build",
4238
- "out",
4239
- "target",
4240
- "vendor",
4241
- "__pycache__",
4242
- // .NET intermediate output and coverage reports: purely generated trees that a
4243
- // C#/coverage-heavy repo pays for on every literal sweep. `bin` is deliberately
4244
- // NOT here — plenty of repos keep real scripts in bin/ (rails, node CLIs), and
4245
- // completeness beats speed; the binary-extension skip below keeps a .NET bin/
4246
- // cheap anyway.
4247
- "obj",
4248
- "coverage"
4249
- ]);
4250
- var MAX_FILES_SCANNED = 2e4;
4251
- var BINARY_EXTS = /* @__PURE__ */ new Set([
4252
- ".dll",
4253
- ".pdb",
4254
- ".exe",
4255
- ".so",
4256
- ".dylib",
4257
- ".a",
4258
- ".lib",
4259
- ".o",
4260
- ".class",
4261
- ".jar",
4262
- ".wasm",
4263
- ".node",
4264
- ".png",
4265
- ".jpg",
4266
- ".jpeg",
4267
- ".gif",
4268
- ".webp",
4269
- ".ico",
4270
- ".icns",
4271
- ".bmp",
4272
- ".zip",
4273
- ".gz",
4274
- ".tgz",
4275
- ".bz2",
4276
- ".xz",
4277
- ".7z",
4278
- ".rar",
4279
- ".woff",
4280
- ".woff2",
4281
- ".ttf",
4282
- ".otf",
4283
- ".eot",
4284
- ".mp3",
4285
- ".mp4",
4286
- ".mov",
4287
- ".avi",
4288
- ".pdf"
4289
- ]);
4290
- function hasBinaryExt(rel2) {
4291
- return BINARY_EXTS.has(path2.extname(rel2).toLowerCase());
4292
3928
  }
4293
- function toPosix2(rel2) {
4294
- return path2.sep === "\\" ? rel2.replaceAll("\\", "/") : rel2;
4295
- }
4296
- async function searchSymbols(graph, root, query, limit) {
4297
- let q = query.trim();
4298
- if (!q) return { matches: [], moreAvailable: false, hint: "query is required" };
4299
- const quoted = /^(["'`])(.+)\1$/.exec(q);
4300
- if (quoted && quoted[2].trim()) q = quoted[2].trim();
4301
- const isPhrase = Boolean(quoted) || /\s/.test(q) || /[/\\]/.test(q) && !q.includes(":") && !/[*?]/.test(q);
4302
- let nodes = findNodes(graph, q).filter((n) => n.kind !== "file");
4303
- if (nodes.length === 0) {
4304
- nodes = fileSymbolNodes(graph, q);
4305
- }
4306
- if (nodes.length === 0 && isPhrase) {
4307
- nodes = reconstructedIdentifierNodes(graph, q);
4308
- }
4309
- if (nodes.length === 0) {
4310
- const tokens = queryTokens(q);
4311
- if (tokens.length >= 2) nodes = multiTokenNodes(graph, tokens);
4312
- }
4313
- const symbolCap = isPhrase ? Math.max(1, Math.floor(limit / 3)) : limit;
4314
- const symbolHits = nodes.slice(0, symbolCap).map((n, i) => ({
4315
- kind: n.kind,
4316
- name: n.qualifiedName,
4317
- file: n.file,
4318
- line: n.span.start,
4319
- score: Math.round((1 - i / Math.max(nodes.length, 1)) * 100) / 100
4320
- }));
4321
- const spare = limit - symbolHits.length;
4322
- const textHits = [];
4323
- let truncatedScan = false;
4324
- let totalTextMatches;
4325
- const skipScan = !isPhrase && hasExactSymbolMatch(nodes, q);
4326
- if (spare > 0 && !skipScan) {
4327
- const seen = new Set(symbolHits.map((h) => `${h.file}:${h.line}`));
4328
- const scan = await scanFiles(root, q, spare, seen, textHits, isPhrase);
4329
- truncatedScan = scan.truncated;
4330
- if (isPhrase) totalTextMatches = scan.total;
4331
- }
4332
- const matches = [...symbolHits, ...textHits];
4333
- if (matches.length === 0) {
3929
+ async function publishPrivateLibrary(req, opts = {}) {
3930
+ const f = opts.fetchImpl ?? globalThis.fetch;
3931
+ if (!f) return { ok: false, status: 0, message: "no network available" };
3932
+ if (!opts.auth) return { ok: false, status: 401, message: "a DSN is required to publish a private library (run `vibgrate login` or set VIBGRATE_DSN)" };
3933
+ const url = `${hostedBase(opts)}/v1/lib/private`;
3934
+ const ctrl = new AbortController();
3935
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
3936
+ try {
3937
+ const res = await f(url, {
3938
+ method: "POST",
3939
+ headers: {
3940
+ "content-type": "application/json",
3941
+ Authorization: `VibgrateDSN ${opts.auth.keyId}:${opts.auth.secret}`,
3942
+ "X-Vibgrate-Timestamp": String(Date.now())
3943
+ },
3944
+ body: JSON.stringify({
3945
+ name: req.name,
3946
+ version: req.version,
3947
+ readme: req.readme,
3948
+ dts: req.dts,
3949
+ compliance: req.compliance,
3950
+ language: req.language,
3951
+ sourceUrl: req.sourceUrl
3952
+ }),
3953
+ signal: ctrl.signal
3954
+ });
3955
+ const data = await res.json().catch(() => ({}));
3956
+ if (!res.ok) return { ok: false, status: res.status, error: typeof data.error === "string" ? data.error : void 0, message: typeof data.message === "string" ? data.message : void 0 };
4334
3957
  return {
4335
- matches,
4336
- moreAvailable: false,
4337
- hint: "no symbol or text match \u2014 for meaning-level questions (symptoms, relationships, what-breaks-if) use query_graph"
3958
+ ok: true,
3959
+ targetId: String(data.targetId ?? ""),
3960
+ ecosystem: String(data.ecosystem ?? ""),
3961
+ entities: Number(data.entities) || 0,
3962
+ indexedTokens: Number(data.indexedTokens) || 0
4338
3963
  };
3964
+ } catch (err) {
3965
+ return { ok: false, status: 0, message: err instanceof Error ? err.message : "request failed" };
3966
+ } finally {
3967
+ clearTimeout(timer);
4339
3968
  }
4340
- const literalTruncated = totalTextMatches !== void 0 && totalTextMatches > textHits.length;
4341
- const moreAvailable = isPhrase ? literalTruncated || truncatedScan : nodes.length > symbolHits.length || truncatedScan;
4342
- const result = { matches, moreAvailable };
4343
- if (totalTextMatches !== void 0) result.totalTextMatches = totalTextMatches;
4344
- if (literalTruncated) {
4345
- result.hint = `showing ${textHits.length} of ${totalTextMatches}${truncatedScan ? "+" : ""} literal matches \u2014 raise limit or narrow the query to get them all`;
4346
- }
4347
- return result;
4348
- }
4349
- function fileSymbolNodes(graph, q) {
4350
- if (!/[/\\]/.test(q) && !/\.\w+$/.test(q)) return [];
4351
- const norm = q.replaceAll("\\", "/").replace(/^\.\//, "");
4352
- const inFile = graph.nodes.filter(
4353
- (n) => n.kind !== "file" && (n.file === norm || n.file.endsWith(`/${norm}`))
4354
- );
4355
- return [...inFile].sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName));
4356
3969
  }
4357
- function hasExactSymbolMatch(nodes, q) {
4358
- if (nodes.length === 0) return false;
4359
- if (/^(.*):(\d+)$/.test(q)) return true;
4360
- const lower = q.toLowerCase();
4361
- return nodes.some((n) => n.id === q || n.qualifiedName.toLowerCase() === lower || n.name.toLowerCase() === lower);
3970
+
3971
+ // src/engine/hosted-cache.ts
3972
+ var CACHE_VERSION = "vg-hosted-docs/1";
3973
+ var HOSTED_DOCS_TTL_MS = 24 * 60 * 60 * 1e3;
3974
+ var MAX_ENTRIES = 64;
3975
+ function cachePath(root) {
3976
+ return path18.join(cacheDir(root), "hosted-docs.json");
4362
3977
  }
4363
- function queryTokens(q) {
4364
- return [...new Set(q.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
3978
+ function hostedDocsCacheKey(req, opts = {}) {
3979
+ const material = JSON.stringify([
3980
+ req.name ?? "",
3981
+ req.targetId ?? "",
3982
+ req.query ?? "",
3983
+ req.verbosity ?? "",
3984
+ req.maxTokens ?? 0,
3985
+ hostedBase(opts),
3986
+ opts.auth?.keyId ?? "anon"
3987
+ ]);
3988
+ return createHash("sha256").update(material).digest("hex");
4365
3989
  }
4366
- function reconstructedIdentifierNodes(graph, q) {
4367
- const words = q.trim().split(/\s+/).filter(Boolean);
4368
- if (words.length < 2) return [];
4369
- const concat = findNodes(graph, words.join(""));
4370
- if (concat.length) return concat.filter((n) => n.kind !== "file");
4371
- const snake = findNodes(graph, words.join("_"));
4372
- return snake.filter((n) => n.kind !== "file");
3990
+ function loadFile(root) {
3991
+ try {
3992
+ const parsed = JSON.parse(fs16.readFileSync(cachePath(root), "utf8"));
3993
+ if (parsed && parsed.version === CACHE_VERSION && parsed.entries && typeof parsed.entries === "object") return parsed;
3994
+ } catch {
3995
+ }
3996
+ return { version: CACHE_VERSION, entries: {} };
4373
3997
  }
4374
- function multiTokenNodes(graph, tokens) {
4375
- const scored = [];
4376
- for (const n of graph.nodes) {
4377
- if (n.kind === "file") continue;
4378
- const name = n.name.toLowerCase();
4379
- const qn = n.qualifiedName.toLowerCase();
4380
- let hits = 0;
4381
- let exact = false;
4382
- for (const t of tokens) {
4383
- if (name === t || qn === t) {
4384
- hits++;
4385
- exact = true;
4386
- } else if (qn.includes(t)) hits++;
3998
+ function saveFile(root, file) {
3999
+ try {
4000
+ const keys = Object.keys(file.entries);
4001
+ if (keys.length > MAX_ENTRIES) {
4002
+ for (const k of keys.sort((a, b) => file.entries[a].at - file.entries[b].at).slice(0, keys.length - MAX_ENTRIES)) {
4003
+ delete file.entries[k];
4004
+ }
4387
4005
  }
4388
- if (exact || hits >= 2) scored.push({ node: n, hits, exact });
4006
+ fs16.mkdirSync(cacheDir(root), { recursive: true });
4007
+ fs16.writeFileSync(cachePath(root), JSON.stringify(file));
4008
+ } catch {
4389
4009
  }
4390
- return scored.sort((a, b) => Number(b.exact) - Number(a.exact) || b.hits - a.hits || b.node.importance - a.node.importance || a.node.qualifiedName.localeCompare(b.node.qualifiedName)).map((e) => e.node);
4391
4010
  }
4392
- async function scanFiles(root, needle, budget, seen, out, countAll) {
4393
- const listing = listCandidateFiles(root, needle);
4394
- const outcome = await scanCandidates(root, listing.files, needle, { collectAll: countAll, budget });
4395
- let overlap = 0;
4396
- for (const h of outcome.hits) {
4397
- if (seen.has(`${h.file}:${h.line}`)) {
4398
- overlap++;
4399
- continue;
4011
+ async function fetchHostedDocsCached(root, req, opts = {}) {
4012
+ const now = opts.now ?? Date.now;
4013
+ const ttl = opts.ttlMs ?? HOSTED_DOCS_TTL_MS;
4014
+ const key = hostedDocsCacheKey(req, opts);
4015
+ const file = loadFile(root);
4016
+ if (!opts.bypass) {
4017
+ const hit = file.entries[key];
4018
+ if (hit && now() - hit.at < ttl && hit.result?.content) {
4019
+ return { ...hit.result, metadata: { ...hit.result.metadata ?? {}, cached: true } };
4400
4020
  }
4401
- seen.add(`${h.file}:${h.line}`);
4402
- if (out.length < budget) out.push({ kind: "text", file: h.file, line: h.line, preview: h.preview });
4403
4021
  }
4404
- return { total: outcome.total - overlap, truncated: listing.truncated || outcome.truncated };
4022
+ const result = await fetchHostedDocs(req, opts);
4023
+ if (result) {
4024
+ file.entries[key] = { at: now(), result };
4025
+ saveFile(root, file);
4026
+ }
4027
+ return result;
4405
4028
  }
4406
- function listCandidateFiles(root, needle) {
4407
- return ripgrepCandidates(root, needle) ?? walkCandidatesCached(root);
4029
+
4030
+ // src/mcp/response.ts
4031
+ var MAX_RESULT_TOKENS = 25e3;
4032
+ function renderToolResult(result, maxTokens = MAX_RESULT_TOKENS) {
4033
+ return toolResult(clampToBudget(compactResult(result), maxTokens));
4408
4034
  }
4409
- var LISTING_TTL_MS = 2e3;
4410
- var listingCache = /* @__PURE__ */ new Map();
4411
- function listingTtlMs() {
4412
- const v = Number(process.env.VG_LISTING_TTL_MS);
4413
- return Number.isFinite(v) && v >= 0 ? v : LISTING_TTL_MS;
4035
+ function toolResult(result) {
4036
+ return {
4037
+ content: [{ type: "text", text: JSON.stringify(result) }],
4038
+ structuredContent: asStructured(result)
4039
+ };
4414
4040
  }
4415
- function walkCandidatesCached(root) {
4416
- const ttl = listingTtlMs();
4417
- if (ttl > 0) {
4418
- const hit = listingCache.get(root);
4419
- if (hit && Date.now() - hit.at < ttl) return hit.listing;
4420
- }
4421
- const listing = walkCandidates(root);
4422
- if (ttl > 0) listingCache.set(root, { listing, at: Date.now() });
4423
- return listing;
4041
+ function compactResult(value) {
4042
+ return prune(value);
4424
4043
  }
4425
- function walkCandidates(root) {
4426
- const files = [];
4427
- let truncated = false;
4428
- const stack = [root];
4429
- while (stack.length) {
4430
- const dir = stack.pop();
4431
- let entries;
4432
- try {
4433
- entries = fs18.readdirSync(dir, { withFileTypes: true });
4434
- } catch {
4435
- continue;
4436
- }
4437
- for (const e of entries) {
4438
- if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
4439
- const abs = path2.join(dir, e.name);
4440
- if (e.isDirectory()) {
4441
- stack.push(abs);
4442
- continue;
4443
- }
4444
- if (hasBinaryExt(e.name)) continue;
4445
- if (files.length >= MAX_FILES_SCANNED) {
4446
- truncated = true;
4447
- continue;
4448
- }
4449
- files.push(toPosix2(path2.relative(root, abs)));
4044
+ function prune(v) {
4045
+ if (Array.isArray(v)) return v.map(prune);
4046
+ if (v && typeof v === "object") {
4047
+ const out = {};
4048
+ for (const [k, raw] of Object.entries(v)) {
4049
+ const pv = prune(raw);
4050
+ if (!isEmpty(pv)) out[k] = pv;
4450
4051
  }
4052
+ return out;
4451
4053
  }
4452
- files.sort();
4453
- return { files, truncated };
4054
+ return v;
4454
4055
  }
4455
- var rgAvailable;
4456
- function hasRipgrep() {
4457
- if (process.env.VG_DISABLE_RIPGREP) return false;
4458
- if (rgAvailable === void 0) {
4459
- try {
4460
- rgAvailable = spawnSync("rg", ["--version"], { stdio: "ignore", timeout: 3e3 }).status === 0;
4461
- } catch {
4462
- rgAvailable = false;
4463
- }
4056
+ function isEmpty(v) {
4057
+ if (v === null || v === void 0 || v === "") return true;
4058
+ if (Array.isArray(v)) return v.length === 0;
4059
+ if (typeof v === "object") return Object.keys(v).length === 0;
4060
+ return false;
4061
+ }
4062
+ function clampToBudget(value, maxTokens = MAX_RESULT_TOKENS) {
4063
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
4064
+ if (tokensOf(value) <= maxTokens) return value;
4065
+ const obj2 = { ...value };
4066
+ const truncated = {};
4067
+ for (let guard = 0; guard < 64 && tokensOf(obj2) > maxTokens; guard++) {
4068
+ const arrays = Object.entries(obj2).filter(
4069
+ (e) => Array.isArray(e[1]) && e[1].length > 0
4070
+ );
4071
+ if (arrays.length === 0) break;
4072
+ arrays.sort((a, b) => tokensOf(b[1]) - tokensOf(a[1]));
4073
+ const [field, arr] = arrays[0];
4074
+ const total = truncated[field]?.total ?? arr.length;
4075
+ const shown = Math.max(1, Math.floor(arr.length / 2));
4076
+ obj2[field] = arr.slice(0, shown);
4077
+ truncated[field] = { shown, total };
4464
4078
  }
4465
- return rgAvailable;
4079
+ if (Object.keys(truncated).length) obj2._truncated = truncated;
4080
+ return obj2;
4466
4081
  }
4467
- function ripgrepCandidates(root, needle) {
4468
- if (!hasRipgrep()) return null;
4469
- const globs = [...IGNORE_DIRS].flatMap((d) => ["--glob", `!**/${d}/**`, "--glob", `!${d}/**`]);
4470
- const res = spawnSync(
4471
- "rg",
4472
- ["--files-with-matches", "--fixed-strings", "--ignore-case", "--no-ignore", "--no-messages", ...globs, "--", needle, "."],
4473
- { cwd: root, encoding: "utf8", timeout: 15e3, maxBuffer: 32 * 1024 * 1024 }
4474
- );
4475
- if (res.error || res.status !== 0 && res.status !== 1 || res.signal) return null;
4476
- const files = parseRgFileList(res.stdout);
4477
- if (files.length > MAX_FILES_SCANNED) return { files: files.slice(0, MAX_FILES_SCANNED), truncated: true };
4478
- return { files, truncated: false };
4082
+ function tokensOf(v) {
4083
+ return countTokens(JSON.stringify(v));
4479
4084
  }
4480
- function parseRgFileList(stdout, sep9 = path2.sep) {
4481
- const seen = /* @__PURE__ */ new Set();
4482
- for (const raw of stdout.split("\n")) {
4483
- if (!raw) continue;
4484
- const posix3 = sep9 === "\\" ? raw.replaceAll("\\", "/") : raw;
4485
- const rel2 = posix3.startsWith("./") ? posix3.slice(2) : posix3;
4486
- if (!rel2) continue;
4487
- const segs = rel2.split("/");
4488
- if (segs.some((s) => s.startsWith(".") || IGNORE_DIRS.has(s))) continue;
4489
- if (hasBinaryExt(rel2)) continue;
4490
- seen.add(rel2);
4085
+ function asStructured(result) {
4086
+ if (result && typeof result === "object" && !Array.isArray(result)) {
4087
+ return result;
4491
4088
  }
4492
- return [...seen].sort();
4089
+ return { result };
4090
+ }
4091
+ function boundList(items, max) {
4092
+ if (!Number.isFinite(max) || max < 0 || items.length <= max) {
4093
+ return { items, total: items.length };
4094
+ }
4095
+ return { items: items.slice(0, max), total: items.length };
4493
4096
  }
4097
+ var NODE_EDGE_CAP = 50;
4494
4098
 
4495
4099
  // src/mcp/tools.ts
4496
4100
  var warmEmbedder = null;
@@ -5375,12 +4979,12 @@ function detectAssistants(root, opts = {}) {
5375
4979
  const which = opts.which ?? whichOnPath;
5376
4980
  const found = [];
5377
4981
  for (const assistant of ASSISTANTS) {
5378
- const repoHit = (assistant.markers ?? []).find((m) => fs18.existsSync(path2.join(root, m)));
4982
+ const repoHit = (assistant.markers ?? []).find((m) => fs16.existsSync(path18.join(root, m)));
5379
4983
  if (repoHit) {
5380
4984
  found.push({ assistant, via: "repo", marker: repoHit });
5381
4985
  continue;
5382
4986
  }
5383
- const homeHit = (assistant.homeMarkers ?? []).find((m) => fs18.existsSync(path2.join(home, m)));
4987
+ const homeHit = (assistant.homeMarkers ?? []).find((m) => fs16.existsSync(path18.join(home, m)));
5384
4988
  if (homeHit) {
5385
4989
  found.push({ assistant, via: "home", marker: homeHit });
5386
4990
  continue;
@@ -5412,19 +5016,19 @@ function installAssistant(a, opts) {
5412
5016
  const skipped = [];
5413
5017
  let note;
5414
5018
  if (a.skill) {
5415
- writeFileEnsured(path2.join(opts.root, a.skill), skillMarkdown(a.id));
5019
+ writeFileEnsured(path18.join(opts.root, a.skill), skillMarkdown(a.id));
5416
5020
  wrote.push(a.skill);
5417
5021
  }
5418
5022
  if (a.mcp) {
5419
5023
  const launch = opts.launch ?? detectServeLaunch();
5420
- upsertMcp(path2.join(opts.root, a.mcp.file), a.mcp, launch);
5024
+ upsertMcp(path18.join(opts.root, a.mcp.file), a.mcp, launch);
5421
5025
  wrote.push(a.mcp.file);
5422
5026
  note = launch.note;
5423
5027
  } else {
5424
5028
  skipped.push("mcp (host-specific setup)");
5425
5029
  }
5426
5030
  if (a.nudge && opts.hook !== false) {
5427
- writeNudge(path2.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
5031
+ writeNudge(path18.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
5428
5032
  wrote.push(a.nudge.file);
5429
5033
  } else if (a.nudge) {
5430
5034
  skipped.push("nudge (--no-hook)");
@@ -5434,41 +5038,41 @@ function installAssistant(a, opts) {
5434
5038
  function uninstallAssistant(a, root, purge) {
5435
5039
  const removed = [];
5436
5040
  if (a.mcp) {
5437
- const file = path2.join(root, a.mcp.file);
5041
+ const file = path18.join(root, a.mcp.file);
5438
5042
  if (removeMcp(file, a.mcp)) removed.push(a.mcp.file);
5439
5043
  }
5440
5044
  if (a.nudge) {
5441
- const file = path2.join(root, a.nudge.file);
5045
+ const file = path18.join(root, a.nudge.file);
5442
5046
  if (a.nudge.kind === "block") {
5443
5047
  if (removeBlock(file)) removed.push(a.nudge.file);
5444
- } else if (fs18.existsSync(file)) {
5445
- fs18.rmSync(file);
5048
+ } else if (fs16.existsSync(file)) {
5049
+ fs16.rmSync(file);
5446
5050
  removed.push(a.nudge.file);
5447
5051
  }
5448
5052
  }
5449
5053
  if (purge && a.skill) {
5450
- const file = path2.join(root, a.skill);
5451
- if (fs18.existsSync(file)) {
5452
- fs18.rmSync(file);
5054
+ const file = path18.join(root, a.skill);
5055
+ if (fs16.existsSync(file)) {
5056
+ fs16.rmSync(file);
5453
5057
  removed.push(a.skill);
5454
5058
  }
5455
5059
  }
5456
5060
  return removed;
5457
5061
  }
5458
5062
  function writeFileEnsured(file, content) {
5459
- fs18.mkdirSync(path2.dirname(file), { recursive: true });
5460
- fs18.writeFileSync(file, content);
5063
+ fs16.mkdirSync(path18.dirname(file), { recursive: true });
5064
+ fs16.writeFileSync(file, content);
5461
5065
  }
5462
5066
  function writeNavigationConfig(root) {
5463
5067
  ensureVibgrateGitignore(root);
5464
- const rel2 = path2.join(".vibgrate", "mcp-navigation.json");
5068
+ const rel2 = path18.join(".vibgrate", "mcp-navigation.json");
5465
5069
  const doc = {
5466
5070
  _readme: "Deferred-loading config for agents embedding `vg serve` via the Claude API (defer_loading + tool-search). The hot navigation core stays in context; the rest load on demand \u2014 ~350-450 schema tokens/step vs ~1,881 for the full set. Hosts without defer_loading ignore this and serve the whole (already optimized) tool set. See docs/graph/VG-NAVIGATION-PROFILE.md.",
5467
5071
  hot_core: [...HOT_TOOLS],
5468
5072
  deferred: deferredToolNames(),
5469
5073
  toolset: navigationToolsetConfig()
5470
5074
  };
5471
- writeFileEnsured(path2.join(root, rel2), `${JSON.stringify(doc, null, 2)}
5075
+ writeFileEnsured(path18.join(root, rel2), `${JSON.stringify(doc, null, 2)}
5472
5076
  `);
5473
5077
  return rel2;
5474
5078
  }
@@ -5483,7 +5087,7 @@ function upsertMcp(file, target, launch) {
5483
5087
  `);
5484
5088
  }
5485
5089
  function removeMcp(file, target) {
5486
- if (!fs18.existsSync(file)) return false;
5090
+ if (!fs16.existsSync(file)) return false;
5487
5091
  const config = readJson(file);
5488
5092
  const bag = config[target.key];
5489
5093
  if (!bag || !(bag.vg !== void 0)) return false;
@@ -5502,8 +5106,8 @@ function refreshInstalledInstructions(root, smallRepo) {
5502
5106
  };
5503
5107
  for (const a of ASSISTANTS) {
5504
5108
  if (a.skill) {
5505
- const file = path2.join(root, a.skill);
5506
- if (fs18.existsSync(file)) {
5109
+ const file = path18.join(root, a.skill);
5110
+ if (fs16.existsSync(file)) {
5507
5111
  const text = readTextSafe(file);
5508
5112
  const from = text === null ? null : staleVersion(text, "# vg \u2014 the code map");
5509
5113
  if (from !== null) {
@@ -5513,8 +5117,8 @@ function refreshInstalledInstructions(root, smallRepo) {
5513
5117
  }
5514
5118
  }
5515
5119
  if (a.nudge) {
5516
- const file = path2.join(root, a.nudge.file);
5517
- if (!fs18.existsSync(file)) continue;
5120
+ const file = path18.join(root, a.nudge.file);
5121
+ if (!fs16.existsSync(file)) continue;
5518
5122
  const text = readTextSafe(file);
5519
5123
  if (text === null) continue;
5520
5124
  if (a.nudge.kind === "block") {
@@ -5538,7 +5142,7 @@ function refreshInstalledInstructions(root, smallRepo) {
5538
5142
  }
5539
5143
  function readTextSafe(file) {
5540
5144
  try {
5541
- return fs18.readFileSync(file, "utf8");
5145
+ return fs16.readFileSync(file, "utf8");
5542
5146
  } catch {
5543
5147
  return null;
5544
5148
  }
@@ -5557,8 +5161,8 @@ ${stripMarkers(nudgeMarkdown(smallRepo, client))}` : stripMarkers(nudgeMarkdown(
5557
5161
  upsertBlock(file, nudgeMarkdown(smallRepo, client));
5558
5162
  }
5559
5163
  function upsertBlock(file, block) {
5560
- fs18.mkdirSync(path2.dirname(file), { recursive: true });
5561
- let existing = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "";
5164
+ fs16.mkdirSync(path18.dirname(file), { recursive: true });
5165
+ let existing = fs16.existsSync(file) ? fs16.readFileSync(file, "utf8") : "";
5562
5166
  const re = new RegExp(`${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}`);
5563
5167
  if (re.test(existing)) existing = existing.replace(re, block);
5564
5168
  else existing = existing.length ? `${existing.replace(/\s*$/, "")}
@@ -5566,20 +5170,20 @@ function upsertBlock(file, block) {
5566
5170
  ${block}
5567
5171
  ` : `${block}
5568
5172
  `;
5569
- fs18.writeFileSync(file, existing);
5173
+ fs16.writeFileSync(file, existing);
5570
5174
  }
5571
5175
  function removeBlock(file) {
5572
- if (!fs18.existsSync(file)) return false;
5573
- const existing = fs18.readFileSync(file, "utf8");
5176
+ if (!fs16.existsSync(file)) return false;
5177
+ const existing = fs16.readFileSync(file, "utf8");
5574
5178
  const re = new RegExp(`\\n*${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}\\n*`);
5575
5179
  if (!re.test(existing)) return false;
5576
- fs18.writeFileSync(file, existing.replace(re, "\n"));
5180
+ fs16.writeFileSync(file, existing.replace(re, "\n"));
5577
5181
  return true;
5578
5182
  }
5579
5183
  function readJson(file) {
5580
- if (!fs18.existsSync(file)) return {};
5184
+ if (!fs16.existsSync(file)) return {};
5581
5185
  try {
5582
- const parsed = JSON.parse(fs18.readFileSync(file, "utf8"));
5186
+ const parsed = JSON.parse(fs16.readFileSync(file, "utf8"));
5583
5187
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5584
5188
  return parsed;
5585
5189
  }
@@ -5599,7 +5203,7 @@ function escapeRe(s) {
5599
5203
  }
5600
5204
  var REFRESH_LOCK_STALE_MS = 10 * 60 * 1e3;
5601
5205
  function refreshLockPath(root) {
5602
- return path2.join(cacheDir(root), "refresh.lock");
5206
+ return path18.join(cacheDir(root), "refresh.lock");
5603
5207
  }
5604
5208
  async function refreshIfStale(root, opts = {}) {
5605
5209
  const probe = probeFreshness(root);
@@ -5630,8 +5234,9 @@ async function refreshIfStale(root, opts = {}) {
5630
5234
  const dir = vibgrateDir(root);
5631
5235
  writeArtifacts(result.graph, {
5632
5236
  root,
5633
- report: fs18.existsSync(path2.join(dir, "GRAPH_REPORT.md")),
5634
- html: fs18.existsSync(path2.join(dir, "graph.html"))
5237
+ graphPath: opts.graphPath,
5238
+ report: fs16.existsSync(path18.join(dir, "GRAPH_REPORT.md")),
5239
+ html: fs16.existsSync(path18.join(dir, "graph.html"))
5635
5240
  });
5636
5241
  }
5637
5242
  writeSnapshot(root, result.graph.provenance.corpusHash, result.fileStats, scope);
@@ -5651,6 +5256,6 @@ async function refreshIfStale(root, opts = {}) {
5651
5256
  }
5652
5257
  }
5653
5258
 
5654
- export { ASSISTANTS, CliError, ExitCode, FREE_PACK, NPX_INVOCATION, ResourceLimitError, SCHEMA_VERSION, SKIP_DIRS, SKIP_FILES, SMALL_REPO_FILES, TOOLS, UsageError, analyze, applyCoverage, applyStaticTestLinkage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, buildModuleResolver, clearStoredCredentials, countTokens, coveringTests, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectAssistants, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, dsnCommand, ensureGitignored, epistemicBreakdown, fetchHostedDocsCached, findGitRoot, gitignoreEntryForCredentials, groundGraph, hasDrift, homeCredentialsPath, hostedBase, ingestHostForRegionId, installAssistant, isTestFile, loadCoverage, loadGraph, loadSnapshot, parseDsn, parseJsonc, probeFreshness, projectCredentialsPath, publishPrivateLibrary, pushCommand, readScanArtifact, readStoredCredentials, refreshIfStale, refreshInstalledInstructions, relativeResolver, renderHtml, renderReport, renderToolResult, resolveCliInvocation, resolveDsn, resolveIngestHost, resolveLimits, scipEdges, selectForBudget, shortestPath, symbolsFromApi, testsToRun, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, vibgrateDir, warmEmbedderInBackground, writeArtifacts, writeNavigationConfig, writeSnapshot, writeStoredCredentials };
5655
- //# sourceMappingURL=chunk-3MXNNBLI.js.map
5656
- //# sourceMappingURL=chunk-3MXNNBLI.js.map
5259
+ export { ASSISTANTS, CliError, ExitCode, FREE_PACK, NPX_INVOCATION, ResourceLimitError, SKIP_DIRS, SKIP_FILES, SMALL_REPO_FILES, TOOLS, UsageError, analyze, applyCoverage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, clearStoredCredentials, countTokens, coveringTests, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, detectAssistants, detectRunner, detectServeLaunch, discover, driftCount, dsnCommand, ensureGitignored, epistemicBreakdown, fetchHostedDocsCached, findGitRoot, gitignoreEntryForCredentials, groundGraph, hasDrift, homeCredentialsPath, hostedBase, ingestHostForRegionId, installAssistant, loadCoverage, loadSnapshot, parseDsn, probeFreshness, projectCredentialsPath, publishPrivateLibrary, pushCommand, readScanArtifact, readStoredCredentials, refreshIfStale, refreshInstalledInstructions, renderToolResult, resolveCliInvocation, resolveDsn, resolveIngestHost, resolveLimits, scipEdges, searchSymbols, selectForBudget, shortestPath, symbolsFromApi, testsToRun, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, warmEmbedderInBackground, writeNavigationConfig, writeSnapshot, writeStoredCredentials };
5260
+ //# sourceMappingURL=chunk-KNELIQJO.js.map
5261
+ //# sourceMappingURL=chunk-KNELIQJO.js.map