scip-query 0.4.1 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export { dataflow } from './queries/dataflow.js';
43
43
  export { slice } from './queries/slice.js';
44
44
  export { redundantReexports } from './queries/redundant-reexports.js';
45
45
  export { similarSignatures } from './queries/similar-signatures.js';
46
+ import { Index } from '@c4312/scip';
46
47
  import 'better-sqlite3';
47
48
 
48
49
  /**
@@ -88,6 +89,22 @@ declare const INDEXER_CONFIGS: Record<SupportedLanguage, IndexerConfig>;
88
89
  /** Get the indexer config for a language */
89
90
  declare function getIndexerConfig(language: SupportedLanguage): IndexerConfig;
90
91
 
92
+ interface MergeScipResult {
93
+ documentCount: number;
94
+ externalSymbolCount: number;
95
+ inputCount: number;
96
+ }
97
+ /**
98
+ * Merge multiple SCIP indices into a single index message.
99
+ *
100
+ * A SCIP index is a code graph snapshot: metadata about one indexed project plus
101
+ * the documents and external symbols discovered by one or more indexers. Merging
102
+ * means producing one unified snapshot so later conversion to SQLite sees the
103
+ * whole repo instead of whichever language indexed last.
104
+ */
105
+ declare function mergeScipIndexes(indexes: readonly Index[]): Index;
106
+ declare function mergeScipFiles(inputPaths: readonly string[], outputPath: string): MergeScipResult;
107
+
91
108
  /**
92
109
  * Check if a binary is available on PATH.
93
110
  */
@@ -250,4 +267,4 @@ declare function installSkills(opts?: {
250
267
  quiet?: boolean;
251
268
  }): InstallSkillsResult;
252
269
 
253
- export { INDEXER_CONFIGS, IndexerConfig, ProjectConfig, ScipLocalSymbol, ScipSymbol, SupportedLanguage, Watcher, WatcherStatus, detectLanguages, getIndexerConfig, getScipVersion, initProjectConfig, installSkills, isBinaryAvailable, isIndexerInstalled, isScipInstalled, leafName, loadProjectConfig, parseSymbol, printScipInstallInstructions, reindex, resolveCacheDir, resolveIndexPaths, shortenSymbol, tryInstallIndexer, tryInstallScipCli };
270
+ export { INDEXER_CONFIGS, IndexerConfig, ProjectConfig, ScipLocalSymbol, ScipSymbol, SupportedLanguage, Watcher, WatcherStatus, detectLanguages, getIndexerConfig, getScipVersion, initProjectConfig, installSkills, isBinaryAvailable, isIndexerInstalled, isScipInstalled, leafName, loadProjectConfig, mergeScipFiles, mergeScipIndexes, parseSymbol, printScipInstallInstructions, reindex, resolveCacheDir, resolveIndexPaths, shortenSymbol, tryInstallIndexer, tryInstallScipCli };
package/dist/index.js CHANGED
@@ -254,7 +254,7 @@ var ScipDatabase = class {
254
254
  // src/gitignore-filter.ts
255
255
  import ignore from "ignore";
256
256
  import { readFileSync, existsSync } from "fs";
257
- import { join, dirname } from "path";
257
+ import { dirname, isAbsolute, join, relative, resolve } from "path";
258
258
  function createGitignoreFilter(projectRoot) {
259
259
  const ig = ignore();
260
260
  let loaded = false;
@@ -271,8 +271,8 @@ function createGitignoreFilter(projectRoot) {
271
271
  ig.add(DEFAULT_IGNORES);
272
272
  }
273
273
  return {
274
- isIgnored: (relativePath) => ig.ignores(relativePath),
275
- filter: (paths) => paths.filter((p) => !ig.ignores(p))
274
+ isIgnored: (relativePath) => safeIgnores(ig, projectRoot, relativePath),
275
+ filter: (paths) => paths.filter((p) => !safeIgnores(ig, projectRoot, p))
276
276
  };
277
277
  }
278
278
  function findGitignoreFiles(projectRoot) {
@@ -352,11 +352,36 @@ Thumbs.db
352
352
  # Type definitions (often noise in queries)
353
353
  *.d.ts
354
354
  `;
355
+ function safeIgnores(ig, projectRoot, inputPath) {
356
+ const relativePath = normalizeForIgnore(projectRoot, inputPath);
357
+ if (!relativePath) {
358
+ return false;
359
+ }
360
+ try {
361
+ return ig.ignores(relativePath);
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+ function normalizeForIgnore(projectRoot, inputPath) {
367
+ if (!inputPath || inputPath === ".") {
368
+ return null;
369
+ }
370
+ if (!isAbsolute(inputPath) && !inputPath.startsWith("..")) {
371
+ return inputPath.replaceAll("\\", "/");
372
+ }
373
+ const absolutePath = isAbsolute(inputPath) ? inputPath : resolve(projectRoot, inputPath);
374
+ const relativePath = relative(projectRoot, absolutePath).replaceAll("\\", "/");
375
+ if (!relativePath || relativePath === "." || relativePath.startsWith("..")) {
376
+ return null;
377
+ }
378
+ return relativePath;
379
+ }
355
380
 
356
381
  // src/reindex/index.ts
357
382
  import { execFileSync as execFileSync3 } from "child_process";
358
- import { existsSync as existsSync5, renameSync } from "fs";
359
- import { join as join5 } from "path";
383
+ import { existsSync as existsSync5, renameSync, rmSync } from "fs";
384
+ import { basename, dirname as dirname2, extname as extname2, join as join5 } from "path";
360
385
 
361
386
  // src/scip-cli.ts
362
387
  import { execFileSync as execFileSync2 } from "child_process";
@@ -991,6 +1016,128 @@ function resolveDotnetProject(projectRoot, suffixes) {
991
1016
  return null;
992
1017
  }
993
1018
 
1019
+ // src/reindex/merge.ts
1020
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
1021
+ import { create } from "@bufbuild/protobuf";
1022
+ import {
1023
+ deserializeSCIP,
1024
+ serializeSCIP,
1025
+ DocumentSchema,
1026
+ IndexSchema,
1027
+ SymbolInformationSchema
1028
+ } from "@c4312/scip";
1029
+ function mergeScipIndexes(indexes) {
1030
+ if (indexes.length === 0) {
1031
+ throw new Error("Cannot merge zero SCIP indexes");
1032
+ }
1033
+ if (indexes.length === 1) {
1034
+ return indexes[0];
1035
+ }
1036
+ const metadata = mergeMetadata(indexes);
1037
+ const documents = mergeDocuments(indexes.flatMap((index) => index.documents ?? []));
1038
+ const externalSymbols = mergeSymbolInfos(indexes.flatMap((index) => index.externalSymbols ?? []));
1039
+ return create(IndexSchema, {
1040
+ metadata,
1041
+ documents,
1042
+ externalSymbols
1043
+ });
1044
+ }
1045
+ function mergeScipFiles(inputPaths, outputPath) {
1046
+ if (inputPaths.length === 0) {
1047
+ throw new Error("Cannot merge zero SCIP files");
1048
+ }
1049
+ const indexes = inputPaths.map((path) => deserializeSCIP(readFileSync2(path)));
1050
+ const merged = mergeScipIndexes(indexes);
1051
+ writeFileSync(outputPath, Buffer.from(serializeSCIP(merged)));
1052
+ return {
1053
+ documentCount: merged.documents.length,
1054
+ externalSymbolCount: merged.externalSymbols.length,
1055
+ inputCount: inputPaths.length
1056
+ };
1057
+ }
1058
+ function mergeMetadata(indexes) {
1059
+ const first = indexes[0]?.metadata;
1060
+ if (!first) {
1061
+ return void 0;
1062
+ }
1063
+ const expectedProjectRoot = first.projectRoot;
1064
+ for (const index of indexes.slice(1)) {
1065
+ const actualProjectRoot = index.metadata?.projectRoot;
1066
+ if (expectedProjectRoot && actualProjectRoot && actualProjectRoot !== expectedProjectRoot) {
1067
+ throw new Error(
1068
+ `Cannot merge SCIP indexes with different project roots: ${expectedProjectRoot} vs ${actualProjectRoot}`
1069
+ );
1070
+ }
1071
+ }
1072
+ return first;
1073
+ }
1074
+ function mergeDocuments(documents) {
1075
+ const byPath = /* @__PURE__ */ new Map();
1076
+ for (const document of documents) {
1077
+ const existing = byPath.get(document.relativePath);
1078
+ if (!existing) {
1079
+ byPath.set(document.relativePath, document);
1080
+ continue;
1081
+ }
1082
+ byPath.set(document.relativePath, create(DocumentSchema, {
1083
+ language: existing.language || document.language,
1084
+ relativePath: existing.relativePath || document.relativePath,
1085
+ occurrences: [...existing.occurrences, ...document.occurrences],
1086
+ symbols: mergeSymbolInfos([...existing.symbols, ...document.symbols]),
1087
+ text: chooseText(existing.text, document.text),
1088
+ positionEncoding: existing.positionEncoding || document.positionEncoding
1089
+ }));
1090
+ }
1091
+ return [...byPath.values()];
1092
+ }
1093
+ function mergeSymbolInfos(symbols2) {
1094
+ const bySymbol = /* @__PURE__ */ new Map();
1095
+ for (const symbol of symbols2) {
1096
+ const existing = bySymbol.get(symbol.symbol);
1097
+ if (!existing) {
1098
+ bySymbol.set(symbol.symbol, symbol);
1099
+ continue;
1100
+ }
1101
+ bySymbol.set(symbol.symbol, create(SymbolInformationSchema, {
1102
+ symbol: existing.symbol,
1103
+ documentation: uniqueStrings([...existing.documentation, ...symbol.documentation]),
1104
+ relationships: mergeRelationships([...existing.relationships, ...symbol.relationships]),
1105
+ kind: existing.kind || symbol.kind,
1106
+ displayName: existing.displayName || symbol.displayName,
1107
+ enclosingSymbol: existing.enclosingSymbol || symbol.enclosingSymbol,
1108
+ signatureDocumentation: existing.signatureDocumentation ?? symbol.signatureDocumentation
1109
+ }));
1110
+ }
1111
+ return [...bySymbol.values()];
1112
+ }
1113
+ function mergeRelationships(relationships) {
1114
+ const seen = /* @__PURE__ */ new Set();
1115
+ const merged = [];
1116
+ for (const relationship of relationships) {
1117
+ const key = [
1118
+ relationship.symbol,
1119
+ relationship.isReference ? "1" : "0",
1120
+ relationship.isImplementation ? "1" : "0",
1121
+ relationship.isTypeDefinition ? "1" : "0",
1122
+ relationship.isDefinition ? "1" : "0"
1123
+ ].join("|");
1124
+ if (seen.has(key)) {
1125
+ continue;
1126
+ }
1127
+ seen.add(key);
1128
+ merged.push(relationship);
1129
+ }
1130
+ return merged;
1131
+ }
1132
+ function chooseText(left, right) {
1133
+ if (!left) return right;
1134
+ if (!right) return left;
1135
+ return left.length >= right.length ? left : right;
1136
+ }
1137
+ function uniqueStrings(values) {
1138
+ return [...new Set(values)];
1139
+ }
1140
+
994
1141
  // src/reindex/index.ts
995
1142
  async function reindex(opts) {
996
1143
  const {
@@ -1026,7 +1173,11 @@ async function reindex(opts) {
1026
1173
  ...process.env,
1027
1174
  NODE_OPTIONS: `--max-old-space-size=${maxHeapMb}`
1028
1175
  };
1029
- for (const lang of languages) {
1176
+ const languageOutputs = languages.map((language, index) => ({
1177
+ language,
1178
+ scipPath: languages.length > 1 ? tempScipPath(outputScip, language, index) : outputScip
1179
+ }));
1180
+ for (const { language: lang, scipPath } of languageOutputs) {
1030
1181
  const config = getIndexerConfig(lang);
1031
1182
  const binaryLabel = describeIndexerBinary(config);
1032
1183
  const projectLocalBinary = resolveProjectLocalIndexerBinary(config, projectRoot);
@@ -1056,7 +1207,7 @@ async function reindex(opts) {
1056
1207
  const indexerEnv = getIndexerExecutionEnv(config, env, resolvedBinary);
1057
1208
  const { binary, args } = config.indexArgs({
1058
1209
  projectRoot,
1059
- outputPath: outputScip,
1210
+ outputPath: scipPath,
1060
1211
  pnpmWorkspaces: opts.pnpmWorkspaces,
1061
1212
  indexerBinary: resolvedBinary
1062
1213
  });
@@ -1075,7 +1226,11 @@ Make sure ${binaryLabel} is installed and available on PATH.`,
1075
1226
  { cause: err }
1076
1227
  );
1077
1228
  }
1078
- moveDefaultOutputIfNeeded(config, projectRoot, outputScip);
1229
+ moveDefaultOutputIfNeeded(config, projectRoot, scipPath);
1230
+ }
1231
+ if (languageOutputs.length > 1) {
1232
+ onStatus(`Merging ${languageOutputs.length} language indexes...`);
1233
+ mergeScipFiles(languageOutputs.map((entry) => entry.scipPath), outputScip);
1079
1234
  }
1080
1235
  onStatus("Converting to SQLite...");
1081
1236
  if (!existsSync5(outputScip)) {
@@ -1090,6 +1245,12 @@ Make sure ${binaryLabel} is installed and available on PATH.`,
1090
1245
  } catch (err) {
1091
1246
  const msg = err instanceof Error ? err.message : String(err);
1092
1247
  throw new Error(`Failed to convert SCIP index to SQLite: ${msg}`, { cause: err });
1248
+ } finally {
1249
+ for (const { scipPath } of languageOutputs) {
1250
+ if (scipPath !== outputScip) {
1251
+ rmSync(scipPath, { force: true });
1252
+ }
1253
+ }
1093
1254
  }
1094
1255
  const durationMs = Date.now() - start;
1095
1256
  onStatus(`Done in ${(durationMs / 1e3).toFixed(1)}s`);
@@ -1104,10 +1265,15 @@ function moveDefaultOutputIfNeeded(config, projectRoot, outputScip) {
1104
1265
  renameSync(defaultOutputPath, outputScip);
1105
1266
  }
1106
1267
  }
1268
+ function tempScipPath(outputScip, language, index) {
1269
+ const extension = extname2(outputScip) || ".scip";
1270
+ const stem = basename(outputScip, extension);
1271
+ return join5(dirname2(outputScip), `${stem}.${index + 1}.${language}${extension}`);
1272
+ }
1107
1273
 
1108
1274
  // src/config.ts
1109
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync6, mkdirSync } from "fs";
1110
- import { join as join6, resolve } from "path";
1275
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync6, mkdirSync } from "fs";
1276
+ import { join as join6, resolve as resolve2 } from "path";
1111
1277
  import { createHash } from "crypto";
1112
1278
  import { homedir } from "os";
1113
1279
  var CONFIG_FILENAME = ".scipquery.json";
@@ -1123,7 +1289,7 @@ function loadProjectConfig(projectRoot) {
1123
1289
  return {};
1124
1290
  }
1125
1291
  try {
1126
- const raw = readFileSync2(configPath, "utf-8");
1292
+ const raw = readFileSync3(configPath, "utf-8");
1127
1293
  return JSON.parse(raw);
1128
1294
  } catch {
1129
1295
  return {};
@@ -1138,10 +1304,10 @@ function resolveWatchConfig(config) {
1138
1304
  function resolveCacheDir(projectRoot, config) {
1139
1305
  const envOverride = process.env["SCIP_QUERY_CACHE_DIR"];
1140
1306
  if (envOverride) return ensureDir(envOverride);
1141
- if (config?.dbPath) return ensureDir(resolve(projectRoot, config.dbPath));
1307
+ if (config?.dbPath) return ensureDir(resolve2(projectRoot, config.dbPath));
1142
1308
  const xdgCache = process.env["XDG_CACHE_HOME"];
1143
1309
  const cacheBase = xdgCache || join6(homedir(), ".cache");
1144
- const projectHash = createHash("sha256").update(resolve(projectRoot)).digest("hex").slice(0, 12);
1310
+ const projectHash = createHash("sha256").update(resolve2(projectRoot)).digest("hex").slice(0, 12);
1145
1311
  const dir = join6(cacheBase, "scip-query", "projects", projectHash);
1146
1312
  return ensureDir(dir);
1147
1313
  }
@@ -1167,7 +1333,7 @@ function initProjectConfig(projectRoot, languages) {
1167
1333
  cooldownMs: 6e4
1168
1334
  }
1169
1335
  };
1170
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1336
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
1171
1337
  return configPath;
1172
1338
  }
1173
1339
  function ensureDir(dir) {
@@ -1178,7 +1344,7 @@ function ensureDir(dir) {
1178
1344
  // src/watch.ts
1179
1345
  import { watch } from "fs";
1180
1346
  import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
1181
- import { join as join7, relative } from "path";
1347
+ import { join as join7, relative as relative2 } from "path";
1182
1348
  import { fork } from "child_process";
1183
1349
  import ignore2 from "ignore";
1184
1350
  var Watcher = class {
@@ -1256,7 +1422,7 @@ var Watcher = class {
1256
1422
  }
1257
1423
  // ── Internal ─────────────────────────────────────────────
1258
1424
  handleFileChange(filename) {
1259
- const rel = relative(this.projectRoot, join7(this.projectRoot, filename));
1425
+ const rel = relative2(this.projectRoot, join7(this.projectRoot, filename));
1260
1426
  if (this.gitignoreFilter.isIgnored(rel)) return;
1261
1427
  if (this.extraIgnore.ignores(rel)) return;
1262
1428
  if (filename.endsWith("index.db") || filename.endsWith("index.scip") || filename.endsWith("index.db.tmp") || filename.endsWith(".scipquery.json")) {
@@ -1337,10 +1503,10 @@ var Watcher = class {
1337
1503
  * Writes to index.db.tmp, then atomically renames to index.db.
1338
1504
  */
1339
1505
  runReindex() {
1340
- return new Promise((resolve3, reject) => {
1506
+ return new Promise((resolve4, reject) => {
1341
1507
  const start = Date.now();
1342
1508
  const tmpDb = this.indexPaths.dbPath + ".tmp";
1343
- const tmpScip = tempScipPath(this.indexPaths.indexPath);
1509
+ const tmpScip = tempScipPath2(this.indexPaths.indexPath);
1344
1510
  const child = fork(
1345
1511
  new URL("./reindex-worker.js", import.meta.url).pathname,
1346
1512
  [],
@@ -1365,7 +1531,7 @@ var Watcher = class {
1365
1531
  if (existsSync7(tmpScip)) {
1366
1532
  renameSync2(tmpScip, this.indexPaths.indexPath);
1367
1533
  }
1368
- resolve3(Date.now() - start);
1534
+ resolve4(Date.now() - start);
1369
1535
  } catch (err) {
1370
1536
  reject(new Error(`Atomic swap failed: ${err}`));
1371
1537
  }
@@ -1381,7 +1547,7 @@ var Watcher = class {
1381
1547
  this.onStatus(status);
1382
1548
  }
1383
1549
  };
1384
- function tempScipPath(indexPath) {
1550
+ function tempScipPath2(indexPath) {
1385
1551
  return indexPath.endsWith(".scip") ? indexPath.slice(0, -".scip".length) + ".tmp.scip" : indexPath + ".tmp.scip";
1386
1552
  }
1387
1553
 
@@ -1393,7 +1559,7 @@ import {
1393
1559
  readlinkSync,
1394
1560
  unlinkSync
1395
1561
  } from "fs";
1396
- import { join as join8, dirname as dirname2, resolve as resolve2 } from "path";
1562
+ import { join as join8, dirname as dirname3, resolve as resolve3 } from "path";
1397
1563
  import { homedir as homedir2, platform as platform3 } from "os";
1398
1564
  import { fileURLToPath } from "url";
1399
1565
  var IS_WINDOWS3 = platform3() === "win32";
@@ -1408,7 +1574,7 @@ function installSkills(opts = {}) {
1408
1574
  const log = opts.quiet ? () => {
1409
1575
  } : console.log;
1410
1576
  const thisFile = fileURLToPath(import.meta.url);
1411
- const skillsSource = resolve2(dirname2(thisFile), "..", "skills");
1577
+ const skillsSource = resolve3(dirname3(thisFile), "..", "skills");
1412
1578
  const targets = [
1413
1579
  join8(homedir2(), ".claude", "skills"),
1414
1580
  join8(homedir2(), ".codex", "skills")
@@ -1419,7 +1585,7 @@ function installSkills(opts = {}) {
1419
1585
  alreadyLinked: []
1420
1586
  };
1421
1587
  for (const targetDir of targets) {
1422
- const parentDir = dirname2(targetDir);
1588
+ const parentDir = dirname3(targetDir);
1423
1589
  if (!existsSync8(parentDir)) {
1424
1590
  continue;
1425
1591
  }
@@ -1435,7 +1601,7 @@ function installSkills(opts = {}) {
1435
1601
  if (existsSync8(target)) {
1436
1602
  try {
1437
1603
  const existing = readlinkSync(target);
1438
- if (resolve2(existing) === resolve2(source)) {
1604
+ if (resolve3(existing) === resolve3(source)) {
1439
1605
  result.alreadyLinked.push(`${toolName}/${skill}`);
1440
1606
  log(` ok: ${skill} \u2192 ${toolName} (already linked)`);
1441
1607
  continue;
@@ -1498,6 +1664,8 @@ export {
1498
1664
  leafName,
1499
1665
  loadProjectConfig,
1500
1666
  members,
1667
+ mergeScipFiles,
1668
+ mergeScipIndexes,
1501
1669
  methods,
1502
1670
  outline,
1503
1671
  parseSymbol,
@@ -11,8 +11,8 @@ import {
11
11
 
12
12
  // src/reindex/index.ts
13
13
  import { execFileSync } from "child_process";
14
- import { existsSync as existsSync3, renameSync } from "fs";
15
- import { join as join3 } from "path";
14
+ import { existsSync as existsSync3, renameSync, rmSync } from "fs";
15
+ import { basename, dirname, extname as extname2, join as join3 } from "path";
16
16
 
17
17
  // src/reindex/detect.ts
18
18
  import { existsSync, readdirSync } from "fs";
@@ -390,6 +390,128 @@ function resolveDotnetProject(projectRoot2, suffixes) {
390
390
  return null;
391
391
  }
392
392
 
393
+ // src/reindex/merge.ts
394
+ import { readFileSync, writeFileSync } from "fs";
395
+ import { create } from "@bufbuild/protobuf";
396
+ import {
397
+ deserializeSCIP,
398
+ serializeSCIP,
399
+ DocumentSchema,
400
+ IndexSchema,
401
+ SymbolInformationSchema
402
+ } from "@c4312/scip";
403
+ function mergeScipIndexes(indexes) {
404
+ if (indexes.length === 0) {
405
+ throw new Error("Cannot merge zero SCIP indexes");
406
+ }
407
+ if (indexes.length === 1) {
408
+ return indexes[0];
409
+ }
410
+ const metadata = mergeMetadata(indexes);
411
+ const documents = mergeDocuments(indexes.flatMap((index) => index.documents ?? []));
412
+ const externalSymbols = mergeSymbolInfos(indexes.flatMap((index) => index.externalSymbols ?? []));
413
+ return create(IndexSchema, {
414
+ metadata,
415
+ documents,
416
+ externalSymbols
417
+ });
418
+ }
419
+ function mergeScipFiles(inputPaths, outputPath) {
420
+ if (inputPaths.length === 0) {
421
+ throw new Error("Cannot merge zero SCIP files");
422
+ }
423
+ const indexes = inputPaths.map((path) => deserializeSCIP(readFileSync(path)));
424
+ const merged = mergeScipIndexes(indexes);
425
+ writeFileSync(outputPath, Buffer.from(serializeSCIP(merged)));
426
+ return {
427
+ documentCount: merged.documents.length,
428
+ externalSymbolCount: merged.externalSymbols.length,
429
+ inputCount: inputPaths.length
430
+ };
431
+ }
432
+ function mergeMetadata(indexes) {
433
+ const first = indexes[0]?.metadata;
434
+ if (!first) {
435
+ return void 0;
436
+ }
437
+ const expectedProjectRoot = first.projectRoot;
438
+ for (const index of indexes.slice(1)) {
439
+ const actualProjectRoot = index.metadata?.projectRoot;
440
+ if (expectedProjectRoot && actualProjectRoot && actualProjectRoot !== expectedProjectRoot) {
441
+ throw new Error(
442
+ `Cannot merge SCIP indexes with different project roots: ${expectedProjectRoot} vs ${actualProjectRoot}`
443
+ );
444
+ }
445
+ }
446
+ return first;
447
+ }
448
+ function mergeDocuments(documents) {
449
+ const byPath = /* @__PURE__ */ new Map();
450
+ for (const document of documents) {
451
+ const existing = byPath.get(document.relativePath);
452
+ if (!existing) {
453
+ byPath.set(document.relativePath, document);
454
+ continue;
455
+ }
456
+ byPath.set(document.relativePath, create(DocumentSchema, {
457
+ language: existing.language || document.language,
458
+ relativePath: existing.relativePath || document.relativePath,
459
+ occurrences: [...existing.occurrences, ...document.occurrences],
460
+ symbols: mergeSymbolInfos([...existing.symbols, ...document.symbols]),
461
+ text: chooseText(existing.text, document.text),
462
+ positionEncoding: existing.positionEncoding || document.positionEncoding
463
+ }));
464
+ }
465
+ return [...byPath.values()];
466
+ }
467
+ function mergeSymbolInfos(symbols) {
468
+ const bySymbol = /* @__PURE__ */ new Map();
469
+ for (const symbol of symbols) {
470
+ const existing = bySymbol.get(symbol.symbol);
471
+ if (!existing) {
472
+ bySymbol.set(symbol.symbol, symbol);
473
+ continue;
474
+ }
475
+ bySymbol.set(symbol.symbol, create(SymbolInformationSchema, {
476
+ symbol: existing.symbol,
477
+ documentation: uniqueStrings([...existing.documentation, ...symbol.documentation]),
478
+ relationships: mergeRelationships([...existing.relationships, ...symbol.relationships]),
479
+ kind: existing.kind || symbol.kind,
480
+ displayName: existing.displayName || symbol.displayName,
481
+ enclosingSymbol: existing.enclosingSymbol || symbol.enclosingSymbol,
482
+ signatureDocumentation: existing.signatureDocumentation ?? symbol.signatureDocumentation
483
+ }));
484
+ }
485
+ return [...bySymbol.values()];
486
+ }
487
+ function mergeRelationships(relationships) {
488
+ const seen = /* @__PURE__ */ new Set();
489
+ const merged = [];
490
+ for (const relationship of relationships) {
491
+ const key = [
492
+ relationship.symbol,
493
+ relationship.isReference ? "1" : "0",
494
+ relationship.isImplementation ? "1" : "0",
495
+ relationship.isTypeDefinition ? "1" : "0",
496
+ relationship.isDefinition ? "1" : "0"
497
+ ].join("|");
498
+ if (seen.has(key)) {
499
+ continue;
500
+ }
501
+ seen.add(key);
502
+ merged.push(relationship);
503
+ }
504
+ return merged;
505
+ }
506
+ function chooseText(left, right) {
507
+ if (!left) return right;
508
+ if (!right) return left;
509
+ return left.length >= right.length ? left : right;
510
+ }
511
+ function uniqueStrings(values) {
512
+ return [...new Set(values)];
513
+ }
514
+
393
515
  // src/reindex/index.ts
394
516
  async function reindex(opts) {
395
517
  const {
@@ -425,7 +547,11 @@ async function reindex(opts) {
425
547
  ...process.env,
426
548
  NODE_OPTIONS: `--max-old-space-size=${maxHeapMb}`
427
549
  };
428
- for (const lang of languages2) {
550
+ const languageOutputs = languages2.map((language, index) => ({
551
+ language,
552
+ scipPath: languages2.length > 1 ? tempScipPath(outputScip2, language, index) : outputScip2
553
+ }));
554
+ for (const { language: lang, scipPath } of languageOutputs) {
429
555
  const config = getIndexerConfig(lang);
430
556
  const binaryLabel = describeIndexerBinary(config);
431
557
  const projectLocalBinary = resolveProjectLocalIndexerBinary(config, projectRoot2);
@@ -455,7 +581,7 @@ async function reindex(opts) {
455
581
  const indexerEnv = getIndexerExecutionEnv(config, env, resolvedBinary);
456
582
  const { binary, args } = config.indexArgs({
457
583
  projectRoot: projectRoot2,
458
- outputPath: outputScip2,
584
+ outputPath: scipPath,
459
585
  pnpmWorkspaces: opts.pnpmWorkspaces,
460
586
  indexerBinary: resolvedBinary
461
587
  });
@@ -474,7 +600,11 @@ Make sure ${binaryLabel} is installed and available on PATH.`,
474
600
  { cause: err }
475
601
  );
476
602
  }
477
- moveDefaultOutputIfNeeded(config, projectRoot2, outputScip2);
603
+ moveDefaultOutputIfNeeded(config, projectRoot2, scipPath);
604
+ }
605
+ if (languageOutputs.length > 1) {
606
+ onStatus(`Merging ${languageOutputs.length} language indexes...`);
607
+ mergeScipFiles(languageOutputs.map((entry) => entry.scipPath), outputScip2);
478
608
  }
479
609
  onStatus("Converting to SQLite...");
480
610
  if (!existsSync3(outputScip2)) {
@@ -489,6 +619,12 @@ Make sure ${binaryLabel} is installed and available on PATH.`,
489
619
  } catch (err) {
490
620
  const msg = err instanceof Error ? err.message : String(err);
491
621
  throw new Error(`Failed to convert SCIP index to SQLite: ${msg}`, { cause: err });
622
+ } finally {
623
+ for (const { scipPath } of languageOutputs) {
624
+ if (scipPath !== outputScip2) {
625
+ rmSync(scipPath, { force: true });
626
+ }
627
+ }
492
628
  }
493
629
  const durationMs = Date.now() - start;
494
630
  onStatus(`Done in ${(durationMs / 1e3).toFixed(1)}s`);
@@ -503,6 +639,11 @@ function moveDefaultOutputIfNeeded(config, projectRoot2, outputScip2) {
503
639
  renameSync(defaultOutputPath, outputScip2);
504
640
  }
505
641
  }
642
+ function tempScipPath(outputScip2, language, index) {
643
+ const extension = extname2(outputScip2) || ".scip";
644
+ const stem = basename(outputScip2, extension);
645
+ return join3(dirname(outputScip2), `${stem}.${index + 1}.${language}${extension}`);
646
+ }
506
647
 
507
648
  // src/reindex-worker.ts
508
649
  var projectRoot = process.env["SCIP_REINDEX_PROJECT_ROOT"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scip-query",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Language-agnostic code intelligence CLI powered by SCIP indexes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,6 +60,8 @@
60
60
  "node": ">=18.0.0"
61
61
  },
62
62
  "dependencies": {
63
+ "@bufbuild/protobuf": "^2.11.0",
64
+ "@c4312/scip": "^0.1.0",
63
65
  "better-sqlite3": "^11.7.0",
64
66
  "commander": "^13.1.0",
65
67
  "ignore": "^7.0.3"