@vibgrate/cli 2026.704.3 → 2026.708.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
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-YN5OGRWU.js';
2
+ import { writeTextFile, pathExists, readJsonFile, computeUpgradeImpact, getChangelogSignals, gitHistoryAvailable, buildVersionTimelines, findPackageTimeline, severityRank, projectTypeToVulnEcosystem, VERSION } from './chunk-ICBBG6U3.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 {
@@ -2574,155 +2857,6 @@ async function getNodeEmbeddings(graph, embedder, root, onProgress) {
2574
2857
  function safe(id) {
2575
2858
  return id.replace(/[^a-z0-9.-]+/gi, "_");
2576
2859
  }
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;
2707
- }
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;
2713
- }
2714
- if (options.html !== false) {
2715
- const htmlPath = path18.join(dir, "graph.html");
2716
- fs18.writeFileSync(htmlPath, renderHtml(graph));
2717
- written.htmlPath = htmlPath;
2718
- }
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;
2723
- }
2724
- return written;
2725
- }
2726
2860
  var SNAPSHOT_VERSION = "vg-freshness/1";
2727
2861
  function snapshotPath(root) {
2728
2862
  return path18.join(cacheDir(root), "freshness.json");
@@ -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) {
@@ -2976,12 +3054,13 @@ function queryGraph(graph, question, options = {}) {
2976
3054
  const budget = options.budget ?? 2e3;
2977
3055
  const limit = options.limit ?? 12;
2978
3056
  const terms = tokenize(question);
3057
+ const weightOf = termWeights(graph, terms);
2979
3058
  const index = new GraphIndex(graph);
2980
3059
  const scored = [];
2981
3060
  for (const node of graph.nodes) {
2982
3061
  if (node.kind === "file" || node.kind === "external") continue;
2983
- const { score, why } = scoreNode(node, terms);
2984
- if (score > 0) scored.push({ node, score: round2(score * (1 + node.importance)), why });
3062
+ const { score, why } = scoreNode(node, terms, weightOf);
3063
+ if (score > 0) scored.push({ node, score: round2(score * (1 + IMPORTANCE_WEIGHT * node.importance)), why });
2985
3064
  }
2986
3065
  scored.sort((a, b) => b.score - a.score || a.node.qualifiedName.localeCompare(b.node.qualifiedName));
2987
3066
  const seeds = scored.slice(0, limit);
@@ -2992,6 +3071,7 @@ async function queryGraphSemantic(graph, question, options) {
2992
3071
  const budget = options.budget ?? 2e3;
2993
3072
  const limit = options.limit ?? 12;
2994
3073
  const terms = tokenize(question);
3074
+ const weightOf = termWeights(graph, terms);
2995
3075
  const index = new GraphIndex(graph);
2996
3076
  const queryVec = await options.embedder.embedQuery(question);
2997
3077
  const lexRaw = /* @__PURE__ */ new Map();
@@ -2999,7 +3079,7 @@ async function queryGraphSemantic(graph, question, options) {
2999
3079
  const whyById = /* @__PURE__ */ new Map();
3000
3080
  for (const node of graph.nodes) {
3001
3081
  if (node.kind === "file" || node.kind === "external") continue;
3002
- const { score, why } = scoreNode(node, terms);
3082
+ const { score, why } = scoreNode(node, terms, weightOf);
3003
3083
  lexRaw.set(node.id, score);
3004
3084
  whyById.set(node.id, why);
3005
3085
  if (score > lexMax) lexMax = score;
@@ -3014,7 +3094,7 @@ async function queryGraphSemantic(graph, question, options) {
3014
3094
  if (hybrid <= 0) continue;
3015
3095
  const lexWhy = whyById.get(node.id);
3016
3096
  const why = lexWhy || (sem > 0.3 ? `semantic match (${sem.toFixed(2)})` : "weak match");
3017
- scored.push({ node, score: round2(hybrid * (1 + node.importance)), why });
3097
+ scored.push({ node, score: round2(hybrid * (1 + IMPORTANCE_WEIGHT * node.importance)), why });
3018
3098
  }
3019
3099
  scored.sort((a, b) => b.score - a.score || a.node.qualifiedName.localeCompare(b.node.qualifiedName));
3020
3100
  const seeds = scored.slice(0, limit);
@@ -3028,7 +3108,7 @@ function tokenize(q2) {
3028
3108
  )
3029
3109
  ];
3030
3110
  }
3031
- function scoreNode(node, terms) {
3111
+ function scoreNode(node, terms, weightOf = () => 1) {
3032
3112
  let score = 0;
3033
3113
  const hits = [];
3034
3114
  const name = node.name.toLowerCase();
@@ -3036,28 +3116,50 @@ function scoreNode(node, terms) {
3036
3116
  const file = node.file.toLowerCase();
3037
3117
  const nameParts = identifierParts(node.name);
3038
3118
  for (const t of terms) {
3119
+ const w = weightOf(t);
3039
3120
  if (name === t) {
3040
- score += 10;
3121
+ score += 10 * w;
3041
3122
  hits.push(t);
3042
3123
  } else if (nameParts.has(t)) {
3043
- score += 6;
3124
+ score += 6 * w;
3044
3125
  hits.push(t);
3045
3126
  } else if (name.includes(t)) {
3046
- score += 4;
3127
+ score += 4 * w;
3047
3128
  hits.push(t);
3048
3129
  } else if (qn.includes(t)) {
3049
- score += 3;
3130
+ score += 3 * w;
3050
3131
  hits.push(t);
3051
3132
  } else if (fuzzyPartMatch(t, nameParts)) {
3052
- score += 2;
3133
+ score += 2 * w;
3053
3134
  hits.push(`~${t}`);
3054
3135
  } else if (file.includes(t)) {
3055
- score += 1;
3136
+ score += 1 * w;
3056
3137
  hits.push(t);
3057
3138
  }
3058
3139
  }
3059
3140
  return { score, why: hits.length ? `matched: ${hits.join(", ")}` : "" };
3060
3141
  }
3142
+ var IMPORTANCE_WEIGHT = 0.4;
3143
+ function termWeights(graph, terms) {
3144
+ if (terms.length === 0) return () => 1;
3145
+ const df = /* @__PURE__ */ new Map();
3146
+ let n = 0;
3147
+ for (const node of graph.nodes) {
3148
+ if (node.kind === "file" || node.kind === "external") continue;
3149
+ n++;
3150
+ const name = node.name.toLowerCase();
3151
+ const parts = identifierParts(node.name);
3152
+ for (const t of terms) {
3153
+ if (parts.has(t) || name.includes(t)) df.set(t, (df.get(t) ?? 0) + 1);
3154
+ }
3155
+ }
3156
+ const w = /* @__PURE__ */ new Map();
3157
+ for (const t of terms) {
3158
+ const idf = Math.log((n + 1) / ((df.get(t) ?? 0) + 1)) + 1;
3159
+ w.set(t, Math.max(0.5, Math.min(8, idf)));
3160
+ }
3161
+ return (t) => w.get(t) ?? 1;
3162
+ }
3061
3163
  function identifierParts(name) {
3062
3164
  return new Set(
3063
3165
  name.split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean).map((s) => s.toLowerCase())
@@ -5416,7 +5518,11 @@ var PREVIEW_CHARS = 120;
5416
5518
  function searchSymbols(graph, root, query, limit) {
5417
5519
  const q2 = query.trim();
5418
5520
  if (!q2) return { matches: [], moreAvailable: false, hint: "query is required" };
5419
- const nodes = findNodes(graph, q2).filter((n) => n.kind !== "file");
5521
+ let nodes = findNodes(graph, q2).filter((n) => n.kind !== "file");
5522
+ if (nodes.length === 0) {
5523
+ const tokens = queryTokens(q2);
5524
+ if (tokens.length >= 2) nodes = multiTokenNodes(graph, tokens);
5525
+ }
5420
5526
  const symbolHits = nodes.slice(0, limit).map((n, i) => ({
5421
5527
  kind: n.kind,
5422
5528
  name: n.qualifiedName,
@@ -5441,6 +5547,21 @@ function searchSymbols(graph, root, query, limit) {
5441
5547
  }
5442
5548
  return { matches, moreAvailable: nodes.length > symbolHits.length || truncatedScan };
5443
5549
  }
5550
+ function queryTokens(q2) {
5551
+ return [...new Set(q2.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
5552
+ }
5553
+ function multiTokenNodes(graph, tokens) {
5554
+ const cov = /* @__PURE__ */ new Map();
5555
+ for (const t of tokens) {
5556
+ for (const n of findNodes(graph, t)) {
5557
+ if (n.kind === "file") continue;
5558
+ const e = cov.get(n.id);
5559
+ if (e) e.hits++;
5560
+ else cov.set(n.id, { node: n, hits: 1 });
5561
+ }
5562
+ }
5563
+ return [...cov.values()].sort((a, b) => b.hits - a.hits || b.node.importance - a.node.importance).map((e) => e.node);
5564
+ }
5444
5565
  function scanFiles(root, needle, budget, seen, out) {
5445
5566
  const lower = needle.toLowerCase();
5446
5567
  let scanned = 0;
@@ -5537,21 +5658,35 @@ var obj = (properties, required = []) => ({
5537
5658
  var RESPONSE_FORMAT = { type: "string", enum: ["concise", "detailed"] };
5538
5659
  var isDetailed = (args) => args.response_format === "detailed";
5539
5660
  var CONCISE_MATCHES = 5;
5661
+ var EPISTEMIC_TIERS = ["observed", "name-matched", "declared"];
5662
+ var EDGE_FILTER_SCHEMA = {
5663
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop edges below this confidence" },
5664
+ epistemic: { type: "string", enum: EPISTEMIC_TIERS, description: "keep only edges of this evidence tier" }
5665
+ };
5666
+ function edgeRecordFilter(args) {
5667
+ const min = numOrU(args.min_edge_confidence);
5668
+ const tier = EPISTEMIC_TIERS.includes(String(args.epistemic)) ? String(args.epistemic) : void 0;
5669
+ if (min == null && !tier) return () => true;
5670
+ return (x) => (min == null || x.edge.confidence >= min) && (!tier || x.edge.epistemic === tier);
5671
+ }
5540
5672
  var TOOLS = [
5541
5673
  {
5542
5674
  name: "orient",
5543
5675
  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.",
5544
- inputSchema: obj(
5545
- {
5546
- question: { type: "string", maxLength: 300 },
5547
- scope: { type: "string", description: "path prefix to orient within" },
5548
- budget: { type: "number", description: "token budget in detailed mode (default 1500)" },
5549
- response_format: RESPONSE_FORMAT
5550
- },
5551
- ["question"]
5552
- ),
5676
+ // `query` is a no-description back-compat alias for `question`: the sibling
5677
+ // discovery tools (search_symbols, resolve_library) take `query`, so an agent
5678
+ // naturally reaches for it here too — accepting it turns what was a wasted
5679
+ // "question is required" round-trip into a hit. Neither is `required` (either
5680
+ // satisfies the in-handler check), matching library_docs/resolve_library.
5681
+ inputSchema: obj({
5682
+ question: { type: "string", maxLength: 300 },
5683
+ query: { type: "string", maxLength: 300 },
5684
+ scope: { type: "string", description: "path prefix to orient within" },
5685
+ budget: { type: "number", description: "token budget in detailed mode (default 1500)" },
5686
+ response_format: RESPONSE_FORMAT
5687
+ }),
5553
5688
  handler: async (graph, args, ctx) => {
5554
- const question = String(args.question ?? "");
5689
+ const question = String(args.question ?? args.query ?? "");
5555
5690
  if (!question) return { error: "bad_request", message: "question is required" };
5556
5691
  const budget = numOr(args.budget, 1500);
5557
5692
  const { q: q2, mode } = await retrieve(graph, question, budget, ctx);
@@ -5567,7 +5702,11 @@ var TOOLS = [
5567
5702
  }
5568
5703
  const shown = detailed ? scoped : scoped.slice(0, CONCISE_MATCHES);
5569
5704
  return {
5570
- summary: summarize(graph, detailed ? 10 : CONCISE_MATCHES),
5705
+ // Concise "find X" calls need a one-line map (counts + languages), not the
5706
+ // full topAreas/topHubs blocks — those were ~half of every orient
5707
+ // response's tokens on the hot discovery path and the caller navigates by
5708
+ // `matches`. Detailed keeps the full overview.
5709
+ summary: detailed ? summarize(graph, 10) : conciseSummary(graph),
5571
5710
  mode,
5572
5711
  // The fact-annotated context block is detail: concise callers navigate
5573
5712
  // by the ranked matches and fetch nodes on demand (plan P2).
@@ -5599,18 +5738,19 @@ var TOOLS = [
5599
5738
  {
5600
5739
  name: "query_graph",
5601
5740
  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.",
5602
- inputSchema: obj(
5603
- {
5604
- question: { type: "string", maxLength: 300 },
5605
- limit: { type: "number", description: "ranked matches to return (default 5)" },
5606
- offset: { type: "number" },
5607
- budget: { type: "number", description: "context-block token budget in detailed mode (default 2000)" },
5608
- response_format: RESPONSE_FORMAT
5609
- },
5610
- ["question"]
5611
- ),
5741
+ // `query` is a no-description back-compat alias for `question` (see orient):
5742
+ // sibling tools take `query`, so accept it here rather than reject the call.
5743
+ inputSchema: obj({
5744
+ question: { type: "string", maxLength: 300 },
5745
+ query: { type: "string", maxLength: 300 },
5746
+ limit: { type: "number", description: "ranked matches to return (default 5)" },
5747
+ offset: { type: "number" },
5748
+ budget: { type: "number", description: "context-block token budget in detailed mode (default 2000)" },
5749
+ response_format: RESPONSE_FORMAT
5750
+ }),
5612
5751
  handler: async (graph, args, ctx) => {
5613
- const question = String(args.question ?? "");
5752
+ const question = String(args.question ?? args.query ?? "");
5753
+ if (!question) return { mode: "lexical", matches: [], hint: "question (or query) is required" };
5614
5754
  const budget = numOr(args.budget, 2e3);
5615
5755
  const { q: r, mode } = await retrieve(graph, question, budget, ctx);
5616
5756
  if (r.matches.length === 0) {
@@ -5639,6 +5779,7 @@ var TOOLS = [
5639
5779
  {
5640
5780
  name: { type: "string" },
5641
5781
  pick: { type: "number", description: "nth candidate if ambiguous" },
5782
+ ...EDGE_FILTER_SCHEMA,
5642
5783
  response_format: RESPONSE_FORMAT
5643
5784
  },
5644
5785
  ["name"]
@@ -5647,6 +5788,9 @@ var TOOLS = [
5647
5788
  const { node, candidates } = resolveOne(graph, String(args.name ?? ""), numOrU(args.pick));
5648
5789
  if (!node) return unresolved(candidates);
5649
5790
  const index = new GraphIndex(graph);
5791
+ const edgeOk = edgeRecordFilter(args);
5792
+ const calleeRecs = index.callees(node.id).filter(edgeOk);
5793
+ const callerRecs = index.callers(node.id).filter(edgeOk);
5650
5794
  const base = {
5651
5795
  name: node.qualifiedName,
5652
5796
  kind: node.kind,
@@ -5658,11 +5802,11 @@ var TOOLS = [
5658
5802
  area: node.area
5659
5803
  };
5660
5804
  if (ctx?.dedup && ctx.seen?.has(node.id)) {
5661
- 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 };
5805
+ return { ...base, repeat: true, callsTotal: uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)).length, calledByTotal: uniqueNames(callerRecs.map((x) => x.node.qualifiedName)).length };
5662
5806
  }
5663
5807
  const edgeCap = isDetailed(args) ? NODE_EDGE_CAP : CONCISE_MATCHES;
5664
- const calls = boundList(uniqueNames(index.callees(node.id).map((x) => x.node.qualifiedName)), edgeCap);
5665
- const calledBy = boundList(uniqueNames(index.callers(node.id).map((x) => x.node.qualifiedName)), edgeCap);
5808
+ const calls = boundList(uniqueNames(calleeRecs.map((x) => x.node.qualifiedName)), edgeCap);
5809
+ const calledBy = boundList(uniqueNames(callerRecs.map((x) => x.node.qualifiedName)), edgeCap);
5666
5810
  ctx?.seen?.add(node.id);
5667
5811
  return {
5668
5812
  ...base,
@@ -5705,6 +5849,7 @@ var TOOLS = [
5705
5849
  change_type: { type: "string", enum: ["modify", "delete", "rename", "add_dependency"] },
5706
5850
  depth: { type: "number", description: "default 4" },
5707
5851
  pick: { type: "number", description: "nth candidate if ambiguous" },
5852
+ min_edge_confidence: { type: "number", description: "0..1 \u2014 drop dependents below this (decayed) confidence" },
5708
5853
  response_format: RESPONSE_FORMAT
5709
5854
  },
5710
5855
  ["name"]
@@ -5714,6 +5859,14 @@ var TOOLS = [
5714
5859
  if (!node) return unresolved(candidates);
5715
5860
  const changeType = ["modify", "delete", "rename", "add_dependency"].includes(String(args.change_type)) ? String(args.change_type) : "modify";
5716
5861
  const r = impactOf(graph, node.id, { depth: numOr(args.depth, 4) });
5862
+ const minConf = numOrU(args.min_edge_confidence);
5863
+ if (minConf != null) {
5864
+ const kept = r.affected.filter((a) => a.confidence >= minConf);
5865
+ r.affected = kept;
5866
+ r.direct = kept.filter((i) => i.depth === 1).length;
5867
+ r.transitive = kept.filter((i) => i.depth > 1).length;
5868
+ r.minEdgeConfidence = kept.length ? Math.min(...kept.map((i) => i.confidence)) : 1;
5869
+ }
5717
5870
  const tests = coveringTests(graph, node);
5718
5871
  const filesAffected = [...new Set(r.affected.map((a) => a.file))];
5719
5872
  const heavyChange = changeType === "delete" || changeType === "rename";
@@ -6065,6 +6218,9 @@ var TOOLS = [
6065
6218
  }
6066
6219
  ];
6067
6220
  var VERBOSITY_BUDGET = { concise: 1500, balanced: 4e3, exhaustive: 12e3 };
6221
+ function conciseSummary(graph) {
6222
+ return { counts: graph.meta.counts, languages: graph.meta.languages };
6223
+ }
6068
6224
  function summarize(graph, top = 10) {
6069
6225
  return {
6070
6226
  counts: graph.meta.counts,
@@ -6119,6 +6275,7 @@ function navigationToolsetConfig(serverName = "vibgrate") {
6119
6275
  }
6120
6276
  var LEDGER = "savings.jsonl";
6121
6277
  var PER_FILE_TOKENS = 400;
6278
+ var SAVINGS_TOOLS = /* @__PURE__ */ new Set(["query_graph", "get_node"]);
6122
6279
  function ledgerPath(root) {
6123
6280
  return path18.join(cacheDir(root), LEDGER);
6124
6281
  }
@@ -6147,6 +6304,7 @@ function readSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
6147
6304
  try {
6148
6305
  const e = JSON.parse(line);
6149
6306
  if (e.ts < cutoff) continue;
6307
+ if (!SAVINGS_TOOLS.has(e.tool)) continue;
6150
6308
  queries++;
6151
6309
  vgTokens += e.vgTokens;
6152
6310
  baselineTokens += e.baselineTokens;
@@ -6172,6 +6330,47 @@ function readSavings(root, days, now, ratePerM = DEFAULT_RATE_PER_M) {
6172
6330
  function round22(x) {
6173
6331
  return Math.round(x * 100) / 100;
6174
6332
  }
6333
+ function readUsage(root, days, now) {
6334
+ const file = ledgerPath(root);
6335
+ const cutoff = now - days * 24 * 60 * 60 * 1e3;
6336
+ const byTool = /* @__PURE__ */ new Map();
6337
+ if (fs18.existsSync(file)) {
6338
+ for (const line of fs18.readFileSync(file, "utf8").split("\n")) {
6339
+ if (!line.trim()) continue;
6340
+ try {
6341
+ const e = JSON.parse(line);
6342
+ if (e.ts < cutoff) continue;
6343
+ const outcome = e.outcome ?? "complete";
6344
+ const row = byTool.get(e.tool) ?? { complete: 0, partial: 0, miss: 0 };
6345
+ row[outcome]++;
6346
+ byTool.set(e.tool, row);
6347
+ } catch {
6348
+ }
6349
+ }
6350
+ }
6351
+ const commands = [...byTool.entries()].map(([tool, r]) => {
6352
+ const calls = r.complete + r.partial + r.miss;
6353
+ return {
6354
+ tool,
6355
+ calls,
6356
+ complete: r.complete,
6357
+ partial: r.partial,
6358
+ miss: r.miss,
6359
+ successPct: calls ? Math.round((r.complete + r.partial) / calls * 100) : 0
6360
+ };
6361
+ }).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
6362
+ const totals = commands.reduce(
6363
+ (t, c) => ({
6364
+ calls: t.calls + c.calls,
6365
+ complete: t.complete + c.complete,
6366
+ partial: t.partial + c.partial,
6367
+ miss: t.miss + c.miss
6368
+ }),
6369
+ { calls: 0, complete: 0, partial: 0, miss: 0 }
6370
+ );
6371
+ const avgSuccessPct = commands.length ? Math.round(commands.reduce((s, c) => s + c.successPct, 0) / commands.length) : 0;
6372
+ return { enabled: savingsRecorded(root), days, commands, totals, avgSuccessPct };
6373
+ }
6175
6374
  var PROBE_INTERVAL_MS = 2e3;
6176
6375
  var MAX_PROBE_INTERVAL_MS = 3e4;
6177
6376
  var PROBE_DUTY_FACTOR = 20;
@@ -6271,9 +6470,7 @@ function createServer(source, opts = {}) {
6271
6470
  try {
6272
6471
  const args = request.params.arguments ?? {};
6273
6472
  const result = await tool.handler(graph, args, { root, local, dedup, seen });
6274
- if (savings && (tool.name === "query_graph" || tool.name === "get_node")) {
6275
- maybeRecordSaving(root, tool.name, result);
6276
- }
6473
+ if (savings) recordUsage(root, tool.name, result);
6277
6474
  return renderToolResult(result);
6278
6475
  } catch (err) {
6279
6476
  return errorResult(`tool "${tool.name}" failed: ${err.message}`);
@@ -6293,14 +6490,57 @@ function sleep(ms) {
6293
6490
  timer.unref?.();
6294
6491
  });
6295
6492
  }
6296
- function maybeRecordSaving(root, tool, result) {
6297
- if (!result || typeof result !== "object") return;
6493
+ function recordUsage(root, tool, result) {
6494
+ const outcome = classifyOutcome(result);
6495
+ let vgTokens = 0;
6496
+ let baselineTokens = 0;
6497
+ if (SAVINGS_TOOLS.has(tool) && result && typeof result === "object") {
6498
+ vgTokens = countTokens(renderedText(renderToolResult(result)));
6499
+ baselineTokens = referencedFiles(result).size * PER_FILE_TOKENS;
6500
+ }
6501
+ recordSaving(root, { tool, outcome, vgTokens, baselineTokens }, Date.now());
6502
+ }
6503
+ function classifyOutcome(result) {
6504
+ if (Array.isArray(result)) return result.length === 0 ? "miss" : "complete";
6505
+ if (!result || typeof result !== "object") return "miss";
6506
+ const r = result;
6507
+ if (typeof r.error === "string" && r.error) return "miss";
6508
+ if (r.connected === false) return "miss";
6509
+ if (Array.isArray(r.matches) && r.matches.length === 0) return "miss";
6510
+ return isPartial(r) ? "partial" : "complete";
6511
+ }
6512
+ function isPartial(r) {
6513
+ if (r.moreAvailable === true) return true;
6514
+ if (r._truncated && typeof r._truncated === "object") return true;
6515
+ for (const [key, value] of Object.entries(r)) {
6516
+ if (typeof value !== "number" || !key.endsWith("Total")) continue;
6517
+ const base = key.slice(0, -"Total".length);
6518
+ const shown = Array.isArray(r[base]) ? r[base].length : 0;
6519
+ if (value > shown) return true;
6520
+ }
6521
+ return false;
6522
+ }
6523
+ function renderedText(rendered) {
6524
+ const block = rendered.content?.find((b) => b.type === "text");
6525
+ return block && "text" in block && typeof block.text === "string" ? block.text : "";
6526
+ }
6527
+ function referencedFiles(result) {
6298
6528
  const r = result;
6299
- const vgTokens = typeof r.tokensEstimate === "number" ? r.tokensEstimate : 0;
6300
- if (vgTokens <= 0) return;
6301
- const files = new Set((r.matches ?? []).map((m) => m.file).filter(Boolean));
6302
- const baselineTokens = files.size * PER_FILE_TOKENS;
6303
- recordSaving(root, { tool, vgTokens, baselineTokens }, Date.now());
6529
+ const files = /* @__PURE__ */ new Set();
6530
+ if (typeof r.file === "string" && r.file) files.add(r.file);
6531
+ for (const m of asArray(r.matches)) {
6532
+ const f = m.file;
6533
+ if (typeof f === "string" && f) files.add(f);
6534
+ }
6535
+ for (const name of [...asArray(r.calls), ...asArray(r.calledBy)]) {
6536
+ if (typeof name !== "string") continue;
6537
+ const i = name.indexOf(":");
6538
+ if (i > 0) files.add(name.slice(0, i));
6539
+ }
6540
+ return files;
6541
+ }
6542
+ function asArray(v) {
6543
+ return Array.isArray(v) ? v : [];
6304
6544
  }
6305
6545
  function errorResult(message) {
6306
6546
  return { content: [{ type: "text", text: message }], isError: true };
@@ -6610,6 +6850,8 @@ function formatForExt(ext) {
6610
6850
  return "dot";
6611
6851
  case ".cypher":
6612
6852
  return "cypher";
6853
+ case ".sql":
6854
+ return "sql";
6613
6855
  case ".md":
6614
6856
  return "md";
6615
6857
  case ".html":
@@ -6634,6 +6876,8 @@ function exportGraph(format, ctx) {
6634
6876
  return dot(ctx.graph);
6635
6877
  case "cypher":
6636
6878
  return cypher(ctx.graph);
6879
+ case "sql":
6880
+ return sql(ctx);
6637
6881
  case "cyclonedx":
6638
6882
  return cyclonedx(ctx);
6639
6883
  case "spdx":
@@ -6696,6 +6940,66 @@ function cypherLabel(s) {
6696
6940
  function q(s) {
6697
6941
  return JSON.stringify(s);
6698
6942
  }
6943
+ function sql(ctx) {
6944
+ const graph = ctx.graph;
6945
+ const lines = [];
6946
+ lines.push("BEGIN;");
6947
+ lines.push("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);");
6948
+ const meta = [
6949
+ ["schemaVersion", graph.schemaVersion],
6950
+ ["generatedAt", ctx.generatedAt],
6951
+ ["corpusHash", graph.provenance.corpusHash],
6952
+ ["tool", graph.provenance.tool],
6953
+ ["toolVersion", graph.provenance.version],
6954
+ ["resolver", graph.provenance.resolver.join(",")],
6955
+ ["fingerprint", graph.provenance.toolchain?.fingerprint ?? null]
6956
+ ];
6957
+ for (const [k, v] of meta) lines.push(`INSERT INTO meta VALUES (${sqlStr(k)}, ${sqlStr(v)});`);
6958
+ lines.push(
6959
+ "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);"
6960
+ );
6961
+ for (const n of graph.nodes) {
6962
+ lines.push(
6963
+ `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)});`
6964
+ );
6965
+ }
6966
+ lines.push(
6967
+ '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);'
6968
+ );
6969
+ for (const e of graph.edges) {
6970
+ lines.push(
6971
+ `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)});`
6972
+ );
6973
+ }
6974
+ lines.push("CREATE TABLE IF NOT EXISTS areas (id INTEGER PRIMARY KEY, label TEXT, size INTEGER, cohesion REAL, external_edges INTEGER);");
6975
+ lines.push("CREATE TABLE IF NOT EXISTS area_members (area_id INTEGER, node_id TEXT);");
6976
+ for (const a of graph.areas) {
6977
+ lines.push(`INSERT INTO areas VALUES (${sqlNum(a.id)}, ${sqlStr(a.label)}, ${sqlNum(a.size)}, ${sqlNum(a.cohesion)}, ${sqlNum(a.externalEdges)});`);
6978
+ for (const m of a.members) lines.push(`INSERT INTO area_members VALUES (${sqlNum(a.id)}, ${sqlStr(m)});`);
6979
+ }
6980
+ if (graph.facts && graph.facts.length) {
6981
+ lines.push("CREATE TABLE IF NOT EXISTS facts (id TEXT PRIMARY KEY, kind TEXT, predicate_json TEXT, derived_by TEXT, confidence TEXT);");
6982
+ lines.push("CREATE TABLE IF NOT EXISTS fact_subjects (fact_id TEXT, node_id TEXT);");
6983
+ lines.push("CREATE TABLE IF NOT EXISTS fact_evidence (fact_id TEXT, file TEXT, span_start INTEGER, span_end INTEGER);");
6984
+ for (const f of graph.facts) {
6985
+ lines.push(`INSERT INTO facts VALUES (${sqlStr(f.id)}, ${sqlStr(f.kind)}, ${sqlStr(JSON.stringify(f.predicate))}, ${sqlStr(f.derivedBy)}, ${sqlStr(f.confidence)});`);
6986
+ for (const s of f.subjectIds) lines.push(`INSERT INTO fact_subjects VALUES (${sqlStr(f.id)}, ${sqlStr(s)});`);
6987
+ 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)});`);
6988
+ }
6989
+ }
6990
+ lines.push("COMMIT;");
6991
+ return lines.join("\n") + "\n";
6992
+ }
6993
+ function sqlStr(v) {
6994
+ if (v == null) return "NULL";
6995
+ return `'${v.replace(/'/g, "''")}'`;
6996
+ }
6997
+ function sqlNum(v) {
6998
+ return v == null || !Number.isFinite(v) ? "NULL" : String(v);
6999
+ }
7000
+ function sqlBool(v) {
7001
+ return v == null ? "NULL" : v ? "1" : "0";
7002
+ }
6699
7003
  function cyclonedx(ctx) {
6700
7004
  const components = [];
6701
7005
  for (const d of ctx.deps ?? []) {
@@ -6735,6 +7039,6 @@ function spdx(ctx) {
6735
7039
  return JSON.stringify(doc, null, 2) + "\n";
6736
7040
  }
6737
7041
 
6738
- 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, writeNavigationConfig, writeSnapshot, writeStoredCredentials };
6739
- //# sourceMappingURL=chunk-JL7FIC6W.js.map
6740
- //# sourceMappingURL=chunk-JL7FIC6W.js.map
7042
+ 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, readUsage, 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 };
7043
+ //# sourceMappingURL=chunk-AWEAREOE.js.map
7044
+ //# sourceMappingURL=chunk-AWEAREOE.js.map