@maxgfr/codeindex 2.13.0 → 2.14.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.
@@ -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.13.0";
17
+ ENGINE_VERSION = "2.14.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 10;
20
20
  }
@@ -348,23 +348,26 @@ function walk(root, opts = {}) {
348
348
  if (!contained(real)) continue;
349
349
  let entries;
350
350
  try {
351
- entries = readdirSync(frame.dir).sort();
351
+ entries = readdirSync(frame.dir, { withFileTypes: true }).sort(
352
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
353
+ );
352
354
  } catch {
353
355
  continue;
354
356
  }
355
357
  let rules = frame.rules;
356
- if (useGitignore && entries.includes(".gitignore")) {
358
+ if (useGitignore && entries.some((e) => e.name === ".gitignore")) {
357
359
  const parsed = parseGitignore(readText(join(frame.dir, ".gitignore")), frame.rel);
358
360
  if (parsed.length) rules = [...rules, ...parsed];
359
361
  }
360
- for (const name2 of entries) {
362
+ for (const entry of entries) {
363
+ const name2 = entry.name;
361
364
  const abs = join(frame.dir, name2);
362
365
  const rel = frame.rel ? `${frame.rel}/${name2}` : name2;
366
+ const isLink = entry.isSymbolicLink();
367
+ if (entry.isDirectory() && ignoreDirs.has(name2)) continue;
363
368
  let st;
364
- let isLink;
365
369
  try {
366
- st = statSync(abs);
367
- isLink = lstatSync(abs).isSymbolicLink();
370
+ st = isLink ? statSync(abs) : lstatSync(abs);
368
371
  } catch {
369
372
  continue;
370
373
  }
@@ -1772,13 +1775,13 @@ async function Module2(moduleArg = {}) {
1772
1775
  }
1773
1776
  readAsync = /* @__PURE__ */ __name(async (url) => {
1774
1777
  if (isFileURI(url)) {
1775
- return new Promise((resolve2, reject) => {
1778
+ return new Promise((resolve3, reject) => {
1776
1779
  var xhr = new XMLHttpRequest();
1777
1780
  xhr.open("GET", url, true);
1778
1781
  xhr.responseType = "arraybuffer";
1779
1782
  xhr.onload = () => {
1780
1783
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
1781
- resolve2(xhr.response);
1784
+ resolve3(xhr.response);
1782
1785
  return;
1783
1786
  }
1784
1787
  reject(xhr.status);
@@ -1974,9 +1977,9 @@ async function Module2(moduleArg = {}) {
1974
1977
  __name(receiveInstantiationResult, "receiveInstantiationResult");
1975
1978
  var info2 = getWasmImports();
1976
1979
  if (Module["instantiateWasm"]) {
1977
- return new Promise((resolve2, reject) => {
1980
+ return new Promise((resolve3, reject) => {
1978
1981
  Module["instantiateWasm"](info2, (mod, inst) => {
1979
- resolve2(receiveInstance(mod, inst));
1982
+ resolve3(receiveInstance(mod, inst));
1980
1983
  });
1981
1984
  });
1982
1985
  }
@@ -3307,8 +3310,8 @@ async function Module2(moduleArg = {}) {
3307
3310
  if (runtimeInitialized) {
3308
3311
  moduleRtn = Module;
3309
3312
  } else {
3310
- moduleRtn = new Promise((resolve2, reject) => {
3311
- readyPromiseResolve = resolve2;
3313
+ moduleRtn = new Promise((resolve3, reject) => {
3314
+ readyPromiseResolve = resolve3;
3312
3315
  readyPromiseReject = reject;
3313
3316
  });
3314
3317
  }
@@ -5539,27 +5542,41 @@ ${JSON.stringify(symbolNames, null, 2)}`);
5539
5542
 
5540
5543
  // src/ast/loader.ts
5541
5544
  import { readFileSync as readFileSync2, existsSync } from "fs";
5545
+ import { homedir } from "os";
5542
5546
  import { dirname, join as join2 } from "path";
5543
5547
  import { fileURLToPath } from "url";
5544
5548
  function grammarKeyForExt(ext) {
5545
5549
  return EXT_GRAMMAR[ext];
5546
5550
  }
5547
- function resolveGrammarDir() {
5548
- const env = process.env.CODEINDEX_GRAMMAR_DIR ?? process.env.ULTRAINDEX_GRAMMAR_DIR;
5549
- if (env && existsSync(env)) return env;
5550
- const here = dirname(fileURLToPath(import.meta.url));
5551
- const candidates = [
5551
+ function sharedGrammarsCacheDir() {
5552
+ const xdg = process.env.XDG_CACHE_HOME;
5553
+ const base = xdg && xdg.trim() ? xdg.trim() : join2(homedir(), ".cache");
5554
+ return join2(base, "codeindex", "grammars", ENGINE_VERSION);
5555
+ }
5556
+ function resolveGrammarsTier(opts = {}) {
5557
+ const cacheDir = sharedGrammarsCacheDir();
5558
+ const legacy = process.env.CODEINDEX_GRAMMAR_DIR ?? process.env.ULTRAINDEX_GRAMMAR_DIR;
5559
+ if (legacy && legacy.trim() && existsSync(legacy)) return { tier: "env", dir: legacy, cacheDir };
5560
+ const here = opts.moduleDir ?? dirname(fileURLToPath(import.meta.url));
5561
+ const adjacent = [
5552
5562
  join2(here, "grammars"),
5553
5563
  // bundle: <...>/scripts/grammars
5554
5564
  join2(here, "..", "..", "scripts", "grammars"),
5555
5565
  // dev: src/ast → <repo>/scripts/grammars
5556
5566
  join2(here, "..", "scripts", "grammars")
5557
5567
  ];
5558
- for (const c2 of candidates) if (existsSync(c2)) return c2;
5559
- return join2(here, "grammars");
5568
+ for (const c2 of adjacent) if (existsSync(c2)) return { tier: "adjacent", dir: c2, cacheDir };
5569
+ const env = process.env.CODEINDEX_GRAMMARS_DIR;
5570
+ if (env && env.trim() && existsSync(env)) return { tier: "env", dir: env, cacheDir };
5571
+ if (existsSync(cacheDir)) return { tier: "cache", dir: cacheDir, cacheDir };
5572
+ return { tier: "none", cacheDir };
5573
+ }
5574
+ function resolveGrammarsDir(opts) {
5575
+ return resolveGrammarsTier(opts).dir;
5560
5576
  }
5561
5577
  async function ensureGrammars(keys) {
5562
- const dir = resolveGrammarDir();
5578
+ const dir = resolveGrammarsDir();
5579
+ if (!dir) return;
5563
5580
  if (!runtimeReady) {
5564
5581
  const runtime = join2(dir, "web-tree-sitter.wasm");
5565
5582
  if (!existsSync(runtime)) return;
@@ -5584,6 +5601,14 @@ async function ensureGrammars(keys) {
5584
5601
  function allGrammarKeys() {
5585
5602
  return [...new Set(Object.values(EXT_GRAMMAR))];
5586
5603
  }
5604
+ function grammarKeysForExts(exts) {
5605
+ const keys = /* @__PURE__ */ new Set();
5606
+ for (const ext of exts) {
5607
+ const key = EXT_GRAMMAR[ext];
5608
+ if (key !== void 0) keys.add(key);
5609
+ }
5610
+ return [...keys].sort();
5611
+ }
5587
5612
  function grammarReady(key) {
5588
5613
  return loaded.has(key);
5589
5614
  }
@@ -5598,6 +5623,7 @@ var init_loader = __esm({
5598
5623
  "src/ast/loader.ts"() {
5599
5624
  "use strict";
5600
5625
  init_web_tree_sitter();
5626
+ init_types();
5601
5627
  EXT_GRAMMAR = {
5602
5628
  ".ts": "typescript",
5603
5629
  ".mts": "typescript",
@@ -6523,7 +6549,7 @@ function scanRepo(root, opts = {}) {
6523
6549
  const scoped = opts.scope ? [...opts.include ?? [], `${opts.scope.replace(/\/+$/, "")}/**`] : opts.include;
6524
6550
  const include = compileGlobs(scoped);
6525
6551
  const exclude = compileGlobs(opts.exclude);
6526
- const { files: walked, capped, excluded } = walk(root, {
6552
+ const { files: walked, capped, excluded } = opts.precomputedWalk ?? walk(root, {
6527
6553
  maxFileBytes: opts.maxBytes,
6528
6554
  maxFiles: opts.maxFiles,
6529
6555
  gitignore: opts.gitignore,
@@ -6534,6 +6560,9 @@ function scanRepo(root, opts = {}) {
6534
6560
  const languages = {};
6535
6561
  const docText = /* @__PURE__ */ new Map();
6536
6562
  const mtimes = /* @__PURE__ */ new Map();
6563
+ const cache = opts.cache;
6564
+ let allReused = cache !== void 0;
6565
+ let cacheDirty = cache === void 0;
6537
6566
  for (const f of walked) {
6538
6567
  if (outPrefix && (f.abs === opts.out || f.abs.startsWith(outPrefix))) continue;
6539
6568
  if (include && !include(f.rel)) continue;
@@ -6552,8 +6581,11 @@ function scanRepo(root, opts = {}) {
6552
6581
  if (cached && cached.hash === hash) {
6553
6582
  files.push(cached.record);
6554
6583
  if (kind === "doc" && content) docText.set(f.rel, content);
6584
+ if (cached.size !== f.size || cached.mtimeMs !== f.mtimeMs) cacheDirty = true;
6555
6585
  continue;
6556
6586
  }
6587
+ allReused = false;
6588
+ cacheDirty = true;
6557
6589
  const record = {
6558
6590
  rel: f.rel,
6559
6591
  ext: f.ext,
@@ -6595,7 +6627,22 @@ function scanRepo(root, opts = {}) {
6595
6627
  files.push(record);
6596
6628
  }
6597
6629
  files.sort(byKey((f) => f.rel));
6598
- return { root, commit: headCommit(root), files, languages, docText, mtimes, capped, excluded };
6630
+ if (cache !== void 0 && files.length !== cache.size) {
6631
+ allReused = false;
6632
+ cacheDirty = true;
6633
+ }
6634
+ return {
6635
+ root,
6636
+ commit: headCommit(root),
6637
+ files,
6638
+ languages,
6639
+ docText,
6640
+ mtimes,
6641
+ capped,
6642
+ excluded,
6643
+ contentUnchanged: allReused,
6644
+ cacheDirty
6645
+ };
6599
6646
  }
6600
6647
  var init_scan = __esm({
6601
6648
  "src/scan.ts"() {
@@ -6614,7 +6661,7 @@ var init_scan = __esm({
6614
6661
 
6615
6662
  // src/resolve.ts
6616
6663
  import { posix } from "path";
6617
- import { join as join3 } from "path";
6664
+ import { join as join4 } from "path";
6618
6665
  function distToSrcCandidates(target) {
6619
6666
  const segs = norm(target).split("/").filter((s) => s !== ".");
6620
6667
  const out2 = [];
@@ -6701,7 +6748,7 @@ function resolveExtends(fileSet, fromDir, ext) {
6701
6748
  function readTsConfig(root, fileSet, rel, warnings, seen) {
6702
6749
  if (seen.has(rel)) return void 0;
6703
6750
  seen.add(rel);
6704
- const cfg = tolerantJsonParse(readText(join3(root, rel)));
6751
+ const cfg = tolerantJsonParse(readText(join4(root, rel)));
6705
6752
  if (cfg === void 0) {
6706
6753
  warnings.push(`unparseable ${rel} \u2014 its path aliases were ignored`);
6707
6754
  return void 0;
@@ -6830,7 +6877,7 @@ function buildResolveContext(scan2) {
6830
6877
  const goModules = [];
6831
6878
  for (const rel of fileSet) {
6832
6879
  if (rel !== "go.mod" && !rel.endsWith("/go.mod")) continue;
6833
- const text = readText(join3(scan2.root, rel));
6880
+ const text = readText(join4(scan2.root, rel));
6834
6881
  const m = /^\s*module\s+(\S+)/m.exec(text);
6835
6882
  if (!m) continue;
6836
6883
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6840,7 +6887,7 @@ function buildResolveContext(scan2) {
6840
6887
  const rustCrates = [];
6841
6888
  for (const rel of fileSet) {
6842
6889
  if (rel !== "Cargo.toml" && !rel.endsWith("/Cargo.toml")) continue;
6843
- const text = readText(join3(scan2.root, rel));
6890
+ const text = readText(join4(scan2.root, rel));
6844
6891
  const m = /\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/ms.exec(text);
6845
6892
  if (!m) continue;
6846
6893
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6867,7 +6914,7 @@ function buildResolveContext(scan2) {
6867
6914
  const workspacePackages = [];
6868
6915
  for (const rel of fileSet) {
6869
6916
  if (rel !== "package.json" && !rel.endsWith("/package.json")) continue;
6870
- const pkg = tolerantJsonParse(readText(join3(scan2.root, rel)));
6917
+ const pkg = tolerantJsonParse(readText(join4(scan2.root, rel)));
6871
6918
  if (pkg === void 0) {
6872
6919
  warnings.push(`unparseable ${rel} \u2014 skipped for workspace resolution`);
6873
6920
  continue;
@@ -6894,7 +6941,7 @@ function buildResolveContext(scan2) {
6894
6941
  const phpPsr4 = [];
6895
6942
  for (const rel of fileSet) {
6896
6943
  if (rel !== "composer.json" && !rel.endsWith("/composer.json")) continue;
6897
- const composer = tolerantJsonParse(readText(join3(scan2.root, rel)));
6944
+ const composer = tolerantJsonParse(readText(join4(scan2.root, rel)));
6898
6945
  if (!composer) {
6899
6946
  warnings.push(`unparseable ${rel} \u2014 skipped for PHP PSR-4 resolution`);
6900
6947
  continue;
@@ -7469,7 +7516,7 @@ var init_calls = __esm({
7469
7516
  });
7470
7517
 
7471
7518
  // src/graph.ts
7472
- import { join as join4 } from "path";
7519
+ import { join as join5 } from "path";
7473
7520
  function isDistinctive(name2) {
7474
7521
  if (name2.length < 5) return false;
7475
7522
  const internalUpper = /[a-z][A-Z]/.test(name2) || /[A-Z]{2}/.test(name2);
@@ -7548,7 +7595,7 @@ function buildGraph(scan2, ctx, modules, moduleOf, meta) {
7548
7595
  if (unique.size) {
7549
7596
  for (const f of scan2.files) {
7550
7597
  if (f.kind !== "doc") continue;
7551
- const content = scan2.docText.get(f.rel) ?? readText(join4(scan2.root, f.rel));
7598
+ const content = scan2.docText.get(f.rel) ?? readText(join5(scan2.root, f.rel));
7552
7599
  if (!content) continue;
7553
7600
  const tokens = /* @__PURE__ */ new Map();
7554
7601
  for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
@@ -7653,217 +7700,567 @@ var init_graph = __esm({
7653
7700
  }
7654
7701
  });
7655
7702
 
7656
- // src/callers.ts
7657
- function computeImportPairs(scan2) {
7658
- const ctx = buildResolveContext(scan2);
7659
- const pairs = /* @__PURE__ */ new Set();
7703
+ // src/render/symbols-json.ts
7704
+ function computeSymbolRefs(scan2) {
7705
+ const unique = uniqueSymbolDefs(scan2);
7706
+ const refs = /* @__PURE__ */ new Map();
7707
+ if (!unique.size) return refs;
7708
+ const add = (name2, file) => {
7709
+ let set = refs.get(name2);
7710
+ if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
7711
+ set.add(file);
7712
+ };
7660
7713
  for (const f of scan2.files) {
7661
- for (const ref of f.refs) {
7662
- if (ref.kind !== "import") continue;
7663
- const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
7664
- if (r.kind === "resolved" && r.target !== f.rel) pairs.add(`${f.rel}|${r.target}`);
7714
+ if (f.kind === "code" && f.idents) {
7715
+ for (const id of f.idents) {
7716
+ const target = unique.get(id);
7717
+ if (target && target !== f.rel) add(id, f.rel);
7718
+ }
7719
+ } else if (f.kind === "doc") {
7720
+ const content = scan2.docText.get(f.rel);
7721
+ if (!content) continue;
7722
+ for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
7723
+ const target = unique.get(tok);
7724
+ if (target && target !== f.rel) add(tok, f.rel);
7725
+ }
7665
7726
  }
7666
7727
  }
7667
- return pairs;
7728
+ return refs;
7668
7729
  }
7669
- function buildCallerIndex(scan2, importPairs, opts = {}) {
7670
- const pairs = importPairs ?? computeImportPairs(scan2);
7671
- const recall = opts.recall === true;
7672
- const defs = /* @__PURE__ */ new Map();
7673
- for (const f of scan2.files) {
7674
- const seen = /* @__PURE__ */ new Set();
7675
- for (const s of f.symbols) {
7676
- if (!s.exported || REFERENCE_KINDS3.has(s.kind)) continue;
7677
- if (seen.has(s.name)) continue;
7678
- seen.add(s.name);
7679
- let arr = defs.get(s.name);
7680
- if (!arr) defs.set(s.name, arr = []);
7681
- arr.push(s);
7682
- }
7683
- }
7684
- const localDefs = /* @__PURE__ */ new Map();
7730
+ function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
7731
+ const defsByName = /* @__PURE__ */ new Map();
7685
7732
  for (const f of scan2.files) {
7686
- const byName = /* @__PURE__ */ new Map();
7687
7733
  for (const s of f.symbols) {
7688
- if (!REFERENCE_KINDS3.has(s.kind) && !byName.has(s.name)) byName.set(s.name, s);
7689
- }
7690
- localDefs.set(f.rel, byName);
7691
- }
7692
- const sites = /* @__PURE__ */ new Map();
7693
- const record = (def, caller) => {
7694
- let entry = sites.get(def.name + "\0" + def.file);
7695
- if (!entry) sites.set(def.name + "\0" + def.file, entry = { def, callers: [] });
7696
- entry.callers.push(caller);
7697
- };
7698
- for (const f of scan2.files) {
7699
- if (!f.calls?.length) continue;
7700
- const family = familyOf(f.lang);
7701
- const own = localDefs.get(f.rel);
7702
- for (const c2 of f.calls) {
7703
- const local = own.get(c2.name);
7704
- if (local) {
7705
- if (local.line !== c2.line)
7706
- record(local, recall ? { file: f.rel, line: c2.line, confidence: "corroborated" } : { file: f.rel, line: c2.line });
7707
- continue;
7708
- }
7709
- const cands = (defs.get(c2.name) ?? []).filter((d) => familyOf(d.lang) === family && d.file !== f.rel).map((d) => ({ file: d.file, lang: d.lang }));
7710
- if (!cands.length) continue;
7711
- const imported = cands.filter((d) => pairs.has(`${f.rel}|${d.file}`));
7712
- const chosen = family === "js" ? imported.length ? pickCandidate(f.rel, imported) : (
7713
- // JS/TS gate: no corroborating import → no binding. Recall mode
7714
- // relaxes this to a unique-repo-wide name match (issue #7).
7715
- recall && cands.length === 1 ? cands[0] : void 0
7716
- ) : imported.length ? pickCandidate(f.rel, imported) : pickCandidate(f.rel, cands);
7717
- if (!chosen) continue;
7718
- const def = defs.get(c2.name).find((d) => d.file === chosen.file);
7719
- record(
7720
- def,
7721
- recall ? { file: f.rel, line: c2.line, confidence: imported.length ? "corroborated" : "unique-name" } : { file: f.rel, line: c2.line }
7722
- );
7734
+ let arr = defsByName.get(s.name);
7735
+ if (!arr) defsByName.set(s.name, arr = []);
7736
+ arr.push({
7737
+ file: s.file,
7738
+ line: s.line,
7739
+ ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
7740
+ kind: s.kind,
7741
+ exported: s.exported,
7742
+ lang: s.lang,
7743
+ ...s.parent ? { parent: s.parent } : {}
7744
+ });
7723
7745
  }
7724
7746
  }
7725
- const index = /* @__PURE__ */ new Map();
7726
- const keys = [...sites.keys()].sort(byStr);
7727
- for (const key of keys) {
7728
- const { def, callers } = sites.get(key);
7729
- callers.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7730
- if (!index.has(def.name)) index.set(def.name, { def, callers });
7731
- else index.set(`${def.name}@${def.file}`, { def, callers });
7747
+ const defs = {};
7748
+ for (const name2 of [...defsByName.keys()].sort(byStr)) {
7749
+ defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
7732
7750
  }
7733
- return index;
7734
- }
7735
- function enclosingSymbol(scan2, file, line) {
7736
- const f = scan2.files.find((x) => x.rel === file);
7737
- if (!f?.symbols.length) return void 0;
7738
- return enclosingAmong(f.symbols, line);
7739
- }
7740
- function enclosingAmong(symbols, line) {
7741
- let best;
7742
- for (const s of symbols) {
7743
- if (REFERENCE_KINDS3.has(s.kind)) continue;
7744
- if (s.line > line) continue;
7745
- if (s.endLine !== void 0 && line > s.endLine) continue;
7746
- if (!best || s.line > best.line || s.line === best.line && (s.endLine ?? Infinity) <= (best.endLine ?? Infinity)) {
7747
- best = s;
7748
- }
7751
+ const refsOut = {};
7752
+ for (const name2 of [...refs.keys()].sort(byStr)) {
7753
+ const files = [...refs.get(name2)].sort(byStr);
7754
+ if (files.length) refsOut[name2] = files;
7749
7755
  }
7750
- return best;
7756
+ return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
7751
7757
  }
7752
- function buildRawCallerIndex(scan2) {
7753
- const byName = /* @__PURE__ */ new Map();
7754
- for (const f of scan2.files) {
7755
- if (!f.calls?.length) continue;
7756
- const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
7757
- for (const c2 of f.calls) {
7758
- const site = { file: f.rel, line: c2.line };
7759
- if (c2.receiver !== void 0) site.receiver = c2.receiver;
7760
- const enc = enclosingAmong(symbols, c2.line);
7761
- if (enc) site.enclosingSymbol = enc;
7762
- let arr = byName.get(c2.name);
7763
- if (!arr) byName.set(c2.name, arr = []);
7764
- arr.push(site);
7765
- }
7766
- }
7767
- const index = /* @__PURE__ */ new Map();
7768
- for (const name2 of [...byName.keys()].sort(byStr)) {
7769
- const sites = byName.get(name2);
7770
- sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7771
- index.set(name2, sites);
7772
- }
7773
- return index;
7758
+ function renderSymbolsJson(index) {
7759
+ return JSON.stringify(index, null, 2) + "\n";
7774
7760
  }
7775
- var REFERENCE_KINDS3;
7776
- var init_callers = __esm({
7777
- "src/callers.ts"() {
7761
+ var init_symbols_json = __esm({
7762
+ "src/render/symbols-json.ts"() {
7778
7763
  "use strict";
7779
- init_calls();
7780
- init_resolve();
7764
+ init_types();
7781
7765
  init_sort();
7782
- REFERENCE_KINDS3 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7766
+ init_graph();
7783
7767
  }
7784
7768
  });
7785
7769
 
7786
- // src/query.ts
7787
- import { join as join5 } from "path";
7788
- function symbolsOverview(scan2, rel) {
7789
- const f = scan2.files.find((x) => x.rel === rel);
7790
- if (!f) return [];
7791
- return [...f.symbols].filter((s) => !REFERENCE_KINDS4.has(s.kind)).sort((a, b) => a.line - b.line || byStr(a.name, b.name));
7792
- }
7793
- function findSymbol(scan2, namePath, opts = {}) {
7794
- const segments = namePath.split("/").filter(Boolean);
7795
- if (!segments.length) return [];
7796
- const leaf = segments[segments.length - 1];
7797
- const parents = segments.slice(0, -1);
7798
- const matchName = (name2, wanted) => opts.substring ? name2.toLowerCase().includes(wanted.toLowerCase()) : name2 === wanted;
7770
+ // src/bm25.ts
7771
+ function subtokens(raw) {
7772
+ const folded = foldText(raw).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
7799
7773
  const out2 = [];
7800
- for (const f of scan2.files) {
7801
- for (const s of f.symbols) {
7802
- if (REFERENCE_KINDS4.has(s.kind)) continue;
7803
- if (!matchName(s.name, leaf)) continue;
7804
- if (parents.length) {
7805
- const parent = parents[parents.length - 1];
7806
- if (!s.parent || s.parent !== parent) continue;
7807
- }
7808
- out2.push({ ...s });
7809
- }
7810
- }
7811
- out2.sort(
7812
- (a, b) => Number(b.name === leaf) - Number(a.name === leaf) || byStr(a.file, b.file) || a.line - b.line
7813
- );
7814
- const capped = out2.slice(0, opts.maxResults ?? 50);
7815
- if (opts.includeBody) {
7816
- for (const m of capped) {
7817
- const end = m.endLine ?? m.line;
7818
- const content = readText(join5(scan2.root, m.file));
7819
- if (!content) continue;
7820
- m.body = content.split("\n").slice(m.line - 1, end).join("\n");
7821
- }
7774
+ const seen = /* @__PURE__ */ new Set();
7775
+ const push = (t) => {
7776
+ if (t.length < 2 || seen.has(t)) return;
7777
+ seen.add(t);
7778
+ out2.push(t);
7779
+ };
7780
+ if (!/\s/.test(raw.trim())) push(foldText(raw).toLowerCase().replace(/[^a-z0-9_]+/g, ""));
7781
+ for (const part of folded.split(/[^A-Za-z0-9]+/)) push(part.toLowerCase());
7782
+ return out2;
7783
+ }
7784
+ function addTerms(doc, text) {
7785
+ for (const t of subtokens(text)) {
7786
+ doc.tf.set(t, (doc.tf.get(t) ?? 0) + 1);
7787
+ doc.len++;
7822
7788
  }
7823
- return capped;
7824
7789
  }
7825
- function findReferences(scan2, name2) {
7826
- const defs = [];
7790
+ function buildDocs(scan2) {
7791
+ const docs = [];
7827
7792
  for (const f of scan2.files) {
7793
+ const doc = { file: f.rel, tf: /* @__PURE__ */ new Map(), len: 0, symbols: [] };
7794
+ const seenSym = /* @__PURE__ */ new Set();
7828
7795
  for (const s of f.symbols) {
7829
- if (s.name === name2 && !REFERENCE_KINDS4.has(s.kind)) defs.push(s);
7830
- }
7831
- }
7832
- defs.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7833
- const index = buildCallerIndex(scan2);
7834
- const entry = index.get(name2);
7835
- const callSites = entry ? entry.callers : [];
7836
- const referencingFiles = /* @__PURE__ */ new Set();
7837
- const unique = uniqueSymbolDefs(scan2);
7838
- const defFile = unique.get(name2);
7839
- for (const f of scan2.files) {
7840
- if (f.rel === defFile) continue;
7841
- if (f.kind === "code" && f.idents?.includes(name2)) referencingFiles.add(f.rel);
7842
- else if (f.kind === "doc") {
7843
- const content = scan2.docText.get(f.rel);
7844
- if (content && new RegExp(`\\b${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content)) {
7845
- referencingFiles.add(f.rel);
7796
+ addTerms(doc, s.name);
7797
+ if (!seenSym.has(s.name)) {
7798
+ seenSym.add(s.name);
7799
+ doc.symbols.push(s.name);
7846
7800
  }
7847
7801
  }
7802
+ for (const seg of f.rel.split("/")) addTerms(doc, seg);
7803
+ for (const h of f.headings) addTerms(doc, h);
7804
+ if (f.summary) addTerms(doc, f.summary);
7805
+ docs.push(doc);
7848
7806
  }
7849
- for (const site of callSites) referencingFiles.add(site.file);
7850
- return { defs, callSites, referencingFiles: [...referencingFiles].sort(byStr) };
7807
+ return docs;
7851
7808
  }
7852
- var REFERENCE_KINDS4;
7853
- var init_query = __esm({
7809
+ function charTrigrams(term) {
7810
+ const padded = `^^${term}$$`;
7811
+ const grams = /* @__PURE__ */ new Set();
7812
+ for (let i2 = 0; i2 + 3 <= padded.length; i2++) grams.add(padded.slice(i2, i2 + 3));
7813
+ return grams;
7814
+ }
7815
+ function diceCoefficient(a, b) {
7816
+ if (!a.size || !b.size) return 0;
7817
+ let inter = 0;
7818
+ for (const g of a) if (b.has(g)) inter++;
7819
+ return 2 * inter / (a.size + b.size);
7820
+ }
7821
+ function buildTrigramIndex(docs) {
7822
+ const index = /* @__PURE__ */ new Map();
7823
+ for (const d of docs) {
7824
+ for (const term of d.tf.keys()) {
7825
+ if (!index.has(term)) index.set(term, charTrigrams(term));
7826
+ }
7827
+ }
7828
+ return index;
7829
+ }
7830
+ function searchIndex(scan2, query, opts = {}) {
7831
+ const terms = [];
7832
+ const seen = /* @__PURE__ */ new Set();
7833
+ for (const kw of keywords(query)) {
7834
+ for (const t of subtokens(kw)) {
7835
+ if (seen.has(t)) continue;
7836
+ seen.add(t);
7837
+ terms.push(t);
7838
+ }
7839
+ }
7840
+ if (!terms.length) return [];
7841
+ const docs = bm25DocsFor(scan2);
7842
+ const n = docs.length;
7843
+ if (!n) return [];
7844
+ let totalLen = 0;
7845
+ for (const d of docs) totalLen += d.len;
7846
+ const avgLen = totalLen / n || 1;
7847
+ const df = /* @__PURE__ */ new Map();
7848
+ for (const t of terms) {
7849
+ let count = 0;
7850
+ for (const d of docs) if (d.tf.has(t)) count++;
7851
+ df.set(t, count);
7852
+ }
7853
+ const fuzzyEnabled = opts.fuzzy ?? true;
7854
+ const fuzzyCandidates = /* @__PURE__ */ new Map();
7855
+ if (fuzzyEnabled) {
7856
+ const unmatched = terms.filter((t) => df.get(t) === 0);
7857
+ if (unmatched.length) {
7858
+ const trigramIndex = bm25TrigramsFor(scan2);
7859
+ for (const t of unmatched) {
7860
+ const grams = charTrigrams(t);
7861
+ const candidates = [];
7862
+ for (const [vocabTerm, vocabGrams] of trigramIndex) {
7863
+ const dice = diceCoefficient(grams, vocabGrams);
7864
+ if (dice >= FUZZY_DICE_THRESHOLD) candidates.push({ term: vocabTerm, dice });
7865
+ }
7866
+ candidates.sort((a, b) => b.dice - a.dice || byStr(a.term, b.term));
7867
+ fuzzyCandidates.set(t, candidates.slice(0, FUZZY_CAP));
7868
+ }
7869
+ }
7870
+ }
7871
+ const vocabDf = /* @__PURE__ */ new Map();
7872
+ const dfOfVocabTerm = (term) => {
7873
+ const known = df.get(term) ?? vocabDf.get(term);
7874
+ if (known !== void 0) return known;
7875
+ let count = 0;
7876
+ for (const d of docs) if (d.tf.has(term)) count++;
7877
+ vocabDf.set(term, count);
7878
+ return count;
7879
+ };
7880
+ const results = [];
7881
+ for (const d of docs) {
7882
+ let score = 0;
7883
+ const matched = [];
7884
+ const symbolTerms = /* @__PURE__ */ new Set();
7885
+ const fuzzyHit = /* @__PURE__ */ new Set();
7886
+ for (const t of terms) {
7887
+ const tf = d.tf.get(t);
7888
+ if (tf) {
7889
+ matched.push(t);
7890
+ symbolTerms.add(t);
7891
+ const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
7892
+ score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
7893
+ continue;
7894
+ }
7895
+ const candidates = fuzzyCandidates.get(t);
7896
+ if (!candidates) continue;
7897
+ for (const cand of candidates) {
7898
+ const ctf = d.tf.get(cand.term);
7899
+ if (!ctf) continue;
7900
+ const cdf = dfOfVocabTerm(cand.term);
7901
+ const idf = Math.log(1 + (n - cdf + 0.5) / (cdf + 0.5));
7902
+ const contribution = idf * (ctf * (K1 + 1)) / (ctf + K1 * (1 - B + B * d.len / avgLen));
7903
+ score += contribution * cand.dice;
7904
+ symbolTerms.add(cand.term);
7905
+ fuzzyHit.add(t);
7906
+ }
7907
+ }
7908
+ if (!matched.length && !fuzzyHit.size) continue;
7909
+ const scored = d.symbols.map((name2) => {
7910
+ const toks = new Set(subtokens(name2));
7911
+ let hits = 0;
7912
+ for (const t of symbolTerms) if (toks.has(t)) hits++;
7913
+ return { name: name2, hits };
7914
+ }).filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits || byStr(a.name, b.name));
7915
+ const result = {
7916
+ file: d.file,
7917
+ score: Number(score.toFixed(4)),
7918
+ matchedTerms: matched.sort(byStr),
7919
+ topSymbols: scored.slice(0, TOP_SYMBOLS).map((s) => s.name)
7920
+ };
7921
+ if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
7922
+ results.push(result);
7923
+ }
7924
+ results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
7925
+ return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
7926
+ }
7927
+ var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP;
7928
+ var init_bm25 = __esm({
7929
+ "src/bm25.ts"() {
7930
+ "use strict";
7931
+ init_derived();
7932
+ init_util();
7933
+ init_sort();
7934
+ K1 = 1.2;
7935
+ B = 0.75;
7936
+ DEFAULT_LIMIT = 20;
7937
+ TOP_SYMBOLS = 5;
7938
+ FUZZY_DICE_THRESHOLD = 0.6;
7939
+ FUZZY_CAP = 3;
7940
+ }
7941
+ });
7942
+
7943
+ // src/complexity.ts
7944
+ import { join as join6 } from "path";
7945
+ function complexityOfSource(source) {
7946
+ return 1 + (source.match(BRANCH_RE) ?? []).length;
7947
+ }
7948
+ function symbolComplexity(scan2, rel, top = 50) {
7949
+ const out2 = [];
7950
+ for (const f of scan2.files) {
7951
+ if (f.kind !== "code") continue;
7952
+ if (rel && f.rel !== rel) continue;
7953
+ if (!f.symbols.length) continue;
7954
+ const lines = readText(join6(scan2.root, f.rel)).split("\n");
7955
+ for (const s of f.symbols) {
7956
+ if (s.kind === "reexport" || s.kind === "reexport-all") continue;
7957
+ const end = s.endLine ?? s.line;
7958
+ const body2 = lines.slice(s.line - 1, end).join("\n");
7959
+ const entry = { file: f.rel, name: s.name, line: s.line, complexity: complexityOfSource(body2) };
7960
+ if (s.endLine !== void 0) entry.endLine = s.endLine;
7961
+ out2.push(entry);
7962
+ }
7963
+ }
7964
+ out2.sort((a, b) => b.complexity - a.complexity || byStr(a.file, b.file) || a.line - b.line);
7965
+ return out2.slice(0, top);
7966
+ }
7967
+ function riskHotspots(scan2, churn, top = 20) {
7968
+ const complexityByFile = fileComplexityFor(scan2);
7969
+ const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
7970
+ const complexity = complexityByFile.get(f.rel);
7971
+ const commits = churn.get(f.rel) ?? 0;
7972
+ return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
7973
+ });
7974
+ out2.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
7975
+ return out2.slice(0, top);
7976
+ }
7977
+ var BRANCH_RE;
7978
+ var init_complexity = __esm({
7979
+ "src/complexity.ts"() {
7980
+ "use strict";
7981
+ init_derived();
7982
+ init_walk();
7983
+ init_sort();
7984
+ BRANCH_RE = /\b(if|elif|elsif|else\s+if|for|foreach|while|until|unless|case|when|match|catch|rescue|except)\b|&&|\|\||(?<![?:])\?(?![?.:])/g;
7985
+ }
7986
+ });
7987
+
7988
+ // src/derived.ts
7989
+ import { join as join7 } from "path";
7990
+ function cacheFor(scan2) {
7991
+ let c2 = caches.get(scan2);
7992
+ if (!c2) caches.set(scan2, c2 = {});
7993
+ return c2;
7994
+ }
7995
+ function resolveContextFor(scan2) {
7996
+ const c2 = cacheFor(scan2);
7997
+ return c2.resolveCtx ??= buildResolveContext(scan2);
7998
+ }
7999
+ function importPairsFor(scan2) {
8000
+ const c2 = cacheFor(scan2);
8001
+ if (!c2.importPairs) {
8002
+ const ctx = resolveContextFor(scan2);
8003
+ const pairs = /* @__PURE__ */ new Set();
8004
+ for (const f of scan2.files) {
8005
+ for (const ref of f.refs) {
8006
+ if (ref.kind !== "import") continue;
8007
+ const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
8008
+ if (r.kind === "resolved" && r.target !== f.rel) pairs.add(`${f.rel}|${r.target}`);
8009
+ }
8010
+ }
8011
+ c2.importPairs = pairs;
8012
+ }
8013
+ return c2.importPairs;
8014
+ }
8015
+ function uniqueDefsFor(scan2) {
8016
+ const c2 = cacheFor(scan2);
8017
+ return c2.uniqueDefs ??= uniqueSymbolDefs(scan2);
8018
+ }
8019
+ function symbolRefsFor(scan2) {
8020
+ const c2 = cacheFor(scan2);
8021
+ return c2.symbolRefs ??= computeSymbolRefs(scan2);
8022
+ }
8023
+ function callerIndexFor(scan2) {
8024
+ const c2 = cacheFor(scan2);
8025
+ return c2.callerIndex ??= buildCallerIndex(scan2, importPairsFor(scan2));
8026
+ }
8027
+ function bm25DocsFor(scan2) {
8028
+ const c2 = cacheFor(scan2);
8029
+ return (c2.bm25 ??= { docs: buildDocs(scan2) }).docs;
8030
+ }
8031
+ function bm25TrigramsFor(scan2) {
8032
+ const c2 = cacheFor(scan2);
8033
+ const bm25 = c2.bm25 ??= { docs: buildDocs(scan2) };
8034
+ return bm25.trigrams ??= buildTrigramIndex(bm25.docs);
8035
+ }
8036
+ function fileComplexityFor(scan2) {
8037
+ const c2 = cacheFor(scan2);
8038
+ if (!c2.fileComplexity) {
8039
+ const m = /* @__PURE__ */ new Map();
8040
+ for (const f of scan2.files) {
8041
+ if (f.kind !== "code") continue;
8042
+ m.set(f.rel, complexityOfSource(readText(join7(scan2.root, f.rel))));
8043
+ }
8044
+ c2.fileComplexity = m;
8045
+ }
8046
+ return c2.fileComplexity;
8047
+ }
8048
+ var caches;
8049
+ var init_derived = __esm({
8050
+ "src/derived.ts"() {
8051
+ "use strict";
8052
+ init_resolve();
8053
+ init_graph();
8054
+ init_symbols_json();
8055
+ init_callers();
8056
+ init_bm25();
8057
+ init_complexity();
8058
+ init_walk();
8059
+ caches = /* @__PURE__ */ new WeakMap();
8060
+ }
8061
+ });
8062
+
8063
+ // src/callers.ts
8064
+ function computeImportPairs(scan2) {
8065
+ return new Set(importPairsFor(scan2));
8066
+ }
8067
+ function buildCallerIndex(scan2, importPairs, opts = {}) {
8068
+ const pairs = importPairs ?? importPairsFor(scan2);
8069
+ const recall = opts.recall === true;
8070
+ const defs = /* @__PURE__ */ new Map();
8071
+ for (const f of scan2.files) {
8072
+ const seen = /* @__PURE__ */ new Set();
8073
+ for (const s of f.symbols) {
8074
+ if (!s.exported || REFERENCE_KINDS3.has(s.kind)) continue;
8075
+ if (seen.has(s.name)) continue;
8076
+ seen.add(s.name);
8077
+ let arr = defs.get(s.name);
8078
+ if (!arr) defs.set(s.name, arr = []);
8079
+ arr.push(s);
8080
+ }
8081
+ }
8082
+ const localDefs = /* @__PURE__ */ new Map();
8083
+ for (const f of scan2.files) {
8084
+ const byName = /* @__PURE__ */ new Map();
8085
+ for (const s of f.symbols) {
8086
+ if (!REFERENCE_KINDS3.has(s.kind) && !byName.has(s.name)) byName.set(s.name, s);
8087
+ }
8088
+ localDefs.set(f.rel, byName);
8089
+ }
8090
+ const sites = /* @__PURE__ */ new Map();
8091
+ const record = (def, caller) => {
8092
+ let entry = sites.get(def.name + "\0" + def.file);
8093
+ if (!entry) sites.set(def.name + "\0" + def.file, entry = { def, callers: [] });
8094
+ entry.callers.push(caller);
8095
+ };
8096
+ for (const f of scan2.files) {
8097
+ if (!f.calls?.length) continue;
8098
+ const family = familyOf(f.lang);
8099
+ const own = localDefs.get(f.rel);
8100
+ for (const c2 of f.calls) {
8101
+ const local = own.get(c2.name);
8102
+ if (local) {
8103
+ if (local.line !== c2.line)
8104
+ record(local, recall ? { file: f.rel, line: c2.line, confidence: "corroborated" } : { file: f.rel, line: c2.line });
8105
+ continue;
8106
+ }
8107
+ const cands = (defs.get(c2.name) ?? []).filter((d) => familyOf(d.lang) === family && d.file !== f.rel).map((d) => ({ file: d.file, lang: d.lang }));
8108
+ if (!cands.length) continue;
8109
+ const imported = cands.filter((d) => pairs.has(`${f.rel}|${d.file}`));
8110
+ const chosen = family === "js" ? imported.length ? pickCandidate(f.rel, imported) : (
8111
+ // JS/TS gate: no corroborating import → no binding. Recall mode
8112
+ // relaxes this to a unique-repo-wide name match (issue #7).
8113
+ recall && cands.length === 1 ? cands[0] : void 0
8114
+ ) : imported.length ? pickCandidate(f.rel, imported) : pickCandidate(f.rel, cands);
8115
+ if (!chosen) continue;
8116
+ const def = defs.get(c2.name).find((d) => d.file === chosen.file);
8117
+ record(
8118
+ def,
8119
+ recall ? { file: f.rel, line: c2.line, confidence: imported.length ? "corroborated" : "unique-name" } : { file: f.rel, line: c2.line }
8120
+ );
8121
+ }
8122
+ }
8123
+ const index = /* @__PURE__ */ new Map();
8124
+ const keys = [...sites.keys()].sort(byStr);
8125
+ for (const key of keys) {
8126
+ const { def, callers } = sites.get(key);
8127
+ callers.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
8128
+ if (!index.has(def.name)) index.set(def.name, { def, callers });
8129
+ else index.set(`${def.name}@${def.file}`, { def, callers });
8130
+ }
8131
+ return index;
8132
+ }
8133
+ function enclosingSymbol(scan2, file, line) {
8134
+ const f = scan2.files.find((x) => x.rel === file);
8135
+ if (!f?.symbols.length) return void 0;
8136
+ return enclosingAmong(f.symbols, line);
8137
+ }
8138
+ function enclosingAmong(symbols, line) {
8139
+ let best;
8140
+ for (const s of symbols) {
8141
+ if (REFERENCE_KINDS3.has(s.kind)) continue;
8142
+ if (s.line > line) continue;
8143
+ if (s.endLine !== void 0 && line > s.endLine) continue;
8144
+ if (!best || s.line > best.line || s.line === best.line && (s.endLine ?? Infinity) <= (best.endLine ?? Infinity)) {
8145
+ best = s;
8146
+ }
8147
+ }
8148
+ return best;
8149
+ }
8150
+ function buildRawCallerIndex(scan2) {
8151
+ const byName = /* @__PURE__ */ new Map();
8152
+ for (const f of scan2.files) {
8153
+ if (!f.calls?.length) continue;
8154
+ const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
8155
+ for (const c2 of f.calls) {
8156
+ const site = { file: f.rel, line: c2.line };
8157
+ if (c2.receiver !== void 0) site.receiver = c2.receiver;
8158
+ const enc = enclosingAmong(symbols, c2.line);
8159
+ if (enc) site.enclosingSymbol = enc;
8160
+ let arr = byName.get(c2.name);
8161
+ if (!arr) byName.set(c2.name, arr = []);
8162
+ arr.push(site);
8163
+ }
8164
+ }
8165
+ const index = /* @__PURE__ */ new Map();
8166
+ for (const name2 of [...byName.keys()].sort(byStr)) {
8167
+ const sites = byName.get(name2);
8168
+ sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
8169
+ index.set(name2, sites);
8170
+ }
8171
+ return index;
8172
+ }
8173
+ var REFERENCE_KINDS3;
8174
+ var init_callers = __esm({
8175
+ "src/callers.ts"() {
8176
+ "use strict";
8177
+ init_calls();
8178
+ init_derived();
8179
+ init_sort();
8180
+ REFERENCE_KINDS3 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
8181
+ }
8182
+ });
8183
+
8184
+ // src/query.ts
8185
+ import { join as join8 } from "path";
8186
+ function symbolsOverview(scan2, rel) {
8187
+ const f = scan2.files.find((x) => x.rel === rel);
8188
+ if (!f) return [];
8189
+ return [...f.symbols].filter((s) => !REFERENCE_KINDS4.has(s.kind)).sort((a, b) => a.line - b.line || byStr(a.name, b.name));
8190
+ }
8191
+ function findSymbol(scan2, namePath, opts = {}) {
8192
+ const segments = namePath.split("/").filter(Boolean);
8193
+ if (!segments.length) return [];
8194
+ const leaf = segments[segments.length - 1];
8195
+ const parents = segments.slice(0, -1);
8196
+ const matchName = (name2, wanted) => opts.substring ? name2.toLowerCase().includes(wanted.toLowerCase()) : name2 === wanted;
8197
+ const out2 = [];
8198
+ for (const f of scan2.files) {
8199
+ for (const s of f.symbols) {
8200
+ if (REFERENCE_KINDS4.has(s.kind)) continue;
8201
+ if (!matchName(s.name, leaf)) continue;
8202
+ if (parents.length) {
8203
+ const parent = parents[parents.length - 1];
8204
+ if (!s.parent || s.parent !== parent) continue;
8205
+ }
8206
+ out2.push({ ...s });
8207
+ }
8208
+ }
8209
+ out2.sort(
8210
+ (a, b) => Number(b.name === leaf) - Number(a.name === leaf) || byStr(a.file, b.file) || a.line - b.line
8211
+ );
8212
+ const capped = out2.slice(0, opts.maxResults ?? 50);
8213
+ if (opts.includeBody) {
8214
+ for (const m of capped) {
8215
+ const end = m.endLine ?? m.line;
8216
+ const content = readText(join8(scan2.root, m.file));
8217
+ if (!content) continue;
8218
+ m.body = content.split("\n").slice(m.line - 1, end).join("\n");
8219
+ }
8220
+ }
8221
+ return capped;
8222
+ }
8223
+ function findReferences(scan2, name2) {
8224
+ const defs = [];
8225
+ for (const f of scan2.files) {
8226
+ for (const s of f.symbols) {
8227
+ if (s.name === name2 && !REFERENCE_KINDS4.has(s.kind)) defs.push(s);
8228
+ }
8229
+ }
8230
+ defs.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
8231
+ const index = callerIndexFor(scan2);
8232
+ const entry = index.get(name2);
8233
+ const callSites = entry ? [...entry.callers] : [];
8234
+ const referencingFiles = /* @__PURE__ */ new Set();
8235
+ const unique = uniqueDefsFor(scan2);
8236
+ const defFile = unique.get(name2);
8237
+ for (const f of scan2.files) {
8238
+ if (f.rel === defFile) continue;
8239
+ if (f.kind === "code" && f.idents?.includes(name2)) referencingFiles.add(f.rel);
8240
+ else if (f.kind === "doc") {
8241
+ const content = scan2.docText.get(f.rel);
8242
+ if (content && new RegExp(`\\b${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content)) {
8243
+ referencingFiles.add(f.rel);
8244
+ }
8245
+ }
8246
+ }
8247
+ for (const site of callSites) referencingFiles.add(site.file);
8248
+ return { defs, callSites, referencingFiles: [...referencingFiles].sort(byStr) };
8249
+ }
8250
+ var REFERENCE_KINDS4;
8251
+ var init_query = __esm({
7854
8252
  "src/query.ts"() {
7855
8253
  "use strict";
7856
8254
  init_walk();
7857
- init_callers();
7858
- init_graph();
8255
+ init_derived();
7859
8256
  init_sort();
7860
8257
  REFERENCE_KINDS4 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7861
8258
  }
7862
8259
  });
7863
8260
 
7864
8261
  // src/edit.ts
7865
- import { readFileSync as readFileSync3, writeFileSync } from "fs";
7866
- import { join as join6 } from "path";
8262
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8263
+ import { join as join9 } from "path";
7867
8264
  function resolveUniqueSymbol(scan2, namePath, file) {
7868
8265
  let matches = findSymbol(scan2, namePath);
7869
8266
  if (file) matches = matches.filter((m) => m.file === file);
@@ -7881,15 +8278,15 @@ function readLines(abs) {
7881
8278
  function replaceSymbolBody(scan2, namePath, body2, file) {
7882
8279
  const sym = resolveUniqueSymbol(scan2, namePath, file);
7883
8280
  const end = sym.endLine ?? sym.line;
7884
- const abs = join6(scan2.root, sym.file);
8281
+ const abs = join9(scan2.root, sym.file);
7885
8282
  const lines = readLines(abs);
7886
8283
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
7887
8284
  lines.splice(sym.line - 1, end - sym.line + 1, ...newLines);
7888
- writeFileSync(abs, lines.join("\n"));
8285
+ writeFileSync2(abs, lines.join("\n"));
7889
8286
  return { file: sym.file, startLine: sym.line, endLine: sym.line + newLines.length - 1, lines: newLines.length };
7890
8287
  }
7891
8288
  function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7892
- const abs = join6(scan2.root, sym.file);
8289
+ const abs = join9(scan2.root, sym.file);
7893
8290
  const lines = readLines(abs);
7894
8291
  const minGap = SEPARATED_KINDS.has(sym.kind) ? 1 : 0;
7895
8292
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
@@ -7898,7 +8295,7 @@ function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7898
8295
  block.push(...newLines);
7899
8296
  if (blankAfter && minGap && lines[index]?.trim() !== "") block.push("");
7900
8297
  lines.splice(index, 0, ...block);
7901
- writeFileSync(abs, lines.join("\n"));
8298
+ writeFileSync2(abs, lines.join("\n"));
7902
8299
  return { file: sym.file, startLine: index + 1, endLine: index + block.length, lines: block.length };
7903
8300
  }
7904
8301
  function insertAfterSymbol(scan2, namePath, body2, file) {
@@ -7920,8 +8317,8 @@ var init_edit = __esm({
7920
8317
  });
7921
8318
 
7922
8319
  // src/memory.ts
7923
- import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
7924
- import { dirname as dirname2, join as join7 } from "path";
8320
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
8321
+ import { dirname as dirname3, join as join10 } from "path";
7925
8322
  function sanitize(name2) {
7926
8323
  const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
7927
8324
  if (!clean) throw new Error("memory name is empty");
@@ -7935,12 +8332,12 @@ function sanitize(name2) {
7935
8332
  return clean;
7936
8333
  }
7937
8334
  function memoryPath(repo, name2) {
7938
- return join7(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
8335
+ return join10(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
7939
8336
  }
7940
8337
  function writeMemory(repo, name2, content) {
7941
8338
  const path = memoryPath(repo, name2);
7942
- mkdirSync(dirname2(path), { recursive: true });
7943
- writeFileSync2(path, content.endsWith("\n") ? content : content + "\n");
8339
+ mkdirSync2(dirname3(path), { recursive: true });
8340
+ writeFileSync3(path, content.endsWith("\n") ? content : content + "\n");
7944
8341
  return sanitize(name2);
7945
8342
  }
7946
8343
  function readMemory(repo, name2) {
@@ -7961,7 +8358,7 @@ function deleteMemory(repo, name2) {
7961
8358
  return true;
7962
8359
  }
7963
8360
  function listMemories(repo) {
7964
- const root = join7(repo, ...MEMORY_DIR);
8361
+ const root = join10(repo, ...MEMORY_DIR);
7965
8362
  const out2 = [];
7966
8363
  const walk2 = (dir, prefix) => {
7967
8364
  let entries;
@@ -7971,7 +8368,7 @@ function listMemories(repo) {
7971
8368
  return;
7972
8369
  }
7973
8370
  for (const e of entries) {
7974
- if (e.isDirectory()) walk2(join7(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
8371
+ if (e.isDirectory()) walk2(join10(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
7975
8372
  else if (e.name.endsWith(".md")) out2.push(prefix ? `${prefix}/${e.name.slice(0, -3)}` : e.name.slice(0, -3));
7976
8373
  }
7977
8374
  };
@@ -7988,7 +8385,7 @@ var init_memory = __esm({
7988
8385
 
7989
8386
  // src/workspaces.ts
7990
8387
  import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
7991
- import { join as join8 } from "path";
8388
+ import { join as join11 } from "path";
7992
8389
  function readJson(path, label, warnings) {
7993
8390
  const raw = readText(path);
7994
8391
  if (!raw) return void 0;
@@ -8039,7 +8436,7 @@ function wsGlobToRegExp(pat) {
8039
8436
  return new RegExp(`^${re}($|/)`);
8040
8437
  }
8041
8438
  function probeNodePkg(root, dir, kind, warnings) {
8042
- const path = join8(root, dir, "package.json");
8439
+ const path = join11(root, dir, "package.json");
8043
8440
  if (!existsSync2(path)) return void 0;
8044
8441
  const manifest = `${dir}/package.json`;
8045
8442
  const pkg = readJson(path, manifest, warnings);
@@ -8053,7 +8450,7 @@ function probeNodePkg(root, dir, kind, warnings) {
8053
8450
  return out2;
8054
8451
  }
8055
8452
  function probeCargo(root, dir) {
8056
- const path = join8(root, dir, "Cargo.toml");
8453
+ const path = join11(root, dir, "Cargo.toml");
8057
8454
  if (!existsSync2(path)) return void 0;
8058
8455
  const body2 = tomlSectionBody(readText(path), "package");
8059
8456
  const out2 = {
@@ -8067,18 +8464,18 @@ function probeCargo(root, dir) {
8067
8464
  return out2;
8068
8465
  }
8069
8466
  function probeGoMod(root, dir) {
8070
- const path = join8(root, dir, "go.mod");
8467
+ const path = join11(root, dir, "go.mod");
8071
8468
  if (!existsSync2(path)) return void 0;
8072
8469
  const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
8073
8470
  return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
8074
8471
  }
8075
8472
  function probeMaven(root, dir) {
8076
- const path = join8(root, dir, "pom.xml");
8473
+ const path = join11(root, dir, "pom.xml");
8077
8474
  if (!existsSync2(path)) return void 0;
8078
8475
  return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
8079
8476
  }
8080
8477
  function probePyproject(root, dir) {
8081
- const path = join8(root, dir, "pyproject.toml");
8478
+ const path = join11(root, dir, "pyproject.toml");
8082
8479
  if (!existsSync2(path)) return void 0;
8083
8480
  const toml = readText(path);
8084
8481
  const project = tomlSectionBody(toml, "project");
@@ -8094,7 +8491,7 @@ function probePyproject(root, dir) {
8094
8491
  return out2;
8095
8492
  }
8096
8493
  function probeComposer(root, dir, warnings) {
8097
- const path = join8(root, dir, "composer.json");
8494
+ const path = join11(root, dir, "composer.json");
8098
8495
  if (!existsSync2(path)) return void 0;
8099
8496
  const manifest = `${dir}/composer.json`;
8100
8497
  const pkg = readJson(path, manifest, warnings);
@@ -8108,7 +8505,7 @@ function probeComposer(root, dir, warnings) {
8108
8505
  return out2;
8109
8506
  }
8110
8507
  function probeNxProject(root, dir, warnings) {
8111
- const path = join8(root, dir, "project.json");
8508
+ const path = join11(root, dir, "project.json");
8112
8509
  if (!existsSync2(path)) return void 0;
8113
8510
  const manifest = `${dir}/project.json`;
8114
8511
  const proj = readJson(path, manifest, warnings);
@@ -8121,7 +8518,7 @@ function probeNxProject(root, dir, warnings) {
8121
8518
  }
8122
8519
  function probeGradle(root, dir) {
8123
8520
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8124
- if (existsSync2(join8(root, dir, f))) {
8521
+ if (existsSync2(join11(root, dir, f))) {
8125
8522
  return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
8126
8523
  }
8127
8524
  }
@@ -8156,7 +8553,7 @@ function addPackage(root, dir, found, kind, warnings) {
8156
8553
  }
8157
8554
  function isDirAt(root, rel) {
8158
8555
  try {
8159
- return statSync3(join8(root, rel)).isDirectory();
8556
+ return statSync3(join11(root, rel)).isDirectory();
8160
8557
  } catch {
8161
8558
  return false;
8162
8559
  }
@@ -8164,7 +8561,7 @@ function isDirAt(root, rel) {
8164
8561
  function subdirsOf(root, base) {
8165
8562
  let entries;
8166
8563
  try {
8167
- entries = readdirSync3(base ? join8(root, base) : root, { withFileTypes: true });
8564
+ entries = readdirSync3(base ? join11(root, base) : root, { withFileTypes: true });
8168
8565
  } catch {
8169
8566
  return [];
8170
8567
  }
@@ -8226,14 +8623,14 @@ function npmFamilyPatterns(root, warnings) {
8226
8623
  if (t.startsWith("!")) negations.push(t.slice(1));
8227
8624
  else positives.push({ pattern: t, kind });
8228
8625
  };
8229
- const pkg = readJson(join8(root, "package.json"), "package.json", warnings);
8626
+ const pkg = readJson(join11(root, "package.json"), "package.json", warnings);
8230
8627
  const ws = pkg?.workspaces;
8231
8628
  if (Array.isArray(ws)) {
8232
8629
  for (const x of ws) if (typeof x === "string") push(x, "npm");
8233
8630
  } else if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
8234
8631
  for (const x of ws.packages) if (typeof x === "string") push(x, "npm");
8235
8632
  }
8236
- const pnpm = readText(join8(root, "pnpm-workspace.yaml"));
8633
+ const pnpm = readText(join11(root, "pnpm-workspace.yaml"));
8237
8634
  let inPackages = false;
8238
8635
  for (const line of pnpm.split(/\r?\n/)) {
8239
8636
  if (/^\S/.test(line)) {
@@ -8247,11 +8644,11 @@ function npmFamilyPatterns(root, warnings) {
8247
8644
  return { positives, negations };
8248
8645
  }
8249
8646
  function fallbackNpmPatterns(root, warnings) {
8250
- const lerna = readJson(join8(root, "lerna.json"), "lerna.json", warnings);
8647
+ const lerna = readJson(join11(root, "lerna.json"), "lerna.json", warnings);
8251
8648
  if (lerna && Array.isArray(lerna.packages)) {
8252
8649
  return lerna.packages.filter((x) => typeof x === "string").map((pattern) => ({ pattern, kind: "lerna" }));
8253
8650
  }
8254
- const nx = readJson(join8(root, "nx.json"), "nx.json", warnings);
8651
+ const nx = readJson(join11(root, "nx.json"), "nx.json", warnings);
8255
8652
  if (nx) {
8256
8653
  const layout = nx.workspaceLayout ?? {};
8257
8654
  const appsDir = typeof layout.appsDir === "string" ? layout.appsDir : "apps";
@@ -8261,7 +8658,7 @@ function fallbackNpmPatterns(root, warnings) {
8261
8658
  return [];
8262
8659
  }
8263
8660
  function detectCargoMembers(root, found, warnings) {
8264
- const toml = readText(join8(root, "Cargo.toml"));
8661
+ const toml = readText(join11(root, "Cargo.toml"));
8265
8662
  if (!toml) return;
8266
8663
  const body2 = tomlSectionBody(toml, "workspace");
8267
8664
  if (!body2) return;
@@ -8276,7 +8673,7 @@ function detectCargoMembers(root, found, warnings) {
8276
8673
  }
8277
8674
  }
8278
8675
  function detectGoWork(root, found, warnings) {
8279
- const gowork = readText(join8(root, "go.work"));
8676
+ const gowork = readText(join11(root, "go.work"));
8280
8677
  if (!gowork) return;
8281
8678
  const dirs = [];
8282
8679
  for (const block of gowork.matchAll(/^use\s*\(([\s\S]*?)\)/gm)) {
@@ -8292,7 +8689,7 @@ function detectGoWork(root, found, warnings) {
8292
8689
  }
8293
8690
  }
8294
8691
  function detectMavenModules(root, found, warnings) {
8295
- const pom = readText(join8(root, "pom.xml"));
8692
+ const pom = readText(join11(root, "pom.xml"));
8296
8693
  if (!pom) return;
8297
8694
  const modules = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1];
8298
8695
  if (!modules) return;
@@ -8301,7 +8698,7 @@ function detectMavenModules(root, found, warnings) {
8301
8698
  }
8302
8699
  }
8303
8700
  function detectUvMembers(root, found, warnings) {
8304
- const toml = readText(join8(root, "pyproject.toml"));
8701
+ const toml = readText(join11(root, "pyproject.toml"));
8305
8702
  if (!toml) return;
8306
8703
  const body2 = tomlSectionBody(toml, "tool.uv.workspace");
8307
8704
  if (!body2) return;
@@ -8316,7 +8713,7 @@ function detectUvMembers(root, found, warnings) {
8316
8713
  }
8317
8714
  }
8318
8715
  function detectComposerPathRepos(root, found, warnings) {
8319
- const composer = readJson(join8(root, "composer.json"), "composer.json", warnings);
8716
+ const composer = readJson(join11(root, "composer.json"), "composer.json", warnings);
8320
8717
  const repos = composer?.repositories;
8321
8718
  if (!Array.isArray(repos)) return;
8322
8719
  for (const r of repos) {
@@ -8327,7 +8724,7 @@ function detectComposerPathRepos(root, found, warnings) {
8327
8724
  }
8328
8725
  function detectGradleIncludes(root, found, warnings) {
8329
8726
  for (const f of ["settings.gradle", "settings.gradle.kts"]) {
8330
- const text = readText(join8(root, f));
8727
+ const text = readText(join11(root, f));
8331
8728
  if (!text) continue;
8332
8729
  for (const line of text.split(/\r?\n/)) {
8333
8730
  if (!/^\s*include[\s(]/.test(line)) continue;
@@ -8339,7 +8736,7 @@ function detectGradleIncludes(root, found, warnings) {
8339
8736
  }
8340
8737
  }
8341
8738
  function npmEdges(root, pkg, byName, warnings) {
8342
- const manifest = readJson(join8(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8739
+ const manifest = readJson(join11(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8343
8740
  if (!manifest) return [];
8344
8741
  const edges = /* @__PURE__ */ new Set();
8345
8742
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -8362,7 +8759,7 @@ function normalizeDepPath(fromDir, rel) {
8362
8759
  return out2.join("/");
8363
8760
  }
8364
8761
  function cargoEdges(root, pkg, byName, byDir) {
8365
- const toml = readText(join8(root, pkg.dir, "Cargo.toml"));
8762
+ const toml = readText(join11(root, pkg.dir, "Cargo.toml"));
8366
8763
  if (!toml) return [];
8367
8764
  const edges = /* @__PURE__ */ new Set();
8368
8765
  for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
@@ -8386,7 +8783,7 @@ function cargoEdges(root, pkg, byName, byDir) {
8386
8783
  return [...edges];
8387
8784
  }
8388
8785
  function goPkgEdges(root, pkg, byName, byDir) {
8389
- const gomod = readText(join8(root, pkg.dir, "go.mod"));
8786
+ const gomod = readText(join11(root, pkg.dir, "go.mod"));
8390
8787
  if (!gomod) return [];
8391
8788
  const edges = /* @__PURE__ */ new Set();
8392
8789
  for (const m of gomod.matchAll(/^\s*(?:require\s+)?([^\s/(][^\s]*)\s+v[^\s]+/gm)) {
@@ -8400,7 +8797,7 @@ function goPkgEdges(root, pkg, byName, byDir) {
8400
8797
  return [...edges];
8401
8798
  }
8402
8799
  function mavenEdges(root, pkg, byName) {
8403
- const pom = readText(join8(root, pkg.dir, "pom.xml"));
8800
+ const pom = readText(join11(root, pkg.dir, "pom.xml"));
8404
8801
  if (!pom) return [];
8405
8802
  const edges = /* @__PURE__ */ new Set();
8406
8803
  for (const m of pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
@@ -8410,7 +8807,7 @@ function mavenEdges(root, pkg, byName) {
8410
8807
  return [...edges];
8411
8808
  }
8412
8809
  function uvEdges(root, pkg, byName) {
8413
- const toml = readText(join8(root, pkg.dir, "pyproject.toml"));
8810
+ const toml = readText(join11(root, pkg.dir, "pyproject.toml"));
8414
8811
  if (!toml) return [];
8415
8812
  const edges = /* @__PURE__ */ new Set();
8416
8813
  const project = tomlSectionBody(toml, "project");
@@ -8430,7 +8827,7 @@ function uvEdges(root, pkg, byName) {
8430
8827
  return [...edges];
8431
8828
  }
8432
8829
  function composerEdges(root, pkg, byName, warnings) {
8433
- const manifest = readJson(join8(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8830
+ const manifest = readJson(join11(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8434
8831
  if (!manifest) return [];
8435
8832
  const edges = /* @__PURE__ */ new Set();
8436
8833
  for (const field of ["require", "require-dev"]) {
@@ -8444,7 +8841,7 @@ function composerEdges(root, pkg, byName, warnings) {
8444
8841
  }
8445
8842
  function gradleEdges(root, pkg, byName, byDir) {
8446
8843
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8447
- const text = readText(join8(root, pkg.dir, f));
8844
+ const text = readText(join11(root, pkg.dir, f));
8448
8845
  if (!text) continue;
8449
8846
  const edges = /* @__PURE__ */ new Set();
8450
8847
  for (const m of text.matchAll(/project\s*\(\s*["']:?([^"']+)["']\s*\)/g)) {
@@ -9027,73 +9424,6 @@ var init_surprise = __esm({
9027
9424
  }
9028
9425
  });
9029
9426
 
9030
- // src/render/symbols-json.ts
9031
- function computeSymbolRefs(scan2) {
9032
- const unique = uniqueSymbolDefs(scan2);
9033
- const refs = /* @__PURE__ */ new Map();
9034
- if (!unique.size) return refs;
9035
- const add = (name2, file) => {
9036
- let set = refs.get(name2);
9037
- if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
9038
- set.add(file);
9039
- };
9040
- for (const f of scan2.files) {
9041
- if (f.kind === "code" && f.idents) {
9042
- for (const id of f.idents) {
9043
- const target = unique.get(id);
9044
- if (target && target !== f.rel) add(id, f.rel);
9045
- }
9046
- } else if (f.kind === "doc") {
9047
- const content = scan2.docText.get(f.rel);
9048
- if (!content) continue;
9049
- for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
9050
- const target = unique.get(tok);
9051
- if (target && target !== f.rel) add(tok, f.rel);
9052
- }
9053
- }
9054
- }
9055
- return refs;
9056
- }
9057
- function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
9058
- const defsByName = /* @__PURE__ */ new Map();
9059
- for (const f of scan2.files) {
9060
- for (const s of f.symbols) {
9061
- let arr = defsByName.get(s.name);
9062
- if (!arr) defsByName.set(s.name, arr = []);
9063
- arr.push({
9064
- file: s.file,
9065
- line: s.line,
9066
- ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
9067
- kind: s.kind,
9068
- exported: s.exported,
9069
- lang: s.lang,
9070
- ...s.parent ? { parent: s.parent } : {}
9071
- });
9072
- }
9073
- }
9074
- const defs = {};
9075
- for (const name2 of [...defsByName.keys()].sort(byStr)) {
9076
- defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
9077
- }
9078
- const refsOut = {};
9079
- for (const name2 of [...refs.keys()].sort(byStr)) {
9080
- const files = [...refs.get(name2)].sort(byStr);
9081
- if (files.length) refsOut[name2] = files;
9082
- }
9083
- return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
9084
- }
9085
- function renderSymbolsJson(index) {
9086
- return JSON.stringify(index, null, 2) + "\n";
9087
- }
9088
- var init_symbols_json = __esm({
9089
- "src/render/symbols-json.ts"() {
9090
- "use strict";
9091
- init_types();
9092
- init_sort();
9093
- init_graph();
9094
- }
9095
- });
9096
-
9097
9427
  // src/render/graph-json.ts
9098
9428
  function sortObject(obj) {
9099
9429
  const out2 = {};
@@ -9113,8 +9443,10 @@ var init_graph_json = __esm({
9113
9443
 
9114
9444
  // src/pipeline.ts
9115
9445
  function buildIndexArtifacts(repo, opts = {}) {
9116
- const scan2 = scanRepo(repo, opts);
9117
- const ctx = buildResolveContext(scan2);
9446
+ return buildArtifactsFromScan(scanRepo(repo, opts), opts);
9447
+ }
9448
+ function buildArtifactsFromScan(scan2, opts = {}) {
9449
+ const ctx = resolveContextFor(scan2);
9118
9450
  const { modules, moduleOf } = buildModules(scan2);
9119
9451
  const graph = buildGraph(scan2, ctx, modules, moduleOf, opts.meta);
9120
9452
  const communities = detectCommunities(graph.modules, graph.moduleEdges, opts.previousCommunities);
@@ -9133,14 +9465,14 @@ function buildIndexArtifacts(repo, opts = {}) {
9133
9465
  }
9134
9466
  const surprises = computeSurprises(graph);
9135
9467
  if (surprises.length) graph.surprises = surprises;
9136
- const symbols = buildSymbolIndex(scan2, computeSymbolRefs(scan2));
9468
+ const symbols = buildSymbolIndex(scan2, symbolRefsFor(scan2));
9137
9469
  return { scan: scan2, graph, symbols };
9138
9470
  }
9139
9471
  var init_pipeline = __esm({
9140
9472
  "src/pipeline.ts"() {
9141
9473
  "use strict";
9142
9474
  init_scan();
9143
- init_resolve();
9475
+ init_derived();
9144
9476
  init_modules();
9145
9477
  init_graph();
9146
9478
  init_community();
@@ -9180,241 +9512,69 @@ function rgBackend(root, pattern, opts) {
9180
9512
  const user = opts.globs ?? [];
9181
9513
  const anchor = (g) => g.startsWith("/") ? g : `/${g}`;
9182
9514
  for (const g of user.filter((g2) => !g2.startsWith("!"))) args2.push("--glob", anchor(g));
9183
- for (const g of user.filter((g2) => g2.startsWith("!"))) args2.push("--glob", `!${anchor(g.slice(1))}`);
9184
- args2.push("--regexp", pattern, "./");
9185
- const res = sh("rg", args2, { cwd: root });
9186
- if (res.missing || !res.ok && res.status !== 1) return void 0;
9187
- const hits = [];
9188
- for (const line of res.stdout.split("\n")) {
9189
- if (!line) continue;
9190
- const nul = line.indexOf("\0");
9191
- if (nul === -1) continue;
9192
- const file = line.slice(0, nul).replace(/^\.\//, "");
9193
- const rest = line.slice(nul + 1);
9194
- const colon = rest.indexOf(":");
9195
- if (colon === -1) continue;
9196
- hits.push({ file, line: Number(rest.slice(0, colon)), text: rest.slice(colon + 1) });
9197
- }
9198
- return hits;
9199
- }
9200
- function jsBackend(root, re, opts) {
9201
- const filter = compileGlobFilter(opts.globs?.map((g) => g.replace(/^(!?)\//, "$1")));
9202
- const hits = [];
9203
- for (const f of walk(root).files) {
9204
- if (filter && !filter(f.rel)) continue;
9205
- const content = readText(f.abs);
9206
- if (!content) continue;
9207
- const lines = content.split("\n");
9208
- for (let i2 = 0; i2 < lines.length; i2++) {
9209
- if (re.test(lines[i2])) hits.push({ file: f.rel, line: i2 + 1, text: lines[i2] });
9210
- }
9211
- }
9212
- return hits;
9213
- }
9214
- function grepRepo(root, pattern, opts = {}) {
9215
- const re = new RegExp(pattern, opts.ignoreCase ? "i" : "");
9216
- const max = opts.maxHits ?? DEFAULT_MAX_HITS;
9217
- let hits;
9218
- if (!opts.noRipgrep && have("rg")) hits = rgBackend(root, pattern, opts);
9219
- hits ??= jsBackend(root, re, opts);
9220
- return sortHits(hits).slice(0, max);
9221
- }
9222
- var DEFAULT_MAX_HITS;
9223
- var init_grep = __esm({
9224
- "src/grep.ts"() {
9225
- "use strict";
9226
- init_walk();
9227
- init_glob();
9228
- init_util();
9229
- init_sort();
9230
- DEFAULT_MAX_HITS = 200;
9231
- }
9232
- });
9233
-
9234
- // src/bm25.ts
9235
- function subtokens(raw) {
9236
- const folded = foldText(raw).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
9237
- const out2 = [];
9238
- const seen = /* @__PURE__ */ new Set();
9239
- const push = (t) => {
9240
- if (t.length < 2 || seen.has(t)) return;
9241
- seen.add(t);
9242
- out2.push(t);
9243
- };
9244
- if (!/\s/.test(raw.trim())) push(foldText(raw).toLowerCase().replace(/[^a-z0-9_]+/g, ""));
9245
- for (const part of folded.split(/[^A-Za-z0-9]+/)) push(part.toLowerCase());
9246
- return out2;
9247
- }
9248
- function addTerms(doc, text) {
9249
- for (const t of subtokens(text)) {
9250
- doc.tf.set(t, (doc.tf.get(t) ?? 0) + 1);
9251
- doc.len++;
9252
- }
9253
- }
9254
- function buildDocs(scan2) {
9255
- const docs = [];
9256
- for (const f of scan2.files) {
9257
- const doc = { file: f.rel, tf: /* @__PURE__ */ new Map(), len: 0, symbols: [] };
9258
- const seenSym = /* @__PURE__ */ new Set();
9259
- for (const s of f.symbols) {
9260
- addTerms(doc, s.name);
9261
- if (!seenSym.has(s.name)) {
9262
- seenSym.add(s.name);
9263
- doc.symbols.push(s.name);
9264
- }
9265
- }
9266
- for (const seg of f.rel.split("/")) addTerms(doc, seg);
9267
- for (const h of f.headings) addTerms(doc, h);
9268
- if (f.summary) addTerms(doc, f.summary);
9269
- docs.push(doc);
9270
- }
9271
- return docs;
9272
- }
9273
- function charTrigrams(term) {
9274
- const padded = `^^${term}$$`;
9275
- const grams = /* @__PURE__ */ new Set();
9276
- for (let i2 = 0; i2 + 3 <= padded.length; i2++) grams.add(padded.slice(i2, i2 + 3));
9277
- return grams;
9278
- }
9279
- function diceCoefficient(a, b) {
9280
- if (!a.size || !b.size) return 0;
9281
- let inter = 0;
9282
- for (const g of a) if (b.has(g)) inter++;
9283
- return 2 * inter / (a.size + b.size);
9284
- }
9285
- function buildTrigramIndex(docs) {
9286
- const index = /* @__PURE__ */ new Map();
9287
- for (const d of docs) {
9288
- for (const term of d.tf.keys()) {
9289
- if (!index.has(term)) index.set(term, charTrigrams(term));
9290
- }
9291
- }
9292
- return index;
9293
- }
9294
- function searchIndex(scan2, query, opts = {}) {
9295
- const terms = [];
9296
- const seen = /* @__PURE__ */ new Set();
9297
- for (const kw of keywords(query)) {
9298
- for (const t of subtokens(kw)) {
9299
- if (seen.has(t)) continue;
9300
- seen.add(t);
9301
- terms.push(t);
9302
- }
9303
- }
9304
- if (!terms.length) return [];
9305
- const docs = buildDocs(scan2);
9306
- const n = docs.length;
9307
- if (!n) return [];
9308
- let totalLen = 0;
9309
- for (const d of docs) totalLen += d.len;
9310
- const avgLen = totalLen / n || 1;
9311
- const df = /* @__PURE__ */ new Map();
9312
- for (const t of terms) {
9313
- let count = 0;
9314
- for (const d of docs) if (d.tf.has(t)) count++;
9315
- df.set(t, count);
9316
- }
9317
- const fuzzyEnabled = opts.fuzzy ?? true;
9318
- const fuzzyCandidates = /* @__PURE__ */ new Map();
9319
- if (fuzzyEnabled) {
9320
- const unmatched = terms.filter((t) => df.get(t) === 0);
9321
- if (unmatched.length) {
9322
- const trigramIndex = buildTrigramIndex(docs);
9323
- for (const t of unmatched) {
9324
- const grams = charTrigrams(t);
9325
- const candidates = [];
9326
- for (const [vocabTerm, vocabGrams] of trigramIndex) {
9327
- const dice = diceCoefficient(grams, vocabGrams);
9328
- if (dice >= FUZZY_DICE_THRESHOLD) candidates.push({ term: vocabTerm, dice });
9329
- }
9330
- candidates.sort((a, b) => b.dice - a.dice || byStr(a.term, b.term));
9331
- fuzzyCandidates.set(t, candidates.slice(0, FUZZY_CAP));
9332
- }
9333
- }
9515
+ for (const g of user.filter((g2) => g2.startsWith("!"))) args2.push("--glob", `!${anchor(g.slice(1))}`);
9516
+ args2.push("--regexp", pattern, "./");
9517
+ const res = sh("rg", args2, { cwd: root });
9518
+ if (res.missing || !res.ok && res.status !== 1) return void 0;
9519
+ const hits = [];
9520
+ for (const line of res.stdout.split("\n")) {
9521
+ if (!line) continue;
9522
+ const nul = line.indexOf("\0");
9523
+ if (nul === -1) continue;
9524
+ const file = line.slice(0, nul).replace(/^\.\//, "");
9525
+ const rest = line.slice(nul + 1);
9526
+ const colon = rest.indexOf(":");
9527
+ if (colon === -1) continue;
9528
+ hits.push({ file, line: Number(rest.slice(0, colon)), text: rest.slice(colon + 1) });
9334
9529
  }
9335
- const vocabDf = /* @__PURE__ */ new Map();
9336
- const dfOfVocabTerm = (term) => {
9337
- const known = df.get(term) ?? vocabDf.get(term);
9338
- if (known !== void 0) return known;
9339
- let count = 0;
9340
- for (const d of docs) if (d.tf.has(term)) count++;
9341
- vocabDf.set(term, count);
9342
- return count;
9343
- };
9344
- const results = [];
9345
- for (const d of docs) {
9346
- let score = 0;
9347
- const matched = [];
9348
- const symbolTerms = /* @__PURE__ */ new Set();
9349
- const fuzzyHit = /* @__PURE__ */ new Set();
9350
- for (const t of terms) {
9351
- const tf = d.tf.get(t);
9352
- if (tf) {
9353
- matched.push(t);
9354
- symbolTerms.add(t);
9355
- const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
9356
- score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
9357
- continue;
9358
- }
9359
- const candidates = fuzzyCandidates.get(t);
9360
- if (!candidates) continue;
9361
- for (const cand of candidates) {
9362
- const ctf = d.tf.get(cand.term);
9363
- if (!ctf) continue;
9364
- const cdf = dfOfVocabTerm(cand.term);
9365
- const idf = Math.log(1 + (n - cdf + 0.5) / (cdf + 0.5));
9366
- const contribution = idf * (ctf * (K1 + 1)) / (ctf + K1 * (1 - B + B * d.len / avgLen));
9367
- score += contribution * cand.dice;
9368
- symbolTerms.add(cand.term);
9369
- fuzzyHit.add(t);
9370
- }
9530
+ return hits;
9531
+ }
9532
+ function jsBackend(root, re, opts) {
9533
+ const filter = compileGlobFilter(opts.globs?.map((g) => g.replace(/^(!?)\//, "$1")));
9534
+ const hits = [];
9535
+ for (const f of walk(root).files) {
9536
+ if (filter && !filter(f.rel)) continue;
9537
+ const content = readText(f.abs);
9538
+ if (!content) continue;
9539
+ const lines = content.split("\n");
9540
+ for (let i2 = 0; i2 < lines.length; i2++) {
9541
+ if (re.test(lines[i2])) hits.push({ file: f.rel, line: i2 + 1, text: lines[i2] });
9371
9542
  }
9372
- if (!matched.length && !fuzzyHit.size) continue;
9373
- const scored = d.symbols.map((name2) => {
9374
- const toks = new Set(subtokens(name2));
9375
- let hits = 0;
9376
- for (const t of symbolTerms) if (toks.has(t)) hits++;
9377
- return { name: name2, hits };
9378
- }).filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits || byStr(a.name, b.name));
9379
- const result = {
9380
- file: d.file,
9381
- score: Number(score.toFixed(4)),
9382
- matchedTerms: matched.sort(byStr),
9383
- topSymbols: scored.slice(0, TOP_SYMBOLS).map((s) => s.name)
9384
- };
9385
- if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
9386
- results.push(result);
9387
9543
  }
9388
- results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
9389
- return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
9544
+ return hits;
9390
9545
  }
9391
- var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP;
9392
- var init_bm25 = __esm({
9393
- "src/bm25.ts"() {
9546
+ function grepRepo(root, pattern, opts = {}) {
9547
+ const re = new RegExp(pattern, opts.ignoreCase ? "i" : "");
9548
+ const max = opts.maxHits ?? DEFAULT_MAX_HITS;
9549
+ let hits;
9550
+ if (!opts.noRipgrep && have("rg")) hits = rgBackend(root, pattern, opts);
9551
+ hits ??= jsBackend(root, re, opts);
9552
+ return sortHits(hits).slice(0, max);
9553
+ }
9554
+ var DEFAULT_MAX_HITS;
9555
+ var init_grep = __esm({
9556
+ "src/grep.ts"() {
9394
9557
  "use strict";
9558
+ init_walk();
9559
+ init_glob();
9395
9560
  init_util();
9396
9561
  init_sort();
9397
- K1 = 1.2;
9398
- B = 0.75;
9399
- DEFAULT_LIMIT = 20;
9400
- TOP_SYMBOLS = 5;
9401
- FUZZY_DICE_THRESHOLD = 0.6;
9402
- FUZZY_CAP = 3;
9562
+ DEFAULT_MAX_HITS = 200;
9403
9563
  }
9404
9564
  });
9405
9565
 
9406
9566
  // src/embed/model.ts
9407
- import { createHash as createHash2 } from "crypto";
9567
+ import { createHash as createHash3 } from "crypto";
9408
9568
  import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9409
- import { join as join10 } from "path";
9569
+ import { join as join13 } from "path";
9410
9570
  function resolveEmbedModelDir(repo) {
9411
9571
  const env = process.env.CODEINDEX_EMBED_DIR;
9412
9572
  const candidates = [];
9413
9573
  if (env) candidates.push(env);
9414
- if (repo) candidates.push(join10(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9415
- candidates.push(join10(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9574
+ if (repo) candidates.push(join13(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9575
+ candidates.push(join13(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9416
9576
  for (const c2 of candidates) {
9417
- if (existsSync3(join10(c2, "model.json"))) return c2;
9577
+ if (existsSync3(join13(c2, "model.json"))) return c2;
9418
9578
  }
9419
9579
  return void 0;
9420
9580
  }
@@ -9447,7 +9607,7 @@ function parseEmbedModel(raw, source) {
9447
9607
  }
9448
9608
  function loadEmbedModel(dir) {
9449
9609
  if (!dir) return void 0;
9450
- const path = join10(dir, "model.json");
9610
+ const path = join13(dir, "model.json");
9451
9611
  if (!existsSync3(path)) return void 0;
9452
9612
  const raw = JSON.parse(readFileSync5(path, "utf8"));
9453
9613
  return parseEmbedModel(raw, path);
@@ -9462,7 +9622,7 @@ async function fetchEmbedModel(url, expectedSha256) {
9462
9622
  if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9463
9623
  const body2 = await res.text();
9464
9624
  if (expectedSha256) {
9465
- const got = createHash2("sha256").update(body2).digest("hex");
9625
+ const got = createHash3("sha256").update(body2).digest("hex");
9466
9626
  if (got !== expectedSha256) {
9467
9627
  throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
9468
9628
  }
@@ -10066,8 +10226,8 @@ var init_repomap = __esm({
10066
10226
 
10067
10227
  // src/deadcode.ts
10068
10228
  function findDeadCode(scan2) {
10069
- const callers = buildCallerIndex(scan2);
10070
- const refs = computeSymbolRefs(scan2);
10229
+ const callers = callerIndexFor(scan2);
10230
+ const refs = symbolRefsFor(scan2);
10071
10231
  const out2 = [];
10072
10232
  const consider = (s) => s.exported && !REFERENCE_KINDS6.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
10073
10233
  for (const f of scan2.files) {
@@ -10086,8 +10246,7 @@ var REFERENCE_KINDS6, ENTRYPOINT_RE;
10086
10246
  var init_deadcode = __esm({
10087
10247
  "src/deadcode.ts"() {
10088
10248
  "use strict";
10089
- init_callers();
10090
- init_symbols_json();
10249
+ init_derived();
10091
10250
  init_tests_map();
10092
10251
  init_sort();
10093
10252
  REFERENCE_KINDS6 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
@@ -10095,49 +10254,6 @@ var init_deadcode = __esm({
10095
10254
  }
10096
10255
  });
10097
10256
 
10098
- // src/complexity.ts
10099
- import { join as join11 } from "path";
10100
- function complexityOfSource(source) {
10101
- return 1 + (source.match(BRANCH_RE) ?? []).length;
10102
- }
10103
- function symbolComplexity(scan2, rel, top = 50) {
10104
- const out2 = [];
10105
- for (const f of scan2.files) {
10106
- if (f.kind !== "code") continue;
10107
- if (rel && f.rel !== rel) continue;
10108
- if (!f.symbols.length) continue;
10109
- const lines = readText(join11(scan2.root, f.rel)).split("\n");
10110
- for (const s of f.symbols) {
10111
- if (s.kind === "reexport" || s.kind === "reexport-all") continue;
10112
- const end = s.endLine ?? s.line;
10113
- const body2 = lines.slice(s.line - 1, end).join("\n");
10114
- const entry = { file: f.rel, name: s.name, line: s.line, complexity: complexityOfSource(body2) };
10115
- if (s.endLine !== void 0) entry.endLine = s.endLine;
10116
- out2.push(entry);
10117
- }
10118
- }
10119
- out2.sort((a, b) => b.complexity - a.complexity || byStr(a.file, b.file) || a.line - b.line);
10120
- return out2.slice(0, top);
10121
- }
10122
- function riskHotspots(scan2, churn, top = 20) {
10123
- const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
10124
- const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
10125
- const commits = churn.get(f.rel) ?? 0;
10126
- return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
10127
- });
10128
- out2.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
10129
- return out2.slice(0, top);
10130
- }
10131
- var BRANCH_RE;
10132
- var init_complexity = __esm({
10133
- "src/complexity.ts"() {
10134
- "use strict";
10135
- init_walk();
10136
- init_sort();
10137
- BRANCH_RE = /\b(if|elif|elsif|else\s+if|for|foreach|while|until|unless|case|when|match|catch|rescue|except)\b|&&|\|\||(?<![?:])\?(?![?.:])/g;
10138
- }
10139
- });
10140
-
10141
10257
  // src/viz.ts
10142
10258
  function renderMermaid(graph, opts = {}) {
10143
10259
  const maxEdges = opts.maxEdges ?? 80;
@@ -10178,13 +10294,17 @@ var init_viz = __esm({
10178
10294
  // src/mcp.ts
10179
10295
  var mcp_exports = {};
10180
10296
  __export(mcp_exports, {
10297
+ getArtifacts: () => getArtifacts,
10298
+ getScan: () => getScan,
10181
10299
  memoizedEmbedModel: () => memoizedEmbedModel,
10182
10300
  memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
10183
10301
  runMcpServer: () => runMcpServer,
10184
- scanFingerprint: () => scanFingerprint
10302
+ scanFingerprint: () => scanFingerprint,
10303
+ toCacheMap: () => toCacheMap,
10304
+ warmGrammarsForRepo: () => warmGrammarsForRepo
10185
10305
  });
10186
10306
  import { statSync as statSync4 } from "fs";
10187
- import { join as join12 } from "path";
10307
+ import { join as join14 } from "path";
10188
10308
  import { createInterface } from "readline";
10189
10309
  function str(v) {
10190
10310
  return typeof v === "string" && v ? v : void 0;
@@ -10208,7 +10328,7 @@ async function memoizedEmbeddingIndex(key, build) {
10208
10328
  function memoizedEmbedModel(modelDir) {
10209
10329
  let stat;
10210
10330
  try {
10211
- stat = statSync4(join12(modelDir, "model.json"));
10331
+ stat = statSync4(join14(modelDir, "model.json"));
10212
10332
  } catch {
10213
10333
  return void 0;
10214
10334
  }
@@ -10218,12 +10338,59 @@ function memoizedEmbedModel(modelDir) {
10218
10338
  if (model) embedModelCache = { key, model };
10219
10339
  return model;
10220
10340
  }
10341
+ function sessionKey(repo, opts) {
10342
+ return repo + "\0" + JSON.stringify({
10343
+ scope: opts.scope,
10344
+ include: opts.include,
10345
+ exclude: opts.exclude,
10346
+ gitignore: opts.gitignore,
10347
+ ignoreDirs: opts.ignoreDirs,
10348
+ maxBytes: opts.maxBytes,
10349
+ maxFiles: opts.maxFiles,
10350
+ maxCallsPerFile: opts.maxCallsPerFile,
10351
+ out: opts.out,
10352
+ fullHash: opts.fullHash
10353
+ });
10354
+ }
10355
+ function toCacheMap(scan2) {
10356
+ const m = /* @__PURE__ */ new Map();
10357
+ for (const f of scan2.files) m.set(f.rel, { hash: f.hash, record: f, size: f.size, mtimeMs: scan2.mtimes.get(f.rel) });
10358
+ return m;
10359
+ }
10360
+ function getScan(repo, opts = {}) {
10361
+ const key = sessionKey(repo, opts);
10362
+ if (sessionCache && sessionCache.key === key) {
10363
+ const fresh = scanRepo(repo, { ...opts, cache: sessionCache.cacheMap });
10364
+ if (fresh.contentUnchanged) {
10365
+ if (fresh.cacheDirty) sessionCache.cacheMap = toCacheMap(fresh);
10366
+ if (sessionCache.scan.commit !== fresh.commit) sessionCache.scan.commit = fresh.commit;
10367
+ return sessionCache.scan;
10368
+ }
10369
+ sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
10370
+ return fresh;
10371
+ }
10372
+ const scan2 = scanRepo(repo, opts);
10373
+ sessionCache = { key, scan: scan2, cacheMap: toCacheMap(scan2) };
10374
+ return scan2;
10375
+ }
10376
+ function getArtifacts(repo, opts = {}) {
10377
+ const scan2 = getScan(repo, opts);
10378
+ if (sessionCache && sessionCache.scan === scan2) {
10379
+ return sessionCache.arts ??= buildArtifactsFromScan(scan2, opts);
10380
+ }
10381
+ return buildArtifactsFromScan(scan2, opts);
10382
+ }
10383
+ async function warmGrammarsForRepo(repo) {
10384
+ const { files } = walk(repo, {});
10385
+ await ensureGrammars(grammarKeysForExts(files.map((f) => f.ext)));
10386
+ }
10221
10387
  async function callTool(name2, args2) {
10222
10388
  const repo = str(args2.repo);
10223
10389
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10224
10390
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
10391
+ if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
10225
10392
  if (name2 === "scan_summary") {
10226
- const scan2 = scanRepo(repo, scanOpts);
10393
+ const scan2 = getScan(repo, scanOpts);
10227
10394
  return JSON.stringify(
10228
10395
  { engineVersion: ENGINE_VERSION, commit: scan2.commit, fileCount: scan2.files.length, languages: scan2.languages, capped: scan2.capped },
10229
10396
  null,
@@ -10231,10 +10398,10 @@ async function callTool(name2, args2) {
10231
10398
  );
10232
10399
  }
10233
10400
  if (name2 === "graph") {
10234
- return renderGraphJson(buildIndexArtifacts(repo, scanOpts).graph);
10401
+ return renderGraphJson(getArtifacts(repo, scanOpts).graph);
10235
10402
  }
10236
10403
  if (name2 === "symbols") {
10237
- const { symbols } = buildIndexArtifacts(repo, scanOpts);
10404
+ const { symbols } = getArtifacts(repo, scanOpts);
10238
10405
  const lookup = str(args2.name);
10239
10406
  if (lookup) {
10240
10407
  return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
@@ -10242,7 +10409,7 @@ async function callTool(name2, args2) {
10242
10409
  return JSON.stringify(symbols, null, 2);
10243
10410
  }
10244
10411
  if (name2 === "callers") {
10245
- const index = buildCallerIndex(scanRepo(repo, scanOpts));
10412
+ const index = buildCallerIndex(getScan(repo, scanOpts));
10246
10413
  const lookup = str(args2.name);
10247
10414
  if (lookup) {
10248
10415
  const entry = index.get(lookup);
@@ -10265,12 +10432,12 @@ async function callTool(name2, args2) {
10265
10432
  if (name2 === "symbols_overview") {
10266
10433
  const file = str(args2.file);
10267
10434
  if (!file) throw new Error("`file` is required");
10268
- return JSON.stringify(symbolsOverview(scanRepo(repo, scanOpts), file), null, 2);
10435
+ return JSON.stringify(symbolsOverview(getScan(repo, scanOpts), file), null, 2);
10269
10436
  }
10270
10437
  if (name2 === "find_symbol") {
10271
10438
  const namePath = str(args2.namePath);
10272
10439
  if (!namePath) throw new Error("`namePath` is required");
10273
- const matches = findSymbol(scanRepo(repo, scanOpts), namePath, {
10440
+ const matches = findSymbol(getScan(repo, scanOpts), namePath, {
10274
10441
  substring: args2.substring === true,
10275
10442
  includeBody: args2.includeBody === true
10276
10443
  });
@@ -10279,15 +10446,17 @@ async function callTool(name2, args2) {
10279
10446
  if (name2 === "find_references") {
10280
10447
  const symName = str(args2.name);
10281
10448
  if (!symName) throw new Error("`name` is required");
10282
- return JSON.stringify(findReferences(scanRepo(repo, scanOpts), symName), null, 2);
10449
+ return JSON.stringify(findReferences(getScan(repo, scanOpts), symName), null, 2);
10283
10450
  }
10284
10451
  if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
10285
10452
  const namePath = str(args2.namePath);
10286
10453
  const body2 = typeof args2.body === "string" ? args2.body : void 0;
10287
10454
  if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
10288
- const scan2 = scanRepo(repo, scanOpts);
10455
+ const scan2 = getScan(repo, scanOpts);
10289
10456
  const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
10290
- return JSON.stringify(fn(scan2, namePath, body2, str(args2.file)), null, 2);
10457
+ const result = fn(scan2, namePath, body2, str(args2.file));
10458
+ sessionCache = void 0;
10459
+ return JSON.stringify(result, null, 2);
10291
10460
  }
10292
10461
  if (name2 === "write_memory") {
10293
10462
  const memName = str(args2.name);
@@ -10311,10 +10480,10 @@ async function callTool(name2, args2) {
10311
10480
  return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
10312
10481
  }
10313
10482
  if (name2 === "dead_code") {
10314
- return JSON.stringify(findDeadCode(scanRepo(repo, scanOpts)), null, 2);
10483
+ return JSON.stringify(findDeadCode(getScan(repo, scanOpts)), null, 2);
10315
10484
  }
10316
10485
  if (name2 === "complexity") {
10317
- const scan2 = scanRepo(repo, scanOpts);
10486
+ const scan2 = getScan(repo, scanOpts);
10318
10487
  if (args2.risk === true) {
10319
10488
  const { churn, ok } = gitChurn(repo);
10320
10489
  return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2);
@@ -10322,15 +10491,15 @@ async function callTool(name2, args2) {
10322
10491
  return JSON.stringify(symbolComplexity(scan2, str(args2.file)), null, 2);
10323
10492
  }
10324
10493
  if (name2 === "mermaid") {
10325
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10494
+ const { graph } = getArtifacts(repo, scanOpts);
10326
10495
  return renderMermaid(graph, { module: str(args2.module) });
10327
10496
  }
10328
10497
  if (name2 === "repo_map") {
10329
- const { scan: scan2, graph } = buildIndexArtifacts(repo, scanOpts);
10498
+ const { scan: scan2, graph } = getArtifacts(repo, scanOpts);
10330
10499
  return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
10331
10500
  }
10332
10501
  if (name2 === "hotspots") {
10333
- const scan2 = scanRepo(repo, scanOpts);
10502
+ const scan2 = getScan(repo, scanOpts);
10334
10503
  const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10335
10504
  return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
10336
10505
  }
@@ -10351,7 +10520,7 @@ async function callTool(name2, args2) {
10351
10520
  if (name2 === "search") {
10352
10521
  const query = str(args2.query);
10353
10522
  if (!query) throw new Error("`query` is required");
10354
- const scan2 = scanRepo(repo, scanOpts);
10523
+ const scan2 = getScan(repo, scanOpts);
10355
10524
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10356
10525
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10357
10526
  if (args2.semantic === true) {
@@ -10406,13 +10575,16 @@ async function callTool(name2, args2) {
10406
10575
  }
10407
10576
  if (name2 === "check_rules") {
10408
10577
  const rules = parseRules(args2.rules);
10409
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10578
+ const { graph } = getArtifacts(repo, scanOpts);
10410
10579
  return JSON.stringify(checkRules(graph, rules), null, 2);
10411
10580
  }
10412
10581
  throw new Error(`unknown tool: ${name2}`);
10413
10582
  }
10414
- async function runMcpServer() {
10415
- await ensureGrammars(allGrammarKeys());
10583
+ async function runMcpServer(opts = {}) {
10584
+ const serverInfo = {
10585
+ name: opts.serverInfo?.name ?? "codeindex",
10586
+ version: opts.serverInfo?.version ?? ENGINE_VERSION
10587
+ };
10416
10588
  const send = (msg) => {
10417
10589
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
10418
10590
  };
@@ -10439,7 +10611,7 @@ async function runMcpServer() {
10439
10611
  result: {
10440
10612
  protocolVersion: "2024-11-05",
10441
10613
  capabilities: { tools: {} },
10442
- serverInfo: { name: "codeindex", version: ENGINE_VERSION }
10614
+ serverInfo
10443
10615
  }
10444
10616
  });
10445
10617
  } else if (req.method === "ping") {
@@ -10467,7 +10639,7 @@ async function runMcpServer() {
10467
10639
  }
10468
10640
  }
10469
10641
  }
10470
- var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache;
10642
+ var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, sessionCache, SCANLESS_TOOLS;
10471
10643
  var init_mcp = __esm({
10472
10644
  "src/mcp.ts"() {
10473
10645
  "use strict";
@@ -10476,6 +10648,7 @@ var init_mcp = __esm({
10476
10648
  init_pipeline();
10477
10649
  init_graph_json();
10478
10650
  init_scan();
10651
+ init_walk();
10479
10652
  init_callers();
10480
10653
  init_workspaces();
10481
10654
  init_git();
@@ -10746,6 +10919,17 @@ var init_mcp = __esm({
10746
10919
  }
10747
10920
  }
10748
10921
  ];
10922
+ SCANLESS_TOOLS = /* @__PURE__ */ new Set([
10923
+ "workspaces",
10924
+ "churn",
10925
+ "coupling",
10926
+ "grep",
10927
+ "write_memory",
10928
+ "read_memory",
10929
+ "list_memories",
10930
+ "delete_memory",
10931
+ "embed_status"
10932
+ ]);
10749
10933
  }
10750
10934
  });
10751
10935
 
@@ -10885,6 +11069,106 @@ init_code();
10885
11069
  init_markdown();
10886
11070
  init_loader();
10887
11071
  init_extract();
11072
+ init_loader();
11073
+
11074
+ // src/ast/grammars-pull.ts
11075
+ init_types();
11076
+ import { createHash as createHash2 } from "crypto";
11077
+ import { mkdirSync, writeFileSync } from "fs";
11078
+ import { dirname as dirname2, resolve, sep as sep2 } from "path";
11079
+ import { gunzipSync } from "zlib";
11080
+ var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
11081
+ function resolveGrammarsPullTarget() {
11082
+ const env = process.env.CODEINDEX_GRAMMARS_URL;
11083
+ if (env && env.trim()) return { url: env.trim() };
11084
+ return { url: DEFAULT_GRAMMARS_URL, sha256Url: `${DEFAULT_GRAMMARS_URL}.sha256` };
11085
+ }
11086
+ async function fetchGrammarsTarball(url, expectedSha256) {
11087
+ const res = await fetch(url);
11088
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
11089
+ const buf = Buffer.from(await res.arrayBuffer());
11090
+ if (expectedSha256) {
11091
+ const got = createHash2("sha256").update(buf).digest("hex");
11092
+ if (got !== expectedSha256) {
11093
+ throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
11094
+ }
11095
+ }
11096
+ return buf;
11097
+ }
11098
+ function asBuffer(u) {
11099
+ return Buffer.isBuffer(u) ? u : Buffer.from(u.buffer, u.byteOffset, u.byteLength);
11100
+ }
11101
+ async function fetchExpectedSha256(url) {
11102
+ const res = await fetch(url);
11103
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
11104
+ const text = await res.text();
11105
+ const hex = (text.trim().split(/\s+/)[0] ?? "").toLowerCase();
11106
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`invalid sha256 sidecar at ${url}`);
11107
+ return hex;
11108
+ }
11109
+ function cstr(block, start2, len) {
11110
+ const slice = block.subarray(start2, start2 + len);
11111
+ const nul = slice.indexOf(0);
11112
+ return slice.toString("utf8", 0, nul === -1 ? slice.length : nul);
11113
+ }
11114
+ function* readTar(buf) {
11115
+ let off = 0;
11116
+ while (off + 512 <= buf.length) {
11117
+ const block = buf.subarray(off, off + 512);
11118
+ let allZero = true;
11119
+ for (let i2 = 0; i2 < 512; i2++) {
11120
+ if (block[i2] !== 0) {
11121
+ allZero = false;
11122
+ break;
11123
+ }
11124
+ }
11125
+ if (allZero) break;
11126
+ const name2 = cstr(block, 0, 100);
11127
+ const prefix = cstr(block, 345, 155);
11128
+ const sizeStr = cstr(block, 124, 12).trim();
11129
+ const size = sizeStr ? parseInt(sizeStr, 8) : 0;
11130
+ const type = String.fromCharCode(block[156] ?? 0);
11131
+ off += 512;
11132
+ const data = buf.subarray(off, off + size);
11133
+ off += Math.ceil(size / 512) * 512;
11134
+ yield { name: prefix ? `${prefix}/${name2}` : name2, type, data };
11135
+ }
11136
+ }
11137
+ function safeRelPath(name2) {
11138
+ if (!name2 || name2.includes("\0")) return null;
11139
+ if (name2.startsWith("/") || name2.startsWith("\\") || /^[A-Za-z]:/.test(name2)) return null;
11140
+ const out2 = [];
11141
+ for (const part of name2.split(/[/\\]/)) {
11142
+ if (part === "" || part === ".") continue;
11143
+ if (part === "..") return null;
11144
+ out2.push(part);
11145
+ }
11146
+ return out2.length ? out2.join("/") : null;
11147
+ }
11148
+ function extractTarInto(rawTar, destDir) {
11149
+ const root = resolve(destDir);
11150
+ const written = [];
11151
+ for (const entry of readTar(asBuffer(rawTar))) {
11152
+ if (entry.type !== "0" && entry.type !== "\0") continue;
11153
+ const rel = safeRelPath(entry.name);
11154
+ if (rel === null) throw new Error(`refusing unsafe tar entry: ${entry.name}`);
11155
+ const dest = resolve(destDir, rel);
11156
+ if (dest !== root && !dest.startsWith(root + sep2)) {
11157
+ throw new Error(`tar entry escapes destination: ${entry.name}`);
11158
+ }
11159
+ mkdirSync(dirname2(dest), { recursive: true });
11160
+ writeFileSync(dest, entry.data);
11161
+ written.push(rel);
11162
+ }
11163
+ return written;
11164
+ }
11165
+ function extractGrammarsTarball(bytes, destDir) {
11166
+ const b = asBuffer(bytes);
11167
+ const raw = b.length >= 2 && b[0] === 31 && b[1] === 139 ? gunzipSync(b) : b;
11168
+ return extractTarInto(raw, destDir);
11169
+ }
11170
+
11171
+ // src/engine.ts
10888
11172
  init_resolve();
10889
11173
  init_modules();
10890
11174
  init_graph();
@@ -10905,7 +11189,7 @@ init_graph_json();
10905
11189
  init_types();
10906
11190
  init_walk();
10907
11191
  init_sort();
10908
- import { join as join9 } from "path";
11192
+ import { join as join12 } from "path";
10909
11193
  var utf8 = new TextEncoder();
10910
11194
  function pushVarint(out2, n) {
10911
11195
  if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
@@ -11075,7 +11359,7 @@ function renderScip(scan2, opts = {}) {
11075
11359
  };
11076
11360
  const documents = [];
11077
11361
  for (const f of docs) {
11078
- const text = readText(join9(scan2.root, f.rel));
11362
+ const text = readText(join12(scan2.root, f.rel));
11079
11363
  const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
11080
11364
  const locate = (lineNo, name2) => {
11081
11365
  const line = lines[lineNo - 1];
@@ -11166,12 +11450,14 @@ init_util();
11166
11450
  init_types();
11167
11451
  init_types();
11168
11452
  init_loader();
11453
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11454
+ import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
11169
11455
  init_pipeline();
11456
+ init_hash();
11170
11457
  init_graph_json();
11171
11458
  init_symbols_json();
11172
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
11173
- import { join as join13, resolve } from "path";
11174
11459
  init_scan();
11460
+ init_walk();
11175
11461
  init_callers();
11176
11462
  init_workspaces();
11177
11463
  init_git();
@@ -11218,6 +11504,14 @@ Commands:
11218
11504
  the source with CODEINDEX_EMBED_URL
11219
11505
  embed serve Print (or --run) the docker command that starts the
11220
11506
  containerized embedding server (rich tier)
11507
+ grammars Tree-sitter wasm grammars (optional AST tier; regex without them).
11508
+ Precedence: bundle-adjacent > CODEINDEX_GRAMMARS_DIR > shared cache:
11509
+ grammars status Active tier (adjacent/env/cache/none), resolved
11510
+ dir, pinned ENGINE_VERSION, pull-needed (JSON)
11511
+ grammars pull Fetch the per-release grammars-<version>.tar.gz
11512
+ asset into the shared cache (sha256-verified,
11513
+ atomic). Override the source with
11514
+ CODEINDEX_GRAMMARS_URL
11221
11515
  rules Architecture rules (forbidden edges, cycles, orphans) validated
11222
11516
  against the link-graph: --config <codeindex.rules.json>; exits 1
11223
11517
  on any error-severity violation (a CI gate)
@@ -11271,10 +11565,10 @@ function parseFlags(args2) {
11271
11565
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
11272
11566
  return n;
11273
11567
  };
11274
- if (a === "--repo") flags2.repo = resolve(next());
11568
+ if (a === "--repo") flags2.repo = resolve2(next());
11275
11569
  else if (a === "--out") {
11276
11570
  const v = next();
11277
- flags2.out = v === "-" ? "-" : resolve(v);
11571
+ flags2.out = v === "-" ? "-" : resolve2(v);
11278
11572
  } else if (a === "--project-root") flags2.projectRoot = next();
11279
11573
  else if (a === "--include") flags2.include.push(next());
11280
11574
  else if (a === "--exclude") flags2.exclude.push(next());
@@ -11289,7 +11583,7 @@ function parseFlags(args2) {
11289
11583
  else if (a === "--budget-tokens") flags2.budgetTokens = num();
11290
11584
  else if (a === "--no-ast") flags2.noAst = true;
11291
11585
  else if (a === "--since") flags2.since = next();
11292
- else if (a === "--config") flags2.config = resolve(next());
11586
+ else if (a === "--config") flags2.config = resolve2(next());
11293
11587
  else if (a === "--limit") flags2.limit = num();
11294
11588
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11295
11589
  else if (a === "--semantic") flags2.semantic = true;
@@ -11301,10 +11595,10 @@ function parseFlags(args2) {
11301
11595
  return flags2;
11302
11596
  }
11303
11597
  function emit(content, out2) {
11304
- if (out2) writeFileSync3(out2, content);
11598
+ if (out2) writeFileSync4(out2, content);
11305
11599
  else process.stdout.write(content);
11306
11600
  }
11307
- function scanOptions(flags2) {
11601
+ function scanOptions(flags2, precomputedWalk) {
11308
11602
  return {
11309
11603
  include: flags2.include.length ? flags2.include : void 0,
11310
11604
  exclude: flags2.exclude.length ? flags2.exclude : void 0,
@@ -11313,9 +11607,14 @@ function scanOptions(flags2) {
11313
11607
  ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0,
11314
11608
  maxFiles: flags2.maxFiles,
11315
11609
  maxBytes: flags2.maxBytes,
11316
- maxCallsPerFile: flags2.maxCalls
11610
+ maxCallsPerFile: flags2.maxCalls,
11611
+ // The walk performed once in runCli to warm the present-language grammars,
11612
+ // reused here so scanRepo does not traverse the tree a second time. Absent
11613
+ // for --no-ast / scan-less commands: scanRepo walks itself, unchanged.
11614
+ precomputedWalk
11317
11615
  };
11318
11616
  }
11617
+ var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
11319
11618
  async function runCli(argv) {
11320
11619
  const [cmd, ...rest] = argv;
11321
11620
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
@@ -11333,46 +11632,102 @@ async function runCli(argv) {
11333
11632
  }
11334
11633
  const flags2 = parseFlags(rest);
11335
11634
  if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11336
- if (!flags2.noAst) await ensureGrammars(allGrammarKeys());
11635
+ const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
11636
+ let precomputedWalk;
11637
+ if (scans && !flags2.noAst) {
11638
+ precomputedWalk = walk(flags2.repo, {
11639
+ maxFileBytes: flags2.maxBytes,
11640
+ maxFiles: flags2.maxFiles,
11641
+ gitignore: flags2.gitignore,
11642
+ ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0
11643
+ });
11644
+ await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
11645
+ }
11337
11646
  if (cmd === "index") {
11338
11647
  if (!flags2.out) throw new Error("index needs --out <dir>");
11339
11648
  const outDir = flags2.out;
11340
- mkdirSync2(outDir, { recursive: true });
11341
- const cachePath = join13(outDir, "cache.json");
11649
+ mkdirSync3(outDir, { recursive: true });
11650
+ const cachePath = join15(outDir, "cache.json");
11342
11651
  let cache;
11652
+ let meta = {};
11343
11653
  try {
11344
11654
  const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
11345
11655
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
11346
11656
  cache = new Map(Object.entries(parsed.files));
11657
+ meta = {
11658
+ engineVersion: parsed.engineVersion,
11659
+ commit: parsed.commit,
11660
+ graphSha1: parsed.graphSha1,
11661
+ symbolsSha1: parsed.symbolsSha1,
11662
+ embed: parsed.embed
11663
+ };
11347
11664
  }
11348
11665
  } catch {
11349
11666
  }
11350
- const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
11351
- writeFileSync3(join13(outDir, "graph.json"), renderGraphJson(graph));
11352
- writeFileSync3(join13(outDir, "symbols.json"), renderSymbolsJson(symbols));
11353
- const files = {};
11354
- for (const f of scan2.files) {
11355
- const entry = { hash: f.hash, record: f, size: f.size };
11356
- const mtime = scan2.mtimes.get(f.rel);
11357
- if (mtime !== void 0) entry.mtimeMs = mtime;
11358
- files[f.rel] = entry;
11359
- }
11360
- writeFileSync3(
11361
- cachePath,
11362
- JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
11363
- );
11364
- let embedNote = "";
11667
+ const scan2 = scanRepo(flags2.repo, { ...scanOptions(flags2, precomputedWalk), cache, out: outDir });
11365
11668
  const modelDir = resolveEmbedModelDir(flags2.repo);
11366
11669
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11367
- if (model) {
11368
- const index = buildEmbeddingIndex(scan2, model);
11369
- writeFileSync3(join13(outDir, "embeddings.bin"), serializeEmbeddings(index));
11370
- embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
11371
- }
11372
- process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
11670
+ const graphPath = join15(outDir, "graph.json");
11671
+ const symbolsPath = join15(outDir, "symbols.json");
11672
+ const embedPath = join15(outDir, "embeddings.bin");
11673
+ const artifactSha = (path) => {
11674
+ try {
11675
+ return sha1(readFileSync6(path));
11676
+ } catch {
11677
+ return void 0;
11678
+ }
11679
+ };
11680
+ const writeCache = (out2) => {
11681
+ const files = {};
11682
+ for (const f of scan2.files) {
11683
+ const entry = { hash: f.hash, record: f, size: f.size };
11684
+ const mtime = scan2.mtimes.get(f.rel);
11685
+ if (mtime !== void 0) entry.mtimeMs = mtime;
11686
+ files[f.rel] = entry;
11687
+ }
11688
+ writeFileSync4(
11689
+ cachePath,
11690
+ JSON.stringify({
11691
+ schemaVersion: SCHEMA_VERSION,
11692
+ extractorVersion: EXTRACTOR_VERSION,
11693
+ engineVersion: ENGINE_VERSION,
11694
+ commit: scan2.commit,
11695
+ graphSha1: out2.graphSha1,
11696
+ symbolsSha1: out2.symbolsSha1,
11697
+ embed: out2.embed,
11698
+ files
11699
+ }) + "\n"
11700
+ );
11701
+ };
11702
+ const embedUnchanged = !model || meta.embed !== void 0 && meta.embed.embedVersion === EMBED_VERSION && meta.embed.modelId === model.modelId && meta.embed.sha1 !== void 0 && artifactSha(embedPath) === meta.embed.sha1;
11703
+ const fastpath = scan2.contentUnchanged && meta.engineVersion === ENGINE_VERSION && meta.commit === scan2.commit && meta.graphSha1 !== void 0 && artifactSha(graphPath) === meta.graphSha1 && meta.symbolsSha1 !== void 0 && artifactSha(symbolsPath) === meta.symbolsSha1 && embedUnchanged;
11704
+ if (fastpath) {
11705
+ if (scan2.cacheDirty) writeCache(meta);
11706
+ process.stderr.write(
11707
+ `codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${scan2.capped ? " (capped)" : ""} (unchanged \u2014 artifacts reused)
11708
+ `
11709
+ );
11710
+ } else {
11711
+ const { graph, symbols } = buildArtifactsFromScan(scan2);
11712
+ const graphJson = renderGraphJson(graph);
11713
+ const symbolsJson = renderSymbolsJson(symbols);
11714
+ writeFileSync4(graphPath, graphJson);
11715
+ writeFileSync4(symbolsPath, symbolsJson);
11716
+ let embedNote = "";
11717
+ let embedMeta;
11718
+ if (model) {
11719
+ const index = buildEmbeddingIndex(scan2, model);
11720
+ const bytes = serializeEmbeddings(index);
11721
+ writeFileSync4(embedPath, bytes);
11722
+ embedMeta = { embedVersion: EMBED_VERSION, modelId: model.modelId, sha1: sha1(bytes) };
11723
+ embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
11724
+ }
11725
+ writeCache({ graphSha1: sha1(graphJson), symbolsSha1: sha1(symbolsJson), embed: embedMeta });
11726
+ process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
11373
11727
  `);
11728
+ }
11374
11729
  } else if (cmd === "scan") {
11375
- const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11730
+ const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11376
11731
  const summary = {
11377
11732
  engineVersion: ENGINE_VERSION,
11378
11733
  commit: scan2.commit,
@@ -11382,30 +11737,30 @@ async function runCli(argv) {
11382
11737
  };
11383
11738
  emit(JSON.stringify(summary, null, 2) + "\n", flags2.out);
11384
11739
  } else if (cmd === "graph") {
11385
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11740
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11386
11741
  emit(renderGraphJson(graph), flags2.out);
11387
11742
  } else if (cmd === "symbols") {
11388
- const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11743
+ const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11389
11744
  emit(renderSymbolsJson(symbols), flags2.out);
11390
11745
  } else if (cmd === "scip") {
11391
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11746
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11392
11747
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
11393
- const out2 = flags2.out ?? resolve("index.scip");
11748
+ const out2 = flags2.out ?? resolve2("index.scip");
11394
11749
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
11395
11750
  else {
11396
- writeFileSync3(out2, bytes);
11751
+ writeFileSync4(out2, bytes);
11397
11752
  process.stderr.write(`codeindex: SCIP index \u2192 ${out2} (${bytes.length} bytes)
11398
11753
  `);
11399
11754
  }
11400
11755
  } else if (cmd === "callers") {
11401
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11756
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11402
11757
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
11403
11758
  const obj = {};
11404
11759
  for (const [name2, entry] of index) obj[name2] = entry;
11405
11760
  emit(JSON.stringify(obj, null, 2) + "\n", flags2.out);
11406
11761
  } else if (cmd === "search") {
11407
11762
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
11408
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11763
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11409
11764
  if (flags2.semantic) {
11410
11765
  const endpoint = resolveEmbedEndpoint();
11411
11766
  const lexical = () => {
@@ -11495,17 +11850,17 @@ async function runCli(argv) {
11495
11850
  return;
11496
11851
  }
11497
11852
  const model = loadEmbedModel(modelDir);
11498
- mkdirSync2(flags2.out, { recursive: true });
11499
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11853
+ mkdirSync3(flags2.out, { recursive: true });
11854
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11500
11855
  const index = buildEmbeddingIndex(scan2, model);
11501
- writeFileSync3(join13(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11856
+ writeFileSync4(join15(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11502
11857
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
11503
11858
  `);
11504
11859
  } else if (sub === "pull") {
11505
11860
  const { url, sha256 } = resolveEmbedPullUrl();
11506
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join13(flags2.repo, ".codeindex", "models");
11507
- mkdirSync2(destDir, { recursive: true });
11508
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join13(destDir, "model.json")}
11861
+ const destDir = process.env.CODEINDEX_EMBED_DIR ?? join15(flags2.repo, ".codeindex", "models");
11862
+ mkdirSync3(destDir, { recursive: true });
11863
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join15(destDir, "model.json")}
11509
11864
  `);
11510
11865
  let body2;
11511
11866
  try {
@@ -11526,16 +11881,102 @@ async function runCli(argv) {
11526
11881
  process.exitCode = 1;
11527
11882
  return;
11528
11883
  }
11529
- writeFileSync3(join13(destDir, "model.json"), body2);
11530
- process.stderr.write(`codeindex: model written to ${join13(destDir, "model.json")}
11884
+ writeFileSync4(join15(destDir, "model.json"), body2);
11885
+ process.stderr.write(`codeindex: model written to ${join15(destDir, "model.json")}
11531
11886
  `);
11532
11887
  } else {
11533
11888
  throw new Error("embed needs a subcommand: status | build | pull | serve");
11534
11889
  }
11890
+ } else if (cmd === "grammars") {
11891
+ const sub = flags2.positional;
11892
+ const cacheDir = sharedGrammarsCacheDir();
11893
+ if (sub === "status") {
11894
+ const info2 = resolveGrammarsTier();
11895
+ const runtimePresent = info2.dir ? existsSync4(join15(info2.dir, "web-tree-sitter.wasm")) : false;
11896
+ const target = resolveGrammarsPullTarget();
11897
+ const status = {
11898
+ engineVersion: ENGINE_VERSION,
11899
+ tier: info2.tier,
11900
+ dir: info2.dir ?? null,
11901
+ cacheDir,
11902
+ runtimePresent,
11903
+ pullNeeded: !runtimePresent,
11904
+ url: target.url
11905
+ };
11906
+ emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
11907
+ } else if (sub === "pull") {
11908
+ const target = resolveGrammarsPullTarget();
11909
+ let expected;
11910
+ if (target.sha256Url) {
11911
+ try {
11912
+ expected = await fetchExpectedSha256(target.sha256Url);
11913
+ } catch (e) {
11914
+ process.stderr.write(
11915
+ `codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
11916
+ `
11917
+ );
11918
+ }
11919
+ }
11920
+ const runtime = join15(cacheDir, "web-tree-sitter.wasm");
11921
+ const markerPath = join15(dirname4(cacheDir), `${ENGINE_VERSION}.sha256`);
11922
+ if (existsSync4(runtime) && expected && existsSync4(markerPath)) {
11923
+ let marker = "";
11924
+ try {
11925
+ marker = readFileSync6(markerPath, "utf8").trim();
11926
+ } catch {
11927
+ }
11928
+ if (marker === expected) {
11929
+ process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)
11930
+ `);
11931
+ return;
11932
+ }
11933
+ }
11934
+ process.stderr.write(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
11935
+ `);
11936
+ let bytes;
11937
+ try {
11938
+ bytes = await fetchGrammarsTarball(target.url, expected);
11939
+ } catch (e) {
11940
+ process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11941
+ `);
11942
+ process.exitCode = 1;
11943
+ return;
11944
+ }
11945
+ let tmp;
11946
+ try {
11947
+ mkdirSync3(dirname4(cacheDir), { recursive: true });
11948
+ tmp = mkdtempSync(join15(dirname4(cacheDir), ".grammars-tmp-"));
11949
+ extractGrammarsTarball(bytes, tmp);
11950
+ if (!existsSync4(join15(tmp, "web-tree-sitter.wasm"))) {
11951
+ throw new Error("archive is missing web-tree-sitter.wasm");
11952
+ }
11953
+ if (existsSync4(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
11954
+ renameSync(tmp, cacheDir);
11955
+ tmp = void 0;
11956
+ if (expected) writeFileSync4(markerPath, expected + "\n");
11957
+ } catch (e) {
11958
+ if (tmp) {
11959
+ try {
11960
+ rmSync2(tmp, { recursive: true, force: true });
11961
+ } catch {
11962
+ }
11963
+ }
11964
+ process.stderr.write(
11965
+ `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11966
+ `
11967
+ );
11968
+ process.exitCode = 1;
11969
+ return;
11970
+ }
11971
+ process.stderr.write(`codeindex: grammars extracted \u2192 ${cacheDir}
11972
+ `);
11973
+ } else {
11974
+ throw new Error("grammars needs a subcommand: status | pull");
11975
+ }
11535
11976
  } else if (cmd === "rules") {
11536
11977
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
11537
11978
  const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
11538
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11979
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11539
11980
  const violations = checkRules(graph, rules);
11540
11981
  const errors = violations.filter((v) => v.severity === "error").length;
11541
11982
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags2.out);
@@ -11556,26 +11997,26 @@ async function runCli(argv) {
11556
11997
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
11557
11998
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags2.out);
11558
11999
  } else if (cmd === "repomap") {
11559
- const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12000
+ const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11560
12001
  emit(renderRepoMap(scan2, graph, { budgetTokens: flags2.budgetTokens }), flags2.out);
11561
12002
  } else if (cmd === "hotspots") {
11562
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12003
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11563
12004
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11564
12005
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11565
12006
  } else if (cmd === "coupling") {
11566
12007
  const { ok, couplings } = changeCoupling(flags2.repo, { since: flags2.since });
11567
12008
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
11568
12009
  } else if (cmd === "deadcode") {
11569
- emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2))), null, 2) + "\n", flags2.out);
12010
+ emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk))), null, 2) + "\n", flags2.out);
11570
12011
  } else if (cmd === "complexity") {
11571
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12012
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11572
12013
  emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
11573
12014
  } else if (cmd === "risk") {
11574
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12015
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11575
12016
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11576
12017
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11577
12018
  } else if (cmd === "mermaid") {
11578
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12019
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11579
12020
  emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
11580
12021
  } else if (cmd === "grep") {
11581
12022
  if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
@@ -11594,6 +12035,7 @@ ${HELP}`);
11594
12035
  }
11595
12036
  }
11596
12037
  export {
12038
+ DEFAULT_GRAMMARS_URL,
11597
12039
  DEFAULT_MAX_FILES,
11598
12040
  EMBED_VERSION,
11599
12041
  ENGINE_VERSION,
@@ -11604,6 +12046,7 @@ export {
11604
12046
  applyCentrality,
11605
12047
  basicTokenize,
11606
12048
  betweennessOf,
12049
+ buildArtifactsFromScan,
11607
12050
  buildCallerIndex,
11608
12051
  buildEmbeddingIndex,
11609
12052
  buildEndpointIndex,
@@ -11646,14 +12089,19 @@ export {
11646
12089
  extToLang,
11647
12090
  extractAst,
11648
12091
  extractCode,
12092
+ extractGrammarsTarball,
11649
12093
  extractMarkdown,
11650
12094
  extractSymbols,
12095
+ extractTarInto,
12096
+ fetchExpectedSha256,
12097
+ fetchGrammarsTarball,
11651
12098
  findDeadCode,
11652
12099
  findReferences,
11653
12100
  findSymbol,
11654
12101
  foldText,
11655
12102
  gitChurn,
11656
12103
  grammarKeyForExt,
12104
+ grammarKeysForExts,
11657
12105
  grammarReady,
11658
12106
  grepRepo,
11659
12107
  hasEmbedModel,
@@ -11695,6 +12143,9 @@ export {
11695
12143
  resolveEmbedEndpoint,
11696
12144
  resolveEmbedModelDir,
11697
12145
  resolveEmbedPullUrl,
12146
+ resolveGrammarsDir,
12147
+ resolveGrammarsPullTarget,
12148
+ resolveGrammarsTier,
11698
12149
  resolveImport,
11699
12150
  resolveUniqueSymbol,
11700
12151
  riskHotspots,
@@ -11708,6 +12159,7 @@ export {
11708
12159
  serializeEmbeddings,
11709
12160
  sh,
11710
12161
  sha1,
12162
+ sharedGrammarsCacheDir,
11711
12163
  shortHash,
11712
12164
  slugify,
11713
12165
  subtokens,