@vibgrate/cli 2026.727.1 → 2026.727.3

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 (130) hide show
  1. package/DOCS.md +21 -18
  2. package/README.md +2 -2
  3. package/dist/baseline-ATM5MXBZ.js +8 -0
  4. package/dist/{baseline-K7YBFKZI.js.map → baseline-ATM5MXBZ.js.map} +1 -1
  5. package/dist/{chunk-OXQNQW5J.js → chunk-2VFWXNS5.js} +4 -4
  6. package/dist/{chunk-OXQNQW5J.js.map → chunk-2VFWXNS5.js.map} +1 -1
  7. package/dist/{chunk-VXGV6NTD.js → chunk-3V24O47U.js} +11 -9
  8. package/dist/chunk-3V24O47U.js.map +1 -0
  9. package/dist/{chunk-C4CKTAZP.js → chunk-6JIKUDWM.js} +4 -4
  10. package/dist/chunk-6JIKUDWM.js.map +1 -0
  11. package/dist/{chunk-6IOQJGFT.js → chunk-6UK6QPTR.js} +96 -140
  12. package/dist/chunk-6UK6QPTR.js.map +1 -0
  13. package/dist/chunk-6UQMXIAG.js +122 -0
  14. package/dist/chunk-6UQMXIAG.js.map +1 -0
  15. package/dist/{chunk-SZLF66FJ.js → chunk-77LYGXBI.js} +5 -5
  16. package/dist/{chunk-SZLF66FJ.js.map → chunk-77LYGXBI.js.map} +1 -1
  17. package/dist/{chunk-KHJO4D5N.js → chunk-7N4Y3E47.js} +2 -2
  18. package/dist/{chunk-KHJO4D5N.js.map → chunk-7N4Y3E47.js.map} +1 -1
  19. package/dist/{chunk-65MEOA4E.js → chunk-AVQSHIJ5.js} +22 -18
  20. package/dist/chunk-AVQSHIJ5.js.map +1 -0
  21. package/dist/chunk-B3YUO3NW.js +469 -0
  22. package/dist/chunk-B3YUO3NW.js.map +1 -0
  23. package/dist/{chunk-YDAT7LD2.js → chunk-C66GOHYB.js} +18 -10
  24. package/dist/chunk-C66GOHYB.js.map +1 -0
  25. package/dist/chunk-DGUM43GV.js +10 -0
  26. package/dist/chunk-DGUM43GV.js.map +1 -0
  27. package/dist/{chunk-HYZVHJ3Q.js → chunk-FFWGT55D.js} +4 -4
  28. package/dist/{chunk-HYZVHJ3Q.js.map → chunk-FFWGT55D.js.map} +1 -1
  29. package/dist/{chunk-4CQ3MGNN.js → chunk-HK3AWUXH.js} +3 -3
  30. package/dist/{chunk-4CQ3MGNN.js.map → chunk-HK3AWUXH.js.map} +1 -1
  31. package/dist/{chunk-S6CYDZEW.js → chunk-MAY5HLDH.js} +772 -200
  32. package/dist/chunk-MAY5HLDH.js.map +1 -0
  33. package/dist/{chunk-7JHPQ6WF.js → chunk-OFUC6RYN.js} +3 -3
  34. package/dist/{chunk-7JHPQ6WF.js.map → chunk-OFUC6RYN.js.map} +1 -1
  35. package/dist/{chunk-3Q7QBWFW.js → chunk-ON25BX6T.js} +9 -3
  36. package/dist/chunk-ON25BX6T.js.map +1 -0
  37. package/dist/{chunk-JZYGEZTU.js → chunk-XPJZTSY7.js} +3 -3
  38. package/dist/{chunk-JZYGEZTU.js.map → chunk-XPJZTSY7.js.map} +1 -1
  39. package/dist/{chunk-WLETRGAC.js → chunk-Y23HMSWU.js} +11 -11
  40. package/dist/{chunk-WLETRGAC.js.map → chunk-Y23HMSWU.js.map} +1 -1
  41. package/dist/{chunk-3BVSDMHV.js → chunk-Z7GP7XTJ.js} +8 -8
  42. package/dist/{chunk-3BVSDMHV.js.map → chunk-Z7GP7XTJ.js.map} +1 -1
  43. package/dist/{chunk-EDAAG7BL.js → chunk-ZBTY6VV4.js} +4 -3
  44. package/dist/chunk-ZBTY6VV4.js.map +1 -0
  45. package/dist/chunk-ZKNB4YH4.js +102 -0
  46. package/dist/chunk-ZKNB4YH4.js.map +1 -0
  47. package/dist/cli.d.ts +3 -0
  48. package/dist/cli.js +475 -404
  49. package/dist/cli.js.map +1 -1
  50. package/dist/{execution-env-3BN7Z737.js → execution-env-ZU45JZ4G.js} +3 -2
  51. package/dist/{execution-env-3BN7Z737.js.map → execution-env-ZU45JZ4G.js.map} +1 -1
  52. package/dist/{federation-EPZDUWCK.js → federation-SZWZIFZ7.js} +3 -2
  53. package/dist/{federation-EPZDUWCK.js.map → federation-SZWZIFZ7.js.map} +1 -1
  54. package/dist/{fs-IPY4FJKH.js → fs-B5HZ2MIU.js} +3 -2
  55. package/dist/fs-B5HZ2MIU.js.map +1 -0
  56. package/dist/{fs-KCABDURV.js → fs-O5IQYXHO.js} +3 -2
  57. package/dist/fs-O5IQYXHO.js.map +1 -0
  58. package/dist/{git-ref-HPRLUFFW.js → git-ref-XDXB6JP5.js} +3 -2
  59. package/dist/{git-ref-HPRLUFFW.js.map → git-ref-XDXB6JP5.js.map} +1 -1
  60. package/dist/graph-backend-UTOL3SHE.js +11 -0
  61. package/dist/{graph-backend-ZMSYOZMW.js.map → graph-backend-UTOL3SHE.js.map} +1 -1
  62. package/dist/index.d.ts +53 -17
  63. package/dist/index.js +11 -10
  64. package/dist/index.js.map +1 -1
  65. package/dist/{interactive-ICL7D7LO.js → interactive-INGJNSON.js} +23 -22
  66. package/dist/interactive-INGJNSON.js.map +1 -0
  67. package/dist/{llm-host-2RAYQSDQ.js → llm-host-7GWDO6IX.js} +3 -2
  68. package/dist/{llm-host-2RAYQSDQ.js.map → llm-host-7GWDO6IX.js.map} +1 -1
  69. package/dist/load-QPOWQD7C.js +8 -0
  70. package/dist/{load-OHVKDX4R.js.map → load-QPOWQD7C.js.map} +1 -1
  71. package/dist/local-runtime-5O2NN3GA.js +5 -0
  72. package/dist/{local-runtime-OJFLX2HI.js.map → local-runtime-5O2NN3GA.js.map} +1 -1
  73. package/dist/mcp-tools-I7JK3S2M.js +4 -0
  74. package/dist/{mcp-tools-I6PME3AV.js.map → mcp-tools-I7JK3S2M.js.map} +1 -1
  75. package/dist/{model-execution-profile-MQ2QCP24.js → model-execution-profile-TFBKCNJG.js} +3 -2
  76. package/dist/{model-execution-profile-MQ2QCP24.js.map → model-execution-profile-TFBKCNJG.js.map} +1 -1
  77. package/dist/{model-orchestrator-FFTZI3U2.js → model-orchestrator-TVONAUPS.js} +5 -4
  78. package/dist/{model-orchestrator-FFTZI3U2.js.map → model-orchestrator-TVONAUPS.js.map} +1 -1
  79. package/dist/parse-worker.d.ts +1 -1
  80. package/dist/parse-worker.js +2 -1
  81. package/dist/parse-worker.js.map +1 -1
  82. package/dist/{paths-MCH75O3G.js → paths-2RAH3GME.js} +3 -2
  83. package/dist/{paths-MCH75O3G.js.map → paths-2RAH3GME.js.map} +1 -1
  84. package/dist/{resolve-gguf-4UPIGPEE.js → resolve-gguf-VPL4YQ6J.js} +3 -2
  85. package/dist/{resolve-gguf-4UPIGPEE.js.map → resolve-gguf-VPL4YQ6J.js.map} +1 -1
  86. package/dist/runtime-session-4AMZIAAG.js +18 -0
  87. package/dist/{runtime-session-IDU5BDUJ.js.map → runtime-session-4AMZIAAG.js.map} +1 -1
  88. package/dist/session-M72522AP.js +11 -0
  89. package/dist/{session-L2OSDRPI.js.map → session-M72522AP.js.map} +1 -1
  90. package/dist/session-store-WFBX7HXL.js +4 -0
  91. package/dist/{session-store-W5RZZHA3.js.map → session-store-WFBX7HXL.js.map} +1 -1
  92. package/dist/sql-extract-JELDZA6W.js +81 -0
  93. package/dist/sql-extract-JELDZA6W.js.map +1 -0
  94. package/dist/{stream-json-F74W5FCK.js → stream-json-6ZBUA2L3.js} +11 -10
  95. package/dist/stream-json-6ZBUA2L3.js.map +1 -0
  96. package/dist/{types-ZMg1mSuu.d.ts → types-DLH-Ritr.d.ts} +22 -1
  97. package/dist/ui-Q6DJ3GCU.js +5 -0
  98. package/dist/ui-Q6DJ3GCU.js.map +1 -0
  99. package/dist/{vgd-EGBQD7BW.js → vgd-CVLKBS7K.js} +7 -6
  100. package/dist/vgd-CVLKBS7K.js.map +1 -0
  101. package/package.json +1 -1
  102. package/dist/baseline-K7YBFKZI.js +0 -7
  103. package/dist/chunk-3Q7QBWFW.js.map +0 -1
  104. package/dist/chunk-5Q54GEHM.js +0 -29
  105. package/dist/chunk-5Q54GEHM.js.map +0 -1
  106. package/dist/chunk-65MEOA4E.js.map +0 -1
  107. package/dist/chunk-6IOQJGFT.js.map +0 -1
  108. package/dist/chunk-C4CKTAZP.js.map +0 -1
  109. package/dist/chunk-EDAAG7BL.js.map +0 -1
  110. package/dist/chunk-EDBV33B7.js +0 -57
  111. package/dist/chunk-EDBV33B7.js.map +0 -1
  112. package/dist/chunk-HABUT7HW.js +0 -233
  113. package/dist/chunk-HABUT7HW.js.map +0 -1
  114. package/dist/chunk-S6CYDZEW.js.map +0 -1
  115. package/dist/chunk-VXGV6NTD.js.map +0 -1
  116. package/dist/chunk-YDAT7LD2.js.map +0 -1
  117. package/dist/fs-IPY4FJKH.js.map +0 -1
  118. package/dist/fs-KCABDURV.js.map +0 -1
  119. package/dist/graph-backend-ZMSYOZMW.js +0 -10
  120. package/dist/interactive-ICL7D7LO.js.map +0 -1
  121. package/dist/load-OHVKDX4R.js +0 -7
  122. package/dist/local-runtime-OJFLX2HI.js +0 -4
  123. package/dist/mcp-tools-I6PME3AV.js +0 -3
  124. package/dist/runtime-session-IDU5BDUJ.js +0 -17
  125. package/dist/session-L2OSDRPI.js +0 -10
  126. package/dist/session-store-W5RZZHA3.js +0 -3
  127. package/dist/stream-json-F74W5FCK.js.map +0 -1
  128. package/dist/ui-HOVOPWMX.js +0 -4
  129. package/dist/ui-HOVOPWMX.js.map +0 -1
  130. package/dist/vgd-EGBQD7BW.js.map +0 -1
@@ -1,17 +1,17 @@
1
- import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-WLETRGAC.js';
2
- import { resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, inventory, localApiSurface, edgeId, buildModuleResolver, resolve, applyStaticTestLinkage, nodeId } from './chunk-C4CKTAZP.js';
1
+ import { resolveVersion, resolveLib, loadCatalog, driftFor, localPackageDocs, readDoc, inventory, localApiSurface, edgeId, buildModuleResolver, resolve, applyStaticTestLinkage, nodeId } from './chunk-6JIKUDWM.js';
2
+ import { loadConfig, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-Y23HMSWU.js';
3
+ import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
3
4
  import { resolveOne, findNodes } from './chunk-J7LKEB2R.js';
4
5
  import { discoverModels } from './chunk-OOIG3RUM.js';
5
- import { impactOf, indexFor, withTimeout, getNodeEmbeddings, queryGraphSemantic, queryGraph, loadEmbedder, isModelReady, cacheDir, loadCache, acquireLock, releaseLock } from './chunk-6IOQJGFT.js';
6
- import { langById, grammarSetVersion, langForExtension, setGrammarsOverride, parseSource } from './chunk-3Q7QBWFW.js';
6
+ import { impactOf, indexFor, withTimeout, getNodeEmbeddings, queryGraphSemantic, queryGraph, loadEmbedder, isModelReady, acquireLock, releaseLock } from './chunk-6UK6QPTR.js';
7
+ import { langById, grammarSetVersion, langForExtension, setGrammarsOverride, parseSource } from './chunk-ON25BX6T.js';
7
8
  import { redactSecrets } from './chunk-RXP66R2E.js';
8
- import { loadGraph, ensureVibgrateGitignore, vibgrateDir, writeArtifacts } from './chunk-HABUT7HW.js';
9
- import { serializeGraph, stableStringify } from './chunk-5Q54GEHM.js';
9
+ import { writeGraphIndex, SCHEMA_VERSION, loadGraph, ensureVibgrateGitignore, vibgrateDir, writeArtifacts } from './chunk-B3YUO3NW.js';
10
+ import { cacheDir, loadCache, serializeGraph, stableStringify } from './chunk-ZKNB4YH4.js';
10
11
  import { shortId, canonicalize, hashString, hashBytes } from './chunk-JFGYT6BI.js';
11
- import { writeTextFile, pathExists, readJsonFile } from './chunk-CRAMOEBE.js';
12
- import * as fs15 from 'fs';
12
+ import * as fs18 from 'fs';
13
13
  import { spawnSync, execFileSync } from 'child_process';
14
- import * as path17 from 'path';
14
+ import * as path19 from 'path';
15
15
  import ignore from 'ignore';
16
16
  import * as v8 from 'v8';
17
17
  import Graph from 'graphology';
@@ -22,6 +22,7 @@ import louvain from 'graphology-communities-louvain';
22
22
  import * as os from 'os';
23
23
  import { fileURLToPath } from 'url';
24
24
  import ts from 'typescript';
25
+ import * as fs8 from 'fs/promises';
25
26
  import { bidirectional } from 'graphology-shortest-path/unweighted.js';
26
27
  import * as crypto from 'crypto';
27
28
  import { createHash } from 'crypto';
@@ -46,9 +47,9 @@ function whichOnPath(cmd) {
46
47
  }
47
48
  function isOwnBinary(binPath) {
48
49
  try {
49
- const real = fs15.realpathSync(binPath);
50
+ const real = fs18.realpathSync(binPath);
50
51
  if (/[\\/]@vibgrate[\\/]cli[\\/]/.test(real)) return true;
51
- const head = fs15.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
52
+ const head = fs18.readFileSync(real, { encoding: "utf8" }).slice(0, 2048);
52
53
  return head.includes("@vibgrate/cli") || head.includes("vibgrate");
53
54
  } catch {
54
55
  return false;
@@ -58,7 +59,7 @@ function isEphemeralNpxBinary(binPath) {
58
59
  const NPX_SEGMENT = /[\\/]_npx[\\/]/;
59
60
  if (NPX_SEGMENT.test(binPath)) return true;
60
61
  try {
61
- return NPX_SEGMENT.test(fs15.realpathSync(binPath));
62
+ return NPX_SEGMENT.test(fs18.realpathSync(binPath));
62
63
  } catch {
63
64
  return false;
64
65
  }
@@ -235,19 +236,19 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
235
236
  "flake.lock"
236
237
  ]);
237
238
  function toPosix(p) {
238
- return p.split(path17.sep).join("/");
239
+ return p.split(path19.sep).join("/");
239
240
  }
240
241
  function buildRootIgnore(root, exclude) {
241
242
  const ig = ignore();
242
- const gitignorePath = path17.join(root, ".gitignore");
243
- if (fs15.existsSync(gitignorePath)) {
244
- ig.add(fs15.readFileSync(gitignorePath, "utf8"));
243
+ const gitignorePath = path19.join(root, ".gitignore");
244
+ if (fs18.existsSync(gitignorePath)) {
245
+ ig.add(fs18.readFileSync(gitignorePath, "utf8"));
245
246
  }
246
247
  if (exclude.length) ig.add(exclude);
247
248
  return ig;
248
249
  }
249
250
  function discover(options) {
250
- const root = path17.resolve(options.root);
251
+ const root = path19.resolve(options.root);
251
252
  const onlyLangs = (options.only ?? []).filter(Boolean);
252
253
  const allowLang = (lang) => onlyLangs.length === 0 || onlyLangs.includes(lang.id);
253
254
  for (const id of onlyLangs) {
@@ -256,27 +257,27 @@ function discover(options) {
256
257
  }
257
258
  }
258
259
  const rootIg = buildRootIgnore(root, options.exclude ?? []);
259
- const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path17.resolve(root, p)) : [root]).filter((p) => fs15.existsSync(p));
260
+ const scopeAbs = (options.paths && options.paths.length ? options.paths.map((p) => path19.resolve(root, p)) : [root]).filter((p) => fs18.existsSync(p));
260
261
  const found = /* @__PURE__ */ new Map();
261
262
  const considerFile = (abs) => {
262
- const rel2 = toPosix(path17.relative(root, abs));
263
+ const rel2 = toPosix(path19.relative(root, abs));
263
264
  if (rel2.startsWith("..")) return;
264
265
  if (rel2 === "" || rootIg.ignores(rel2)) return;
265
- if (SKIP_FILES.has(path17.basename(abs).toLowerCase())) return;
266
- const lang = langForExtension(path17.extname(abs));
266
+ if (SKIP_FILES.has(path19.basename(abs).toLowerCase())) return;
267
+ const lang = langForExtension(path19.extname(abs));
267
268
  if (!lang || !allowLang(lang)) return;
268
269
  found.set(rel2, { rel: rel2, abs, lang });
269
270
  };
270
271
  const walk = (dir) => {
271
272
  let entries;
272
273
  try {
273
- entries = fs15.readdirSync(dir, { withFileTypes: true });
274
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
274
275
  } catch {
275
276
  return;
276
277
  }
277
278
  for (const entry of entries) {
278
- const abs = path17.join(dir, entry.name);
279
- const rel2 = toPosix(path17.relative(root, abs));
279
+ const abs = path19.join(dir, entry.name);
280
+ const rel2 = toPosix(path19.relative(root, abs));
280
281
  if (entry.isDirectory()) {
281
282
  if (SKIP_DIRS.has(entry.name)) continue;
282
283
  if (rel2 && rootIg.ignores(`${rel2}/`)) continue;
@@ -287,7 +288,7 @@ function discover(options) {
287
288
  }
288
289
  };
289
290
  for (const scope of scopeAbs) {
290
- const stat = fs15.statSync(scope);
291
+ const stat = fs18.statSync(scope);
291
292
  if (stat.isDirectory()) walk(scope);
292
293
  else if (stat.isFile()) considerFile(scope);
293
294
  }
@@ -540,25 +541,36 @@ function mulberry32(seed) {
540
541
  var CLUSTER_SEED = 42;
541
542
 
542
543
  // src/engine/analyze.ts
544
+ var TIER_LARGE_NODES = 5e3;
545
+ var TIER_XL_NODES = 5e4;
546
+ function pickAnalysisTier(nodeCount, forced) {
547
+ if (forced) return forced;
548
+ if (nodeCount > TIER_XL_NODES) return "xl";
549
+ if (nodeCount > TIER_LARGE_NODES) return "large";
550
+ return "full";
551
+ }
543
552
  var DEP_EDGES = /* @__PURE__ */ new Set(["call", "import", "extends", "implements", "references"]);
544
553
  var CLUSTER_EDGE_KINDS = /* @__PURE__ */ new Set([...DEP_EDGES, "contains"]);
545
554
  var IMPORTANCE_WEIGHTS = { pagerank: 0.4, betweenness: 0.25, eigenvector: 0.2, degree: 0.15 };
546
555
  var DEFAULT_BETWEENNESS_LIMIT = 5e3;
547
556
  function analyze(nodes, edges, options = {}) {
548
- const mode = options.cluster ?? "louvain";
557
+ const tier = pickAnalysisTier(nodes.length, options.tier);
558
+ const mode = options.cluster ?? (tier === "xl" ? "louvain" : "louvain");
549
559
  const ids = [...nodes.map((n) => n.id)].sort((a, b) => a.localeCompare(b));
550
560
  const dg = simpleGraph(ids, edges, DEP_EDGES, "directed");
551
561
  const pr = safeMetric(() => pagerank(dg), ids);
552
- const bw = dg.order <= (options.betweennessLimit ?? DEFAULT_BETWEENNESS_LIMIT) ? safeMetric(() => betweenness(dg), ids) : zeros(ids);
553
- const ev = safeMetric(() => eigenvector(dg), ids);
562
+ const bwLimit = options.betweennessLimit ?? DEFAULT_BETWEENNESS_LIMIT;
563
+ const bw = tier === "full" && dg.order <= bwLimit ? safeMetric(() => betweenness(dg), ids) : zeros(ids);
564
+ const ev = tier === "full" || tier === "large" ? safeMetric(() => eigenvector(dg), ids) : zeros(ids);
554
565
  const deg = degreeMap(dg, ids);
555
566
  const prN = normalize(pr);
556
567
  const bwN = normalize(bw);
557
568
  const evN = normalize(ev);
558
569
  const degN = normalize(deg);
570
+ 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;
559
571
  const importance = /* @__PURE__ */ new Map();
560
572
  for (const id of ids) {
561
- 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);
573
+ 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);
562
574
  importance.set(id, score);
563
575
  }
564
576
  const importanceN = normalize(importance);
@@ -567,7 +579,7 @@ function analyze(nodes, edges, options = {}) {
567
579
  const variance = vals.reduce((a, b) => a + (b - mean) ** 2, 0) / (vals.length || 1);
568
580
  const std = Math.sqrt(variance);
569
581
  const hubThreshold = mean + 2 * std;
570
- const { areaByNode, mode: actualMode } = cluster(ids, edges, mode);
582
+ const { areaByNode, mode: actualMode } = tier === "xl" ? clusterFileLevel(nodes, edges, mode) : cluster(ids, edges, mode);
571
583
  const outNodes = nodes.map((n) => ({
572
584
  ...n,
573
585
  centrality: {
@@ -582,7 +594,42 @@ function analyze(nodes, edges, options = {}) {
582
594
  })).sort((a, b) => a.id.localeCompare(b.id));
583
595
  const areas = buildAreas(outNodes, edges, areaByNode);
584
596
  const outEdges = scoreSurprise(edges, areaByNode);
585
- return { nodes: outNodes, edges: outEdges, areas, cluster: actualMode };
597
+ return { nodes: outNodes, edges: outEdges, areas, cluster: actualMode, tier };
598
+ }
599
+ function clusterFileLevel(nodes, edges, mode) {
600
+ const fileNodes = nodes.filter((n) => n.kind === "file");
601
+ const fileIds = fileNodes.map((n) => n.id).sort((a, b) => a.localeCompare(b));
602
+ if (fileIds.length === 0) {
603
+ return { areaByNode: new Map(nodes.map((n) => [n.id, -1])), mode: "none" };
604
+ }
605
+ const fileIdByPath = new Map(fileNodes.map((n) => [n.file, n.id]));
606
+ const fileOf = /* @__PURE__ */ new Map();
607
+ for (const n of nodes) {
608
+ if (n.kind === "file") fileOf.set(n.id, n.id);
609
+ else if (n.file) {
610
+ const fid = fileIdByPath.get(n.file);
611
+ if (fid) fileOf.set(n.id, fid);
612
+ }
613
+ }
614
+ const fileEdges = [];
615
+ const seen = /* @__PURE__ */ new Set();
616
+ for (const e of edges) {
617
+ if (!DEP_EDGES.has(e.kind)) continue;
618
+ const sf = fileOf.get(e.src);
619
+ const df = fileOf.get(e.dst);
620
+ if (!sf || !df || sf === df) continue;
621
+ const key = `${e.kind}|${sf}|${df}`;
622
+ if (seen.has(key)) continue;
623
+ seen.add(key);
624
+ fileEdges.push({ ...e, src: sf, dst: df, id: key });
625
+ }
626
+ const { areaByNode: fileAreas, mode: used } = cluster(fileIds, fileEdges, mode);
627
+ const areaByNode = /* @__PURE__ */ new Map();
628
+ for (const n of nodes) {
629
+ const fid = fileOf.get(n.id);
630
+ areaByNode.set(n.id, fid ? fileAreas.get(fid) ?? -1 : -1);
631
+ }
632
+ return { areaByNode, mode: used };
586
633
  }
587
634
  function simpleGraph(ids, edges, kinds, type) {
588
635
  const g = new Graph({ type, multi: false, allowSelfLoops: false });
@@ -723,14 +770,14 @@ var DEFAULT_PATHS = [
723
770
  ];
724
771
  function loadCoverage(root, explicit) {
725
772
  const candidates = (explicit && explicit.length ? explicit : DEFAULT_PATHS).map(
726
- (p) => path17.resolve(root, p)
773
+ (p) => path19.resolve(root, p)
727
774
  );
728
- const found = candidates.filter((p) => fs15.existsSync(p));
775
+ const found = candidates.filter((p) => fs18.existsSync(p));
729
776
  if (found.length === 0) return null;
730
777
  const map = /* @__PURE__ */ new Map();
731
778
  for (const file of found) {
732
779
  try {
733
- const text = fs15.readFileSync(file, "utf8");
780
+ const text = fs18.readFileSync(file, "utf8");
734
781
  if (file.endsWith(".json")) mergeIstanbul(map, text, root);
735
782
  else mergeLcov(map, text, root);
736
783
  } catch {
@@ -739,8 +786,8 @@ function loadCoverage(root, explicit) {
739
786
  return map.size ? map : null;
740
787
  }
741
788
  function rel(root, p) {
742
- const abs = path17.isAbsolute(p) ? p : path17.resolve(root, p);
743
- return path17.relative(root, abs).split(path17.sep).join("/");
789
+ const abs = path19.isAbsolute(p) ? p : path19.resolve(root, p);
790
+ return path19.relative(root, abs).split(path19.sep).join("/");
744
791
  }
745
792
  function bump(map, file, line, hits) {
746
793
  let lh = map.get(file);
@@ -1027,9 +1074,6 @@ function matchConfidence(entry, nameParts, imports, callees) {
1027
1074
  }
1028
1075
  return conf;
1029
1076
  }
1030
-
1031
- // src/schema.ts
1032
- var SCHEMA_VERSION = "vg-graph/1.0";
1033
1077
  var DEFAULT_INLINE_THRESHOLD = 24;
1034
1078
  async function parseFiles(files, options = {}) {
1035
1079
  const threshold = options.inlineThreshold ?? DEFAULT_INLINE_THRESHOLD;
@@ -1050,7 +1094,7 @@ async function parseInline(files, options) {
1050
1094
  onProgress?.(0, files.length);
1051
1095
  for (const file of files) {
1052
1096
  try {
1053
- const source = fs15.readFileSync(file.abs, "utf8");
1097
+ const source = fs18.readFileSync(file.abs, "utf8");
1054
1098
  out.push(await parseSource(file.rel, file.lang.id, source));
1055
1099
  } catch (err) {
1056
1100
  out.push(emptyParse(file, `parse failed: ${err.message}`));
@@ -1105,9 +1149,9 @@ function isWorkerOom(err) {
1105
1149
  return e?.code === "ERR_WORKER_OUT_OF_MEMORY" || /out of memory/i.test(e?.message ?? "");
1106
1150
  }
1107
1151
  function resolveWorkerFile() {
1108
- const here = path17.dirname(fileURLToPath(import.meta.url));
1109
- const candidate = path17.join(here, "parse-worker.js");
1110
- return fs15.existsSync(candidate) ? candidate : null;
1152
+ const here = path19.dirname(fileURLToPath(import.meta.url));
1153
+ const candidate = path19.join(here, "parse-worker.js");
1154
+ return fs18.existsSync(candidate) ? candidate : null;
1111
1155
  }
1112
1156
  function chunk(items, buckets) {
1113
1157
  const out = Array.from({ length: Math.min(buckets, items.length || 1) }, () => []);
@@ -1356,7 +1400,7 @@ function enclosing3(nodes, line) {
1356
1400
  return best;
1357
1401
  }
1358
1402
  function normalize2(p) {
1359
- return path17.resolve(p).split(path17.sep).join("/");
1403
+ return path19.resolve(p).split(path19.sep).join("/");
1360
1404
  }
1361
1405
 
1362
1406
  // src/engine/epistemic.ts
@@ -1599,14 +1643,14 @@ var IMPORTANCE = {
1599
1643
  "other-config": 0.08
1600
1644
  };
1601
1645
  function toPosix2(p) {
1602
- return p.split(path17.sep).join("/");
1646
+ return p.split(path19.sep).join("/");
1603
1647
  }
1604
1648
  function buildRootIgnore2(root, exclude) {
1605
1649
  const ig = ignore();
1606
- const gitignorePath = path17.join(root, ".gitignore");
1607
- if (fs15.existsSync(gitignorePath)) {
1650
+ const gitignorePath = path19.join(root, ".gitignore");
1651
+ if (fs18.existsSync(gitignorePath)) {
1608
1652
  try {
1609
- ig.add(fs15.readFileSync(gitignorePath, "utf8"));
1653
+ ig.add(fs18.readFileSync(gitignorePath, "utf8"));
1610
1654
  } catch {
1611
1655
  }
1612
1656
  }
@@ -1615,7 +1659,7 @@ function buildRootIgnore2(root, exclude) {
1615
1659
  }
1616
1660
  function classifyProjectContext(relOrName) {
1617
1661
  const rel2 = relOrName.replace(/\\/g, "/");
1618
- const base = path17.basename(rel2);
1662
+ const base = path19.basename(rel2);
1619
1663
  const baseLower = base.toLowerCase();
1620
1664
  if (/^\.env($|\.)/i.test(base) && !isEnvExampleName(baseLower)) return null;
1621
1665
  if (SKIP_FILES.has(baseLower)) return null;
@@ -1628,7 +1672,7 @@ function classifyProjectContext(relOrName) {
1628
1672
  for (const { re, category } of PATH_PATTERNS) {
1629
1673
  if (re.test(rel2)) return category;
1630
1674
  }
1631
- const ext = path17.extname(baseLower);
1675
+ const ext = path19.extname(baseLower);
1632
1676
  if (DOC_EXTENSIONS.has(ext)) return ext === ".txt" ? "text" : "markdown";
1633
1677
  if (API_EXTENSIONS.has(ext)) return "api-contract";
1634
1678
  if ((ext === ".yaml" || ext === ".yml" || ext === ".json") && /openapi|swagger|asyncapi/i.test(base)) {
@@ -1646,24 +1690,24 @@ function isEnvExampleName(baseLower) {
1646
1690
  return false;
1647
1691
  }
1648
1692
  function discoverDocs(options) {
1649
- const root = path17.resolve(options.root);
1693
+ const root = path19.resolve(options.root);
1650
1694
  const maxFiles = options.maxFiles ?? DOC_MAX_FILES;
1651
1695
  const rootIg = buildRootIgnore2(root, options.exclude ?? []);
1652
- const scopeAbs = (options.paths?.length ? options.paths.map((p) => path17.resolve(root, p)) : [root]).filter(
1653
- (p) => fs15.existsSync(p)
1696
+ const scopeAbs = (options.paths?.length ? options.paths.map((p) => path19.resolve(root, p)) : [root]).filter(
1697
+ (p) => fs18.existsSync(p)
1654
1698
  );
1655
1699
  const found = /* @__PURE__ */ new Map();
1656
1700
  const consider = (abs) => {
1657
1701
  if (found.size >= maxFiles) return;
1658
- const rel2 = toPosix2(path17.relative(root, abs));
1702
+ const rel2 = toPosix2(path19.relative(root, abs));
1659
1703
  if (!rel2 || rel2.startsWith("..")) return;
1660
1704
  if (rootIg.ignores(rel2)) return;
1661
- const base = path17.basename(abs);
1705
+ const base = path19.basename(abs);
1662
1706
  if (SKIP_FILES.has(base.toLowerCase())) return;
1663
1707
  const category = classifyProjectContext(rel2);
1664
1708
  if (!category) return;
1665
1709
  try {
1666
- const st = fs15.statSync(abs);
1710
+ const st = fs18.statSync(abs);
1667
1711
  if (!st.isFile() || st.size > DOC_FILE_MAX_BYTES) return;
1668
1712
  if (st.size === 0) return;
1669
1713
  } catch {
@@ -1675,14 +1719,14 @@ function discoverDocs(options) {
1675
1719
  if (found.size >= maxFiles || depth > 12) return;
1676
1720
  let entries;
1677
1721
  try {
1678
- entries = fs15.readdirSync(dir, { withFileTypes: true });
1722
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
1679
1723
  } catch {
1680
1724
  return;
1681
1725
  }
1682
1726
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1683
1727
  if (found.size >= maxFiles) return;
1684
- const abs = path17.join(dir, entry.name);
1685
- const rel2 = toPosix2(path17.relative(root, abs));
1728
+ const abs = path19.join(dir, entry.name);
1729
+ const rel2 = toPosix2(path19.relative(root, abs));
1686
1730
  if (entry.isDirectory()) {
1687
1731
  if (SKIP_DIRS.has(entry.name)) continue;
1688
1732
  if (rel2 && rootIg.ignores(`${rel2}/`) && !rel2.startsWith(".github")) continue;
@@ -1694,7 +1738,7 @@ function discoverDocs(options) {
1694
1738
  };
1695
1739
  for (const scope of scopeAbs) {
1696
1740
  try {
1697
- const st = fs15.statSync(scope);
1741
+ const st = fs18.statSync(scope);
1698
1742
  if (st.isDirectory()) walk(scope, 0);
1699
1743
  else if (st.isFile()) consider(scope);
1700
1744
  } catch {
@@ -1705,7 +1749,7 @@ function discoverDocs(options) {
1705
1749
  function prepareDocBody(rel2, raw, category) {
1706
1750
  const scrubbed = redactSecrets(raw);
1707
1751
  if (scrubbed.length <= DOC_BODY_MAX_CHARS) return scrubbed;
1708
- if (category === "manifest" && path17.basename(rel2).toLowerCase() === "package.json") {
1752
+ if (category === "manifest" && path19.basename(rel2).toLowerCase() === "package.json") {
1709
1753
  return summarizePackageJson(scrubbed);
1710
1754
  }
1711
1755
  if (category === "api-contract" && /\.(ya?ml|json)$/i.test(rel2)) {
@@ -1751,14 +1795,14 @@ function documentNodesFromDocs(docs) {
1751
1795
  for (const d of docs) {
1752
1796
  let raw = "";
1753
1797
  try {
1754
- raw = fs15.readFileSync(d.abs, "utf8");
1798
+ raw = fs18.readFileSync(d.abs, "utf8");
1755
1799
  } catch {
1756
1800
  continue;
1757
1801
  }
1758
1802
  const category = d.category ?? d.kind;
1759
1803
  const scrubbed = prepareDocBody(d.rel, raw, category);
1760
1804
  if (!scrubbed.trim()) continue;
1761
- const name = path17.basename(d.rel);
1805
+ const name = path19.basename(d.rel);
1762
1806
  const qualifiedName = d.rel.replace(/\\/g, "/");
1763
1807
  const id = nodeId({
1764
1808
  kind: "document",
@@ -1787,17 +1831,362 @@ function documentNodesFromDocs(docs) {
1787
1831
  }
1788
1832
  return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id));
1789
1833
  }
1834
+ function shardTsProjects(root, files) {
1835
+ if (files.length === 0) return [];
1836
+ const cache = /* @__PURE__ */ new Map();
1837
+ const byKey = /* @__PURE__ */ new Map();
1838
+ for (const f of files) {
1839
+ const dir = path19.dirname(f.abs);
1840
+ const key = nearestProjectKey(root, dir, cache);
1841
+ const list = byKey.get(key);
1842
+ if (list) list.push(f);
1843
+ else byKey.set(key, [f]);
1844
+ }
1845
+ return [...byKey.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([key, shardFiles]) => ({
1846
+ key,
1847
+ files: [...shardFiles].sort((a, b) => a.rel.localeCompare(b.rel))
1848
+ }));
1849
+ }
1850
+ function nearestProjectKey(root, startDir, cache) {
1851
+ const hit = cache.get(startDir);
1852
+ if (hit !== void 0) return hit;
1853
+ let dir = startDir;
1854
+ const rootAbs = path19.resolve(root);
1855
+ while (true) {
1856
+ if (fs18.existsSync(path19.join(dir, "tsconfig.json")) || fs18.existsSync(path19.join(dir, "tsconfig.base.json")) || fs18.existsSync(path19.join(dir, "package.json"))) {
1857
+ const key = toPosix3(path19.relative(rootAbs, dir)) || ".";
1858
+ cache.set(startDir, key);
1859
+ return key;
1860
+ }
1861
+ if (dir === rootAbs || path19.dirname(dir) === dir) {
1862
+ cache.set(startDir, ".");
1863
+ return ".";
1864
+ }
1865
+ dir = path19.dirname(dir);
1866
+ }
1867
+ }
1868
+ function toPosix3(p) {
1869
+ return p.split(path19.sep).join("/");
1870
+ }
1871
+
1872
+ // src/engine/timing.ts
1873
+ var StageTimer = class {
1874
+ marks = /* @__PURE__ */ new Map();
1875
+ durations = {};
1876
+ start(name) {
1877
+ this.marks.set(name, nowMs());
1878
+ }
1879
+ end(name) {
1880
+ const t0 = this.marks.get(name);
1881
+ if (t0 === void 0) return 0;
1882
+ const ms = Math.round((nowMs() - t0) * 1e3) / 1e3;
1883
+ this.durations[name] = ms;
1884
+ this.marks.delete(name);
1885
+ return ms;
1886
+ }
1887
+ /** Run `fn` under a named stage. */
1888
+ async measure(name, fn) {
1889
+ this.start(name);
1890
+ try {
1891
+ return await fn();
1892
+ } finally {
1893
+ this.end(name);
1894
+ }
1895
+ }
1896
+ snapshot() {
1897
+ return { ...this.durations };
1898
+ }
1899
+ };
1900
+ function nowMs() {
1901
+ return Number(process.hrtime.bigint() / 1000n) / 1e3;
1902
+ }
1903
+
1904
+ // src/engine/summaries.ts
1905
+ var DEPEND_EDGES = /* @__PURE__ */ new Set(["call", "import", "extends", "implements", "references"]);
1906
+ var HUB_LIMIT = 40;
1907
+ function buildSummaries(graph) {
1908
+ const index = indexFor(graph);
1909
+ const candidates = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external" && n.kind !== "document").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, HUB_LIMIT);
1910
+ const hubs = candidates.map((n) => {
1911
+ const d1 = reverseDependents(index, n.id, 1);
1912
+ const d2 = reverseDependents(index, n.id, 2);
1913
+ const files = /* @__PURE__ */ new Set();
1914
+ for (const id of d2) {
1915
+ const node = index.node(id);
1916
+ if (node?.file) files.add(node.file);
1917
+ }
1918
+ return {
1919
+ id: n.id,
1920
+ name: n.qualifiedName,
1921
+ kind: n.kind,
1922
+ file: n.file,
1923
+ importance: n.importance,
1924
+ direct: d1.size,
1925
+ depth2: d2.size,
1926
+ files: files.size,
1927
+ tested: n.tested
1928
+ };
1929
+ });
1930
+ hubs.sort((a, b) => b.importance - a.importance || a.name.localeCompare(b.name));
1931
+ return { hubs };
1932
+ }
1933
+ function reverseDependents(index, rootId, maxDepth) {
1934
+ const affected = /* @__PURE__ */ new Set();
1935
+ let frontier = [rootId];
1936
+ for (let depth = 1; depth <= maxDepth; depth++) {
1937
+ const next = [];
1938
+ for (const id of frontier) {
1939
+ for (const e of index.in(id)) {
1940
+ if (!DEPEND_EDGES.has(e.kind)) continue;
1941
+ if (e.src === rootId || affected.has(e.src)) continue;
1942
+ affected.add(e.src);
1943
+ next.push(e.src);
1944
+ }
1945
+ }
1946
+ frontier = next;
1947
+ if (frontier.length === 0) break;
1948
+ }
1949
+ return affected;
1950
+ }
1951
+ var emptyNode = (partial) => ({
1952
+ ...partial,
1953
+ span: partial.span ?? { start: 1, end: 1 },
1954
+ importance: 0,
1955
+ centrality: { degree: 0, pagerank: 0, betweenness: 0, eigenvector: 0 },
1956
+ area: -1,
1957
+ isHub: false,
1958
+ tested: null
1959
+ });
1960
+ function extractManifests(root, opts = {}) {
1961
+ const absRoot = path19.resolve(root);
1962
+ const ig = buildRootIgnore3(absRoot, opts.exclude ?? []);
1963
+ const scopes = (opts.paths?.length ? opts.paths : ["."]).map((p) => path19.resolve(absRoot, p)).filter((p) => fs18.existsSync(p));
1964
+ const found = /* @__PURE__ */ new Map();
1965
+ for (const scope of scopes) {
1966
+ walkManifests(absRoot, scope, ig, found);
1967
+ }
1968
+ const nodes = /* @__PURE__ */ new Map();
1969
+ const edges = /* @__PURE__ */ new Map();
1970
+ let deps = 0;
1971
+ for (const rel2 of [...found.keys()].sort()) {
1972
+ const abs = found.get(rel2);
1973
+ const base = path19.posix.basename(rel2);
1974
+ try {
1975
+ if (base === "package.json") {
1976
+ deps += ingestPackageJson(rel2, abs, nodes, edges);
1977
+ } else if (base === "go.mod") {
1978
+ deps += ingestGoMod(rel2, abs, nodes, edges);
1979
+ }
1980
+ } catch {
1981
+ }
1982
+ }
1983
+ return {
1984
+ nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
1985
+ edges: [...edges.values()].sort(
1986
+ (a, b) => a.kind.localeCompare(b.kind) || a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst)
1987
+ ),
1988
+ files: found.size,
1989
+ deps
1990
+ };
1991
+ }
1992
+ function ingestPackageJson(rel2, abs, nodes, edges) {
1993
+ const raw = JSON.parse(fs18.readFileSync(abs, "utf8"));
1994
+ const pkgName = raw.name && String(raw.name).trim() || path19.posix.dirname(rel2) || ".";
1995
+ const localId = nodeId({ kind: "package", qualifiedName: pkgName, file: rel2 });
1996
+ nodes.set(
1997
+ localId,
1998
+ emptyNode({
1999
+ id: localId,
2000
+ kind: "package",
2001
+ name: pkgName.includes("/") ? pkgName.split("/").pop() : pkgName,
2002
+ qualifiedName: pkgName,
2003
+ file: rel2,
2004
+ lang: "json"
2005
+ })
2006
+ );
2007
+ const fileId = nodeId({ kind: "file", qualifiedName: rel2, file: rel2 });
2008
+ nodes.set(
2009
+ fileId,
2010
+ emptyNode({
2011
+ id: fileId,
2012
+ kind: "file",
2013
+ name: path19.posix.basename(rel2),
2014
+ qualifiedName: rel2,
2015
+ file: rel2,
2016
+ lang: "json"
2017
+ })
2018
+ );
2019
+ addEdge(edges, "contains", fileId, localId, 1);
2020
+ let n = 0;
2021
+ const depMaps = [raw.dependencies, raw.peerDependencies, raw.optionalDependencies];
2022
+ const names = /* @__PURE__ */ new Set();
2023
+ for (const m of depMaps) {
2024
+ if (!m || typeof m !== "object") continue;
2025
+ for (const name of Object.keys(m)) {
2026
+ if (name) names.add(name);
2027
+ }
2028
+ }
2029
+ for (const name of [...names].sort()) {
2030
+ const extId = ensureExternal(nodes, name);
2031
+ addEdge(edges, "import", localId, extId, 1);
2032
+ n++;
2033
+ }
2034
+ return n;
2035
+ }
2036
+ function ingestGoMod(rel2, abs, nodes, edges) {
2037
+ const text = fs18.readFileSync(abs, "utf8");
2038
+ let moduleName = path19.posix.dirname(rel2) || ".";
2039
+ const moduleMatch = /^\s*module\s+(\S+)/m.exec(text);
2040
+ if (moduleMatch) moduleName = moduleMatch[1];
2041
+ const localId = nodeId({ kind: "package", qualifiedName: moduleName, file: rel2 });
2042
+ nodes.set(
2043
+ localId,
2044
+ emptyNode({
2045
+ id: localId,
2046
+ kind: "package",
2047
+ name: moduleName.split("/").pop() ?? moduleName,
2048
+ qualifiedName: moduleName,
2049
+ file: rel2,
2050
+ lang: "go"
2051
+ })
2052
+ );
2053
+ const fileId = nodeId({ kind: "file", qualifiedName: rel2, file: rel2 });
2054
+ nodes.set(
2055
+ fileId,
2056
+ emptyNode({
2057
+ id: fileId,
2058
+ kind: "file",
2059
+ name: path19.posix.basename(rel2),
2060
+ qualifiedName: rel2,
2061
+ file: rel2,
2062
+ lang: "go"
2063
+ })
2064
+ );
2065
+ addEdge(edges, "contains", fileId, localId, 1);
2066
+ const reqNames = /* @__PURE__ */ new Set();
2067
+ const block = /require\s*\(([\s\S]*?)\)/g;
2068
+ let bm;
2069
+ while ((bm = block.exec(text)) !== null) {
2070
+ for (const line of bm[1].split("\n")) {
2071
+ const m = /^\s*(\S+)\s+v\S+/.exec(line);
2072
+ if (m && !line.trim().startsWith("//")) reqNames.add(m[1]);
2073
+ }
2074
+ }
2075
+ const single = /^\s*require\s+(\S+)\s+v\S+/gm;
2076
+ let sm;
2077
+ while ((sm = single.exec(text)) !== null) reqNames.add(sm[1]);
2078
+ let n = 0;
2079
+ for (const name of [...reqNames].sort()) {
2080
+ const extId = ensureExternal(nodes, name);
2081
+ addEdge(edges, "import", localId, extId, 1);
2082
+ n++;
2083
+ }
2084
+ return n;
2085
+ }
2086
+ function ensureExternal(nodes, name) {
2087
+ const id = nodeId({ kind: "external", qualifiedName: name, file: "" });
2088
+ if (!nodes.has(id)) {
2089
+ nodes.set(
2090
+ id,
2091
+ emptyNode({
2092
+ id,
2093
+ kind: "external",
2094
+ name,
2095
+ qualifiedName: name,
2096
+ file: "",
2097
+ lang: "external"
2098
+ })
2099
+ );
2100
+ }
2101
+ return id;
2102
+ }
2103
+ function addEdge(edges, kind, src, dst, confidence) {
2104
+ if (src === dst) return;
2105
+ const id = edgeId(kind, src, dst);
2106
+ if (edges.has(id)) return;
2107
+ edges.set(id, {
2108
+ id,
2109
+ kind,
2110
+ src,
2111
+ dst,
2112
+ resolution: "heuristic",
2113
+ confidence,
2114
+ epistemic: "declared",
2115
+ count: 1
2116
+ });
2117
+ }
2118
+ function buildRootIgnore3(root, exclude) {
2119
+ const ig = ignore();
2120
+ const gitignorePath = path19.join(root, ".gitignore");
2121
+ if (fs18.existsSync(gitignorePath)) {
2122
+ try {
2123
+ ig.add(fs18.readFileSync(gitignorePath, "utf8"));
2124
+ } catch {
2125
+ }
2126
+ }
2127
+ if (exclude.length) ig.add(exclude);
2128
+ return ig;
2129
+ }
2130
+ function walkManifests(root, dir, ig, found) {
2131
+ let entries;
2132
+ try {
2133
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
2134
+ } catch {
2135
+ return;
2136
+ }
2137
+ for (const entry of entries) {
2138
+ const abs = path19.join(dir, entry.name);
2139
+ const rel2 = path19.relative(root, abs).split(path19.sep).join("/");
2140
+ if (entry.isDirectory()) {
2141
+ if (SKIP_DIRS.has(entry.name)) continue;
2142
+ if (rel2 && ig.ignores(`${rel2}/`)) continue;
2143
+ walkManifests(root, abs, ig, found);
2144
+ } else if (entry.isFile()) {
2145
+ const base = entry.name;
2146
+ if (base !== "package.json" && base !== "go.mod") continue;
2147
+ if (rel2 && ig.ignores(rel2)) continue;
2148
+ found.set(rel2, abs);
2149
+ }
2150
+ }
2151
+ }
2152
+ async function hashFilesParallel(jobs, concurrency) {
2153
+ if (jobs.length === 0) return [];
2154
+ const limit = Math.max(
2155
+ 1,
2156
+ Math.min(concurrency ?? Math.max(1, (os.cpus()?.length ?? 4) * 2), jobs.length)
2157
+ );
2158
+ const out = new Array(jobs.length);
2159
+ let next = 0;
2160
+ async function worker() {
2161
+ for (; ; ) {
2162
+ const i = next++;
2163
+ if (i >= jobs.length) return;
2164
+ const job = jobs[i];
2165
+ try {
2166
+ const buf = await fs8.readFile(job.abs);
2167
+ out[i] = { rel: job.rel, hash: hashBytes(buf), ok: true };
2168
+ } catch {
2169
+ out[i] = { rel: job.rel, hash: "", ok: false };
2170
+ }
2171
+ }
2172
+ }
2173
+ await Promise.all(Array.from({ length: limit }, () => worker()));
2174
+ return out;
2175
+ }
1790
2176
 
1791
2177
  // src/engine/build.ts
1792
2178
  async function buildGraph(options) {
1793
- const start = nowMs();
1794
- const root = path17.resolve(options.root);
2179
+ const timer = new StageTimer();
2180
+ timer.start("total");
2181
+ const root = path19.resolve(options.root);
2182
+ timer.start("discover");
1795
2183
  const files = discover({
1796
2184
  root,
1797
2185
  only: options.only,
1798
2186
  exclude: options.exclude,
1799
2187
  paths: options.paths
1800
2188
  });
2189
+ timer.end("discover");
1801
2190
  const limits = resolveLimits(options.limits);
1802
2191
  if (limits.maxFiles > 0 && files.length > limits.maxFiles) {
1803
2192
  throw new ResourceLimitError(
@@ -1811,15 +2200,18 @@ async function buildGraph(options) {
1811
2200
  grammars,
1812
2201
  disabled: options.noCache
1813
2202
  });
2203
+ timer.start("hash");
1814
2204
  const hashes = /* @__PURE__ */ new Map();
1815
2205
  const fileStats = [];
1816
2206
  const toParse = [];
1817
2207
  const reused = [];
1818
2208
  const buildWarnings = [];
2209
+ let statHits = 0;
2210
+ const pendingHash = [];
1819
2211
  for (const file of files) {
1820
2212
  let stat;
1821
2213
  try {
1822
- stat = fs15.statSync(file.abs);
2214
+ stat = fs18.statSync(file.abs);
1823
2215
  } catch {
1824
2216
  continue;
1825
2217
  }
@@ -1835,18 +2227,47 @@ async function buildGraph(options) {
1835
2227
  );
1836
2228
  continue;
1837
2229
  }
1838
- let hash = "";
1839
- try {
1840
- hash = hashBytes(fs15.readFileSync(file.abs));
1841
- } catch {
2230
+ const byStat = options.noCache ? void 0 : cache.getByStat(file.rel, stat.mtimeMs, stat.size);
2231
+ if (byStat) {
2232
+ hashes.set(file.rel, byStat.hash);
2233
+ if (byStat.parse.hash !== byStat.hash) byStat.parse.hash = byStat.hash;
2234
+ fileStats.push({
2235
+ rel: file.rel,
2236
+ size: stat.size,
2237
+ mtimeMs: stat.mtimeMs,
2238
+ hash: byStat.hash
2239
+ });
2240
+ reused.push(byStat.parse);
2241
+ statHits++;
1842
2242
  continue;
1843
2243
  }
1844
- hashes.set(file.rel, hash);
1845
- fileStats.push({ rel: file.rel, size: stat.size, mtimeMs: stat.mtimeMs, hash });
1846
- const cached2 = cache.get(file.rel, hash);
1847
- if (cached2) reused.push(cached2);
1848
- else toParse.push(file);
2244
+ pendingHash.push({ file, size: stat.size, mtimeMs: stat.mtimeMs });
2245
+ }
2246
+ if (pendingHash.length) {
2247
+ const jobs = pendingHash.map((p) => ({ rel: p.file.rel, abs: p.file.abs }));
2248
+ const results = await hashFilesParallel(jobs, options.jobs);
2249
+ const byRel = new Map(results.map((r) => [r.rel, r]));
2250
+ for (const p of pendingHash) {
2251
+ const r = byRel.get(p.file.rel);
2252
+ if (!r || !r.ok) continue;
2253
+ hashes.set(p.file.rel, r.hash);
2254
+ fileStats.push({
2255
+ rel: p.file.rel,
2256
+ size: p.size,
2257
+ mtimeMs: p.mtimeMs,
2258
+ hash: r.hash
2259
+ });
2260
+ const cached2 = cache.get(p.file.rel, r.hash);
2261
+ if (cached2) {
2262
+ reused.push(cached2);
2263
+ cache.set(p.file.rel, cached2, { mtimeMs: p.mtimeMs, size: p.size });
2264
+ } else {
2265
+ toParse.push(p.file);
2266
+ }
2267
+ }
1849
2268
  }
2269
+ timer.end("hash");
2270
+ timer.start("parse");
1850
2271
  const parsedNew = await parseFiles(toParse, {
1851
2272
  jobs: options.jobs,
1852
2273
  inline: options.inline,
@@ -1854,8 +2275,12 @@ async function buildGraph(options) {
1854
2275
  grammarsDir: options.grammarsDir,
1855
2276
  memoryBudgetMb: limits.memoryBudgetMb
1856
2277
  });
2278
+ timer.end("parse");
1857
2279
  checkMemoryBudget("parse", limits.memoryBudgetMb);
1858
- for (const p of parsedNew) cache.set(p.rel, p);
2280
+ for (const p of parsedNew) {
2281
+ const st = fileStats.find((f) => f.rel === p.rel);
2282
+ cache.set(p.rel, p, st ? { mtimeMs: st.mtimeMs, size: st.size } : void 0);
2283
+ }
1859
2284
  const currentRels = new Set(files.map((f) => f.rel));
1860
2285
  cache.prune(currentRels);
1861
2286
  cache.save();
@@ -1863,31 +2288,85 @@ async function buildGraph(options) {
1863
2288
  (a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0
1864
2289
  );
1865
2290
  const warnings = [...buildWarnings, ...parses.flatMap((p) => p.warnings ?? [])];
2291
+ timer.start("resolve");
1866
2292
  const moduleResolver = buildModuleResolver(root, new Set(parses.map((p) => p.rel)));
1867
2293
  const resolved = resolve(parses, moduleResolver);
2294
+ timer.end("resolve");
1868
2295
  checkMemoryBudget("resolve", limits.memoryBudgetMb);
2296
+ const manifests = extractManifests(root, {
2297
+ exclude: options.exclude,
2298
+ paths: options.paths
2299
+ });
2300
+ if (manifests.files > 0) {
2301
+ const byId = new Map(resolved.nodes.map((n) => [n.id, n]));
2302
+ for (const n of manifests.nodes) {
2303
+ if (!byId.has(n.id)) {
2304
+ byId.set(n.id, n);
2305
+ resolved.nodes.push(n);
2306
+ }
2307
+ }
2308
+ const edgeById = new Map(resolved.edges.map((e) => [e.id, e]));
2309
+ for (const e of manifests.edges) {
2310
+ if (!edgeById.has(e.id)) {
2311
+ edgeById.set(e.id, e);
2312
+ resolved.edges.push(e);
2313
+ }
2314
+ }
2315
+ resolved.nodes.sort((a, b) => a.id.localeCompare(b.id));
2316
+ resolved.edges.sort(
2317
+ (a, b) => a.kind.localeCompare(b.kind) || a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst)
2318
+ );
2319
+ }
1869
2320
  const nodeFileById = new Map(resolved.nodes.map((n) => [n.id, n.file]));
1870
2321
  let edges = resolved.edges;
1871
2322
  const resolvers = [...resolved.stats.resolvers];
1872
2323
  const preciseCoveredFiles = /* @__PURE__ */ new Set();
1873
2324
  let tscStats;
1874
- 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 }));
2325
+ const skipTsc = options.noTsc || options.fast;
2326
+ 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 }));
1875
2327
  if (limits.tscMaxFiles > 0 && tsFiles.length > limits.tscMaxFiles) {
1876
2328
  warnings.push(
1877
2329
  `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)`
1878
2330
  );
1879
2331
  tsFiles = [];
1880
2332
  }
2333
+ timer.start("tsc");
1881
2334
  if (tsFiles.length) {
1882
- const res = tsResolveEdges(root, tsFiles, resolved.nodes);
1883
- if (res.stats.files > 0) {
1884
- edges = mergePreciseEdges(edges, res.edges, res.coveredFiles, nodeFileById);
1885
- for (const f of res.coveredFiles) preciseCoveredFiles.add(f);
2335
+ const shards = shardTsProjects(root, tsFiles);
2336
+ let filesCovered = 0;
2337
+ let calls = 0;
2338
+ let jsx = 0;
2339
+ let heritage = 0;
2340
+ let resolvedCount = 0;
2341
+ const coveredAll = /* @__PURE__ */ new Set();
2342
+ const preciseAll = [];
2343
+ for (const shard of shards) {
2344
+ const res = tsResolveEdges(root, shard.files, resolved.nodes);
2345
+ if (res.stats.files === 0) continue;
2346
+ filesCovered += res.stats.files;
2347
+ calls += res.stats.calls;
2348
+ jsx += res.stats.jsx;
2349
+ heritage += res.stats.heritage;
2350
+ resolvedCount += res.stats.resolved;
2351
+ for (const f of res.coveredFiles) coveredAll.add(f);
2352
+ preciseAll.push(...res.edges);
2353
+ }
2354
+ if (filesCovered > 0) {
2355
+ edges = mergePreciseEdges(edges, preciseAll, coveredAll, nodeFileById);
2356
+ for (const f of coveredAll) preciseCoveredFiles.add(f);
1886
2357
  if (!resolvers.includes("tsc")) resolvers.unshift("tsc");
1887
- tscStats = res.stats;
2358
+ tscStats = {
2359
+ files: filesCovered,
2360
+ calls,
2361
+ jsx,
2362
+ heritage,
2363
+ resolved: resolvedCount,
2364
+ shards: shards.length
2365
+ };
1888
2366
  }
1889
2367
  checkMemoryBudget("typescript resolution", limits.memoryBudgetMb);
1890
2368
  }
2369
+ timer.end("tsc");
1891
2370
  const scip = options.noScip ? null : loadScipIndex(root, options.scip);
1892
2371
  let scipStats;
1893
2372
  if (scip) {
@@ -1908,8 +2387,8 @@ async function buildGraph(options) {
1908
2387
  });
1909
2388
  for (const d of docs) {
1910
2389
  try {
1911
- const st = fs15.statSync(d.abs);
1912
- const hash = hashBytes(fs15.readFileSync(d.abs));
2390
+ const st = fs18.statSync(d.abs);
2391
+ const hash = hashBytes(fs18.readFileSync(d.abs));
1913
2392
  hashes.set(d.rel, hash);
1914
2393
  fileStats.push({ rel: d.rel, size: st.size, mtimeMs: st.mtimeMs, hash });
1915
2394
  } catch {
@@ -1917,7 +2396,12 @@ async function buildGraph(options) {
1917
2396
  }
1918
2397
  const docNodes = documentNodesFromDocs(docs);
1919
2398
  if (docNodes.length) nodes = [...nodes, ...docNodes];
1920
- const analysis = analyze(nodes, linked.edges, { cluster: options.cluster });
2399
+ timer.start("analyze");
2400
+ const analysis = analyze(nodes, linked.edges, {
2401
+ cluster: options.cluster,
2402
+ tier: options.analysisTier
2403
+ });
2404
+ timer.end("analyze");
1921
2405
  checkMemoryBudget("analysis", limits.memoryBudgetMb);
1922
2406
  const languages = [.../* @__PURE__ */ new Set([...parses.map((p) => p.lang), ...docNodes.map((n) => n.lang)])].sort();
1923
2407
  const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
@@ -1945,8 +2429,9 @@ async function buildGraph(options) {
1945
2429
  toolchain
1946
2430
  },
1947
2431
  meta: {
1948
- root: path17.basename(root) === "" ? "." : ".",
2432
+ root: path19.basename(root) === "" ? "." : ".",
1949
2433
  languages,
2434
+ analysisTier: analysis.tier,
1950
2435
  counts: {
1951
2436
  nodes: analysis.nodes.length,
1952
2437
  edges: analysis.edges.length,
@@ -1964,7 +2449,8 @@ async function buildGraph(options) {
1964
2449
  const survivingIds = new Set(analysis.nodes.map((n) => n.id));
1965
2450
  const unknowns = resolved.unresolved.filter((u) => !preciseCoveredFiles.has(u.fromRel) && survivingIds.has(u.from)).map((u) => ({ from: u.from, name: u.name, kind: u.kind, count: u.count }));
1966
2451
  if (unknowns.length) graph.unknowns = unknowns;
1967
- if (options.deep) {
2452
+ graph.summaries = buildSummaries(graph);
2453
+ {
1968
2454
  const facts = buildFacts(parses, analysis.nodes, analysis.edges);
1969
2455
  if (facts.length) graph.facts = facts;
1970
2456
  }
@@ -1972,16 +2458,27 @@ async function buildGraph(options) {
1972
2458
  const grounding = groundGraph(analysis.nodes, analysis.edges, parses);
1973
2459
  if (grounding.length) graph.grounding = grounding;
1974
2460
  }
2461
+ let indexResult;
2462
+ if (!options.noIndex) {
2463
+ timer.start("index");
2464
+ const wr = writeGraphIndex(root, graph);
2465
+ indexResult = { ok: wr.ok, path: wr.ok ? wr.path : void 0, reason: wr.reason };
2466
+ timer.end("index");
2467
+ }
2468
+ timer.end("total");
2469
+ const stages = timer.snapshot();
1975
2470
  return {
1976
2471
  graph,
1977
- timing: { totalMs: Math.round((nowMs() - start) * 1e3) / 1e3 },
2472
+ timing: { totalMs: stages.total ?? 0, stages },
1978
2473
  reparsed: parsedNew.length,
1979
2474
  reused: reused.length,
2475
+ statHits,
1980
2476
  totalFiles: files.length,
1981
2477
  fileStats,
1982
2478
  resolveStats: resolved.stats,
1983
2479
  tsc: tscStats,
1984
2480
  scip: scipStats,
2481
+ index: indexResult,
1985
2482
  warnings
1986
2483
  };
1987
2484
  }
@@ -1991,14 +2488,14 @@ function toRepoRel(p) {
1991
2488
  function loadScipIndex(root, explicit) {
1992
2489
  const candidates = [
1993
2490
  explicit,
1994
- path17.join(root, "index.scip"),
1995
- path17.join(root, ".vibgrate", "index.scip")
2491
+ path19.join(root, "index.scip"),
2492
+ path19.join(root, ".vibgrate", "index.scip")
1996
2493
  ].filter((p) => Boolean(p));
1997
2494
  for (const file of candidates) {
1998
- const abs = path17.isAbsolute(file) ? file : path17.resolve(root, file);
1999
- if (!fs15.existsSync(abs)) continue;
2495
+ const abs = path19.isAbsolute(file) ? file : path19.resolve(root, file);
2496
+ if (!fs18.existsSync(abs)) continue;
2000
2497
  try {
2001
- const index = decodeScipIndex(new Uint8Array(fs15.readFileSync(abs)));
2498
+ const index = decodeScipIndex(new Uint8Array(fs18.readFileSync(abs)));
2002
2499
  if (index.documents.length) {
2003
2500
  return { index, tool: index.toolVersion ? `${index.toolName} ${index.toolVersion}` : index.toolName };
2004
2501
  }
@@ -2047,9 +2544,6 @@ function computeCorpusHash(parses, hashes) {
2047
2544
  const list = [...byRel.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
2048
2545
  return hashString(canonicalize(list));
2049
2546
  }
2050
- function nowMs() {
2051
- return Number(process.hrtime.bigint() / 1000n) / 1e3;
2052
- }
2053
2547
 
2054
2548
  // src/engine/verify.ts
2055
2549
  var PINNED = "1970-01-01T00:00:00.000Z";
@@ -2116,21 +2610,21 @@ function usageError(message) {
2116
2610
  }
2117
2611
  var SNAPSHOT_VERSION = "vg-freshness/1";
2118
2612
  function snapshotPath(root) {
2119
- return path17.join(cacheDir(root), "freshness.json");
2613
+ return path19.join(cacheDir(root), "freshness.json");
2120
2614
  }
2121
2615
  function writeSnapshot(root, corpusHash, fileStats, scope = {}) {
2122
2616
  const files = {};
2123
2617
  for (const f of fileStats) files[f.rel] = { size: f.size, mtimeMs: f.mtimeMs, hash: f.hash };
2124
2618
  const snapshot = { version: SNAPSHOT_VERSION, corpusHash, scope: pruneScope(scope), files };
2125
2619
  try {
2126
- fs15.mkdirSync(cacheDir(root), { recursive: true });
2127
- fs15.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2620
+ fs18.mkdirSync(cacheDir(root), { recursive: true });
2621
+ fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2128
2622
  } catch {
2129
2623
  }
2130
2624
  }
2131
2625
  function loadSnapshot(root) {
2132
2626
  try {
2133
- const loaded = JSON.parse(fs15.readFileSync(snapshotPath(root), "utf8"));
2627
+ const loaded = JSON.parse(fs18.readFileSync(snapshotPath(root), "utf8"));
2134
2628
  if (loaded.version === SNAPSHOT_VERSION && loaded.files && typeof loaded.corpusHash === "string") {
2135
2629
  return { ...loaded, scope: loaded.scope ?? {} };
2136
2630
  }
@@ -2150,7 +2644,14 @@ function probeFreshness(root) {
2150
2644
  const { scope } = snapshot;
2151
2645
  let discovered;
2152
2646
  try {
2153
- discovered = discover({ root, only: scope.only, exclude: scope.exclude, paths: scope.paths });
2647
+ const code = discover({ root, only: scope.only, exclude: scope.exclude, paths: scope.paths });
2648
+ const docs = discoverDocs({ root, exclude: scope.exclude, paths: scope.paths });
2649
+ const byRel = /* @__PURE__ */ new Map();
2650
+ for (const f of code) byRel.set(f.rel, f.abs);
2651
+ for (const d of docs) {
2652
+ if (!byRel.has(d.rel)) byRel.set(d.rel, d.abs);
2653
+ }
2654
+ discovered = [...byRel.entries()].map(([rel2, abs]) => ({ rel: rel2, abs })).sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
2154
2655
  } catch {
2155
2656
  return null;
2156
2657
  }
@@ -2166,15 +2667,25 @@ function probeFreshness(root) {
2166
2667
  }
2167
2668
  let stat;
2168
2669
  try {
2169
- stat = fs15.statSync(file.abs);
2670
+ stat = fs18.statSync(file.abs);
2170
2671
  } catch {
2171
2672
  drift.removed.push(file.rel);
2172
2673
  continue;
2173
2674
  }
2174
2675
  if (stat.size === entry.size && stat.mtimeMs === entry.mtimeMs) continue;
2676
+ if (entry.hash.startsWith("vg:oversize:")) {
2677
+ const expected = `vg:oversize:${stat.size}`;
2678
+ if (entry.hash === expected) {
2679
+ snapshot.files[file.rel] = { size: stat.size, mtimeMs: stat.mtimeMs, hash: entry.hash };
2680
+ absorbed = true;
2681
+ } else {
2682
+ drift.changed.push(file.rel);
2683
+ }
2684
+ continue;
2685
+ }
2175
2686
  let hash = "";
2176
2687
  try {
2177
- hash = hashBytes(fs15.readFileSync(file.abs));
2688
+ hash = hashBytes(fs18.readFileSync(file.abs));
2178
2689
  } catch {
2179
2690
  drift.removed.push(file.rel);
2180
2691
  continue;
@@ -2191,7 +2702,7 @@ function probeFreshness(root) {
2191
2702
  }
2192
2703
  if (absorbed && !hasDrift(drift)) {
2193
2704
  try {
2194
- fs15.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2705
+ fs18.writeFileSync(snapshotPath(root), stableStringify(snapshot, 0));
2195
2706
  } catch {
2196
2707
  }
2197
2708
  }
@@ -2237,6 +2748,30 @@ function shortestPath(graph, srcId, dstId) {
2237
2748
  if (reverse) return { ids: reverse.slice().reverse(), direction: "reverse" };
2238
2749
  return null;
2239
2750
  }
2751
+ var NEIGHBOR_CAP = 6;
2752
+ function pathDisconnect(graph, srcId, dstId) {
2753
+ const idx = indexFor(graph);
2754
+ const from = neighborhood(idx, srcId);
2755
+ const to = neighborhood(idx, dstId);
2756
+ const hint = from.calls.length || from.calledBy.length || to.calls.length || to.calledBy.length ? "No direct call/import chain links these symbols. Try `vg show` / `vg impact` on each end, or path via a shared hub." : "Neither endpoint has call/import neighbors in the map \u2014 they may be isolated docs/externals, or the link needs a more precise build.";
2757
+ return { connected: false, from, to, hint };
2758
+ }
2759
+ function neighborhood(idx, id) {
2760
+ const node = idx.node(id);
2761
+ const name = node?.qualifiedName ?? id;
2762
+ const calls = uniqueNames(idx.callees(id).map((x) => x.node.qualifiedName));
2763
+ const calledBy = uniqueNames(idx.callers(id).map((x) => x.node.qualifiedName));
2764
+ const imports = uniqueNames(
2765
+ idx.out(id, "import").map((e) => idx.node(e.dst)?.qualifiedName).filter((n) => !!n)
2766
+ );
2767
+ const importedBy = uniqueNames(
2768
+ idx.in(id, "import").map((e) => idx.node(e.src)?.qualifiedName).filter((n) => !!n)
2769
+ );
2770
+ return { name, calls, calledBy, imports, importedBy };
2771
+ }
2772
+ function uniqueNames(names) {
2773
+ return [...new Set(names)].sort((a, b) => a.localeCompare(b)).slice(0, NEIGHBOR_CAP);
2774
+ }
2240
2775
  function coveringTests(graph, node, index) {
2241
2776
  const idx = index ?? indexFor(graph);
2242
2777
  const byId = idx.nodeById;
@@ -2272,10 +2807,10 @@ function testsToRun(graph, rootId, depth = 4) {
2272
2807
  };
2273
2808
  }
2274
2809
  function detectRunner(root, lang) {
2275
- const pkgPath = path17.join(root, "package.json");
2276
- if (fs15.existsSync(pkgPath)) {
2810
+ const pkgPath = path19.join(root, "package.json");
2811
+ if (fs18.existsSync(pkgPath)) {
2277
2812
  try {
2278
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
2813
+ const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf8"));
2279
2814
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2280
2815
  if (deps.vitest) return { name: "vitest", command: (f) => `npx vitest run ${f.join(" ")}`.trim() };
2281
2816
  if (deps.jest) return { name: "jest", command: (f) => `npx jest ${f.join(" ")}`.trim() };
@@ -2283,10 +2818,10 @@ function detectRunner(root, lang) {
2283
2818
  } catch {
2284
2819
  }
2285
2820
  }
2286
- if (lang === "py" || fs15.existsSync(path17.join(root, "pyproject.toml")) || fs15.existsSync(path17.join(root, "pytest.ini"))) {
2821
+ if (lang === "py" || fs18.existsSync(path19.join(root, "pyproject.toml")) || fs18.existsSync(path19.join(root, "pytest.ini"))) {
2287
2822
  return { name: "pytest", command: (f) => `pytest ${f.join(" ")}`.trim() };
2288
2823
  }
2289
- if (lang === "go" || fs15.existsSync(path17.join(root, "go.mod"))) {
2824
+ if (lang === "go" || fs18.existsSync(path19.join(root, "go.mod"))) {
2290
2825
  return { name: "go test", command: () => "go test ./..." };
2291
2826
  }
2292
2827
  if (lang === "cs" || hasFile(root, /\.csproj$/) || hasFile(root, /\.sln$/)) {
@@ -2297,16 +2832,16 @@ function detectRunner(root, lang) {
2297
2832
  }
2298
2833
  function hasFile(root, re) {
2299
2834
  try {
2300
- return fs15.readdirSync(root).some((f) => re.test(f));
2835
+ return fs18.readdirSync(root).some((f) => re.test(f));
2301
2836
  } catch {
2302
2837
  return false;
2303
2838
  }
2304
2839
  }
2305
2840
  function findGitRoot(startDir = process.cwd()) {
2306
- let dir = path17.resolve(startDir);
2841
+ let dir = path19.resolve(startDir);
2307
2842
  for (; ; ) {
2308
- if (fs15.existsSync(path17.join(dir, ".git"))) return dir;
2309
- const parent = path17.dirname(dir);
2843
+ if (fs18.existsSync(path19.join(dir, ".git"))) return dir;
2844
+ const parent = path19.dirname(dir);
2310
2845
  if (parent === dir) return null;
2311
2846
  dir = parent;
2312
2847
  }
@@ -2317,19 +2852,19 @@ function normalizeEntry(line) {
2317
2852
  function ensureGitignored(entry, startDir = process.cwd()) {
2318
2853
  const root = findGitRoot(startDir);
2319
2854
  if (!root) return { status: "not-a-repo", entry };
2320
- const gitignorePath = path17.join(root, ".gitignore");
2855
+ const gitignorePath = path19.join(root, ".gitignore");
2321
2856
  const target = normalizeEntry(entry);
2322
2857
  let existing = "";
2323
2858
  let fileExisted = true;
2324
2859
  try {
2325
- existing = fs15.readFileSync(gitignorePath, "utf8");
2860
+ existing = fs18.readFileSync(gitignorePath, "utf8");
2326
2861
  } catch {
2327
2862
  fileExisted = false;
2328
2863
  }
2329
2864
  const alreadyPresent = existing.split(/\r?\n/).some((line) => normalizeEntry(line) === target);
2330
2865
  if (alreadyPresent) return { status: "present", entry, gitignorePath };
2331
2866
  const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n");
2332
- fs15.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
2867
+ fs18.appendFileSync(gitignorePath, `${needsLeadingNewline ? "\n" : ""}${entry}
2333
2868
  `, "utf8");
2334
2869
  return { status: fileExisted ? "added" : "created", entry, gitignorePath };
2335
2870
  }
@@ -2483,14 +3018,14 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
2483
3018
  console.log(chalk.dim("The secret must be registered on your Vibgrate ingest API."));
2484
3019
  }
2485
3020
  if (opts.write) {
2486
- const writePath = path17.resolve(opts.write);
3021
+ const writePath = path19.resolve(opts.write);
2487
3022
  await writeTextFile(writePath, dsn + "\n");
2488
3023
  console.log("");
2489
3024
  console.log(chalk.green("\u2714") + ` DSN written to ${opts.write}`);
2490
- const root = findGitRoot(path17.dirname(writePath));
2491
- const rel2 = root ? path17.relative(root, writePath).split(path17.sep).join("/") : "";
3025
+ const root = findGitRoot(path19.dirname(writePath));
3026
+ const rel2 = root ? path19.relative(root, writePath).split(path19.sep).join("/") : "";
2492
3027
  if (root && rel2 && !rel2.startsWith("..")) {
2493
- const res = ensureGitignored(rel2, path17.dirname(writePath));
3028
+ const res = ensureGitignored(rel2, path19.dirname(writePath));
2494
3029
  if (res.status === "created") {
2495
3030
  console.log(chalk.green("\u2714") + ` Created .gitignore and ignored ${rel2}`);
2496
3031
  } else if (res.status === "added") {
@@ -2508,30 +3043,30 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
2508
3043
  var STORE_DIRNAME = ".vibgrate";
2509
3044
  var STORE_FILENAME = "credentials.json";
2510
3045
  function homeCredentialsPath() {
2511
- return path17.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
3046
+ return path19.join(os.homedir(), STORE_DIRNAME, STORE_FILENAME);
2512
3047
  }
2513
3048
  function projectCredentialsPath(cwd = process.cwd()) {
2514
3049
  const root = findGitRoot(cwd) ?? cwd;
2515
- return path17.join(root, STORE_DIRNAME, STORE_FILENAME);
3050
+ return path19.join(root, STORE_DIRNAME, STORE_FILENAME);
2516
3051
  }
2517
3052
  function credentialsPath(opts = {}) {
2518
3053
  const override = process.env.VIBGRATE_CREDENTIALS?.trim();
2519
- if (override) return path17.resolve(override);
3054
+ if (override) return path19.resolve(override);
2520
3055
  const local = projectCredentialsPath(opts.cwd);
2521
3056
  if (opts.local) return local;
2522
- if (fs15.existsSync(local)) return local;
3057
+ if (fs18.existsSync(local)) return local;
2523
3058
  return homeCredentialsPath();
2524
3059
  }
2525
3060
  function gitignoreEntryForCredentials(repoRoot, credsFile = credentialsPath()) {
2526
- const rel2 = path17.relative(repoRoot, credsFile);
2527
- if (rel2 && !rel2.startsWith("..") && !path17.isAbsolute(rel2)) {
2528
- return rel2.split(path17.sep).join("/");
3061
+ const rel2 = path19.relative(repoRoot, credsFile);
3062
+ if (rel2 && !rel2.startsWith("..") && !path19.isAbsolute(rel2)) {
3063
+ return rel2.split(path19.sep).join("/");
2529
3064
  }
2530
3065
  return `${STORE_DIRNAME}/${STORE_FILENAME}`;
2531
3066
  }
2532
3067
  function readStoredCredentials(opts = {}) {
2533
3068
  try {
2534
- const raw = fs15.readFileSync(credentialsPath(opts), "utf8");
3069
+ const raw = fs18.readFileSync(credentialsPath(opts), "utf8");
2535
3070
  const parsed = JSON.parse(raw);
2536
3071
  return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
2537
3072
  } catch {
@@ -2540,16 +3075,16 @@ function readStoredCredentials(opts = {}) {
2540
3075
  }
2541
3076
  function writeStoredCredentials(creds, opts = {}) {
2542
3077
  const file = credentialsPath(opts);
2543
- fs15.mkdirSync(path17.dirname(file), { recursive: true });
2544
- fs15.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
3078
+ fs18.mkdirSync(path19.dirname(file), { recursive: true });
3079
+ fs18.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf8", mode: 384 });
2545
3080
  try {
2546
- fs15.chmodSync(file, 384);
3081
+ fs18.chmodSync(file, 384);
2547
3082
  } catch {
2548
3083
  }
2549
3084
  }
2550
3085
  function clearStoredCredentials(opts = {}) {
2551
3086
  try {
2552
- fs15.rmSync(credentialsPath(opts));
3087
+ fs18.rmSync(credentialsPath(opts));
2553
3088
  return true;
2554
3089
  } catch {
2555
3090
  return false;
@@ -2920,7 +3455,7 @@ var pushCommand = new Command("push").description("Push scan results to Vibgrate
2920
3455
  if (opts.strict) process.exit(1);
2921
3456
  return;
2922
3457
  }
2923
- const filePath = path17.resolve(opts.file);
3458
+ const filePath = path19.resolve(opts.file);
2924
3459
  if (!await pathExists(filePath)) {
2925
3460
  console.error(chalk.red(`Scan artifact not found: ${filePath}`));
2926
3461
  console.error(chalk.dim('Run "vibgrate scan" first.'));
@@ -3015,7 +3550,7 @@ function measure(root, files) {
3015
3550
  for (const rel2 of files) {
3016
3551
  let size;
3017
3552
  try {
3018
- size = fs15.statSync(path17.join(root, rel2)).size;
3553
+ size = fs18.statSync(path19.join(root, rel2)).size;
3019
3554
  } catch {
3020
3555
  continue;
3021
3556
  }
@@ -3038,9 +3573,9 @@ function matchLines(text, needleLower) {
3038
3573
  }
3039
3574
  function readCandidate(abs, knownSize) {
3040
3575
  try {
3041
- const size = knownSize ?? fs15.statSync(abs).size;
3576
+ const size = knownSize ?? fs18.statSync(abs).size;
3042
3577
  if (size > MAX_FILE_BYTES) return null;
3043
- const text = fs15.readFileSync(abs, "utf8");
3578
+ const text = fs18.readFileSync(abs, "utf8");
3044
3579
  return text.includes(NUL) ? null : text;
3045
3580
  } catch {
3046
3581
  return null;
@@ -3051,7 +3586,7 @@ function scanInline(root, files, needleLower, cap, stopAtCap, sizes) {
3051
3586
  let total = 0;
3052
3587
  let truncated = false;
3053
3588
  for (const rel2 of files) {
3054
- const text = readCandidate(path17.join(root, rel2), sizes?.get(rel2));
3589
+ const text = readCandidate(path19.join(root, rel2), sizes?.get(rel2));
3055
3590
  if (text === null) continue;
3056
3591
  for (const h of matchLines(text, needleLower)) {
3057
3592
  total++;
@@ -3118,12 +3653,12 @@ var WORKER_SOURCE = [
3118
3653
  "parentPort.postMessage({ hits, total, truncated });"
3119
3654
  ].join("\n");
3120
3655
  function runWorker(root, files, needleLower) {
3121
- return new Promise((resolve11, reject) => {
3656
+ return new Promise((resolve13, reject) => {
3122
3657
  const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { root, files, needleLower } });
3123
3658
  let settled = false;
3124
3659
  w.once("message", (m) => {
3125
3660
  settled = true;
3126
- resolve11(m);
3661
+ resolve13(m);
3127
3662
  });
3128
3663
  w.once("error", reject);
3129
3664
  w.once("exit", (code) => {
@@ -3192,10 +3727,10 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
3192
3727
  ".pdf"
3193
3728
  ]);
3194
3729
  function hasBinaryExt(rel2) {
3195
- return BINARY_EXTS.has(path17.extname(rel2).toLowerCase());
3730
+ return BINARY_EXTS.has(path19.extname(rel2).toLowerCase());
3196
3731
  }
3197
- function toPosix3(rel2) {
3198
- return path17.sep === "\\" ? rel2.replaceAll("\\", "/") : rel2;
3732
+ function toPosix4(rel2) {
3733
+ return path19.sep === "\\" ? rel2.replaceAll("\\", "/") : rel2;
3199
3734
  }
3200
3735
  async function searchSymbols(graph, root, query, limit) {
3201
3736
  let q = query.trim();
@@ -3334,13 +3869,13 @@ function walkCandidates(root) {
3334
3869
  const dir = stack.pop();
3335
3870
  let entries;
3336
3871
  try {
3337
- entries = fs15.readdirSync(dir, { withFileTypes: true });
3872
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
3338
3873
  } catch {
3339
3874
  continue;
3340
3875
  }
3341
3876
  for (const e of entries) {
3342
3877
  if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
3343
- const abs = path17.join(dir, e.name);
3878
+ const abs = path19.join(dir, e.name);
3344
3879
  if (e.isDirectory()) {
3345
3880
  stack.push(abs);
3346
3881
  continue;
@@ -3350,7 +3885,7 @@ function walkCandidates(root) {
3350
3885
  truncated = true;
3351
3886
  continue;
3352
3887
  }
3353
- files.push(toPosix3(path17.relative(root, abs)));
3888
+ files.push(toPosix4(path19.relative(root, abs)));
3354
3889
  }
3355
3890
  }
3356
3891
  files.sort();
@@ -3381,12 +3916,12 @@ function ripgrepCandidates(root, needle) {
3381
3916
  if (files.length > MAX_FILES_SCANNED) return { files: files.slice(0, MAX_FILES_SCANNED), truncated: true };
3382
3917
  return { files, truncated: false };
3383
3918
  }
3384
- function parseRgFileList(stdout, sep8 = path17.sep) {
3919
+ function parseRgFileList(stdout, sep10 = path19.sep) {
3385
3920
  const seen = /* @__PURE__ */ new Set();
3386
3921
  for (const raw of stdout.split("\n")) {
3387
3922
  if (!raw) continue;
3388
- const posix = sep8 === "\\" ? raw.replaceAll("\\", "/") : raw;
3389
- const rel2 = posix.startsWith("./") ? posix.slice(2) : posix;
3923
+ const posix2 = sep10 === "\\" ? raw.replaceAll("\\", "/") : raw;
3924
+ const rel2 = posix2.startsWith("./") ? posix2.slice(2) : posix2;
3390
3925
  if (!rel2) continue;
3391
3926
  const segs = rel2.split("/");
3392
3927
  if (segs.some((s) => s.startsWith(".") || IGNORE_DIRS.has(s))) continue;
@@ -3397,8 +3932,8 @@ function parseRgFileList(stdout, sep8 = path17.sep) {
3397
3932
  }
3398
3933
  function readScanArtifact(root) {
3399
3934
  try {
3400
- const file = path17.join(root, ".vibgrate", "scan_result.json");
3401
- return JSON.parse(fs15.readFileSync(file, "utf8"));
3935
+ const file = path19.join(root, ".vibgrate", "scan_result.json");
3936
+ return JSON.parse(fs18.readFileSync(file, "utf8"));
3402
3937
  } catch {
3403
3938
  return null;
3404
3939
  }
@@ -3788,7 +4323,7 @@ var CACHE_VERSION = "vg-hosted-docs/1";
3788
4323
  var HOSTED_DOCS_TTL_MS = 24 * 60 * 60 * 1e3;
3789
4324
  var MAX_ENTRIES = 64;
3790
4325
  function cachePath(root) {
3791
- return path17.join(cacheDir(root), "hosted-docs.json");
4326
+ return path19.join(cacheDir(root), "hosted-docs.json");
3792
4327
  }
3793
4328
  function hostedDocsCacheKey(req, opts = {}) {
3794
4329
  const material = JSON.stringify([
@@ -3804,7 +4339,7 @@ function hostedDocsCacheKey(req, opts = {}) {
3804
4339
  }
3805
4340
  function loadFile(root) {
3806
4341
  try {
3807
- const parsed = JSON.parse(fs15.readFileSync(cachePath(root), "utf8"));
4342
+ const parsed = JSON.parse(fs18.readFileSync(cachePath(root), "utf8"));
3808
4343
  if (parsed && parsed.version === CACHE_VERSION && parsed.entries && typeof parsed.entries === "object") return parsed;
3809
4344
  } catch {
3810
4345
  }
@@ -3818,8 +4353,8 @@ function saveFile(root, file) {
3818
4353
  delete file.entries[k];
3819
4354
  }
3820
4355
  }
3821
- fs15.mkdirSync(cacheDir(root), { recursive: true });
3822
- fs15.writeFileSync(cachePath(root), JSON.stringify(file));
4356
+ fs18.mkdirSync(cacheDir(root), { recursive: true });
4357
+ fs18.writeFileSync(cachePath(root), JSON.stringify(file));
3823
4358
  } catch {
3824
4359
  }
3825
4360
  }
@@ -4107,11 +4642,11 @@ var TOOLS = [
4107
4642
  area: node.area
4108
4643
  };
4109
4644
  if (ctx?.dedup && ctx.seen?.has(node.id)) {
4110
- return { ...base, repeat: true, callsTotal: uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames(callerRecs.map((x) => x.node.qualifiedName)).length };
4645
+ return { ...base, repeat: true, callsTotal: uniqueNames2(calleeRecs.map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames2(callerRecs.map((x) => x.node.qualifiedName)).length };
4111
4646
  }
4112
4647
  const edgeCap = isDetailed(args) ? NODE_EDGE_CAP : CONCISE_MATCHES;
4113
- const calls = boundList(uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)), edgeCap);
4114
- const calledBy = boundList(uniqueNames(callerRecs.map((x) => x.node.qualifiedName)), edgeCap);
4648
+ const calls = boundList(uniqueNames2(calleeRecs.map((x) => x.node.qualifiedName)), edgeCap);
4649
+ const calledBy = boundList(uniqueNames2(callerRecs.map((x) => x.node.qualifiedName)), edgeCap);
4115
4650
  ctx?.seen?.add(node.id);
4116
4651
  return {
4117
4652
  ...base,
@@ -4140,7 +4675,7 @@ var TOOLS = [
4140
4675
  if (!ra.node) return { endpoint: "a", ...unresolved(ra.candidates) };
4141
4676
  if (!rb.node) return { endpoint: "b", ...unresolved(rb.candidates) };
4142
4677
  const result = shortestPath(graph, ra.node.id, rb.node.id);
4143
- if (!result) return { connected: false };
4678
+ if (!result) return pathDisconnect(graph, ra.node.id, rb.node.id);
4144
4679
  const byId = new Map(graph.nodes.map((n) => [n.id, n]));
4145
4680
  return { connected: true, direction: result.direction, path: result.ids.map((id) => byId.get(id)?.qualifiedName ?? id) };
4146
4681
  }
@@ -4233,18 +4768,47 @@ var TOOLS = [
4233
4768
  },
4234
4769
  {
4235
4770
  name: "list_hubs",
4236
- description: "Most-depended-on symbols.",
4771
+ description: "Most-depended-on symbols (includes precomputed blast-radius counts when available).",
4237
4772
  inputSchema: obj({ limit: { type: "number" } }),
4238
- handler: (graph, args) => graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance).slice(0, numOr(args.limit, 20)).map((n) => ({ name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start, importance: n.importance, isHub: n.isHub }))
4773
+ handler: (graph, args) => {
4774
+ const limit = numOr(args.limit, 20);
4775
+ if (graph.summaries?.hubs?.length) {
4776
+ return graph.summaries.hubs.slice(0, limit).map((h) => ({
4777
+ name: h.name,
4778
+ kind: h.kind,
4779
+ file: h.file,
4780
+ importance: h.importance,
4781
+ isHub: true,
4782
+ directDependents: h.direct,
4783
+ depth2Dependents: h.depth2,
4784
+ filesAffected: h.files,
4785
+ tested: h.tested
4786
+ }));
4787
+ }
4788
+ return graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance).slice(0, limit).map((n) => ({
4789
+ name: n.qualifiedName,
4790
+ kind: n.kind,
4791
+ file: n.file,
4792
+ line: n.span.start,
4793
+ importance: n.importance,
4794
+ isHub: n.isHub
4795
+ }));
4796
+ }
4239
4797
  },
4240
4798
  {
4241
4799
  name: "get_facts",
4242
- description: "Deterministic facts for a node (contract/invariant); needs a --deep build.",
4800
+ description: "Deterministic facts for a node (contract / invariant / characterization).",
4243
4801
  inputSchema: obj({ name: { type: "string" } }, ["name"]),
4244
4802
  handler: (graph, args) => {
4245
4803
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""));
4246
4804
  if (!node) return unresolved(candidates);
4247
- if (!graph.facts) return { node: node.qualifiedName, facts: [], note: "facts require a --deep build" };
4805
+ if (!graph.facts) {
4806
+ return {
4807
+ node: node.qualifiedName,
4808
+ facts: [],
4809
+ note: "map has no facts \u2014 rebuild with a current vg (facts ship on every build)"
4810
+ };
4811
+ }
4248
4812
  return { node: node.qualifiedName, facts: graph.facts.filter((f) => f.subjectIds.includes(node.id)) };
4249
4813
  }
4250
4814
  },
@@ -4536,14 +5100,22 @@ function conciseSummary(graph) {
4536
5100
  return { counts: graph.meta.counts, languages: graph.meta.languages };
4537
5101
  }
4538
5102
  function summarize(graph, top = 10) {
5103
+ const topHubs = graph.summaries?.hubs?.length ? graph.summaries.hubs.slice(0, top).map((h) => ({
5104
+ name: h.name,
5105
+ file: h.file,
5106
+ importance: h.importance,
5107
+ directDependents: h.direct,
5108
+ depth2Dependents: h.depth2
5109
+ })) : graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, top).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }));
4539
5110
  return {
4540
5111
  counts: graph.meta.counts,
4541
5112
  languages: graph.meta.languages,
4542
5113
  cluster: graph.meta.cluster,
5114
+ analysisTier: graph.meta.analysisTier ?? "full",
4543
5115
  resolver: graph.provenance.resolver,
4544
5116
  generatedAt: graph.generatedAt,
4545
5117
  topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, top).map((a) => ({ id: a.id, label: a.label, size: a.size })),
4546
- topHubs: graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, top).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }))
5118
+ topHubs
4547
5119
  };
4548
5120
  }
4549
5121
  function stripId(item) {
@@ -4571,7 +5143,7 @@ function numOrU(v) {
4571
5143
  const n = Number(v);
4572
5144
  return Number.isFinite(n) ? n : void 0;
4573
5145
  }
4574
- function uniqueNames(xs) {
5146
+ function uniqueNames2(xs) {
4575
5147
  return [...new Set(xs)];
4576
5148
  }
4577
5149
  var HOT_TOOLS = ["orient", "search_symbols", "query_graph", "get_node"];
@@ -4794,12 +5366,12 @@ function detectAssistants(root, opts = {}) {
4794
5366
  const which = opts.which ?? whichOnPath;
4795
5367
  const found = [];
4796
5368
  for (const assistant of ASSISTANTS) {
4797
- const repoHit = (assistant.markers ?? []).find((m) => fs15.existsSync(path17.join(root, m)));
5369
+ const repoHit = (assistant.markers ?? []).find((m) => fs18.existsSync(path19.join(root, m)));
4798
5370
  if (repoHit) {
4799
5371
  found.push({ assistant, via: "repo", marker: repoHit });
4800
5372
  continue;
4801
5373
  }
4802
- const homeHit = (assistant.homeMarkers ?? []).find((m) => fs15.existsSync(path17.join(home, m)));
5374
+ const homeHit = (assistant.homeMarkers ?? []).find((m) => fs18.existsSync(path19.join(home, m)));
4803
5375
  if (homeHit) {
4804
5376
  found.push({ assistant, via: "home", marker: homeHit });
4805
5377
  continue;
@@ -4831,19 +5403,19 @@ function installAssistant(a, opts) {
4831
5403
  const skipped = [];
4832
5404
  let note;
4833
5405
  if (a.skill) {
4834
- writeFileEnsured(path17.join(opts.root, a.skill), skillMarkdown(a.id));
5406
+ writeFileEnsured(path19.join(opts.root, a.skill), skillMarkdown(a.id));
4835
5407
  wrote.push(a.skill);
4836
5408
  }
4837
5409
  if (a.mcp) {
4838
5410
  const launch = opts.launch ?? detectServeLaunch();
4839
- upsertMcp(path17.join(opts.root, a.mcp.file), a.mcp, launch);
5411
+ upsertMcp(path19.join(opts.root, a.mcp.file), a.mcp, launch);
4840
5412
  wrote.push(a.mcp.file);
4841
5413
  note = launch.note;
4842
5414
  } else {
4843
5415
  skipped.push("mcp (host-specific setup)");
4844
5416
  }
4845
5417
  if (a.nudge && opts.hook !== false) {
4846
- writeNudge(path17.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
5418
+ writeNudge(path19.join(opts.root, a.nudge.file), a.nudge, opts.smallRepo, a.id);
4847
5419
  wrote.push(a.nudge.file);
4848
5420
  } else if (a.nudge) {
4849
5421
  skipped.push("nudge (--no-hook)");
@@ -4853,41 +5425,41 @@ function installAssistant(a, opts) {
4853
5425
  function uninstallAssistant(a, root, purge) {
4854
5426
  const removed = [];
4855
5427
  if (a.mcp) {
4856
- const file = path17.join(root, a.mcp.file);
5428
+ const file = path19.join(root, a.mcp.file);
4857
5429
  if (removeMcp(file, a.mcp)) removed.push(a.mcp.file);
4858
5430
  }
4859
5431
  if (a.nudge) {
4860
- const file = path17.join(root, a.nudge.file);
5432
+ const file = path19.join(root, a.nudge.file);
4861
5433
  if (a.nudge.kind === "block") {
4862
5434
  if (removeBlock(file)) removed.push(a.nudge.file);
4863
- } else if (fs15.existsSync(file)) {
4864
- fs15.rmSync(file);
5435
+ } else if (fs18.existsSync(file)) {
5436
+ fs18.rmSync(file);
4865
5437
  removed.push(a.nudge.file);
4866
5438
  }
4867
5439
  }
4868
5440
  if (purge && a.skill) {
4869
- const file = path17.join(root, a.skill);
4870
- if (fs15.existsSync(file)) {
4871
- fs15.rmSync(file);
5441
+ const file = path19.join(root, a.skill);
5442
+ if (fs18.existsSync(file)) {
5443
+ fs18.rmSync(file);
4872
5444
  removed.push(a.skill);
4873
5445
  }
4874
5446
  }
4875
5447
  return removed;
4876
5448
  }
4877
5449
  function writeFileEnsured(file, content) {
4878
- fs15.mkdirSync(path17.dirname(file), { recursive: true });
4879
- fs15.writeFileSync(file, content);
5450
+ fs18.mkdirSync(path19.dirname(file), { recursive: true });
5451
+ fs18.writeFileSync(file, content);
4880
5452
  }
4881
5453
  function writeNavigationConfig(root) {
4882
5454
  ensureVibgrateGitignore(root);
4883
- const rel2 = path17.join(".vibgrate", "mcp-navigation.json");
5455
+ const rel2 = path19.join(".vibgrate", "mcp-navigation.json");
4884
5456
  const doc = {
4885
5457
  _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.",
4886
5458
  hot_core: [...HOT_TOOLS],
4887
5459
  deferred: deferredToolNames(),
4888
5460
  toolset: navigationToolsetConfig()
4889
5461
  };
4890
- writeFileEnsured(path17.join(root, rel2), `${JSON.stringify(doc, null, 2)}
5462
+ writeFileEnsured(path19.join(root, rel2), `${JSON.stringify(doc, null, 2)}
4891
5463
  `);
4892
5464
  return rel2;
4893
5465
  }
@@ -4902,7 +5474,7 @@ function upsertMcp(file, target, launch) {
4902
5474
  `);
4903
5475
  }
4904
5476
  function removeMcp(file, target) {
4905
- if (!fs15.existsSync(file)) return false;
5477
+ if (!fs18.existsSync(file)) return false;
4906
5478
  const config = readJson(file);
4907
5479
  const bag = config[target.key];
4908
5480
  if (!bag || !(bag.vg !== void 0)) return false;
@@ -4921,8 +5493,8 @@ function refreshInstalledInstructions(root, smallRepo) {
4921
5493
  };
4922
5494
  for (const a of ASSISTANTS) {
4923
5495
  if (a.skill) {
4924
- const file = path17.join(root, a.skill);
4925
- if (fs15.existsSync(file)) {
5496
+ const file = path19.join(root, a.skill);
5497
+ if (fs18.existsSync(file)) {
4926
5498
  const text = readTextSafe(file);
4927
5499
  const from = text === null ? null : staleVersion(text, "# vg \u2014 the code map");
4928
5500
  if (from !== null) {
@@ -4932,8 +5504,8 @@ function refreshInstalledInstructions(root, smallRepo) {
4932
5504
  }
4933
5505
  }
4934
5506
  if (a.nudge) {
4935
- const file = path17.join(root, a.nudge.file);
4936
- if (!fs15.existsSync(file)) continue;
5507
+ const file = path19.join(root, a.nudge.file);
5508
+ if (!fs18.existsSync(file)) continue;
4937
5509
  const text = readTextSafe(file);
4938
5510
  if (text === null) continue;
4939
5511
  if (a.nudge.kind === "block") {
@@ -4957,7 +5529,7 @@ function refreshInstalledInstructions(root, smallRepo) {
4957
5529
  }
4958
5530
  function readTextSafe(file) {
4959
5531
  try {
4960
- return fs15.readFileSync(file, "utf8");
5532
+ return fs18.readFileSync(file, "utf8");
4961
5533
  } catch {
4962
5534
  return null;
4963
5535
  }
@@ -4976,8 +5548,8 @@ ${stripMarkers(nudgeMarkdown(smallRepo, client))}` : stripMarkers(nudgeMarkdown(
4976
5548
  upsertBlock(file, nudgeMarkdown(smallRepo, client));
4977
5549
  }
4978
5550
  function upsertBlock(file, block) {
4979
- fs15.mkdirSync(path17.dirname(file), { recursive: true });
4980
- let existing = fs15.existsSync(file) ? fs15.readFileSync(file, "utf8") : "";
5551
+ fs18.mkdirSync(path19.dirname(file), { recursive: true });
5552
+ let existing = fs18.existsSync(file) ? fs18.readFileSync(file, "utf8") : "";
4981
5553
  const re = new RegExp(`${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}`);
4982
5554
  if (re.test(existing)) existing = existing.replace(re, block);
4983
5555
  else existing = existing.length ? `${existing.replace(/\s*$/, "")}
@@ -4985,20 +5557,20 @@ function upsertBlock(file, block) {
4985
5557
  ${block}
4986
5558
  ` : `${block}
4987
5559
  `;
4988
- fs15.writeFileSync(file, existing);
5560
+ fs18.writeFileSync(file, existing);
4989
5561
  }
4990
5562
  function removeBlock(file) {
4991
- if (!fs15.existsSync(file)) return false;
4992
- const existing = fs15.readFileSync(file, "utf8");
5563
+ if (!fs18.existsSync(file)) return false;
5564
+ const existing = fs18.readFileSync(file, "utf8");
4993
5565
  const re = new RegExp(`\\n*${escapeRe(NUDGE_BEGIN)}[\\s\\S]*?${escapeRe(NUDGE_END)}\\n*`);
4994
5566
  if (!re.test(existing)) return false;
4995
- fs15.writeFileSync(file, existing.replace(re, "\n"));
5567
+ fs18.writeFileSync(file, existing.replace(re, "\n"));
4996
5568
  return true;
4997
5569
  }
4998
5570
  function readJson(file) {
4999
- if (!fs15.existsSync(file)) return {};
5571
+ if (!fs18.existsSync(file)) return {};
5000
5572
  try {
5001
- const parsed = JSON.parse(fs15.readFileSync(file, "utf8"));
5573
+ const parsed = JSON.parse(fs18.readFileSync(file, "utf8"));
5002
5574
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5003
5575
  return parsed;
5004
5576
  }
@@ -5018,7 +5590,7 @@ function escapeRe(s) {
5018
5590
  }
5019
5591
  var REFRESH_LOCK_STALE_MS = 10 * 60 * 1e3;
5020
5592
  function refreshLockPath(root) {
5021
- return path17.join(cacheDir(root), "refresh.lock");
5593
+ return path19.join(cacheDir(root), "refresh.lock");
5022
5594
  }
5023
5595
  async function refreshIfStale(root, opts = {}) {
5024
5596
  const probe = probeFreshness(root);
@@ -5050,8 +5622,8 @@ async function refreshIfStale(root, opts = {}) {
5050
5622
  writeArtifacts(result.graph, {
5051
5623
  root,
5052
5624
  graphPath: opts.graphPath,
5053
- report: fs15.existsSync(path17.join(dir, "GRAPH_REPORT.md")),
5054
- html: fs15.existsSync(path17.join(dir, "graph.html"))
5625
+ report: fs18.existsSync(path19.join(dir, "GRAPH_REPORT.md")),
5626
+ html: fs18.existsSync(path19.join(dir, "graph.html"))
5055
5627
  });
5056
5628
  }
5057
5629
  writeSnapshot(root, result.graph.provenance.corpusHash, result.fileStats, scope);
@@ -5071,6 +5643,6 @@ async function refreshIfStale(root, opts = {}) {
5071
5643
  }
5072
5644
  }
5073
5645
 
5074
- export { ASSISTANTS, CliError, ExitCode, FREE_PACK, NPX_INVOCATION, ResourceLimitError, SCHEMA_VERSION, 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 };
5075
- //# sourceMappingURL=chunk-S6CYDZEW.js.map
5076
- //# sourceMappingURL=chunk-S6CYDZEW.js.map
5646
+ 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, pathDisconnect, 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 };
5647
+ //# sourceMappingURL=chunk-MAY5HLDH.js.map
5648
+ //# sourceMappingURL=chunk-MAY5HLDH.js.map