@maxgfr/codeindex 2.12.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,9 +14,9 @@ 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.12.0";
17
+ ENGINE_VERSION = "2.14.0";
18
18
  SCHEMA_VERSION = 4;
19
- EXTRACTOR_VERSION = 9;
19
+ EXTRACTOR_VERSION = 10;
20
20
  }
21
21
  });
22
22
 
@@ -320,6 +320,7 @@ function walk(root, opts = {}) {
320
320
  const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
321
321
  const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
322
322
  const useGitignore = opts.gitignore !== false;
323
+ const ignoreDirs = opts.ignoreDirs ? new Set(opts.ignoreDirs) : IGNORE_DIRS;
323
324
  const out2 = [];
324
325
  let capped = false;
325
326
  let excluded = 0;
@@ -347,28 +348,31 @@ function walk(root, opts = {}) {
347
348
  if (!contained(real)) continue;
348
349
  let entries;
349
350
  try {
350
- 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
+ );
351
354
  } catch {
352
355
  continue;
353
356
  }
354
357
  let rules = frame.rules;
355
- if (useGitignore && entries.includes(".gitignore")) {
358
+ if (useGitignore && entries.some((e) => e.name === ".gitignore")) {
356
359
  const parsed = parseGitignore(readText(join(frame.dir, ".gitignore")), frame.rel);
357
360
  if (parsed.length) rules = [...rules, ...parsed];
358
361
  }
359
- for (const name2 of entries) {
362
+ for (const entry of entries) {
363
+ const name2 = entry.name;
360
364
  const abs = join(frame.dir, name2);
361
365
  const rel = frame.rel ? `${frame.rel}/${name2}` : name2;
366
+ const isLink = entry.isSymbolicLink();
367
+ if (entry.isDirectory() && ignoreDirs.has(name2)) continue;
362
368
  let st;
363
- let isLink;
364
369
  try {
365
- st = statSync(abs);
366
- isLink = lstatSync(abs).isSymbolicLink();
370
+ st = isLink ? statSync(abs) : lstatSync(abs);
367
371
  } catch {
368
372
  continue;
369
373
  }
370
374
  if (st.isDirectory()) {
371
- if (IGNORE_DIRS.has(name2)) continue;
375
+ if (ignoreDirs.has(name2)) continue;
372
376
  if (isLink) continue;
373
377
  if (useGitignore && rules.length && isIgnored(rules, rel, true)) continue;
374
378
  stack.push({ dir: abs, rel, rules });
@@ -463,6 +467,7 @@ var init_walk = __esm({
463
467
  ".cache",
464
468
  "tmp",
465
469
  ".ultraindex",
470
+ ".codeindex",
466
471
  "Pods",
467
472
  "DerivedData",
468
473
  ".terraform",
@@ -901,7 +906,7 @@ var init_js_ts = __esm({
901
906
  RULES = [
902
907
  { re: /^\s*export\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
903
908
  { re: /^\s*export\s+default\s+(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: true },
904
- { re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
909
+ { re: /^\s*export\s+default\s+(?:abstract\s+)?class\s+(?!extends\b)(?<name>[\w$]+)/, kind: "class", exported: true },
905
910
  { re: /^\s*(?:async\s+)?function\s+(?<name>[\w$]+)/, kind: "function", exported: false },
906
911
  { re: /^\s*export\s+(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: true },
907
912
  { re: /^\s*(?:abstract\s+)?class\s+(?<name>[\w$]+)/, kind: "class", exported: false },
@@ -1770,13 +1775,13 @@ async function Module2(moduleArg = {}) {
1770
1775
  }
1771
1776
  readAsync = /* @__PURE__ */ __name(async (url) => {
1772
1777
  if (isFileURI(url)) {
1773
- return new Promise((resolve2, reject) => {
1778
+ return new Promise((resolve3, reject) => {
1774
1779
  var xhr = new XMLHttpRequest();
1775
1780
  xhr.open("GET", url, true);
1776
1781
  xhr.responseType = "arraybuffer";
1777
1782
  xhr.onload = () => {
1778
1783
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
1779
- resolve2(xhr.response);
1784
+ resolve3(xhr.response);
1780
1785
  return;
1781
1786
  }
1782
1787
  reject(xhr.status);
@@ -1972,9 +1977,9 @@ async function Module2(moduleArg = {}) {
1972
1977
  __name(receiveInstantiationResult, "receiveInstantiationResult");
1973
1978
  var info2 = getWasmImports();
1974
1979
  if (Module["instantiateWasm"]) {
1975
- return new Promise((resolve2, reject) => {
1980
+ return new Promise((resolve3, reject) => {
1976
1981
  Module["instantiateWasm"](info2, (mod, inst) => {
1977
- resolve2(receiveInstance(mod, inst));
1982
+ resolve3(receiveInstance(mod, inst));
1978
1983
  });
1979
1984
  });
1980
1985
  }
@@ -3305,8 +3310,8 @@ async function Module2(moduleArg = {}) {
3305
3310
  if (runtimeInitialized) {
3306
3311
  moduleRtn = Module;
3307
3312
  } else {
3308
- moduleRtn = new Promise((resolve2, reject) => {
3309
- readyPromiseResolve = resolve2;
3313
+ moduleRtn = new Promise((resolve3, reject) => {
3314
+ readyPromiseResolve = resolve3;
3310
3315
  readyPromiseReject = reject;
3311
3316
  });
3312
3317
  }
@@ -5537,27 +5542,41 @@ ${JSON.stringify(symbolNames, null, 2)}`);
5537
5542
 
5538
5543
  // src/ast/loader.ts
5539
5544
  import { readFileSync as readFileSync2, existsSync } from "fs";
5545
+ import { homedir } from "os";
5540
5546
  import { dirname, join as join2 } from "path";
5541
5547
  import { fileURLToPath } from "url";
5542
5548
  function grammarKeyForExt(ext) {
5543
5549
  return EXT_GRAMMAR[ext];
5544
5550
  }
5545
- function resolveGrammarDir() {
5546
- const env = process.env.CODEINDEX_GRAMMAR_DIR ?? process.env.ULTRAINDEX_GRAMMAR_DIR;
5547
- if (env && existsSync(env)) return env;
5548
- const here = dirname(fileURLToPath(import.meta.url));
5549
- 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 = [
5550
5562
  join2(here, "grammars"),
5551
5563
  // bundle: <...>/scripts/grammars
5552
5564
  join2(here, "..", "..", "scripts", "grammars"),
5553
5565
  // dev: src/ast → <repo>/scripts/grammars
5554
5566
  join2(here, "..", "scripts", "grammars")
5555
5567
  ];
5556
- for (const c2 of candidates) if (existsSync(c2)) return c2;
5557
- 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;
5558
5576
  }
5559
5577
  async function ensureGrammars(keys) {
5560
- const dir = resolveGrammarDir();
5578
+ const dir = resolveGrammarsDir();
5579
+ if (!dir) return;
5561
5580
  if (!runtimeReady) {
5562
5581
  const runtime = join2(dir, "web-tree-sitter.wasm");
5563
5582
  if (!existsSync(runtime)) return;
@@ -5582,6 +5601,14 @@ async function ensureGrammars(keys) {
5582
5601
  function allGrammarKeys() {
5583
5602
  return [...new Set(Object.values(EXT_GRAMMAR))];
5584
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
+ }
5585
5612
  function grammarReady(key) {
5586
5613
  return loaded.has(key);
5587
5614
  }
@@ -5596,6 +5623,7 @@ var init_loader = __esm({
5596
5623
  "src/ast/loader.ts"() {
5597
5624
  "use strict";
5598
5625
  init_web_tree_sitter();
5626
+ init_types();
5599
5627
  EXT_GRAMMAR = {
5600
5628
  ".ts": "typescript",
5601
5629
  ".mts": "typescript",
@@ -5718,7 +5746,7 @@ function readReceiver(node) {
5718
5746
  const name2 = obj ? readName(obj) : void 0;
5719
5747
  return name2 && /^[A-Za-z_]\w*$/.test(name2) ? name2 : void 0;
5720
5748
  }
5721
- function collectCalls(root, spec) {
5749
+ function collectCalls(root, spec, maxCalls = MAX_CALLS) {
5722
5750
  if (!spec.calls) return [];
5723
5751
  const out2 = [];
5724
5752
  const seen = /* @__PURE__ */ new Set();
@@ -5749,7 +5777,7 @@ function collectCalls(root, spec) {
5749
5777
  };
5750
5778
  visit(root);
5751
5779
  out2.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
5752
- return out2.slice(0, MAX_CALLS);
5780
+ return out2.slice(0, maxCalls);
5753
5781
  }
5754
5782
  function collectImportedNames(root, spec) {
5755
5783
  if (!spec.imports?.import_statement) return [];
@@ -5776,7 +5804,7 @@ function collectImportedNames(root, spec) {
5776
5804
  visit(root);
5777
5805
  return [...found].sort(byStr).slice(0, MAX_IMPORTED_NAMES);
5778
5806
  }
5779
- function extractAst(rel, ext, content) {
5807
+ function extractAst(rel, ext, content, opts = {}) {
5780
5808
  const key = grammarKeyForExt(ext);
5781
5809
  if (!key || !grammarReady(key)) return void 0;
5782
5810
  const spec = SPECS[key];
@@ -5962,7 +5990,7 @@ function extractAst(rel, ext, content) {
5962
5990
  }
5963
5991
  const refs = collectImports(root, spec);
5964
5992
  const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
5965
- const calls = collectCalls(root, spec);
5993
+ const calls = collectCalls(root, spec, opts.maxCalls);
5966
5994
  const importedNames = collectImportedNames(root, spec);
5967
5995
  let pkg;
5968
5996
  if (spec.lang === "java") {
@@ -6398,12 +6426,12 @@ function extractImports(ext, content) {
6398
6426
  }
6399
6427
  return [...specs].map((spec) => ({ kind: "import", spec }));
6400
6428
  }
6401
- function collectCallsRegex(content, symbols = []) {
6429
+ function collectCallsRegex(content, symbols = [], maxCalls = 512) {
6402
6430
  const out2 = /* @__PURE__ */ new Map();
6403
6431
  const ownDefLines = new Set(symbols.map((s) => `${s.name} ${s.line}`));
6404
6432
  const lines = content.split("\n");
6405
6433
  const CALL_RE = /(?:\bnew\s+)?(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g;
6406
- for (let i2 = 0; i2 < lines.length && out2.size < 512; i2++) {
6434
+ for (let i2 = 0; i2 < lines.length && out2.size < maxCalls; i2++) {
6407
6435
  const line = lines[i2];
6408
6436
  const trimmed = line.trimStart();
6409
6437
  if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
@@ -6418,7 +6446,7 @@ function collectCallsRegex(content, symbols = []) {
6418
6446
  CALL_RE.lastIndex = 0;
6419
6447
  let m;
6420
6448
  const fallbackExcluded = /* @__PURE__ */ new Set();
6421
- while ((m = CALL_RE.exec(line)) !== null && out2.size < 512) {
6449
+ while ((m = CALL_RE.exec(line)) !== null && out2.size < maxCalls) {
6422
6450
  const receiver = m[1];
6423
6451
  const name2 = m[2];
6424
6452
  if (name2.length < 2 || CALL_KEYWORDS.has(name2)) continue;
@@ -6435,8 +6463,8 @@ function collectCallsRegex(content, symbols = []) {
6435
6463
  }
6436
6464
  return [...out2.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.line - b.line);
6437
6465
  }
6438
- function extractCode(rel, ext, content) {
6439
- const ast = extractAst(rel, ext, content);
6466
+ function extractCode(rel, ext, content, opts = {}) {
6467
+ const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
6440
6468
  const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
6441
6469
  const known = new Set(symbols.map((s) => s.name));
6442
6470
  const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
@@ -6452,7 +6480,7 @@ function extractCode(rel, ext, content) {
6452
6480
  // collector otherwise, so caller indexes exist without the wasm sidecar.
6453
6481
  // `symbols` (this file's own regex-extracted defs) lets the collector
6454
6482
  // exclude a definition's own name+line from its call candidates.
6455
- calls: ast ? ast.calls : collectCallsRegex(content, symbols),
6483
+ calls: ast ? ast.calls : collectCallsRegex(content, symbols, opts.maxCallsPerFile),
6456
6484
  importedNames: ast?.importedNames
6457
6485
  };
6458
6486
  }
@@ -6521,16 +6549,20 @@ function scanRepo(root, opts = {}) {
6521
6549
  const scoped = opts.scope ? [...opts.include ?? [], `${opts.scope.replace(/\/+$/, "")}/**`] : opts.include;
6522
6550
  const include = compileGlobs(scoped);
6523
6551
  const exclude = compileGlobs(opts.exclude);
6524
- const { files: walked, capped, excluded } = walk(root, {
6552
+ const { files: walked, capped, excluded } = opts.precomputedWalk ?? walk(root, {
6525
6553
  maxFileBytes: opts.maxBytes,
6526
6554
  maxFiles: opts.maxFiles,
6527
- gitignore: opts.gitignore
6555
+ gitignore: opts.gitignore,
6556
+ ignoreDirs: opts.ignoreDirs
6528
6557
  });
6529
6558
  const outPrefix = opts.out ? opts.out.replace(/\/+$/, "") + "/" : null;
6530
6559
  const files = [];
6531
6560
  const languages = {};
6532
6561
  const docText = /* @__PURE__ */ new Map();
6533
6562
  const mtimes = /* @__PURE__ */ new Map();
6563
+ const cache = opts.cache;
6564
+ let allReused = cache !== void 0;
6565
+ let cacheDirty = cache === void 0;
6534
6566
  for (const f of walked) {
6535
6567
  if (outPrefix && (f.abs === opts.out || f.abs.startsWith(outPrefix))) continue;
6536
6568
  if (include && !include(f.rel)) continue;
@@ -6549,8 +6581,11 @@ function scanRepo(root, opts = {}) {
6549
6581
  if (cached && cached.hash === hash) {
6550
6582
  files.push(cached.record);
6551
6583
  if (kind === "doc" && content) docText.set(f.rel, content);
6584
+ if (cached.size !== f.size || cached.mtimeMs !== f.mtimeMs) cacheDirty = true;
6552
6585
  continue;
6553
6586
  }
6587
+ allReused = false;
6588
+ cacheDirty = true;
6554
6589
  const record = {
6555
6590
  rel: f.rel,
6556
6591
  ext: f.ext,
@@ -6573,7 +6608,7 @@ function scanRepo(root, opts = {}) {
6573
6608
  } else if (kind === "doc") {
6574
6609
  record.title = basename(f.rel);
6575
6610
  } else if (kind === "code") {
6576
- const code = extractCode(f.rel, f.ext, content);
6611
+ const code = extractCode(f.rel, f.ext, content, { maxCallsPerFile: opts.maxCallsPerFile });
6577
6612
  record.title = basename(f.rel);
6578
6613
  record.summary = code.summary;
6579
6614
  record.symbols = code.symbols;
@@ -6592,7 +6627,22 @@ function scanRepo(root, opts = {}) {
6592
6627
  files.push(record);
6593
6628
  }
6594
6629
  files.sort(byKey((f) => f.rel));
6595
- 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
+ };
6596
6646
  }
6597
6647
  var init_scan = __esm({
6598
6648
  "src/scan.ts"() {
@@ -6611,7 +6661,7 @@ var init_scan = __esm({
6611
6661
 
6612
6662
  // src/resolve.ts
6613
6663
  import { posix } from "path";
6614
- import { join as join3 } from "path";
6664
+ import { join as join4 } from "path";
6615
6665
  function distToSrcCandidates(target) {
6616
6666
  const segs = norm(target).split("/").filter((s) => s !== ".");
6617
6667
  const out2 = [];
@@ -6698,7 +6748,7 @@ function resolveExtends(fileSet, fromDir, ext) {
6698
6748
  function readTsConfig(root, fileSet, rel, warnings, seen) {
6699
6749
  if (seen.has(rel)) return void 0;
6700
6750
  seen.add(rel);
6701
- const cfg = tolerantJsonParse(readText(join3(root, rel)));
6751
+ const cfg = tolerantJsonParse(readText(join4(root, rel)));
6702
6752
  if (cfg === void 0) {
6703
6753
  warnings.push(`unparseable ${rel} \u2014 its path aliases were ignored`);
6704
6754
  return void 0;
@@ -6827,7 +6877,7 @@ function buildResolveContext(scan2) {
6827
6877
  const goModules = [];
6828
6878
  for (const rel of fileSet) {
6829
6879
  if (rel !== "go.mod" && !rel.endsWith("/go.mod")) continue;
6830
- const text = readText(join3(scan2.root, rel));
6880
+ const text = readText(join4(scan2.root, rel));
6831
6881
  const m = /^\s*module\s+(\S+)/m.exec(text);
6832
6882
  if (!m) continue;
6833
6883
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6837,7 +6887,7 @@ function buildResolveContext(scan2) {
6837
6887
  const rustCrates = [];
6838
6888
  for (const rel of fileSet) {
6839
6889
  if (rel !== "Cargo.toml" && !rel.endsWith("/Cargo.toml")) continue;
6840
- const text = readText(join3(scan2.root, rel));
6890
+ const text = readText(join4(scan2.root, rel));
6841
6891
  const m = /\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/ms.exec(text);
6842
6892
  if (!m) continue;
6843
6893
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6864,7 +6914,7 @@ function buildResolveContext(scan2) {
6864
6914
  const workspacePackages = [];
6865
6915
  for (const rel of fileSet) {
6866
6916
  if (rel !== "package.json" && !rel.endsWith("/package.json")) continue;
6867
- const pkg = tolerantJsonParse(readText(join3(scan2.root, rel)));
6917
+ const pkg = tolerantJsonParse(readText(join4(scan2.root, rel)));
6868
6918
  if (pkg === void 0) {
6869
6919
  warnings.push(`unparseable ${rel} \u2014 skipped for workspace resolution`);
6870
6920
  continue;
@@ -6891,7 +6941,7 @@ function buildResolveContext(scan2) {
6891
6941
  const phpPsr4 = [];
6892
6942
  for (const rel of fileSet) {
6893
6943
  if (rel !== "composer.json" && !rel.endsWith("/composer.json")) continue;
6894
- const composer = tolerantJsonParse(readText(join3(scan2.root, rel)));
6944
+ const composer = tolerantJsonParse(readText(join4(scan2.root, rel)));
6895
6945
  if (!composer) {
6896
6946
  warnings.push(`unparseable ${rel} \u2014 skipped for PHP PSR-4 resolution`);
6897
6947
  continue;
@@ -7466,7 +7516,7 @@ var init_calls = __esm({
7466
7516
  });
7467
7517
 
7468
7518
  // src/graph.ts
7469
- import { join as join4 } from "path";
7519
+ import { join as join5 } from "path";
7470
7520
  function isDistinctive(name2) {
7471
7521
  if (name2.length < 5) return false;
7472
7522
  const internalUpper = /[a-z][A-Z]/.test(name2) || /[A-Z]{2}/.test(name2);
@@ -7545,7 +7595,7 @@ function buildGraph(scan2, ctx, modules, moduleOf, meta) {
7545
7595
  if (unique.size) {
7546
7596
  for (const f of scan2.files) {
7547
7597
  if (f.kind !== "doc") continue;
7548
- 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));
7549
7599
  if (!content) continue;
7550
7600
  const tokens = /* @__PURE__ */ new Map();
7551
7601
  for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
@@ -7650,217 +7700,567 @@ var init_graph = __esm({
7650
7700
  }
7651
7701
  });
7652
7702
 
7653
- // src/callers.ts
7654
- function computeImportPairs(scan2) {
7655
- const ctx = buildResolveContext(scan2);
7656
- 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
+ };
7657
7713
  for (const f of scan2.files) {
7658
- for (const ref of f.refs) {
7659
- if (ref.kind !== "import") continue;
7660
- const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
7661
- 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
+ }
7662
7726
  }
7663
7727
  }
7664
- return pairs;
7728
+ return refs;
7665
7729
  }
7666
- function buildCallerIndex(scan2, importPairs, opts = {}) {
7667
- const pairs = importPairs ?? computeImportPairs(scan2);
7668
- const recall = opts.recall === true;
7669
- const defs = /* @__PURE__ */ new Map();
7670
- for (const f of scan2.files) {
7671
- const seen = /* @__PURE__ */ new Set();
7672
- for (const s of f.symbols) {
7673
- if (!s.exported || REFERENCE_KINDS3.has(s.kind)) continue;
7674
- if (seen.has(s.name)) continue;
7675
- seen.add(s.name);
7676
- let arr = defs.get(s.name);
7677
- if (!arr) defs.set(s.name, arr = []);
7678
- arr.push(s);
7679
- }
7680
- }
7681
- const localDefs = /* @__PURE__ */ new Map();
7730
+ function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
7731
+ const defsByName = /* @__PURE__ */ new Map();
7682
7732
  for (const f of scan2.files) {
7683
- const byName = /* @__PURE__ */ new Map();
7684
7733
  for (const s of f.symbols) {
7685
- if (!REFERENCE_KINDS3.has(s.kind) && !byName.has(s.name)) byName.set(s.name, s);
7686
- }
7687
- localDefs.set(f.rel, byName);
7688
- }
7689
- const sites = /* @__PURE__ */ new Map();
7690
- const record = (def, caller) => {
7691
- let entry = sites.get(def.name + "\0" + def.file);
7692
- if (!entry) sites.set(def.name + "\0" + def.file, entry = { def, callers: [] });
7693
- entry.callers.push(caller);
7694
- };
7695
- for (const f of scan2.files) {
7696
- if (!f.calls?.length) continue;
7697
- const family = familyOf(f.lang);
7698
- const own = localDefs.get(f.rel);
7699
- for (const c2 of f.calls) {
7700
- const local = own.get(c2.name);
7701
- if (local) {
7702
- if (local.line !== c2.line)
7703
- record(local, recall ? { file: f.rel, line: c2.line, confidence: "corroborated" } : { file: f.rel, line: c2.line });
7704
- continue;
7705
- }
7706
- const cands = (defs.get(c2.name) ?? []).filter((d) => familyOf(d.lang) === family && d.file !== f.rel).map((d) => ({ file: d.file, lang: d.lang }));
7707
- if (!cands.length) continue;
7708
- const imported = cands.filter((d) => pairs.has(`${f.rel}|${d.file}`));
7709
- const chosen = family === "js" ? imported.length ? pickCandidate(f.rel, imported) : (
7710
- // JS/TS gate: no corroborating import → no binding. Recall mode
7711
- // relaxes this to a unique-repo-wide name match (issue #7).
7712
- recall && cands.length === 1 ? cands[0] : void 0
7713
- ) : imported.length ? pickCandidate(f.rel, imported) : pickCandidate(f.rel, cands);
7714
- if (!chosen) continue;
7715
- const def = defs.get(c2.name).find((d) => d.file === chosen.file);
7716
- record(
7717
- def,
7718
- recall ? { file: f.rel, line: c2.line, confidence: imported.length ? "corroborated" : "unique-name" } : { file: f.rel, line: c2.line }
7719
- );
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
+ });
7720
7745
  }
7721
7746
  }
7722
- const index = /* @__PURE__ */ new Map();
7723
- const keys = [...sites.keys()].sort(byStr);
7724
- for (const key of keys) {
7725
- const { def, callers } = sites.get(key);
7726
- callers.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7727
- if (!index.has(def.name)) index.set(def.name, { def, callers });
7728
- 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));
7729
7750
  }
7730
- return index;
7731
- }
7732
- function enclosingSymbol(scan2, file, line) {
7733
- const f = scan2.files.find((x) => x.rel === file);
7734
- if (!f?.symbols.length) return void 0;
7735
- return enclosingAmong(f.symbols, line);
7736
- }
7737
- function enclosingAmong(symbols, line) {
7738
- let best;
7739
- for (const s of symbols) {
7740
- if (REFERENCE_KINDS3.has(s.kind)) continue;
7741
- if (s.line > line) continue;
7742
- if (s.endLine !== void 0 && line > s.endLine) continue;
7743
- if (!best || s.line > best.line || s.line === best.line && (s.endLine ?? Infinity) <= (best.endLine ?? Infinity)) {
7744
- best = s;
7745
- }
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;
7746
7755
  }
7747
- return best;
7756
+ return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
7748
7757
  }
7749
- function buildRawCallerIndex(scan2) {
7750
- const byName = /* @__PURE__ */ new Map();
7751
- for (const f of scan2.files) {
7752
- if (!f.calls?.length) continue;
7753
- const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
7754
- for (const c2 of f.calls) {
7755
- const site = { file: f.rel, line: c2.line };
7756
- if (c2.receiver !== void 0) site.receiver = c2.receiver;
7757
- const enc = enclosingAmong(symbols, c2.line);
7758
- if (enc) site.enclosingSymbol = enc;
7759
- let arr = byName.get(c2.name);
7760
- if (!arr) byName.set(c2.name, arr = []);
7761
- arr.push(site);
7762
- }
7763
- }
7764
- const index = /* @__PURE__ */ new Map();
7765
- for (const name2 of [...byName.keys()].sort(byStr)) {
7766
- const sites = byName.get(name2);
7767
- sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7768
- index.set(name2, sites);
7769
- }
7770
- return index;
7758
+ function renderSymbolsJson(index) {
7759
+ return JSON.stringify(index, null, 2) + "\n";
7771
7760
  }
7772
- var REFERENCE_KINDS3;
7773
- var init_callers = __esm({
7774
- "src/callers.ts"() {
7761
+ var init_symbols_json = __esm({
7762
+ "src/render/symbols-json.ts"() {
7775
7763
  "use strict";
7776
- init_calls();
7777
- init_resolve();
7764
+ init_types();
7778
7765
  init_sort();
7779
- REFERENCE_KINDS3 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7766
+ init_graph();
7780
7767
  }
7781
7768
  });
7782
7769
 
7783
- // src/query.ts
7784
- import { join as join5 } from "path";
7785
- function symbolsOverview(scan2, rel) {
7786
- const f = scan2.files.find((x) => x.rel === rel);
7787
- if (!f) return [];
7788
- return [...f.symbols].filter((s) => !REFERENCE_KINDS4.has(s.kind)).sort((a, b) => a.line - b.line || byStr(a.name, b.name));
7789
- }
7790
- function findSymbol(scan2, namePath, opts = {}) {
7791
- const segments = namePath.split("/").filter(Boolean);
7792
- if (!segments.length) return [];
7793
- const leaf = segments[segments.length - 1];
7794
- const parents = segments.slice(0, -1);
7795
- 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");
7796
7773
  const out2 = [];
7797
- for (const f of scan2.files) {
7798
- for (const s of f.symbols) {
7799
- if (REFERENCE_KINDS4.has(s.kind)) continue;
7800
- if (!matchName(s.name, leaf)) continue;
7801
- if (parents.length) {
7802
- const parent = parents[parents.length - 1];
7803
- if (!s.parent || s.parent !== parent) continue;
7804
- }
7805
- out2.push({ ...s });
7806
- }
7807
- }
7808
- out2.sort(
7809
- (a, b) => Number(b.name === leaf) - Number(a.name === leaf) || byStr(a.file, b.file) || a.line - b.line
7810
- );
7811
- const capped = out2.slice(0, opts.maxResults ?? 50);
7812
- if (opts.includeBody) {
7813
- for (const m of capped) {
7814
- const end = m.endLine ?? m.line;
7815
- const content = readText(join5(scan2.root, m.file));
7816
- if (!content) continue;
7817
- m.body = content.split("\n").slice(m.line - 1, end).join("\n");
7818
- }
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++;
7819
7788
  }
7820
- return capped;
7821
7789
  }
7822
- function findReferences(scan2, name2) {
7823
- const defs = [];
7790
+ function buildDocs(scan2) {
7791
+ const docs = [];
7824
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();
7825
7795
  for (const s of f.symbols) {
7826
- if (s.name === name2 && !REFERENCE_KINDS4.has(s.kind)) defs.push(s);
7827
- }
7828
- }
7829
- defs.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7830
- const index = buildCallerIndex(scan2);
7831
- const entry = index.get(name2);
7832
- const callSites = entry ? entry.callers : [];
7833
- const referencingFiles = /* @__PURE__ */ new Set();
7834
- const unique = uniqueSymbolDefs(scan2);
7835
- const defFile = unique.get(name2);
7836
- for (const f of scan2.files) {
7837
- if (f.rel === defFile) continue;
7838
- if (f.kind === "code" && f.idents?.includes(name2)) referencingFiles.add(f.rel);
7839
- else if (f.kind === "doc") {
7840
- const content = scan2.docText.get(f.rel);
7841
- if (content && new RegExp(`\\b${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content)) {
7842
- 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);
7843
7800
  }
7844
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);
7845
7806
  }
7846
- for (const site of callSites) referencingFiles.add(site.file);
7847
- return { defs, callSites, referencingFiles: [...referencingFiles].sort(byStr) };
7807
+ return docs;
7848
7808
  }
7849
- var REFERENCE_KINDS4;
7850
- var init_query = __esm({
7851
- "src/query.ts"() {
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"() {
7852
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();
7853
7982
  init_walk();
7854
- init_callers();
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();
7855
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({
8252
+ "src/query.ts"() {
8253
+ "use strict";
8254
+ init_walk();
8255
+ init_derived();
7856
8256
  init_sort();
7857
8257
  REFERENCE_KINDS4 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7858
8258
  }
7859
8259
  });
7860
8260
 
7861
8261
  // src/edit.ts
7862
- import { readFileSync as readFileSync3, writeFileSync } from "fs";
7863
- import { join as join6 } from "path";
8262
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8263
+ import { join as join9 } from "path";
7864
8264
  function resolveUniqueSymbol(scan2, namePath, file) {
7865
8265
  let matches = findSymbol(scan2, namePath);
7866
8266
  if (file) matches = matches.filter((m) => m.file === file);
@@ -7878,15 +8278,15 @@ function readLines(abs) {
7878
8278
  function replaceSymbolBody(scan2, namePath, body2, file) {
7879
8279
  const sym = resolveUniqueSymbol(scan2, namePath, file);
7880
8280
  const end = sym.endLine ?? sym.line;
7881
- const abs = join6(scan2.root, sym.file);
8281
+ const abs = join9(scan2.root, sym.file);
7882
8282
  const lines = readLines(abs);
7883
8283
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
7884
8284
  lines.splice(sym.line - 1, end - sym.line + 1, ...newLines);
7885
- writeFileSync(abs, lines.join("\n"));
8285
+ writeFileSync2(abs, lines.join("\n"));
7886
8286
  return { file: sym.file, startLine: sym.line, endLine: sym.line + newLines.length - 1, lines: newLines.length };
7887
8287
  }
7888
8288
  function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7889
- const abs = join6(scan2.root, sym.file);
8289
+ const abs = join9(scan2.root, sym.file);
7890
8290
  const lines = readLines(abs);
7891
8291
  const minGap = SEPARATED_KINDS.has(sym.kind) ? 1 : 0;
7892
8292
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
@@ -7895,7 +8295,7 @@ function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7895
8295
  block.push(...newLines);
7896
8296
  if (blankAfter && minGap && lines[index]?.trim() !== "") block.push("");
7897
8297
  lines.splice(index, 0, ...block);
7898
- writeFileSync(abs, lines.join("\n"));
8298
+ writeFileSync2(abs, lines.join("\n"));
7899
8299
  return { file: sym.file, startLine: index + 1, endLine: index + block.length, lines: block.length };
7900
8300
  }
7901
8301
  function insertAfterSymbol(scan2, namePath, body2, file) {
@@ -7917,8 +8317,8 @@ var init_edit = __esm({
7917
8317
  });
7918
8318
 
7919
8319
  // src/memory.ts
7920
- import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
7921
- 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";
7922
8322
  function sanitize(name2) {
7923
8323
  const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
7924
8324
  if (!clean) throw new Error("memory name is empty");
@@ -7932,12 +8332,12 @@ function sanitize(name2) {
7932
8332
  return clean;
7933
8333
  }
7934
8334
  function memoryPath(repo, name2) {
7935
- return join7(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
8335
+ return join10(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
7936
8336
  }
7937
8337
  function writeMemory(repo, name2, content) {
7938
8338
  const path = memoryPath(repo, name2);
7939
- mkdirSync(dirname2(path), { recursive: true });
7940
- writeFileSync2(path, content.endsWith("\n") ? content : content + "\n");
8339
+ mkdirSync2(dirname3(path), { recursive: true });
8340
+ writeFileSync3(path, content.endsWith("\n") ? content : content + "\n");
7941
8341
  return sanitize(name2);
7942
8342
  }
7943
8343
  function readMemory(repo, name2) {
@@ -7958,7 +8358,7 @@ function deleteMemory(repo, name2) {
7958
8358
  return true;
7959
8359
  }
7960
8360
  function listMemories(repo) {
7961
- const root = join7(repo, ...MEMORY_DIR);
8361
+ const root = join10(repo, ...MEMORY_DIR);
7962
8362
  const out2 = [];
7963
8363
  const walk2 = (dir, prefix) => {
7964
8364
  let entries;
@@ -7968,7 +8368,7 @@ function listMemories(repo) {
7968
8368
  return;
7969
8369
  }
7970
8370
  for (const e of entries) {
7971
- 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);
7972
8372
  else if (e.name.endsWith(".md")) out2.push(prefix ? `${prefix}/${e.name.slice(0, -3)}` : e.name.slice(0, -3));
7973
8373
  }
7974
8374
  };
@@ -7985,7 +8385,7 @@ var init_memory = __esm({
7985
8385
 
7986
8386
  // src/workspaces.ts
7987
8387
  import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
7988
- import { join as join8 } from "path";
8388
+ import { join as join11 } from "path";
7989
8389
  function readJson(path, label, warnings) {
7990
8390
  const raw = readText(path);
7991
8391
  if (!raw) return void 0;
@@ -8036,7 +8436,7 @@ function wsGlobToRegExp(pat) {
8036
8436
  return new RegExp(`^${re}($|/)`);
8037
8437
  }
8038
8438
  function probeNodePkg(root, dir, kind, warnings) {
8039
- const path = join8(root, dir, "package.json");
8439
+ const path = join11(root, dir, "package.json");
8040
8440
  if (!existsSync2(path)) return void 0;
8041
8441
  const manifest = `${dir}/package.json`;
8042
8442
  const pkg = readJson(path, manifest, warnings);
@@ -8050,7 +8450,7 @@ function probeNodePkg(root, dir, kind, warnings) {
8050
8450
  return out2;
8051
8451
  }
8052
8452
  function probeCargo(root, dir) {
8053
- const path = join8(root, dir, "Cargo.toml");
8453
+ const path = join11(root, dir, "Cargo.toml");
8054
8454
  if (!existsSync2(path)) return void 0;
8055
8455
  const body2 = tomlSectionBody(readText(path), "package");
8056
8456
  const out2 = {
@@ -8064,18 +8464,18 @@ function probeCargo(root, dir) {
8064
8464
  return out2;
8065
8465
  }
8066
8466
  function probeGoMod(root, dir) {
8067
- const path = join8(root, dir, "go.mod");
8467
+ const path = join11(root, dir, "go.mod");
8068
8468
  if (!existsSync2(path)) return void 0;
8069
8469
  const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
8070
8470
  return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
8071
8471
  }
8072
8472
  function probeMaven(root, dir) {
8073
- const path = join8(root, dir, "pom.xml");
8473
+ const path = join11(root, dir, "pom.xml");
8074
8474
  if (!existsSync2(path)) return void 0;
8075
8475
  return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
8076
8476
  }
8077
8477
  function probePyproject(root, dir) {
8078
- const path = join8(root, dir, "pyproject.toml");
8478
+ const path = join11(root, dir, "pyproject.toml");
8079
8479
  if (!existsSync2(path)) return void 0;
8080
8480
  const toml = readText(path);
8081
8481
  const project = tomlSectionBody(toml, "project");
@@ -8091,7 +8491,7 @@ function probePyproject(root, dir) {
8091
8491
  return out2;
8092
8492
  }
8093
8493
  function probeComposer(root, dir, warnings) {
8094
- const path = join8(root, dir, "composer.json");
8494
+ const path = join11(root, dir, "composer.json");
8095
8495
  if (!existsSync2(path)) return void 0;
8096
8496
  const manifest = `${dir}/composer.json`;
8097
8497
  const pkg = readJson(path, manifest, warnings);
@@ -8105,7 +8505,7 @@ function probeComposer(root, dir, warnings) {
8105
8505
  return out2;
8106
8506
  }
8107
8507
  function probeNxProject(root, dir, warnings) {
8108
- const path = join8(root, dir, "project.json");
8508
+ const path = join11(root, dir, "project.json");
8109
8509
  if (!existsSync2(path)) return void 0;
8110
8510
  const manifest = `${dir}/project.json`;
8111
8511
  const proj = readJson(path, manifest, warnings);
@@ -8118,7 +8518,7 @@ function probeNxProject(root, dir, warnings) {
8118
8518
  }
8119
8519
  function probeGradle(root, dir) {
8120
8520
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8121
- if (existsSync2(join8(root, dir, f))) {
8521
+ if (existsSync2(join11(root, dir, f))) {
8122
8522
  return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
8123
8523
  }
8124
8524
  }
@@ -8153,7 +8553,7 @@ function addPackage(root, dir, found, kind, warnings) {
8153
8553
  }
8154
8554
  function isDirAt(root, rel) {
8155
8555
  try {
8156
- return statSync3(join8(root, rel)).isDirectory();
8556
+ return statSync3(join11(root, rel)).isDirectory();
8157
8557
  } catch {
8158
8558
  return false;
8159
8559
  }
@@ -8161,7 +8561,7 @@ function isDirAt(root, rel) {
8161
8561
  function subdirsOf(root, base) {
8162
8562
  let entries;
8163
8563
  try {
8164
- entries = readdirSync3(base ? join8(root, base) : root, { withFileTypes: true });
8564
+ entries = readdirSync3(base ? join11(root, base) : root, { withFileTypes: true });
8165
8565
  } catch {
8166
8566
  return [];
8167
8567
  }
@@ -8223,14 +8623,14 @@ function npmFamilyPatterns(root, warnings) {
8223
8623
  if (t.startsWith("!")) negations.push(t.slice(1));
8224
8624
  else positives.push({ pattern: t, kind });
8225
8625
  };
8226
- const pkg = readJson(join8(root, "package.json"), "package.json", warnings);
8626
+ const pkg = readJson(join11(root, "package.json"), "package.json", warnings);
8227
8627
  const ws = pkg?.workspaces;
8228
8628
  if (Array.isArray(ws)) {
8229
8629
  for (const x of ws) if (typeof x === "string") push(x, "npm");
8230
8630
  } else if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
8231
8631
  for (const x of ws.packages) if (typeof x === "string") push(x, "npm");
8232
8632
  }
8233
- const pnpm = readText(join8(root, "pnpm-workspace.yaml"));
8633
+ const pnpm = readText(join11(root, "pnpm-workspace.yaml"));
8234
8634
  let inPackages = false;
8235
8635
  for (const line of pnpm.split(/\r?\n/)) {
8236
8636
  if (/^\S/.test(line)) {
@@ -8244,11 +8644,11 @@ function npmFamilyPatterns(root, warnings) {
8244
8644
  return { positives, negations };
8245
8645
  }
8246
8646
  function fallbackNpmPatterns(root, warnings) {
8247
- const lerna = readJson(join8(root, "lerna.json"), "lerna.json", warnings);
8647
+ const lerna = readJson(join11(root, "lerna.json"), "lerna.json", warnings);
8248
8648
  if (lerna && Array.isArray(lerna.packages)) {
8249
8649
  return lerna.packages.filter((x) => typeof x === "string").map((pattern) => ({ pattern, kind: "lerna" }));
8250
8650
  }
8251
- const nx = readJson(join8(root, "nx.json"), "nx.json", warnings);
8651
+ const nx = readJson(join11(root, "nx.json"), "nx.json", warnings);
8252
8652
  if (nx) {
8253
8653
  const layout = nx.workspaceLayout ?? {};
8254
8654
  const appsDir = typeof layout.appsDir === "string" ? layout.appsDir : "apps";
@@ -8258,7 +8658,7 @@ function fallbackNpmPatterns(root, warnings) {
8258
8658
  return [];
8259
8659
  }
8260
8660
  function detectCargoMembers(root, found, warnings) {
8261
- const toml = readText(join8(root, "Cargo.toml"));
8661
+ const toml = readText(join11(root, "Cargo.toml"));
8262
8662
  if (!toml) return;
8263
8663
  const body2 = tomlSectionBody(toml, "workspace");
8264
8664
  if (!body2) return;
@@ -8273,7 +8673,7 @@ function detectCargoMembers(root, found, warnings) {
8273
8673
  }
8274
8674
  }
8275
8675
  function detectGoWork(root, found, warnings) {
8276
- const gowork = readText(join8(root, "go.work"));
8676
+ const gowork = readText(join11(root, "go.work"));
8277
8677
  if (!gowork) return;
8278
8678
  const dirs = [];
8279
8679
  for (const block of gowork.matchAll(/^use\s*\(([\s\S]*?)\)/gm)) {
@@ -8289,7 +8689,7 @@ function detectGoWork(root, found, warnings) {
8289
8689
  }
8290
8690
  }
8291
8691
  function detectMavenModules(root, found, warnings) {
8292
- const pom = readText(join8(root, "pom.xml"));
8692
+ const pom = readText(join11(root, "pom.xml"));
8293
8693
  if (!pom) return;
8294
8694
  const modules = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1];
8295
8695
  if (!modules) return;
@@ -8298,7 +8698,7 @@ function detectMavenModules(root, found, warnings) {
8298
8698
  }
8299
8699
  }
8300
8700
  function detectUvMembers(root, found, warnings) {
8301
- const toml = readText(join8(root, "pyproject.toml"));
8701
+ const toml = readText(join11(root, "pyproject.toml"));
8302
8702
  if (!toml) return;
8303
8703
  const body2 = tomlSectionBody(toml, "tool.uv.workspace");
8304
8704
  if (!body2) return;
@@ -8313,7 +8713,7 @@ function detectUvMembers(root, found, warnings) {
8313
8713
  }
8314
8714
  }
8315
8715
  function detectComposerPathRepos(root, found, warnings) {
8316
- const composer = readJson(join8(root, "composer.json"), "composer.json", warnings);
8716
+ const composer = readJson(join11(root, "composer.json"), "composer.json", warnings);
8317
8717
  const repos = composer?.repositories;
8318
8718
  if (!Array.isArray(repos)) return;
8319
8719
  for (const r of repos) {
@@ -8324,7 +8724,7 @@ function detectComposerPathRepos(root, found, warnings) {
8324
8724
  }
8325
8725
  function detectGradleIncludes(root, found, warnings) {
8326
8726
  for (const f of ["settings.gradle", "settings.gradle.kts"]) {
8327
- const text = readText(join8(root, f));
8727
+ const text = readText(join11(root, f));
8328
8728
  if (!text) continue;
8329
8729
  for (const line of text.split(/\r?\n/)) {
8330
8730
  if (!/^\s*include[\s(]/.test(line)) continue;
@@ -8336,7 +8736,7 @@ function detectGradleIncludes(root, found, warnings) {
8336
8736
  }
8337
8737
  }
8338
8738
  function npmEdges(root, pkg, byName, warnings) {
8339
- 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);
8340
8740
  if (!manifest) return [];
8341
8741
  const edges = /* @__PURE__ */ new Set();
8342
8742
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -8359,7 +8759,7 @@ function normalizeDepPath(fromDir, rel) {
8359
8759
  return out2.join("/");
8360
8760
  }
8361
8761
  function cargoEdges(root, pkg, byName, byDir) {
8362
- const toml = readText(join8(root, pkg.dir, "Cargo.toml"));
8762
+ const toml = readText(join11(root, pkg.dir, "Cargo.toml"));
8363
8763
  if (!toml) return [];
8364
8764
  const edges = /* @__PURE__ */ new Set();
8365
8765
  for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
@@ -8383,7 +8783,7 @@ function cargoEdges(root, pkg, byName, byDir) {
8383
8783
  return [...edges];
8384
8784
  }
8385
8785
  function goPkgEdges(root, pkg, byName, byDir) {
8386
- const gomod = readText(join8(root, pkg.dir, "go.mod"));
8786
+ const gomod = readText(join11(root, pkg.dir, "go.mod"));
8387
8787
  if (!gomod) return [];
8388
8788
  const edges = /* @__PURE__ */ new Set();
8389
8789
  for (const m of gomod.matchAll(/^\s*(?:require\s+)?([^\s/(][^\s]*)\s+v[^\s]+/gm)) {
@@ -8397,7 +8797,7 @@ function goPkgEdges(root, pkg, byName, byDir) {
8397
8797
  return [...edges];
8398
8798
  }
8399
8799
  function mavenEdges(root, pkg, byName) {
8400
- const pom = readText(join8(root, pkg.dir, "pom.xml"));
8800
+ const pom = readText(join11(root, pkg.dir, "pom.xml"));
8401
8801
  if (!pom) return [];
8402
8802
  const edges = /* @__PURE__ */ new Set();
8403
8803
  for (const m of pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
@@ -8407,7 +8807,7 @@ function mavenEdges(root, pkg, byName) {
8407
8807
  return [...edges];
8408
8808
  }
8409
8809
  function uvEdges(root, pkg, byName) {
8410
- const toml = readText(join8(root, pkg.dir, "pyproject.toml"));
8810
+ const toml = readText(join11(root, pkg.dir, "pyproject.toml"));
8411
8811
  if (!toml) return [];
8412
8812
  const edges = /* @__PURE__ */ new Set();
8413
8813
  const project = tomlSectionBody(toml, "project");
@@ -8427,7 +8827,7 @@ function uvEdges(root, pkg, byName) {
8427
8827
  return [...edges];
8428
8828
  }
8429
8829
  function composerEdges(root, pkg, byName, warnings) {
8430
- 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);
8431
8831
  if (!manifest) return [];
8432
8832
  const edges = /* @__PURE__ */ new Set();
8433
8833
  for (const field of ["require", "require-dev"]) {
@@ -8441,7 +8841,7 @@ function composerEdges(root, pkg, byName, warnings) {
8441
8841
  }
8442
8842
  function gradleEdges(root, pkg, byName, byDir) {
8443
8843
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8444
- const text = readText(join8(root, pkg.dir, f));
8844
+ const text = readText(join11(root, pkg.dir, f));
8445
8845
  if (!text) continue;
8446
8846
  const edges = /* @__PURE__ */ new Set();
8447
8847
  for (const m of text.matchAll(/project\s*\(\s*["']:?([^"']+)["']\s*\)/g)) {
@@ -8977,117 +9377,50 @@ var init_tests_map = __esm({
8977
9377
  ];
8978
9378
  TEST_DIR = /(^|\/)(tests?|__tests?__|spec|specs|e2e)(\/|$)/i;
8979
9379
  }
8980
- });
8981
-
8982
- // src/surprise.ts
8983
- function computeSurprises(graph) {
8984
- const commOf = /* @__PURE__ */ new Map();
8985
- const tierOf2 = /* @__PURE__ */ new Map();
8986
- for (const m of graph.modules) {
8987
- if (m.community !== void 0) commOf.set(m.slug, m.community);
8988
- tierOf2.set(m.slug, m.tier);
8989
- }
8990
- const pairCount = /* @__PURE__ */ new Map();
8991
- const pairKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
8992
- const candidates = [];
8993
- for (const e of graph.moduleEdges) {
8994
- if (e.dangling) continue;
8995
- const ca = commOf.get(e.from);
8996
- const cb = commOf.get(e.to);
8997
- if (ca === void 0 || cb === void 0 || ca === cb) continue;
8998
- pairCount.set(pairKey(ca, cb), (pairCount.get(pairKey(ca, cb)) ?? 0) + 1);
8999
- if (!DEP_KINDS.has(e.kind)) continue;
9000
- if (tierOf2.get(e.to) === 0) continue;
9001
- candidates.push({ edge: e, comms: [ca, cb] });
9002
- }
9003
- return candidates.filter((c2) => pairCount.get(pairKey(c2.comms[0], c2.comms[1])) <= MAX_PAIR_EDGES).map((c2) => ({
9004
- from: c2.edge.from,
9005
- to: c2.edge.to,
9006
- kind: c2.edge.kind,
9007
- weight: c2.edge.weight,
9008
- communities: c2.comms,
9009
- pairEdges: pairCount.get(pairKey(c2.comms[0], c2.comms[1]))
9010
- })).sort((a, b) => a.pairEdges - b.pairEdges || byStr(a.from, b.from) || byStr(a.to, b.to)).slice(0, SURPRISE_CAP);
9011
- }
9012
- function isSurprising(graph, from, to) {
9013
- const list = graph.surprises ?? computeSurprises(graph);
9014
- return list.some((s) => s.from === from && s.to === to);
9015
- }
9016
- var SURPRISE_CAP, MAX_PAIR_EDGES, DEP_KINDS;
9017
- var init_surprise = __esm({
9018
- "src/surprise.ts"() {
9019
- "use strict";
9020
- init_sort();
9021
- SURPRISE_CAP = 24;
9022
- MAX_PAIR_EDGES = 2;
9023
- DEP_KINDS = /* @__PURE__ */ new Set(["import", "call", "use"]);
9024
- }
9025
- });
9026
-
9027
- // src/render/symbols-json.ts
9028
- function computeSymbolRefs(scan2) {
9029
- const unique = uniqueSymbolDefs(scan2);
9030
- const refs = /* @__PURE__ */ new Map();
9031
- if (!unique.size) return refs;
9032
- const add = (name2, file) => {
9033
- let set = refs.get(name2);
9034
- if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
9035
- set.add(file);
9036
- };
9037
- for (const f of scan2.files) {
9038
- if (f.kind === "code" && f.idents) {
9039
- for (const id of f.idents) {
9040
- const target = unique.get(id);
9041
- if (target && target !== f.rel) add(id, f.rel);
9042
- }
9043
- } else if (f.kind === "doc") {
9044
- const content = scan2.docText.get(f.rel);
9045
- if (!content) continue;
9046
- for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
9047
- const target = unique.get(tok);
9048
- if (target && target !== f.rel) add(tok, f.rel);
9049
- }
9050
- }
9051
- }
9052
- return refs;
9053
- }
9054
- function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
9055
- const defsByName = /* @__PURE__ */ new Map();
9056
- for (const f of scan2.files) {
9057
- for (const s of f.symbols) {
9058
- let arr = defsByName.get(s.name);
9059
- if (!arr) defsByName.set(s.name, arr = []);
9060
- arr.push({
9061
- file: s.file,
9062
- line: s.line,
9063
- ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
9064
- kind: s.kind,
9065
- exported: s.exported,
9066
- lang: s.lang,
9067
- ...s.parent ? { parent: s.parent } : {}
9068
- });
9069
- }
9070
- }
9071
- const defs = {};
9072
- for (const name2 of [...defsByName.keys()].sort(byStr)) {
9073
- defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
9380
+ });
9381
+
9382
+ // src/surprise.ts
9383
+ function computeSurprises(graph) {
9384
+ const commOf = /* @__PURE__ */ new Map();
9385
+ const tierOf2 = /* @__PURE__ */ new Map();
9386
+ for (const m of graph.modules) {
9387
+ if (m.community !== void 0) commOf.set(m.slug, m.community);
9388
+ tierOf2.set(m.slug, m.tier);
9074
9389
  }
9075
- const refsOut = {};
9076
- for (const name2 of [...refs.keys()].sort(byStr)) {
9077
- const files = [...refs.get(name2)].sort(byStr);
9078
- if (files.length) refsOut[name2] = files;
9390
+ const pairCount = /* @__PURE__ */ new Map();
9391
+ const pairKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
9392
+ const candidates = [];
9393
+ for (const e of graph.moduleEdges) {
9394
+ if (e.dangling) continue;
9395
+ const ca = commOf.get(e.from);
9396
+ const cb = commOf.get(e.to);
9397
+ if (ca === void 0 || cb === void 0 || ca === cb) continue;
9398
+ pairCount.set(pairKey(ca, cb), (pairCount.get(pairKey(ca, cb)) ?? 0) + 1);
9399
+ if (!DEP_KINDS.has(e.kind)) continue;
9400
+ if (tierOf2.get(e.to) === 0) continue;
9401
+ candidates.push({ edge: e, comms: [ca, cb] });
9079
9402
  }
9080
- return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
9403
+ return candidates.filter((c2) => pairCount.get(pairKey(c2.comms[0], c2.comms[1])) <= MAX_PAIR_EDGES).map((c2) => ({
9404
+ from: c2.edge.from,
9405
+ to: c2.edge.to,
9406
+ kind: c2.edge.kind,
9407
+ weight: c2.edge.weight,
9408
+ communities: c2.comms,
9409
+ pairEdges: pairCount.get(pairKey(c2.comms[0], c2.comms[1]))
9410
+ })).sort((a, b) => a.pairEdges - b.pairEdges || byStr(a.from, b.from) || byStr(a.to, b.to)).slice(0, SURPRISE_CAP);
9081
9411
  }
9082
- function renderSymbolsJson(index) {
9083
- return JSON.stringify(index, null, 2) + "\n";
9412
+ function isSurprising(graph, from, to) {
9413
+ const list = graph.surprises ?? computeSurprises(graph);
9414
+ return list.some((s) => s.from === from && s.to === to);
9084
9415
  }
9085
- var init_symbols_json = __esm({
9086
- "src/render/symbols-json.ts"() {
9416
+ var SURPRISE_CAP, MAX_PAIR_EDGES, DEP_KINDS;
9417
+ var init_surprise = __esm({
9418
+ "src/surprise.ts"() {
9087
9419
  "use strict";
9088
- init_types();
9089
9420
  init_sort();
9090
- init_graph();
9421
+ SURPRISE_CAP = 24;
9422
+ MAX_PAIR_EDGES = 2;
9423
+ DEP_KINDS = /* @__PURE__ */ new Set(["import", "call", "use"]);
9091
9424
  }
9092
9425
  });
9093
9426
 
@@ -9110,8 +9443,10 @@ var init_graph_json = __esm({
9110
9443
 
9111
9444
  // src/pipeline.ts
9112
9445
  function buildIndexArtifacts(repo, opts = {}) {
9113
- const scan2 = scanRepo(repo, opts);
9114
- const ctx = buildResolveContext(scan2);
9446
+ return buildArtifactsFromScan(scanRepo(repo, opts), opts);
9447
+ }
9448
+ function buildArtifactsFromScan(scan2, opts = {}) {
9449
+ const ctx = resolveContextFor(scan2);
9115
9450
  const { modules, moduleOf } = buildModules(scan2);
9116
9451
  const graph = buildGraph(scan2, ctx, modules, moduleOf, opts.meta);
9117
9452
  const communities = detectCommunities(graph.modules, graph.moduleEdges, opts.previousCommunities);
@@ -9130,14 +9465,14 @@ function buildIndexArtifacts(repo, opts = {}) {
9130
9465
  }
9131
9466
  const surprises = computeSurprises(graph);
9132
9467
  if (surprises.length) graph.surprises = surprises;
9133
- const symbols = buildSymbolIndex(scan2, computeSymbolRefs(scan2));
9468
+ const symbols = buildSymbolIndex(scan2, symbolRefsFor(scan2));
9134
9469
  return { scan: scan2, graph, symbols };
9135
9470
  }
9136
9471
  var init_pipeline = __esm({
9137
9472
  "src/pipeline.ts"() {
9138
9473
  "use strict";
9139
9474
  init_scan();
9140
- init_resolve();
9475
+ init_derived();
9141
9476
  init_modules();
9142
9477
  init_graph();
9143
9478
  init_community();
@@ -9159,275 +9494,99 @@ function rgBackend(root, pattern, opts) {
9159
9494
  "--null",
9160
9495
  // path\0line:text — a `:12:` inside a filename can't corrupt parsing
9161
9496
  "--color=never",
9162
- "--no-messages",
9163
- "--hidden",
9164
- "--no-require-git",
9165
- "--no-ignore-global",
9166
- "--no-ignore-exclude",
9167
- "--no-ignore-parent",
9168
- "--no-ignore-dot",
9169
- "--max-filesize",
9170
- "1M"
9171
- ];
9172
- for (const d of IGNORE_DIRS) args2.push("--glob", `!**/${d}/**`);
9173
- for (const l of LOCKFILES) args2.push("--iglob", `!**/${l}`);
9174
- for (const ext of BINARY_EXT) args2.push("--iglob", `!**/*${ext}`);
9175
- args2.push("--glob", "!*.min.js", "--glob", "!*.min.css");
9176
- if (opts.ignoreCase) args2.push("--ignore-case");
9177
- const user = opts.globs ?? [];
9178
- const anchor = (g) => g.startsWith("/") ? g : `/${g}`;
9179
- for (const g of user.filter((g2) => !g2.startsWith("!"))) args2.push("--glob", anchor(g));
9180
- for (const g of user.filter((g2) => g2.startsWith("!"))) args2.push("--glob", `!${anchor(g.slice(1))}`);
9181
- args2.push("--regexp", pattern, "./");
9182
- const res = sh("rg", args2, { cwd: root });
9183
- if (res.missing || !res.ok && res.status !== 1) return void 0;
9184
- const hits = [];
9185
- for (const line of res.stdout.split("\n")) {
9186
- if (!line) continue;
9187
- const nul = line.indexOf("\0");
9188
- if (nul === -1) continue;
9189
- const file = line.slice(0, nul).replace(/^\.\//, "");
9190
- const rest = line.slice(nul + 1);
9191
- const colon = rest.indexOf(":");
9192
- if (colon === -1) continue;
9193
- hits.push({ file, line: Number(rest.slice(0, colon)), text: rest.slice(colon + 1) });
9194
- }
9195
- return hits;
9196
- }
9197
- function jsBackend(root, re, opts) {
9198
- const filter = compileGlobFilter(opts.globs?.map((g) => g.replace(/^(!?)\//, "$1")));
9199
- const hits = [];
9200
- for (const f of walk(root).files) {
9201
- if (filter && !filter(f.rel)) continue;
9202
- const content = readText(f.abs);
9203
- if (!content) continue;
9204
- const lines = content.split("\n");
9205
- for (let i2 = 0; i2 < lines.length; i2++) {
9206
- if (re.test(lines[i2])) hits.push({ file: f.rel, line: i2 + 1, text: lines[i2] });
9207
- }
9208
- }
9209
- return hits;
9210
- }
9211
- function grepRepo(root, pattern, opts = {}) {
9212
- const re = new RegExp(pattern, opts.ignoreCase ? "i" : "");
9213
- const max = opts.maxHits ?? DEFAULT_MAX_HITS;
9214
- let hits;
9215
- if (!opts.noRipgrep && have("rg")) hits = rgBackend(root, pattern, opts);
9216
- hits ??= jsBackend(root, re, opts);
9217
- return sortHits(hits).slice(0, max);
9218
- }
9219
- var DEFAULT_MAX_HITS;
9220
- var init_grep = __esm({
9221
- "src/grep.ts"() {
9222
- "use strict";
9223
- init_walk();
9224
- init_glob();
9225
- init_util();
9226
- init_sort();
9227
- DEFAULT_MAX_HITS = 200;
9228
- }
9229
- });
9230
-
9231
- // src/bm25.ts
9232
- function subtokens(raw) {
9233
- const folded = foldText(raw).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
9234
- const out2 = [];
9235
- const seen = /* @__PURE__ */ new Set();
9236
- const push = (t) => {
9237
- if (t.length < 2 || seen.has(t)) return;
9238
- seen.add(t);
9239
- out2.push(t);
9240
- };
9241
- if (!/\s/.test(raw.trim())) push(foldText(raw).toLowerCase().replace(/[^a-z0-9_]+/g, ""));
9242
- for (const part of folded.split(/[^A-Za-z0-9]+/)) push(part.toLowerCase());
9243
- return out2;
9244
- }
9245
- function addTerms(doc, text) {
9246
- for (const t of subtokens(text)) {
9247
- doc.tf.set(t, (doc.tf.get(t) ?? 0) + 1);
9248
- doc.len++;
9249
- }
9250
- }
9251
- function buildDocs(scan2) {
9252
- const docs = [];
9253
- for (const f of scan2.files) {
9254
- const doc = { file: f.rel, tf: /* @__PURE__ */ new Map(), len: 0, symbols: [] };
9255
- const seenSym = /* @__PURE__ */ new Set();
9256
- for (const s of f.symbols) {
9257
- addTerms(doc, s.name);
9258
- if (!seenSym.has(s.name)) {
9259
- seenSym.add(s.name);
9260
- doc.symbols.push(s.name);
9261
- }
9262
- }
9263
- for (const seg of f.rel.split("/")) addTerms(doc, seg);
9264
- for (const h of f.headings) addTerms(doc, h);
9265
- if (f.summary) addTerms(doc, f.summary);
9266
- docs.push(doc);
9267
- }
9268
- return docs;
9269
- }
9270
- function charTrigrams(term) {
9271
- const padded = `^^${term}$$`;
9272
- const grams = /* @__PURE__ */ new Set();
9273
- for (let i2 = 0; i2 + 3 <= padded.length; i2++) grams.add(padded.slice(i2, i2 + 3));
9274
- return grams;
9275
- }
9276
- function diceCoefficient(a, b) {
9277
- if (!a.size || !b.size) return 0;
9278
- let inter = 0;
9279
- for (const g of a) if (b.has(g)) inter++;
9280
- return 2 * inter / (a.size + b.size);
9281
- }
9282
- function buildTrigramIndex(docs) {
9283
- const index = /* @__PURE__ */ new Map();
9284
- for (const d of docs) {
9285
- for (const term of d.tf.keys()) {
9286
- if (!index.has(term)) index.set(term, charTrigrams(term));
9287
- }
9288
- }
9289
- return index;
9290
- }
9291
- function searchIndex(scan2, query, opts = {}) {
9292
- const terms = [];
9293
- const seen = /* @__PURE__ */ new Set();
9294
- for (const kw of keywords(query)) {
9295
- for (const t of subtokens(kw)) {
9296
- if (seen.has(t)) continue;
9297
- seen.add(t);
9298
- terms.push(t);
9299
- }
9300
- }
9301
- if (!terms.length) return [];
9302
- const docs = buildDocs(scan2);
9303
- const n = docs.length;
9304
- if (!n) return [];
9305
- let totalLen = 0;
9306
- for (const d of docs) totalLen += d.len;
9307
- const avgLen = totalLen / n || 1;
9308
- const df = /* @__PURE__ */ new Map();
9309
- for (const t of terms) {
9310
- let count = 0;
9311
- for (const d of docs) if (d.tf.has(t)) count++;
9312
- df.set(t, count);
9313
- }
9314
- const fuzzyEnabled = opts.fuzzy ?? true;
9315
- const fuzzyCandidates = /* @__PURE__ */ new Map();
9316
- if (fuzzyEnabled) {
9317
- const unmatched = terms.filter((t) => df.get(t) === 0);
9318
- if (unmatched.length) {
9319
- const trigramIndex = buildTrigramIndex(docs);
9320
- for (const t of unmatched) {
9321
- const grams = charTrigrams(t);
9322
- const candidates = [];
9323
- for (const [vocabTerm, vocabGrams] of trigramIndex) {
9324
- const dice = diceCoefficient(grams, vocabGrams);
9325
- if (dice >= FUZZY_DICE_THRESHOLD) candidates.push({ term: vocabTerm, dice });
9326
- }
9327
- candidates.sort((a, b) => b.dice - a.dice || byStr(a.term, b.term));
9328
- fuzzyCandidates.set(t, candidates.slice(0, FUZZY_CAP));
9329
- }
9330
- }
9331
- }
9332
- const vocabDf = /* @__PURE__ */ new Map();
9333
- const dfOfVocabTerm = (term) => {
9334
- const known = df.get(term) ?? vocabDf.get(term);
9335
- if (known !== void 0) return known;
9336
- let count = 0;
9337
- for (const d of docs) if (d.tf.has(term)) count++;
9338
- vocabDf.set(term, count);
9339
- return count;
9340
- };
9341
- const results = [];
9342
- for (const d of docs) {
9343
- let score = 0;
9344
- const matched = [];
9345
- const symbolTerms = /* @__PURE__ */ new Set();
9346
- const fuzzyHit = /* @__PURE__ */ new Set();
9347
- for (const t of terms) {
9348
- const tf = d.tf.get(t);
9349
- if (tf) {
9350
- matched.push(t);
9351
- symbolTerms.add(t);
9352
- const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
9353
- score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
9354
- continue;
9355
- }
9356
- const candidates = fuzzyCandidates.get(t);
9357
- if (!candidates) continue;
9358
- for (const cand of candidates) {
9359
- const ctf = d.tf.get(cand.term);
9360
- if (!ctf) continue;
9361
- const cdf = dfOfVocabTerm(cand.term);
9362
- const idf = Math.log(1 + (n - cdf + 0.5) / (cdf + 0.5));
9363
- const contribution = idf * (ctf * (K1 + 1)) / (ctf + K1 * (1 - B + B * d.len / avgLen));
9364
- score += contribution * cand.dice;
9365
- symbolTerms.add(cand.term);
9366
- fuzzyHit.add(t);
9367
- }
9497
+ "--no-messages",
9498
+ "--hidden",
9499
+ "--no-require-git",
9500
+ "--no-ignore-global",
9501
+ "--no-ignore-exclude",
9502
+ "--no-ignore-parent",
9503
+ "--no-ignore-dot",
9504
+ "--max-filesize",
9505
+ "1M"
9506
+ ];
9507
+ for (const d of IGNORE_DIRS) args2.push("--glob", `!**/${d}/**`);
9508
+ for (const l of LOCKFILES) args2.push("--iglob", `!**/${l}`);
9509
+ for (const ext of BINARY_EXT) args2.push("--iglob", `!**/*${ext}`);
9510
+ args2.push("--glob", "!*.min.js", "--glob", "!*.min.css");
9511
+ if (opts.ignoreCase) args2.push("--ignore-case");
9512
+ const user = opts.globs ?? [];
9513
+ const anchor = (g) => g.startsWith("/") ? g : `/${g}`;
9514
+ for (const g of user.filter((g2) => !g2.startsWith("!"))) args2.push("--glob", anchor(g));
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) });
9529
+ }
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] });
9368
9542
  }
9369
- if (!matched.length && !fuzzyHit.size) continue;
9370
- const scored = d.symbols.map((name2) => {
9371
- const toks = new Set(subtokens(name2));
9372
- let hits = 0;
9373
- for (const t of symbolTerms) if (toks.has(t)) hits++;
9374
- return { name: name2, hits };
9375
- }).filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits || byStr(a.name, b.name));
9376
- const result = {
9377
- file: d.file,
9378
- score: Number(score.toFixed(4)),
9379
- matchedTerms: matched.sort(byStr),
9380
- topSymbols: scored.slice(0, TOP_SYMBOLS).map((s) => s.name)
9381
- };
9382
- if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
9383
- results.push(result);
9384
9543
  }
9385
- results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
9386
- return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
9544
+ return hits;
9387
9545
  }
9388
- var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP;
9389
- var init_bm25 = __esm({
9390
- "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"() {
9391
9557
  "use strict";
9558
+ init_walk();
9559
+ init_glob();
9392
9560
  init_util();
9393
9561
  init_sort();
9394
- K1 = 1.2;
9395
- B = 0.75;
9396
- DEFAULT_LIMIT = 20;
9397
- TOP_SYMBOLS = 5;
9398
- FUZZY_DICE_THRESHOLD = 0.6;
9399
- FUZZY_CAP = 3;
9562
+ DEFAULT_MAX_HITS = 200;
9400
9563
  }
9401
9564
  });
9402
9565
 
9403
9566
  // src/embed/model.ts
9404
- import { createHash as createHash2 } from "crypto";
9567
+ import { createHash as createHash3 } from "crypto";
9405
9568
  import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9406
- import { join as join10 } from "path";
9569
+ import { join as join13 } from "path";
9407
9570
  function resolveEmbedModelDir(repo) {
9408
9571
  const env = process.env.CODEINDEX_EMBED_DIR;
9409
9572
  const candidates = [];
9410
9573
  if (env) candidates.push(env);
9411
- if (repo) candidates.push(join10(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9412
- 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));
9413
9576
  for (const c2 of candidates) {
9414
- if (existsSync3(join10(c2, "model.json"))) return c2;
9577
+ if (existsSync3(join13(c2, "model.json"))) return c2;
9415
9578
  }
9416
9579
  return void 0;
9417
9580
  }
9418
9581
  function hasEmbedModel(repo) {
9419
9582
  return resolveEmbedModelDir(repo) !== void 0;
9420
9583
  }
9421
- function loadEmbedModel(dir) {
9422
- if (!dir) return void 0;
9423
- const path = join10(dir, "model.json");
9424
- if (!existsSync3(path)) return void 0;
9425
- const raw = JSON.parse(readFileSync5(path, "utf8"));
9426
- const { modelId, dim, vocab, weights } = raw;
9427
- if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${path}`);
9428
- if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${path}`);
9584
+ function parseEmbedModel(raw, source) {
9585
+ const { modelId, dim, vocab, weights, unk: rawUnk } = raw ?? {};
9586
+ if (typeof modelId !== "string" || !modelId) throw new Error(`embed model: missing modelId in ${source}`);
9587
+ if (!Number.isInteger(dim) || dim <= 0) throw new Error(`embed model: bad dim ${dim} in ${source}`);
9429
9588
  if (!Array.isArray(vocab) || !Array.isArray(weights) || vocab.length !== weights.length) {
9430
- throw new Error(`embed model: vocab/weights length mismatch in ${path}`);
9589
+ throw new Error(`embed model: vocab/weights length mismatch in ${source}`);
9431
9590
  }
9432
9591
  const vocabSize = vocab.length;
9433
9592
  const flat = new Float64Array(vocabSize * dim);
@@ -9442,10 +9601,17 @@ function loadEmbedModel(dir) {
9442
9601
  }
9443
9602
  for (let d = 0; d < dim; d++) flat[i2 * dim + d] = Number(row[d]);
9444
9603
  }
9445
- const unk = typeof raw.unk === "string" ? raw.unk : "[UNK]";
9604
+ const unk = typeof rawUnk === "string" ? rawUnk : "[UNK]";
9446
9605
  const unkId = vmap.has(unk) ? vmap.get(unk) : -1;
9447
9606
  return { modelId, dim, unk, unkId, vocabSize, vocab: vmap, weights: flat };
9448
9607
  }
9608
+ function loadEmbedModel(dir) {
9609
+ if (!dir) return void 0;
9610
+ const path = join13(dir, "model.json");
9611
+ if (!existsSync3(path)) return void 0;
9612
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
9613
+ return parseEmbedModel(raw, path);
9614
+ }
9449
9615
  function resolveEmbedPullUrl() {
9450
9616
  const env = process.env.CODEINDEX_EMBED_URL;
9451
9617
  if (env && env.trim()) return { url: env.trim() };
@@ -9456,7 +9622,7 @@ async function fetchEmbedModel(url, expectedSha256) {
9456
9622
  if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9457
9623
  const body2 = await res.text();
9458
9624
  if (expectedSha256) {
9459
- const got = createHash2("sha256").update(body2).digest("hex");
9625
+ const got = createHash3("sha256").update(body2).digest("hex");
9460
9626
  if (got !== expectedSha256) {
9461
9627
  throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
9462
9628
  }
@@ -10060,8 +10226,8 @@ var init_repomap = __esm({
10060
10226
 
10061
10227
  // src/deadcode.ts
10062
10228
  function findDeadCode(scan2) {
10063
- const callers = buildCallerIndex(scan2);
10064
- const refs = computeSymbolRefs(scan2);
10229
+ const callers = callerIndexFor(scan2);
10230
+ const refs = symbolRefsFor(scan2);
10065
10231
  const out2 = [];
10066
10232
  const consider = (s) => s.exported && !REFERENCE_KINDS6.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
10067
10233
  for (const f of scan2.files) {
@@ -10080,8 +10246,7 @@ var REFERENCE_KINDS6, ENTRYPOINT_RE;
10080
10246
  var init_deadcode = __esm({
10081
10247
  "src/deadcode.ts"() {
10082
10248
  "use strict";
10083
- init_callers();
10084
- init_symbols_json();
10249
+ init_derived();
10085
10250
  init_tests_map();
10086
10251
  init_sort();
10087
10252
  REFERENCE_KINDS6 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
@@ -10089,49 +10254,6 @@ var init_deadcode = __esm({
10089
10254
  }
10090
10255
  });
10091
10256
 
10092
- // src/complexity.ts
10093
- import { join as join11 } from "path";
10094
- function complexityOfSource(source) {
10095
- return 1 + (source.match(BRANCH_RE) ?? []).length;
10096
- }
10097
- function symbolComplexity(scan2, rel, top = 50) {
10098
- const out2 = [];
10099
- for (const f of scan2.files) {
10100
- if (f.kind !== "code") continue;
10101
- if (rel && f.rel !== rel) continue;
10102
- if (!f.symbols.length) continue;
10103
- const lines = readText(join11(scan2.root, f.rel)).split("\n");
10104
- for (const s of f.symbols) {
10105
- if (s.kind === "reexport" || s.kind === "reexport-all") continue;
10106
- const end = s.endLine ?? s.line;
10107
- const body2 = lines.slice(s.line - 1, end).join("\n");
10108
- const entry = { file: f.rel, name: s.name, line: s.line, complexity: complexityOfSource(body2) };
10109
- if (s.endLine !== void 0) entry.endLine = s.endLine;
10110
- out2.push(entry);
10111
- }
10112
- }
10113
- out2.sort((a, b) => b.complexity - a.complexity || byStr(a.file, b.file) || a.line - b.line);
10114
- return out2.slice(0, top);
10115
- }
10116
- function riskHotspots(scan2, churn, top = 20) {
10117
- const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
10118
- const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
10119
- const commits = churn.get(f.rel) ?? 0;
10120
- return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
10121
- });
10122
- out2.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
10123
- return out2.slice(0, top);
10124
- }
10125
- var BRANCH_RE;
10126
- var init_complexity = __esm({
10127
- "src/complexity.ts"() {
10128
- "use strict";
10129
- init_walk();
10130
- init_sort();
10131
- BRANCH_RE = /\b(if|elif|elsif|else\s+if|for|foreach|while|until|unless|case|when|match|catch|rescue|except)\b|&&|\|\||(?<![?:])\?(?![?.:])/g;
10132
- }
10133
- });
10134
-
10135
10257
  // src/viz.ts
10136
10258
  function renderMermaid(graph, opts = {}) {
10137
10259
  const maxEdges = opts.maxEdges ?? 80;
@@ -10172,10 +10294,17 @@ var init_viz = __esm({
10172
10294
  // src/mcp.ts
10173
10295
  var mcp_exports = {};
10174
10296
  __export(mcp_exports, {
10297
+ getArtifacts: () => getArtifacts,
10298
+ getScan: () => getScan,
10299
+ memoizedEmbedModel: () => memoizedEmbedModel,
10175
10300
  memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
10176
10301
  runMcpServer: () => runMcpServer,
10177
- scanFingerprint: () => scanFingerprint
10302
+ scanFingerprint: () => scanFingerprint,
10303
+ toCacheMap: () => toCacheMap,
10304
+ warmGrammarsForRepo: () => warmGrammarsForRepo
10178
10305
  });
10306
+ import { statSync as statSync4 } from "fs";
10307
+ import { join as join14 } from "path";
10179
10308
  import { createInterface } from "readline";
10180
10309
  function str(v) {
10181
10310
  return typeof v === "string" && v ? v : void 0;
@@ -10196,12 +10325,72 @@ async function memoizedEmbeddingIndex(key, build) {
10196
10325
  embeddingIndexCache = { key: cacheKey, index };
10197
10326
  return index;
10198
10327
  }
10328
+ function memoizedEmbedModel(modelDir) {
10329
+ let stat;
10330
+ try {
10331
+ stat = statSync4(join14(modelDir, "model.json"));
10332
+ } catch {
10333
+ return void 0;
10334
+ }
10335
+ const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
10336
+ if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
10337
+ const model = loadEmbedModel(modelDir);
10338
+ if (model) embedModelCache = { key, model };
10339
+ return model;
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
+ }
10199
10387
  async function callTool(name2, args2) {
10200
10388
  const repo = str(args2.repo);
10201
10389
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10202
10390
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
10391
+ if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
10203
10392
  if (name2 === "scan_summary") {
10204
- const scan2 = scanRepo(repo, scanOpts);
10393
+ const scan2 = getScan(repo, scanOpts);
10205
10394
  return JSON.stringify(
10206
10395
  { engineVersion: ENGINE_VERSION, commit: scan2.commit, fileCount: scan2.files.length, languages: scan2.languages, capped: scan2.capped },
10207
10396
  null,
@@ -10209,10 +10398,10 @@ async function callTool(name2, args2) {
10209
10398
  );
10210
10399
  }
10211
10400
  if (name2 === "graph") {
10212
- return renderGraphJson(buildIndexArtifacts(repo, scanOpts).graph);
10401
+ return renderGraphJson(getArtifacts(repo, scanOpts).graph);
10213
10402
  }
10214
10403
  if (name2 === "symbols") {
10215
- const { symbols } = buildIndexArtifacts(repo, scanOpts);
10404
+ const { symbols } = getArtifacts(repo, scanOpts);
10216
10405
  const lookup = str(args2.name);
10217
10406
  if (lookup) {
10218
10407
  return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
@@ -10220,7 +10409,7 @@ async function callTool(name2, args2) {
10220
10409
  return JSON.stringify(symbols, null, 2);
10221
10410
  }
10222
10411
  if (name2 === "callers") {
10223
- const index = buildCallerIndex(scanRepo(repo, scanOpts));
10412
+ const index = buildCallerIndex(getScan(repo, scanOpts));
10224
10413
  const lookup = str(args2.name);
10225
10414
  if (lookup) {
10226
10415
  const entry = index.get(lookup);
@@ -10243,12 +10432,12 @@ async function callTool(name2, args2) {
10243
10432
  if (name2 === "symbols_overview") {
10244
10433
  const file = str(args2.file);
10245
10434
  if (!file) throw new Error("`file` is required");
10246
- return JSON.stringify(symbolsOverview(scanRepo(repo, scanOpts), file), null, 2);
10435
+ return JSON.stringify(symbolsOverview(getScan(repo, scanOpts), file), null, 2);
10247
10436
  }
10248
10437
  if (name2 === "find_symbol") {
10249
10438
  const namePath = str(args2.namePath);
10250
10439
  if (!namePath) throw new Error("`namePath` is required");
10251
- const matches = findSymbol(scanRepo(repo, scanOpts), namePath, {
10440
+ const matches = findSymbol(getScan(repo, scanOpts), namePath, {
10252
10441
  substring: args2.substring === true,
10253
10442
  includeBody: args2.includeBody === true
10254
10443
  });
@@ -10257,15 +10446,17 @@ async function callTool(name2, args2) {
10257
10446
  if (name2 === "find_references") {
10258
10447
  const symName = str(args2.name);
10259
10448
  if (!symName) throw new Error("`name` is required");
10260
- return JSON.stringify(findReferences(scanRepo(repo, scanOpts), symName), null, 2);
10449
+ return JSON.stringify(findReferences(getScan(repo, scanOpts), symName), null, 2);
10261
10450
  }
10262
10451
  if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
10263
10452
  const namePath = str(args2.namePath);
10264
10453
  const body2 = typeof args2.body === "string" ? args2.body : void 0;
10265
10454
  if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
10266
- const scan2 = scanRepo(repo, scanOpts);
10455
+ const scan2 = getScan(repo, scanOpts);
10267
10456
  const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
10268
- 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);
10269
10460
  }
10270
10461
  if (name2 === "write_memory") {
10271
10462
  const memName = str(args2.name);
@@ -10289,10 +10480,10 @@ async function callTool(name2, args2) {
10289
10480
  return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
10290
10481
  }
10291
10482
  if (name2 === "dead_code") {
10292
- return JSON.stringify(findDeadCode(scanRepo(repo, scanOpts)), null, 2);
10483
+ return JSON.stringify(findDeadCode(getScan(repo, scanOpts)), null, 2);
10293
10484
  }
10294
10485
  if (name2 === "complexity") {
10295
- const scan2 = scanRepo(repo, scanOpts);
10486
+ const scan2 = getScan(repo, scanOpts);
10296
10487
  if (args2.risk === true) {
10297
10488
  const { churn, ok } = gitChurn(repo);
10298
10489
  return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2);
@@ -10300,15 +10491,15 @@ async function callTool(name2, args2) {
10300
10491
  return JSON.stringify(symbolComplexity(scan2, str(args2.file)), null, 2);
10301
10492
  }
10302
10493
  if (name2 === "mermaid") {
10303
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10494
+ const { graph } = getArtifacts(repo, scanOpts);
10304
10495
  return renderMermaid(graph, { module: str(args2.module) });
10305
10496
  }
10306
10497
  if (name2 === "repo_map") {
10307
- const { scan: scan2, graph } = buildIndexArtifacts(repo, scanOpts);
10498
+ const { scan: scan2, graph } = getArtifacts(repo, scanOpts);
10308
10499
  return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
10309
10500
  }
10310
10501
  if (name2 === "hotspots") {
10311
- const scan2 = scanRepo(repo, scanOpts);
10502
+ const scan2 = getScan(repo, scanOpts);
10312
10503
  const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10313
10504
  return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
10314
10505
  }
@@ -10329,7 +10520,7 @@ async function callTool(name2, args2) {
10329
10520
  if (name2 === "search") {
10330
10521
  const query = str(args2.query);
10331
10522
  if (!query) throw new Error("`query` is required");
10332
- const scan2 = scanRepo(repo, scanOpts);
10523
+ const scan2 = getScan(repo, scanOpts);
10333
10524
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10334
10525
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10335
10526
  if (args2.semantic === true) {
@@ -10350,7 +10541,7 @@ async function callTool(name2, args2) {
10350
10541
  }
10351
10542
  }
10352
10543
  const modelDir = resolveEmbedModelDir(repo);
10353
- const model = modelDir ? loadEmbedModel(modelDir) : void 0;
10544
+ const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
10354
10545
  if (model) {
10355
10546
  const index = await memoizedEmbeddingIndex(
10356
10547
  { mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
@@ -10370,7 +10561,7 @@ async function callTool(name2, args2) {
10370
10561
  }
10371
10562
  if (name2 === "embed_status") {
10372
10563
  const modelDir = resolveEmbedModelDir(repo);
10373
- const model = modelDir ? loadEmbedModel(modelDir) : void 0;
10564
+ const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
10374
10565
  const endpoint = resolveEmbedEndpoint();
10375
10566
  const mode = endpoint ? "endpoint" : model ? "static" : "none";
10376
10567
  const status = {
@@ -10384,13 +10575,16 @@ async function callTool(name2, args2) {
10384
10575
  }
10385
10576
  if (name2 === "check_rules") {
10386
10577
  const rules = parseRules(args2.rules);
10387
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10578
+ const { graph } = getArtifacts(repo, scanOpts);
10388
10579
  return JSON.stringify(checkRules(graph, rules), null, 2);
10389
10580
  }
10390
10581
  throw new Error(`unknown tool: ${name2}`);
10391
10582
  }
10392
- async function runMcpServer() {
10393
- 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
+ };
10394
10588
  const send = (msg) => {
10395
10589
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
10396
10590
  };
@@ -10417,7 +10611,7 @@ async function runMcpServer() {
10417
10611
  result: {
10418
10612
  protocolVersion: "2024-11-05",
10419
10613
  capabilities: { tools: {} },
10420
- serverInfo: { name: "codeindex", version: ENGINE_VERSION }
10614
+ serverInfo
10421
10615
  }
10422
10616
  });
10423
10617
  } else if (req.method === "ping") {
@@ -10445,7 +10639,7 @@ async function runMcpServer() {
10445
10639
  }
10446
10640
  }
10447
10641
  }
10448
- var repoProp, scopeProps, TOOLS, embeddingIndexCache;
10642
+ var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, sessionCache, SCANLESS_TOOLS;
10449
10643
  var init_mcp = __esm({
10450
10644
  "src/mcp.ts"() {
10451
10645
  "use strict";
@@ -10454,6 +10648,7 @@ var init_mcp = __esm({
10454
10648
  init_pipeline();
10455
10649
  init_graph_json();
10456
10650
  init_scan();
10651
+ init_walk();
10457
10652
  init_callers();
10458
10653
  init_workspaces();
10459
10654
  init_git();
@@ -10724,6 +10919,17 @@ var init_mcp = __esm({
10724
10919
  }
10725
10920
  }
10726
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
+ ]);
10727
10933
  }
10728
10934
  });
10729
10935
 
@@ -10863,6 +11069,106 @@ init_code();
10863
11069
  init_markdown();
10864
11070
  init_loader();
10865
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
10866
11172
  init_resolve();
10867
11173
  init_modules();
10868
11174
  init_graph();
@@ -10883,7 +11189,7 @@ init_graph_json();
10883
11189
  init_types();
10884
11190
  init_walk();
10885
11191
  init_sort();
10886
- import { join as join9 } from "path";
11192
+ import { join as join12 } from "path";
10887
11193
  var utf8 = new TextEncoder();
10888
11194
  function pushVarint(out2, n) {
10889
11195
  if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
@@ -11053,7 +11359,7 @@ function renderScip(scan2, opts = {}) {
11053
11359
  };
11054
11360
  const documents = [];
11055
11361
  for (const f of docs) {
11056
- const text = readText(join9(scan2.root, f.rel));
11362
+ const text = readText(join12(scan2.root, f.rel));
11057
11363
  const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
11058
11364
  const locate = (lineNo, name2) => {
11059
11365
  const line = lines[lineNo - 1];
@@ -11144,12 +11450,14 @@ init_util();
11144
11450
  init_types();
11145
11451
  init_types();
11146
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";
11147
11455
  init_pipeline();
11456
+ init_hash();
11148
11457
  init_graph_json();
11149
11458
  init_symbols_json();
11150
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
11151
- import { join as join12, resolve } from "path";
11152
11459
  init_scan();
11460
+ init_walk();
11153
11461
  init_callers();
11154
11462
  init_workspaces();
11155
11463
  init_git();
@@ -11196,6 +11504,14 @@ Commands:
11196
11504
  the source with CODEINDEX_EMBED_URL
11197
11505
  embed serve Print (or --run) the docker command that starts the
11198
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
11199
11515
  rules Architecture rules (forbidden edges, cycles, orphans) validated
11200
11516
  against the link-graph: --config <codeindex.rules.json>; exits 1
11201
11517
  on any error-severity violation (a CI gate)
@@ -11216,8 +11532,11 @@ Flags:
11216
11532
  --exclude <glob> Exclude matching paths (repeatable)
11217
11533
  --scope <dir> Restrict to one directory (sugar for --include '<dir>/**')
11218
11534
  --no-gitignore Do not honor .gitignore files (default: honored)
11535
+ --ignore-dir <name> Directory names to skip (repeatable) \u2014 REPLACES the
11536
+ default ignored-directory set, never merges with it
11219
11537
  --max-files <n> Cap walked files (default 20000)
11220
11538
  --max-bytes <n> Skip files above this size (default 1 MiB)
11539
+ --max-calls <n> Per-file call-site cap for extraction (default 512)
11221
11540
  --no-ast Skip tree-sitter grammars even when present (regex tier)
11222
11541
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
11223
11542
  --limit <n> Max results for \`search\` (default 20)
@@ -11232,7 +11551,7 @@ Flags:
11232
11551
  each site corroborated|unique-name
11233
11552
  `;
11234
11553
  function parseFlags(args2) {
11235
- const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, noAst: false, fuzzy: true, semantic: false };
11554
+ const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
11236
11555
  for (let i2 = 0; i2 < args2.length; i2++) {
11237
11556
  const a = args2[i2];
11238
11557
  const next = () => {
@@ -11246,23 +11565,25 @@ function parseFlags(args2) {
11246
11565
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
11247
11566
  return n;
11248
11567
  };
11249
- if (a === "--repo") flags2.repo = resolve(next());
11568
+ if (a === "--repo") flags2.repo = resolve2(next());
11250
11569
  else if (a === "--out") {
11251
11570
  const v = next();
11252
- flags2.out = v === "-" ? "-" : resolve(v);
11571
+ flags2.out = v === "-" ? "-" : resolve2(v);
11253
11572
  } else if (a === "--project-root") flags2.projectRoot = next();
11254
11573
  else if (a === "--include") flags2.include.push(next());
11255
11574
  else if (a === "--exclude") flags2.exclude.push(next());
11256
11575
  else if (a === "--scope") flags2.scope = next();
11257
11576
  else if (a === "--no-gitignore") flags2.gitignore = false;
11577
+ else if (a === "--ignore-dir") flags2.ignoreDirs.push(next());
11258
11578
  else if (a === "--max-files") flags2.maxFiles = num();
11259
11579
  else if (a === "--max-bytes") flags2.maxBytes = num();
11580
+ else if (a === "--max-calls") flags2.maxCalls = num();
11260
11581
  else if (a === "--ignore-case") flags2.ignoreCase = true;
11261
11582
  else if (a === "--max-hits") flags2.maxHits = num();
11262
11583
  else if (a === "--budget-tokens") flags2.budgetTokens = num();
11263
11584
  else if (a === "--no-ast") flags2.noAst = true;
11264
11585
  else if (a === "--since") flags2.since = next();
11265
- else if (a === "--config") flags2.config = resolve(next());
11586
+ else if (a === "--config") flags2.config = resolve2(next());
11266
11587
  else if (a === "--limit") flags2.limit = num();
11267
11588
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11268
11589
  else if (a === "--semantic") flags2.semantic = true;
@@ -11274,19 +11595,26 @@ function parseFlags(args2) {
11274
11595
  return flags2;
11275
11596
  }
11276
11597
  function emit(content, out2) {
11277
- if (out2) writeFileSync3(out2, content);
11598
+ if (out2) writeFileSync4(out2, content);
11278
11599
  else process.stdout.write(content);
11279
11600
  }
11280
- function scanOptions(flags2) {
11601
+ function scanOptions(flags2, precomputedWalk) {
11281
11602
  return {
11282
11603
  include: flags2.include.length ? flags2.include : void 0,
11283
11604
  exclude: flags2.exclude.length ? flags2.exclude : void 0,
11284
11605
  scope: flags2.scope,
11285
11606
  gitignore: flags2.gitignore,
11607
+ ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0,
11286
11608
  maxFiles: flags2.maxFiles,
11287
- maxBytes: flags2.maxBytes
11609
+ maxBytes: flags2.maxBytes,
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
11288
11615
  };
11289
11616
  }
11617
+ var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
11290
11618
  async function runCli(argv) {
11291
11619
  const [cmd, ...rest] = argv;
11292
11620
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
@@ -11304,46 +11632,102 @@ async function runCli(argv) {
11304
11632
  }
11305
11633
  const flags2 = parseFlags(rest);
11306
11634
  if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11307
- 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
+ }
11308
11646
  if (cmd === "index") {
11309
11647
  if (!flags2.out) throw new Error("index needs --out <dir>");
11310
11648
  const outDir = flags2.out;
11311
- mkdirSync2(outDir, { recursive: true });
11312
- const cachePath = join12(outDir, "cache.json");
11649
+ mkdirSync3(outDir, { recursive: true });
11650
+ const cachePath = join15(outDir, "cache.json");
11313
11651
  let cache;
11652
+ let meta = {};
11314
11653
  try {
11315
11654
  const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
11316
11655
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
11317
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
+ };
11318
11664
  }
11319
11665
  } catch {
11320
11666
  }
11321
- const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
11322
- writeFileSync3(join12(outDir, "graph.json"), renderGraphJson(graph));
11323
- writeFileSync3(join12(outDir, "symbols.json"), renderSymbolsJson(symbols));
11324
- const files = {};
11325
- for (const f of scan2.files) {
11326
- const entry = { hash: f.hash, record: f, size: f.size };
11327
- const mtime = scan2.mtimes.get(f.rel);
11328
- if (mtime !== void 0) entry.mtimeMs = mtime;
11329
- files[f.rel] = entry;
11330
- }
11331
- writeFileSync3(
11332
- cachePath,
11333
- JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
11334
- );
11335
- let embedNote = "";
11667
+ const scan2 = scanRepo(flags2.repo, { ...scanOptions(flags2, precomputedWalk), cache, out: outDir });
11336
11668
  const modelDir = resolveEmbedModelDir(flags2.repo);
11337
11669
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11338
- if (model) {
11339
- const index = buildEmbeddingIndex(scan2, model);
11340
- writeFileSync3(join12(outDir, "embeddings.bin"), serializeEmbeddings(index));
11341
- embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
11342
- }
11343
- 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)" : ""}
11344
11727
  `);
11728
+ }
11345
11729
  } else if (cmd === "scan") {
11346
- const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11730
+ const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11347
11731
  const summary = {
11348
11732
  engineVersion: ENGINE_VERSION,
11349
11733
  commit: scan2.commit,
@@ -11353,30 +11737,30 @@ async function runCli(argv) {
11353
11737
  };
11354
11738
  emit(JSON.stringify(summary, null, 2) + "\n", flags2.out);
11355
11739
  } else if (cmd === "graph") {
11356
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11740
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11357
11741
  emit(renderGraphJson(graph), flags2.out);
11358
11742
  } else if (cmd === "symbols") {
11359
- const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11743
+ const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11360
11744
  emit(renderSymbolsJson(symbols), flags2.out);
11361
11745
  } else if (cmd === "scip") {
11362
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11746
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11363
11747
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
11364
- const out2 = flags2.out ?? resolve("index.scip");
11748
+ const out2 = flags2.out ?? resolve2("index.scip");
11365
11749
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
11366
11750
  else {
11367
- writeFileSync3(out2, bytes);
11751
+ writeFileSync4(out2, bytes);
11368
11752
  process.stderr.write(`codeindex: SCIP index \u2192 ${out2} (${bytes.length} bytes)
11369
11753
  `);
11370
11754
  }
11371
11755
  } else if (cmd === "callers") {
11372
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11756
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11373
11757
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
11374
11758
  const obj = {};
11375
11759
  for (const [name2, entry] of index) obj[name2] = entry;
11376
11760
  emit(JSON.stringify(obj, null, 2) + "\n", flags2.out);
11377
11761
  } else if (cmd === "search") {
11378
11762
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
11379
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11763
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11380
11764
  if (flags2.semantic) {
11381
11765
  const endpoint = resolveEmbedEndpoint();
11382
11766
  const lexical = () => {
@@ -11466,17 +11850,17 @@ async function runCli(argv) {
11466
11850
  return;
11467
11851
  }
11468
11852
  const model = loadEmbedModel(modelDir);
11469
- mkdirSync2(flags2.out, { recursive: true });
11470
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11853
+ mkdirSync3(flags2.out, { recursive: true });
11854
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11471
11855
  const index = buildEmbeddingIndex(scan2, model);
11472
- writeFileSync3(join12(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11856
+ writeFileSync4(join15(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11473
11857
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
11474
11858
  `);
11475
11859
  } else if (sub === "pull") {
11476
11860
  const { url, sha256 } = resolveEmbedPullUrl();
11477
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join12(flags2.repo, ".codeindex", "models");
11478
- mkdirSync2(destDir, { recursive: true });
11479
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join12(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")}
11480
11864
  `);
11481
11865
  let body2;
11482
11866
  try {
@@ -11488,22 +11872,111 @@ async function runCli(argv) {
11488
11872
  return;
11489
11873
  }
11490
11874
  try {
11491
- JSON.parse(body2);
11492
- } catch {
11493
- process.stderr.write("codeindex: pull failed \u2014 response is not a valid model.json (expected JSON)\n");
11875
+ parseEmbedModel(JSON.parse(body2), url);
11876
+ } catch (e) {
11877
+ process.stderr.write(
11878
+ `codeindex: pull failed \u2014 response is not a valid model.json (${e instanceof Error ? e.message : String(e)}) (nothing written)
11879
+ `
11880
+ );
11494
11881
  process.exitCode = 1;
11495
11882
  return;
11496
11883
  }
11497
- writeFileSync3(join12(destDir, "model.json"), body2);
11498
- process.stderr.write(`codeindex: model written to ${join12(destDir, "model.json")}
11884
+ writeFileSync4(join15(destDir, "model.json"), body2);
11885
+ process.stderr.write(`codeindex: model written to ${join15(destDir, "model.json")}
11499
11886
  `);
11500
11887
  } else {
11501
11888
  throw new Error("embed needs a subcommand: status | build | pull | serve");
11502
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
+ }
11503
11976
  } else if (cmd === "rules") {
11504
11977
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
11505
11978
  const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
11506
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11979
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11507
11980
  const violations = checkRules(graph, rules);
11508
11981
  const errors = violations.filter((v) => v.severity === "error").length;
11509
11982
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags2.out);
@@ -11524,26 +11997,26 @@ async function runCli(argv) {
11524
11997
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
11525
11998
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags2.out);
11526
11999
  } else if (cmd === "repomap") {
11527
- const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12000
+ const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11528
12001
  emit(renderRepoMap(scan2, graph, { budgetTokens: flags2.budgetTokens }), flags2.out);
11529
12002
  } else if (cmd === "hotspots") {
11530
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12003
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11531
12004
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11532
12005
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11533
12006
  } else if (cmd === "coupling") {
11534
12007
  const { ok, couplings } = changeCoupling(flags2.repo, { since: flags2.since });
11535
12008
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
11536
12009
  } else if (cmd === "deadcode") {
11537
- 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);
11538
12011
  } else if (cmd === "complexity") {
11539
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12012
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11540
12013
  emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
11541
12014
  } else if (cmd === "risk") {
11542
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12015
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11543
12016
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11544
12017
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11545
12018
  } else if (cmd === "mermaid") {
11546
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12019
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11547
12020
  emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
11548
12021
  } else if (cmd === "grep") {
11549
12022
  if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
@@ -11562,6 +12035,7 @@ ${HELP}`);
11562
12035
  }
11563
12036
  }
11564
12037
  export {
12038
+ DEFAULT_GRAMMARS_URL,
11565
12039
  DEFAULT_MAX_FILES,
11566
12040
  EMBED_VERSION,
11567
12041
  ENGINE_VERSION,
@@ -11572,6 +12046,7 @@ export {
11572
12046
  applyCentrality,
11573
12047
  basicTokenize,
11574
12048
  betweennessOf,
12049
+ buildArtifactsFromScan,
11575
12050
  buildCallerIndex,
11576
12051
  buildEmbeddingIndex,
11577
12052
  buildEndpointIndex,
@@ -11614,14 +12089,19 @@ export {
11614
12089
  extToLang,
11615
12090
  extractAst,
11616
12091
  extractCode,
12092
+ extractGrammarsTarball,
11617
12093
  extractMarkdown,
11618
12094
  extractSymbols,
12095
+ extractTarInto,
12096
+ fetchExpectedSha256,
12097
+ fetchGrammarsTarball,
11619
12098
  findDeadCode,
11620
12099
  findReferences,
11621
12100
  findSymbol,
11622
12101
  foldText,
11623
12102
  gitChurn,
11624
12103
  grammarKeyForExt,
12104
+ grammarKeysForExts,
11625
12105
  grammarReady,
11626
12106
  grepRepo,
11627
12107
  hasEmbedModel,
@@ -11663,6 +12143,9 @@ export {
11663
12143
  resolveEmbedEndpoint,
11664
12144
  resolveEmbedModelDir,
11665
12145
  resolveEmbedPullUrl,
12146
+ resolveGrammarsDir,
12147
+ resolveGrammarsPullTarget,
12148
+ resolveGrammarsTier,
11666
12149
  resolveImport,
11667
12150
  resolveUniqueSymbol,
11668
12151
  riskHotspots,
@@ -11676,6 +12159,7 @@ export {
11676
12159
  serializeEmbeddings,
11677
12160
  sh,
11678
12161
  sha1,
12162
+ sharedGrammarsCacheDir,
11679
12163
  shortHash,
11680
12164
  slugify,
11681
12165
  subtokens,