@maxgfr/codeindex 2.7.0 → 2.8.0

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/README.md CHANGED
@@ -24,7 +24,9 @@ ultraeval, reconstruct, construct, ultra11y).
24
24
  `mention` edges at file and module level, plus Louvain communities, PageRank/
25
25
  betweenness centrality, a tests→code map, and surprise-edge detection.
26
26
  - **Render** byte-stable `graph.json` / `symbols.json` (two builds of an
27
- unchanged repo are byte-identical).
27
+ unchanged repo are byte-identical), plus a **SCIP** code-intelligence index
28
+ (`index.scip`) via a hand-rolled zero-dependency protobuf encoder — validated
29
+ by the official `scip` CLI (`stats`/`lint`).
28
30
 
29
31
  ## Use as a library (the vendoring model)
30
32
 
@@ -68,6 +70,7 @@ brew install maxgfr/tap/codeindex # or: npm i -g @maxgfr/codeindex
68
70
 
69
71
  codeindex index --repo . --out .codeindex # graph + symbols + incremental cache
70
72
  codeindex graph --repo . > graph.json
73
+ codeindex scip --repo . --out index.scip # SCIP index (--out - for stdout)
71
74
  codeindex callers --repo . # per-symbol caller index
72
75
  codeindex grep 'pattern' --repo .
73
76
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxgfr/codeindex",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Self-contained, deterministic repo-indexing engine: walk + language detection + symbol/import extraction (tree-sitter AST with regex fallback) + import resolution + typed cross-file link-graph + analytics. Ships as a single zero-dependency engine.mjs that consumer tools vendor.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -59,7 +59,7 @@
59
59
  "@semantic-release/git": "^10.0.1",
60
60
  "@tree-sitter-grammars/tree-sitter-lua": "0.4.1",
61
61
  "@types/node": "^20.14.0",
62
- "semantic-release": "^24.2.0",
62
+ "semantic-release": "^25.0.8",
63
63
  "tree-sitter-bash": "0.25.1",
64
64
  "tree-sitter-c": "^0.24.1",
65
65
  "tree-sitter-c-sharp": "^0.23.5",
@@ -1,4 +1,4 @@
1
- declare const ENGINE_VERSION = "2.7.0";
1
+ declare const ENGINE_VERSION = "2.8.0";
2
2
  declare const SCHEMA_VERSION = 4;
3
3
  declare const EXTRACTOR_VERSION = 6;
4
4
  type FileKind = "code" | "doc" | "config" | "asset" | "other";
@@ -415,6 +415,11 @@ declare function renderSymbolsJson(index: SymbolIndex): string;
415
415
 
416
416
  declare function renderGraphJson(graph: Graph): string;
417
417
 
418
+ interface RenderScipOptions {
419
+ projectRoot?: string;
420
+ }
421
+ declare function renderScip(scan: RepoScan, opts?: RenderScipOptions): Uint8Array;
422
+
418
423
  interface BuildIndexOptions extends ScanOptions {
419
424
  meta?: {
420
425
  version?: string;
@@ -615,4 +620,4 @@ declare function rrf<T>(lists: T[][], keyOf: (item: T) => string, k?: number): M
615
620
 
616
621
  declare function runCli(argv: string[]): Promise<void>;
617
622
 
618
- export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawRef, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type ShResult, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, betweennessOf, buildCallerIndex, buildGraph, buildIndexArtifacts, buildModules, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, detectCommunities, detectWorkspaces, diffFiles, diffHunks, enclosingSymbol, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, have, headCommit, insertAfterSymbol, insertBeforeSymbol, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, pagerankOf, parseGitignore, parseRules, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveImport, resolveUniqueSymbol, riskHotspots, rrf, runCli, runMcpServer, scanRepo, searchIndex, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, writeMemory };
623
+ export { type ArchRule, type BuildIndexOptions, type BuiltinRule, type CallerEntry, type CallerIndex, type CallerIndexOptions, type CallerSite, type ChangeCoupling, type CodeInfo, type CodeSymbol, type CouplingOptions, DEFAULT_MAX_FILES, type DeadSymbol, type DiffFile, type DiffSpec, ENGINE_VERSION, EXTRACTOR_VERSION, type Edge, type EdgeKind, type EditResult, type FileCategory, type FileKind, type FileNode, type FileRecord, type FindSymbolOptions, type ForbiddenEdgeRule, type Graph, type GrepOptions, type Hotspot, type Hunk, type IgnoreRule, type IndexArtifacts, MARKDOWN_EXT, type MarkdownInfo, type MermaidOptions, type ModuleInfo, type ModuleNode, type RawRef, type RenderScipOptions, type RepoMapOptions, type RepoScan, type Resolution, type ResolveContext, type RiskHotspot, type RuleSeverity, type RuleViolation, SCHEMA_VERSION, type ScanOptions, type SearchHit, type SearchOptions, type SearchResult, type ShResult, type SurpriseEdge, type SymbolComplexity, type SymbolIndex, type SymbolMatch, type SymbolReferences, type TestMap, type Tier, type WalkOptions, type WalkResult, type WalkedFile, type WorkspaceInfo, type WorkspaceKind, type WorkspacePackage, allGrammarKeys, applyCentrality, betweennessOf, buildCallerIndex, buildGraph, buildIndexArtifacts, buildModules, buildResolveContext, buildSymbolIndex, byKey, byStr, categorize, changeCoupling, changedSince, checkRules, classify, clip, clipInline, communityOf, compileGlobs, complexityOfSource, computeImportPairs, computeSurprises, computeSymbolRefs, computeTestMap, deleteMemory, detectCommunities, detectWorkspaces, diffFiles, diffHunks, enclosingSymbol, ensureGrammars, escapeRegExp, extToLang, extractAst, extractCode, extractMarkdown, extractSymbols, findDeadCode, findReferences, findSymbol, foldText, gitChurn, grammarKeyForExt, grammarReady, grepRepo, have, headCommit, insertAfterSymbol, insertBeforeSymbol, isCode, isDoc, isGitWorktree, isIgnored, isSurprising, isTestFile, isTestPath, keywords, languageOf, listMemories, pagerankOf, parseGitignore, parseRules, rankHotspots, rankedKeywords, readMemory, readText, renderGraphJson, renderMermaid, renderRepoMap, renderScip, renderSymbolsJson, replaceSymbolBody, resolveBaseRef, resolveCallEdges, resolveDocLink, resolveImport, resolveUniqueSymbol, riskHotspots, rrf, runCli, runMcpServer, scanRepo, searchIndex, sh, sha1, shortHash, slugify, subtokens, symbolComplexity, symbolsOverview, testsForModule, tierForPath, uniqueSymbolDefs, untestedModules, untrackedFiles, walk, writeMemory };
@@ -14,7 +14,7 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.7.0";
17
+ ENGINE_VERSION = "2.8.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 6;
20
20
  }
@@ -9551,7 +9551,7 @@ function findDeadCode(scan2) {
9551
9551
  const callers = buildCallerIndex(scan2);
9552
9552
  const refs = computeSymbolRefs(scan2);
9553
9553
  const out2 = [];
9554
- const consider = (s) => s.exported && !REFERENCE_KINDS5.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
9554
+ const consider = (s) => s.exported && !REFERENCE_KINDS6.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
9555
9555
  for (const f of scan2.files) {
9556
9556
  for (const s of f.symbols) {
9557
9557
  if (!consider(s)) continue;
@@ -9564,7 +9564,7 @@ function findDeadCode(scan2) {
9564
9564
  }
9565
9565
  return out2.sort((a, b) => byStr(a.tier, b.tier) || byStr(a.file, b.file) || a.line - b.line);
9566
9566
  }
9567
- var REFERENCE_KINDS5, ENTRYPOINT_RE;
9567
+ var REFERENCE_KINDS6, ENTRYPOINT_RE;
9568
9568
  var init_deadcode = __esm({
9569
9569
  "src/deadcode.ts"() {
9570
9570
  "use strict";
@@ -9572,13 +9572,13 @@ var init_deadcode = __esm({
9572
9572
  init_symbols_json();
9573
9573
  init_tests_map();
9574
9574
  init_sort();
9575
- REFERENCE_KINDS5 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
9575
+ REFERENCE_KINDS6 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
9576
9576
  ENTRYPOINT_RE = /(^|\/)(index|main|cli|app|server|engine)\.[a-z]+$/;
9577
9577
  }
9578
9578
  });
9579
9579
 
9580
9580
  // src/complexity.ts
9581
- import { join as join9 } from "path";
9581
+ import { join as join10 } from "path";
9582
9582
  function complexityOfSource(source) {
9583
9583
  return 1 + (source.match(BRANCH_RE) ?? []).length;
9584
9584
  }
@@ -9588,7 +9588,7 @@ function symbolComplexity(scan2, rel, top = 50) {
9588
9588
  if (f.kind !== "code") continue;
9589
9589
  if (rel && f.rel !== rel) continue;
9590
9590
  if (!f.symbols.length) continue;
9591
- const lines = readText(join9(scan2.root, f.rel)).split("\n");
9591
+ const lines = readText(join10(scan2.root, f.rel)).split("\n");
9592
9592
  for (const s of f.symbols) {
9593
9593
  if (s.kind === "reexport" || s.kind === "reexport-all") continue;
9594
9594
  const end = s.endLine ?? s.line;
@@ -9603,7 +9603,7 @@ function symbolComplexity(scan2, rel, top = 50) {
9603
9603
  }
9604
9604
  function riskHotspots(scan2, churn, top = 20) {
9605
9605
  const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
9606
- const complexity = complexityOfSource(readText(join9(scan2.root, f.rel)));
9606
+ const complexity = complexityOfSource(readText(join10(scan2.root, f.rel)));
9607
9607
  const commits = churn.get(f.rel) ?? 0;
9608
9608
  return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
9609
9609
  });
@@ -10285,6 +10285,247 @@ init_tests_map();
10285
10285
  init_surprise();
10286
10286
  init_symbols_json();
10287
10287
  init_graph_json();
10288
+
10289
+ // src/render/scip.ts
10290
+ init_types();
10291
+ init_walk();
10292
+ init_sort();
10293
+ import { join as join9 } from "path";
10294
+ var utf8 = new TextEncoder();
10295
+ function pushVarint(out2, n) {
10296
+ if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
10297
+ while (n > 127) {
10298
+ out2.push(n & 127 | 128);
10299
+ n = Math.floor(n / 128);
10300
+ }
10301
+ out2.push(n & 127);
10302
+ }
10303
+ function pushTag(out2, field, wire) {
10304
+ pushVarint(out2, field * 8 + wire);
10305
+ }
10306
+ function pushVarintField(out2, field, n) {
10307
+ pushTag(out2, field, 0);
10308
+ pushVarint(out2, n);
10309
+ }
10310
+ function pushLenDelim(out2, field, payload) {
10311
+ pushTag(out2, field, 2);
10312
+ pushVarint(out2, payload.length);
10313
+ for (let i2 = 0; i2 < payload.length; i2++) out2.push(payload[i2]);
10314
+ }
10315
+ function pushString(out2, field, s) {
10316
+ pushLenDelim(out2, field, utf8.encode(s));
10317
+ }
10318
+ function pushPackedInt32(out2, field, values) {
10319
+ const payload = [];
10320
+ for (const v of values) pushVarint(payload, v);
10321
+ pushLenDelim(out2, field, payload);
10322
+ }
10323
+ var F_INDEX_METADATA = 1;
10324
+ var F_INDEX_DOCUMENTS = 2;
10325
+ var F_META_TOOL_INFO = 2;
10326
+ var F_META_PROJECT_ROOT = 3;
10327
+ var F_META_TEXT_ENCODING = 4;
10328
+ var F_TOOL_NAME = 1;
10329
+ var F_TOOL_VERSION = 2;
10330
+ var F_DOC_RELPATH = 1;
10331
+ var F_DOC_OCCURRENCES = 2;
10332
+ var F_DOC_SYMBOLS = 3;
10333
+ var F_DOC_LANGUAGE = 4;
10334
+ var F_DOC_POSITION_ENCODING = 6;
10335
+ var F_OCC_RANGE = 1;
10336
+ var F_OCC_SYMBOL = 2;
10337
+ var F_OCC_ROLES = 3;
10338
+ var F_SI_SYMBOL = 1;
10339
+ var F_SI_KIND = 5;
10340
+ var F_SI_DISPLAY_NAME = 6;
10341
+ var F_SI_ENCLOSING = 8;
10342
+ var TEXT_ENCODING_UTF8 = 1;
10343
+ var ROLE_DEFINITION = 1;
10344
+ var POSITION_ENCODING_UTF16 = 2;
10345
+ var KIND = {
10346
+ function: 17,
10347
+ // Function
10348
+ method: 26,
10349
+ // Method
10350
+ class: 7,
10351
+ // Class
10352
+ interface: 21,
10353
+ // Interface
10354
+ enum: 11,
10355
+ // Enum
10356
+ struct: 49,
10357
+ // Struct
10358
+ trait: 53,
10359
+ // Trait
10360
+ type: 54,
10361
+ // Type
10362
+ const: 8,
10363
+ // Constant
10364
+ var: 61
10365
+ // Variable
10366
+ };
10367
+ var SYMBOL_PREFIX = "codeindex . . . ";
10368
+ var SIMPLE_ID = /^[A-Za-z0-9_+\-$]+$/;
10369
+ function escapeId(name2) {
10370
+ return SIMPLE_ID.test(name2) ? name2 : "`" + name2.replace(/`/g, "``") + "`";
10371
+ }
10372
+ function fileNamespace(rel) {
10373
+ return "`" + rel.replace(/`/g, "``") + "`/";
10374
+ }
10375
+ function parentDescriptor(parent) {
10376
+ return escapeId(parent) + "#";
10377
+ }
10378
+ var TYPE_KINDS = /* @__PURE__ */ new Set(["class", "interface", "enum", "struct", "trait", "type"]);
10379
+ var METHOD_KINDS = /* @__PURE__ */ new Set(["function", "method", "def"]);
10380
+ function suffixFor(kind) {
10381
+ if (TYPE_KINDS.has(kind)) return "#";
10382
+ if (METHOD_KINDS.has(kind)) return "().";
10383
+ return ".";
10384
+ }
10385
+ function baseSymbol(rel, sym) {
10386
+ let s = SYMBOL_PREFIX + fileNamespace(rel);
10387
+ if (sym.parent) s += parentDescriptor(sym.parent);
10388
+ return s + escapeId(sym.name) + suffixFor(sym.kind);
10389
+ }
10390
+ function enclosingSymbolOf(rel, parent) {
10391
+ return SYMBOL_PREFIX + fileNamespace(rel) + parentDescriptor(parent);
10392
+ }
10393
+ function makeUnique(base, line, used) {
10394
+ if (!used.has(base)) {
10395
+ used.add(base);
10396
+ return base;
10397
+ }
10398
+ for (let n = 0; ; n++) {
10399
+ const disambiguator = n === 0 ? String(line) : `${line}_${n}`;
10400
+ const cand = `${base}(${disambiguator})`;
10401
+ if (!used.has(cand)) {
10402
+ used.add(cand);
10403
+ return cand;
10404
+ }
10405
+ }
10406
+ }
10407
+ function familyOf2(lang) {
10408
+ if (lang === "typescript" || lang === "javascript") return "js";
10409
+ if (lang === "c" || lang === "cpp") return "c";
10410
+ return lang;
10411
+ }
10412
+ var REFERENCE_KINDS5 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
10413
+ function isIdentByte(code) {
10414
+ return code >= 48 && code <= 57 || // 0-9
10415
+ code >= 65 && code <= 90 || // A-Z
10416
+ code >= 97 && code <= 122 || // a-z
10417
+ code === 95 || // _
10418
+ code === 36;
10419
+ }
10420
+ function findWord(line, name2) {
10421
+ if (!name2) return null;
10422
+ const wordy = /^[A-Za-z_$][\w$]*$/.test(name2);
10423
+ let from = 0;
10424
+ for (; ; ) {
10425
+ const idx = line.indexOf(name2, from);
10426
+ if (idx < 0) return null;
10427
+ if (!wordy) return [idx, idx + name2.length];
10428
+ const before = idx > 0 ? line.charCodeAt(idx - 1) : -1;
10429
+ const afterIdx = idx + name2.length;
10430
+ const after = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1;
10431
+ if (!isIdentByte(before) && !isIdentByte(after)) return [idx, idx + name2.length];
10432
+ from = idx + 1;
10433
+ }
10434
+ }
10435
+ function renderScip(scan2, opts = {}) {
10436
+ const projectRoot = opts.projectRoot ?? "file://" + scan2.root.replace(/\\/g, "/");
10437
+ const docs = scan2.files.filter((f) => f.kind === "code" && f.symbols.length > 0);
10438
+ const docDefs = /* @__PURE__ */ new Map();
10439
+ const defByName = /* @__PURE__ */ new Map();
10440
+ for (const f of docs) {
10441
+ const used = /* @__PURE__ */ new Set();
10442
+ const entries = [];
10443
+ for (const sym of f.symbols) {
10444
+ const symbolString = makeUnique(baseSymbol(f.rel, sym), sym.line, used);
10445
+ entries.push({ sym, symbolString });
10446
+ if (sym.exported && !REFERENCE_KINDS5.has(sym.kind)) {
10447
+ let arr = defByName.get(sym.name);
10448
+ if (!arr) defByName.set(sym.name, arr = []);
10449
+ arr.push({ symbolString, family: familyOf2(sym.lang) });
10450
+ }
10451
+ }
10452
+ docDefs.set(f.rel, entries);
10453
+ }
10454
+ const resolveRef = (name2, callerFamily) => {
10455
+ const cands = defByName.get(name2);
10456
+ if (!cands || cands.length !== 1) return void 0;
10457
+ const only = cands[0];
10458
+ return only.family === callerFamily ? only.symbolString : void 0;
10459
+ };
10460
+ const documents = [];
10461
+ for (const f of docs) {
10462
+ const text = readText(join9(scan2.root, f.rel));
10463
+ const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
10464
+ const locate = (lineNo, name2) => {
10465
+ const line = lines[lineNo - 1];
10466
+ if (line === void 0) return [lineNo - 1, 0, 0];
10467
+ const r = findWord(line, name2);
10468
+ return r ? [lineNo - 1, r[0], r[1]] : [lineNo - 1, 0, line.length];
10469
+ };
10470
+ const entries = docDefs.get(f.rel);
10471
+ const occs = [];
10472
+ for (const { sym, symbolString } of entries) {
10473
+ occs.push({ range: locate(sym.line, sym.name), symbol: symbolString, roles: ROLE_DEFINITION });
10474
+ }
10475
+ const callerFamily = familyOf2(f.lang);
10476
+ for (const c2 of f.calls ?? []) {
10477
+ const target = resolveRef(c2.name, callerFamily);
10478
+ if (!target) continue;
10479
+ occs.push({ range: locate(c2.line, c2.name), symbol: target, roles: 0 });
10480
+ }
10481
+ occs.sort(
10482
+ (a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1] || a.range[2] - b.range[2] || a.roles - b.roles || byStr(a.symbol, b.symbol)
10483
+ );
10484
+ const seenOcc = /* @__PURE__ */ new Set();
10485
+ const infos = entries.map(({ sym, symbolString }) => ({
10486
+ symbol: symbolString,
10487
+ displayName: sym.name,
10488
+ kind: KIND[sym.kind],
10489
+ enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : void 0
10490
+ })).sort((a, b) => byStr(a.symbol, b.symbol));
10491
+ const doc = [];
10492
+ pushString(doc, F_DOC_RELPATH, f.rel);
10493
+ for (const o of occs) {
10494
+ const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
10495
+ if (seenOcc.has(key)) continue;
10496
+ seenOcc.add(key);
10497
+ const ob = [];
10498
+ pushPackedInt32(ob, F_OCC_RANGE, o.range);
10499
+ pushString(ob, F_OCC_SYMBOL, o.symbol);
10500
+ if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
10501
+ pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
10502
+ }
10503
+ for (const si of infos) {
10504
+ const sb = [];
10505
+ pushString(sb, F_SI_SYMBOL, si.symbol);
10506
+ if (si.kind !== void 0) pushVarintField(sb, F_SI_KIND, si.kind);
10507
+ pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
10508
+ if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
10509
+ pushLenDelim(doc, F_DOC_SYMBOLS, sb);
10510
+ }
10511
+ pushString(doc, F_DOC_LANGUAGE, f.lang);
10512
+ pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
10513
+ documents.push(doc);
10514
+ }
10515
+ const toolInfo = [];
10516
+ pushString(toolInfo, F_TOOL_NAME, "codeindex");
10517
+ pushString(toolInfo, F_TOOL_VERSION, ENGINE_VERSION);
10518
+ const metadata2 = [];
10519
+ pushLenDelim(metadata2, F_META_TOOL_INFO, toolInfo);
10520
+ pushString(metadata2, F_META_PROJECT_ROOT, projectRoot);
10521
+ pushVarintField(metadata2, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
10522
+ const index = [];
10523
+ pushLenDelim(index, F_INDEX_METADATA, metadata2);
10524
+ for (const d of documents) pushLenDelim(index, F_INDEX_DOCUMENTS, d);
10525
+ return Uint8Array.from(index);
10526
+ }
10527
+
10528
+ // src/engine.ts
10288
10529
  init_pipeline();
10289
10530
  init_git();
10290
10531
  init_grep();
@@ -10307,6 +10548,8 @@ init_loader();
10307
10548
  init_pipeline();
10308
10549
  init_graph_json();
10309
10550
  init_symbols_json();
10551
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
10552
+ import { join as join11, resolve } from "path";
10310
10553
  init_scan();
10311
10554
  init_callers();
10312
10555
  init_workspaces();
@@ -10319,8 +10562,6 @@ init_complexity();
10319
10562
  init_viz();
10320
10563
  init_bm25();
10321
10564
  init_rules();
10322
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
10323
- import { join as join10, resolve } from "path";
10324
10565
  var HELP = `codeindex engine v${ENGINE_VERSION} \u2014 deterministic repo indexing
10325
10566
 
10326
10567
  Usage: engine.mjs <command> [flags]
@@ -10331,6 +10572,8 @@ Commands:
10331
10572
  scan Scan summary: file count, language histogram, capped flag
10332
10573
  graph Full link-graph (graph.json bytes) to stdout or --out
10333
10574
  symbols Symbol index (symbols.json bytes) to stdout or --out
10575
+ scip SCIP code-intelligence index (protobuf bytes) into --out
10576
+ (default index.scip; --out - writes to stdout)
10334
10577
  callers Per-symbol caller index (JSON)
10335
10578
  workspaces Monorepo packages + dependency graph (JSON)
10336
10579
  churn Per-file git commit counts (JSON; --since <ref> to bound)
@@ -10349,7 +10592,10 @@ Commands:
10349
10592
 
10350
10593
  Flags:
10351
10594
  --repo <dir> Repo root (default: cwd)
10352
- --out <file> Write output to a file instead of stdout
10595
+ --out <file> Write output to a file instead of stdout (\`scip\`: --out -
10596
+ writes the binary index to stdout)
10597
+ --project-root <uri> \`scip\`: override Metadata.project_root (default
10598
+ file://<repo>); pin it for a byte-reproducible index
10353
10599
  --include <glob> Only include matching paths (repeatable)
10354
10600
  --exclude <glob> Exclude matching paths (repeatable)
10355
10601
  --scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
@@ -10379,7 +10625,10 @@ function parseFlags(args2) {
10379
10625
  return n;
10380
10626
  };
10381
10627
  if (a === "--repo") flags2.repo = resolve(next());
10382
- else if (a === "--out") flags2.out = resolve(next());
10628
+ else if (a === "--out") {
10629
+ const v = next();
10630
+ flags2.out = v === "-" ? "-" : resolve(v);
10631
+ } else if (a === "--project-root") flags2.projectRoot = next();
10383
10632
  else if (a === "--include") flags2.include.push(next());
10384
10633
  else if (a === "--exclude") flags2.exclude.push(next());
10385
10634
  else if (a === "--scope") flags2.scope = next();
@@ -10435,7 +10684,7 @@ async function runCli(argv) {
10435
10684
  if (!flags2.out) throw new Error("index needs --out <dir>");
10436
10685
  const outDir = flags2.out;
10437
10686
  mkdirSync2(outDir, { recursive: true });
10438
- const cachePath = join10(outDir, "cache.json");
10687
+ const cachePath = join11(outDir, "cache.json");
10439
10688
  let cache;
10440
10689
  try {
10441
10690
  const parsed = JSON.parse(readFileSync5(cachePath, "utf8"));
@@ -10445,8 +10694,8 @@ async function runCli(argv) {
10445
10694
  } catch {
10446
10695
  }
10447
10696
  const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
10448
- writeFileSync3(join10(outDir, "graph.json"), renderGraphJson(graph));
10449
- writeFileSync3(join10(outDir, "symbols.json"), renderSymbolsJson(symbols));
10697
+ writeFileSync3(join11(outDir, "graph.json"), renderGraphJson(graph));
10698
+ writeFileSync3(join11(outDir, "symbols.json"), renderSymbolsJson(symbols));
10450
10699
  const files = {};
10451
10700
  for (const f of scan2.files) {
10452
10701
  const entry = { hash: f.hash, record: f, size: f.size };
@@ -10476,6 +10725,16 @@ async function runCli(argv) {
10476
10725
  } else if (cmd === "symbols") {
10477
10726
  const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
10478
10727
  emit(renderSymbolsJson(symbols), flags2.out);
10728
+ } else if (cmd === "scip") {
10729
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
10730
+ const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
10731
+ const out2 = flags2.out ?? resolve("index.scip");
10732
+ if (out2 === "-") process.stdout.write(Buffer.from(bytes));
10733
+ else {
10734
+ writeFileSync3(out2, bytes);
10735
+ process.stderr.write(`codeindex: SCIP index \u2192 ${out2} (${bytes.length} bytes)
10736
+ `);
10737
+ }
10479
10738
  } else if (cmd === "callers") {
10480
10739
  const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
10481
10740
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
@@ -10624,6 +10883,7 @@ export {
10624
10883
  renderGraphJson,
10625
10884
  renderMermaid,
10626
10885
  renderRepoMap,
10886
+ renderScip,
10627
10887
  renderSymbolsJson,
10628
10888
  replaceSymbolBody,
10629
10889
  resolveBaseRef,
package/src/engine-cli.ts CHANGED
@@ -6,6 +6,7 @@ import { ensureGrammars, allGrammarKeys } from "./ast/loader.js";
6
6
  import { buildIndexArtifacts, type BuildIndexOptions } from "./pipeline.js";
7
7
  import { renderGraphJson } from "./render/graph-json.js";
8
8
  import { renderSymbolsJson } from "./render/symbols-json.js";
9
+ import { renderScip } from "./render/scip.js";
9
10
  import { scanRepo } from "./scan.js";
10
11
  import { buildCallerIndex } from "./callers.js";
11
12
  import { detectWorkspaces } from "./workspaces.js";
@@ -29,6 +30,8 @@ Commands:
29
30
  scan Scan summary: file count, language histogram, capped flag
30
31
  graph Full link-graph (graph.json bytes) to stdout or --out
31
32
  symbols Symbol index (symbols.json bytes) to stdout or --out
33
+ scip SCIP code-intelligence index (protobuf bytes) into --out
34
+ (default index.scip; --out - writes to stdout)
32
35
  callers Per-symbol caller index (JSON)
33
36
  workspaces Monorepo packages + dependency graph (JSON)
34
37
  churn Per-file git commit counts (JSON; --since <ref> to bound)
@@ -47,7 +50,10 @@ Commands:
47
50
 
48
51
  Flags:
49
52
  --repo <dir> Repo root (default: cwd)
50
- --out <file> Write output to a file instead of stdout
53
+ --out <file> Write output to a file instead of stdout (\`scip\`: --out -
54
+ writes the binary index to stdout)
55
+ --project-root <uri> \`scip\`: override Metadata.project_root (default
56
+ file://<repo>); pin it for a byte-reproducible index
51
57
  --include <glob> Only include matching paths (repeatable)
52
58
  --exclude <glob> Exclude matching paths (repeatable)
53
59
  --scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
@@ -79,6 +85,7 @@ interface CliFlags {
79
85
  config?: string; // rules config path
80
86
  limit?: number; // search result cap
81
87
  recall?: boolean; // callers: recall-oriented binding
88
+ projectRoot?: string; // scip: override Metadata.project_root
82
89
  positional?: string; // e.g. the grep pattern or search query
83
90
  }
84
91
 
@@ -98,7 +105,10 @@ function parseFlags(args: string[]): CliFlags {
98
105
  return n;
99
106
  };
100
107
  if (a === "--repo") flags.repo = resolve(next());
101
- else if (a === "--out") flags.out = resolve(next());
108
+ else if (a === "--out") {
109
+ const v = next();
110
+ flags.out = v === "-" ? "-" : resolve(v); // "-" = stdout (scip binary)
111
+ } else if (a === "--project-root") flags.projectRoot = next();
102
112
  else if (a === "--include") flags.include.push(next());
103
113
  else if (a === "--exclude") flags.exclude.push(next());
104
114
  else if (a === "--scope") flags.scope = next();
@@ -206,6 +216,15 @@ export async function runCli(argv: string[]): Promise<void> {
206
216
  } else if (cmd === "symbols") {
207
217
  const { symbols } = buildIndexArtifacts(flags.repo, scanOptions(flags));
208
218
  emit(renderSymbolsJson(symbols), flags.out);
219
+ } else if (cmd === "scip") {
220
+ const scan = scanRepo(flags.repo, scanOptions(flags));
221
+ const bytes = renderScip(scan, { projectRoot: flags.projectRoot });
222
+ const out = flags.out ?? resolve("index.scip");
223
+ if (out === "-") process.stdout.write(Buffer.from(bytes));
224
+ else {
225
+ writeFileSync(out, bytes);
226
+ process.stderr.write(`codeindex: SCIP index → ${out} (${bytes.length} bytes)\n`);
227
+ }
209
228
  } else if (cmd === "callers") {
210
229
  const scan = scanRepo(flags.repo, scanOptions(flags));
211
230
  const index = buildCallerIndex(scan, undefined, { recall: flags.recall });
package/src/engine.ts CHANGED
@@ -75,6 +75,8 @@ export { computeSurprises, isSurprising } from "./surprise.js";
75
75
  // Symbol index + machine renderers (render-to-string; consumers own persistence).
76
76
  export { buildSymbolIndex, computeSymbolRefs, renderSymbolsJson } from "./render/symbols-json.js";
77
77
  export { renderGraphJson } from "./render/graph-json.js";
78
+ export { renderScip } from "./render/scip.js";
79
+ export type { RenderScipOptions } from "./render/scip.js";
78
80
 
79
81
  // One-call pipeline.
80
82
  export { buildIndexArtifacts } from "./pipeline.js";
@@ -0,0 +1,369 @@
1
+ // SCIP index export — a hand-rolled, zero-dependency protobuf wire-format
2
+ // encoder plus the codeindex→SCIP mapping and `renderScip`.
3
+ //
4
+ // Every field number and enum value below is copied VERBATIM from the pinned
5
+ // scip.proto (never guessed):
6
+ // https://raw.githubusercontent.com/sourcegraph/scip/44d39fcfc95486d066a796e2cec8c7ec5d429aae/scip.proto
7
+ // sourcegraph/scip @ 44d39fcfc95486d066a796e2cec8c7ec5d429aae
8
+ //
9
+ // Scope: we emit the classic packed `Occurrence.range` (field 1) that
10
+ // scip-typescript emits and that `scip stats`/`scip lint` read; the typed
11
+ // single/multi-line ranges (fields 8-11) are deliberately out of scope. The
12
+ // symbol grammar (scheme/package/descriptors and the suffixes `/` namespace,
13
+ // `#` type, `.` term, `().` method) is taken from the `Symbol` message comment.
14
+
15
+ import { join } from "node:path";
16
+ import { ENGINE_VERSION } from "../types.js";
17
+ import type { CodeSymbol } from "../types.js";
18
+ import type { RepoScan } from "../scan.js";
19
+ import { readText } from "../walk.js";
20
+ import { byStr } from "../sort.js";
21
+
22
+ export interface RenderScipOptions {
23
+ // URI-encoded absolute path to the index root (SCIP `Metadata.project_root`).
24
+ // Overridable so a build is byte-reproducible regardless of the machine's
25
+ // checkout path; defaults to `file://` + the posix repo root.
26
+ projectRoot?: string;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Protobuf wire-format primitives (proto3). Messages are assembled as plain
31
+ // number[] byte buffers and embedded into their parent as length-delimited
32
+ // (wire type 2). Only the two wire types we use are implemented: varint (0)
33
+ // and length-delimited (2).
34
+ // ---------------------------------------------------------------------------
35
+ type Bytes = number[];
36
+ const utf8 = new TextEncoder();
37
+
38
+ // Unsigned LEB128. Every value we encode (field tags, enum values, ranges,
39
+ // sub-message byte lengths) is a small non-negative integer well under 2^31.
40
+ function pushVarint(out: Bytes, n: number): void {
41
+ if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
42
+ while (n > 0x7f) {
43
+ out.push((n & 0x7f) | 0x80);
44
+ n = Math.floor(n / 128);
45
+ }
46
+ out.push(n & 0x7f);
47
+ }
48
+
49
+ function pushTag(out: Bytes, field: number, wire: number): void {
50
+ pushVarint(out, field * 8 + wire);
51
+ }
52
+
53
+ function pushVarintField(out: Bytes, field: number, n: number): void {
54
+ pushTag(out, field, 0);
55
+ pushVarint(out, n);
56
+ }
57
+
58
+ function pushLenDelim(out: Bytes, field: number, payload: ArrayLike<number>): void {
59
+ pushTag(out, field, 2);
60
+ pushVarint(out, payload.length);
61
+ for (let i = 0; i < payload.length; i++) out.push(payload[i]!);
62
+ }
63
+
64
+ function pushString(out: Bytes, field: number, s: string): void {
65
+ pushLenDelim(out, field, utf8.encode(s));
66
+ }
67
+
68
+ // A `repeated int32` in packed encoding: a single length-delimited blob holding
69
+ // the concatenated varints of every element (this is the piece the plain
70
+ // per-element encoding would get wrong).
71
+ function pushPackedInt32(out: Bytes, field: number, values: number[]): void {
72
+ const payload: Bytes = [];
73
+ for (const v of values) pushVarint(payload, v);
74
+ pushLenDelim(out, field, payload);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Pinned scip.proto field numbers & enum values (SHA 44d39fc…).
79
+ // ---------------------------------------------------------------------------
80
+ const F_INDEX_METADATA = 1;
81
+ const F_INDEX_DOCUMENTS = 2; // Index.external_symbols = 3 (unused — always empty)
82
+ const F_META_TOOL_INFO = 2; // Metadata.version = 1 is UnspecifiedProtocolVersion(0), omitted
83
+ const F_META_PROJECT_ROOT = 3;
84
+ const F_META_TEXT_ENCODING = 4;
85
+ const F_TOOL_NAME = 1;
86
+ const F_TOOL_VERSION = 2;
87
+ const F_DOC_RELPATH = 1;
88
+ const F_DOC_OCCURRENCES = 2;
89
+ const F_DOC_SYMBOLS = 3;
90
+ const F_DOC_LANGUAGE = 4;
91
+ const F_DOC_POSITION_ENCODING = 6; // Document.position_encoding = 6 (Document.text = 5 is unused — we never embed source text)
92
+ const F_OCC_RANGE = 1;
93
+ const F_OCC_SYMBOL = 2;
94
+ const F_OCC_ROLES = 3;
95
+ const F_SI_SYMBOL = 1;
96
+ const F_SI_KIND = 5;
97
+ const F_SI_DISPLAY_NAME = 6;
98
+ const F_SI_ENCLOSING = 8;
99
+
100
+ const TEXT_ENCODING_UTF8 = 1; // TextEncoding.UTF8
101
+ const ROLE_DEFINITION = 0x1; // SymbolRole.Definition (a reference omits the field → 0)
102
+
103
+ // PositionEncoding.UTF16CodeUnitOffsetFromLineStart = 2. `locate`/`findWord`
104
+ // below compute range offsets with JS string `indexOf`/`.length`, i.e. UTF-16
105
+ // code units — this is the encoding that matches that computation (NOT
106
+ // Metadata.text_document_encoding, which is the on-disk file encoding and is
107
+ // unrelated to how `character` offsets in ranges are interpreted).
108
+ const POSITION_ENCODING_UTF16 = 2; // PositionEncoding.UTF16CodeUnitOffsetFromLineStart
109
+
110
+ // SymbolInformation.Kind — only the codeindex kinds we can map; anything else
111
+ // leaves the field unset (UnspecifiedKind = 0).
112
+ const KIND: Record<string, number> = {
113
+ function: 17, // Function
114
+ method: 26, // Method
115
+ class: 7, // Class
116
+ interface: 21, // Interface
117
+ enum: 11, // Enum
118
+ struct: 49, // Struct
119
+ trait: 53, // Trait
120
+ type: 54, // Type
121
+ const: 8, // Constant
122
+ var: 61, // Variable
123
+ };
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // SCIP symbol strings (the `Symbol` grammar). Local, minimal scheme:
127
+ // <scheme:codeindex> ' ' <manager:.> ' ' <package-name:.> ' ' <version:.> ' ' <descriptors>
128
+ // Descriptors: the file path as a backtick-escaped namespace, an optional parent
129
+ // type descriptor, then the symbol with a kind-dependent suffix.
130
+ // ---------------------------------------------------------------------------
131
+ const SYMBOL_PREFIX = "codeindex . . . ";
132
+ const SIMPLE_ID = /^[A-Za-z0-9_+\-$]+$/; // <identifier-character> = _ + - $ letters digits
133
+
134
+ function escapeId(name: string): string {
135
+ // A simple-identifier is used as-is; anything else is a backtick-escaped
136
+ // identifier (backticks doubled).
137
+ return SIMPLE_ID.test(name) ? name : "`" + name.replace(/`/g, "``") + "`";
138
+ }
139
+
140
+ function fileNamespace(rel: string): string {
141
+ // A repo path always contains '/' (and usually '.') so it is never a
142
+ // simple-identifier — always backtick-escape, then the namespace suffix '/'.
143
+ return "`" + rel.replace(/`/g, "``") + "`/";
144
+ }
145
+
146
+ // Enclosing members hang off their parent, which we model as a type descriptor.
147
+ function parentDescriptor(parent: string): string {
148
+ return escapeId(parent) + "#";
149
+ }
150
+
151
+ const TYPE_KINDS = new Set(["class", "interface", "enum", "struct", "trait", "type"]);
152
+ const METHOD_KINDS = new Set(["function", "method", "def"]);
153
+
154
+ function suffixFor(kind: string): string {
155
+ if (TYPE_KINDS.has(kind)) return "#"; // <type>
156
+ if (METHOD_KINDS.has(kind)) return "()."; // <method>
157
+ return "."; // <term> — const/var/reexport/… default
158
+ }
159
+
160
+ function baseSymbol(rel: string, sym: CodeSymbol): string {
161
+ let s = SYMBOL_PREFIX + fileNamespace(rel);
162
+ if (sym.parent) s += parentDescriptor(sym.parent);
163
+ return s + escapeId(sym.name) + suffixFor(sym.kind);
164
+ }
165
+
166
+ function enclosingSymbolOf(rel: string, parent: string): string {
167
+ return SYMBOL_PREFIX + fileNamespace(rel) + parentDescriptor(parent);
168
+ }
169
+
170
+ // Guarantee a unique symbol string per document. On a same name+kind collision
171
+ // we append a `(<disambiguator>)` parameter descriptor — grammar-valid after any
172
+ // suffix (type/term/method) and deterministic (seeded by the declaration line).
173
+ function makeUnique(base: string, line: number, used: Set<string>): string {
174
+ if (!used.has(base)) {
175
+ used.add(base);
176
+ return base;
177
+ }
178
+ for (let n = 0; ; n++) {
179
+ const disambiguator = n === 0 ? String(line) : `${line}_${n}`;
180
+ const cand = `${base}(${disambiguator})`;
181
+ if (!used.has(cand)) {
182
+ used.add(cand);
183
+ return cand;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Collapse TS/JS and C/C++ into one family so a reference never binds across
189
+ // unrelated languages (mirrors calls.ts `familyOf`).
190
+ function familyOf(lang: string): string {
191
+ if (lang === "typescript" || lang === "javascript") return "js";
192
+ if (lang === "c" || lang === "cpp") return "c";
193
+ return lang;
194
+ }
195
+
196
+ // A barrel re-export / default alias is a reference to a symbol declared
197
+ // elsewhere — never a call target (mirrors calls.ts REFERENCE_KINDS).
198
+ const REFERENCE_KINDS = new Set(["reexport", "reexport-all", "default"]);
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Range location. CodeSymbol has no columns, so we re-read each file once and
202
+ // locate the name on its declaration line. Deterministic: same content → same
203
+ // bytes.
204
+ // ---------------------------------------------------------------------------
205
+ function isIdentByte(code: number): boolean {
206
+ return (
207
+ (code >= 48 && code <= 57) || // 0-9
208
+ (code >= 65 && code <= 90) || // A-Z
209
+ (code >= 97 && code <= 122) || // a-z
210
+ code === 95 || // _
211
+ code === 36 // $
212
+ );
213
+ }
214
+
215
+ // [startChar, endChar) of `name` on `line`, preferring a whole-word match for a
216
+ // trivial identifier; null when not found.
217
+ function findWord(line: string, name: string): [number, number] | null {
218
+ if (!name) return null;
219
+ const wordy = /^[A-Za-z_$][\w$]*$/.test(name);
220
+ let from = 0;
221
+ for (;;) {
222
+ const idx = line.indexOf(name, from);
223
+ if (idx < 0) return null;
224
+ if (!wordy) return [idx, idx + name.length];
225
+ const before = idx > 0 ? line.charCodeAt(idx - 1) : -1;
226
+ const afterIdx = idx + name.length;
227
+ const after = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1;
228
+ if (!isIdentByte(before) && !isIdentByte(after)) return [idx, idx + name.length];
229
+ from = idx + 1;
230
+ }
231
+ }
232
+
233
+ interface Occ {
234
+ range: number[]; // [line0, startChar, endChar]
235
+ symbol: string;
236
+ roles: number; // 0 = reference, ROLE_DEFINITION = definition
237
+ }
238
+
239
+ interface DefEntry {
240
+ sym: CodeSymbol;
241
+ symbolString: string;
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // renderScip — map a RepoScan to a SCIP Index and encode it to bytes.
246
+ // ---------------------------------------------------------------------------
247
+ export function renderScip(scan: RepoScan, opts: RenderScipOptions = {}): Uint8Array {
248
+ const projectRoot = opts.projectRoot ?? "file://" + scan.root.replace(/\\/g, "/");
249
+
250
+ // One Document per `code` file that declares ≥1 symbol, in scan order (already
251
+ // sorted by rel).
252
+ const docs = scan.files.filter((f) => f.kind === "code" && f.symbols.length > 0);
253
+
254
+ // Pass 1 — assign every symbol its final (per-document-unique) symbol string,
255
+ // and index the exported, non-reference-kind defs by name so references can be
256
+ // resolved against globally-unique names.
257
+ const docDefs = new Map<string, DefEntry[]>();
258
+ const defByName = new Map<string, { symbolString: string; family: string }[]>();
259
+ for (const f of docs) {
260
+ const used = new Set<string>();
261
+ const entries: DefEntry[] = [];
262
+ for (const sym of f.symbols) {
263
+ const symbolString = makeUnique(baseSymbol(f.rel, sym), sym.line, used);
264
+ entries.push({ sym, symbolString });
265
+ if (sym.exported && !REFERENCE_KINDS.has(sym.kind)) {
266
+ let arr = defByName.get(sym.name);
267
+ if (!arr) defByName.set(sym.name, (arr = []));
268
+ arr.push({ symbolString, family: familyOf(sym.lang) });
269
+ }
270
+ }
271
+ docDefs.set(f.rel, entries);
272
+ }
273
+
274
+ // A call resolves to a reference only when the name is defined exactly once in
275
+ // the whole index and in the caller's language family (conservative, like
276
+ // resolveCallEdges — ambiguous names are skipped).
277
+ const resolveRef = (name: string, callerFamily: string): string | undefined => {
278
+ const cands = defByName.get(name);
279
+ if (!cands || cands.length !== 1) return undefined;
280
+ const only = cands[0]!;
281
+ return only.family === callerFamily ? only.symbolString : undefined;
282
+ };
283
+
284
+ // Pass 2 — encode each Document.
285
+ const documents: Bytes[] = [];
286
+ for (const f of docs) {
287
+ const text = readText(join(scan.root, f.rel));
288
+ const lines = text.split("\n").map((l) => (l.endsWith("\r") ? l.slice(0, -1) : l));
289
+ const locate = (lineNo: number, name: string): number[] => {
290
+ const line = lines[lineNo - 1];
291
+ if (line === undefined) return [lineNo - 1, 0, 0];
292
+ const r = findWord(line, name);
293
+ return r ? [lineNo - 1, r[0], r[1]] : [lineNo - 1, 0, line.length];
294
+ };
295
+
296
+ const entries = docDefs.get(f.rel)!;
297
+ const occs: Occ[] = [];
298
+ for (const { sym, symbolString } of entries) {
299
+ occs.push({ range: locate(sym.line, sym.name), symbol: symbolString, roles: ROLE_DEFINITION });
300
+ }
301
+ const callerFamily = familyOf(f.lang);
302
+ for (const c of f.calls ?? []) {
303
+ const target = resolveRef(c.name, callerFamily);
304
+ if (!target) continue;
305
+ occs.push({ range: locate(c.line, c.name), symbol: target, roles: 0 });
306
+ }
307
+ // Deterministic occurrence order + dedupe of exact duplicates.
308
+ occs.sort(
309
+ (a, b) =>
310
+ a.range[0]! - b.range[0]! ||
311
+ a.range[1]! - b.range[1]! ||
312
+ a.range[2]! - b.range[2]! ||
313
+ a.roles - b.roles ||
314
+ byStr(a.symbol, b.symbol),
315
+ );
316
+ const seenOcc = new Set<string>();
317
+
318
+ // One SymbolInformation per definition, sorted by symbol string.
319
+ const infos = entries
320
+ .map(({ sym, symbolString }) => ({
321
+ symbol: symbolString,
322
+ displayName: sym.name,
323
+ kind: KIND[sym.kind],
324
+ enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : undefined,
325
+ }))
326
+ .sort((a, b) => byStr(a.symbol, b.symbol));
327
+
328
+ const doc: Bytes = [];
329
+ pushString(doc, F_DOC_RELPATH, f.rel);
330
+ for (const o of occs) {
331
+ const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
332
+ if (seenOcc.has(key)) continue;
333
+ seenOcc.add(key);
334
+ const ob: Bytes = [];
335
+ pushPackedInt32(ob, F_OCC_RANGE, o.range);
336
+ pushString(ob, F_OCC_SYMBOL, o.symbol);
337
+ if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
338
+ pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
339
+ }
340
+ for (const si of infos) {
341
+ const sb: Bytes = [];
342
+ pushString(sb, F_SI_SYMBOL, si.symbol);
343
+ if (si.kind !== undefined) pushVarintField(sb, F_SI_KIND, si.kind);
344
+ pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
345
+ if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
346
+ pushLenDelim(doc, F_DOC_SYMBOLS, sb);
347
+ }
348
+ pushString(doc, F_DOC_LANGUAGE, f.lang);
349
+ pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
350
+ documents.push(doc);
351
+ }
352
+
353
+ // Metadata { tool_info, project_root, text_document_encoding }.
354
+ const toolInfo: Bytes = [];
355
+ pushString(toolInfo, F_TOOL_NAME, "codeindex");
356
+ pushString(toolInfo, F_TOOL_VERSION, ENGINE_VERSION);
357
+
358
+ const metadata: Bytes = [];
359
+ pushLenDelim(metadata, F_META_TOOL_INFO, toolInfo);
360
+ pushString(metadata, F_META_PROJECT_ROOT, projectRoot);
361
+ pushVarintField(metadata, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
362
+
363
+ // Index { metadata, documents }.
364
+ const index: Bytes = [];
365
+ pushLenDelim(index, F_INDEX_METADATA, metadata);
366
+ for (const d of documents) pushLenDelim(index, F_INDEX_DOCUMENTS, d);
367
+
368
+ return Uint8Array.from(index);
369
+ }
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Single source of truth for the engine version the bundle reports. Kept in
2
2
  // lockstep with package.json by the release pipeline. Do not edit by hand
3
3
  // outside a release.
4
- export const ENGINE_VERSION = "2.7.0";
4
+ export const ENGINE_VERSION = "2.8.0";
5
5
 
6
6
  // Bumped whenever the on-disk artifact shape changes, so a consumer can reject
7
7
  // an index written by an incompatible engine instead of misreading it. The