@vibgrate/cli 2026.711.1 → 2026.715.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.
package/dist/cli.js CHANGED
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocs, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-MIJ3ZSSF.js';
3
- import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-X5YT263H.js';
4
- import { loadAdvancedScanHook, baselineCommand } from './chunk-3BWPEPWH.js';
5
- import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-2PCL4ZME.js';
2
+ import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-D4BPWARI.js';
3
+ import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-VFO5UDAT.js';
4
+ import { loadAdvancedScanHook, baselineCommand } from './chunk-6DKWB2OI.js';
5
+ import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-C5AVF7PT.js';
6
6
  import './chunk-RXP66R2E.js';
7
7
  import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-GI6W53LM.js';
8
- import * as fs6 from 'fs';
9
- import fs6__default, { realpathSync } from 'fs';
8
+ import * as fs7 from 'fs';
9
+ import fs7__default, { realpathSync } from 'fs';
10
10
  import * as path from 'path';
11
11
  import path__default from 'path';
12
- import { fileURLToPath } from 'url';
12
+ import { fileURLToPath, pathToFileURL } from 'url';
13
13
  import { Command, CommanderError, Option } from 'commander';
14
14
  import { execSync, spawn, spawnSync, execFileSync, execFile } from 'child_process';
15
15
  import * as crypto from 'crypto';
16
16
  import { randomUUID } from 'crypto';
17
17
  import chalk8 from 'chalk';
18
18
  import * as os from 'os';
19
+ import * as fs13 from 'fs/promises';
19
20
  import * as readline from 'readline';
20
- import * as fs15 from 'fs/promises';
21
21
  import semver from 'semver';
22
22
 
23
23
  var DSSE_PAYLOAD_TYPE = "application/vnd.in-toto+json";
@@ -178,14 +178,14 @@ async function signGraphAttestation(root, graph, opts) {
178
178
  const explicitKey = opts.key ?? process.env.VG_ATTEST_KEY;
179
179
  const keyPath = explicitKey ? path.resolve(explicitKey) : path.join(root, ".vibgrate", DEFAULT_KEY);
180
180
  let keyGeneratedAt;
181
- if (!fs6.existsSync(keyPath)) {
181
+ if (!fs7.existsSync(keyPath)) {
182
182
  if (explicitKey) {
183
183
  throw new CliError(`signing key not found: ${explicitKey}`, ExitCode.USAGE_ERROR);
184
184
  }
185
185
  const kp = generateKeypair();
186
- fs6.mkdirSync(path.dirname(keyPath), { recursive: true });
187
- fs6.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
188
- fs6.writeFileSync(`${keyPath}.pub`, kp.publicPem);
186
+ fs7.mkdirSync(path.dirname(keyPath), { recursive: true });
187
+ fs7.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
188
+ fs7.writeFileSync(`${keyPath}.pub`, kp.publicPem);
189
189
  keyGeneratedAt = keyPath;
190
190
  notices.push(
191
191
  `minted a new Ed25519 signing key at ${rel(root, keyPath)} (keyid ${kp.keyid}) \u2014 keep it, add it to .gitignore, and reuse it to re-sign reproducibly`
@@ -193,7 +193,7 @@ async function signGraphAttestation(root, graph, opts) {
193
193
  }
194
194
  let privateKey;
195
195
  try {
196
- privateKey = crypto.createPrivateKey(fs6.readFileSync(keyPath, "utf8"));
196
+ privateKey = crypto.createPrivateKey(fs7.readFileSync(keyPath, "utf8"));
197
197
  } catch {
198
198
  throw new CliError(`could not read an Ed25519 private key from ${rel(root, keyPath)}`, ExitCode.USAGE_ERROR);
199
199
  }
@@ -207,8 +207,8 @@ async function signGraphAttestation(root, graph, opts) {
207
207
  const statement = buildStatement(graph, { commit });
208
208
  const envelope = signStatement(statement, privateKey);
209
209
  const outPath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
210
- fs6.mkdirSync(path.dirname(outPath), { recursive: true });
211
- fs6.writeFileSync(outPath, serializeEnvelope(envelope));
210
+ fs7.mkdirSync(path.dirname(outPath), { recursive: true });
211
+ fs7.writeFileSync(outPath, serializeEnvelope(envelope));
212
212
  return {
213
213
  summary: {
214
214
  keyid: envelope.signatures[0]?.keyid ?? "",
@@ -223,7 +223,7 @@ async function signGraphAttestation(root, graph, opts) {
223
223
  }
224
224
  function verifyGraphAttestation(root, opts) {
225
225
  const bundlePath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
226
- if (!fs6.existsSync(bundlePath)) {
226
+ if (!fs7.existsSync(bundlePath)) {
227
227
  if (opts.attestation || opts.pub) {
228
228
  throw new CliError(
229
229
  `no attestation at ${rel(root, bundlePath)} \u2014 sign one with \`vg build --attest\``,
@@ -232,8 +232,8 @@ function verifyGraphAttestation(root, opts) {
232
232
  }
233
233
  return { bundlePath, missing: true };
234
234
  }
235
- const envelope = parseEnvelope(fs6.readFileSync(bundlePath, "utf8"));
236
- const publicKeyPem = opts.pub ? fs6.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
235
+ const envelope = parseEnvelope(fs7.readFileSync(bundlePath, "utf8"));
236
+ const publicKeyPem = opts.pub ? fs7.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
237
237
  const graph = loadGraph(root) ?? void 0;
238
238
  return { bundlePath, missing: false, result: verifyEnvelope(envelope, { publicKeyPem, graph }) };
239
239
  }
@@ -245,15 +245,15 @@ async function gitCommitInfo(root) {
245
245
  return { sha, shortSha: sha.slice(0, 7), branch, dirty: dirty === true ? true : dirty === false ? false : void 0 };
246
246
  }
247
247
  function gitBranch(root) {
248
- return new Promise((resolve16) => {
248
+ return new Promise((resolve17) => {
249
249
  execFile(
250
250
  "git",
251
251
  ["rev-parse", "--abbrev-ref", "HEAD"],
252
252
  { cwd: root, timeout: 5e3, windowsHide: true, encoding: "utf8" },
253
253
  (err, stdout) => {
254
- if (err) return resolve16(void 0);
254
+ if (err) return resolve17(void 0);
255
255
  const name = String(stdout).trim();
256
- resolve16(name && name !== "HEAD" ? name : void 0);
256
+ resolve17(name && name !== "HEAD" ? name : void 0);
257
257
  }
258
258
  );
259
259
  });
@@ -584,8 +584,8 @@ function writeExport(graph, target) {
584
584
  ExitCode.USAGE_ERROR
585
585
  );
586
586
  }
587
- fs6.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
588
- fs6.writeFileSync(target, content);
587
+ fs7.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
588
+ fs7.writeFileSync(target, content);
589
589
  }
590
590
  function collect(value, prev) {
591
591
  return [...prev, value];
@@ -604,7 +604,7 @@ async function runStatus(global) {
604
604
  const root = path.resolve(global.cwd ?? ".");
605
605
  const graphPath = global.graph ?? defaultGraphPath(root);
606
606
  const graph = loadGraph(root, graphPath);
607
- const hasCache = fs6.existsSync(path.join(cacheDir(root), "parse-cache.json"));
607
+ const hasCache = fs7.existsSync(path.join(cacheDir(root), "parse-cache.json"));
608
608
  let stale = null;
609
609
  let staleExact = false;
610
610
  if (graph) {
@@ -1289,14 +1289,14 @@ var INSTALL_ID_FILE = path.join(INSTALL_ID_DIR, "install-id");
1289
1289
  var SEND_TIMEOUT_MS = 4e3;
1290
1290
  function installId() {
1291
1291
  try {
1292
- const existing = fs6.readFileSync(INSTALL_ID_FILE, "utf8").trim();
1292
+ const existing = fs7.readFileSync(INSTALL_ID_FILE, "utf8").trim();
1293
1293
  if (isUuid(existing)) return existing;
1294
1294
  } catch {
1295
1295
  }
1296
1296
  const id = randomUuid();
1297
1297
  try {
1298
- fs6.mkdirSync(INSTALL_ID_DIR, { recursive: true });
1299
- fs6.writeFileSync(INSTALL_ID_FILE, id + "\n", { mode: 384 });
1298
+ fs7.mkdirSync(INSTALL_ID_DIR, { recursive: true });
1299
+ fs7.writeFileSync(INSTALL_ID_FILE, id + "\n", { mode: 384 });
1300
1300
  } catch {
1301
1301
  }
1302
1302
  return id;
@@ -1320,7 +1320,7 @@ function ledgerFilePath(root) {
1320
1320
  }
1321
1321
  function readOffset(root) {
1322
1322
  try {
1323
- const raw = JSON.parse(fs6.readFileSync(offsetPath(root), "utf8"));
1323
+ const raw = JSON.parse(fs7.readFileSync(offsetPath(root), "utf8"));
1324
1324
  return typeof raw.offset === "number" && raw.offset >= 0 ? raw.offset : 0;
1325
1325
  } catch {
1326
1326
  return 0;
@@ -1328,8 +1328,8 @@ function readOffset(root) {
1328
1328
  }
1329
1329
  function writeOffset(root, offset) {
1330
1330
  try {
1331
- fs6.mkdirSync(cacheDir(root), { recursive: true });
1332
- fs6.writeFileSync(offsetPath(root), JSON.stringify({ offset }) + "\n");
1331
+ fs7.mkdirSync(cacheDir(root), { recursive: true });
1332
+ fs7.writeFileSync(offsetPath(root), JSON.stringify({ offset }) + "\n");
1333
1333
  } catch {
1334
1334
  }
1335
1335
  }
@@ -1337,7 +1337,7 @@ function buildBatch(root, fromOffset, id = installId()) {
1337
1337
  const file = ledgerFilePath(root);
1338
1338
  let size = 0;
1339
1339
  try {
1340
- size = fs6.statSync(file).size;
1340
+ size = fs7.statSync(file).size;
1341
1341
  } catch {
1342
1342
  return { batch: null, newOffset: 0 };
1343
1343
  }
@@ -1345,13 +1345,13 @@ function buildBatch(root, fromOffset, id = installId()) {
1345
1345
  if (start >= size) return { batch: null, newOffset: size };
1346
1346
  let text = "";
1347
1347
  try {
1348
- const fd = fs6.openSync(file, "r");
1348
+ const fd = fs7.openSync(file, "r");
1349
1349
  try {
1350
1350
  const buf = Buffer.alloc(size - start);
1351
- fs6.readSync(fd, buf, 0, buf.length, start);
1351
+ fs7.readSync(fd, buf, 0, buf.length, start);
1352
1352
  text = buf.toString("utf8");
1353
1353
  } finally {
1354
- fs6.closeSync(fd);
1354
+ fs7.closeSync(fd);
1355
1355
  }
1356
1356
  } catch {
1357
1357
  return { batch: null, newOffset: fromOffset };
@@ -1497,7 +1497,7 @@ function registerServe(program) {
1497
1497
  }
1498
1498
  async function ensureServableGraph(root, graphPath, global, refresh, opts = {}) {
1499
1499
  if (refresh) {
1500
- if (!fs6.existsSync(graphPath)) {
1500
+ if (!fs7.existsSync(graphPath)) {
1501
1501
  info(c.dim("vg \xB7 no map found \u2014 building it before serving\u2026"));
1502
1502
  await runBuild(
1503
1503
  [],
@@ -1514,7 +1514,7 @@ async function ensureServableGraph(root, graphPath, global, refresh, opts = {})
1514
1514
  }
1515
1515
  }
1516
1516
  }
1517
- if (!fs6.existsSync(graphPath)) {
1517
+ if (!fs7.existsSync(graphPath)) {
1518
1518
  throw new CliError(
1519
1519
  `no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
1520
1520
  ExitCode.NOT_FOUND
@@ -1569,12 +1569,587 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
1569
1569
  info(c.red(`vg serve: request error: ${err.message}`));
1570
1570
  }
1571
1571
  });
1572
- await new Promise((resolve16) => httpServer.listen(port, host, resolve16));
1572
+ await new Promise((resolve17) => httpServer.listen(port, host, resolve17));
1573
1573
  info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
1574
1574
  await new Promise(() => {
1575
1575
  });
1576
1576
  }
1577
1577
 
1578
+ // src/lsp/protocol.ts
1579
+ var ErrorCodes = {
1580
+ MethodNotFound: -32601,
1581
+ InternalError: -32603
1582
+ };
1583
+ var MessageReader = class {
1584
+ buf = Buffer.alloc(0);
1585
+ /** Feed a chunk; returns every complete message now available. */
1586
+ push(chunk) {
1587
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
1588
+ const out2 = [];
1589
+ for (; ; ) {
1590
+ const headerEnd = this.buf.indexOf("\r\n\r\n");
1591
+ if (headerEnd === -1) break;
1592
+ const header = this.buf.subarray(0, headerEnd).toString("ascii");
1593
+ const match = /content-length:\s*(\d+)/i.exec(header);
1594
+ if (!match) {
1595
+ this.buf = this.buf.subarray(headerEnd + 4);
1596
+ continue;
1597
+ }
1598
+ const length = Number(match[1]);
1599
+ const bodyStart = headerEnd + 4;
1600
+ if (this.buf.length < bodyStart + length) break;
1601
+ const body = this.buf.subarray(bodyStart, bodyStart + length).toString("utf8");
1602
+ this.buf = this.buf.subarray(bodyStart + length);
1603
+ try {
1604
+ out2.push(JSON.parse(body));
1605
+ } catch {
1606
+ }
1607
+ }
1608
+ return out2;
1609
+ }
1610
+ };
1611
+ var MessageWriter = class {
1612
+ constructor(out2) {
1613
+ this.out = out2;
1614
+ }
1615
+ out;
1616
+ write(msg) {
1617
+ const body = Buffer.from(JSON.stringify(msg), "utf8");
1618
+ this.out.write(`Content-Length: ${body.length}\r
1619
+ \r
1620
+ `);
1621
+ this.out.write(body);
1622
+ }
1623
+ };
1624
+ var Connection = class {
1625
+ reader = new MessageReader();
1626
+ writer;
1627
+ requests = /* @__PURE__ */ new Map();
1628
+ notifications = /* @__PURE__ */ new Map();
1629
+ constructor(input = process.stdin, output = process.stdout) {
1630
+ this.writer = new MessageWriter(output);
1631
+ input.on("data", (chunk) => {
1632
+ for (const msg of this.reader.push(chunk)) void this.dispatch(msg);
1633
+ });
1634
+ }
1635
+ onRequest(method, handler) {
1636
+ this.requests.set(method, handler);
1637
+ }
1638
+ onNotification(method, handler) {
1639
+ this.notifications.set(method, handler);
1640
+ }
1641
+ /** Push a server→client notification (diagnostics, `vibgrate/score`, …). */
1642
+ notify(method, params) {
1643
+ this.writer.write({ jsonrpc: "2.0", method, params });
1644
+ }
1645
+ async dispatch(msg) {
1646
+ if (!msg.method) return;
1647
+ const isRequest = msg.id !== void 0 && msg.id !== null;
1648
+ if (!isRequest) {
1649
+ const handler2 = this.notifications.get(msg.method);
1650
+ if (handler2) {
1651
+ try {
1652
+ await handler2(msg.method, msg.params);
1653
+ } catch {
1654
+ }
1655
+ }
1656
+ return;
1657
+ }
1658
+ const handler = this.requests.get(msg.method);
1659
+ if (!handler) {
1660
+ this.writer.write({
1661
+ jsonrpc: "2.0",
1662
+ id: msg.id,
1663
+ error: { code: ErrorCodes.MethodNotFound, message: `Unhandled method: ${msg.method}` }
1664
+ });
1665
+ return;
1666
+ }
1667
+ try {
1668
+ const result = await handler(msg.method, msg.params);
1669
+ this.writer.write({ jsonrpc: "2.0", id: msg.id, result: result ?? null });
1670
+ } catch (err) {
1671
+ this.writer.write({
1672
+ jsonrpc: "2.0",
1673
+ id: msg.id,
1674
+ error: {
1675
+ code: ErrorCodes.InternalError,
1676
+ message: err instanceof Error ? err.message : "Internal error"
1677
+ }
1678
+ });
1679
+ }
1680
+ }
1681
+ };
1682
+
1683
+ // src/lsp/manifest-positions.ts
1684
+ function manifestKind(filePath) {
1685
+ const base = (filePath.split(/[\\/]/).pop() ?? "").toLowerCase();
1686
+ if (base === "package.json") return "package.json";
1687
+ if (base === "go.mod") return "go.mod";
1688
+ if (base === "requirements.txt" || /^requirements.*\.txt$/.test(base)) return "requirements.txt";
1689
+ if (base === "pyproject.toml") return "pyproject.toml";
1690
+ if (base === "cargo.toml") return "Cargo.toml";
1691
+ if (base === "pom.xml") return "pom.xml";
1692
+ if (base === "build.gradle" || base === "build.gradle.kts") return "build.gradle";
1693
+ if (base === "gemfile") return "Gemfile";
1694
+ if (base === "composer.json") return "composer.json";
1695
+ if (base === "pubspec.yaml") return "pubspec.yaml";
1696
+ if (base === "mix.exs") return "mix.exs";
1697
+ if (base.endsWith(".csproj") || base.endsWith(".fsproj") || base.endsWith(".vbproj")) return "csproj";
1698
+ if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Dockerfile";
1699
+ return "unknown";
1700
+ }
1701
+ function isManifest(filePath) {
1702
+ return manifestKind(filePath) !== "unknown";
1703
+ }
1704
+ function esc(s) {
1705
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1706
+ }
1707
+ function findPackageLine(text, pkg, kind) {
1708
+ const lines = text.split(/\r?\n/);
1709
+ const p = esc(pkg);
1710
+ const patterns = (() => {
1711
+ switch (kind) {
1712
+ case "package.json":
1713
+ case "composer.json":
1714
+ return [new RegExp(`^\\s*"${p}"\\s*:`)];
1715
+ case "go.mod":
1716
+ return [new RegExp(`^\\s*(?:require\\s+)?${p}\\s+v?\\d`)];
1717
+ case "requirements.txt":
1718
+ return [new RegExp(`^\\s*${p}\\s*(?:\\[[^\\]]*\\])?\\s*(?:[=<>!~]|$)`, "i")];
1719
+ case "pyproject.toml":
1720
+ case "Cargo.toml":
1721
+ return [
1722
+ new RegExp(`^\\s*${p}\\s*=`),
1723
+ new RegExp(`^\\s*"${p}\\s*(?:[=<>!~\\[]|")`)
1724
+ ];
1725
+ case "pom.xml":
1726
+ return [new RegExp(`<artifactId>\\s*${p}\\s*</artifactId>`)];
1727
+ case "build.gradle":
1728
+ return [new RegExp(`["']${p}(?:["':])`)];
1729
+ case "Gemfile":
1730
+ return [new RegExp(`^\\s*gem\\s+["']${p}["']`)];
1731
+ case "pubspec.yaml":
1732
+ return [new RegExp(`^\\s*${p}\\s*:`)];
1733
+ case "mix.exs":
1734
+ return [new RegExp(`\\{\\s*:${p}\\s*,`)];
1735
+ case "csproj":
1736
+ return [new RegExp(`Include\\s*=\\s*"${p}"`, "i")];
1737
+ case "Dockerfile":
1738
+ return [new RegExp(`^\\s*FROM\\s+.*${p}`, "i")];
1739
+ default:
1740
+ return [];
1741
+ }
1742
+ })();
1743
+ for (const re of patterns) {
1744
+ for (let i = 0; i < lines.length; i++) {
1745
+ if (re.test(lines[i])) return i;
1746
+ }
1747
+ }
1748
+ return -1;
1749
+ }
1750
+ function findRuntimeLine(text, kind) {
1751
+ const lines = text.split(/\r?\n/);
1752
+ const patterns = (() => {
1753
+ switch (kind) {
1754
+ case "package.json":
1755
+ return [/^\s*"node"\s*:/, /^\s*"engines"\s*:/];
1756
+ case "go.mod":
1757
+ return [/^\s*go\s+\d/];
1758
+ case "pyproject.toml":
1759
+ return [/^\s*(?:requires-python|python)\s*=/];
1760
+ case "pom.xml":
1761
+ return [/<(?:java\.version|maven\.compiler\.(?:source|target|release))>/];
1762
+ case "build.gradle":
1763
+ return [/(?:sourceCompatibility|targetCompatibility|JavaVersion)/];
1764
+ case "csproj":
1765
+ return [/<TargetFrameworks?>/i];
1766
+ case "Dockerfile":
1767
+ return [/^\s*FROM\s+/i];
1768
+ default:
1769
+ return [];
1770
+ }
1771
+ })();
1772
+ for (const re of patterns) {
1773
+ for (let i = 0; i < lines.length; i++) {
1774
+ if (re.test(lines[i])) return i;
1775
+ }
1776
+ }
1777
+ return -1;
1778
+ }
1779
+ function endOfLine(text, line) {
1780
+ const lines = text.split(/\r?\n/);
1781
+ return (lines[line] ?? "").length;
1782
+ }
1783
+
1784
+ // src/lsp/server.ts
1785
+ function bandForDependency(dep) {
1786
+ if (dep.drift === "major-behind") {
1787
+ return (dep.majorsBehind ?? 1) >= 2 ? "high" : "moderate";
1788
+ }
1789
+ if (dep.drift === "minor-behind") return "moderate";
1790
+ return "low";
1791
+ }
1792
+ function severityFor(level) {
1793
+ if (level === "note") return 3;
1794
+ return 2;
1795
+ }
1796
+ var DIAGNOSTIC_RULES = /* @__PURE__ */ new Set([
1797
+ "vibgrate/runtime-eol",
1798
+ "vibgrate/runtime-lag",
1799
+ "vibgrate/unmaintained",
1800
+ "vibgrate/abandoned",
1801
+ "vibgrate/license-change",
1802
+ "vibgrate/eol"
1803
+ ]);
1804
+ var VibgrateLanguageServer = class {
1805
+ constructor(opts, output = process.stdout) {
1806
+ this.opts = opts;
1807
+ this.conn = new Connection(process.stdin, output);
1808
+ this.register();
1809
+ }
1810
+ opts;
1811
+ conn;
1812
+ docs = /* @__PURE__ */ new Map();
1813
+ // uri → text
1814
+ artifact = null;
1815
+ scanning = false;
1816
+ queued = false;
1817
+ debounce = null;
1818
+ shuttingDown = false;
1819
+ register() {
1820
+ this.conn.onRequest("initialize", () => ({
1821
+ capabilities: {
1822
+ // 1 = Full sync. We only actually act on save, but full sync keeps our
1823
+ // buffer honest so hovers land on the text the user is looking at.
1824
+ textDocumentSync: 1,
1825
+ hoverProvider: true,
1826
+ codeLensProvider: { resolveProvider: false },
1827
+ executeCommandProvider: {
1828
+ commands: ["vibgrate.rescan", "vibgrate.explain", "vibgrate.routeFix"]
1829
+ }
1830
+ },
1831
+ serverInfo: { name: "Vibgrate", version: VERSION }
1832
+ }));
1833
+ this.conn.onNotification("initialized", () => this.scheduleScan(0));
1834
+ this.conn.onNotification("textDocument/didOpen", (_m, params) => {
1835
+ const p = params;
1836
+ const uri = p.textDocument?.uri;
1837
+ if (!uri || !isManifest(uriToPath(uri))) return;
1838
+ this.docs.set(uri, p.textDocument?.text ?? "");
1839
+ if (this.artifact) this.publishForDoc(uri);
1840
+ else this.scheduleScan(0);
1841
+ });
1842
+ this.conn.onNotification("textDocument/didChange", (_m, params) => {
1843
+ const p = params;
1844
+ const uri = p.textDocument?.uri;
1845
+ if (!uri || !this.docs.has(uri)) return;
1846
+ const full = p.contentChanges?.[p.contentChanges.length - 1]?.text;
1847
+ if (typeof full === "string") this.docs.set(uri, full);
1848
+ if (this.artifact) this.publishForDoc(uri);
1849
+ });
1850
+ this.conn.onNotification("textDocument/didSave", (_m, params) => {
1851
+ const p = params;
1852
+ const uri = p.textDocument?.uri;
1853
+ if (!uri || !this.docs.has(uri)) return;
1854
+ this.scheduleScan(400);
1855
+ });
1856
+ this.conn.onNotification("textDocument/didClose", (_m, params) => {
1857
+ const p = params;
1858
+ if (p.textDocument?.uri) this.docs.delete(p.textDocument.uri);
1859
+ });
1860
+ this.conn.onRequest("textDocument/hover", (_m, params) => this.onHover(params));
1861
+ this.conn.onRequest("textDocument/codeLens", (_m, params) => this.onCodeLens(params));
1862
+ this.conn.onRequest("workspace/executeCommand", (_m, params) => {
1863
+ const p = params;
1864
+ if (p.command === "vibgrate.rescan") {
1865
+ this.scheduleScan(0);
1866
+ return { ok: true };
1867
+ }
1868
+ return null;
1869
+ });
1870
+ this.conn.onRequest("shutdown", () => {
1871
+ this.shuttingDown = true;
1872
+ if (this.debounce) clearTimeout(this.debounce);
1873
+ return null;
1874
+ });
1875
+ this.conn.onNotification("exit", () => {
1876
+ process.exit(this.shuttingDown ? 0 : 1);
1877
+ });
1878
+ }
1879
+ // ── Scanning ─────────────────────────────────────────────────────────────
1880
+ scheduleScan(delayMs) {
1881
+ if (this.debounce) clearTimeout(this.debounce);
1882
+ this.debounce = setTimeout(() => void this.scan(), delayMs);
1883
+ }
1884
+ async scan() {
1885
+ if (this.scanning) {
1886
+ this.queued = true;
1887
+ return;
1888
+ }
1889
+ this.scanning = true;
1890
+ try {
1891
+ const scanOpts = {
1892
+ format: "json",
1893
+ concurrency: 8,
1894
+ offline: this.opts.offline,
1895
+ noLocalArtifacts: true,
1896
+ // an editor scan must not litter the repo
1897
+ noGraph: true,
1898
+ // the code map is not needed to score drift; skip the cost
1899
+ quiet: true,
1900
+ // no spinners, no artifact dump — an editor is not a terminal
1901
+ vibgrateVersion: VERSION
1902
+ };
1903
+ const advanced = await loadAdvancedScanHook();
1904
+ this.artifact = await runCoreScan(this.opts.root, scanOpts, advanced);
1905
+ this.publishScore();
1906
+ for (const uri of this.docs.keys()) this.publishForDoc(uri);
1907
+ } catch (err) {
1908
+ this.conn.notify("window/logMessage", {
1909
+ type: 3,
1910
+ message: `Vibgrate scan failed: ${err instanceof Error ? err.message : String(err)}`
1911
+ });
1912
+ this.conn.notify("vibgrate/status", { state: "error" });
1913
+ } finally {
1914
+ this.scanning = false;
1915
+ if (this.queued) {
1916
+ this.queued = false;
1917
+ this.scheduleScan(0);
1918
+ }
1919
+ }
1920
+ }
1921
+ // ── Publishing ───────────────────────────────────────────────────────────
1922
+ publishScore() {
1923
+ const a = this.artifact;
1924
+ if (!a) return;
1925
+ let behind = 0;
1926
+ let unmaintained = 0;
1927
+ let total = 0;
1928
+ for (const proj of a.projects ?? []) {
1929
+ for (const dep of proj.dependencies ?? []) {
1930
+ total++;
1931
+ if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
1932
+ }
1933
+ }
1934
+ const eol = (a.findings ?? []).filter(
1935
+ (f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
1936
+ ).length;
1937
+ unmaintained = (a.findings ?? []).filter(
1938
+ (f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
1939
+ ).length;
1940
+ const band = a.drift.riskLevel ?? "low";
1941
+ const payload = {
1942
+ score: a.drift.score,
1943
+ band,
1944
+ // v3 §2.4: `estimated` means "no timestamps at all" — it is NOT an
1945
+ // offline marker. An air-gapped scan against a dated snapshot is Verified.
1946
+ mode: hasReleaseDates(a) ? "verified" : "estimated",
1947
+ methodology: a.drift.methodologyVersion ?? "unknown",
1948
+ scale: "0 best, 100 worst",
1949
+ counts: { behind, eol, unmaintained, total },
1950
+ rootPath: a.rootPath,
1951
+ scannedAt: a.timestamp
1952
+ };
1953
+ this.conn.notify("vibgrate/score", payload);
1954
+ }
1955
+ /** Decorations + (optionally) diagnostics for one open manifest. */
1956
+ publishForDoc(uri) {
1957
+ const a = this.artifact;
1958
+ const text = this.docs.get(uri);
1959
+ if (!a || text === void 0) return;
1960
+ const filePath = uriToPath(uri);
1961
+ const kind = manifestKind(filePath);
1962
+ const project = projectForFile(this.opts.root, a, filePath);
1963
+ const items = [];
1964
+ for (const dep of project?.dependencies ?? []) {
1965
+ if (dep.drift === "current" || dep.drift === "unknown") continue;
1966
+ const line = findPackageLine(text, dep.package, kind);
1967
+ if (line === -1) continue;
1968
+ items.push({
1969
+ line,
1970
+ endCol: endOfLine(text, line),
1971
+ text: inlineLabel(dep),
1972
+ band: bandForDependency(dep),
1973
+ package: dep.package
1974
+ });
1975
+ }
1976
+ this.conn.notify("vibgrate/inline", { uri, items });
1977
+ if (!this.opts.diagnostics) {
1978
+ this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics: [] });
1979
+ return;
1980
+ }
1981
+ const diagnostics = (a.findings ?? []).filter((f) => DIAGNOSTIC_RULES.has(f.ruleId)).filter((f) => project && sameProject(f.location, project.path)).map((f) => {
1982
+ const line = Math.max(0, findRuntimeLine(text, kind));
1983
+ return {
1984
+ range: {
1985
+ start: { line, character: 0 },
1986
+ end: { line, character: endOfLine(text, line) }
1987
+ },
1988
+ severity: severityFor(f.level),
1989
+ // never 1 — see file header
1990
+ source: "vibgrate",
1991
+ code: f.ruleId,
1992
+ message: f.message
1993
+ };
1994
+ });
1995
+ this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics });
1996
+ }
1997
+ // ── Requests ─────────────────────────────────────────────────────────────
1998
+ onHover(params) {
1999
+ const p = params;
2000
+ const uri = p.textDocument?.uri;
2001
+ const line = p.position?.line;
2002
+ const a = this.artifact;
2003
+ const text = uri ? this.docs.get(uri) : void 0;
2004
+ if (!uri || line === void 0 || !a || text === void 0) return null;
2005
+ const filePath = uriToPath(uri);
2006
+ const kind = manifestKind(filePath);
2007
+ const project = projectForFile(this.opts.root, a, filePath);
2008
+ const dep = (project?.dependencies ?? []).find(
2009
+ (d) => findPackageLine(text, d.package, kind) === line
2010
+ );
2011
+ if (!dep) return null;
2012
+ return {
2013
+ contents: { kind: "markdown", value: hoverMarkdown(dep) }
2014
+ };
2015
+ }
2016
+ onCodeLens(params) {
2017
+ const p = params;
2018
+ const uri = p.textDocument?.uri;
2019
+ const a = this.artifact;
2020
+ if (!uri || !a || !this.docs.has(uri)) return [];
2021
+ const behind = (a.projects ?? []).reduce(
2022
+ (n, proj) => n + (proj.dependencies ?? []).filter(
2023
+ (d) => d.drift === "major-behind" || d.drift === "minor-behind"
2024
+ ).length,
2025
+ 0
2026
+ );
2027
+ const eol = (a.findings ?? []).filter(
2028
+ (f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
2029
+ ).length;
2030
+ const mode = hasReleaseDates(a) ? "" : "~";
2031
+ const title = `Vibgrate \xB7 drift ${mode}${a.drift.score} (${a.drift.riskLevel}) \xB7 ${behind} behind \xB7 ${eol} EOL`;
2032
+ return [
2033
+ {
2034
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
2035
+ command: { title, command: "vibgrate.openPanel", arguments: [] }
2036
+ }
2037
+ ];
2038
+ }
2039
+ };
2040
+ function uriToPath(uri) {
2041
+ try {
2042
+ return fileURLToPath(uri);
2043
+ } catch {
2044
+ return uri;
2045
+ }
2046
+ }
2047
+ function projectForFile(root, a, filePath) {
2048
+ const dir = path.dirname(path.resolve(filePath));
2049
+ let best;
2050
+ let bestLen = -1;
2051
+ for (const proj of a.projects ?? []) {
2052
+ const projDir = path.resolve(root, proj.path);
2053
+ if ((dir === projDir || dir.startsWith(projDir + path.sep)) && projDir.length > bestLen) {
2054
+ best = proj;
2055
+ bestLen = projDir.length;
2056
+ }
2057
+ }
2058
+ return best;
2059
+ }
2060
+ function sameProject(location, projectPath) {
2061
+ return path.normalize(location) === path.normalize(projectPath);
2062
+ }
2063
+ function hasReleaseDates(a) {
2064
+ for (const proj of a.projects ?? []) {
2065
+ for (const dep of proj.dependencies ?? []) {
2066
+ if (dep.ageDays !== null && dep.ageDays !== void 0) return true;
2067
+ }
2068
+ }
2069
+ return false;
2070
+ }
2071
+ function inlineLabel(dep) {
2072
+ const bits = [];
2073
+ if (dep.drift === "major-behind") {
2074
+ const n = dep.majorsBehind ?? 1;
2075
+ bits.push(`${n} major behind`);
2076
+ } else if (dep.drift === "minor-behind") {
2077
+ bits.push("minor behind");
2078
+ }
2079
+ if (dep.latestStable) bits.push(dep.latestStable);
2080
+ if (dep.ageDays != null && dep.ageDays >= 1) {
2081
+ bits.push(`${Math.round(dep.ageDays).toLocaleString("en-US")}d stale`);
2082
+ }
2083
+ return bits.join(" \xB7 ");
2084
+ }
2085
+ function hoverMarkdown(dep) {
2086
+ const lines = [];
2087
+ lines.push(`**${dep.package}**`);
2088
+ lines.push("");
2089
+ const yours = dep.resolvedVersion ?? dep.currentSpec;
2090
+ if (dep.latestStable && dep.latestStable !== yours) {
2091
+ lines.push(`| | |`);
2092
+ lines.push(`|---|---|`);
2093
+ lines.push(`| yours | \`${yours}\` |`);
2094
+ lines.push(`| latest | \`${dep.latestStable}\` |`);
2095
+ if (dep.majorsBehind) lines.push(`| majors behind | ${dep.majorsBehind} |`);
2096
+ if (dep.ageDays != null && dep.ageDays >= 1) {
2097
+ lines.push(`| age | ${Math.round(dep.ageDays).toLocaleString("en-US")} days |`);
2098
+ }
2099
+ if (dep.libyears != null && dep.libyears >= 0.01) {
2100
+ lines.push(`| libyears | ${dep.libyears.toFixed(2)} |`);
2101
+ }
2102
+ const license = dep.license?.spdxId ?? dep.license?.raw;
2103
+ if (license) lines.push(`| license | ${license} |`);
2104
+ } else {
2105
+ lines.push(`\`${yours}\` \u2014 current.`);
2106
+ }
2107
+ lines.push("");
2108
+ lines.push("$(shield) Resolved locally from your lockfile. No source uploaded.");
2109
+ return lines.join("\n");
2110
+ }
2111
+ function startLanguageServer(opts) {
2112
+ if (!fs7.existsSync(opts.root)) {
2113
+ process.stderr.write(`vg lsp: root does not exist: ${opts.root}
2114
+ `);
2115
+ process.exit(1);
2116
+ }
2117
+ const realWrite = process.stdout.write.bind(process.stdout);
2118
+ const protocolOut = {
2119
+ write(chunk) {
2120
+ return realWrite(chunk);
2121
+ }
2122
+ };
2123
+ process.stdout.write = ((chunk, ...rest) => process.stderr.write(chunk, ...rest));
2124
+ process.stderr.write(
2125
+ `vg lsp ${VERSION} \u2014 serving ${pathToFileURL(opts.root).href}${opts.offline ? " (offline)" : ""}
2126
+ `
2127
+ );
2128
+ new VibgrateLanguageServer(opts, protocolOut);
2129
+ }
2130
+
2131
+ // src/commands/lsp.ts
2132
+ function registerLsp(program) {
2133
+ const cmd = program.command("lsp").description("start the Vibgrate language server (stdio) \u2014 the engine behind the IDE extensions").option(
2134
+ "--diagnostics",
2135
+ "also publish Problems-panel diagnostics (EOL runtime, unmaintained, license change). Off by default: drift is not a defect, and the Problems panel is not ours to fill",
2136
+ false
2137
+ ).action(async function(opts) {
2138
+ const global = readGlobal(this);
2139
+ const root = rootOf(global);
2140
+ startLanguageServer({
2141
+ root,
2142
+ // `--local` is the established air-gap flag across the CLI; honour it
2143
+ // here too, so an offline editor session stays offline.
2144
+ offline: global.local === true,
2145
+ diagnostics: opts.diagnostics === true
2146
+ });
2147
+ await new Promise(() => {
2148
+ });
2149
+ });
2150
+ applyGlobalOptions(cmd);
2151
+ }
2152
+
1578
2153
  // src/commands/install.ts
1579
2154
  var SMALL_REPO_FILES = 150;
1580
2155
  function registerInstall(program) {
@@ -1593,7 +2168,7 @@ function registerInstall(program) {
1593
2168
  }
1594
2169
  return;
1595
2170
  }
1596
- const targets = opts.all ? ASSISTANTS : tools.map(resolve5);
2171
+ const targets = opts.all ? ASSISTANTS : tools.map(resolve6);
1597
2172
  if (targets.length === 0) throw usageError("name an assistant (e.g. `vg install claude`) or use --all / --list");
1598
2173
  const graph = loadGraph(root, global.graph);
1599
2174
  const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
@@ -1617,7 +2192,7 @@ function registerInstall(program) {
1617
2192
  const uninstall = program.command("uninstall").description("remove vg from an AI assistant").argument("<tools...>", "assistant ids").option("--purge", "also delete the skill file").action(function(tools, opts) {
1618
2193
  const global = readGlobal(this);
1619
2194
  const root = rootOf(global);
1620
- const results = tools.map(resolve5).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
2195
+ const results = tools.map(resolve6).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
1621
2196
  if (global.json) {
1622
2197
  json({ root, results });
1623
2198
  return;
@@ -1628,7 +2203,7 @@ function registerInstall(program) {
1628
2203
  });
1629
2204
  applyGlobalOptions(uninstall);
1630
2205
  }
1631
- function resolve5(id) {
2206
+ function resolve6(id) {
1632
2207
  const a = assistantById(id);
1633
2208
  if (!a) {
1634
2209
  const near = ASSISTANTS.filter(
@@ -1649,7 +2224,7 @@ function registerShare(program) {
1649
2224
  const global = readGlobal(this);
1650
2225
  const root = rootOf(global);
1651
2226
  const gitDir = path.join(root, ".git");
1652
- if (!fs6.existsSync(gitDir)) {
2227
+ if (!fs7.existsSync(gitDir)) {
1653
2228
  throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
1654
2229
  }
1655
2230
  if (opts.undo) {
@@ -1671,22 +2246,22 @@ function registerShare(program) {
1671
2246
  }
1672
2247
  function writeVibgrateGitignore(root, reports) {
1673
2248
  const dir = path.join(root, ".vibgrate");
1674
- fs6.mkdirSync(dir, { recursive: true });
2249
+ fs7.mkdirSync(dir, { recursive: true });
1675
2250
  const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
1676
2251
  if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
1677
- fs6.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
2252
+ fs7.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
1678
2253
  `);
1679
2254
  }
1680
2255
  function writeGitAttributes(root) {
1681
2256
  const file = path.join(root, ".gitattributes");
1682
2257
  const line = ".vibgrate/graph.json merge=vg";
1683
- let content = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
2258
+ let content = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "";
1684
2259
  if (!content.split("\n").some((l) => l.trim() === line)) {
1685
2260
  content = content.length ? `${content.replace(/\s*$/, "")}
1686
2261
  ${line}
1687
2262
  ` : `${line}
1688
2263
  `;
1689
- fs6.writeFileSync(file, content);
2264
+ fs7.writeFileSync(file, content);
1690
2265
  }
1691
2266
  }
1692
2267
  function configureMergeDriver(root) {
@@ -1695,7 +2270,7 @@ function configureMergeDriver(root) {
1695
2270
  }
1696
2271
  function installPreCommitHook(root) {
1697
2272
  const hooksDir = hookDir(root);
1698
- fs6.mkdirSync(hooksDir, { recursive: true });
2273
+ fs7.mkdirSync(hooksDir, { recursive: true });
1699
2274
  const file = path.join(hooksDir, "pre-commit");
1700
2275
  const block = [
1701
2276
  HOOK_BEGIN,
@@ -1705,7 +2280,7 @@ function installPreCommitHook(root) {
1705
2280
  "fi",
1706
2281
  HOOK_END
1707
2282
  ].join("\n");
1708
- let content = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "#!/bin/sh\n";
2283
+ let content = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "#!/bin/sh\n";
1709
2284
  const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
1710
2285
  if (re.test(content)) content = content.replace(re, block);
1711
2286
  else content = `${content.replace(/\s*$/, "")}
@@ -1714,21 +2289,21 @@ ${block}
1714
2289
  `;
1715
2290
  if (!content.startsWith("#!")) content = `#!/bin/sh
1716
2291
  ${content}`;
1717
- fs6.writeFileSync(file, content);
1718
- fs6.chmodSync(file, 493);
2292
+ fs7.writeFileSync(file, content);
2293
+ fs7.chmodSync(file, 493);
1719
2294
  }
1720
2295
  function undo(root) {
1721
2296
  const hookFile = path.join(hookDir(root), "pre-commit");
1722
- if (fs6.existsSync(hookFile)) {
2297
+ if (fs7.existsSync(hookFile)) {
1723
2298
  const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
1724
- const next2 = fs6.readFileSync(hookFile, "utf8").replace(re, "\n");
1725
- if (next2.trim() === "#!/bin/sh") fs6.rmSync(hookFile);
1726
- else fs6.writeFileSync(hookFile, next2);
2299
+ const next2 = fs7.readFileSync(hookFile, "utf8").replace(re, "\n");
2300
+ if (next2.trim() === "#!/bin/sh") fs7.rmSync(hookFile);
2301
+ else fs7.writeFileSync(hookFile, next2);
1727
2302
  }
1728
2303
  const attr = path.join(root, ".gitattributes");
1729
- if (fs6.existsSync(attr)) {
1730
- const next2 = fs6.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
1731
- fs6.writeFileSync(attr, next2);
2304
+ if (fs7.existsSync(attr)) {
2305
+ const next2 = fs7.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
2306
+ fs7.writeFileSync(attr, next2);
1732
2307
  }
1733
2308
  try {
1734
2309
  git(root, ["config", "--remove-section", "merge.vg"]);
@@ -1861,7 +2436,7 @@ function grepBaselineTokens(term, files) {
1861
2436
  if (f.size > 512 * 1024) continue;
1862
2437
  let content = "";
1863
2438
  try {
1864
- content = fs6.readFileSync(f.abs, "utf8");
2439
+ content = fs7.readFileSync(f.abs, "utf8");
1865
2440
  } catch {
1866
2441
  continue;
1867
2442
  }
@@ -1871,7 +2446,7 @@ function grepBaselineTokens(term, files) {
1871
2446
  }
1872
2447
  function safeSize(abs) {
1873
2448
  try {
1874
- return fs6.statSync(abs).size;
2449
+ return fs7.statSync(abs).size;
1875
2450
  } catch {
1876
2451
  return 0;
1877
2452
  }
@@ -2205,7 +2780,7 @@ function loadStandards(root) {
2205
2780
  const p = path__default.join(root, rel3);
2206
2781
  let text;
2207
2782
  try {
2208
- text = fs6__default.readFileSync(p, "utf8");
2783
+ text = fs7__default.readFileSync(p, "utf8");
2209
2784
  } catch {
2210
2785
  continue;
2211
2786
  }
@@ -2443,7 +3018,7 @@ async function addCmd(root, rest, opts, asJson) {
2443
3018
  }
2444
3019
  function readPackageVersion(root) {
2445
3020
  try {
2446
- const pkg = JSON.parse(fs6__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
3021
+ const pkg = JSON.parse(fs7__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
2447
3022
  return typeof pkg.version === "string" ? pkg.version : void 0;
2448
3023
  } catch {
2449
3024
  return void 0;
@@ -2451,7 +3026,7 @@ function readPackageVersion(root) {
2451
3026
  }
2452
3027
  function readOptionalFile(p) {
2453
3028
  try {
2454
- const t = fs6__default.readFileSync(p, "utf8");
3029
+ const t = fs7__default.readFileSync(p, "utf8");
2455
3030
  return t.trim() ? t : void 0;
2456
3031
  } catch {
2457
3032
  return void 0;
@@ -2535,7 +3110,8 @@ async function showCmd(root, name, budget, asJson, opts = {}) {
2535
3110
  if (online && !quality.sufficient) {
2536
3111
  const dsn = resolveDsn();
2537
3112
  const parsed = dsn ? parseDsn$1(dsn) : null;
2538
- hosted = await fetchHostedDocs(
3113
+ hosted = await fetchHostedDocsCached(
3114
+ root,
2539
3115
  { name: displayName, query: name, maxTokens: budget },
2540
3116
  { region: opts.region, ingest: opts.ingest, auth: parsed ? { keyId: parsed.keyId, secret: parsed.secret } : void 0 }
2541
3117
  );
@@ -2596,8 +3172,8 @@ function registerExport(program) {
2596
3172
  out(content.trimEnd());
2597
3173
  return;
2598
3174
  }
2599
- fs6.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
2600
- fs6.writeFileSync(file, content);
3175
+ fs7.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
3176
+ fs7.writeFileSync(file, content);
2601
3177
  info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
2602
3178
  });
2603
3179
  applyGlobalOptions(cmd);
@@ -2620,24 +3196,24 @@ function registerBundle(program) {
2620
3196
  } catch {
2621
3197
  throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
2622
3198
  }
2623
- fs6.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
3199
+ fs7.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
2624
3200
  let grammarCount = 0;
2625
3201
  for (const { fileName, absPath } of grammarFiles) {
2626
- fs6.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
3202
+ fs7.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
2627
3203
  grammarCount++;
2628
3204
  }
2629
3205
  const included = [`grammars/ (${grammarCount} .wasm)`];
2630
3206
  const graphSrc = global.graph ?? defaultGraphPath(root);
2631
- if (fs6.existsSync(graphSrc)) {
2632
- fs6.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
3207
+ if (fs7.existsSync(graphSrc)) {
3208
+ fs7.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
2633
3209
  included.push("graph.json");
2634
3210
  }
2635
3211
  const cat = catalogPath(root);
2636
- if (fs6.existsSync(cat)) {
2637
- fs6.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
3212
+ if (fs7.existsSync(cat)) {
3213
+ fs7.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
2638
3214
  included.push("vibgrate.lib.json");
2639
- if (fs6.existsSync(libDir(root))) {
2640
- fs6.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
3215
+ if (fs7.existsSync(libDir(root))) {
3216
+ fs7.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
2641
3217
  included.push("lib/");
2642
3218
  }
2643
3219
  }
@@ -2649,7 +3225,7 @@ function registerBundle(program) {
2649
3225
  included,
2650
3226
  usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
2651
3227
  };
2652
- fs6.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
3228
+ fs7.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
2653
3229
  `);
2654
3230
  if (global.json) {
2655
3231
  json({ out: path.relative(root, outDir), ...manifest });
@@ -2674,7 +3250,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
2674
3250
  console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
2675
3251
  }
2676
3252
  if (opts.baseline) {
2677
- const { runBaseline } = await import('./baseline-V5XGS76K.js');
3253
+ const { runBaseline } = await import('./baseline-BTLZWSNS.js');
2678
3254
  await runBaseline(rootDir);
2679
3255
  }
2680
3256
  console.log("");
@@ -2683,6 +3259,270 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
2683
3259
  console.log(` ${chalk8.cyan("vibgrate baseline")} Create a drift baseline`);
2684
3260
  console.log("");
2685
3261
  });
3262
+ var REACHABILITY_ANALYZER_VERSION = "vg-reach-1.0";
3263
+ var ECOSYSTEM_MATCH_CONFIDENCE = {
3264
+ npm: 0.8,
3265
+ Go: 0.8,
3266
+ "crates.io": 0.7,
3267
+ PyPI: 0.65,
3268
+ RubyGems: 0.6
3269
+ };
3270
+ var GENERIC_SYMBOL_NAMES = /* @__PURE__ */ new Set([
3271
+ "get",
3272
+ "set",
3273
+ "run",
3274
+ "main",
3275
+ "init",
3276
+ "call",
3277
+ "apply",
3278
+ "exec",
3279
+ "send",
3280
+ "open",
3281
+ "read",
3282
+ "write",
3283
+ "parse",
3284
+ "load",
3285
+ "save",
3286
+ "start",
3287
+ "stop",
3288
+ "new",
3289
+ "create",
3290
+ "update",
3291
+ "delete",
3292
+ "handle",
3293
+ "process",
3294
+ "render",
3295
+ "value",
3296
+ "data",
3297
+ "next"
3298
+ ]);
3299
+ var MAX_FILES_PER_PACKAGE = 25;
3300
+ var MAX_FILE_BYTES = 256 * 1024;
3301
+ var MAX_MODULES_PER_PACKAGE = 50;
3302
+ var MAX_IMPORTED_MODULE_PACKAGES = 500;
3303
+ var MAX_FINDINGS = 1e3;
3304
+ function normalizePython(name) {
3305
+ return name.toLowerCase().replace(/[-.]/g, "_");
3306
+ }
3307
+ function specifierMatchesPackage(ecosystem, pkg, spec) {
3308
+ switch (ecosystem) {
3309
+ case "npm":
3310
+ case "Go":
3311
+ case "RubyGems":
3312
+ return spec === pkg || spec.startsWith(`${pkg}/`);
3313
+ case "PyPI": {
3314
+ const top = spec.split(".")[0] ?? spec;
3315
+ return normalizePython(top) === normalizePython(pkg);
3316
+ }
3317
+ case "crates.io": {
3318
+ const top = (spec.split("::")[0] ?? spec).toLowerCase().replace(/-/g, "_");
3319
+ return top === pkg.toLowerCase().replace(/-/g, "_");
3320
+ }
3321
+ default:
3322
+ return false;
3323
+ }
3324
+ }
3325
+ function symbolIdentifier(symbol) {
3326
+ const cleaned = symbol.replace(/\(.*\)$/, "");
3327
+ const segments = cleaned.split(/[./:#]|::/).filter(Boolean);
3328
+ return segments[segments.length - 1] ?? cleaned;
3329
+ }
3330
+ function isMatchableIdentifier(identifier) {
3331
+ return identifier.length >= 3 && !GENERIC_SYMBOL_NAMES.has(identifier.toLowerCase());
3332
+ }
3333
+ function collectPackageImports(graph, ecosystem, pkg) {
3334
+ if (!(ecosystem in ECOSYSTEM_MATCH_CONFIDENCE)) {
3335
+ return { supported: false, modules: [], importingFiles: [] };
3336
+ }
3337
+ const idx = indexFor(graph);
3338
+ const modules = /* @__PURE__ */ new Set();
3339
+ const files = /* @__PURE__ */ new Set();
3340
+ for (const node of graph.nodes) {
3341
+ if (node.kind !== "external") continue;
3342
+ if (!specifierMatchesPackage(ecosystem, pkg, node.name)) continue;
3343
+ modules.add(node.name);
3344
+ for (const edge of idx.in(node.id, "import")) {
3345
+ const src = idx.nodeById.get(edge.src);
3346
+ if (src?.file) files.add(src.file);
3347
+ }
3348
+ }
3349
+ return {
3350
+ supported: true,
3351
+ modules: [...modules].sort(),
3352
+ importingFiles: [...files].sort()
3353
+ };
3354
+ }
3355
+ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3) {
3356
+ const content = await readFile3(path.join(rootDir, relFile));
3357
+ if (!content) return false;
3358
+ const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3359
+ return new RegExp(`\\b${escaped}\\b`).test(content);
3360
+ }
3361
+ async function defaultReadFile(absPath) {
3362
+ try {
3363
+ const stat2 = await fs13.stat(absPath);
3364
+ if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
3365
+ return await fs13.readFile(absPath, "utf8");
3366
+ } catch {
3367
+ return null;
3368
+ }
3369
+ }
3370
+ async function analyzeReachability(input) {
3371
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3372
+ const readFile3 = input.readFile ?? defaultReadFile;
3373
+ if (!input.graph) {
3374
+ return {
3375
+ analyzerVersion: REACHABILITY_ANALYZER_VERSION,
3376
+ source: "none",
3377
+ generatedAt,
3378
+ manifestAdvisoryCount: input.manifest.length,
3379
+ findings: input.manifest.slice(0, MAX_FINDINGS).map((entry) => ({
3380
+ advisoryId: entry.advisoryId,
3381
+ ecosystem: entry.ecosystem,
3382
+ package: entry.package,
3383
+ version: entry.version,
3384
+ tier: "unknown",
3385
+ evidence: "no code map available for this scan",
3386
+ graphConfidence: 0
3387
+ }))
3388
+ };
3389
+ }
3390
+ const evidenceByPackage = /* @__PURE__ */ new Map();
3391
+ const importedModules = [];
3392
+ const seenPackages = /* @__PURE__ */ new Set();
3393
+ for (const dep of input.dependencies) {
3394
+ const key = `${dep.ecosystem}|${dep.package}`;
3395
+ if (seenPackages.has(key)) continue;
3396
+ seenPackages.add(key);
3397
+ const evidence = collectPackageImports(input.graph, dep.ecosystem, dep.package);
3398
+ evidenceByPackage.set(key, evidence);
3399
+ if (evidence.supported && importedModules.length < MAX_IMPORTED_MODULE_PACKAGES) {
3400
+ importedModules.push({
3401
+ ecosystem: dep.ecosystem,
3402
+ package: dep.package,
3403
+ modules: evidence.modules.slice(0, MAX_MODULES_PER_PACKAGE),
3404
+ importingFiles: evidence.importingFiles.length
3405
+ });
3406
+ }
3407
+ }
3408
+ importedModules.sort(
3409
+ (a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.package.localeCompare(b.package)
3410
+ );
3411
+ const findings = [];
3412
+ for (const entry of input.manifest) {
3413
+ const key = `${entry.ecosystem}|${entry.package}`;
3414
+ const evidence = evidenceByPackage.get(key) ?? collectPackageImports(input.graph, entry.ecosystem, entry.package);
3415
+ const baseConfidence = ECOSYSTEM_MATCH_CONFIDENCE[entry.ecosystem] ?? 0;
3416
+ if (!evidence.supported) {
3417
+ findings.push({
3418
+ advisoryId: entry.advisoryId,
3419
+ ecosystem: entry.ecosystem,
3420
+ package: entry.package,
3421
+ version: entry.version,
3422
+ tier: "unknown",
3423
+ evidence: `import matching is not supported for the ${entry.ecosystem} ecosystem yet`,
3424
+ graphConfidence: 0
3425
+ });
3426
+ continue;
3427
+ }
3428
+ if (evidence.importingFiles.length === 0) {
3429
+ findings.push({
3430
+ advisoryId: entry.advisoryId,
3431
+ ecosystem: entry.ecosystem,
3432
+ package: entry.package,
3433
+ version: entry.version,
3434
+ tier: "not_reached",
3435
+ evidence: `no import of ${entry.package} was found in the code map`,
3436
+ graphConfidence: baseConfidence
3437
+ });
3438
+ continue;
3439
+ }
3440
+ const matchableSymbols = entry.symbols.map((s) => ({ symbol: s.symbol, identifier: symbolIdentifier(s.symbol) })).filter((s) => isMatchableIdentifier(s.identifier));
3441
+ if (entry.symbolCoverage !== "function" || matchableSymbols.length === 0) {
3442
+ findings.push({
3443
+ advisoryId: entry.advisoryId,
3444
+ ecosystem: entry.ecosystem,
3445
+ package: entry.package,
3446
+ version: entry.version,
3447
+ tier: "potentially_reachable",
3448
+ evidence: `${entry.package} is imported by ${evidence.importingFiles.length} file${evidence.importingFiles.length === 1 ? "" : "s"}; vulnerable symbols unresolved (coverage: ${entry.symbolCoverage})`,
3449
+ callPath: evidence.importingFiles.slice(0, 10),
3450
+ graphConfidence: Math.min(baseConfidence, 0.6)
3451
+ });
3452
+ continue;
3453
+ }
3454
+ const filesToCheck = evidence.importingFiles.slice(0, MAX_FILES_PER_PACKAGE);
3455
+ let anySymbolFound = false;
3456
+ for (const { symbol, identifier } of matchableSymbols) {
3457
+ const referencingFiles = [];
3458
+ for (const relFile of filesToCheck) {
3459
+ if (await fileReferencesIdentifier(input.rootDir, relFile, identifier, readFile3)) {
3460
+ referencingFiles.push(relFile);
3461
+ if (referencingFiles.length >= 5) break;
3462
+ }
3463
+ }
3464
+ if (referencingFiles.length > 0) {
3465
+ anySymbolFound = true;
3466
+ findings.push({
3467
+ advisoryId: entry.advisoryId,
3468
+ ecosystem: entry.ecosystem,
3469
+ package: entry.package,
3470
+ version: entry.version,
3471
+ symbol,
3472
+ tier: "reachable",
3473
+ evidence: `vulnerable symbol \`${identifier}\` is referenced in ${referencingFiles.length} importing file${referencingFiles.length === 1 ? "" : "s"}`,
3474
+ callPath: referencingFiles,
3475
+ graphConfidence: Math.min(baseConfidence, 0.75)
3476
+ });
3477
+ }
3478
+ }
3479
+ if (!anySymbolFound) {
3480
+ findings.push({
3481
+ advisoryId: entry.advisoryId,
3482
+ ecosystem: entry.ecosystem,
3483
+ package: entry.package,
3484
+ version: entry.version,
3485
+ tier: "not_reached",
3486
+ evidence: `${entry.package} is imported, but none of its ${matchableSymbols.length} vulnerable symbol${matchableSymbols.length === 1 ? "" : "s"} is referenced in the ${filesToCheck.length} importing file${filesToCheck.length === 1 ? "" : "s"} checked`,
3487
+ callPath: evidence.importingFiles.slice(0, 10),
3488
+ graphConfidence: Math.min(baseConfidence, 0.55)
3489
+ });
3490
+ }
3491
+ }
3492
+ findings.sort(
3493
+ (a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.package.localeCompare(b.package) || a.advisoryId.localeCompare(b.advisoryId) || (a.symbol ?? "").localeCompare(b.symbol ?? "")
3494
+ );
3495
+ return {
3496
+ analyzerVersion: REACHABILITY_ANALYZER_VERSION,
3497
+ source: "graph",
3498
+ generatedAt,
3499
+ manifestAdvisoryCount: input.manifest.length,
3500
+ importedModules,
3501
+ findings: findings.slice(0, MAX_FINDINGS)
3502
+ };
3503
+ }
3504
+ function collectPreflightDependencies(projects, projectTypeToEcosystem) {
3505
+ const deduped = /* @__PURE__ */ new Map();
3506
+ for (const project of projects) {
3507
+ const ecosystem = projectTypeToEcosystem[project.type];
3508
+ if (!ecosystem) continue;
3509
+ for (const dep of project.dependencies) {
3510
+ const version = dep.resolvedVersion || normalizeVersionSpec(dep.currentSpec);
3511
+ if (!version) continue;
3512
+ const key = `${ecosystem}:${dep.package}:${version}`;
3513
+ if (!deduped.has(key)) {
3514
+ deduped.set(key, { ecosystem, package: dep.package, version });
3515
+ }
3516
+ }
3517
+ }
3518
+ return [...deduped.values()].sort(
3519
+ (a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.package.localeCompare(b.package) || a.version.localeCompare(b.version)
3520
+ );
3521
+ }
3522
+ function normalizeVersionSpec(spec) {
3523
+ const match = spec.trim().match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
3524
+ return match?.[0] ?? null;
3525
+ }
2686
3526
 
2687
3527
  // src/reporting/utils/ingest-id-output.ts
2688
3528
  function emitIngestIdLine(ingestId, options) {
@@ -2738,6 +3578,47 @@ function printAiContextPrompt(detectedAssistant) {
2738
3578
  function shouldBuildCodeMap(opts) {
2739
3579
  return opts.graph !== false && !opts.maxPrivacy && !opts.noLocalArtifacts;
2740
3580
  }
3581
+ async function attachReachability(artifact, rootDir, opts, graph) {
3582
+ try {
3583
+ const dsn = resolveDsn(opts.dsn);
3584
+ if (!dsn) return;
3585
+ const parsed = parseDsn(dsn);
3586
+ if (!parsed) return;
3587
+ const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
3588
+ const dependencies = collectPreflightDependencies(artifact.projects, PROJECT_TYPE_TO_OSV_ECOSYSTEM);
3589
+ if (dependencies.length === 0) return;
3590
+ const manifest = await fetchRiskySymbols(parsed, ingestHost, dependencies);
3591
+ if (manifest.status !== "ok" || !manifest.advisories) return;
3592
+ artifact.reachability = await analyzeReachability({
3593
+ graph,
3594
+ rootDir,
3595
+ manifest: manifest.advisories,
3596
+ dependencies
3597
+ });
3598
+ if (!opts.quiet && manifest.advisories.length > 0) {
3599
+ const tiers = artifact.reachability.findings.reduce((acc, f) => {
3600
+ acc[f.tier] = (acc[f.tier] ?? 0) + 1;
3601
+ return acc;
3602
+ }, {});
3603
+ const parts = [
3604
+ tiers.reachable ? `${tiers.reachable} reachable` : null,
3605
+ tiers.potentially_reachable ? `${tiers.potentially_reachable} potentially reachable` : null,
3606
+ tiers.not_reached ? `${tiers.not_reached} not reached` : null,
3607
+ tiers.unknown ? `${tiers.unknown} unknown` : null
3608
+ ].filter(Boolean);
3609
+ console.log(
3610
+ chalk8.dim(
3611
+ `Reachability: ${manifest.advisories.length} advisor${manifest.advisories.length === 1 ? "y" : "ies"} checked against the code map \u2014 ${parts.join(", ")}`
3612
+ )
3613
+ );
3614
+ }
3615
+ } catch (e) {
3616
+ if (!opts.quiet) {
3617
+ const msg = e instanceof Error ? e.message : String(e);
3618
+ console.log(chalk8.dim(`Reachability check skipped: ${msg}`));
3619
+ }
3620
+ }
3621
+ }
2741
3622
  async function autoPush(artifact, rootDir, opts) {
2742
3623
  const dsn = resolveDsn(opts.dsn);
2743
3624
  if (!dsn) {
@@ -2752,7 +3633,9 @@ async function autoPush(artifact, rootDir, opts) {
2752
3633
  if (opts.strict) process.exit(1);
2753
3634
  return;
2754
3635
  }
2755
- const { body, contentEncoding } = await prepareCompressedUpload(artifact);
3636
+ const config = await loadConfig(rootDir);
3637
+ const databaseSchemaCaps = config.scanners !== false ? config.scanners?.databaseSchema : void 0;
3638
+ const { body, contentEncoding } = await prepareCompressedUpload(artifact, { databaseSchemaCaps });
2756
3639
  const timestamp = String(Date.now());
2757
3640
  let host = parsed.host;
2758
3641
  if (opts.region) {
@@ -2777,7 +3660,11 @@ async function autoPush(artifact, rootDir, opts) {
2777
3660
  body,
2778
3661
  contentEncoding,
2779
3662
  timestamp,
2780
- force: opts.force
3663
+ force: opts.force,
3664
+ // Set only by an automated caller running the scan on the workspace's
3665
+ // behalf (e.g. a Vibgrate-hosted remediation run) — never a customer flag.
3666
+ runId: process.env.VIBGRATE_SCAN_RUN_ID,
3667
+ runToken: process.env.VIBGRATE_SCAN_RUN_TOKEN
2781
3668
  });
2782
3669
  host = uploadedHost;
2783
3670
  if (!response.ok) {
@@ -2852,7 +3739,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
2852
3739
  'Exclude paths matching a glob pattern. Repeatable, and a single value may list several patterns separated by commas or semicolons (e.g. --exclude "legacy/**,vendor/**"). Merged with excludes from the config file.',
2853
3740
  collectExcludes,
2854
3741
  []
2855
- ).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
3742
+ ).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if DriftScore is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
2856
3743
  const rootDir = path.resolve(targetPath);
2857
3744
  if (!await pathExists$1(rootDir)) {
2858
3745
  console.error(chalk8.red(`Path does not exist: ${rootDir}`));
@@ -2987,6 +3874,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
2987
3874
  invocation: resolveCliInvocation()
2988
3875
  };
2989
3876
  const wantGraph = shouldBuildCodeMap({ graph: opts.graph, maxPrivacy: opts.maxPrivacy, noLocalArtifacts });
3877
+ let builtGraph = null;
2990
3878
  if (wantGraph) {
2991
3879
  scanOpts.postScan = async (report) => {
2992
3880
  const result = await buildGraph({
@@ -2994,6 +3882,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
2994
3882
  exclude: opts.exclude,
2995
3883
  onParseProgress: (done, total) => report(done, total, "parsing")
2996
3884
  });
3885
+ builtGraph = result.graph;
2997
3886
  writeArtifacts(result.graph, { root: rootDir });
2998
3887
  writeSnapshot(rootDir, result.graph.provenance.corpusHash, result.fileStats, {
2999
3888
  exclude: opts.exclude
@@ -3024,7 +3913,7 @@ Failing: findings detected at warn level or above.`));
3024
3913
  }
3025
3914
  if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
3026
3915
  console.error(chalk8.red(`
3027
- Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
3916
+ Failing fitness function: DriftScore ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
3028
3917
  process.exit(2);
3029
3918
  }
3030
3919
  if (scanOpts.driftWorseningPercent !== void 0) {
@@ -3043,6 +3932,9 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
3043
3932
  }
3044
3933
  }
3045
3934
  }
3935
+ if (willPush && hasDsn && builtGraph) {
3936
+ await attachReachability(artifact, rootDir, scanOpts, builtGraph);
3937
+ }
3046
3938
  if (willPush) {
3047
3939
  await autoPush(artifact, rootDir, scanOpts);
3048
3940
  }
@@ -3115,7 +4007,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
3115
4007
  function scanStaleness(rootDir, artifactPath) {
3116
4008
  let ref;
3117
4009
  try {
3118
- ref = fs6.statSync(artifactPath).mtimeMs;
4010
+ ref = fs7.statSync(artifactPath).mtimeMs;
3119
4011
  } catch {
3120
4012
  return { stale: false };
3121
4013
  }
@@ -3127,7 +4019,7 @@ function scanStaleness(rootDir, artifactPath) {
3127
4019
  if (depth > 8 || scanned > MAX_ENTRIES) return;
3128
4020
  let entries;
3129
4021
  try {
3130
- entries = fs6.readdirSync(dir, { withFileTypes: true });
4022
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
3131
4023
  } catch {
3132
4024
  return;
3133
4025
  }
@@ -3142,7 +4034,7 @@ function scanStaleness(rootDir, artifactPath) {
3142
4034
  const abs = path.join(dir, e.name);
3143
4035
  let mtimeMs;
3144
4036
  try {
3145
- mtimeMs = fs6.statSync(abs).mtimeMs;
4037
+ mtimeMs = fs7.statSync(abs).mtimeMs;
3146
4038
  } catch {
3147
4039
  continue;
3148
4040
  }
@@ -3229,7 +4121,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3229
4121
  ]);
3230
4122
  var MAX_FILES = 8e3;
3231
4123
  var MAX_DEPTH = 12;
3232
- var MAX_FILE_BYTES = 1048576;
4124
+ var MAX_FILE_BYTES2 = 1048576;
3233
4125
  var EXT_ECOSYSTEM = {
3234
4126
  ".ts": "npm",
3235
4127
  ".tsx": "npm",
@@ -3276,7 +4168,7 @@ function analyzeTree(root, packages) {
3276
4168
  if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
3277
4169
  let entries;
3278
4170
  try {
3279
- entries = fs6.readdirSync(dir, { withFileTypes: true });
4171
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
3280
4172
  } catch {
3281
4173
  return;
3282
4174
  }
@@ -3294,8 +4186,8 @@ function analyzeTree(root, packages) {
3294
4186
  const full = path.join(dir, entry.name);
3295
4187
  let content;
3296
4188
  try {
3297
- if (fs6.statSync(full).size > MAX_FILE_BYTES) continue;
3298
- content = fs6.readFileSync(full, "utf8");
4189
+ if (fs7.statSync(full).size > MAX_FILE_BYTES2) continue;
4190
+ content = fs7.readFileSync(full, "utf8");
3299
4191
  } catch {
3300
4192
  continue;
3301
4193
  }
@@ -3486,6 +4378,11 @@ function estimateDriftScore(artifact, upgraded) {
3486
4378
  for (const dep of p.dependencies ?? []) {
3487
4379
  if (!upgraded.has(dep.package)) continue;
3488
4380
  const mb3 = dep.majorsBehind ?? 0;
4381
+ dep.majorsBehind = 0;
4382
+ dep.drift = "current";
4383
+ if (dep.libyears != null) dep.libyears = 0;
4384
+ if (dep.ageDays != null) dep.ageDays = 0;
4385
+ if (dep.latestStable) dep.resolvedVersion = dep.latestStable;
3489
4386
  if (mb3 >= 2 && buckets.twoPlusBehind > 0) {
3490
4387
  buckets.twoPlusBehind--;
3491
4388
  buckets.current++;
@@ -3629,8 +4526,8 @@ async function fetchLatestVersion() {
3629
4526
  }
3630
4527
  async function writeCache(data) {
3631
4528
  try {
3632
- await fs15.mkdir(CACHE_DIR, { recursive: true });
3633
- await fs15.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
4529
+ await fs13.mkdir(CACHE_DIR, { recursive: true });
4530
+ await fs13.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
3634
4531
  } catch {
3635
4532
  }
3636
4533
  }
@@ -3707,11 +4604,11 @@ async function isDevDependency(cwd) {
3707
4604
  async function confirmWorkspaceRoot(pm) {
3708
4605
  if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
3709
4606
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3710
- return new Promise((resolve16) => {
4607
+ return new Promise((resolve17) => {
3711
4608
  rl.question(chalk8.yellow(`Update the ${pm} workspace root with -w? [y/N]: `), (ans) => {
3712
4609
  rl.close();
3713
4610
  const t = ans.trim().toLowerCase();
3714
- resolve16(t === "y" || t === "yes");
4611
+ resolve17(t === "y" || t === "yes");
3715
4612
  });
3716
4613
  });
3717
4614
  }
@@ -3839,7 +4736,7 @@ async function repositoryIdentity(rootDir, override) {
3839
4736
  return { name: override?.trim() || void 0 };
3840
4737
  }
3841
4738
  }
3842
- var fixCommand = new Command("fix").description("Analyse drift and get ranked, risk-tiered upgrade plans from the hosted planner (requires login/DSN; read-only \u2014 never edits your project)").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
4739
+ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade plans from the hosted planner and optionally apply them (prompts before changing files; --yes applies non-interactively, --dry-run previews, --no-apply only prints). Requires login/DSN.").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
3843
4740
  const rootDir = path.resolve(targetPath);
3844
4741
  if (!await pathExists(rootDir)) {
3845
4742
  console.error(chalk8.red(`Path does not exist: ${rootDir}`));
@@ -4038,14 +4935,14 @@ function promptPlanSelection(response) {
4038
4935
  const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
4039
4936
  const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
4040
4937
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4041
- return new Promise((resolve16) => {
4938
+ return new Promise((resolve17) => {
4042
4939
  rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
4043
4940
  rl.close();
4044
4941
  const t = ans.trim().toLowerCase();
4045
- if (t === "q" || t === "quit") return resolve16(null);
4942
+ if (t === "q" || t === "quit") return resolve17(null);
4046
4943
  const n = t === "" ? def : Number.parseInt(t, 10);
4047
- if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve16(null);
4048
- resolve16(plans[n - 1].tier);
4944
+ if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve17(null);
4945
+ resolve17(plans[n - 1].tier);
4049
4946
  });
4050
4947
  });
4051
4948
  }
@@ -4416,6 +5313,26 @@ function formatExtended(ext) {
4416
5313
  }
4417
5314
  lines.push("");
4418
5315
  }
5316
+ if (ext.databaseSchema) {
5317
+ const ds = ext.databaseSchema;
5318
+ lines.push(chalk8.bold.underline(" Database Schema"));
5319
+ const dsHead = [];
5320
+ if (ds.providers.length > 0) dsHead.push(ds.providers.join(", "));
5321
+ dsHead.push(`${ds.models.length} model${ds.models.length !== 1 ? "s" : ""}`);
5322
+ dsHead.push(`${ds.enums.length} enum${ds.enums.length !== 1 ? "s" : ""}`);
5323
+ lines.push(` ${dsHead.join(" \xB7 ")}`);
5324
+ const bySource = /* @__PURE__ */ new Map();
5325
+ for (const m of ds.models) bySource.set(m.source, (bySource.get(m.source) ?? 0) + 1);
5326
+ if (bySource.size > 1) {
5327
+ const sourceSummary = [...bySource.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([s, n]) => `${s}: ${n}`).join(", ");
5328
+ lines.push(` Sources: ${sourceSummary}`);
5329
+ }
5330
+ if (ds.models.length > 0) {
5331
+ const preview = ds.models.slice(0, 5).map((m) => m.name).join(", ");
5332
+ lines.push(` Models: ${chalk8.white(preview)}${ds.models.length > 5 ? chalk8.dim(` (+${ds.models.length - 5} more)`) : ""}`);
5333
+ }
5334
+ lines.push("");
5335
+ }
4419
5336
  if (ext.dependencyGraph) {
4420
5337
  const dg = ext.dependencyGraph;
4421
5338
  if (dg.lockfileType) {
@@ -4857,7 +5774,7 @@ function openUrl(url) {
4857
5774
  }
4858
5775
 
4859
5776
  // src/reporting/commands/login.ts
4860
- var delay = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
5777
+ var delay = (ms) => new Promise((resolve17) => setTimeout(resolve17, ms));
4861
5778
  function isInside(dir, file) {
4862
5779
  const rel3 = path.relative(dir, file);
4863
5780
  return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
@@ -5304,7 +6221,7 @@ function formatDeltaText(base, current) {
5304
6221
  "===================",
5305
6222
  `Baseline: ${base.timestamp}`,
5306
6223
  `Current: ${current.timestamp}`,
5307
- `Drift score delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
6224
+ `DriftScore delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
5308
6225
  "",
5309
6226
  `Added dependencies (${added.length})`,
5310
6227
  ...added.map((d) => ` + ${d}`),
@@ -5382,6 +6299,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
5382
6299
  "areas",
5383
6300
  "oddities",
5384
6301
  "serve",
6302
+ "lsp",
5385
6303
  "install",
5386
6304
  "uninstall",
5387
6305
  "share",
@@ -5422,6 +6340,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
5422
6340
  registerImpact(program);
5423
6341
  registerUnknowns(program);
5424
6342
  registerServe(program);
6343
+ registerLsp(program);
5425
6344
  registerInstall(program);
5426
6345
  registerShare(program);
5427
6346
  registerBenchmark(program);
@@ -5503,7 +6422,7 @@ function dispatch(argv, cwd) {
5503
6422
  }
5504
6423
  const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
5505
6424
  if (looksLikeQuestion) return ["ask", ...args];
5506
- if (fs6.existsSync(path.resolve(cwd, first))) {
6425
+ if (fs7.existsSync(path.resolve(cwd, first))) {
5507
6426
  return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
5508
6427
  }
5509
6428
  const moved = MOVED_COMMANDS[first];