@vibgrate/cli 2026.704.1 → 2026.708.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { langById, shortId, canonicalize, grammarSetVersion, hashString, hashBytes, langForExtension, setGrammarsOverride, parseSource } from './chunk-X5YT263H.js';
2
- import { writeTextFile, pathExists, readJsonFile, VERSION, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem } from './chunk-RJHYTD62.js';
1
+ import { hashString, langById, shortId, canonicalize, grammarSetVersion, hashBytes, langForExtension, setGrammarsOverride, parseSource } from './chunk-X5YT263H.js';
2
+ import { writeTextFile, pathExists, readJsonFile, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-SRWMZBLG.js';
3
3
  import * as fs18 from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
5
  import * as path18 from 'path';
@@ -1624,6 +1624,7 @@ function resolve4(parses, resolver) {
1624
1624
  }
1625
1625
  }
1626
1626
  const edges = new EdgeSet();
1627
+ const unresolved2 = /* @__PURE__ */ new Map();
1627
1628
  const stats = {
1628
1629
  callsResolved: 0,
1629
1630
  callsUnresolved: 0,
@@ -1672,6 +1673,7 @@ function resolve4(parses, resolver) {
1672
1673
  stats.callsResolved++;
1673
1674
  } else {
1674
1675
  stats.callsUnresolved++;
1676
+ bumpUnresolved(unresolved2, srcId, call.callee, "call", p.rel);
1675
1677
  }
1676
1678
  }
1677
1679
  }
@@ -1683,6 +1685,7 @@ function resolve4(parses, resolver) {
1683
1685
  if (!srcId) continue;
1684
1686
  const target = resolveType(h.superName, p.rel, p.lang, imported, defsByName);
1685
1687
  if (target) edges.add(h.kind, srcId, target.id, "heuristic", 0.85);
1688
+ else bumpUnresolved(unresolved2, srcId, h.superName, h.kind, p.rel);
1686
1689
  }
1687
1690
  }
1688
1691
  const nodes = [...baseNodes.values()].map((n) => ({
@@ -1693,7 +1696,16 @@ function resolve4(parses, resolver) {
1693
1696
  isHub: false,
1694
1697
  tested: null
1695
1698
  }));
1696
- return { nodes, edges: edges.toArray(), stats };
1699
+ const unresolvedList = [...unresolved2.values()].sort(
1700
+ (a, b) => a.from.localeCompare(b.from) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
1701
+ );
1702
+ return { nodes, edges: edges.toArray(), unresolved: unresolvedList, stats };
1703
+ }
1704
+ function bumpUnresolved(map, from, name, kind, fromRel) {
1705
+ const key = `${from}\0${kind}\0${name}`;
1706
+ const existing = map.get(key);
1707
+ if (existing) existing.count++;
1708
+ else map.set(key, { from, name, kind, count: 1, fromRel });
1697
1709
  }
1698
1710
  function addNode(map, n) {
1699
1711
  if (!map.has(n.id)) map.set(n.id, n);
@@ -2036,6 +2048,30 @@ function loadCache(root, opts) {
2036
2048
  };
2037
2049
  }
2038
2050
 
2051
+ // src/engine/epistemic.ts
2052
+ var SEMANTIC_RESOLVERS = /* @__PURE__ */ new Set(["scip", "stackgraph", "tsc"]);
2053
+ function classifyEpistemic(edge, dstKind) {
2054
+ if (SEMANTIC_RESOLVERS.has(edge.resolution)) return "observed";
2055
+ switch (edge.kind) {
2056
+ case "contains":
2057
+ return "observed";
2058
+ case "import":
2059
+ return dstKind === "external" || dstKind === void 0 ? "declared" : "observed";
2060
+ case "test":
2061
+ case "coverage":
2062
+ return "declared";
2063
+ default:
2064
+ return "name-matched";
2065
+ }
2066
+ }
2067
+ function epistemicBreakdown(edges) {
2068
+ const out = { observed: 0, "name-matched": 0, declared: 0 };
2069
+ for (const e of edges) {
2070
+ if (e.epistemic) out[e.epistemic]++;
2071
+ }
2072
+ return out;
2073
+ }
2074
+
2039
2075
  // src/engine/build.ts
2040
2076
  async function buildGraph(options) {
2041
2077
  const start = nowMs();
@@ -2117,6 +2153,7 @@ async function buildGraph(options) {
2117
2153
  const nodeFileById = new Map(resolved.nodes.map((n) => [n.id, n.file]));
2118
2154
  let edges = resolved.edges;
2119
2155
  const resolvers = [...resolved.stats.resolvers];
2156
+ const preciseCoveredFiles = /* @__PURE__ */ new Set();
2120
2157
  let tscStats;
2121
2158
  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 }));
2122
2159
  if (limits.tscMaxFiles > 0 && tsFiles.length > limits.tscMaxFiles) {
@@ -2129,6 +2166,7 @@ async function buildGraph(options) {
2129
2166
  const res = tsResolveEdges(root, tsFiles, resolved.nodes);
2130
2167
  if (res.stats.files > 0) {
2131
2168
  edges = mergePreciseEdges(edges, res.edges, res.coveredFiles, nodeFileById);
2169
+ for (const f of res.coveredFiles) preciseCoveredFiles.add(f);
2132
2170
  if (!resolvers.includes("tsc")) resolvers.unshift("tsc");
2133
2171
  tscStats = res.stats;
2134
2172
  }
@@ -2139,6 +2177,7 @@ async function buildGraph(options) {
2139
2177
  if (scip) {
2140
2178
  const res = scipEdges(scip.index, resolved.nodes, toRepoRel);
2141
2179
  edges = mergePreciseEdges(edges, res.edges, res.coveredFiles, nodeFileById);
2180
+ for (const f of res.coveredFiles) preciseCoveredFiles.add(f);
2142
2181
  if (!resolvers.includes("scip")) resolvers.unshift("scip");
2143
2182
  scipStats = { ...res.stats, tool: scip.tool };
2144
2183
  }
@@ -2151,6 +2190,11 @@ async function buildGraph(options) {
2151
2190
  const languages = [...new Set(parses.map((p) => p.lang))].sort();
2152
2191
  const edgeKinds = [...new Set(analysis.edges.map((e) => e.kind))].sort();
2153
2192
  const corpusHash = computeCorpusHash(parses, hashes);
2193
+ const nodeKindById = new Map(analysis.nodes.map((n) => [n.id, n.kind]));
2194
+ for (const e of analysis.edges) {
2195
+ e.epistemic = classifyEpistemic(e, nodeKindById.get(e.dst));
2196
+ }
2197
+ const toolchain = computeToolchain(grammars, resolvers);
2154
2198
  const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2155
2199
  const testsCount = linked.testFiles.length;
2156
2200
  const untestedCount = analysis.nodes.filter(
@@ -2165,7 +2209,8 @@ async function buildGraph(options) {
2165
2209
  grammars: Object.fromEntries(languages.map((l) => [l, grammars])),
2166
2210
  resolver: resolvers,
2167
2211
  deep: options.deep ?? false,
2168
- corpusHash
2212
+ corpusHash,
2213
+ toolchain
2169
2214
  },
2170
2215
  meta: {
2171
2216
  root: path18.basename(root) === "" ? "." : ".",
@@ -2184,6 +2229,9 @@ async function buildGraph(options) {
2184
2229
  edges: analysis.edges,
2185
2230
  areas: analysis.areas
2186
2231
  };
2232
+ const survivingIds = new Set(analysis.nodes.map((n) => n.id));
2233
+ 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 }));
2234
+ if (unknowns.length) graph.unknowns = unknowns;
2187
2235
  if (options.deep) {
2188
2236
  const facts = buildFacts(parses, analysis.nodes, analysis.edges);
2189
2237
  if (facts.length) graph.facts = facts;
@@ -2242,6 +2290,23 @@ function mergePreciseEdges(base, precise, coveredFiles, nodeFileById) {
2242
2290
  (a, b) => a.kind.localeCompare(b.kind) || a.src.localeCompare(b.src) || a.dst.localeCompare(b.dst)
2243
2291
  );
2244
2292
  }
2293
+ function computeToolchain(grammars, resolvers) {
2294
+ const sortedResolvers = [...new Set(resolvers)].sort();
2295
+ return {
2296
+ schema: SCHEMA_VERSION,
2297
+ tool: VERSION,
2298
+ grammars,
2299
+ resolvers: sortedResolvers,
2300
+ fingerprint: shortId(
2301
+ canonicalize({
2302
+ schema: SCHEMA_VERSION,
2303
+ tool: VERSION,
2304
+ grammars,
2305
+ resolvers: sortedResolvers
2306
+ })
2307
+ )
2308
+ };
2309
+ }
2245
2310
  function computeCorpusHash(parses, hashes) {
2246
2311
  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);
2247
2312
  return hashString(canonicalize(list));
@@ -2249,6 +2314,224 @@ function computeCorpusHash(parses, hashes) {
2249
2314
  function nowMs() {
2250
2315
  return Number(process.hrtime.bigint() / 1000n) / 1e3;
2251
2316
  }
2317
+
2318
+ // src/engine/report.ts
2319
+ function renderReport(graph) {
2320
+ const { meta, provenance, nodes, edges } = graph;
2321
+ const lines = [];
2322
+ lines.push("# Code Graph Report");
2323
+ lines.push("");
2324
+ lines.push(`Generated by \`vg\` ${provenance.version} \xB7 schema \`${graph.schemaVersion}\``);
2325
+ lines.push("");
2326
+ lines.push("## Summary");
2327
+ lines.push("");
2328
+ lines.push(`- **Nodes:** ${meta.counts.nodes.toLocaleString("en-US")}`);
2329
+ lines.push(`- **Edges:** ${meta.counts.edges.toLocaleString("en-US")}`);
2330
+ lines.push(`- **Areas:** ${meta.counts.areas.toLocaleString("en-US")}`);
2331
+ lines.push(`- **Languages:** ${meta.languages.join(", ") || "\u2014"}`);
2332
+ lines.push(`- **Clustering:** ${meta.cluster}`);
2333
+ lines.push(`- **Resolvers:** ${provenance.resolver.join(", ")}`);
2334
+ lines.push(`- **Edge kinds:** ${meta.edgeKinds.join(", ") || "\u2014"}`);
2335
+ lines.push("");
2336
+ const fileNodes = nodes.filter((n) => n.kind === "file");
2337
+ const defsByFile = /* @__PURE__ */ new Map();
2338
+ for (const n of nodes) {
2339
+ if (n.kind === "file" || n.kind === "external") continue;
2340
+ defsByFile.set(n.file, (defsByFile.get(n.file) ?? 0) + 1);
2341
+ }
2342
+ const topFiles = [...defsByFile.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 15);
2343
+ if (topFiles.length) {
2344
+ lines.push("## Largest files (by definitions)");
2345
+ lines.push("");
2346
+ lines.push("| File | Definitions |");
2347
+ lines.push("| --- | ---: |");
2348
+ for (const [file, count] of topFiles) lines.push(`| \`${file}\` | ${count} |`);
2349
+ lines.push("");
2350
+ }
2351
+ 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);
2352
+ if (topNodes.length) {
2353
+ lines.push("## Most-connected definitions");
2354
+ lines.push("");
2355
+ lines.push("| Symbol | Kind | File | Importance |");
2356
+ lines.push("| --- | --- | --- | ---: |");
2357
+ for (const n of topNodes) {
2358
+ lines.push(
2359
+ `| \`${n.qualifiedName}\` | ${n.kind} | \`${n.file}\`:${n.span.start} | ${n.importance.toFixed(3)} |`
2360
+ );
2361
+ }
2362
+ lines.push("");
2363
+ }
2364
+ const byKind = /* @__PURE__ */ new Map();
2365
+ for (const e of edges) byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + 1);
2366
+ if (byKind.size) {
2367
+ lines.push("## Edges by kind");
2368
+ lines.push("");
2369
+ lines.push("| Kind | Count |");
2370
+ lines.push("| --- | ---: |");
2371
+ for (const [kind, count] of [...byKind.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
2372
+ lines.push(`| ${kind} | ${count} |`);
2373
+ }
2374
+ lines.push("");
2375
+ }
2376
+ lines.push("---");
2377
+ lines.push("");
2378
+ lines.push(`_Files: ${fileNodes.length} \xB7 Corpus: \`${provenance.corpusHash.slice(0, 16)}\u2026\`_`);
2379
+ lines.push("");
2380
+ return lines.join("\n");
2381
+ }
2382
+
2383
+ // src/engine/html.ts
2384
+ function renderHtml(graph) {
2385
+ 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);
2386
+ const rows = top.map(
2387
+ (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>`
2388
+ ).join("\n");
2389
+ const embedded = stableStringify(graph, 0);
2390
+ return `<!doctype html>
2391
+ <html lang="en">
2392
+ <head>
2393
+ <meta charset="utf-8">
2394
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2395
+ <title>vg \xB7 code graph</title>
2396
+ <style>
2397
+ :root { color-scheme: light dark; }
2398
+ body { font: 14px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }
2399
+ h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; }
2400
+ table { border-collapse: collapse; width: 100%; }
2401
+ th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #8884; }
2402
+ td:last-child, th:last-child { text-align: right; }
2403
+ .meta { color: #888; } code { font-size: 0.9em; }
2404
+ </style>
2405
+ </head>
2406
+ <body>
2407
+ <h1>vg \xB7 code graph</h1>
2408
+ <p class="meta">${graph.meta.counts.nodes} nodes \xB7 ${graph.meta.counts.edges} edges \xB7
2409
+ ${graph.meta.counts.areas} areas \xB7 ${esc(graph.meta.languages.join(", ") || "\u2014")} \xB7
2410
+ clustering: ${esc(graph.meta.cluster)} \xB7 vg ${esc(graph.provenance.version)}</p>
2411
+ <h2>Most-connected definitions</h2>
2412
+ <table>
2413
+ <thead><tr><th>Symbol</th><th>Kind</th><th>Location</th><th>Importance</th></tr></thead>
2414
+ <tbody>
2415
+ ${rows}
2416
+ </tbody>
2417
+ </table>
2418
+ <p class="meta">Interactive WebGL view arrives in Phase 1. The full graph is embedded below for tooling.</p>
2419
+ <script type="application/json" id="vg-graph">
2420
+ ${embedded}
2421
+ </script>
2422
+ </body>
2423
+ </html>
2424
+ `;
2425
+ }
2426
+ function esc(s) {
2427
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2428
+ }
2429
+ function vibgrateDir(root) {
2430
+ return path18.join(root, ".vibgrate");
2431
+ }
2432
+ function defaultGraphPath(root) {
2433
+ return path18.join(vibgrateDir(root), "graph.json");
2434
+ }
2435
+ function writeArtifacts(graph, options) {
2436
+ const dir = vibgrateDir(options.root);
2437
+ fs18.mkdirSync(dir, { recursive: true });
2438
+ const graphPath = options.graphPath ?? defaultGraphPath(options.root);
2439
+ fs18.mkdirSync(path18.dirname(graphPath), { recursive: true });
2440
+ const tmp = `${graphPath}.${process.pid}.tmp`;
2441
+ try {
2442
+ fs18.writeFileSync(tmp, serializeGraph(graph));
2443
+ fs18.renameSync(tmp, graphPath);
2444
+ } catch (err) {
2445
+ fs18.rmSync(tmp, { force: true });
2446
+ throw err;
2447
+ }
2448
+ const written = { graphPath };
2449
+ if (options.report !== false) {
2450
+ const reportPath = path18.join(dir, "GRAPH_REPORT.md");
2451
+ fs18.writeFileSync(reportPath, renderReport(graph));
2452
+ written.reportPath = reportPath;
2453
+ }
2454
+ if (options.html !== false) {
2455
+ const htmlPath = path18.join(dir, "graph.html");
2456
+ fs18.writeFileSync(htmlPath, renderHtml(graph));
2457
+ written.htmlPath = htmlPath;
2458
+ }
2459
+ if (graph.facts && graph.facts.length) {
2460
+ const factsPath = path18.join(dir, "facts.jsonl");
2461
+ fs18.writeFileSync(factsPath, graph.facts.map((f) => JSON.stringify(f)).join("\n") + "\n");
2462
+ written.factsPath = factsPath;
2463
+ }
2464
+ return written;
2465
+ }
2466
+ function loadGraph(root, graphPath) {
2467
+ const file = graphPath ?? defaultGraphPath(root);
2468
+ if (!fs18.existsSync(file)) return null;
2469
+ return parseGraph(fs18.readFileSync(file, "utf8"));
2470
+ }
2471
+
2472
+ // src/engine/verify.ts
2473
+ var PINNED = "1970-01-01T00:00:00.000Z";
2474
+ async function verifyDeterminism(opts) {
2475
+ const base = { root: opts.root, only: opts.only, exclude: opts.exclude, generatedAt: PINNED };
2476
+ const buildA = await buildGraph({ ...base, noCache: true, jobs: opts.jobs });
2477
+ const a = serializeGraph(buildA.graph);
2478
+ const b = serializeGraph((await buildGraph({ ...base, noCache: true, jobs: 1 })).graph);
2479
+ const cached2 = serializeGraph((await buildGraph({ ...base, noCache: false })).graph);
2480
+ const checks = [
2481
+ { name: "run-to-run determinism", ok: a === b, detail: diffHint(a, b) },
2482
+ { name: "cache safety (incremental == full)", ok: a === cached2, detail: diffHint(a, cached2) }
2483
+ ];
2484
+ const scoped = Boolean(opts.only?.length || opts.exclude?.length);
2485
+ const committed = scoped ? null : loadGraph(opts.root);
2486
+ const committedFp = committed?.provenance.toolchain?.fingerprint;
2487
+ const currentFp = buildA.graph.provenance.toolchain?.fingerprint;
2488
+ if (committedFp && currentFp) {
2489
+ const ok = committedFp === currentFp;
2490
+ checks.push({
2491
+ name: "toolchain fingerprint matches committed graph",
2492
+ ok,
2493
+ detail: ok ? void 0 : `committed toolchain ${committedFp} != current ${currentFp} (grammar/resolver versions differ \u2014 rebuild or align the toolchain)`
2494
+ });
2495
+ }
2496
+ return {
2497
+ ok: checks.every((c) => c.ok),
2498
+ checks,
2499
+ digest: hashString(a)
2500
+ };
2501
+ }
2502
+ function diffHint(x, y) {
2503
+ if (x === y) return void 0;
2504
+ const xl = x.split("\n");
2505
+ const yl = y.split("\n");
2506
+ const n = Math.max(xl.length, yl.length);
2507
+ for (let i = 0; i < n; i++) {
2508
+ if (xl[i] !== yl[i]) {
2509
+ return `first divergence at line ${i + 1}`;
2510
+ }
2511
+ }
2512
+ return "outputs differ in length";
2513
+ }
2514
+
2515
+ // src/util/exit.ts
2516
+ var ExitCode = {
2517
+ OK: 0,
2518
+ ERROR: 1,
2519
+ GATE_FAILED: 2,
2520
+ NOT_FOUND: 3,
2521
+ NON_DETERMINISTIC: 4,
2522
+ USAGE_ERROR: 5
2523
+ };
2524
+ var CliError = class extends Error {
2525
+ code;
2526
+ constructor(message, code = ExitCode.ERROR) {
2527
+ super(message);
2528
+ this.name = "CliError";
2529
+ this.code = code;
2530
+ }
2531
+ };
2532
+ function usageError(message) {
2533
+ return new CliError(message, ExitCode.USAGE_ERROR);
2534
+ }
2252
2535
  var DEFAULT_LOCK_STALE_MS = 15 * 60 * 1e3;
2253
2536
  function isProcessAlive(pid) {
2254
2537
  try {
@@ -2508,220 +2791,71 @@ function readCacheEntries(file, modelId) {
2508
2791
  try {
2509
2792
  const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2510
2793
  if (loaded.model === modelId && loaded.entries) return loaded.entries;
2511
- } catch {
2512
- }
2513
- return {};
2514
- }
2515
- async function getNodeEmbeddings(graph, embedder, root, onProgress) {
2516
- const file = vectorCachePath(root, embedder.id);
2517
- let cache = { model: embedder.id, entries: {} };
2518
- if (fs18.existsSync(file)) {
2519
- try {
2520
- const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2521
- if (loaded.model === embedder.id && loaded.entries) cache = loaded;
2522
- } catch {
2523
- }
2524
- }
2525
- const areaLabel = new Map(graph.areas.map((a) => [a.id, a.label]));
2526
- const targets = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external");
2527
- const toEmbed = [];
2528
- const vectors = /* @__PURE__ */ new Map();
2529
- for (const n of targets) {
2530
- const text = nodeEmbedText(n, areaLabel.get(n.area));
2531
- const h = hashString(text);
2532
- const cached2 = cache.entries[n.id];
2533
- if (cached2 && cached2.hash === h) vectors.set(n.id, cached2.vec);
2534
- else toEmbed.push({ id: n.id, text, hash: h });
2535
- }
2536
- const persist = () => {
2537
- try {
2538
- fs18.mkdirSync(cacheDir(root), { recursive: true });
2539
- const tmp = `${file}.tmp.${process.pid}`;
2540
- fs18.writeFileSync(tmp, JSON.stringify(cache));
2541
- fs18.renameSync(tmp, file);
2542
- } catch {
2543
- }
2544
- };
2545
- if (toEmbed.length) {
2546
- const lock = `${file}.lock`;
2547
- if (!acquireLock(lock)) return vectors;
2548
- try {
2549
- onProgress?.(0, toEmbed.length);
2550
- let lastWrite = Date.now();
2551
- for (let i = 0; i < toEmbed.length; i += EMBED_CHUNK) {
2552
- const slice = toEmbed.slice(i, i + EMBED_CHUNK);
2553
- const vecs = await embedder.embed(slice.map((t) => t.text));
2554
- slice.forEach((t, j) => {
2555
- const vec = vecs[j] ?? [];
2556
- vectors.set(t.id, vec);
2557
- cache.entries[t.id] = { hash: t.hash, vec };
2558
- });
2559
- onProgress?.(Math.min(i + EMBED_CHUNK, toEmbed.length), toEmbed.length);
2560
- if (Date.now() - lastWrite >= CACHE_WRITE_INTERVAL_MS) {
2561
- persist();
2562
- lastWrite = Date.now();
2563
- }
2564
- }
2565
- const live = new Set(targets.map((n) => n.id));
2566
- for (const id of Object.keys(cache.entries)) if (!live.has(id)) delete cache.entries[id];
2567
- persist();
2568
- } finally {
2569
- releaseLock(lock);
2570
- }
2571
- }
2572
- return vectors;
2573
- }
2574
- function safe(id) {
2575
- return id.replace(/[^a-z0-9.-]+/gi, "_");
2576
- }
2577
-
2578
- // src/engine/report.ts
2579
- function renderReport(graph) {
2580
- const { meta, provenance, nodes, edges } = graph;
2581
- const lines = [];
2582
- lines.push("# Code Graph Report");
2583
- lines.push("");
2584
- lines.push(`Generated by \`vg\` ${provenance.version} \xB7 schema \`${graph.schemaVersion}\``);
2585
- lines.push("");
2586
- lines.push("## Summary");
2587
- lines.push("");
2588
- lines.push(`- **Nodes:** ${meta.counts.nodes.toLocaleString("en-US")}`);
2589
- lines.push(`- **Edges:** ${meta.counts.edges.toLocaleString("en-US")}`);
2590
- lines.push(`- **Areas:** ${meta.counts.areas.toLocaleString("en-US")}`);
2591
- lines.push(`- **Languages:** ${meta.languages.join(", ") || "\u2014"}`);
2592
- lines.push(`- **Clustering:** ${meta.cluster}`);
2593
- lines.push(`- **Resolvers:** ${provenance.resolver.join(", ")}`);
2594
- lines.push(`- **Edge kinds:** ${meta.edgeKinds.join(", ") || "\u2014"}`);
2595
- lines.push("");
2596
- const fileNodes = nodes.filter((n) => n.kind === "file");
2597
- const defsByFile = /* @__PURE__ */ new Map();
2598
- for (const n of nodes) {
2599
- if (n.kind === "file" || n.kind === "external") continue;
2600
- defsByFile.set(n.file, (defsByFile.get(n.file) ?? 0) + 1);
2601
- }
2602
- const topFiles = [...defsByFile.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 15);
2603
- if (topFiles.length) {
2604
- lines.push("## Largest files (by definitions)");
2605
- lines.push("");
2606
- lines.push("| File | Definitions |");
2607
- lines.push("| --- | ---: |");
2608
- for (const [file, count] of topFiles) lines.push(`| \`${file}\` | ${count} |`);
2609
- lines.push("");
2610
- }
2611
- 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);
2612
- if (topNodes.length) {
2613
- lines.push("## Most-connected definitions");
2614
- lines.push("");
2615
- lines.push("| Symbol | Kind | File | Importance |");
2616
- lines.push("| --- | --- | --- | ---: |");
2617
- for (const n of topNodes) {
2618
- lines.push(
2619
- `| \`${n.qualifiedName}\` | ${n.kind} | \`${n.file}\`:${n.span.start} | ${n.importance.toFixed(3)} |`
2620
- );
2621
- }
2622
- lines.push("");
2623
- }
2624
- const byKind = /* @__PURE__ */ new Map();
2625
- for (const e of edges) byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + 1);
2626
- if (byKind.size) {
2627
- lines.push("## Edges by kind");
2628
- lines.push("");
2629
- lines.push("| Kind | Count |");
2630
- lines.push("| --- | ---: |");
2631
- for (const [kind, count] of [...byKind.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
2632
- lines.push(`| ${kind} | ${count} |`);
2633
- }
2634
- lines.push("");
2635
- }
2636
- lines.push("---");
2637
- lines.push("");
2638
- lines.push(`_Files: ${fileNodes.length} \xB7 Corpus: \`${provenance.corpusHash.slice(0, 16)}\u2026\`_`);
2639
- lines.push("");
2640
- return lines.join("\n");
2641
- }
2642
-
2643
- // src/engine/html.ts
2644
- function renderHtml(graph) {
2645
- 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);
2646
- const rows = top.map(
2647
- (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>`
2648
- ).join("\n");
2649
- const embedded = stableStringify(graph, 0);
2650
- return `<!doctype html>
2651
- <html lang="en">
2652
- <head>
2653
- <meta charset="utf-8">
2654
- <meta name="viewport" content="width=device-width, initial-scale=1">
2655
- <title>vg \xB7 code graph</title>
2656
- <style>
2657
- :root { color-scheme: light dark; }
2658
- body { font: 14px/1.5 system-ui, sans-serif; margin: 2rem auto; max-width: 60rem; padding: 0 1rem; }
2659
- h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; }
2660
- table { border-collapse: collapse; width: 100%; }
2661
- th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #8884; }
2662
- td:last-child, th:last-child { text-align: right; }
2663
- .meta { color: #888; } code { font-size: 0.9em; }
2664
- </style>
2665
- </head>
2666
- <body>
2667
- <h1>vg \xB7 code graph</h1>
2668
- <p class="meta">${graph.meta.counts.nodes} nodes \xB7 ${graph.meta.counts.edges} edges \xB7
2669
- ${graph.meta.counts.areas} areas \xB7 ${esc(graph.meta.languages.join(", ") || "\u2014")} \xB7
2670
- clustering: ${esc(graph.meta.cluster)} \xB7 vg ${esc(graph.provenance.version)}</p>
2671
- <h2>Most-connected definitions</h2>
2672
- <table>
2673
- <thead><tr><th>Symbol</th><th>Kind</th><th>Location</th><th>Importance</th></tr></thead>
2674
- <tbody>
2675
- ${rows}
2676
- </tbody>
2677
- </table>
2678
- <p class="meta">Interactive WebGL view arrives in Phase 1. The full graph is embedded below for tooling.</p>
2679
- <script type="application/json" id="vg-graph">
2680
- ${embedded}
2681
- </script>
2682
- </body>
2683
- </html>
2684
- `;
2685
- }
2686
- function esc(s) {
2687
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2688
- }
2689
- function vibgrateDir(root) {
2690
- return path18.join(root, ".vibgrate");
2691
- }
2692
- function defaultGraphPath(root) {
2693
- return path18.join(vibgrateDir(root), "graph.json");
2694
- }
2695
- function writeArtifacts(graph, options) {
2696
- const dir = vibgrateDir(options.root);
2697
- fs18.mkdirSync(dir, { recursive: true });
2698
- const graphPath = options.graphPath ?? defaultGraphPath(options.root);
2699
- fs18.mkdirSync(path18.dirname(graphPath), { recursive: true });
2700
- const tmp = `${graphPath}.${process.pid}.tmp`;
2701
- try {
2702
- fs18.writeFileSync(tmp, serializeGraph(graph));
2703
- fs18.renameSync(tmp, graphPath);
2704
- } catch (err) {
2705
- fs18.rmSync(tmp, { force: true });
2706
- throw err;
2794
+ } catch {
2707
2795
  }
2708
- const written = { graphPath };
2709
- if (options.report !== false) {
2710
- const reportPath = path18.join(dir, "GRAPH_REPORT.md");
2711
- fs18.writeFileSync(reportPath, renderReport(graph));
2712
- written.reportPath = reportPath;
2796
+ return {};
2797
+ }
2798
+ async function getNodeEmbeddings(graph, embedder, root, onProgress) {
2799
+ const file = vectorCachePath(root, embedder.id);
2800
+ let cache = { model: embedder.id, entries: {} };
2801
+ if (fs18.existsSync(file)) {
2802
+ try {
2803
+ const loaded = JSON.parse(fs18.readFileSync(file, "utf8"));
2804
+ if (loaded.model === embedder.id && loaded.entries) cache = loaded;
2805
+ } catch {
2806
+ }
2713
2807
  }
2714
- if (options.html !== false) {
2715
- const htmlPath = path18.join(dir, "graph.html");
2716
- fs18.writeFileSync(htmlPath, renderHtml(graph));
2717
- written.htmlPath = htmlPath;
2808
+ const areaLabel = new Map(graph.areas.map((a) => [a.id, a.label]));
2809
+ const targets = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external");
2810
+ const toEmbed = [];
2811
+ const vectors = /* @__PURE__ */ new Map();
2812
+ for (const n of targets) {
2813
+ const text = nodeEmbedText(n, areaLabel.get(n.area));
2814
+ const h = hashString(text);
2815
+ const cached2 = cache.entries[n.id];
2816
+ if (cached2 && cached2.hash === h) vectors.set(n.id, cached2.vec);
2817
+ else toEmbed.push({ id: n.id, text, hash: h });
2718
2818
  }
2719
- if (graph.facts && graph.facts.length) {
2720
- const factsPath = path18.join(dir, "facts.jsonl");
2721
- fs18.writeFileSync(factsPath, graph.facts.map((f) => JSON.stringify(f)).join("\n") + "\n");
2722
- written.factsPath = factsPath;
2819
+ const persist = () => {
2820
+ try {
2821
+ fs18.mkdirSync(cacheDir(root), { recursive: true });
2822
+ const tmp = `${file}.tmp.${process.pid}`;
2823
+ fs18.writeFileSync(tmp, JSON.stringify(cache));
2824
+ fs18.renameSync(tmp, file);
2825
+ } catch {
2826
+ }
2827
+ };
2828
+ if (toEmbed.length) {
2829
+ const lock = `${file}.lock`;
2830
+ if (!acquireLock(lock)) return vectors;
2831
+ try {
2832
+ onProgress?.(0, toEmbed.length);
2833
+ let lastWrite = Date.now();
2834
+ for (let i = 0; i < toEmbed.length; i += EMBED_CHUNK) {
2835
+ const slice = toEmbed.slice(i, i + EMBED_CHUNK);
2836
+ const vecs = await embedder.embed(slice.map((t) => t.text));
2837
+ slice.forEach((t, j) => {
2838
+ const vec = vecs[j] ?? [];
2839
+ vectors.set(t.id, vec);
2840
+ cache.entries[t.id] = { hash: t.hash, vec };
2841
+ });
2842
+ onProgress?.(Math.min(i + EMBED_CHUNK, toEmbed.length), toEmbed.length);
2843
+ if (Date.now() - lastWrite >= CACHE_WRITE_INTERVAL_MS) {
2844
+ persist();
2845
+ lastWrite = Date.now();
2846
+ }
2847
+ }
2848
+ const live = new Set(targets.map((n) => n.id));
2849
+ for (const id of Object.keys(cache.entries)) if (!live.has(id)) delete cache.entries[id];
2850
+ persist();
2851
+ } finally {
2852
+ releaseLock(lock);
2853
+ }
2723
2854
  }
2724
- return written;
2855
+ return vectors;
2856
+ }
2857
+ function safe(id) {
2858
+ return id.replace(/[^a-z0-9.-]+/gi, "_");
2725
2859
  }
2726
2860
  var SNAPSHOT_VERSION = "vg-freshness/1";
2727
2861
  function snapshotPath(root) {
@@ -2824,62 +2958,6 @@ function pruneScope(scope) {
2824
2958
  return out;
2825
2959
  }
2826
2960
 
2827
- // src/util/exit.ts
2828
- var ExitCode = {
2829
- OK: 0,
2830
- ERROR: 1,
2831
- GATE_FAILED: 2,
2832
- NOT_FOUND: 3,
2833
- NON_DETERMINISTIC: 4,
2834
- USAGE_ERROR: 5
2835
- };
2836
- var CliError = class extends Error {
2837
- code;
2838
- constructor(message, code = ExitCode.ERROR) {
2839
- super(message);
2840
- this.name = "CliError";
2841
- this.code = code;
2842
- }
2843
- };
2844
- function usageError(message) {
2845
- return new CliError(message, ExitCode.USAGE_ERROR);
2846
- }
2847
- function loadGraph(root, graphPath) {
2848
- const file = graphPath ?? defaultGraphPath(root);
2849
- if (!fs18.existsSync(file)) return null;
2850
- return parseGraph(fs18.readFileSync(file, "utf8"));
2851
- }
2852
-
2853
- // src/engine/verify.ts
2854
- var PINNED = "1970-01-01T00:00:00.000Z";
2855
- async function verifyDeterminism(opts) {
2856
- const base = { root: opts.root, only: opts.only, exclude: opts.exclude, generatedAt: PINNED };
2857
- const a = serializeGraph((await buildGraph({ ...base, noCache: true, jobs: opts.jobs })).graph);
2858
- const b = serializeGraph((await buildGraph({ ...base, noCache: true, jobs: 1 })).graph);
2859
- const cached2 = serializeGraph((await buildGraph({ ...base, noCache: false })).graph);
2860
- const checks = [
2861
- { name: "run-to-run determinism", ok: a === b, detail: diffHint(a, b) },
2862
- { name: "cache safety (incremental == full)", ok: a === cached2, detail: diffHint(a, cached2) }
2863
- ];
2864
- return {
2865
- ok: checks.every((c) => c.ok),
2866
- checks,
2867
- digest: hashString(a)
2868
- };
2869
- }
2870
- function diffHint(x, y) {
2871
- if (x === y) return void 0;
2872
- const xl = x.split("\n");
2873
- const yl = y.split("\n");
2874
- const n = Math.max(xl.length, yl.length);
2875
- for (let i = 0; i < n; i++) {
2876
- if (xl[i] !== yl[i]) {
2877
- return `first divergence at line ${i + 1}`;
2878
- }
2879
- }
2880
- return "outputs differ in length";
2881
- }
2882
-
2883
2961
  // src/engine/relations.ts
2884
2962
  var GraphIndex = class {
2885
2963
  constructor(graph) {
@@ -5409,33 +5487,155 @@ function boundList(items, max) {
5409
5487
  return { items: items.slice(0, max), total: items.length };
5410
5488
  }
5411
5489
  var NODE_EDGE_CAP = 50;
5490
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([".git", ".vibgrate", "node_modules", "dist", "build", "out", "target", "vendor", "__pycache__"]);
5491
+ var MAX_FILE_BYTES = 1e6;
5492
+ var MAX_FILES_SCANNED = 2e4;
5493
+ var PREVIEW_CHARS = 120;
5494
+ function searchSymbols(graph, root, query, limit) {
5495
+ const q2 = query.trim();
5496
+ if (!q2) return { matches: [], moreAvailable: false, hint: "query is required" };
5497
+ const nodes = findNodes(graph, q2).filter((n) => n.kind !== "file");
5498
+ const symbolHits = nodes.slice(0, limit).map((n, i) => ({
5499
+ kind: n.kind,
5500
+ name: n.qualifiedName,
5501
+ file: n.file,
5502
+ line: n.span.start,
5503
+ score: Math.round((1 - i / Math.max(nodes.length, 1)) * 100) / 100
5504
+ }));
5505
+ const spare = limit - symbolHits.length;
5506
+ const textHits = [];
5507
+ let truncatedScan = false;
5508
+ if (spare > 0) {
5509
+ const seen = new Set(symbolHits.map((h) => `${h.file}:${h.line}`));
5510
+ truncatedScan = scanFiles(root, q2, spare, seen, textHits);
5511
+ }
5512
+ const matches = [...symbolHits, ...textHits];
5513
+ if (matches.length === 0) {
5514
+ return {
5515
+ matches,
5516
+ moreAvailable: false,
5517
+ hint: "no symbol or text match \u2014 for meaning-level questions (symptoms, relationships, what-breaks-if) use query_graph"
5518
+ };
5519
+ }
5520
+ return { matches, moreAvailable: nodes.length > symbolHits.length || truncatedScan };
5521
+ }
5522
+ function scanFiles(root, needle, budget, seen, out) {
5523
+ const lower = needle.toLowerCase();
5524
+ let scanned = 0;
5525
+ const stack = [root];
5526
+ while (stack.length) {
5527
+ const dir = stack.pop();
5528
+ let entries;
5529
+ try {
5530
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
5531
+ } catch {
5532
+ continue;
5533
+ }
5534
+ entries.sort((a, b) => a.name.localeCompare(b.name));
5535
+ for (const e of entries) {
5536
+ if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
5537
+ const abs = path18.join(dir, e.name);
5538
+ if (e.isDirectory()) {
5539
+ stack.push(abs);
5540
+ continue;
5541
+ }
5542
+ if (++scanned > MAX_FILES_SCANNED) return true;
5543
+ let text;
5544
+ try {
5545
+ if (fs18.statSync(abs).size > MAX_FILE_BYTES) continue;
5546
+ text = fs18.readFileSync(abs, "utf8");
5547
+ } catch {
5548
+ continue;
5549
+ }
5550
+ if (!text.toLowerCase().includes(lower)) continue;
5551
+ const rel2 = path18.relative(root, abs);
5552
+ const lines = text.split("\n");
5553
+ for (let i = 0; i < lines.length; i++) {
5554
+ if (!lines[i].toLowerCase().includes(lower)) continue;
5555
+ const key = `${rel2}:${i + 1}`;
5556
+ if (seen.has(key)) continue;
5557
+ seen.add(key);
5558
+ out.push({ kind: "text", file: rel2, line: i + 1, preview: lines[i].trim().slice(0, PREVIEW_CHARS) });
5559
+ if (out.length >= budget) return true;
5560
+ }
5561
+ }
5562
+ }
5563
+ return false;
5564
+ }
5412
5565
 
5413
5566
  // src/mcp/tools.ts
5414
- var embedderPromise;
5415
- function sharedEmbedder(local) {
5416
- if (!embedderPromise) embedderPromise = loadEmbedder({ local });
5417
- return embedderPromise;
5567
+ var warmEmbedder = null;
5568
+ var bgWarmStarted = false;
5569
+ function warmEmbedderInBackground(local) {
5570
+ if (local || bgWarmStarted || isModelReady()) return;
5571
+ bgWarmStarted = true;
5572
+ loadEmbedder({}).then((e) => {
5573
+ if (e) warmEmbedder = Promise.resolve(e);
5574
+ }).catch(() => {
5575
+ bgWarmStarted = false;
5576
+ });
5577
+ }
5578
+ function readyEmbedder(local) {
5579
+ if (warmEmbedder) return warmEmbedder;
5580
+ if (local) return warmEmbedder = loadEmbedder({ local, noDownload: true });
5581
+ if (isModelReady()) return warmEmbedder = loadEmbedder({ noDownload: true });
5582
+ warmEmbedderInBackground(local);
5583
+ return Promise.resolve(null);
5584
+ }
5585
+ async function retrieve(graph, question, budget, ctx) {
5586
+ let mode = "lexical";
5587
+ try {
5588
+ const q2 = await withTimeout(
5589
+ (async () => {
5590
+ const embedder = await readyEmbedder(ctx.local);
5591
+ if (!embedder) return null;
5592
+ const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5593
+ const r = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5594
+ mode = `semantic (${embedder.id})`;
5595
+ return r;
5596
+ })(),
5597
+ SEMANTIC_BUDGET_MS,
5598
+ "semantic path over budget"
5599
+ );
5600
+ if (q2) return { q: q2, mode };
5601
+ } catch {
5602
+ }
5603
+ return { q: queryGraph(graph, question, { budget }), mode: "lexical" };
5418
5604
  }
5605
+ var SEMANTIC_BUDGET_MS = (() => {
5606
+ const n = Number(process.env.VG_SEMANTIC_BUDGET_MS);
5607
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
5608
+ })();
5419
5609
  var obj = (properties, required = []) => ({
5420
5610
  type: "object",
5421
5611
  properties,
5422
- required,
5612
+ ...required.length ? { required } : {},
5423
5613
  additionalProperties: false
5424
5614
  });
5615
+ var RESPONSE_FORMAT = { type: "string", enum: ["concise", "detailed"] };
5616
+ var isDetailed = (args) => args.response_format === "detailed";
5617
+ var CONCISE_MATCHES = 5;
5618
+ var EPISTEMIC_TIERS = ["observed", "name-matched", "declared"];
5619
+ var EDGE_FILTER_SCHEMA = {
5620
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop edges below this confidence" },
5621
+ epistemic: { type: "string", enum: EPISTEMIC_TIERS, description: "keep only edges of this evidence tier" }
5622
+ };
5623
+ function edgeRecordFilter(args) {
5624
+ const min = numOrU(args.min_edge_confidence);
5625
+ const tier = EPISTEMIC_TIERS.includes(String(args.epistemic)) ? String(args.epistemic) : void 0;
5626
+ if (min == null && !tier) return () => true;
5627
+ return (x) => (min == null || x.edge.confidence >= min) && (!tier || x.edge.epistemic === tier);
5628
+ }
5425
5629
  var TOOLS = [
5426
- {
5427
- name: "get_graph_summary",
5428
- description: "High-level summary of the code map: counts, languages, clustering, top areas and hubs.",
5429
- inputSchema: obj({}),
5430
- handler: (graph) => summarize(graph)
5431
- },
5432
5630
  {
5433
5631
  name: "orient",
5434
- description: "ONE-SHOT orientation before changing code: the map summary + the most relevant nodes for your question + the blast radius of the top hit, in a single call. Prefer this over separate get_graph_summary + query_graph + impact_of \u2014 same answer, far fewer round-trips (each round-trip re-bills the whole conversation).",
5632
+ description: "Start here: one call returns the map overview, ranked matches for your question, and the top hit\u2019s blast radius \u2014 replaces summary+query+impact round-trips.",
5435
5633
  inputSchema: obj(
5436
5634
  {
5437
- question: { type: "string", description: "what you are about to do or look for" },
5438
- budget: { type: "number", description: "approx token budget for the context block (default 1500)" }
5635
+ question: { type: "string", maxLength: 300 },
5636
+ scope: { type: "string", description: "path prefix to orient within" },
5637
+ budget: { type: "number", description: "token budget in detailed mode (default 1500)" },
5638
+ response_format: RESPONSE_FORMAT
5439
5639
  },
5440
5640
  ["question"]
5441
5641
  ),
@@ -5443,73 +5643,103 @@ var TOOLS = [
5443
5643
  const question = String(args.question ?? "");
5444
5644
  if (!question) return { error: "bad_request", message: "question is required" };
5445
5645
  const budget = numOr(args.budget, 1500);
5446
- const embedder = await sharedEmbedder(ctx.local);
5447
- let q2;
5448
- let mode = "lexical";
5449
- if (embedder) {
5450
- const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5451
- q2 = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5452
- mode = `semantic (${embedder.id})`;
5453
- } else {
5454
- q2 = queryGraph(graph, question, { budget });
5455
- }
5646
+ const { q: q2, mode } = await retrieve(graph, question, budget, ctx);
5647
+ const rawScope = String(args.scope ?? "").trim();
5648
+ const scope = rawScope === "." || rawScope === "./" ? "" : rawScope.replace(/^\.\//, "");
5649
+ const scoped = scope ? q2.matches.filter((m) => m.node.file.startsWith(scope)) : q2.matches;
5650
+ const detailed = isDetailed(args);
5456
5651
  let topImpact = null;
5457
- const top = q2.matches[0]?.node;
5458
- if (top) {
5652
+ const top = scoped[0]?.node;
5653
+ if (detailed && top) {
5459
5654
  const r = impactOf(graph, top.id, { depth: 3 });
5460
5655
  topImpact = { node: top.qualifiedName, direct: r.direct, transitive: r.transitive, affected: r.affected.slice(0, 10).map(stripId) };
5461
5656
  }
5657
+ const shown = detailed ? scoped : scoped.slice(0, CONCISE_MATCHES);
5462
5658
  return {
5463
- summary: summarize(graph),
5659
+ summary: summarize(graph, detailed ? 10 : CONCISE_MATCHES),
5464
5660
  mode,
5465
- context: q2.context,
5466
- matches: q2.matches.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5467
- topImpact
5661
+ // The fact-annotated context block is detail: concise callers navigate
5662
+ // by the ranked matches and fetch nodes on demand (plan P2).
5663
+ ...detailed ? { context: q2.context } : {},
5664
+ matches: shown.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5665
+ // A neutral availability flag, not a directive to keep querying: a
5666
+ // retrieval tool that coaches "narrow the question / see more" nudges the
5667
+ // over-navigation that re-bills the whole context on every extra step.
5668
+ ...scoped.length > shown.length ? { moreAvailable: true } : {},
5669
+ ...detailed ? { topImpact } : {}
5468
5670
  };
5469
5671
  }
5470
5672
  },
5673
+ {
5674
+ name: "search_symbols",
5675
+ description: "Find a known name or string fast: ranked symbol lookup with literal file-search fallthrough. Use first for most discovery; use query_graph for meaning.",
5676
+ inputSchema: obj(
5677
+ {
5678
+ query: { type: "string", maxLength: 120 },
5679
+ limit: { type: "number", description: "default 8, max 20" }
5680
+ },
5681
+ ["query"]
5682
+ ),
5683
+ handler: (graph, args, ctx) => {
5684
+ const limit = Math.min(20, numOr(args.limit, 8));
5685
+ return searchSymbols(graph, ctx.root, String(args.query ?? ""), limit);
5686
+ }
5687
+ },
5471
5688
  {
5472
5689
  name: "query_graph",
5473
- description: "Ask the map a natural-language question; returns a budget-bounded, fact-annotated context block plus ranked matches.",
5690
+ description: "Find code by meaning when you don\u2019t know the name: symptoms, relationships, what-breaks-if. For a known name or literal string use search_symbols.",
5474
5691
  inputSchema: obj(
5475
5692
  {
5476
- question: { type: "string", description: "the question" },
5477
- budget: { type: "number", description: "approx token budget (default 2000)" }
5693
+ question: { type: "string", maxLength: 300 },
5694
+ limit: { type: "number", description: "ranked matches to return (default 5)" },
5695
+ offset: { type: "number" },
5696
+ budget: { type: "number", description: "context-block token budget in detailed mode (default 2000)" },
5697
+ response_format: RESPONSE_FORMAT
5478
5698
  },
5479
5699
  ["question"]
5480
5700
  ),
5481
5701
  handler: async (graph, args, ctx) => {
5482
5702
  const question = String(args.question ?? "");
5483
5703
  const budget = numOr(args.budget, 2e3);
5484
- let r;
5485
- let mode = "lexical";
5486
- const embedder = await sharedEmbedder(ctx.local);
5487
- if (embedder) {
5488
- const nodeVectors = await getNodeEmbeddings(graph, embedder, ctx.root);
5489
- r = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors });
5490
- mode = `semantic (${embedder.id})`;
5491
- } else {
5492
- r = queryGraph(graph, question, { budget });
5704
+ const { q: r, mode } = await retrieve(graph, question, budget, ctx);
5705
+ if (r.matches.length === 0) {
5706
+ return { mode, matches: [], hint: "no semantic match \u2014 for a known name or literal string use search_symbols; otherwise rephrase around the behaviour you observe" };
5493
5707
  }
5708
+ const limit = numOr(args.limit, CONCISE_MATCHES);
5709
+ const offset = Math.max(0, numOrU(args.offset) ?? 0);
5710
+ const page = r.matches.slice(offset, offset + limit);
5494
5711
  return {
5495
5712
  mode,
5496
- context: r.context,
5497
- tokensEstimate: r.tokensEstimate,
5498
- matches: r.matches.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score }))
5713
+ summary: `${r.matches.length} match${r.matches.length === 1 ? "" : "es"}; top: ${r.matches[0].node.qualifiedName}`,
5714
+ // The fact-annotated context block is detail (plan P2): ranked matches
5715
+ // first, fetch nodes on demand.
5716
+ ...isDetailed(args) ? { context: r.context, tokensEstimate: r.tokensEstimate } : {},
5717
+ matches: page.map((m) => ({ name: m.node.qualifiedName, kind: m.node.kind, file: m.node.file, line: m.node.span.start, score: m.score })),
5718
+ // Neutral pagination fact (nextOffset if the model genuinely needs more),
5719
+ // not a "narrow the question" nudge that invites another discovery round.
5720
+ ...offset + page.length < r.matches.length ? { moreAvailable: true, nextOffset: offset + page.length } : {}
5499
5721
  };
5500
5722
  }
5501
5723
  },
5502
5724
  {
5503
5725
  name: "get_node",
5504
- description: "Explain a node: kind, signature, callers, callees, area, importance. Resolves by qualified/short name, file:line, glob, or id.",
5726
+ description: "Inspect one symbol: signature, callers, callees, area. Accepts name, file:line, glob or id.",
5505
5727
  inputSchema: obj(
5506
- { name: { type: "string" }, pick: { type: "number", description: "choose nth candidate when ambiguous" } },
5728
+ {
5729
+ name: { type: "string" },
5730
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5731
+ ...EDGE_FILTER_SCHEMA,
5732
+ response_format: RESPONSE_FORMAT
5733
+ },
5507
5734
  ["name"]
5508
5735
  ),
5509
5736
  handler: (graph, args, ctx) => {
5510
5737
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5511
5738
  if (!node) return unresolved(candidates);
5512
5739
  const index = new GraphIndex(graph);
5740
+ const edgeOk = edgeRecordFilter(args);
5741
+ const calleeRecs = index.callees(node.id).filter(edgeOk);
5742
+ const callerRecs = index.callers(node.id).filter(edgeOk);
5513
5743
  const base = {
5514
5744
  name: node.qualifiedName,
5515
5745
  kind: node.kind,
@@ -5521,10 +5751,11 @@ var TOOLS = [
5521
5751
  area: node.area
5522
5752
  };
5523
5753
  if (ctx?.dedup && ctx.seen?.has(node.id)) {
5524
- return { ...base, repeat: true, callsTotal: uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)).length };
5754
+ return { ...base, repeat: true, callsTotal: uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames(callerRecs.map((x) => x.node.qualifiedName)).length };
5525
5755
  }
5526
- const calls = boundList(uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)), NODE_EDGE_CAP);
5527
- const calledBy = boundList(uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)), NODE_EDGE_CAP);
5756
+ const edgeCap = isDetailed(args) ? NODE_EDGE_CAP : CONCISE_MATCHES;
5757
+ const calls = boundList(uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)), edgeCap);
5758
+ const calledBy = boundList(uniqueNames(callerRecs.map((x) => x.node.qualifiedName)), edgeCap);
5528
5759
  ctx?.seen?.add(node.id);
5529
5760
  return {
5530
5761
  ...base,
@@ -5537,13 +5768,13 @@ var TOOLS = [
5537
5768
  },
5538
5769
  {
5539
5770
  name: "find_path",
5540
- description: "Shortest connection between two nodes (how A reaches B).",
5771
+ description: "Shortest connection from a to b.",
5541
5772
  inputSchema: obj(
5542
5773
  {
5543
5774
  a: { type: "string" },
5544
5775
  b: { type: "string" },
5545
- pick_a: { type: "number", description: "choose nth candidate for a when ambiguous" },
5546
- pick_b: { type: "number", description: "choose nth candidate for b when ambiguous" }
5776
+ pick_a: { type: "number" },
5777
+ pick_b: { type: "number" }
5547
5778
  },
5548
5779
  ["a", "b"]
5549
5780
  ),
@@ -5560,25 +5791,85 @@ var TOOLS = [
5560
5791
  },
5561
5792
  {
5562
5793
  name: "impact_of",
5563
- description: "What breaks if a node changes: the reverse-reachability blast radius with per-result depth and confidence.",
5794
+ description: "Blast radius of a change: what breaks if this symbol changes \u2014 dependents, files, covering tests, risk.",
5564
5795
  inputSchema: obj(
5565
5796
  {
5566
5797
  name: { type: "string" },
5567
- depth: { type: "number", description: "max depth (default 4)" },
5568
- pick: { type: "number", description: "choose nth candidate when ambiguous" }
5798
+ change_type: { type: "string", enum: ["modify", "delete", "rename", "add_dependency"] },
5799
+ depth: { type: "number", description: "default 4" },
5800
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5801
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop dependents below this (decayed) confidence" },
5802
+ response_format: RESPONSE_FORMAT
5569
5803
  },
5570
5804
  ["name"]
5571
5805
  ),
5572
5806
  handler: (graph, args) => {
5573
5807
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5574
5808
  if (!node) return unresolved(candidates);
5809
+ const changeType = ["modify", "delete", "rename", "add_dependency"].includes(String(args.change_type)) ? String(args.change_type) : "modify";
5575
5810
  const r = impactOf(graph, node.id, { depth: numOr(args.depth, 4) });
5576
- return { root: r.root.name, direct: r.direct, transitive: r.transitive, affected: r.affected.slice(0, 100).map(stripId) };
5811
+ const minConf = numOrU(args.min_edge_confidence);
5812
+ if (minConf != null) {
5813
+ const kept = r.affected.filter((a) => a.confidence >= minConf);
5814
+ r.affected = kept;
5815
+ r.direct = kept.filter((i) => i.depth === 1).length;
5816
+ r.transitive = kept.filter((i) => i.depth > 1).length;
5817
+ r.minEdgeConfidence = kept.length ? Math.min(...kept.map((i) => i.confidence)) : 1;
5818
+ }
5819
+ const tests = coveringTests(graph, node);
5820
+ const filesAffected = [...new Set(r.affected.map((a) => a.file))];
5821
+ const heavyChange = changeType === "delete" || changeType === "rename";
5822
+ const riskLevel = node.isHub || r.direct >= 10 || r.transitive >= 50 || heavyChange && r.direct >= 3 ? "high" : r.direct >= 3 || r.transitive >= 10 ? "medium" : "low";
5823
+ const summary = `${changeType} ${r.root.name}: ${r.direct} direct dependent${r.direct === 1 ? "" : "s"}, ${r.transitive} transitive across ${filesAffected.length} file${filesAffected.length === 1 ? "" : "s"}; ${tests.length} covering test${tests.length === 1 ? "" : "s"} \u2014 ${riskLevel} risk.`;
5824
+ return {
5825
+ root: r.root.name,
5826
+ changeType,
5827
+ directCallers: r.direct,
5828
+ transitiveCount: r.transitive,
5829
+ filesAffected: filesAffected.slice(0, 20),
5830
+ testsAffected: tests.length,
5831
+ riskLevel,
5832
+ summary,
5833
+ // The full row set is detail; concise callers act on the contract above.
5834
+ ...isDetailed(args) ? { affected: r.affected.slice(0, 100).map(stripId) } : r.affected.length > 0 ? { hint: 'response_format:"detailed" lists the affected nodes' } : {}
5835
+ };
5836
+ }
5837
+ },
5838
+ {
5839
+ name: "tests_for",
5840
+ description: "Which tests cover this symbol \u2014 is a change here tested?",
5841
+ inputSchema: obj(
5842
+ {
5843
+ name: { type: "string" },
5844
+ pick: { type: "number", description: "nth candidate if ambiguous" },
5845
+ response_format: RESPONSE_FORMAT
5846
+ },
5847
+ ["name"]
5848
+ ),
5849
+ handler: (graph, args) => {
5850
+ const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5851
+ if (!node) return unresolved(candidates);
5852
+ const covers = coveringTests(graph, node);
5853
+ return {
5854
+ node: node.qualifiedName,
5855
+ tested: node.tested,
5856
+ coverage: node.coverage ?? null,
5857
+ testCount: covers.length,
5858
+ covers: isDetailed(args) ? covers : covers.slice(0, 10),
5859
+ ...covers.length > 10 && !isDetailed(args) ? { moreAvailable: true } : {},
5860
+ ...covers.length === 0 ? { hint: "no covering tests found \u2014 a change here lands untested" } : {}
5861
+ };
5577
5862
  }
5578
5863
  },
5864
+ {
5865
+ name: "get_graph_summary",
5866
+ description: "Code map overview: counts, languages, top areas and hubs.",
5867
+ inputSchema: obj({}),
5868
+ handler: (graph) => summarize(graph)
5869
+ },
5579
5870
  {
5580
5871
  name: "list_areas",
5581
- description: "The natural code groupings (communities), each labelled, sized, with cohesion.",
5872
+ description: "Code areas (communities) by size.",
5582
5873
  inputSchema: obj({ limit: { type: "number" } }),
5583
5874
  // Strip `members` (raw content-hash id arrays): a model cannot use them and
5584
5875
  // on a large repo they ballooned this result past 20k tokens.
@@ -5586,23 +5877,13 @@ var TOOLS = [
5586
5877
  },
5587
5878
  {
5588
5879
  name: "list_hubs",
5589
- description: "The most-depended-on code (centrality outliers).",
5880
+ description: "Most-depended-on symbols.",
5590
5881
  inputSchema: obj({ limit: { type: "number" } }),
5591
5882
  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 }))
5592
5883
  },
5593
- {
5594
- name: "tests_for",
5595
- description: "Which tests cover a node (static call linkage + runtime coverage), with the linkage basis.",
5596
- inputSchema: obj({ name: { type: "string" }, pick: { type: "number", description: "choose nth candidate when ambiguous" } }, ["name"]),
5597
- handler: (graph, args) => {
5598
- const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5599
- if (!node) return unresolved(candidates);
5600
- return { node: node.qualifiedName, tested: node.tested, coverage: node.coverage ?? null, covers: coveringTests(graph, node) };
5601
- }
5602
- },
5603
5884
  {
5604
5885
  name: "get_facts",
5605
- description: "The deterministic open facts for a node (contract/invariant/characterization). Requires a --deep build.",
5886
+ description: "Deterministic facts for a node (contract/invariant); needs a --deep build.",
5606
5887
  inputSchema: obj({ name: { type: "string" } }, ["name"]),
5607
5888
  handler: (graph, args) => {
5608
5889
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""));
@@ -5613,7 +5894,7 @@ var TOOLS = [
5613
5894
  },
5614
5895
  {
5615
5896
  name: "guide_node",
5616
- description: "Cited relevant standards/practices for a node (OWASP/CWE), honest about recommended vs conjectured.",
5897
+ description: "Cited standards/practices for a node (OWASP/CWE).",
5617
5898
  inputSchema: obj({ name: { type: "string" } }, ["name"]),
5618
5899
  handler: (graph, args) => {
5619
5900
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""));
@@ -5627,17 +5908,17 @@ var TOOLS = [
5627
5908
  },
5628
5909
  {
5629
5910
  name: "check_drift",
5630
- description: 'Offline dependency inventory across npm/pypi/go (currency enrichment is the CLI\u2019s --online opt-in). Pass attribute:true to add git "who added this / who set the version" attribution for npm deps.',
5911
+ description: "Offline dependency inventory (npm/pypi/go); attribute:true adds git who-added attribution.",
5631
5912
  inputSchema: obj(
5632
- { attribute: { type: "boolean", description: "enrich npm deps with git introduction attribution (requires git; slower)" } },
5913
+ { attribute: { type: "boolean" } },
5633
5914
  []
5634
5915
  ),
5635
5916
  handler: (_graph, args, ctx) => attributedInventory(ctx.root, { attribute: args.attribute === true })
5636
5917
  },
5637
5918
  {
5638
5919
  name: "vuln_attribution",
5639
- description: "Who introduced each open vulnerability and how long you have been exposed, plus CRA remediation metrics: open exposure windows, SLA breaches, and real remediation time (MTTR) reconstructed from vulnerable versions that were later bumped out or removed in git history. Offline read of the last `vg scan --vulns`.",
5640
- inputSchema: obj({ package: { type: "string", description: "optional: restrict to one package" } }, []),
5920
+ description: "Who introduced each open vulnerability, exposure windows, and CRA remediation metrics (MTTR, SLA breaches). Reads the last `vg scan --vulns`.",
5921
+ inputSchema: obj({ package: { type: "string", description: "restrict to one package" } }, []),
5641
5922
  handler: (_graph, args, ctx) => {
5642
5923
  const data = loadVulnerabilities(ctx.root);
5643
5924
  if (!data) {
@@ -5666,9 +5947,9 @@ var TOOLS = [
5666
5947
  },
5667
5948
  {
5668
5949
  name: "list_vulnerabilities",
5669
- description: "Known vulnerabilities for installed dependencies, from the last `vg scan --vulns`. Offline read of the local scan artifact (no network); reports advisory id/CVE, severity, CVSS, and the fixed version.",
5950
+ description: "Known vulnerabilities from the last `vg scan --vulns`: id/CVE, severity, CVSS, fixed version.",
5670
5951
  inputSchema: obj(
5671
- { severity: { type: "string", enum: ["low", "moderate", "high", "critical"], description: "optional minimum severity to include" } },
5952
+ { severity: { type: "string", enum: ["low", "moderate", "high", "critical"], description: "minimum severity" } },
5672
5953
  []
5673
5954
  ),
5674
5955
  handler: (_graph, args, ctx) => {
@@ -5704,11 +5985,11 @@ var TOOLS = [
5704
5985
  },
5705
5986
  {
5706
5987
  name: "upgrade_impact",
5707
- description: 'A local "what breaks if I upgrade this" brief for a package: major-version distance + interim majors to step through, source blast radius (how many files import it), open vulnerabilities the upgrade would fix, and a recommended posture. Offline; richest after `vg scan`. Pass changelog:true to also fetch breaking-change signals from the package\'s GitHub releases (online; ignored under --local).',
5988
+ description: "What breaks if you upgrade a package: major distance, import blast radius, vulns fixed, recommended posture. changelog:true adds GitHub breaking-change signals (online).",
5708
5989
  inputSchema: obj(
5709
5990
  {
5710
- package: { type: "string", description: "package name to assess" },
5711
- changelog: { type: "boolean", description: "also fetch breaking-change signals from GitHub releases between your version and latest (online; ignored under --local)" }
5991
+ package: { type: "string" },
5992
+ changelog: { type: "boolean" }
5712
5993
  },
5713
5994
  ["package"]
5714
5995
  ),
@@ -5724,19 +6005,19 @@ var TOOLS = [
5724
6005
  },
5725
6006
  {
5726
6007
  name: "list_models",
5727
- description: "The local model fleet discovered on disk (Ollama / LM Studio / gguf). Offline, no launch.",
6008
+ description: "Local models on disk (Ollama / LM Studio / gguf).",
5728
6009
  inputSchema: obj({}),
5729
6010
  handler: () => ({ models: discoverModels() })
5730
6011
  },
5731
6012
  {
5732
6013
  name: "resolve_library",
5733
- description: "Resolve a library name/query to its canonical id + the version for YOUR project (version-correct + drift-annotated).",
6014
+ description: "Resolve a library to its canonical id and the version YOUR project uses (drift-annotated).",
5734
6015
  // Canonical §4: `query` (+ optional `context_project`). `name` kept as a back-compat alias.
5735
6016
  inputSchema: obj(
5736
6017
  {
5737
- query: { type: "string", description: "library name or natural-language query" },
5738
- name: { type: "string", description: "alias for query (back-compat)" },
5739
- context_project: { type: "string", description: "optional lockfile/manifest context (informational; the local server uses the project root)" }
6018
+ query: { type: "string", description: "library name or query" },
6019
+ name: { type: "string" },
6020
+ context_project: { type: "string" }
5740
6021
  },
5741
6022
  []
5742
6023
  ),
@@ -5779,19 +6060,19 @@ var TOOLS = [
5779
6060
  },
5780
6061
  {
5781
6062
  name: "library_docs",
5782
- description: "Version-correct, drift-annotated usage docs for a library \u2014 from the committed catalog or the installed package on disk.",
6063
+ description: "Version-correct usage docs for a library, sliced to a token budget.",
5783
6064
  // Canonical §4: `targetId`/`query`, `verbosity`, `max_tokens`. `name`/`tokens` kept as aliases.
5784
6065
  inputSchema: obj(
5785
6066
  {
5786
- targetId: { type: "string", description: "canonical id from resolve_library" },
6067
+ targetId: { type: "string", description: "id from resolve_library" },
5787
6068
  query: { type: "string", description: "library name or query" },
5788
- name: { type: "string", description: "alias for query (back-compat)" },
5789
- context_project: { type: "string", description: "optional project context (informational)" },
5790
- verbosity: { type: "string", enum: ["concise", "balanced", "exhaustive"], description: "detail level (sets the default token budget)" },
5791
- max_tokens: { type: "number", description: "explicit token budget (overrides verbosity)" },
5792
- tokens: { type: "number", description: "alias for max_tokens (back-compat)" },
5793
- enterprise_strict: { type: "boolean", description: "reserved (enterprise policy enforcement \u2014 hosted surface)" },
5794
- follow_up: { type: "boolean", description: "reserved (next slice \u2014 hosted surface)" }
6069
+ name: { type: "string" },
6070
+ context_project: { type: "string" },
6071
+ verbosity: { type: "string", enum: ["concise", "balanced", "exhaustive"] },
6072
+ max_tokens: { type: "number", description: "token budget" },
6073
+ tokens: { type: "number" },
6074
+ enterprise_strict: { type: "boolean" },
6075
+ follow_up: { type: "boolean" }
5795
6076
  },
5796
6077
  []
5797
6078
  ),
@@ -5886,15 +6167,15 @@ var TOOLS = [
5886
6167
  }
5887
6168
  ];
5888
6169
  var VERBOSITY_BUDGET = { concise: 1500, balanced: 4e3, exhaustive: 12e3 };
5889
- function summarize(graph) {
6170
+ function summarize(graph, top = 10) {
5890
6171
  return {
5891
6172
  counts: graph.meta.counts,
5892
6173
  languages: graph.meta.languages,
5893
6174
  cluster: graph.meta.cluster,
5894
6175
  resolver: graph.provenance.resolver,
5895
6176
  generatedAt: graph.generatedAt,
5896
- topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, 10).map((a) => ({ id: a.id, label: a.label, size: a.size })),
5897
- topHubs: graph.nodes.filter((n) => n.isHub).sort((a, b) => b.importance - a.importance).slice(0, 10).map((n) => ({ name: n.qualifiedName, file: n.file, importance: n.importance }))
6177
+ topAreas: [...graph.areas].sort((a, b) => b.size - a.size).slice(0, top).map((a) => ({ id: a.id, label: a.label, size: a.size })),
6178
+ 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 }))
5898
6179
  };
5899
6180
  }
5900
6181
  function stripId(item) {
@@ -5925,6 +6206,19 @@ function numOrU(v) {
5925
6206
  function uniqueNames(xs) {
5926
6207
  return [...new Set(xs)];
5927
6208
  }
6209
+ var HOT_TOOLS = ["orient", "search_symbols", "query_graph", "get_node"];
6210
+ function deferredToolNames() {
6211
+ const hot = new Set(HOT_TOOLS);
6212
+ return TOOLS.map((t) => t.name).filter((n) => !hot.has(n));
6213
+ }
6214
+ function navigationToolsetConfig(serverName = "vibgrate") {
6215
+ return {
6216
+ type: "mcp_toolset",
6217
+ mcp_server_name: serverName,
6218
+ default_config: { defer_loading: true },
6219
+ configs: Object.fromEntries(HOT_TOOLS.map((n) => [n, { defer_loading: false }]))
6220
+ };
6221
+ }
5928
6222
  var LEDGER = "savings.jsonl";
5929
6223
  var PER_FILE_TOKENS = 400;
5930
6224
  function ledgerPath(root) {
@@ -6047,7 +6341,13 @@ function createServer(source, opts = {}) {
6047
6341
  const seen = /* @__PURE__ */ new Set();
6048
6342
  const server = new Server(
6049
6343
  { name: "vg", version: VERSION },
6050
- { capabilities: { tools: {} } }
6344
+ {
6345
+ capabilities: { tools: {} },
6346
+ // Routing guidance once at the server level (hosts that surface
6347
+ // `instructions` get it at zero per-step schema cost): the flashlight
6348
+ // vs the map.
6349
+ instructions: 'vg is a code map. Use search_symbols to find a known name or literal string fast. Use orient/query_graph for meaning: symptoms, relationships, and what-breaks-if. Responses are concise by default; pass response_format:"detailed" only when a node proves load-bearing. Navigate as little as possible: one good search/query usually locates the code. As soon as you have the file and line, read that file and make the edit \u2014 do not call further graph tools unless the edit fails or the match was wrong.'
6350
+ }
6051
6351
  );
6052
6352
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
6053
6353
  tools: TOOLS.map((t) => ({
@@ -6086,6 +6386,7 @@ function createServer(source, opts = {}) {
6086
6386
  async function serveStdio(graphPath, opts = {}) {
6087
6387
  const source = new GraphSource(graphPath, opts.refresh !== false);
6088
6388
  const server = createServer(source, opts);
6389
+ warmEmbedderInBackground(opts.local);
6089
6390
  await server.connect(new StdioServerTransport());
6090
6391
  }
6091
6392
  function sleep(ms) {
@@ -6310,6 +6611,18 @@ function writeFileEnsured(file, content) {
6310
6611
  fs18.mkdirSync(path18.dirname(file), { recursive: true });
6311
6612
  fs18.writeFileSync(file, content);
6312
6613
  }
6614
+ function writeNavigationConfig(root) {
6615
+ const rel2 = path18.join(".vibgrate", "mcp-navigation.json");
6616
+ const doc = {
6617
+ _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.",
6618
+ hot_core: [...HOT_TOOLS],
6619
+ deferred: deferredToolNames(),
6620
+ toolset: navigationToolsetConfig()
6621
+ };
6622
+ writeFileEnsured(path18.join(root, rel2), `${JSON.stringify(doc, null, 2)}
6623
+ `);
6624
+ return rel2;
6625
+ }
6313
6626
  function upsertMcp(file, target, launch) {
6314
6627
  const config = readJson(file);
6315
6628
  const entry = mcpServerEntry(launch);
@@ -6399,6 +6712,8 @@ function formatForExt(ext) {
6399
6712
  return "dot";
6400
6713
  case ".cypher":
6401
6714
  return "cypher";
6715
+ case ".sql":
6716
+ return "sql";
6402
6717
  case ".md":
6403
6718
  return "md";
6404
6719
  case ".html":
@@ -6423,6 +6738,8 @@ function exportGraph(format, ctx) {
6423
6738
  return dot(ctx.graph);
6424
6739
  case "cypher":
6425
6740
  return cypher(ctx.graph);
6741
+ case "sql":
6742
+ return sql(ctx);
6426
6743
  case "cyclonedx":
6427
6744
  return cyclonedx(ctx);
6428
6745
  case "spdx":
@@ -6485,6 +6802,66 @@ function cypherLabel(s) {
6485
6802
  function q(s) {
6486
6803
  return JSON.stringify(s);
6487
6804
  }
6805
+ function sql(ctx) {
6806
+ const graph = ctx.graph;
6807
+ const lines = [];
6808
+ lines.push("BEGIN;");
6809
+ lines.push("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);");
6810
+ const meta = [
6811
+ ["schemaVersion", graph.schemaVersion],
6812
+ ["generatedAt", ctx.generatedAt],
6813
+ ["corpusHash", graph.provenance.corpusHash],
6814
+ ["tool", graph.provenance.tool],
6815
+ ["toolVersion", graph.provenance.version],
6816
+ ["resolver", graph.provenance.resolver.join(",")],
6817
+ ["fingerprint", graph.provenance.toolchain?.fingerprint ?? null]
6818
+ ];
6819
+ for (const [k, v] of meta) lines.push(`INSERT INTO meta VALUES (${sqlStr(k)}, ${sqlStr(v)});`);
6820
+ lines.push(
6821
+ "CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT, name TEXT, qualified_name TEXT, file TEXT, span_start INTEGER, span_end INTEGER, lang TEXT, visibility TEXT, signature TEXT, importance REAL, area INTEGER, is_hub INTEGER, tested INTEGER, coverage REAL);"
6822
+ );
6823
+ for (const n of graph.nodes) {
6824
+ lines.push(
6825
+ `INSERT INTO nodes VALUES (${sqlStr(n.id)}, ${sqlStr(n.kind)}, ${sqlStr(n.name)}, ${sqlStr(n.qualifiedName)}, ${sqlStr(n.file)}, ${sqlNum(n.span.start)}, ${sqlNum(n.span.end)}, ${sqlStr(n.lang)}, ${sqlStr(n.visibility)}, ${sqlStr(n.signature)}, ${sqlNum(n.importance)}, ${sqlNum(n.area)}, ${sqlBool(n.isHub)}, ${sqlBool(n.tested)}, ${sqlNum(n.coverage)});`
6826
+ );
6827
+ }
6828
+ lines.push(
6829
+ 'CREATE TABLE IF NOT EXISTS edges (id TEXT PRIMARY KEY, kind TEXT, src TEXT, dst TEXT, resolution TEXT, confidence REAL, epistemic TEXT, surprise REAL, "count" INTEGER);'
6830
+ );
6831
+ for (const e of graph.edges) {
6832
+ lines.push(
6833
+ `INSERT INTO edges VALUES (${sqlStr(e.id)}, ${sqlStr(e.kind)}, ${sqlStr(e.src)}, ${sqlStr(e.dst)}, ${sqlStr(e.resolution)}, ${sqlNum(e.confidence)}, ${sqlStr(e.epistemic)}, ${sqlNum(e.surprise)}, ${sqlNum(e.count)});`
6834
+ );
6835
+ }
6836
+ lines.push("CREATE TABLE IF NOT EXISTS areas (id INTEGER PRIMARY KEY, label TEXT, size INTEGER, cohesion REAL, external_edges INTEGER);");
6837
+ lines.push("CREATE TABLE IF NOT EXISTS area_members (area_id INTEGER, node_id TEXT);");
6838
+ for (const a of graph.areas) {
6839
+ lines.push(`INSERT INTO areas VALUES (${sqlNum(a.id)}, ${sqlStr(a.label)}, ${sqlNum(a.size)}, ${sqlNum(a.cohesion)}, ${sqlNum(a.externalEdges)});`);
6840
+ for (const m of a.members) lines.push(`INSERT INTO area_members VALUES (${sqlNum(a.id)}, ${sqlStr(m)});`);
6841
+ }
6842
+ if (graph.facts && graph.facts.length) {
6843
+ lines.push("CREATE TABLE IF NOT EXISTS facts (id TEXT PRIMARY KEY, kind TEXT, predicate_json TEXT, derived_by TEXT, confidence TEXT);");
6844
+ lines.push("CREATE TABLE IF NOT EXISTS fact_subjects (fact_id TEXT, node_id TEXT);");
6845
+ lines.push("CREATE TABLE IF NOT EXISTS fact_evidence (fact_id TEXT, file TEXT, span_start INTEGER, span_end INTEGER);");
6846
+ for (const f of graph.facts) {
6847
+ lines.push(`INSERT INTO facts VALUES (${sqlStr(f.id)}, ${sqlStr(f.kind)}, ${sqlStr(JSON.stringify(f.predicate))}, ${sqlStr(f.derivedBy)}, ${sqlStr(f.confidence)});`);
6848
+ for (const s of f.subjectIds) lines.push(`INSERT INTO fact_subjects VALUES (${sqlStr(f.id)}, ${sqlStr(s)});`);
6849
+ for (const ev of f.evidence) lines.push(`INSERT INTO fact_evidence VALUES (${sqlStr(f.id)}, ${sqlStr(ev.file)}, ${sqlNum(ev.span.start)}, ${sqlNum(ev.span.end)});`);
6850
+ }
6851
+ }
6852
+ lines.push("COMMIT;");
6853
+ return lines.join("\n") + "\n";
6854
+ }
6855
+ function sqlStr(v) {
6856
+ if (v == null) return "NULL";
6857
+ return `'${v.replace(/'/g, "''")}'`;
6858
+ }
6859
+ function sqlNum(v) {
6860
+ return v == null || !Number.isFinite(v) ? "NULL" : String(v);
6861
+ }
6862
+ function sqlBool(v) {
6863
+ return v == null ? "NULL" : v ? "1" : "0";
6864
+ }
6488
6865
  function cyclonedx(ctx) {
6489
6866
  const components = [];
6490
6867
  for (const d of ctx.deps ?? []) {
@@ -6524,6 +6901,6 @@ function spdx(ctx) {
6524
6901
  return JSON.stringify(doc, null, 2) + "\n";
6525
6902
  }
6526
6903
 
6527
- export { ASSISTANTS, CliError, ECOSYSTEMS, ExitCode, FREE_PACK, GraphIndex, GraphSource, NPX_INVOCATION, ResourceLimitError, SCHEMA_VERSION, SKIP_DIRS, SKIP_FILES, TOOLS, UsageError, addLibrary, analyze, applyCoverage, applyStaticTestLinkage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, buildModuleResolver, cacheDir, catalogPath, clearModelCache, clearStoredCredentials, cosine, countPending, coveringTests, createServer, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, driftFor, dsnCommand, embeddingsCached, enrichOnline, ensureGitignored, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, installAssistant, inventory, isModelReady, isTestFile, libDir, libId, loadCatalog, loadCoverage, loadEmbedder, loadGraph, loadSnapshot, localApiSurface, localPackageDocs, modelCacheInfo, nodeById, nodeEmbedText, parseDsn, parseGraph, parseJsonc, probeFreshness, publishPrivateLibrary, pushCommand, queryGraph, queryGraphSemantic, readDoc, readSavings, readScanArtifact, recordSaving, refreshIfStale, relativeResolver, renderHtml, renderReport, resolveCliInvocation, resolveDsn, resolveEmbedModel, resolveIngestHost, resolveLib, resolveLimits, resolveOne, resolveVersion, saveCatalog, savingsRecorded, scipEdges, selectForBudget, serializeGraph, serveStdio, shortestPath, stableStringify, symbolsFromApi, testsToRun, unavailableMessage, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, vibgrateDir, writeArtifacts, writeSnapshot, writeStoredCredentials };
6528
- //# sourceMappingURL=chunk-YODVLV37.js.map
6529
- //# sourceMappingURL=chunk-YODVLV37.js.map
6904
+ export { ASSISTANTS, CliError, ECOSYSTEMS, ExitCode, FREE_PACK, GraphIndex, GraphSource, NPX_INVOCATION, ResourceLimitError, SCHEMA_VERSION, SKIP_DIRS, SKIP_FILES, TOOLS, UsageError, addLibrary, analyze, applyCoverage, applyStaticTestLinkage, assessDocQuality, assistantById, availableRegionIds, buildFacts, buildGraph, buildModuleResolver, cacheDir, catalogPath, clearModelCache, clearStoredCredentials, cosine, countPending, coveringTests, createServer, createWorkspaceDsn, credentialsPath, dashHostForIngestHost, decodeScipIndex, defaultGraphPath, detectRunner, detectServeLaunch, discover, discoverModels, driftCount, driftFor, dsnCommand, embeddingsCached, enrichOnline, ensureGitignored, epistemicBreakdown, exportGraph, fetchHostedDocs, findGitRoot, findNodes, formatForExt, getNodeEmbeddings, gitignoreEntryForCredentials, groundGraph, hasDrift, identifierParts, impactOf, ingestHostForRegionId, installAssistant, inventory, isModelReady, isTestFile, libDir, libId, loadCatalog, loadCoverage, loadEmbedder, loadGraph, loadSnapshot, localApiSurface, localPackageDocs, modelCacheInfo, nodeById, nodeEmbedText, parseDsn, parseGraph, parseJsonc, probeFreshness, publishPrivateLibrary, pushCommand, queryGraph, queryGraphSemantic, readDoc, readSavings, readScanArtifact, recordSaving, refreshIfStale, relativeResolver, renderHtml, renderReport, resolveCliInvocation, resolveDsn, resolveEmbedModel, resolveIngestHost, resolveLib, resolveLimits, resolveOne, resolveVersion, saveCatalog, savingsRecorded, scipEdges, selectForBudget, serializeGraph, serveStdio, shortestPath, stableStringify, symbolsFromApi, testsToRun, unavailableMessage, uninstallAssistant, uploadScanArtifact, usageError, verifyDeterminism, vibgrateDir, writeArtifacts, writeNavigationConfig, writeSnapshot, writeStoredCredentials };
6905
+ //# sourceMappingURL=chunk-2J7DPIAL.js.map
6906
+ //# sourceMappingURL=chunk-2J7DPIAL.js.map