@maxgfr/codeindex 2.13.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
14
14
  var init_types = __esm({
15
15
  "src/types.ts"() {
16
16
  "use strict";
17
- ENGINE_VERSION = "2.13.0";
17
+ ENGINE_VERSION = "2.15.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 10;
20
20
  }
@@ -348,23 +348,26 @@ function walk(root, opts = {}) {
348
348
  if (!contained(real)) continue;
349
349
  let entries;
350
350
  try {
351
- entries = readdirSync(frame.dir).sort();
351
+ entries = readdirSync(frame.dir, { withFileTypes: true }).sort(
352
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
353
+ );
352
354
  } catch {
353
355
  continue;
354
356
  }
355
357
  let rules = frame.rules;
356
- if (useGitignore && entries.includes(".gitignore")) {
358
+ if (useGitignore && entries.some((e) => e.name === ".gitignore")) {
357
359
  const parsed = parseGitignore(readText(join(frame.dir, ".gitignore")), frame.rel);
358
360
  if (parsed.length) rules = [...rules, ...parsed];
359
361
  }
360
- for (const name2 of entries) {
362
+ for (const entry of entries) {
363
+ const name2 = entry.name;
361
364
  const abs = join(frame.dir, name2);
362
365
  const rel = frame.rel ? `${frame.rel}/${name2}` : name2;
366
+ const isLink = entry.isSymbolicLink();
367
+ if (entry.isDirectory() && ignoreDirs.has(name2)) continue;
363
368
  let st;
364
- let isLink;
365
369
  try {
366
- st = statSync(abs);
367
- isLink = lstatSync(abs).isSymbolicLink();
370
+ st = isLink ? statSync(abs) : lstatSync(abs);
368
371
  } catch {
369
372
  continue;
370
373
  }
@@ -1772,13 +1775,13 @@ async function Module2(moduleArg = {}) {
1772
1775
  }
1773
1776
  readAsync = /* @__PURE__ */ __name(async (url) => {
1774
1777
  if (isFileURI(url)) {
1775
- return new Promise((resolve2, reject) => {
1778
+ return new Promise((resolve3, reject) => {
1776
1779
  var xhr = new XMLHttpRequest();
1777
1780
  xhr.open("GET", url, true);
1778
1781
  xhr.responseType = "arraybuffer";
1779
1782
  xhr.onload = () => {
1780
1783
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
1781
- resolve2(xhr.response);
1784
+ resolve3(xhr.response);
1782
1785
  return;
1783
1786
  }
1784
1787
  reject(xhr.status);
@@ -1974,9 +1977,9 @@ async function Module2(moduleArg = {}) {
1974
1977
  __name(receiveInstantiationResult, "receiveInstantiationResult");
1975
1978
  var info2 = getWasmImports();
1976
1979
  if (Module["instantiateWasm"]) {
1977
- return new Promise((resolve2, reject) => {
1980
+ return new Promise((resolve3, reject) => {
1978
1981
  Module["instantiateWasm"](info2, (mod, inst) => {
1979
- resolve2(receiveInstance(mod, inst));
1982
+ resolve3(receiveInstance(mod, inst));
1980
1983
  });
1981
1984
  });
1982
1985
  }
@@ -3307,8 +3310,8 @@ async function Module2(moduleArg = {}) {
3307
3310
  if (runtimeInitialized) {
3308
3311
  moduleRtn = Module;
3309
3312
  } else {
3310
- moduleRtn = new Promise((resolve2, reject) => {
3311
- readyPromiseResolve = resolve2;
3313
+ moduleRtn = new Promise((resolve3, reject) => {
3314
+ readyPromiseResolve = resolve3;
3312
3315
  readyPromiseReject = reject;
3313
3316
  });
3314
3317
  }
@@ -5539,27 +5542,41 @@ ${JSON.stringify(symbolNames, null, 2)}`);
5539
5542
 
5540
5543
  // src/ast/loader.ts
5541
5544
  import { readFileSync as readFileSync2, existsSync } from "fs";
5545
+ import { homedir } from "os";
5542
5546
  import { dirname, join as join2 } from "path";
5543
5547
  import { fileURLToPath } from "url";
5544
5548
  function grammarKeyForExt(ext) {
5545
5549
  return EXT_GRAMMAR[ext];
5546
5550
  }
5547
- function resolveGrammarDir() {
5548
- const env = process.env.CODEINDEX_GRAMMAR_DIR ?? process.env.ULTRAINDEX_GRAMMAR_DIR;
5549
- if (env && existsSync(env)) return env;
5550
- const here = dirname(fileURLToPath(import.meta.url));
5551
- const candidates = [
5551
+ function sharedGrammarsCacheDir() {
5552
+ const xdg = process.env.XDG_CACHE_HOME;
5553
+ const base = xdg && xdg.trim() ? xdg.trim() : join2(homedir(), ".cache");
5554
+ return join2(base, "codeindex", "grammars", ENGINE_VERSION);
5555
+ }
5556
+ function resolveGrammarsTier(opts = {}) {
5557
+ const cacheDir = sharedGrammarsCacheDir();
5558
+ const legacy = process.env.CODEINDEX_GRAMMAR_DIR ?? process.env.ULTRAINDEX_GRAMMAR_DIR;
5559
+ if (legacy && legacy.trim() && existsSync(legacy)) return { tier: "env", dir: legacy, cacheDir };
5560
+ const here = opts.moduleDir ?? dirname(fileURLToPath(import.meta.url));
5561
+ const adjacent = [
5552
5562
  join2(here, "grammars"),
5553
5563
  // bundle: <...>/scripts/grammars
5554
5564
  join2(here, "..", "..", "scripts", "grammars"),
5555
5565
  // dev: src/ast → <repo>/scripts/grammars
5556
5566
  join2(here, "..", "scripts", "grammars")
5557
5567
  ];
5558
- for (const c2 of candidates) if (existsSync(c2)) return c2;
5559
- return join2(here, "grammars");
5568
+ for (const c2 of adjacent) if (existsSync(c2)) return { tier: "adjacent", dir: c2, cacheDir };
5569
+ const env = process.env.CODEINDEX_GRAMMARS_DIR;
5570
+ if (env && env.trim() && existsSync(env)) return { tier: "env", dir: env, cacheDir };
5571
+ if (existsSync(cacheDir)) return { tier: "cache", dir: cacheDir, cacheDir };
5572
+ return { tier: "none", cacheDir };
5573
+ }
5574
+ function resolveGrammarsDir(opts) {
5575
+ return resolveGrammarsTier(opts).dir;
5560
5576
  }
5561
5577
  async function ensureGrammars(keys) {
5562
- const dir = resolveGrammarDir();
5578
+ const dir = resolveGrammarsDir();
5579
+ if (!dir) return;
5563
5580
  if (!runtimeReady) {
5564
5581
  const runtime = join2(dir, "web-tree-sitter.wasm");
5565
5582
  if (!existsSync(runtime)) return;
@@ -5584,6 +5601,14 @@ async function ensureGrammars(keys) {
5584
5601
  function allGrammarKeys() {
5585
5602
  return [...new Set(Object.values(EXT_GRAMMAR))];
5586
5603
  }
5604
+ function grammarKeysForExts(exts) {
5605
+ const keys = /* @__PURE__ */ new Set();
5606
+ for (const ext of exts) {
5607
+ const key = EXT_GRAMMAR[ext];
5608
+ if (key !== void 0) keys.add(key);
5609
+ }
5610
+ return [...keys].sort();
5611
+ }
5587
5612
  function grammarReady(key) {
5588
5613
  return loaded.has(key);
5589
5614
  }
@@ -5598,6 +5623,7 @@ var init_loader = __esm({
5598
5623
  "src/ast/loader.ts"() {
5599
5624
  "use strict";
5600
5625
  init_web_tree_sitter();
5626
+ init_types();
5601
5627
  EXT_GRAMMAR = {
5602
5628
  ".ts": "typescript",
5603
5629
  ".mts": "typescript",
@@ -6523,7 +6549,7 @@ function scanRepo(root, opts = {}) {
6523
6549
  const scoped = opts.scope ? [...opts.include ?? [], `${opts.scope.replace(/\/+$/, "")}/**`] : opts.include;
6524
6550
  const include = compileGlobs(scoped);
6525
6551
  const exclude = compileGlobs(opts.exclude);
6526
- const { files: walked, capped, excluded } = walk(root, {
6552
+ const { files: walked, capped, excluded } = opts.precomputedWalk ?? walk(root, {
6527
6553
  maxFileBytes: opts.maxBytes,
6528
6554
  maxFiles: opts.maxFiles,
6529
6555
  gitignore: opts.gitignore,
@@ -6534,6 +6560,9 @@ function scanRepo(root, opts = {}) {
6534
6560
  const languages = {};
6535
6561
  const docText = /* @__PURE__ */ new Map();
6536
6562
  const mtimes = /* @__PURE__ */ new Map();
6563
+ const cache = opts.cache;
6564
+ let allReused = cache !== void 0;
6565
+ let cacheDirty = cache === void 0;
6537
6566
  for (const f of walked) {
6538
6567
  if (outPrefix && (f.abs === opts.out || f.abs.startsWith(outPrefix))) continue;
6539
6568
  if (include && !include(f.rel)) continue;
@@ -6552,8 +6581,11 @@ function scanRepo(root, opts = {}) {
6552
6581
  if (cached && cached.hash === hash) {
6553
6582
  files.push(cached.record);
6554
6583
  if (kind === "doc" && content) docText.set(f.rel, content);
6584
+ if (cached.size !== f.size || cached.mtimeMs !== f.mtimeMs) cacheDirty = true;
6555
6585
  continue;
6556
6586
  }
6587
+ allReused = false;
6588
+ cacheDirty = true;
6557
6589
  const record = {
6558
6590
  rel: f.rel,
6559
6591
  ext: f.ext,
@@ -6595,7 +6627,22 @@ function scanRepo(root, opts = {}) {
6595
6627
  files.push(record);
6596
6628
  }
6597
6629
  files.sort(byKey((f) => f.rel));
6598
- return { root, commit: headCommit(root), files, languages, docText, mtimes, capped, excluded };
6630
+ if (cache !== void 0 && files.length !== cache.size) {
6631
+ allReused = false;
6632
+ cacheDirty = true;
6633
+ }
6634
+ return {
6635
+ root,
6636
+ commit: headCommit(root),
6637
+ files,
6638
+ languages,
6639
+ docText,
6640
+ mtimes,
6641
+ capped,
6642
+ excluded,
6643
+ contentUnchanged: allReused,
6644
+ cacheDirty
6645
+ };
6599
6646
  }
6600
6647
  var init_scan = __esm({
6601
6648
  "src/scan.ts"() {
@@ -6614,7 +6661,7 @@ var init_scan = __esm({
6614
6661
 
6615
6662
  // src/resolve.ts
6616
6663
  import { posix } from "path";
6617
- import { join as join3 } from "path";
6664
+ import { join as join4 } from "path";
6618
6665
  function distToSrcCandidates(target) {
6619
6666
  const segs = norm(target).split("/").filter((s) => s !== ".");
6620
6667
  const out2 = [];
@@ -6701,7 +6748,7 @@ function resolveExtends(fileSet, fromDir, ext) {
6701
6748
  function readTsConfig(root, fileSet, rel, warnings, seen) {
6702
6749
  if (seen.has(rel)) return void 0;
6703
6750
  seen.add(rel);
6704
- const cfg = tolerantJsonParse(readText(join3(root, rel)));
6751
+ const cfg = tolerantJsonParse(readText(join4(root, rel)));
6705
6752
  if (cfg === void 0) {
6706
6753
  warnings.push(`unparseable ${rel} \u2014 its path aliases were ignored`);
6707
6754
  return void 0;
@@ -6830,7 +6877,7 @@ function buildResolveContext(scan2) {
6830
6877
  const goModules = [];
6831
6878
  for (const rel of fileSet) {
6832
6879
  if (rel !== "go.mod" && !rel.endsWith("/go.mod")) continue;
6833
- const text = readText(join3(scan2.root, rel));
6880
+ const text = readText(join4(scan2.root, rel));
6834
6881
  const m = /^\s*module\s+(\S+)/m.exec(text);
6835
6882
  if (!m) continue;
6836
6883
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6840,7 +6887,7 @@ function buildResolveContext(scan2) {
6840
6887
  const rustCrates = [];
6841
6888
  for (const rel of fileSet) {
6842
6889
  if (rel !== "Cargo.toml" && !rel.endsWith("/Cargo.toml")) continue;
6843
- const text = readText(join3(scan2.root, rel));
6890
+ const text = readText(join4(scan2.root, rel));
6844
6891
  const m = /\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/ms.exec(text);
6845
6892
  if (!m) continue;
6846
6893
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6867,7 +6914,7 @@ function buildResolveContext(scan2) {
6867
6914
  const workspacePackages = [];
6868
6915
  for (const rel of fileSet) {
6869
6916
  if (rel !== "package.json" && !rel.endsWith("/package.json")) continue;
6870
- const pkg = tolerantJsonParse(readText(join3(scan2.root, rel)));
6917
+ const pkg = tolerantJsonParse(readText(join4(scan2.root, rel)));
6871
6918
  if (pkg === void 0) {
6872
6919
  warnings.push(`unparseable ${rel} \u2014 skipped for workspace resolution`);
6873
6920
  continue;
@@ -6894,7 +6941,7 @@ function buildResolveContext(scan2) {
6894
6941
  const phpPsr4 = [];
6895
6942
  for (const rel of fileSet) {
6896
6943
  if (rel !== "composer.json" && !rel.endsWith("/composer.json")) continue;
6897
- const composer = tolerantJsonParse(readText(join3(scan2.root, rel)));
6944
+ const composer = tolerantJsonParse(readText(join4(scan2.root, rel)));
6898
6945
  if (!composer) {
6899
6946
  warnings.push(`unparseable ${rel} \u2014 skipped for PHP PSR-4 resolution`);
6900
6947
  continue;
@@ -7469,7 +7516,7 @@ var init_calls = __esm({
7469
7516
  });
7470
7517
 
7471
7518
  // src/graph.ts
7472
- import { join as join4 } from "path";
7519
+ import { join as join5 } from "path";
7473
7520
  function isDistinctive(name2) {
7474
7521
  if (name2.length < 5) return false;
7475
7522
  const internalUpper = /[a-z][A-Z]/.test(name2) || /[A-Z]{2}/.test(name2);
@@ -7548,7 +7595,7 @@ function buildGraph(scan2, ctx, modules, moduleOf, meta) {
7548
7595
  if (unique.size) {
7549
7596
  for (const f of scan2.files) {
7550
7597
  if (f.kind !== "doc") continue;
7551
- const content = scan2.docText.get(f.rel) ?? readText(join4(scan2.root, f.rel));
7598
+ const content = scan2.docText.get(f.rel) ?? readText(join5(scan2.root, f.rel));
7552
7599
  if (!content) continue;
7553
7600
  const tokens = /* @__PURE__ */ new Map();
7554
7601
  for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
@@ -7653,217 +7700,567 @@ var init_graph = __esm({
7653
7700
  }
7654
7701
  });
7655
7702
 
7656
- // src/callers.ts
7657
- function computeImportPairs(scan2) {
7658
- const ctx = buildResolveContext(scan2);
7659
- const pairs = /* @__PURE__ */ new Set();
7703
+ // src/render/symbols-json.ts
7704
+ function computeSymbolRefs(scan2) {
7705
+ const unique = uniqueSymbolDefs(scan2);
7706
+ const refs = /* @__PURE__ */ new Map();
7707
+ if (!unique.size) return refs;
7708
+ const add = (name2, file) => {
7709
+ let set = refs.get(name2);
7710
+ if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
7711
+ set.add(file);
7712
+ };
7660
7713
  for (const f of scan2.files) {
7661
- for (const ref of f.refs) {
7662
- if (ref.kind !== "import") continue;
7663
- const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
7664
- if (r.kind === "resolved" && r.target !== f.rel) pairs.add(`${f.rel}|${r.target}`);
7714
+ if (f.kind === "code" && f.idents) {
7715
+ for (const id of f.idents) {
7716
+ const target = unique.get(id);
7717
+ if (target && target !== f.rel) add(id, f.rel);
7718
+ }
7719
+ } else if (f.kind === "doc") {
7720
+ const content = scan2.docText.get(f.rel);
7721
+ if (!content) continue;
7722
+ for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
7723
+ const target = unique.get(tok);
7724
+ if (target && target !== f.rel) add(tok, f.rel);
7725
+ }
7665
7726
  }
7666
7727
  }
7667
- return pairs;
7728
+ return refs;
7668
7729
  }
7669
- function buildCallerIndex(scan2, importPairs, opts = {}) {
7670
- const pairs = importPairs ?? computeImportPairs(scan2);
7671
- const recall = opts.recall === true;
7672
- const defs = /* @__PURE__ */ new Map();
7673
- for (const f of scan2.files) {
7674
- const seen = /* @__PURE__ */ new Set();
7675
- for (const s of f.symbols) {
7676
- if (!s.exported || REFERENCE_KINDS3.has(s.kind)) continue;
7677
- if (seen.has(s.name)) continue;
7678
- seen.add(s.name);
7679
- let arr = defs.get(s.name);
7680
- if (!arr) defs.set(s.name, arr = []);
7681
- arr.push(s);
7682
- }
7683
- }
7684
- const localDefs = /* @__PURE__ */ new Map();
7730
+ function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
7731
+ const defsByName = /* @__PURE__ */ new Map();
7685
7732
  for (const f of scan2.files) {
7686
- const byName = /* @__PURE__ */ new Map();
7687
7733
  for (const s of f.symbols) {
7688
- if (!REFERENCE_KINDS3.has(s.kind) && !byName.has(s.name)) byName.set(s.name, s);
7689
- }
7690
- localDefs.set(f.rel, byName);
7691
- }
7692
- const sites = /* @__PURE__ */ new Map();
7693
- const record = (def, caller) => {
7694
- let entry = sites.get(def.name + "\0" + def.file);
7695
- if (!entry) sites.set(def.name + "\0" + def.file, entry = { def, callers: [] });
7696
- entry.callers.push(caller);
7697
- };
7698
- for (const f of scan2.files) {
7699
- if (!f.calls?.length) continue;
7700
- const family = familyOf(f.lang);
7701
- const own = localDefs.get(f.rel);
7702
- for (const c2 of f.calls) {
7703
- const local = own.get(c2.name);
7704
- if (local) {
7705
- if (local.line !== c2.line)
7706
- record(local, recall ? { file: f.rel, line: c2.line, confidence: "corroborated" } : { file: f.rel, line: c2.line });
7707
- continue;
7708
- }
7709
- const cands = (defs.get(c2.name) ?? []).filter((d) => familyOf(d.lang) === family && d.file !== f.rel).map((d) => ({ file: d.file, lang: d.lang }));
7710
- if (!cands.length) continue;
7711
- const imported = cands.filter((d) => pairs.has(`${f.rel}|${d.file}`));
7712
- const chosen = family === "js" ? imported.length ? pickCandidate(f.rel, imported) : (
7713
- // JS/TS gate: no corroborating import → no binding. Recall mode
7714
- // relaxes this to a unique-repo-wide name match (issue #7).
7715
- recall && cands.length === 1 ? cands[0] : void 0
7716
- ) : imported.length ? pickCandidate(f.rel, imported) : pickCandidate(f.rel, cands);
7717
- if (!chosen) continue;
7718
- const def = defs.get(c2.name).find((d) => d.file === chosen.file);
7719
- record(
7720
- def,
7721
- recall ? { file: f.rel, line: c2.line, confidence: imported.length ? "corroborated" : "unique-name" } : { file: f.rel, line: c2.line }
7722
- );
7734
+ let arr = defsByName.get(s.name);
7735
+ if (!arr) defsByName.set(s.name, arr = []);
7736
+ arr.push({
7737
+ file: s.file,
7738
+ line: s.line,
7739
+ ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
7740
+ kind: s.kind,
7741
+ exported: s.exported,
7742
+ lang: s.lang,
7743
+ ...s.parent ? { parent: s.parent } : {}
7744
+ });
7723
7745
  }
7724
7746
  }
7725
- const index = /* @__PURE__ */ new Map();
7726
- const keys = [...sites.keys()].sort(byStr);
7727
- for (const key of keys) {
7728
- const { def, callers } = sites.get(key);
7729
- callers.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7730
- if (!index.has(def.name)) index.set(def.name, { def, callers });
7731
- else index.set(`${def.name}@${def.file}`, { def, callers });
7747
+ const defs = {};
7748
+ for (const name2 of [...defsByName.keys()].sort(byStr)) {
7749
+ defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
7732
7750
  }
7733
- return index;
7734
- }
7735
- function enclosingSymbol(scan2, file, line) {
7736
- const f = scan2.files.find((x) => x.rel === file);
7737
- if (!f?.symbols.length) return void 0;
7738
- return enclosingAmong(f.symbols, line);
7739
- }
7740
- function enclosingAmong(symbols, line) {
7741
- let best;
7742
- for (const s of symbols) {
7743
- if (REFERENCE_KINDS3.has(s.kind)) continue;
7744
- if (s.line > line) continue;
7745
- if (s.endLine !== void 0 && line > s.endLine) continue;
7746
- if (!best || s.line > best.line || s.line === best.line && (s.endLine ?? Infinity) <= (best.endLine ?? Infinity)) {
7747
- best = s;
7748
- }
7751
+ const refsOut = {};
7752
+ for (const name2 of [...refs.keys()].sort(byStr)) {
7753
+ const files = [...refs.get(name2)].sort(byStr);
7754
+ if (files.length) refsOut[name2] = files;
7749
7755
  }
7750
- return best;
7756
+ return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
7751
7757
  }
7752
- function buildRawCallerIndex(scan2) {
7753
- const byName = /* @__PURE__ */ new Map();
7754
- for (const f of scan2.files) {
7755
- if (!f.calls?.length) continue;
7756
- const symbols = f.symbols.filter((s) => !REFERENCE_KINDS3.has(s.kind));
7757
- for (const c2 of f.calls) {
7758
- const site = { file: f.rel, line: c2.line };
7759
- if (c2.receiver !== void 0) site.receiver = c2.receiver;
7760
- const enc = enclosingAmong(symbols, c2.line);
7761
- if (enc) site.enclosingSymbol = enc;
7762
- let arr = byName.get(c2.name);
7763
- if (!arr) byName.set(c2.name, arr = []);
7764
- arr.push(site);
7765
- }
7766
- }
7767
- const index = /* @__PURE__ */ new Map();
7768
- for (const name2 of [...byName.keys()].sort(byStr)) {
7769
- const sites = byName.get(name2);
7770
- sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7771
- index.set(name2, sites);
7772
- }
7773
- return index;
7758
+ function renderSymbolsJson(index) {
7759
+ return JSON.stringify(index, null, 2) + "\n";
7774
7760
  }
7775
- var REFERENCE_KINDS3;
7776
- var init_callers = __esm({
7777
- "src/callers.ts"() {
7761
+ var init_symbols_json = __esm({
7762
+ "src/render/symbols-json.ts"() {
7778
7763
  "use strict";
7779
- init_calls();
7780
- init_resolve();
7764
+ init_types();
7781
7765
  init_sort();
7782
- REFERENCE_KINDS3 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7766
+ init_graph();
7783
7767
  }
7784
7768
  });
7785
7769
 
7786
- // src/query.ts
7787
- import { join as join5 } from "path";
7788
- function symbolsOverview(scan2, rel) {
7789
- const f = scan2.files.find((x) => x.rel === rel);
7790
- if (!f) return [];
7791
- return [...f.symbols].filter((s) => !REFERENCE_KINDS4.has(s.kind)).sort((a, b) => a.line - b.line || byStr(a.name, b.name));
7792
- }
7793
- function findSymbol(scan2, namePath, opts = {}) {
7794
- const segments = namePath.split("/").filter(Boolean);
7795
- if (!segments.length) return [];
7796
- const leaf = segments[segments.length - 1];
7797
- const parents = segments.slice(0, -1);
7798
- const matchName = (name2, wanted) => opts.substring ? name2.toLowerCase().includes(wanted.toLowerCase()) : name2 === wanted;
7770
+ // src/bm25.ts
7771
+ function subtokens(raw) {
7772
+ const folded = foldText(raw).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
7799
7773
  const out2 = [];
7800
- for (const f of scan2.files) {
7801
- for (const s of f.symbols) {
7802
- if (REFERENCE_KINDS4.has(s.kind)) continue;
7803
- if (!matchName(s.name, leaf)) continue;
7804
- if (parents.length) {
7805
- const parent = parents[parents.length - 1];
7806
- if (!s.parent || s.parent !== parent) continue;
7807
- }
7808
- out2.push({ ...s });
7809
- }
7810
- }
7811
- out2.sort(
7812
- (a, b) => Number(b.name === leaf) - Number(a.name === leaf) || byStr(a.file, b.file) || a.line - b.line
7813
- );
7814
- const capped = out2.slice(0, opts.maxResults ?? 50);
7815
- if (opts.includeBody) {
7816
- for (const m of capped) {
7817
- const end = m.endLine ?? m.line;
7818
- const content = readText(join5(scan2.root, m.file));
7819
- if (!content) continue;
7820
- m.body = content.split("\n").slice(m.line - 1, end).join("\n");
7821
- }
7774
+ const seen = /* @__PURE__ */ new Set();
7775
+ const push = (t) => {
7776
+ if (t.length < 2 || seen.has(t)) return;
7777
+ seen.add(t);
7778
+ out2.push(t);
7779
+ };
7780
+ if (!/\s/.test(raw.trim())) push(foldText(raw).toLowerCase().replace(/[^a-z0-9_]+/g, ""));
7781
+ for (const part of folded.split(/[^A-Za-z0-9]+/)) push(part.toLowerCase());
7782
+ return out2;
7783
+ }
7784
+ function addTerms(doc, text) {
7785
+ for (const t of subtokens(text)) {
7786
+ doc.tf.set(t, (doc.tf.get(t) ?? 0) + 1);
7787
+ doc.len++;
7822
7788
  }
7823
- return capped;
7824
7789
  }
7825
- function findReferences(scan2, name2) {
7826
- const defs = [];
7790
+ function buildDocs(scan2) {
7791
+ const docs = [];
7827
7792
  for (const f of scan2.files) {
7793
+ const doc = { file: f.rel, tf: /* @__PURE__ */ new Map(), len: 0, symbols: [] };
7794
+ const seenSym = /* @__PURE__ */ new Set();
7828
7795
  for (const s of f.symbols) {
7829
- if (s.name === name2 && !REFERENCE_KINDS4.has(s.kind)) defs.push(s);
7830
- }
7831
- }
7832
- defs.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7833
- const index = buildCallerIndex(scan2);
7834
- const entry = index.get(name2);
7835
- const callSites = entry ? entry.callers : [];
7836
- const referencingFiles = /* @__PURE__ */ new Set();
7837
- const unique = uniqueSymbolDefs(scan2);
7838
- const defFile = unique.get(name2);
7839
- for (const f of scan2.files) {
7840
- if (f.rel === defFile) continue;
7841
- if (f.kind === "code" && f.idents?.includes(name2)) referencingFiles.add(f.rel);
7842
- else if (f.kind === "doc") {
7843
- const content = scan2.docText.get(f.rel);
7844
- if (content && new RegExp(`\\b${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(content)) {
7845
- referencingFiles.add(f.rel);
7796
+ addTerms(doc, s.name);
7797
+ if (!seenSym.has(s.name)) {
7798
+ seenSym.add(s.name);
7799
+ doc.symbols.push(s.name);
7846
7800
  }
7847
7801
  }
7802
+ for (const seg of f.rel.split("/")) addTerms(doc, seg);
7803
+ for (const h of f.headings) addTerms(doc, h);
7804
+ if (f.summary) addTerms(doc, f.summary);
7805
+ docs.push(doc);
7848
7806
  }
7849
- for (const site of callSites) referencingFiles.add(site.file);
7850
- return { defs, callSites, referencingFiles: [...referencingFiles].sort(byStr) };
7807
+ return docs;
7851
7808
  }
7852
- var REFERENCE_KINDS4;
7853
- var init_query = __esm({
7854
- "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"() {
7855
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();
7856
7982
  init_walk();
7857
- 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();
7858
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();
7859
8256
  init_sort();
7860
8257
  REFERENCE_KINDS4 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7861
8258
  }
7862
8259
  });
7863
8260
 
7864
8261
  // src/edit.ts
7865
- import { readFileSync as readFileSync3, writeFileSync } from "fs";
7866
- import { join as join6 } from "path";
8262
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8263
+ import { join as join9 } from "path";
7867
8264
  function resolveUniqueSymbol(scan2, namePath, file) {
7868
8265
  let matches = findSymbol(scan2, namePath);
7869
8266
  if (file) matches = matches.filter((m) => m.file === file);
@@ -7881,15 +8278,15 @@ function readLines(abs) {
7881
8278
  function replaceSymbolBody(scan2, namePath, body2, file) {
7882
8279
  const sym = resolveUniqueSymbol(scan2, namePath, file);
7883
8280
  const end = sym.endLine ?? sym.line;
7884
- const abs = join6(scan2.root, sym.file);
8281
+ const abs = join9(scan2.root, sym.file);
7885
8282
  const lines = readLines(abs);
7886
8283
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
7887
8284
  lines.splice(sym.line - 1, end - sym.line + 1, ...newLines);
7888
- writeFileSync(abs, lines.join("\n"));
8285
+ writeFileSync2(abs, lines.join("\n"));
7889
8286
  return { file: sym.file, startLine: sym.line, endLine: sym.line + newLines.length - 1, lines: newLines.length };
7890
8287
  }
7891
8288
  function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7892
- const abs = join6(scan2.root, sym.file);
8289
+ const abs = join9(scan2.root, sym.file);
7893
8290
  const lines = readLines(abs);
7894
8291
  const minGap = SEPARATED_KINDS.has(sym.kind) ? 1 : 0;
7895
8292
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
@@ -7898,7 +8295,7 @@ function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
7898
8295
  block.push(...newLines);
7899
8296
  if (blankAfter && minGap && lines[index]?.trim() !== "") block.push("");
7900
8297
  lines.splice(index, 0, ...block);
7901
- writeFileSync(abs, lines.join("\n"));
8298
+ writeFileSync2(abs, lines.join("\n"));
7902
8299
  return { file: sym.file, startLine: index + 1, endLine: index + block.length, lines: block.length };
7903
8300
  }
7904
8301
  function insertAfterSymbol(scan2, namePath, body2, file) {
@@ -7920,8 +8317,8 @@ var init_edit = __esm({
7920
8317
  });
7921
8318
 
7922
8319
  // src/memory.ts
7923
- import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
7924
- import { dirname as dirname2, join as join7 } from "path";
8320
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
8321
+ import { dirname as dirname3, join as join10 } from "path";
7925
8322
  function sanitize(name2) {
7926
8323
  const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
7927
8324
  if (!clean) throw new Error("memory name is empty");
@@ -7935,12 +8332,12 @@ function sanitize(name2) {
7935
8332
  return clean;
7936
8333
  }
7937
8334
  function memoryPath(repo, name2) {
7938
- return join7(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
8335
+ return join10(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
7939
8336
  }
7940
8337
  function writeMemory(repo, name2, content) {
7941
8338
  const path = memoryPath(repo, name2);
7942
- mkdirSync(dirname2(path), { recursive: true });
7943
- writeFileSync2(path, content.endsWith("\n") ? content : content + "\n");
8339
+ mkdirSync2(dirname3(path), { recursive: true });
8340
+ writeFileSync3(path, content.endsWith("\n") ? content : content + "\n");
7944
8341
  return sanitize(name2);
7945
8342
  }
7946
8343
  function readMemory(repo, name2) {
@@ -7961,7 +8358,7 @@ function deleteMemory(repo, name2) {
7961
8358
  return true;
7962
8359
  }
7963
8360
  function listMemories(repo) {
7964
- const root = join7(repo, ...MEMORY_DIR);
8361
+ const root = join10(repo, ...MEMORY_DIR);
7965
8362
  const out2 = [];
7966
8363
  const walk2 = (dir, prefix) => {
7967
8364
  let entries;
@@ -7971,7 +8368,7 @@ function listMemories(repo) {
7971
8368
  return;
7972
8369
  }
7973
8370
  for (const e of entries) {
7974
- if (e.isDirectory()) walk2(join7(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
8371
+ if (e.isDirectory()) walk2(join10(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
7975
8372
  else if (e.name.endsWith(".md")) out2.push(prefix ? `${prefix}/${e.name.slice(0, -3)}` : e.name.slice(0, -3));
7976
8373
  }
7977
8374
  };
@@ -7988,7 +8385,7 @@ var init_memory = __esm({
7988
8385
 
7989
8386
  // src/workspaces.ts
7990
8387
  import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
7991
- import { join as join8 } from "path";
8388
+ import { join as join11 } from "path";
7992
8389
  function readJson(path, label, warnings) {
7993
8390
  const raw = readText(path);
7994
8391
  if (!raw) return void 0;
@@ -8039,7 +8436,7 @@ function wsGlobToRegExp(pat) {
8039
8436
  return new RegExp(`^${re}($|/)`);
8040
8437
  }
8041
8438
  function probeNodePkg(root, dir, kind, warnings) {
8042
- const path = join8(root, dir, "package.json");
8439
+ const path = join11(root, dir, "package.json");
8043
8440
  if (!existsSync2(path)) return void 0;
8044
8441
  const manifest = `${dir}/package.json`;
8045
8442
  const pkg = readJson(path, manifest, warnings);
@@ -8053,7 +8450,7 @@ function probeNodePkg(root, dir, kind, warnings) {
8053
8450
  return out2;
8054
8451
  }
8055
8452
  function probeCargo(root, dir) {
8056
- const path = join8(root, dir, "Cargo.toml");
8453
+ const path = join11(root, dir, "Cargo.toml");
8057
8454
  if (!existsSync2(path)) return void 0;
8058
8455
  const body2 = tomlSectionBody(readText(path), "package");
8059
8456
  const out2 = {
@@ -8067,18 +8464,18 @@ function probeCargo(root, dir) {
8067
8464
  return out2;
8068
8465
  }
8069
8466
  function probeGoMod(root, dir) {
8070
- const path = join8(root, dir, "go.mod");
8467
+ const path = join11(root, dir, "go.mod");
8071
8468
  if (!existsSync2(path)) return void 0;
8072
8469
  const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
8073
8470
  return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
8074
8471
  }
8075
8472
  function probeMaven(root, dir) {
8076
- const path = join8(root, dir, "pom.xml");
8473
+ const path = join11(root, dir, "pom.xml");
8077
8474
  if (!existsSync2(path)) return void 0;
8078
8475
  return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
8079
8476
  }
8080
8477
  function probePyproject(root, dir) {
8081
- const path = join8(root, dir, "pyproject.toml");
8478
+ const path = join11(root, dir, "pyproject.toml");
8082
8479
  if (!existsSync2(path)) return void 0;
8083
8480
  const toml = readText(path);
8084
8481
  const project = tomlSectionBody(toml, "project");
@@ -8094,7 +8491,7 @@ function probePyproject(root, dir) {
8094
8491
  return out2;
8095
8492
  }
8096
8493
  function probeComposer(root, dir, warnings) {
8097
- const path = join8(root, dir, "composer.json");
8494
+ const path = join11(root, dir, "composer.json");
8098
8495
  if (!existsSync2(path)) return void 0;
8099
8496
  const manifest = `${dir}/composer.json`;
8100
8497
  const pkg = readJson(path, manifest, warnings);
@@ -8108,7 +8505,7 @@ function probeComposer(root, dir, warnings) {
8108
8505
  return out2;
8109
8506
  }
8110
8507
  function probeNxProject(root, dir, warnings) {
8111
- const path = join8(root, dir, "project.json");
8508
+ const path = join11(root, dir, "project.json");
8112
8509
  if (!existsSync2(path)) return void 0;
8113
8510
  const manifest = `${dir}/project.json`;
8114
8511
  const proj = readJson(path, manifest, warnings);
@@ -8121,7 +8518,7 @@ function probeNxProject(root, dir, warnings) {
8121
8518
  }
8122
8519
  function probeGradle(root, dir) {
8123
8520
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8124
- if (existsSync2(join8(root, dir, f))) {
8521
+ if (existsSync2(join11(root, dir, f))) {
8125
8522
  return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
8126
8523
  }
8127
8524
  }
@@ -8156,7 +8553,7 @@ function addPackage(root, dir, found, kind, warnings) {
8156
8553
  }
8157
8554
  function isDirAt(root, rel) {
8158
8555
  try {
8159
- return statSync3(join8(root, rel)).isDirectory();
8556
+ return statSync3(join11(root, rel)).isDirectory();
8160
8557
  } catch {
8161
8558
  return false;
8162
8559
  }
@@ -8164,7 +8561,7 @@ function isDirAt(root, rel) {
8164
8561
  function subdirsOf(root, base) {
8165
8562
  let entries;
8166
8563
  try {
8167
- entries = readdirSync3(base ? join8(root, base) : root, { withFileTypes: true });
8564
+ entries = readdirSync3(base ? join11(root, base) : root, { withFileTypes: true });
8168
8565
  } catch {
8169
8566
  return [];
8170
8567
  }
@@ -8226,14 +8623,14 @@ function npmFamilyPatterns(root, warnings) {
8226
8623
  if (t.startsWith("!")) negations.push(t.slice(1));
8227
8624
  else positives.push({ pattern: t, kind });
8228
8625
  };
8229
- const pkg = readJson(join8(root, "package.json"), "package.json", warnings);
8626
+ const pkg = readJson(join11(root, "package.json"), "package.json", warnings);
8230
8627
  const ws = pkg?.workspaces;
8231
8628
  if (Array.isArray(ws)) {
8232
8629
  for (const x of ws) if (typeof x === "string") push(x, "npm");
8233
8630
  } else if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
8234
8631
  for (const x of ws.packages) if (typeof x === "string") push(x, "npm");
8235
8632
  }
8236
- const pnpm = readText(join8(root, "pnpm-workspace.yaml"));
8633
+ const pnpm = readText(join11(root, "pnpm-workspace.yaml"));
8237
8634
  let inPackages = false;
8238
8635
  for (const line of pnpm.split(/\r?\n/)) {
8239
8636
  if (/^\S/.test(line)) {
@@ -8247,11 +8644,11 @@ function npmFamilyPatterns(root, warnings) {
8247
8644
  return { positives, negations };
8248
8645
  }
8249
8646
  function fallbackNpmPatterns(root, warnings) {
8250
- const lerna = readJson(join8(root, "lerna.json"), "lerna.json", warnings);
8647
+ const lerna = readJson(join11(root, "lerna.json"), "lerna.json", warnings);
8251
8648
  if (lerna && Array.isArray(lerna.packages)) {
8252
8649
  return lerna.packages.filter((x) => typeof x === "string").map((pattern) => ({ pattern, kind: "lerna" }));
8253
8650
  }
8254
- const nx = readJson(join8(root, "nx.json"), "nx.json", warnings);
8651
+ const nx = readJson(join11(root, "nx.json"), "nx.json", warnings);
8255
8652
  if (nx) {
8256
8653
  const layout = nx.workspaceLayout ?? {};
8257
8654
  const appsDir = typeof layout.appsDir === "string" ? layout.appsDir : "apps";
@@ -8261,7 +8658,7 @@ function fallbackNpmPatterns(root, warnings) {
8261
8658
  return [];
8262
8659
  }
8263
8660
  function detectCargoMembers(root, found, warnings) {
8264
- const toml = readText(join8(root, "Cargo.toml"));
8661
+ const toml = readText(join11(root, "Cargo.toml"));
8265
8662
  if (!toml) return;
8266
8663
  const body2 = tomlSectionBody(toml, "workspace");
8267
8664
  if (!body2) return;
@@ -8276,7 +8673,7 @@ function detectCargoMembers(root, found, warnings) {
8276
8673
  }
8277
8674
  }
8278
8675
  function detectGoWork(root, found, warnings) {
8279
- const gowork = readText(join8(root, "go.work"));
8676
+ const gowork = readText(join11(root, "go.work"));
8280
8677
  if (!gowork) return;
8281
8678
  const dirs = [];
8282
8679
  for (const block of gowork.matchAll(/^use\s*\(([\s\S]*?)\)/gm)) {
@@ -8292,7 +8689,7 @@ function detectGoWork(root, found, warnings) {
8292
8689
  }
8293
8690
  }
8294
8691
  function detectMavenModules(root, found, warnings) {
8295
- const pom = readText(join8(root, "pom.xml"));
8692
+ const pom = readText(join11(root, "pom.xml"));
8296
8693
  if (!pom) return;
8297
8694
  const modules = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1];
8298
8695
  if (!modules) return;
@@ -8301,7 +8698,7 @@ function detectMavenModules(root, found, warnings) {
8301
8698
  }
8302
8699
  }
8303
8700
  function detectUvMembers(root, found, warnings) {
8304
- const toml = readText(join8(root, "pyproject.toml"));
8701
+ const toml = readText(join11(root, "pyproject.toml"));
8305
8702
  if (!toml) return;
8306
8703
  const body2 = tomlSectionBody(toml, "tool.uv.workspace");
8307
8704
  if (!body2) return;
@@ -8316,7 +8713,7 @@ function detectUvMembers(root, found, warnings) {
8316
8713
  }
8317
8714
  }
8318
8715
  function detectComposerPathRepos(root, found, warnings) {
8319
- const composer = readJson(join8(root, "composer.json"), "composer.json", warnings);
8716
+ const composer = readJson(join11(root, "composer.json"), "composer.json", warnings);
8320
8717
  const repos = composer?.repositories;
8321
8718
  if (!Array.isArray(repos)) return;
8322
8719
  for (const r of repos) {
@@ -8327,7 +8724,7 @@ function detectComposerPathRepos(root, found, warnings) {
8327
8724
  }
8328
8725
  function detectGradleIncludes(root, found, warnings) {
8329
8726
  for (const f of ["settings.gradle", "settings.gradle.kts"]) {
8330
- const text = readText(join8(root, f));
8727
+ const text = readText(join11(root, f));
8331
8728
  if (!text) continue;
8332
8729
  for (const line of text.split(/\r?\n/)) {
8333
8730
  if (!/^\s*include[\s(]/.test(line)) continue;
@@ -8339,7 +8736,7 @@ function detectGradleIncludes(root, found, warnings) {
8339
8736
  }
8340
8737
  }
8341
8738
  function npmEdges(root, pkg, byName, warnings) {
8342
- const manifest = readJson(join8(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8739
+ const manifest = readJson(join11(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8343
8740
  if (!manifest) return [];
8344
8741
  const edges = /* @__PURE__ */ new Set();
8345
8742
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -8362,7 +8759,7 @@ function normalizeDepPath(fromDir, rel) {
8362
8759
  return out2.join("/");
8363
8760
  }
8364
8761
  function cargoEdges(root, pkg, byName, byDir) {
8365
- const toml = readText(join8(root, pkg.dir, "Cargo.toml"));
8762
+ const toml = readText(join11(root, pkg.dir, "Cargo.toml"));
8366
8763
  if (!toml) return [];
8367
8764
  const edges = /* @__PURE__ */ new Set();
8368
8765
  for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
@@ -8386,7 +8783,7 @@ function cargoEdges(root, pkg, byName, byDir) {
8386
8783
  return [...edges];
8387
8784
  }
8388
8785
  function goPkgEdges(root, pkg, byName, byDir) {
8389
- const gomod = readText(join8(root, pkg.dir, "go.mod"));
8786
+ const gomod = readText(join11(root, pkg.dir, "go.mod"));
8390
8787
  if (!gomod) return [];
8391
8788
  const edges = /* @__PURE__ */ new Set();
8392
8789
  for (const m of gomod.matchAll(/^\s*(?:require\s+)?([^\s/(][^\s]*)\s+v[^\s]+/gm)) {
@@ -8400,7 +8797,7 @@ function goPkgEdges(root, pkg, byName, byDir) {
8400
8797
  return [...edges];
8401
8798
  }
8402
8799
  function mavenEdges(root, pkg, byName) {
8403
- const pom = readText(join8(root, pkg.dir, "pom.xml"));
8800
+ const pom = readText(join11(root, pkg.dir, "pom.xml"));
8404
8801
  if (!pom) return [];
8405
8802
  const edges = /* @__PURE__ */ new Set();
8406
8803
  for (const m of pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
@@ -8410,7 +8807,7 @@ function mavenEdges(root, pkg, byName) {
8410
8807
  return [...edges];
8411
8808
  }
8412
8809
  function uvEdges(root, pkg, byName) {
8413
- const toml = readText(join8(root, pkg.dir, "pyproject.toml"));
8810
+ const toml = readText(join11(root, pkg.dir, "pyproject.toml"));
8414
8811
  if (!toml) return [];
8415
8812
  const edges = /* @__PURE__ */ new Set();
8416
8813
  const project = tomlSectionBody(toml, "project");
@@ -8430,7 +8827,7 @@ function uvEdges(root, pkg, byName) {
8430
8827
  return [...edges];
8431
8828
  }
8432
8829
  function composerEdges(root, pkg, byName, warnings) {
8433
- const manifest = readJson(join8(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8830
+ const manifest = readJson(join11(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8434
8831
  if (!manifest) return [];
8435
8832
  const edges = /* @__PURE__ */ new Set();
8436
8833
  for (const field of ["require", "require-dev"]) {
@@ -8444,7 +8841,7 @@ function composerEdges(root, pkg, byName, warnings) {
8444
8841
  }
8445
8842
  function gradleEdges(root, pkg, byName, byDir) {
8446
8843
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8447
- const text = readText(join8(root, pkg.dir, f));
8844
+ const text = readText(join11(root, pkg.dir, f));
8448
8845
  if (!text) continue;
8449
8846
  const edges = /* @__PURE__ */ new Set();
8450
8847
  for (const m of text.matchAll(/project\s*\(\s*["']:?([^"']+)["']\s*\)/g)) {
@@ -8976,121 +9373,54 @@ var init_tests_map = __esm({
8976
9373
  /_test\.rb$/,
8977
9374
  /Test\.php$/,
8978
9375
  /(Test|Tests)\.cs$/,
8979
- /_test\.exs$/
8980
- ];
8981
- TEST_DIR = /(^|\/)(tests?|__tests?__|spec|specs|e2e)(\/|$)/i;
8982
- }
8983
- });
8984
-
8985
- // src/surprise.ts
8986
- function computeSurprises(graph) {
8987
- const commOf = /* @__PURE__ */ new Map();
8988
- const tierOf2 = /* @__PURE__ */ new Map();
8989
- for (const m of graph.modules) {
8990
- if (m.community !== void 0) commOf.set(m.slug, m.community);
8991
- tierOf2.set(m.slug, m.tier);
8992
- }
8993
- const pairCount = /* @__PURE__ */ new Map();
8994
- const pairKey = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
8995
- const candidates = [];
8996
- for (const e of graph.moduleEdges) {
8997
- if (e.dangling) continue;
8998
- const ca = commOf.get(e.from);
8999
- const cb = commOf.get(e.to);
9000
- if (ca === void 0 || cb === void 0 || ca === cb) continue;
9001
- pairCount.set(pairKey(ca, cb), (pairCount.get(pairKey(ca, cb)) ?? 0) + 1);
9002
- if (!DEP_KINDS.has(e.kind)) continue;
9003
- if (tierOf2.get(e.to) === 0) continue;
9004
- candidates.push({ edge: e, comms: [ca, cb] });
9005
- }
9006
- return candidates.filter((c2) => pairCount.get(pairKey(c2.comms[0], c2.comms[1])) <= MAX_PAIR_EDGES).map((c2) => ({
9007
- from: c2.edge.from,
9008
- to: c2.edge.to,
9009
- kind: c2.edge.kind,
9010
- weight: c2.edge.weight,
9011
- communities: c2.comms,
9012
- pairEdges: pairCount.get(pairKey(c2.comms[0], c2.comms[1]))
9013
- })).sort((a, b) => a.pairEdges - b.pairEdges || byStr(a.from, b.from) || byStr(a.to, b.to)).slice(0, SURPRISE_CAP);
9014
- }
9015
- function isSurprising(graph, from, to) {
9016
- const list = graph.surprises ?? computeSurprises(graph);
9017
- return list.some((s) => s.from === from && s.to === to);
9018
- }
9019
- var SURPRISE_CAP, MAX_PAIR_EDGES, DEP_KINDS;
9020
- var init_surprise = __esm({
9021
- "src/surprise.ts"() {
9022
- "use strict";
9023
- init_sort();
9024
- SURPRISE_CAP = 24;
9025
- MAX_PAIR_EDGES = 2;
9026
- DEP_KINDS = /* @__PURE__ */ new Set(["import", "call", "use"]);
9027
- }
9028
- });
9029
-
9030
- // src/render/symbols-json.ts
9031
- function computeSymbolRefs(scan2) {
9032
- const unique = uniqueSymbolDefs(scan2);
9033
- const refs = /* @__PURE__ */ new Map();
9034
- if (!unique.size) return refs;
9035
- const add = (name2, file) => {
9036
- let set = refs.get(name2);
9037
- if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
9038
- set.add(file);
9039
- };
9040
- for (const f of scan2.files) {
9041
- if (f.kind === "code" && f.idents) {
9042
- for (const id of f.idents) {
9043
- const target = unique.get(id);
9044
- if (target && target !== f.rel) add(id, f.rel);
9045
- }
9046
- } else if (f.kind === "doc") {
9047
- const content = scan2.docText.get(f.rel);
9048
- if (!content) continue;
9049
- for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
9050
- const target = unique.get(tok);
9051
- if (target && target !== f.rel) add(tok, f.rel);
9052
- }
9053
- }
9054
- }
9055
- return refs;
9056
- }
9057
- function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
9058
- const defsByName = /* @__PURE__ */ new Map();
9059
- for (const f of scan2.files) {
9060
- for (const s of f.symbols) {
9061
- let arr = defsByName.get(s.name);
9062
- if (!arr) defsByName.set(s.name, arr = []);
9063
- arr.push({
9064
- file: s.file,
9065
- line: s.line,
9066
- ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
9067
- kind: s.kind,
9068
- exported: s.exported,
9069
- lang: s.lang,
9070
- ...s.parent ? { parent: s.parent } : {}
9071
- });
9072
- }
9376
+ /_test\.exs$/
9377
+ ];
9378
+ TEST_DIR = /(^|\/)(tests?|__tests?__|spec|specs|e2e)(\/|$)/i;
9073
9379
  }
9074
- const defs = {};
9075
- for (const name2 of [...defsByName.keys()].sort(byStr)) {
9076
- defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
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);
9077
9389
  }
9078
- const refsOut = {};
9079
- for (const name2 of [...refs.keys()].sort(byStr)) {
9080
- const files = [...refs.get(name2)].sort(byStr);
9081
- if (files.length) refsOut[name2] = files;
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] });
9082
9402
  }
9083
- 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);
9084
9411
  }
9085
- function renderSymbolsJson(index) {
9086
- 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);
9087
9415
  }
9088
- var init_symbols_json = __esm({
9089
- "src/render/symbols-json.ts"() {
9416
+ var SURPRISE_CAP, MAX_PAIR_EDGES, DEP_KINDS;
9417
+ var init_surprise = __esm({
9418
+ "src/surprise.ts"() {
9090
9419
  "use strict";
9091
- init_types();
9092
9420
  init_sort();
9093
- init_graph();
9421
+ SURPRISE_CAP = 24;
9422
+ MAX_PAIR_EDGES = 2;
9423
+ DEP_KINDS = /* @__PURE__ */ new Set(["import", "call", "use"]);
9094
9424
  }
9095
9425
  });
9096
9426
 
@@ -9113,8 +9443,10 @@ var init_graph_json = __esm({
9113
9443
 
9114
9444
  // src/pipeline.ts
9115
9445
  function buildIndexArtifacts(repo, opts = {}) {
9116
- const scan2 = scanRepo(repo, opts);
9117
- const ctx = buildResolveContext(scan2);
9446
+ return buildArtifactsFromScan(scanRepo(repo, opts), opts);
9447
+ }
9448
+ function buildArtifactsFromScan(scan2, opts = {}) {
9449
+ const ctx = resolveContextFor(scan2);
9118
9450
  const { modules, moduleOf } = buildModules(scan2);
9119
9451
  const graph = buildGraph(scan2, ctx, modules, moduleOf, opts.meta);
9120
9452
  const communities = detectCommunities(graph.modules, graph.moduleEdges, opts.previousCommunities);
@@ -9133,14 +9465,14 @@ function buildIndexArtifacts(repo, opts = {}) {
9133
9465
  }
9134
9466
  const surprises = computeSurprises(graph);
9135
9467
  if (surprises.length) graph.surprises = surprises;
9136
- const symbols = buildSymbolIndex(scan2, computeSymbolRefs(scan2));
9468
+ const symbols = buildSymbolIndex(scan2, symbolRefsFor(scan2));
9137
9469
  return { scan: scan2, graph, symbols };
9138
9470
  }
9139
9471
  var init_pipeline = __esm({
9140
9472
  "src/pipeline.ts"() {
9141
9473
  "use strict";
9142
9474
  init_scan();
9143
- init_resolve();
9475
+ init_derived();
9144
9476
  init_modules();
9145
9477
  init_graph();
9146
9478
  init_community();
@@ -9168,253 +9500,81 @@ function rgBackend(root, pattern, opts) {
9168
9500
  "--no-ignore-global",
9169
9501
  "--no-ignore-exclude",
9170
9502
  "--no-ignore-parent",
9171
- "--no-ignore-dot",
9172
- "--max-filesize",
9173
- "1M"
9174
- ];
9175
- for (const d of IGNORE_DIRS) args2.push("--glob", `!**/${d}/**`);
9176
- for (const l of LOCKFILES) args2.push("--iglob", `!**/${l}`);
9177
- for (const ext of BINARY_EXT) args2.push("--iglob", `!**/*${ext}`);
9178
- args2.push("--glob", "!*.min.js", "--glob", "!*.min.css");
9179
- if (opts.ignoreCase) args2.push("--ignore-case");
9180
- const user = opts.globs ?? [];
9181
- const anchor = (g) => g.startsWith("/") ? g : `/${g}`;
9182
- for (const g of user.filter((g2) => !g2.startsWith("!"))) args2.push("--glob", anchor(g));
9183
- for (const g of user.filter((g2) => g2.startsWith("!"))) args2.push("--glob", `!${anchor(g.slice(1))}`);
9184
- args2.push("--regexp", pattern, "./");
9185
- const res = sh("rg", args2, { cwd: root });
9186
- if (res.missing || !res.ok && res.status !== 1) return void 0;
9187
- const hits = [];
9188
- for (const line of res.stdout.split("\n")) {
9189
- if (!line) continue;
9190
- const nul = line.indexOf("\0");
9191
- if (nul === -1) continue;
9192
- const file = line.slice(0, nul).replace(/^\.\//, "");
9193
- const rest = line.slice(nul + 1);
9194
- const colon = rest.indexOf(":");
9195
- if (colon === -1) continue;
9196
- hits.push({ file, line: Number(rest.slice(0, colon)), text: rest.slice(colon + 1) });
9197
- }
9198
- return hits;
9199
- }
9200
- function jsBackend(root, re, opts) {
9201
- const filter = compileGlobFilter(opts.globs?.map((g) => g.replace(/^(!?)\//, "$1")));
9202
- const hits = [];
9203
- for (const f of walk(root).files) {
9204
- if (filter && !filter(f.rel)) continue;
9205
- const content = readText(f.abs);
9206
- if (!content) continue;
9207
- const lines = content.split("\n");
9208
- for (let i2 = 0; i2 < lines.length; i2++) {
9209
- if (re.test(lines[i2])) hits.push({ file: f.rel, line: i2 + 1, text: lines[i2] });
9210
- }
9211
- }
9212
- return hits;
9213
- }
9214
- function grepRepo(root, pattern, opts = {}) {
9215
- const re = new RegExp(pattern, opts.ignoreCase ? "i" : "");
9216
- const max = opts.maxHits ?? DEFAULT_MAX_HITS;
9217
- let hits;
9218
- if (!opts.noRipgrep && have("rg")) hits = rgBackend(root, pattern, opts);
9219
- hits ??= jsBackend(root, re, opts);
9220
- return sortHits(hits).slice(0, max);
9221
- }
9222
- var DEFAULT_MAX_HITS;
9223
- var init_grep = __esm({
9224
- "src/grep.ts"() {
9225
- "use strict";
9226
- init_walk();
9227
- init_glob();
9228
- init_util();
9229
- init_sort();
9230
- DEFAULT_MAX_HITS = 200;
9231
- }
9232
- });
9233
-
9234
- // src/bm25.ts
9235
- function subtokens(raw) {
9236
- const folded = foldText(raw).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
9237
- const out2 = [];
9238
- const seen = /* @__PURE__ */ new Set();
9239
- const push = (t) => {
9240
- if (t.length < 2 || seen.has(t)) return;
9241
- seen.add(t);
9242
- out2.push(t);
9243
- };
9244
- if (!/\s/.test(raw.trim())) push(foldText(raw).toLowerCase().replace(/[^a-z0-9_]+/g, ""));
9245
- for (const part of folded.split(/[^A-Za-z0-9]+/)) push(part.toLowerCase());
9246
- return out2;
9247
- }
9248
- function addTerms(doc, text) {
9249
- for (const t of subtokens(text)) {
9250
- doc.tf.set(t, (doc.tf.get(t) ?? 0) + 1);
9251
- doc.len++;
9252
- }
9253
- }
9254
- function buildDocs(scan2) {
9255
- const docs = [];
9256
- for (const f of scan2.files) {
9257
- const doc = { file: f.rel, tf: /* @__PURE__ */ new Map(), len: 0, symbols: [] };
9258
- const seenSym = /* @__PURE__ */ new Set();
9259
- for (const s of f.symbols) {
9260
- addTerms(doc, s.name);
9261
- if (!seenSym.has(s.name)) {
9262
- seenSym.add(s.name);
9263
- doc.symbols.push(s.name);
9264
- }
9265
- }
9266
- for (const seg of f.rel.split("/")) addTerms(doc, seg);
9267
- for (const h of f.headings) addTerms(doc, h);
9268
- if (f.summary) addTerms(doc, f.summary);
9269
- docs.push(doc);
9270
- }
9271
- return docs;
9272
- }
9273
- function charTrigrams(term) {
9274
- const padded = `^^${term}$$`;
9275
- const grams = /* @__PURE__ */ new Set();
9276
- for (let i2 = 0; i2 + 3 <= padded.length; i2++) grams.add(padded.slice(i2, i2 + 3));
9277
- return grams;
9278
- }
9279
- function diceCoefficient(a, b) {
9280
- if (!a.size || !b.size) return 0;
9281
- let inter = 0;
9282
- for (const g of a) if (b.has(g)) inter++;
9283
- return 2 * inter / (a.size + b.size);
9284
- }
9285
- function buildTrigramIndex(docs) {
9286
- const index = /* @__PURE__ */ new Map();
9287
- for (const d of docs) {
9288
- for (const term of d.tf.keys()) {
9289
- if (!index.has(term)) index.set(term, charTrigrams(term));
9290
- }
9291
- }
9292
- return index;
9293
- }
9294
- function searchIndex(scan2, query, opts = {}) {
9295
- const terms = [];
9296
- const seen = /* @__PURE__ */ new Set();
9297
- for (const kw of keywords(query)) {
9298
- for (const t of subtokens(kw)) {
9299
- if (seen.has(t)) continue;
9300
- seen.add(t);
9301
- terms.push(t);
9302
- }
9303
- }
9304
- if (!terms.length) return [];
9305
- const docs = buildDocs(scan2);
9306
- const n = docs.length;
9307
- if (!n) return [];
9308
- let totalLen = 0;
9309
- for (const d of docs) totalLen += d.len;
9310
- const avgLen = totalLen / n || 1;
9311
- const df = /* @__PURE__ */ new Map();
9312
- for (const t of terms) {
9313
- let count = 0;
9314
- for (const d of docs) if (d.tf.has(t)) count++;
9315
- df.set(t, count);
9316
- }
9317
- const fuzzyEnabled = opts.fuzzy ?? true;
9318
- const fuzzyCandidates = /* @__PURE__ */ new Map();
9319
- if (fuzzyEnabled) {
9320
- const unmatched = terms.filter((t) => df.get(t) === 0);
9321
- if (unmatched.length) {
9322
- const trigramIndex = buildTrigramIndex(docs);
9323
- for (const t of unmatched) {
9324
- const grams = charTrigrams(t);
9325
- const candidates = [];
9326
- for (const [vocabTerm, vocabGrams] of trigramIndex) {
9327
- const dice = diceCoefficient(grams, vocabGrams);
9328
- if (dice >= FUZZY_DICE_THRESHOLD) candidates.push({ term: vocabTerm, dice });
9329
- }
9330
- candidates.sort((a, b) => b.dice - a.dice || byStr(a.term, b.term));
9331
- fuzzyCandidates.set(t, candidates.slice(0, FUZZY_CAP));
9332
- }
9333
- }
9334
- }
9335
- const vocabDf = /* @__PURE__ */ new Map();
9336
- const dfOfVocabTerm = (term) => {
9337
- const known = df.get(term) ?? vocabDf.get(term);
9338
- if (known !== void 0) return known;
9339
- let count = 0;
9340
- for (const d of docs) if (d.tf.has(term)) count++;
9341
- vocabDf.set(term, count);
9342
- return count;
9343
- };
9344
- const results = [];
9345
- for (const d of docs) {
9346
- let score = 0;
9347
- const matched = [];
9348
- const symbolTerms = /* @__PURE__ */ new Set();
9349
- const fuzzyHit = /* @__PURE__ */ new Set();
9350
- for (const t of terms) {
9351
- const tf = d.tf.get(t);
9352
- if (tf) {
9353
- matched.push(t);
9354
- symbolTerms.add(t);
9355
- const idf = Math.log(1 + (n - df.get(t) + 0.5) / (df.get(t) + 0.5));
9356
- score += idf * (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * d.len / avgLen));
9357
- continue;
9358
- }
9359
- const candidates = fuzzyCandidates.get(t);
9360
- if (!candidates) continue;
9361
- for (const cand of candidates) {
9362
- const ctf = d.tf.get(cand.term);
9363
- if (!ctf) continue;
9364
- const cdf = dfOfVocabTerm(cand.term);
9365
- const idf = Math.log(1 + (n - cdf + 0.5) / (cdf + 0.5));
9366
- const contribution = idf * (ctf * (K1 + 1)) / (ctf + K1 * (1 - B + B * d.len / avgLen));
9367
- score += contribution * cand.dice;
9368
- symbolTerms.add(cand.term);
9369
- fuzzyHit.add(t);
9370
- }
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] });
9371
9542
  }
9372
- if (!matched.length && !fuzzyHit.size) continue;
9373
- const scored = d.symbols.map((name2) => {
9374
- const toks = new Set(subtokens(name2));
9375
- let hits = 0;
9376
- for (const t of symbolTerms) if (toks.has(t)) hits++;
9377
- return { name: name2, hits };
9378
- }).filter((s) => s.hits > 0).sort((a, b) => b.hits - a.hits || byStr(a.name, b.name));
9379
- const result = {
9380
- file: d.file,
9381
- score: Number(score.toFixed(4)),
9382
- matchedTerms: matched.sort(byStr),
9383
- topSymbols: scored.slice(0, TOP_SYMBOLS).map((s) => s.name)
9384
- };
9385
- if (fuzzyHit.size) result.fuzzyTerms = [...fuzzyHit].sort(byStr);
9386
- results.push(result);
9387
9543
  }
9388
- results.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
9389
- return results.slice(0, opts.limit ?? DEFAULT_LIMIT);
9544
+ return hits;
9390
9545
  }
9391
- var K1, B, DEFAULT_LIMIT, TOP_SYMBOLS, FUZZY_DICE_THRESHOLD, FUZZY_CAP;
9392
- var init_bm25 = __esm({
9393
- "src/bm25.ts"() {
9546
+ function grepRepo(root, pattern, opts = {}) {
9547
+ const re = new RegExp(pattern, opts.ignoreCase ? "i" : "");
9548
+ const max = opts.maxHits ?? DEFAULT_MAX_HITS;
9549
+ let hits;
9550
+ if (!opts.noRipgrep && have("rg")) hits = rgBackend(root, pattern, opts);
9551
+ hits ??= jsBackend(root, re, opts);
9552
+ return sortHits(hits).slice(0, max);
9553
+ }
9554
+ var DEFAULT_MAX_HITS;
9555
+ var init_grep = __esm({
9556
+ "src/grep.ts"() {
9394
9557
  "use strict";
9558
+ init_walk();
9559
+ init_glob();
9395
9560
  init_util();
9396
9561
  init_sort();
9397
- K1 = 1.2;
9398
- B = 0.75;
9399
- DEFAULT_LIMIT = 20;
9400
- TOP_SYMBOLS = 5;
9401
- FUZZY_DICE_THRESHOLD = 0.6;
9402
- FUZZY_CAP = 3;
9562
+ DEFAULT_MAX_HITS = 200;
9403
9563
  }
9404
9564
  });
9405
9565
 
9406
9566
  // src/embed/model.ts
9407
- import { createHash as createHash2 } from "crypto";
9567
+ import { createHash as createHash3 } from "crypto";
9408
9568
  import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
9409
- import { join as join10 } from "path";
9569
+ import { join as join13 } from "path";
9410
9570
  function resolveEmbedModelDir(repo) {
9411
9571
  const env = process.env.CODEINDEX_EMBED_DIR;
9412
9572
  const candidates = [];
9413
9573
  if (env) candidates.push(env);
9414
- if (repo) candidates.push(join10(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9415
- candidates.push(join10(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9574
+ if (repo) candidates.push(join13(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9575
+ candidates.push(join13(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9416
9576
  for (const c2 of candidates) {
9417
- if (existsSync3(join10(c2, "model.json"))) return c2;
9577
+ if (existsSync3(join13(c2, "model.json"))) return c2;
9418
9578
  }
9419
9579
  return void 0;
9420
9580
  }
@@ -9447,7 +9607,7 @@ function parseEmbedModel(raw, source) {
9447
9607
  }
9448
9608
  function loadEmbedModel(dir) {
9449
9609
  if (!dir) return void 0;
9450
- const path = join10(dir, "model.json");
9610
+ const path = join13(dir, "model.json");
9451
9611
  if (!existsSync3(path)) return void 0;
9452
9612
  const raw = JSON.parse(readFileSync5(path, "utf8"));
9453
9613
  return parseEmbedModel(raw, path);
@@ -9462,7 +9622,7 @@ async function fetchEmbedModel(url, expectedSha256) {
9462
9622
  if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9463
9623
  const body2 = await res.text();
9464
9624
  if (expectedSha256) {
9465
- const got = createHash2("sha256").update(body2).digest("hex");
9625
+ const got = createHash3("sha256").update(body2).digest("hex");
9466
9626
  if (got !== expectedSha256) {
9467
9627
  throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
9468
9628
  }
@@ -10066,8 +10226,8 @@ var init_repomap = __esm({
10066
10226
 
10067
10227
  // src/deadcode.ts
10068
10228
  function findDeadCode(scan2) {
10069
- const callers = buildCallerIndex(scan2);
10070
- const refs = computeSymbolRefs(scan2);
10229
+ const callers = callerIndexFor(scan2);
10230
+ const refs = symbolRefsFor(scan2);
10071
10231
  const out2 = [];
10072
10232
  const consider = (s) => s.exported && !REFERENCE_KINDS6.has(s.kind) && !isTestPath(s.file) && !ENTRYPOINT_RE.test(s.file);
10073
10233
  for (const f of scan2.files) {
@@ -10086,8 +10246,7 @@ var REFERENCE_KINDS6, ENTRYPOINT_RE;
10086
10246
  var init_deadcode = __esm({
10087
10247
  "src/deadcode.ts"() {
10088
10248
  "use strict";
10089
- init_callers();
10090
- init_symbols_json();
10249
+ init_derived();
10091
10250
  init_tests_map();
10092
10251
  init_sort();
10093
10252
  REFERENCE_KINDS6 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
@@ -10095,49 +10254,6 @@ var init_deadcode = __esm({
10095
10254
  }
10096
10255
  });
10097
10256
 
10098
- // src/complexity.ts
10099
- import { join as join11 } from "path";
10100
- function complexityOfSource(source) {
10101
- return 1 + (source.match(BRANCH_RE) ?? []).length;
10102
- }
10103
- function symbolComplexity(scan2, rel, top = 50) {
10104
- const out2 = [];
10105
- for (const f of scan2.files) {
10106
- if (f.kind !== "code") continue;
10107
- if (rel && f.rel !== rel) continue;
10108
- if (!f.symbols.length) continue;
10109
- const lines = readText(join11(scan2.root, f.rel)).split("\n");
10110
- for (const s of f.symbols) {
10111
- if (s.kind === "reexport" || s.kind === "reexport-all") continue;
10112
- const end = s.endLine ?? s.line;
10113
- const body2 = lines.slice(s.line - 1, end).join("\n");
10114
- const entry = { file: f.rel, name: s.name, line: s.line, complexity: complexityOfSource(body2) };
10115
- if (s.endLine !== void 0) entry.endLine = s.endLine;
10116
- out2.push(entry);
10117
- }
10118
- }
10119
- out2.sort((a, b) => b.complexity - a.complexity || byStr(a.file, b.file) || a.line - b.line);
10120
- return out2.slice(0, top);
10121
- }
10122
- function riskHotspots(scan2, churn, top = 20) {
10123
- const out2 = scan2.files.filter((f) => f.kind === "code").map((f) => {
10124
- const complexity = complexityOfSource(readText(join11(scan2.root, f.rel)));
10125
- const commits = churn.get(f.rel) ?? 0;
10126
- return { file: f.rel, complexity, commits, score: (commits + 1) * complexity };
10127
- });
10128
- out2.sort((a, b) => b.score - a.score || byStr(a.file, b.file));
10129
- return out2.slice(0, top);
10130
- }
10131
- var BRANCH_RE;
10132
- var init_complexity = __esm({
10133
- "src/complexity.ts"() {
10134
- "use strict";
10135
- init_walk();
10136
- init_sort();
10137
- BRANCH_RE = /\b(if|elif|elsif|else\s+if|for|foreach|while|until|unless|case|when|match|catch|rescue|except)\b|&&|\|\||(?<![?:])\?(?![?.:])/g;
10138
- }
10139
- });
10140
-
10141
10257
  // src/viz.ts
10142
10258
  function renderMermaid(graph, opts = {}) {
10143
10259
  const maxEdges = opts.maxEdges ?? 80;
@@ -10178,13 +10294,17 @@ var init_viz = __esm({
10178
10294
  // src/mcp.ts
10179
10295
  var mcp_exports = {};
10180
10296
  __export(mcp_exports, {
10297
+ getArtifacts: () => getArtifacts,
10298
+ getScan: () => getScan,
10181
10299
  memoizedEmbedModel: () => memoizedEmbedModel,
10182
10300
  memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
10183
10301
  runMcpServer: () => runMcpServer,
10184
- scanFingerprint: () => scanFingerprint
10302
+ scanFingerprint: () => scanFingerprint,
10303
+ toCacheMap: () => toCacheMap,
10304
+ warmGrammarsForRepo: () => warmGrammarsForRepo
10185
10305
  });
10186
- import { statSync as statSync4 } from "fs";
10187
- import { join as join12 } from "path";
10306
+ import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
10307
+ import { join as join14 } from "path";
10188
10308
  import { createInterface } from "readline";
10189
10309
  function str(v) {
10190
10310
  return typeof v === "string" && v ? v : void 0;
@@ -10208,7 +10328,7 @@ async function memoizedEmbeddingIndex(key, build) {
10208
10328
  function memoizedEmbedModel(modelDir) {
10209
10329
  let stat;
10210
10330
  try {
10211
- stat = statSync4(join12(modelDir, "model.json"));
10331
+ stat = statSync4(join14(modelDir, "model.json"));
10212
10332
  } catch {
10213
10333
  return void 0;
10214
10334
  }
@@ -10218,12 +10338,115 @@ function memoizedEmbedModel(modelDir) {
10218
10338
  if (model) embedModelCache = { key, model };
10219
10339
  return model;
10220
10340
  }
10341
+ function sessionKey(repo, opts) {
10342
+ return repo + "\0" + JSON.stringify({
10343
+ scope: opts.scope,
10344
+ include: opts.include,
10345
+ exclude: opts.exclude,
10346
+ gitignore: opts.gitignore,
10347
+ ignoreDirs: opts.ignoreDirs,
10348
+ maxBytes: opts.maxBytes,
10349
+ maxFiles: opts.maxFiles,
10350
+ maxCallsPerFile: opts.maxCallsPerFile,
10351
+ out: opts.out,
10352
+ fullHash: opts.fullHash
10353
+ });
10354
+ }
10355
+ function toCacheMap(scan2) {
10356
+ const m = /* @__PURE__ */ new Map();
10357
+ for (const f of scan2.files) m.set(f.rel, { hash: f.hash, record: f, size: f.size, mtimeMs: scan2.mtimes.get(f.rel) });
10358
+ return m;
10359
+ }
10360
+ function readPersistedIndex(repo) {
10361
+ let parsed;
10362
+ try {
10363
+ parsed = JSON.parse(readFileSync6(join14(repo, ".codeindex", "cache.json"), "utf8"));
10364
+ } catch {
10365
+ return void 0;
10366
+ }
10367
+ if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION || parsed.extractorVersion !== EXTRACTOR_VERSION || !parsed.files) {
10368
+ return void 0;
10369
+ }
10370
+ const cacheMap = new Map(Object.entries(parsed.files));
10371
+ const meta = {
10372
+ engineVersion: parsed.engineVersion,
10373
+ commit: parsed.commit,
10374
+ graphSha1: parsed.graphSha1,
10375
+ symbolsSha1: parsed.symbolsSha1
10376
+ };
10377
+ return { cacheMap, meta };
10378
+ }
10379
+ function preloadArtifacts(repo, scan2, meta) {
10380
+ if (!scan2.contentUnchanged || meta.engineVersion !== ENGINE_VERSION || meta.commit !== scan2.commit || meta.graphSha1 === void 0 || meta.symbolsSha1 === void 0) {
10381
+ return void 0;
10382
+ }
10383
+ const dir = join14(repo, ".codeindex");
10384
+ let graphBytes;
10385
+ let symbolsBytes;
10386
+ try {
10387
+ graphBytes = readFileSync6(join14(dir, "graph.json"));
10388
+ symbolsBytes = readFileSync6(join14(dir, "symbols.json"));
10389
+ } catch {
10390
+ return void 0;
10391
+ }
10392
+ if (sha1(graphBytes) !== meta.graphSha1 || sha1(symbolsBytes) !== meta.symbolsSha1) {
10393
+ return void 0;
10394
+ }
10395
+ try {
10396
+ const graph = JSON.parse(graphBytes.toString("utf8"));
10397
+ const symbols = JSON.parse(symbolsBytes.toString("utf8"));
10398
+ if (graph.schemaVersion !== SCHEMA_VERSION || symbols.schemaVersion !== SCHEMA_VERSION) return void 0;
10399
+ return { scan: scan2, graph, symbols };
10400
+ } catch {
10401
+ return void 0;
10402
+ }
10403
+ }
10404
+ function preloadSession(repo, opts) {
10405
+ const persisted = readPersistedIndex(repo);
10406
+ if (!persisted) return void 0;
10407
+ const scan2 = scanRepo(repo, { ...opts, cache: persisted.cacheMap });
10408
+ const arts = preloadArtifacts(repo, scan2, persisted.meta);
10409
+ return { scan: scan2, cacheMap: toCacheMap(scan2), arts };
10410
+ }
10411
+ function getScan(repo, opts = {}) {
10412
+ const key = sessionKey(repo, opts);
10413
+ if (sessionCache && sessionCache.key === key) {
10414
+ const fresh = scanRepo(repo, { ...opts, cache: sessionCache.cacheMap });
10415
+ if (fresh.contentUnchanged) {
10416
+ if (fresh.cacheDirty) sessionCache.cacheMap = toCacheMap(fresh);
10417
+ if (sessionCache.scan.commit !== fresh.commit) sessionCache.scan.commit = fresh.commit;
10418
+ return sessionCache.scan;
10419
+ }
10420
+ sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
10421
+ return fresh;
10422
+ }
10423
+ const preloaded = preloadSession(repo, opts);
10424
+ if (preloaded) {
10425
+ sessionCache = { key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts };
10426
+ return preloaded.scan;
10427
+ }
10428
+ const scan2 = scanRepo(repo, opts);
10429
+ sessionCache = { key, scan: scan2, cacheMap: toCacheMap(scan2) };
10430
+ return scan2;
10431
+ }
10432
+ function getArtifacts(repo, opts = {}) {
10433
+ const scan2 = getScan(repo, opts);
10434
+ if (sessionCache && sessionCache.scan === scan2) {
10435
+ return sessionCache.arts ??= buildArtifactsFromScan(scan2, opts);
10436
+ }
10437
+ return buildArtifactsFromScan(scan2, opts);
10438
+ }
10439
+ async function warmGrammarsForRepo(repo) {
10440
+ const { files } = walk(repo, {});
10441
+ await ensureGrammars(grammarKeysForExts(files.map((f) => f.ext)));
10442
+ }
10221
10443
  async function callTool(name2, args2) {
10222
10444
  const repo = str(args2.repo);
10223
10445
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10224
10446
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
10447
+ if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
10225
10448
  if (name2 === "scan_summary") {
10226
- const scan2 = scanRepo(repo, scanOpts);
10449
+ const scan2 = getScan(repo, scanOpts);
10227
10450
  return JSON.stringify(
10228
10451
  { engineVersion: ENGINE_VERSION, commit: scan2.commit, fileCount: scan2.files.length, languages: scan2.languages, capped: scan2.capped },
10229
10452
  null,
@@ -10231,10 +10454,10 @@ async function callTool(name2, args2) {
10231
10454
  );
10232
10455
  }
10233
10456
  if (name2 === "graph") {
10234
- return renderGraphJson(buildIndexArtifacts(repo, scanOpts).graph);
10457
+ return renderGraphJson(getArtifacts(repo, scanOpts).graph);
10235
10458
  }
10236
10459
  if (name2 === "symbols") {
10237
- const { symbols } = buildIndexArtifacts(repo, scanOpts);
10460
+ const { symbols } = getArtifacts(repo, scanOpts);
10238
10461
  const lookup = str(args2.name);
10239
10462
  if (lookup) {
10240
10463
  return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
@@ -10242,7 +10465,7 @@ async function callTool(name2, args2) {
10242
10465
  return JSON.stringify(symbols, null, 2);
10243
10466
  }
10244
10467
  if (name2 === "callers") {
10245
- const index = buildCallerIndex(scanRepo(repo, scanOpts));
10468
+ const index = buildCallerIndex(getScan(repo, scanOpts));
10246
10469
  const lookup = str(args2.name);
10247
10470
  if (lookup) {
10248
10471
  const entry = index.get(lookup);
@@ -10265,12 +10488,12 @@ async function callTool(name2, args2) {
10265
10488
  if (name2 === "symbols_overview") {
10266
10489
  const file = str(args2.file);
10267
10490
  if (!file) throw new Error("`file` is required");
10268
- return JSON.stringify(symbolsOverview(scanRepo(repo, scanOpts), file), null, 2);
10491
+ return JSON.stringify(symbolsOverview(getScan(repo, scanOpts), file), null, 2);
10269
10492
  }
10270
10493
  if (name2 === "find_symbol") {
10271
10494
  const namePath = str(args2.namePath);
10272
10495
  if (!namePath) throw new Error("`namePath` is required");
10273
- const matches = findSymbol(scanRepo(repo, scanOpts), namePath, {
10496
+ const matches = findSymbol(getScan(repo, scanOpts), namePath, {
10274
10497
  substring: args2.substring === true,
10275
10498
  includeBody: args2.includeBody === true
10276
10499
  });
@@ -10279,15 +10502,17 @@ async function callTool(name2, args2) {
10279
10502
  if (name2 === "find_references") {
10280
10503
  const symName = str(args2.name);
10281
10504
  if (!symName) throw new Error("`name` is required");
10282
- return JSON.stringify(findReferences(scanRepo(repo, scanOpts), symName), null, 2);
10505
+ return JSON.stringify(findReferences(getScan(repo, scanOpts), symName), null, 2);
10283
10506
  }
10284
10507
  if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
10285
10508
  const namePath = str(args2.namePath);
10286
10509
  const body2 = typeof args2.body === "string" ? args2.body : void 0;
10287
10510
  if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
10288
- const scan2 = scanRepo(repo, scanOpts);
10511
+ const scan2 = getScan(repo, scanOpts);
10289
10512
  const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
10290
- return JSON.stringify(fn(scan2, namePath, body2, str(args2.file)), null, 2);
10513
+ const result = fn(scan2, namePath, body2, str(args2.file));
10514
+ sessionCache = void 0;
10515
+ return JSON.stringify(result, null, 2);
10291
10516
  }
10292
10517
  if (name2 === "write_memory") {
10293
10518
  const memName = str(args2.name);
@@ -10311,10 +10536,10 @@ async function callTool(name2, args2) {
10311
10536
  return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
10312
10537
  }
10313
10538
  if (name2 === "dead_code") {
10314
- return JSON.stringify(findDeadCode(scanRepo(repo, scanOpts)), null, 2);
10539
+ return JSON.stringify(findDeadCode(getScan(repo, scanOpts)), null, 2);
10315
10540
  }
10316
10541
  if (name2 === "complexity") {
10317
- const scan2 = scanRepo(repo, scanOpts);
10542
+ const scan2 = getScan(repo, scanOpts);
10318
10543
  if (args2.risk === true) {
10319
10544
  const { churn, ok } = gitChurn(repo);
10320
10545
  return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2);
@@ -10322,15 +10547,15 @@ async function callTool(name2, args2) {
10322
10547
  return JSON.stringify(symbolComplexity(scan2, str(args2.file)), null, 2);
10323
10548
  }
10324
10549
  if (name2 === "mermaid") {
10325
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10550
+ const { graph } = getArtifacts(repo, scanOpts);
10326
10551
  return renderMermaid(graph, { module: str(args2.module) });
10327
10552
  }
10328
10553
  if (name2 === "repo_map") {
10329
- const { scan: scan2, graph } = buildIndexArtifacts(repo, scanOpts);
10554
+ const { scan: scan2, graph } = getArtifacts(repo, scanOpts);
10330
10555
  return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
10331
10556
  }
10332
10557
  if (name2 === "hotspots") {
10333
- const scan2 = scanRepo(repo, scanOpts);
10558
+ const scan2 = getScan(repo, scanOpts);
10334
10559
  const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10335
10560
  return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
10336
10561
  }
@@ -10351,7 +10576,7 @@ async function callTool(name2, args2) {
10351
10576
  if (name2 === "search") {
10352
10577
  const query = str(args2.query);
10353
10578
  if (!query) throw new Error("`query` is required");
10354
- const scan2 = scanRepo(repo, scanOpts);
10579
+ const scan2 = getScan(repo, scanOpts);
10355
10580
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10356
10581
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10357
10582
  if (args2.semantic === true) {
@@ -10406,13 +10631,16 @@ async function callTool(name2, args2) {
10406
10631
  }
10407
10632
  if (name2 === "check_rules") {
10408
10633
  const rules = parseRules(args2.rules);
10409
- const { graph } = buildIndexArtifacts(repo, scanOpts);
10634
+ const { graph } = getArtifacts(repo, scanOpts);
10410
10635
  return JSON.stringify(checkRules(graph, rules), null, 2);
10411
10636
  }
10412
10637
  throw new Error(`unknown tool: ${name2}`);
10413
10638
  }
10414
- async function runMcpServer() {
10415
- await ensureGrammars(allGrammarKeys());
10639
+ async function runMcpServer(opts = {}) {
10640
+ const serverInfo = {
10641
+ name: opts.serverInfo?.name ?? "codeindex",
10642
+ version: opts.serverInfo?.version ?? ENGINE_VERSION
10643
+ };
10416
10644
  const send = (msg) => {
10417
10645
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
10418
10646
  };
@@ -10439,7 +10667,7 @@ async function runMcpServer() {
10439
10667
  result: {
10440
10668
  protocolVersion: "2024-11-05",
10441
10669
  capabilities: { tools: {} },
10442
- serverInfo: { name: "codeindex", version: ENGINE_VERSION }
10670
+ serverInfo
10443
10671
  }
10444
10672
  });
10445
10673
  } else if (req.method === "ping") {
@@ -10467,7 +10695,7 @@ async function runMcpServer() {
10467
10695
  }
10468
10696
  }
10469
10697
  }
10470
- var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache;
10698
+ var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, sessionCache, SCANLESS_TOOLS;
10471
10699
  var init_mcp = __esm({
10472
10700
  "src/mcp.ts"() {
10473
10701
  "use strict";
@@ -10476,6 +10704,7 @@ var init_mcp = __esm({
10476
10704
  init_pipeline();
10477
10705
  init_graph_json();
10478
10706
  init_scan();
10707
+ init_walk();
10479
10708
  init_callers();
10480
10709
  init_workspaces();
10481
10710
  init_git();
@@ -10746,6 +10975,17 @@ var init_mcp = __esm({
10746
10975
  }
10747
10976
  }
10748
10977
  ];
10978
+ SCANLESS_TOOLS = /* @__PURE__ */ new Set([
10979
+ "workspaces",
10980
+ "churn",
10981
+ "coupling",
10982
+ "grep",
10983
+ "write_memory",
10984
+ "read_memory",
10985
+ "list_memories",
10986
+ "delete_memory",
10987
+ "embed_status"
10988
+ ]);
10749
10989
  }
10750
10990
  });
10751
10991
 
@@ -10885,6 +11125,106 @@ init_code();
10885
11125
  init_markdown();
10886
11126
  init_loader();
10887
11127
  init_extract();
11128
+ init_loader();
11129
+
11130
+ // src/ast/grammars-pull.ts
11131
+ init_types();
11132
+ import { createHash as createHash2 } from "crypto";
11133
+ import { mkdirSync, writeFileSync } from "fs";
11134
+ import { dirname as dirname2, resolve, sep as sep2 } from "path";
11135
+ import { gunzipSync } from "zlib";
11136
+ var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
11137
+ function resolveGrammarsPullTarget() {
11138
+ const env = process.env.CODEINDEX_GRAMMARS_URL;
11139
+ if (env && env.trim()) return { url: env.trim() };
11140
+ return { url: DEFAULT_GRAMMARS_URL, sha256Url: `${DEFAULT_GRAMMARS_URL}.sha256` };
11141
+ }
11142
+ async function fetchGrammarsTarball(url, expectedSha256) {
11143
+ const res = await fetch(url);
11144
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
11145
+ const buf = Buffer.from(await res.arrayBuffer());
11146
+ if (expectedSha256) {
11147
+ const got = createHash2("sha256").update(buf).digest("hex");
11148
+ if (got !== expectedSha256) {
11149
+ throw new Error(`sha256 mismatch: expected ${expectedSha256}, got ${got}`);
11150
+ }
11151
+ }
11152
+ return buf;
11153
+ }
11154
+ function asBuffer(u) {
11155
+ return Buffer.isBuffer(u) ? u : Buffer.from(u.buffer, u.byteOffset, u.byteLength);
11156
+ }
11157
+ async function fetchExpectedSha256(url) {
11158
+ const res = await fetch(url);
11159
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
11160
+ const text = await res.text();
11161
+ const hex = (text.trim().split(/\s+/)[0] ?? "").toLowerCase();
11162
+ if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error(`invalid sha256 sidecar at ${url}`);
11163
+ return hex;
11164
+ }
11165
+ function cstr(block, start2, len) {
11166
+ const slice = block.subarray(start2, start2 + len);
11167
+ const nul = slice.indexOf(0);
11168
+ return slice.toString("utf8", 0, nul === -1 ? slice.length : nul);
11169
+ }
11170
+ function* readTar(buf) {
11171
+ let off = 0;
11172
+ while (off + 512 <= buf.length) {
11173
+ const block = buf.subarray(off, off + 512);
11174
+ let allZero = true;
11175
+ for (let i2 = 0; i2 < 512; i2++) {
11176
+ if (block[i2] !== 0) {
11177
+ allZero = false;
11178
+ break;
11179
+ }
11180
+ }
11181
+ if (allZero) break;
11182
+ const name2 = cstr(block, 0, 100);
11183
+ const prefix = cstr(block, 345, 155);
11184
+ const sizeStr = cstr(block, 124, 12).trim();
11185
+ const size = sizeStr ? parseInt(sizeStr, 8) : 0;
11186
+ const type = String.fromCharCode(block[156] ?? 0);
11187
+ off += 512;
11188
+ const data = buf.subarray(off, off + size);
11189
+ off += Math.ceil(size / 512) * 512;
11190
+ yield { name: prefix ? `${prefix}/${name2}` : name2, type, data };
11191
+ }
11192
+ }
11193
+ function safeRelPath(name2) {
11194
+ if (!name2 || name2.includes("\0")) return null;
11195
+ if (name2.startsWith("/") || name2.startsWith("\\") || /^[A-Za-z]:/.test(name2)) return null;
11196
+ const out2 = [];
11197
+ for (const part of name2.split(/[/\\]/)) {
11198
+ if (part === "" || part === ".") continue;
11199
+ if (part === "..") return null;
11200
+ out2.push(part);
11201
+ }
11202
+ return out2.length ? out2.join("/") : null;
11203
+ }
11204
+ function extractTarInto(rawTar, destDir) {
11205
+ const root = resolve(destDir);
11206
+ const written = [];
11207
+ for (const entry of readTar(asBuffer(rawTar))) {
11208
+ if (entry.type !== "0" && entry.type !== "\0") continue;
11209
+ const rel = safeRelPath(entry.name);
11210
+ if (rel === null) throw new Error(`refusing unsafe tar entry: ${entry.name}`);
11211
+ const dest = resolve(destDir, rel);
11212
+ if (dest !== root && !dest.startsWith(root + sep2)) {
11213
+ throw new Error(`tar entry escapes destination: ${entry.name}`);
11214
+ }
11215
+ mkdirSync(dirname2(dest), { recursive: true });
11216
+ writeFileSync(dest, entry.data);
11217
+ written.push(rel);
11218
+ }
11219
+ return written;
11220
+ }
11221
+ function extractGrammarsTarball(bytes, destDir) {
11222
+ const b = asBuffer(bytes);
11223
+ const raw = b.length >= 2 && b[0] === 31 && b[1] === 139 ? gunzipSync(b) : b;
11224
+ return extractTarInto(raw, destDir);
11225
+ }
11226
+
11227
+ // src/engine.ts
10888
11228
  init_resolve();
10889
11229
  init_modules();
10890
11230
  init_graph();
@@ -10905,7 +11245,7 @@ init_graph_json();
10905
11245
  init_types();
10906
11246
  init_walk();
10907
11247
  init_sort();
10908
- import { join as join9 } from "path";
11248
+ import { join as join12 } from "path";
10909
11249
  var utf8 = new TextEncoder();
10910
11250
  function pushVarint(out2, n) {
10911
11251
  if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
@@ -11075,7 +11415,7 @@ function renderScip(scan2, opts = {}) {
11075
11415
  };
11076
11416
  const documents = [];
11077
11417
  for (const f of docs) {
11078
- const text = readText(join9(scan2.root, f.rel));
11418
+ const text = readText(join12(scan2.root, f.rel));
11079
11419
  const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
11080
11420
  const locate = (lineNo, name2) => {
11081
11421
  const line = lines[lineNo - 1];
@@ -11166,12 +11506,14 @@ init_util();
11166
11506
  init_types();
11167
11507
  init_types();
11168
11508
  init_loader();
11509
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
11510
+ import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
11169
11511
  init_pipeline();
11512
+ init_hash();
11170
11513
  init_graph_json();
11171
11514
  init_symbols_json();
11172
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
11173
- import { join as join13, resolve } from "path";
11174
11515
  init_scan();
11516
+ init_walk();
11175
11517
  init_callers();
11176
11518
  init_workspaces();
11177
11519
  init_git();
@@ -11218,6 +11560,14 @@ Commands:
11218
11560
  the source with CODEINDEX_EMBED_URL
11219
11561
  embed serve Print (or --run) the docker command that starts the
11220
11562
  containerized embedding server (rich tier)
11563
+ grammars Tree-sitter wasm grammars (optional AST tier; regex without them).
11564
+ Precedence: bundle-adjacent > CODEINDEX_GRAMMARS_DIR > shared cache:
11565
+ grammars status Active tier (adjacent/env/cache/none), resolved
11566
+ dir, pinned ENGINE_VERSION, pull-needed (JSON)
11567
+ grammars pull Fetch the per-release grammars-<version>.tar.gz
11568
+ asset into the shared cache (sha256-verified,
11569
+ atomic). Override the source with
11570
+ CODEINDEX_GRAMMARS_URL
11221
11571
  rules Architecture rules (forbidden edges, cycles, orphans) validated
11222
11572
  against the link-graph: --config <codeindex.rules.json>; exits 1
11223
11573
  on any error-severity violation (a CI gate)
@@ -11271,10 +11621,10 @@ function parseFlags(args2) {
11271
11621
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
11272
11622
  return n;
11273
11623
  };
11274
- if (a === "--repo") flags2.repo = resolve(next());
11624
+ if (a === "--repo") flags2.repo = resolve2(next());
11275
11625
  else if (a === "--out") {
11276
11626
  const v = next();
11277
- flags2.out = v === "-" ? "-" : resolve(v);
11627
+ flags2.out = v === "-" ? "-" : resolve2(v);
11278
11628
  } else if (a === "--project-root") flags2.projectRoot = next();
11279
11629
  else if (a === "--include") flags2.include.push(next());
11280
11630
  else if (a === "--exclude") flags2.exclude.push(next());
@@ -11289,7 +11639,7 @@ function parseFlags(args2) {
11289
11639
  else if (a === "--budget-tokens") flags2.budgetTokens = num();
11290
11640
  else if (a === "--no-ast") flags2.noAst = true;
11291
11641
  else if (a === "--since") flags2.since = next();
11292
- else if (a === "--config") flags2.config = resolve(next());
11642
+ else if (a === "--config") flags2.config = resolve2(next());
11293
11643
  else if (a === "--limit") flags2.limit = num();
11294
11644
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11295
11645
  else if (a === "--semantic") flags2.semantic = true;
@@ -11301,10 +11651,10 @@ function parseFlags(args2) {
11301
11651
  return flags2;
11302
11652
  }
11303
11653
  function emit(content, out2) {
11304
- if (out2) writeFileSync3(out2, content);
11654
+ if (out2) writeFileSync4(out2, content);
11305
11655
  else process.stdout.write(content);
11306
11656
  }
11307
- function scanOptions(flags2) {
11657
+ function scanOptions(flags2, precomputedWalk) {
11308
11658
  return {
11309
11659
  include: flags2.include.length ? flags2.include : void 0,
11310
11660
  exclude: flags2.exclude.length ? flags2.exclude : void 0,
@@ -11313,9 +11663,14 @@ function scanOptions(flags2) {
11313
11663
  ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0,
11314
11664
  maxFiles: flags2.maxFiles,
11315
11665
  maxBytes: flags2.maxBytes,
11316
- maxCallsPerFile: flags2.maxCalls
11666
+ maxCallsPerFile: flags2.maxCalls,
11667
+ // The walk performed once in runCli to warm the present-language grammars,
11668
+ // reused here so scanRepo does not traverse the tree a second time. Absent
11669
+ // for --no-ast / scan-less commands: scanRepo walks itself, unchanged.
11670
+ precomputedWalk
11317
11671
  };
11318
11672
  }
11673
+ var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "workspaces", "grammars"]);
11319
11674
  async function runCli(argv) {
11320
11675
  const [cmd, ...rest] = argv;
11321
11676
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
@@ -11333,46 +11688,102 @@ async function runCli(argv) {
11333
11688
  }
11334
11689
  const flags2 = parseFlags(rest);
11335
11690
  if (!existsSync4(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11336
- if (!flags2.noAst) await ensureGrammars(allGrammarKeys());
11691
+ const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
11692
+ let precomputedWalk;
11693
+ if (scans && !flags2.noAst) {
11694
+ precomputedWalk = walk(flags2.repo, {
11695
+ maxFileBytes: flags2.maxBytes,
11696
+ maxFiles: flags2.maxFiles,
11697
+ gitignore: flags2.gitignore,
11698
+ ignoreDirs: flags2.ignoreDirs.length ? flags2.ignoreDirs : void 0
11699
+ });
11700
+ await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
11701
+ }
11337
11702
  if (cmd === "index") {
11338
11703
  if (!flags2.out) throw new Error("index needs --out <dir>");
11339
11704
  const outDir = flags2.out;
11340
- mkdirSync2(outDir, { recursive: true });
11341
- const cachePath = join13(outDir, "cache.json");
11705
+ mkdirSync3(outDir, { recursive: true });
11706
+ const cachePath = join15(outDir, "cache.json");
11342
11707
  let cache;
11708
+ let meta = {};
11343
11709
  try {
11344
- const parsed = JSON.parse(readFileSync6(cachePath, "utf8"));
11710
+ const parsed = JSON.parse(readFileSync7(cachePath, "utf8"));
11345
11711
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
11346
11712
  cache = new Map(Object.entries(parsed.files));
11713
+ meta = {
11714
+ engineVersion: parsed.engineVersion,
11715
+ commit: parsed.commit,
11716
+ graphSha1: parsed.graphSha1,
11717
+ symbolsSha1: parsed.symbolsSha1,
11718
+ embed: parsed.embed
11719
+ };
11347
11720
  }
11348
11721
  } catch {
11349
11722
  }
11350
- const { scan: scan2, graph, symbols } = buildIndexArtifacts(flags2.repo, { ...scanOptions(flags2), cache, out: outDir });
11351
- writeFileSync3(join13(outDir, "graph.json"), renderGraphJson(graph));
11352
- writeFileSync3(join13(outDir, "symbols.json"), renderSymbolsJson(symbols));
11353
- const files = {};
11354
- for (const f of scan2.files) {
11355
- const entry = { hash: f.hash, record: f, size: f.size };
11356
- const mtime = scan2.mtimes.get(f.rel);
11357
- if (mtime !== void 0) entry.mtimeMs = mtime;
11358
- files[f.rel] = entry;
11359
- }
11360
- writeFileSync3(
11361
- cachePath,
11362
- JSON.stringify({ schemaVersion: SCHEMA_VERSION, extractorVersion: EXTRACTOR_VERSION, files }) + "\n"
11363
- );
11364
- let embedNote = "";
11723
+ const scan2 = scanRepo(flags2.repo, { ...scanOptions(flags2, precomputedWalk), cache, out: outDir });
11365
11724
  const modelDir = resolveEmbedModelDir(flags2.repo);
11366
11725
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
11367
- if (model) {
11368
- const index = buildEmbeddingIndex(scan2, model);
11369
- writeFileSync3(join13(outDir, "embeddings.bin"), serializeEmbeddings(index));
11370
- embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
11371
- }
11372
- process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
11726
+ const graphPath = join15(outDir, "graph.json");
11727
+ const symbolsPath = join15(outDir, "symbols.json");
11728
+ const embedPath = join15(outDir, "embeddings.bin");
11729
+ const artifactSha = (path) => {
11730
+ try {
11731
+ return sha1(readFileSync7(path));
11732
+ } catch {
11733
+ return void 0;
11734
+ }
11735
+ };
11736
+ const writeCache = (out2) => {
11737
+ const files = {};
11738
+ for (const f of scan2.files) {
11739
+ const entry = { hash: f.hash, record: f, size: f.size };
11740
+ const mtime = scan2.mtimes.get(f.rel);
11741
+ if (mtime !== void 0) entry.mtimeMs = mtime;
11742
+ files[f.rel] = entry;
11743
+ }
11744
+ writeFileSync4(
11745
+ cachePath,
11746
+ JSON.stringify({
11747
+ schemaVersion: SCHEMA_VERSION,
11748
+ extractorVersion: EXTRACTOR_VERSION,
11749
+ engineVersion: ENGINE_VERSION,
11750
+ commit: scan2.commit,
11751
+ graphSha1: out2.graphSha1,
11752
+ symbolsSha1: out2.symbolsSha1,
11753
+ embed: out2.embed,
11754
+ files
11755
+ }) + "\n"
11756
+ );
11757
+ };
11758
+ 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;
11759
+ 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;
11760
+ if (fastpath) {
11761
+ if (scan2.cacheDirty) writeCache(meta);
11762
+ process.stderr.write(
11763
+ `codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${scan2.capped ? " (capped)" : ""} (unchanged \u2014 artifacts reused)
11764
+ `
11765
+ );
11766
+ } else {
11767
+ const { graph, symbols } = buildArtifactsFromScan(scan2);
11768
+ const graphJson = renderGraphJson(graph);
11769
+ const symbolsJson = renderSymbolsJson(symbols);
11770
+ writeFileSync4(graphPath, graphJson);
11771
+ writeFileSync4(symbolsPath, symbolsJson);
11772
+ let embedNote = "";
11773
+ let embedMeta;
11774
+ if (model) {
11775
+ const index = buildEmbeddingIndex(scan2, model);
11776
+ const bytes = serializeEmbeddings(index);
11777
+ writeFileSync4(embedPath, bytes);
11778
+ embedMeta = { embedVersion: EMBED_VERSION, modelId: model.modelId, sha1: sha1(bytes) };
11779
+ embedNote = ` + embeddings.bin (${index.records.length} records, model ${model.modelId})`;
11780
+ }
11781
+ writeCache({ graphSha1: sha1(graphJson), symbolsSha1: sha1(symbolsJson), embed: embedMeta });
11782
+ process.stderr.write(`codeindex: ${scan2.files.length} files \u2192 ${outDir}/graph.json + symbols.json${embedNote}${scan2.capped ? " (capped)" : ""}
11373
11783
  `);
11784
+ }
11374
11785
  } else if (cmd === "scan") {
11375
- const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11786
+ const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11376
11787
  const summary = {
11377
11788
  engineVersion: ENGINE_VERSION,
11378
11789
  commit: scan2.commit,
@@ -11382,30 +11793,30 @@ async function runCli(argv) {
11382
11793
  };
11383
11794
  emit(JSON.stringify(summary, null, 2) + "\n", flags2.out);
11384
11795
  } else if (cmd === "graph") {
11385
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11796
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11386
11797
  emit(renderGraphJson(graph), flags2.out);
11387
11798
  } else if (cmd === "symbols") {
11388
- const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
11799
+ const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11389
11800
  emit(renderSymbolsJson(symbols), flags2.out);
11390
11801
  } else if (cmd === "scip") {
11391
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11802
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11392
11803
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
11393
- const out2 = flags2.out ?? resolve("index.scip");
11804
+ const out2 = flags2.out ?? resolve2("index.scip");
11394
11805
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
11395
11806
  else {
11396
- writeFileSync3(out2, bytes);
11807
+ writeFileSync4(out2, bytes);
11397
11808
  process.stderr.write(`codeindex: SCIP index \u2192 ${out2} (${bytes.length} bytes)
11398
11809
  `);
11399
11810
  }
11400
11811
  } else if (cmd === "callers") {
11401
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11812
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11402
11813
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
11403
11814
  const obj = {};
11404
11815
  for (const [name2, entry] of index) obj[name2] = entry;
11405
11816
  emit(JSON.stringify(obj, null, 2) + "\n", flags2.out);
11406
11817
  } else if (cmd === "search") {
11407
11818
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
11408
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11819
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11409
11820
  if (flags2.semantic) {
11410
11821
  const endpoint = resolveEmbedEndpoint();
11411
11822
  const lexical = () => {
@@ -11495,17 +11906,17 @@ async function runCli(argv) {
11495
11906
  return;
11496
11907
  }
11497
11908
  const model = loadEmbedModel(modelDir);
11498
- mkdirSync2(flags2.out, { recursive: true });
11499
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
11909
+ mkdirSync3(flags2.out, { recursive: true });
11910
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11500
11911
  const index = buildEmbeddingIndex(scan2, model);
11501
- writeFileSync3(join13(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11912
+ writeFileSync4(join15(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
11502
11913
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
11503
11914
  `);
11504
11915
  } else if (sub === "pull") {
11505
11916
  const { url, sha256 } = resolveEmbedPullUrl();
11506
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join13(flags2.repo, ".codeindex", "models");
11507
- mkdirSync2(destDir, { recursive: true });
11508
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join13(destDir, "model.json")}
11917
+ const destDir = process.env.CODEINDEX_EMBED_DIR ?? join15(flags2.repo, ".codeindex", "models");
11918
+ mkdirSync3(destDir, { recursive: true });
11919
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join15(destDir, "model.json")}
11509
11920
  `);
11510
11921
  let body2;
11511
11922
  try {
@@ -11526,16 +11937,102 @@ async function runCli(argv) {
11526
11937
  process.exitCode = 1;
11527
11938
  return;
11528
11939
  }
11529
- writeFileSync3(join13(destDir, "model.json"), body2);
11530
- process.stderr.write(`codeindex: model written to ${join13(destDir, "model.json")}
11940
+ writeFileSync4(join15(destDir, "model.json"), body2);
11941
+ process.stderr.write(`codeindex: model written to ${join15(destDir, "model.json")}
11531
11942
  `);
11532
11943
  } else {
11533
11944
  throw new Error("embed needs a subcommand: status | build | pull | serve");
11534
11945
  }
11946
+ } else if (cmd === "grammars") {
11947
+ const sub = flags2.positional;
11948
+ const cacheDir = sharedGrammarsCacheDir();
11949
+ if (sub === "status") {
11950
+ const info2 = resolveGrammarsTier();
11951
+ const runtimePresent = info2.dir ? existsSync4(join15(info2.dir, "web-tree-sitter.wasm")) : false;
11952
+ const target = resolveGrammarsPullTarget();
11953
+ const status = {
11954
+ engineVersion: ENGINE_VERSION,
11955
+ tier: info2.tier,
11956
+ dir: info2.dir ?? null,
11957
+ cacheDir,
11958
+ runtimePresent,
11959
+ pullNeeded: !runtimePresent,
11960
+ url: target.url
11961
+ };
11962
+ emit(JSON.stringify(status, null, 2) + "\n", flags2.out);
11963
+ } else if (sub === "pull") {
11964
+ const target = resolveGrammarsPullTarget();
11965
+ let expected;
11966
+ if (target.sha256Url) {
11967
+ try {
11968
+ expected = await fetchExpectedSha256(target.sha256Url);
11969
+ } catch (e) {
11970
+ process.stderr.write(
11971
+ `codeindex: could not fetch checksum (${e instanceof Error ? e.message : String(e)}) \u2014 proceeding unverified
11972
+ `
11973
+ );
11974
+ }
11975
+ }
11976
+ const runtime = join15(cacheDir, "web-tree-sitter.wasm");
11977
+ const markerPath = join15(dirname4(cacheDir), `${ENGINE_VERSION}.sha256`);
11978
+ if (existsSync4(runtime) && expected && existsSync4(markerPath)) {
11979
+ let marker = "";
11980
+ try {
11981
+ marker = readFileSync7(markerPath, "utf8").trim();
11982
+ } catch {
11983
+ }
11984
+ if (marker === expected) {
11985
+ process.stderr.write(`codeindex: grammars already present at ${cacheDir} (up to date)
11986
+ `);
11987
+ return;
11988
+ }
11989
+ }
11990
+ process.stderr.write(`codeindex: fetching grammars from ${target.url} \u2192 ${cacheDir}
11991
+ `);
11992
+ let bytes;
11993
+ try {
11994
+ bytes = await fetchGrammarsTarball(target.url, expected);
11995
+ } catch (e) {
11996
+ process.stderr.write(`codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
11997
+ `);
11998
+ process.exitCode = 1;
11999
+ return;
12000
+ }
12001
+ let tmp;
12002
+ try {
12003
+ mkdirSync3(dirname4(cacheDir), { recursive: true });
12004
+ tmp = mkdtempSync(join15(dirname4(cacheDir), ".grammars-tmp-"));
12005
+ extractGrammarsTarball(bytes, tmp);
12006
+ if (!existsSync4(join15(tmp, "web-tree-sitter.wasm"))) {
12007
+ throw new Error("archive is missing web-tree-sitter.wasm");
12008
+ }
12009
+ if (existsSync4(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
12010
+ renameSync(tmp, cacheDir);
12011
+ tmp = void 0;
12012
+ if (expected) writeFileSync4(markerPath, expected + "\n");
12013
+ } catch (e) {
12014
+ if (tmp) {
12015
+ try {
12016
+ rmSync2(tmp, { recursive: true, force: true });
12017
+ } catch {
12018
+ }
12019
+ }
12020
+ process.stderr.write(
12021
+ `codeindex: pull failed \u2014 ${e instanceof Error ? e.message : String(e)} (nothing written)
12022
+ `
12023
+ );
12024
+ process.exitCode = 1;
12025
+ return;
12026
+ }
12027
+ process.stderr.write(`codeindex: grammars extracted \u2192 ${cacheDir}
12028
+ `);
12029
+ } else {
12030
+ throw new Error("grammars needs a subcommand: status | pull");
12031
+ }
11535
12032
  } else if (cmd === "rules") {
11536
12033
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
11537
- const rules = parseRules(JSON.parse(readFileSync6(flags2.config, "utf8")));
11538
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12034
+ const rules = parseRules(JSON.parse(readFileSync7(flags2.config, "utf8")));
12035
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11539
12036
  const violations = checkRules(graph, rules);
11540
12037
  const errors = violations.filter((v) => v.severity === "error").length;
11541
12038
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags2.out);
@@ -11556,26 +12053,26 @@ async function runCli(argv) {
11556
12053
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
11557
12054
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags2.out);
11558
12055
  } else if (cmd === "repomap") {
11559
- const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12056
+ const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11560
12057
  emit(renderRepoMap(scan2, graph, { budgetTokens: flags2.budgetTokens }), flags2.out);
11561
12058
  } else if (cmd === "hotspots") {
11562
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12059
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11563
12060
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11564
12061
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11565
12062
  } else if (cmd === "coupling") {
11566
12063
  const { ok, couplings } = changeCoupling(flags2.repo, { since: flags2.since });
11567
12064
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
11568
12065
  } else if (cmd === "deadcode") {
11569
- emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2))), null, 2) + "\n", flags2.out);
12066
+ emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk))), null, 2) + "\n", flags2.out);
11570
12067
  } else if (cmd === "complexity") {
11571
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12068
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11572
12069
  emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
11573
12070
  } else if (cmd === "risk") {
11574
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2));
12071
+ const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
11575
12072
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
11576
12073
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
11577
12074
  } else if (cmd === "mermaid") {
11578
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2));
12075
+ const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
11579
12076
  emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
11580
12077
  } else if (cmd === "grep") {
11581
12078
  if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
@@ -11594,6 +12091,7 @@ ${HELP}`);
11594
12091
  }
11595
12092
  }
11596
12093
  export {
12094
+ DEFAULT_GRAMMARS_URL,
11597
12095
  DEFAULT_MAX_FILES,
11598
12096
  EMBED_VERSION,
11599
12097
  ENGINE_VERSION,
@@ -11604,6 +12102,7 @@ export {
11604
12102
  applyCentrality,
11605
12103
  basicTokenize,
11606
12104
  betweennessOf,
12105
+ buildArtifactsFromScan,
11607
12106
  buildCallerIndex,
11608
12107
  buildEmbeddingIndex,
11609
12108
  buildEndpointIndex,
@@ -11646,14 +12145,19 @@ export {
11646
12145
  extToLang,
11647
12146
  extractAst,
11648
12147
  extractCode,
12148
+ extractGrammarsTarball,
11649
12149
  extractMarkdown,
11650
12150
  extractSymbols,
12151
+ extractTarInto,
12152
+ fetchExpectedSha256,
12153
+ fetchGrammarsTarball,
11651
12154
  findDeadCode,
11652
12155
  findReferences,
11653
12156
  findSymbol,
11654
12157
  foldText,
11655
12158
  gitChurn,
11656
12159
  grammarKeyForExt,
12160
+ grammarKeysForExts,
11657
12161
  grammarReady,
11658
12162
  grepRepo,
11659
12163
  hasEmbedModel,
@@ -11695,6 +12199,9 @@ export {
11695
12199
  resolveEmbedEndpoint,
11696
12200
  resolveEmbedModelDir,
11697
12201
  resolveEmbedPullUrl,
12202
+ resolveGrammarsDir,
12203
+ resolveGrammarsPullTarget,
12204
+ resolveGrammarsTier,
11698
12205
  resolveImport,
11699
12206
  resolveUniqueSymbol,
11700
12207
  riskHotspots,
@@ -11708,6 +12215,7 @@ export {
11708
12215
  serializeEmbeddings,
11709
12216
  sh,
11710
12217
  sha1,
12218
+ sharedGrammarsCacheDir,
11711
12219
  shortHash,
11712
12220
  slugify,
11713
12221
  subtokens,