@maxgfr/codeindex 2.17.0 → 2.18.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.17.0";
17
+ ENGINE_VERSION = "2.18.0";
18
18
  SCHEMA_VERSION = 4;
19
19
  EXTRACTOR_VERSION = 10;
20
20
  }
@@ -606,9 +606,9 @@ function diffFiles(dir, spec) {
606
606
  }
607
607
  }
608
608
  const byPath = new Map(out2.map((f) => [f.path, f]));
609
- const num = sh("git", [...gitArgs(dir), "diff", "-z", "-M", "--numstat", ...rangeArgs(spec)]);
610
- if (num.ok) {
611
- const toks = num.stdout.split("\0");
609
+ const num2 = sh("git", [...gitArgs(dir), "diff", "-z", "-M", "--numstat", ...rangeArgs(spec)]);
610
+ if (num2.ok) {
611
+ const toks = num2.stdout.split("\0");
612
612
  let i2 = 0;
613
613
  while (i2 < toks.length) {
614
614
  const head = toks[i2++];
@@ -2841,7 +2841,7 @@ async function Module2(moduleArg = {}) {
2841
2841
  _fd_close.sig = "ii";
2842
2842
  var INT53_MAX = 9007199254740992;
2843
2843
  var INT53_MIN = -9007199254740992;
2844
- var bigintToI53Checked = /* @__PURE__ */ __name((num) => num < INT53_MIN || num > INT53_MAX ? NaN : Number(num), "bigintToI53Checked");
2844
+ var bigintToI53Checked = /* @__PURE__ */ __name((num2) => num2 < INT53_MIN || num2 > INT53_MAX ? NaN : Number(num2), "bigintToI53Checked");
2845
2845
  function _fd_seek(fd, offset, whence, newOffset) {
2846
2846
  offset = bigintToI53Checked(offset);
2847
2847
  return 70;
@@ -2859,7 +2859,7 @@ async function Module2(moduleArg = {}) {
2859
2859
  }
2860
2860
  }, "printChar");
2861
2861
  var _fd_write = /* @__PURE__ */ __name((fd, iov, iovcnt, pnum) => {
2862
- var num = 0;
2862
+ var num2 = 0;
2863
2863
  for (var i2 = 0; i2 < iovcnt; i2++) {
2864
2864
  var ptr = LE_HEAP_LOAD_U32((iov >> 2) * 4);
2865
2865
  var len = LE_HEAP_LOAD_U32((iov + 4 >> 2) * 4);
@@ -2867,9 +2867,9 @@ async function Module2(moduleArg = {}) {
2867
2867
  for (var j = 0; j < len; j++) {
2868
2868
  printChar(fd, HEAPU8[ptr + j]);
2869
2869
  }
2870
- num += len;
2870
+ num2 += len;
2871
2871
  }
2872
- LE_HEAP_STORE_U32((pnum >> 2) * 4, num);
2872
+ LE_HEAP_STORE_U32((pnum >> 2) * 4, num2);
2873
2873
  return 0;
2874
2874
  }, "_fd_write");
2875
2875
  _fd_write.sig = "iippp";
@@ -5663,65 +5663,30 @@ var init_loader = __esm({
5663
5663
  });
5664
5664
 
5665
5665
  // src/ast/extract.ts
5666
- function collectRefIdents(root, defNames) {
5667
- const found = /* @__PURE__ */ new Set();
5668
- const visit = (node) => {
5669
- if (node.namedChildCount === 0 && /identifier|constant|(^|_)name$/.test(node.type) && /^[A-Za-z_]\w{4,}$/.test(node.text) && !defNames.has(node.text)) {
5670
- found.add(node.text);
5671
- }
5672
- for (let i2 = 0; i2 < node.namedChildCount; i2++) visit(node.namedChild(i2));
5673
- };
5674
- visit(root);
5675
- return [...found].sort().slice(0, MAX_REF_IDENTS);
5676
- }
5677
- function firstLine(node) {
5678
- const nl = node.text.indexOf("\n");
5679
- return (nl === -1 ? node.text : node.text.slice(0, nl)).trim().slice(0, 200);
5666
+ function firstLine(node, src) {
5667
+ const start2 = node.startIndex;
5668
+ const end = node.endIndex;
5669
+ const nl = src.indexOf("\n", start2);
5670
+ const stop2 = nl === -1 || nl > end ? end : nl;
5671
+ return src.slice(start2, stop2).trim().slice(0, 200);
5680
5672
  }
5681
5673
  function nameOf(node) {
5682
5674
  const named = node.childForFieldName("name");
5683
5675
  if (named?.text) return named.text;
5684
5676
  let decl = node.childForFieldName("declarator");
5685
5677
  while (decl) {
5686
- if (decl.namedChildCount === 0 && /(^|_)identifier$/.test(decl.type)) return decl.text;
5678
+ if (decl.namedChildren.length === 0 && /(^|_)identifier$/.test(decl.type)) return decl.text;
5687
5679
  const next = decl.childForFieldName("declarator");
5688
5680
  if (!next || next === decl) break;
5689
5681
  decl = next;
5690
5682
  }
5691
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5692
- const c2 = node.namedChild(i2);
5683
+ for (const c2 of node.namedChildren) {
5693
5684
  if (/(^|_)(identifier|name|constant)$/.test(c2.type)) return c2.text;
5694
5685
  }
5695
5686
  return void 0;
5696
5687
  }
5697
- function collectImports(root, spec) {
5698
- if (!spec.imports) return [];
5699
- const out2 = [];
5700
- const seen = /* @__PURE__ */ new Set();
5701
- const add = (s) => {
5702
- const v = s.trim();
5703
- if (v && !seen.has(v)) {
5704
- seen.add(v);
5705
- out2.push({ kind: "import", spec: v });
5706
- }
5707
- };
5708
- const visit = (node) => {
5709
- const how = spec.imports[node.type];
5710
- if (how === "string") {
5711
- const str2 = findFirst(node, (n) => /string/.test(n.type));
5712
- if (str2) add(str2.text.replace(/^['"]|['"]$/g, ""));
5713
- } else if (how === "path") {
5714
- const name2 = node.childForFieldName("name") ?? node.childForFieldName("module_name");
5715
- add((name2 ?? node).text.replace(/^(import|from)\s+/, "").split(/\s+/)[0]);
5716
- }
5717
- for (let i2 = 0; i2 < node.namedChildCount; i2++) visit(node.namedChild(i2));
5718
- };
5719
- visit(root);
5720
- return out2;
5721
- }
5722
5688
  function findFirst(node, pred) {
5723
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5724
- const c2 = node.namedChild(i2);
5689
+ for (const c2 of node.namedChildren) {
5725
5690
  if (pred(c2)) return c2;
5726
5691
  const deep = findFirst(c2, pred);
5727
5692
  if (deep) return deep;
@@ -5730,79 +5695,104 @@ function findFirst(node, pred) {
5730
5695
  }
5731
5696
  function readName(node) {
5732
5697
  if (!node) return void 0;
5733
- if (node.namedChildCount === 0) return IDENT_LEAF.test(node.type) ? node.text : void 0;
5698
+ const kids = node.namedChildren;
5699
+ if (kids.length === 0) return IDENT_LEAF.test(node.type) ? node.text : void 0;
5734
5700
  const seg = node.childForFieldName("name") ?? node.childForFieldName("property") ?? node.childForFieldName("attribute") ?? node.childForFieldName("field") ?? // Callee wrappers that point at the real callee via a `function` field:
5735
5701
  // scala's generic_function (`foo[Int](x)`) and a curried/chained
5736
5702
  // call_expression callee (`curried(a)(b)`) — descend to the inner name
5737
5703
  // instead of tripping over type_arguments/arguments as the last child.
5738
5704
  node.childForFieldName("function");
5739
5705
  if (seg) return readName(seg);
5740
- const last = node.namedChild(node.namedChildCount - 1);
5706
+ const last = kids[kids.length - 1];
5741
5707
  return last && last !== node ? readName(last) : void 0;
5742
5708
  }
5743
5709
  function readReceiver(node) {
5744
- if (!node || node.namedChildCount === 0) return void 0;
5710
+ if (!node || node.namedChildren.length === 0) return void 0;
5745
5711
  const obj = node.childForFieldName("object") ?? node.childForFieldName("operand") ?? node.childForFieldName("value") ?? node.childForFieldName("path") ?? node.childForFieldName("expression") ?? node.childForFieldName("argument") ?? node.childForFieldName("receiver") ?? node.childForFieldName("table");
5746
5712
  const name2 = obj ? readName(obj) : void 0;
5747
5713
  return name2 && /^[A-Za-z_]\w*$/.test(name2) ? name2 : void 0;
5748
5714
  }
5749
- function collectCalls(root, spec, maxCalls = MAX_CALLS) {
5750
- if (!spec.calls) return [];
5751
- const out2 = [];
5752
- const seen = /* @__PURE__ */ new Set();
5753
- const add = (name2, node, receiver) => {
5715
+ function collectAll(root, spec, defNames, maxCalls, wantImports) {
5716
+ const identsFound = /* @__PURE__ */ new Set();
5717
+ const wantCalls = spec.calls !== void 0;
5718
+ const calls = [];
5719
+ const callSeen = /* @__PURE__ */ new Set();
5720
+ const addCall = (name2, node, receiver) => {
5754
5721
  if (!name2 || name2.length < 2 || !/^[A-Za-z_]\w*$/.test(name2)) return;
5755
5722
  const line = node.startPosition.row + 1;
5756
5723
  const key = `${name2} ${line}`;
5757
- if (seen.has(key)) return;
5758
- seen.add(key);
5759
- out2.push(receiver ? { name: name2, line, receiver } : { name: name2, line });
5724
+ if (callSeen.has(key)) return;
5725
+ callSeen.add(key);
5726
+ calls.push(receiver ? { name: name2, line, receiver } : { name: name2, line });
5760
5727
  };
5761
- const visit = (node) => {
5762
- const how = spec.calls[node.type];
5763
- if (how === "function") {
5764
- const callee = node.childForFieldName("function") ?? node.childForFieldName("callee") ?? node.childForFieldName("method") ?? node.childForFieldName("name");
5765
- add(readName(callee), node, readReceiver(callee) ?? readReceiver(node));
5766
- } else if (how === "member") {
5767
- add(readName(node.childForFieldName("name")), node, readReceiver(node));
5768
- } else if (how === "constructor") {
5769
- let t = node.childForFieldName("constructor") ?? node.childForFieldName("type") ?? node.childForFieldName("name");
5770
- for (let i2 = 0; !t && i2 < node.namedChildCount; i2++) {
5771
- const c2 = node.namedChild(i2);
5772
- if (IDENT_LEAF.test(c2.type)) t = c2;
5773
- }
5774
- add(readName(t), node, readReceiver(t ?? null));
5775
- }
5776
- for (let i2 = 0; i2 < node.namedChildCount; i2++) visit(node.namedChild(i2));
5728
+ const wantNames = spec.imports?.import_statement !== void 0;
5729
+ const namesFound = /* @__PURE__ */ new Set();
5730
+ const wantRefs = wantImports && spec.imports !== void 0;
5731
+ const refs = [];
5732
+ const refSeen = /* @__PURE__ */ new Set();
5733
+ const addRef = (s) => {
5734
+ const v = s.trim();
5735
+ if (v && !refSeen.has(v)) {
5736
+ refSeen.add(v);
5737
+ refs.push({ kind: "import", spec: v });
5738
+ }
5777
5739
  };
5778
- visit(root);
5779
- out2.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
5780
- return out2.slice(0, maxCalls);
5781
- }
5782
- function collectImportedNames(root, spec) {
5783
- if (!spec.imports?.import_statement) return [];
5784
- const found = /* @__PURE__ */ new Set();
5785
5740
  const visit = (node) => {
5786
- if (node.type === "import_statement") {
5787
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5788
- const clause = node.namedChild(i2);
5741
+ const type = node.type;
5742
+ const kids = node.namedChildren;
5743
+ if (kids.length === 0 && REF_IDENT_TYPE.test(type)) {
5744
+ const text = node.text;
5745
+ if (REF_IDENT_TEXT.test(text) && !defNames.has(text)) identsFound.add(text);
5746
+ }
5747
+ if (wantCalls) {
5748
+ const how = spec.calls[type];
5749
+ if (how === "function") {
5750
+ const callee = node.childForFieldName("function") ?? node.childForFieldName("callee") ?? node.childForFieldName("method") ?? node.childForFieldName("name");
5751
+ addCall(readName(callee), node, readReceiver(callee) ?? readReceiver(node));
5752
+ } else if (how === "member") {
5753
+ addCall(readName(node.childForFieldName("name")), node, readReceiver(node));
5754
+ } else if (how === "constructor") {
5755
+ let t = node.childForFieldName("constructor") ?? node.childForFieldName("type") ?? node.childForFieldName("name");
5756
+ for (let i2 = 0; !t && i2 < kids.length; i2++) {
5757
+ const c2 = kids[i2];
5758
+ if (IDENT_LEAF.test(c2.type)) t = c2;
5759
+ }
5760
+ addCall(readName(t), node, readReceiver(t ?? null));
5761
+ }
5762
+ }
5763
+ if (wantNames && type === "import_statement") {
5764
+ for (const clause of kids) {
5789
5765
  if (clause.type !== "import_clause") continue;
5790
- for (let j = 0; j < clause.namedChildCount; j++) {
5791
- const named = clause.namedChild(j);
5766
+ for (const named of clause.namedChildren) {
5792
5767
  if (named.type !== "named_imports") continue;
5793
- for (let k = 0; k < named.namedChildCount; k++) {
5794
- const specifier = named.namedChild(k);
5768
+ for (const specifier of named.namedChildren) {
5795
5769
  if (specifier.type !== "import_specifier") continue;
5796
- const nm = specifier.childForFieldName("name") ?? specifier.namedChild(0);
5797
- if (nm?.text) found.add(nm.text);
5770
+ const nm = specifier.childForFieldName("name") ?? specifier.namedChildren[0];
5771
+ if (nm?.text) namesFound.add(nm.text);
5798
5772
  }
5799
5773
  }
5800
5774
  }
5801
5775
  }
5802
- for (let i2 = 0; i2 < node.namedChildCount; i2++) visit(node.namedChild(i2));
5776
+ if (wantRefs) {
5777
+ const how = spec.imports[type];
5778
+ if (how === "string") {
5779
+ const str2 = findFirst(node, (n) => /string/.test(n.type));
5780
+ if (str2) addRef(str2.text.replace(/^['"]|['"]$/g, ""));
5781
+ } else if (how === "path") {
5782
+ const name2 = node.childForFieldName("name") ?? node.childForFieldName("module_name");
5783
+ addRef((name2 ?? node).text.replace(/^(import|from)\s+/, "").split(/\s+/)[0]);
5784
+ }
5785
+ }
5786
+ for (const c2 of kids) visit(c2);
5803
5787
  };
5804
5788
  visit(root);
5805
- return [...found].sort(byStr).slice(0, MAX_IMPORTED_NAMES);
5789
+ calls.sort((a, b) => byStr(a.name, b.name) || a.line - b.line);
5790
+ return {
5791
+ refs,
5792
+ idents: [...identsFound].sort().slice(0, MAX_REF_IDENTS),
5793
+ calls: calls.slice(0, maxCalls),
5794
+ importedNames: [...namesFound].sort(byStr).slice(0, MAX_IMPORTED_NAMES)
5795
+ };
5806
5796
  }
5807
5797
  function extractAst(rel, ext, content, opts = {}) {
5808
5798
  const key = grammarKeyForExt(ext);
@@ -5822,20 +5812,17 @@ function extractAst(rel, ext, content, opts = {}) {
5822
5812
  const walk2 = (node, parent, exported) => {
5823
5813
  const nowExported = exported || node.type === "export_statement";
5824
5814
  if (node.type === "export_statement") {
5825
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5826
- const c2 = node.namedChild(i2);
5815
+ for (const c2 of node.namedChildren) {
5827
5816
  if (c2.type === "identifier") exportedNames.add(c2.text);
5828
5817
  else if (c2.type === "export_clause") {
5829
- for (let j = 0; j < c2.namedChildCount; j++) {
5830
- const spec2 = c2.namedChild(j);
5831
- const nm = spec2.childForFieldName("name") ?? spec2.namedChild(0);
5818
+ for (const spec2 of c2.namedChildren) {
5819
+ const nm = spec2.childForFieldName("name") ?? spec2.namedChildren[0];
5832
5820
  if (nm?.text) exportedNames.add(nm.text);
5833
5821
  }
5834
5822
  }
5835
5823
  }
5836
5824
  if (stem && node.children.some((c2) => c2.type === "default")) {
5837
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5838
- const c2 = node.namedChild(i2);
5825
+ for (const c2 of node.namedChildren) {
5839
5826
  const fnLike = ANON_DEFAULT_FN.has(c2.type);
5840
5827
  const classLike = ANON_DEFAULT_CLASS.has(c2.type);
5841
5828
  if ((fnLike || classLike) && !c2.childForFieldName("name")) {
@@ -5845,7 +5832,7 @@ function extractAst(rel, ext, content, opts = {}) {
5845
5832
  file: rel,
5846
5833
  line: node.startPosition.row + 1,
5847
5834
  endLine: node.endPosition.row + 1,
5848
- signature: firstLine(node),
5835
+ signature: firstLine(node, content),
5849
5836
  exported: true,
5850
5837
  lang: spec.lang
5851
5838
  });
@@ -5855,14 +5842,13 @@ function extractAst(rel, ext, content, opts = {}) {
5855
5842
  }
5856
5843
  }
5857
5844
  if (spec.assignments && node.type === "expression_statement") {
5858
- const expr = node.namedChild(0);
5845
+ const expr = node.namedChildren[0];
5859
5846
  if (expr?.type === "assignment_expression") {
5860
5847
  const left = expr.childForFieldName("left");
5861
5848
  const right = expr.childForFieldName("right");
5862
5849
  if (left?.type === "member_expression" && left.text === "module.exports" && right) {
5863
5850
  if (right.type === "object") {
5864
- for (let i2 = 0; i2 < right.namedChildCount; i2++) {
5865
- const p = right.namedChild(i2);
5851
+ for (const p of right.namedChildren) {
5866
5852
  if (p.type === "shorthand_property_identifier") exportedNames.add(p.text);
5867
5853
  else if (p.type === "pair") {
5868
5854
  const k = p.childForFieldName("key");
@@ -5900,7 +5886,7 @@ function extractAst(rel, ext, content, opts = {}) {
5900
5886
  line: expr.startPosition.row + 1,
5901
5887
  endLine: expr.endPosition.row + 1,
5902
5888
  ...parent ? { parent } : {},
5903
- signature: firstLine(expr),
5889
+ signature: firstLine(expr, content),
5904
5890
  exported: nowExported || exportedAssign,
5905
5891
  lang: spec.lang
5906
5892
  });
@@ -5920,7 +5906,7 @@ function extractAst(rel, ext, content, opts = {}) {
5920
5906
  line: expr.startPosition.row + 1,
5921
5907
  endLine: expr.endPosition.row + 1,
5922
5908
  ...parent ? { parent } : {},
5923
- signature: firstLine(expr),
5909
+ signature: firstLine(expr, content),
5924
5910
  exported: true,
5925
5911
  lang: spec.lang
5926
5912
  });
@@ -5934,11 +5920,14 @@ function extractAst(rel, ext, content, opts = {}) {
5934
5920
  if (spec.assignments && node.type === "assignment_statement") {
5935
5921
  const vars = node.children.find((c2) => c2.type === "variable_list");
5936
5922
  const vals = node.children.find((c2) => c2.type === "expression_list");
5937
- const pairs = Math.min(vars?.namedChildCount ?? 0, vals?.namedChildCount ?? 0);
5923
+ const targets = vars?.namedChildren ?? [];
5924
+ const values = vals?.namedChildren ?? [];
5925
+ const pairs = Math.min(targets.length, values.length);
5938
5926
  for (let i2 = 0; i2 < pairs; i2++) {
5939
- const target = vars.namedChild(i2);
5940
- const value = vals.namedChild(i2);
5927
+ const target = targets[i2];
5928
+ const value = values[i2];
5941
5929
  if (value.type !== "function_definition" || !/^[\w.:]+$/.test(target.text)) continue;
5930
+ const line = firstLine(node, content);
5942
5931
  symbols.push({
5943
5932
  name: target.text,
5944
5933
  kind: "function",
@@ -5946,8 +5935,8 @@ function extractAst(rel, ext, content, opts = {}) {
5946
5935
  line: node.startPosition.row + 1,
5947
5936
  endLine: node.endPosition.row + 1,
5948
5937
  ...parent ? { parent } : {},
5949
- signature: firstLine(node),
5950
- exported: nowExported || spec.exported(firstLine(node), target.text),
5938
+ signature: line,
5939
+ exported: nowExported || spec.exported(line, target.text),
5951
5940
  lang: spec.lang
5952
5941
  });
5953
5942
  }
@@ -5957,7 +5946,7 @@ function extractAst(rel, ext, content, opts = {}) {
5957
5946
  if (kind) {
5958
5947
  const name2 = nameOf(node);
5959
5948
  if (name2) {
5960
- const line = firstLine(node);
5949
+ const line = firstLine(node, content);
5961
5950
  symbols.push({
5962
5951
  name: name2,
5963
5952
  kind,
@@ -5969,31 +5958,33 @@ function extractAst(rel, ext, content, opts = {}) {
5969
5958
  exported: nowExported || spec.exported(line, name2),
5970
5959
  lang: spec.lang
5971
5960
  });
5972
- for (let i2 = 0; i2 < node.namedChildCount; i2++) {
5973
- walkBody(node.namedChild(i2), name2, nowExported);
5974
- }
5961
+ for (const c2 of node.namedChildren) walkBody(c2, name2, nowExported);
5975
5962
  return;
5976
5963
  }
5977
5964
  }
5978
5965
  if (spec.containers.has(node.type)) {
5979
- for (let i2 = 0; i2 < node.namedChildCount; i2++) walk2(node.namedChild(i2), parent, nowExported);
5966
+ for (const c2 of node.namedChildren) walk2(c2, parent, nowExported);
5980
5967
  }
5981
5968
  };
5982
5969
  const walkBody = (node, parent, exported) => {
5983
5970
  if (spec.containers.has(node.type)) {
5984
- for (let i2 = 0; i2 < node.namedChildCount; i2++) walk2(node.namedChild(i2), parent, exported);
5971
+ for (const c2 of node.namedChildren) walk2(c2, parent, exported);
5985
5972
  }
5986
5973
  };
5987
5974
  walk2(root, void 0, false);
5988
5975
  if (exportedNames.size) {
5989
5976
  for (const s of symbols) if (!s.exported && exportedNames.has(s.name)) s.exported = true;
5990
5977
  }
5991
- const refs = collectImports(root, spec);
5992
- const idents = collectRefIdents(root, new Set(symbols.map((s) => s.name)));
5993
- const calls = collectCalls(root, spec, opts.maxCalls);
5994
- const importedNames = collectImportedNames(root, spec);
5978
+ const wantImports = opts.imports !== false;
5979
+ const { refs, idents, calls, importedNames } = collectAll(
5980
+ root,
5981
+ spec,
5982
+ new Set(symbols.map((s) => s.name)),
5983
+ opts.maxCalls ?? MAX_CALLS,
5984
+ wantImports
5985
+ );
5995
5986
  let pkg;
5996
- if (spec.lang === "java") {
5987
+ if (wantImports && spec.lang === "java") {
5997
5988
  const p = findFirst(root, (n) => n.type === "package_declaration");
5998
5989
  if (p) pkg = p.text.replace(/^package\s+/, "").replace(/;.*$/, "").trim();
5999
5990
  }
@@ -6004,7 +5995,7 @@ function extractAst(rel, ext, content, opts = {}) {
6004
5995
  tree?.delete();
6005
5996
  }
6006
5997
  }
6007
- var MAX_REF_IDENTS, MAX_CALLS, MAX_IMPORTED_NAMES, ANON_DEFAULT_FN, ANON_DEFAULT_CLASS, byPublicKeyword, byNotPrivate, byNotLocal, byPub, byCapital, byPyConvention, always, neverExport, TS_SPEC, SPECS, IDENT_LEAF;
5998
+ var MAX_REF_IDENTS, MAX_CALLS, MAX_IMPORTED_NAMES, ANON_DEFAULT_FN, ANON_DEFAULT_CLASS, REF_IDENT_TYPE, REF_IDENT_TEXT, byPublicKeyword, byNotPrivate, byNotLocal, byPub, byCapital, byPyConvention, always, neverExport, TS_SPEC, SPECS, IDENT_LEAF;
6008
5999
  var init_extract = __esm({
6009
6000
  "src/ast/extract.ts"() {
6010
6001
  "use strict";
@@ -6022,6 +6013,8 @@ var init_extract = __esm({
6022
6013
  "arrow_function"
6023
6014
  ]);
6024
6015
  ANON_DEFAULT_CLASS = /* @__PURE__ */ new Set(["class", "class_declaration", "abstract_class_declaration"]);
6016
+ REF_IDENT_TYPE = /identifier|constant|(^|_)name$/;
6017
+ REF_IDENT_TEXT = /^[A-Za-z_]\w{4,}$/;
6025
6018
  byPublicKeyword = (line) => /\b(public|internal)\b/.test(line);
6026
6019
  byNotPrivate = (line) => !/\b(private|protected)\b/.test(line);
6027
6020
  byNotLocal = (line) => !/^local\b/.test(line);
@@ -6464,7 +6457,7 @@ function collectCallsRegex(content, symbols = [], maxCalls = 512) {
6464
6457
  return [...out2.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : a.line - b.line);
6465
6458
  }
6466
6459
  function extractCode(rel, ext, content, opts = {}) {
6467
- const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile });
6460
+ const ast = extractAst(rel, ext, content, { maxCalls: opts.maxCallsPerFile, imports: false });
6468
6461
  const symbols = (ast ? ast.symbols : extractSymbols(rel, ext, content)).slice(0, 400);
6469
6462
  const known = new Set(symbols.map((s) => s.name));
6470
6463
  const reexports = extractReexports(rel, content, symbols).filter((s) => !known.has(s.name));
@@ -6545,7 +6538,35 @@ function countLines(s) {
6545
6538
  for (let i2 = 0; i2 < s.length; i2++) if (s.charCodeAt(i2) === 10) n++;
6546
6539
  return n;
6547
6540
  }
6548
- function scanRepo(root, opts = {}) {
6541
+ function buildCodeRecord(rel, ext, size, content, hash, lang, opts = {}) {
6542
+ const record = {
6543
+ rel,
6544
+ ext,
6545
+ size,
6546
+ lines: countLines(content),
6547
+ hash,
6548
+ kind: "code",
6549
+ lang,
6550
+ headings: [],
6551
+ symbols: [],
6552
+ refs: []
6553
+ };
6554
+ if (content) {
6555
+ const code = extractCode(rel, ext, content, { maxCallsPerFile: opts.maxCallsPerFile });
6556
+ record.title = basename(rel);
6557
+ record.summary = code.summary;
6558
+ record.symbols = code.symbols;
6559
+ record.refs = code.refs;
6560
+ record.pkg = code.pkg;
6561
+ record.idents = code.idents;
6562
+ record.calls = code.calls;
6563
+ record.importedNames = code.importedNames;
6564
+ } else {
6565
+ record.title = basename(rel);
6566
+ }
6567
+ return record;
6568
+ }
6569
+ function* keptFiles(root, opts) {
6549
6570
  const scoped = opts.scope ? [...opts.include ?? [], `${opts.scope.replace(/\/+$/, "")}/**`] : opts.include;
6550
6571
  const include = compileGlobs(scoped);
6551
6572
  const exclude = compileGlobs(opts.exclude);
@@ -6556,6 +6577,34 @@ function scanRepo(root, opts = {}) {
6556
6577
  ignoreDirs: opts.ignoreDirs
6557
6578
  });
6558
6579
  const outPrefix = opts.out ? opts.out.replace(/\/+$/, "") + "/" : null;
6580
+ for (const f of walked) {
6581
+ if (outPrefix && (f.abs === opts.out || f.abs.startsWith(outPrefix))) continue;
6582
+ if (include && !include(f.rel)) continue;
6583
+ if (exclude && exclude(f.rel)) continue;
6584
+ yield { f, kind: classify(f.rel, f.ext), lang: extToLang(f.ext) };
6585
+ }
6586
+ return { capped, excluded };
6587
+ }
6588
+ function keptCodeFiles(root, opts = {}) {
6589
+ const out2 = [];
6590
+ const it = keptFiles(root, opts);
6591
+ for (let step = it.next(); !step.done; step = it.next()) {
6592
+ if (step.value.kind === "code") out2.push({ f: step.value.f, lang: step.value.lang });
6593
+ }
6594
+ return out2;
6595
+ }
6596
+ function scanSummary(root, opts = {}) {
6597
+ const languages = {};
6598
+ let fileCount = 0;
6599
+ const it = keptFiles(root, opts);
6600
+ let step = it.next();
6601
+ for (; !step.done; step = it.next()) {
6602
+ languages[step.value.lang] = (languages[step.value.lang] ?? 0) + 1;
6603
+ fileCount++;
6604
+ }
6605
+ return { root, commit: headCommit(root), fileCount, languages, capped: step.value.capped, excluded: step.value.excluded };
6606
+ }
6607
+ function scanRepo(root, opts = {}) {
6559
6608
  const files = [];
6560
6609
  const languages = {};
6561
6610
  const docText = /* @__PURE__ */ new Map();
@@ -6563,12 +6612,10 @@ function scanRepo(root, opts = {}) {
6563
6612
  const cache = opts.cache;
6564
6613
  let allReused = cache !== void 0;
6565
6614
  let cacheDirty = cache === void 0;
6566
- for (const f of walked) {
6567
- if (outPrefix && (f.abs === opts.out || f.abs.startsWith(outPrefix))) continue;
6568
- if (include && !include(f.rel)) continue;
6569
- if (exclude && exclude(f.rel)) continue;
6570
- const kind = classify(f.rel, f.ext);
6571
- const lang = extToLang(f.ext);
6615
+ const it = keptFiles(root, opts);
6616
+ let step = it.next();
6617
+ for (; !step.done; step = it.next()) {
6618
+ const { f, kind, lang } = step.value;
6572
6619
  languages[lang] = (languages[lang] ?? 0) + 1;
6573
6620
  mtimes.set(f.rel, f.mtimeMs);
6574
6621
  const cached = opts.cache?.get(f.rel);
@@ -6576,8 +6623,10 @@ function scanRepo(root, opts = {}) {
6576
6623
  files.push(cached.record);
6577
6624
  continue;
6578
6625
  }
6579
- const content = readText(f.abs);
6580
- const hash = sha1(content);
6626
+ const pre = opts.extracted?.get(f.rel);
6627
+ const preUsable = pre && pre.size === f.size && pre.mtimeMs === f.mtimeMs ? pre : void 0;
6628
+ const content = preUsable ? void 0 : readText(f.abs);
6629
+ const hash = preUsable ? preUsable.record.hash : sha1(content);
6581
6630
  if (cached && cached.hash === hash) {
6582
6631
  files.push(cached.record);
6583
6632
  if (kind === "doc" && content) docText.set(f.rel, content);
@@ -6586,6 +6635,10 @@ function scanRepo(root, opts = {}) {
6586
6635
  }
6587
6636
  allReused = false;
6588
6637
  cacheDirty = true;
6638
+ if (preUsable) {
6639
+ files.push(preUsable.record);
6640
+ continue;
6641
+ }
6589
6642
  const record = {
6590
6643
  rel: f.rel,
6591
6644
  ext: f.ext,
@@ -6638,8 +6691,8 @@ function scanRepo(root, opts = {}) {
6638
6691
  languages,
6639
6692
  docText,
6640
6693
  mtimes,
6641
- capped,
6642
- excluded,
6694
+ capped: step.value.capped,
6695
+ excluded: step.value.excluded,
6643
6696
  contentUnchanged: allReused,
6644
6697
  cacheDirty
6645
6698
  };
@@ -6659,9 +6712,79 @@ var init_scan = __esm({
6659
6712
  }
6660
6713
  });
6661
6714
 
6715
+ // src/preload.ts
6716
+ import { readFileSync as readFileSync3 } from "fs";
6717
+ import { join as join3 } from "path";
6718
+ function toCacheMap(scan2) {
6719
+ const m = /* @__PURE__ */ new Map();
6720
+ for (const f of scan2.files) m.set(f.rel, { hash: f.hash, record: f, size: f.size, mtimeMs: scan2.mtimes.get(f.rel) });
6721
+ return m;
6722
+ }
6723
+ function readPersistedIndex(repo, indexDir = INDEX_DIR) {
6724
+ let parsed;
6725
+ try {
6726
+ parsed = JSON.parse(readFileSync3(join3(repo, indexDir, "cache.json"), "utf8"));
6727
+ } catch {
6728
+ return void 0;
6729
+ }
6730
+ if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION || parsed.extractorVersion !== EXTRACTOR_VERSION || !parsed.files) {
6731
+ return void 0;
6732
+ }
6733
+ return {
6734
+ cacheMap: new Map(Object.entries(parsed.files)),
6735
+ meta: {
6736
+ engineVersion: parsed.engineVersion,
6737
+ commit: parsed.commit,
6738
+ graphSha1: parsed.graphSha1,
6739
+ symbolsSha1: parsed.symbolsSha1
6740
+ }
6741
+ };
6742
+ }
6743
+ function preloadArtifacts(repo, scan2, meta, indexDir = INDEX_DIR) {
6744
+ if (!scan2.contentUnchanged || meta.engineVersion !== ENGINE_VERSION || meta.commit !== scan2.commit || meta.graphSha1 === void 0 || meta.symbolsSha1 === void 0) {
6745
+ return void 0;
6746
+ }
6747
+ const dir = join3(repo, indexDir);
6748
+ let graphBytes;
6749
+ let symbolsBytes;
6750
+ try {
6751
+ graphBytes = readFileSync3(join3(dir, "graph.json"));
6752
+ symbolsBytes = readFileSync3(join3(dir, "symbols.json"));
6753
+ } catch {
6754
+ return void 0;
6755
+ }
6756
+ if (sha1(graphBytes) !== meta.graphSha1 || sha1(symbolsBytes) !== meta.symbolsSha1) {
6757
+ return void 0;
6758
+ }
6759
+ try {
6760
+ const graph = JSON.parse(graphBytes.toString("utf8"));
6761
+ const symbols = JSON.parse(symbolsBytes.toString("utf8"));
6762
+ if (graph.schemaVersion !== SCHEMA_VERSION || symbols.schemaVersion !== SCHEMA_VERSION) return void 0;
6763
+ return { scan: scan2, graph, symbols };
6764
+ } catch {
6765
+ return void 0;
6766
+ }
6767
+ }
6768
+ function preloadSession(repo, opts, indexDir = INDEX_DIR) {
6769
+ const persisted = readPersistedIndex(repo, indexDir);
6770
+ if (!persisted) return void 0;
6771
+ const scan2 = scanRepo(repo, { ...opts, cache: persisted.cacheMap });
6772
+ return { scan: scan2, cacheMap: toCacheMap(scan2), arts: preloadArtifacts(repo, scan2, persisted.meta, indexDir) };
6773
+ }
6774
+ var INDEX_DIR;
6775
+ var init_preload = __esm({
6776
+ "src/preload.ts"() {
6777
+ "use strict";
6778
+ init_types();
6779
+ init_scan();
6780
+ init_hash();
6781
+ INDEX_DIR = ".codeindex";
6782
+ }
6783
+ });
6784
+
6662
6785
  // src/resolve.ts
6663
6786
  import { posix } from "path";
6664
- import { join as join4 } from "path";
6787
+ import { join as join6 } from "path";
6665
6788
  function distToSrcCandidates(target) {
6666
6789
  const segs = norm(target).split("/").filter((s) => s !== ".");
6667
6790
  const out2 = [];
@@ -6748,7 +6871,7 @@ function resolveExtends(fileSet, fromDir, ext) {
6748
6871
  function readTsConfig(root, fileSet, rel, warnings, seen) {
6749
6872
  if (seen.has(rel)) return void 0;
6750
6873
  seen.add(rel);
6751
- const cfg = tolerantJsonParse(readText(join4(root, rel)));
6874
+ const cfg = tolerantJsonParse(readText(join6(root, rel)));
6752
6875
  if (cfg === void 0) {
6753
6876
  warnings.push(`unparseable ${rel} \u2014 its path aliases were ignored`);
6754
6877
  return void 0;
@@ -6877,7 +7000,7 @@ function buildResolveContext(scan2) {
6877
7000
  const goModules = [];
6878
7001
  for (const rel of fileSet) {
6879
7002
  if (rel !== "go.mod" && !rel.endsWith("/go.mod")) continue;
6880
- const text = readText(join4(scan2.root, rel));
7003
+ const text = readText(join6(scan2.root, rel));
6881
7004
  const m = /^\s*module\s+(\S+)/m.exec(text);
6882
7005
  if (!m) continue;
6883
7006
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6887,7 +7010,7 @@ function buildResolveContext(scan2) {
6887
7010
  const rustCrates = [];
6888
7011
  for (const rel of fileSet) {
6889
7012
  if (rel !== "Cargo.toml" && !rel.endsWith("/Cargo.toml")) continue;
6890
- const text = readText(join4(scan2.root, rel));
7013
+ const text = readText(join6(scan2.root, rel));
6891
7014
  const m = /\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/ms.exec(text);
6892
7015
  if (!m) continue;
6893
7016
  const dir = rel.includes("/") ? posix.dirname(rel) : "";
@@ -6914,7 +7037,7 @@ function buildResolveContext(scan2) {
6914
7037
  const workspacePackages = [];
6915
7038
  for (const rel of fileSet) {
6916
7039
  if (rel !== "package.json" && !rel.endsWith("/package.json")) continue;
6917
- const pkg = tolerantJsonParse(readText(join4(scan2.root, rel)));
7040
+ const pkg = tolerantJsonParse(readText(join6(scan2.root, rel)));
6918
7041
  if (pkg === void 0) {
6919
7042
  warnings.push(`unparseable ${rel} \u2014 skipped for workspace resolution`);
6920
7043
  continue;
@@ -6941,7 +7064,7 @@ function buildResolveContext(scan2) {
6941
7064
  const phpPsr4 = [];
6942
7065
  for (const rel of fileSet) {
6943
7066
  if (rel !== "composer.json" && !rel.endsWith("/composer.json")) continue;
6944
- const composer = tolerantJsonParse(readText(join4(scan2.root, rel)));
7067
+ const composer = tolerantJsonParse(readText(join6(scan2.root, rel)));
6945
7068
  if (!composer) {
6946
7069
  warnings.push(`unparseable ${rel} \u2014 skipped for PHP PSR-4 resolution`);
6947
7070
  continue;
@@ -7515,255 +7638,191 @@ var init_calls = __esm({
7515
7638
  }
7516
7639
  });
7517
7640
 
7518
- // src/graph.ts
7519
- import { join as join5 } from "path";
7520
- function isDistinctive(name2) {
7521
- if (name2.length < 5) return false;
7522
- const internalUpper = /[a-z][A-Z]/.test(name2) || /[A-Z]{2}/.test(name2);
7523
- return internalUpper || name2.includes("_") || /\d/.test(name2);
7641
+ // src/render/symbols-json.ts
7642
+ function computeSymbolRefs(scan2) {
7643
+ const unique = uniqueDefsFor(scan2);
7644
+ const refs = /* @__PURE__ */ new Map();
7645
+ if (!unique.size) return refs;
7646
+ const add = (name2, file) => {
7647
+ let set = refs.get(name2);
7648
+ if (!set) refs.set(name2, set = /* @__PURE__ */ new Set());
7649
+ set.add(file);
7650
+ };
7651
+ for (const f of scan2.files) {
7652
+ if (f.kind === "code" && f.idents) {
7653
+ for (const id of f.idents) {
7654
+ const target = unique.get(id);
7655
+ if (target && target !== f.rel) add(id, f.rel);
7656
+ }
7657
+ } else if (f.kind === "doc") {
7658
+ const content = scan2.docText.get(f.rel);
7659
+ if (!content) continue;
7660
+ for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
7661
+ const target = unique.get(tok);
7662
+ if (target && target !== f.rel) add(tok, f.rel);
7663
+ }
7664
+ }
7665
+ }
7666
+ return refs;
7524
7667
  }
7525
- function uniqueSymbolDefs(scan2) {
7526
- const byName = /* @__PURE__ */ new Map();
7668
+ function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
7669
+ const defsByName = /* @__PURE__ */ new Map();
7527
7670
  for (const f of scan2.files) {
7528
7671
  for (const s of f.symbols) {
7529
- if (!s.exported || REFERENCE_KINDS2.has(s.kind) || !isDistinctive(s.name)) continue;
7530
- let set = byName.get(s.name);
7531
- if (!set) byName.set(s.name, set = /* @__PURE__ */ new Set());
7532
- set.add(f.rel);
7672
+ let arr = defsByName.get(s.name);
7673
+ if (!arr) defsByName.set(s.name, arr = []);
7674
+ arr.push({
7675
+ file: s.file,
7676
+ line: s.line,
7677
+ ...s.endLine !== void 0 ? { endLine: s.endLine } : {},
7678
+ kind: s.kind,
7679
+ exported: s.exported,
7680
+ lang: s.lang,
7681
+ ...s.parent ? { parent: s.parent } : {}
7682
+ });
7533
7683
  }
7534
7684
  }
7535
- const unique = /* @__PURE__ */ new Map();
7536
- for (const [name2, files] of byName) if (files.size === 1) unique.set(name2, [...files][0]);
7537
- return unique;
7685
+ const defs = {};
7686
+ for (const name2 of [...defsByName.keys()].sort(byStr)) {
7687
+ defs[name2] = defsByName.get(name2).slice().sort((a, b) => byStr(a.file, b.file) || a.line - b.line || byStr(a.kind, b.kind));
7688
+ }
7689
+ const refsOut = {};
7690
+ for (const name2 of [...refs.keys()].sort(byStr)) {
7691
+ const files = [...refs.get(name2)].sort(byStr);
7692
+ if (files.length) refsOut[name2] = files;
7693
+ }
7694
+ return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
7538
7695
  }
7539
- function collect(edges, e) {
7540
- const k = keyOf(e.from, e.to, e.kind);
7541
- const prev = edges.get(k);
7542
- if (prev) {
7543
- prev.weight += e.weight;
7544
- return;
7696
+ function renderSymbolsJson(index) {
7697
+ return JSON.stringify(index, null, 2) + "\n";
7698
+ }
7699
+ var init_symbols_json = __esm({
7700
+ "src/render/symbols-json.ts"() {
7701
+ "use strict";
7702
+ init_types();
7703
+ init_sort();
7704
+ init_derived();
7545
7705
  }
7546
- edges.set(k, { ...e });
7706
+ });
7707
+
7708
+ // src/callers.ts
7709
+ function computeImportPairs(scan2) {
7710
+ return new Set(importPairsFor(scan2));
7547
7711
  }
7548
- function buildGraph(scan2, ctx, modules, moduleOf, meta) {
7549
- const fileEdgeMap = /* @__PURE__ */ new Map();
7550
- const importPairs = /* @__PURE__ */ new Set();
7712
+ function buildCallerIndex(scan2, importPairs, opts = {}) {
7713
+ const pairs = importPairs ?? importPairsFor(scan2);
7714
+ const recall = opts.recall === true;
7715
+ const defs = /* @__PURE__ */ new Map();
7551
7716
  for (const f of scan2.files) {
7552
- for (const ref of f.refs) {
7553
- if (ref.kind === "doc-link") {
7554
- const r = resolveDocLink(f.rel, ref.spec, ctx);
7555
- if (r.kind === "external") continue;
7556
- if (r.kind === "dangling") {
7557
- collect(fileEdgeMap, { from: f.rel, to: ref.spec, kind: "doc-link", weight: 1, dangling: true, reason: r.reason });
7558
- } else if (r.target !== f.rel) {
7559
- collect(fileEdgeMap, { from: f.rel, to: r.target, kind: "doc-link", weight: 1 });
7560
- }
7561
- } else {
7562
- const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
7563
- if (r.kind === "external") continue;
7564
- if (r.kind === "dangling") {
7565
- collect(fileEdgeMap, { from: f.rel, to: ref.spec, kind: "import", weight: 1, dangling: true, reason: r.reason });
7566
- } else if (r.target !== f.rel) {
7567
- collect(fileEdgeMap, { from: f.rel, to: r.target, kind: "import", weight: 1 });
7568
- importPairs.add(`${f.rel}|${r.target}`);
7569
- }
7570
- }
7717
+ const seen = /* @__PURE__ */ new Set();
7718
+ for (const s of f.symbols) {
7719
+ if (!s.exported || REFERENCE_KINDS2.has(s.kind)) continue;
7720
+ if (seen.has(s.name)) continue;
7721
+ seen.add(s.name);
7722
+ let arr = defs.get(s.name);
7723
+ if (!arr) defs.set(s.name, arr = []);
7724
+ arr.push(s);
7571
7725
  }
7572
7726
  }
7573
- const callPairs = /* @__PURE__ */ new Set();
7574
- for (const e of resolveCallEdges(scan2, importPairs)) {
7575
- collect(fileEdgeMap, e);
7576
- callPairs.add(`${e.from}|${e.to}`);
7577
- }
7578
- const unique = uniqueSymbolDefs(scan2);
7579
- if (unique.size) {
7580
- for (const f of scan2.files) {
7581
- if (f.kind !== "code" || !f.idents?.length) continue;
7582
- const perTarget = /* @__PURE__ */ new Map();
7583
- for (const id of f.idents) {
7584
- const target = unique.get(id);
7585
- if (!target || target === f.rel) continue;
7586
- perTarget.set(target, (perTarget.get(target) ?? 0) + 1);
7587
- }
7588
- for (const [target, count] of perTarget) {
7589
- const pair = `${f.rel}|${target}`;
7590
- if (importPairs.has(pair) || callPairs.has(pair)) continue;
7591
- collect(fileEdgeMap, { from: f.rel, to: target, kind: "use", weight: Math.min(count, 5) });
7592
- }
7727
+ const localDefs = /* @__PURE__ */ new Map();
7728
+ for (const f of scan2.files) {
7729
+ const byName = /* @__PURE__ */ new Map();
7730
+ for (const s of f.symbols) {
7731
+ if (!REFERENCE_KINDS2.has(s.kind) && !byName.has(s.name)) byName.set(s.name, s);
7593
7732
  }
7733
+ localDefs.set(f.rel, byName);
7594
7734
  }
7595
- if (unique.size) {
7596
- for (const f of scan2.files) {
7597
- if (f.kind !== "doc") continue;
7598
- const content = scan2.docText.get(f.rel) ?? readText(join5(scan2.root, f.rel));
7599
- if (!content) continue;
7600
- const tokens = /* @__PURE__ */ new Map();
7601
- for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
7602
- if (unique.has(tok)) tokens.set(tok, (tokens.get(tok) ?? 0) + 1);
7603
- }
7604
- for (const [name2, count] of tokens) {
7605
- const target = unique.get(name2);
7606
- if (target === f.rel) continue;
7607
- collect(fileEdgeMap, { from: f.rel, to: target, kind: "mention", weight: Math.min(count, 5) });
7735
+ const sites = /* @__PURE__ */ new Map();
7736
+ const record = (def, caller) => {
7737
+ let entry = sites.get(def.name + "\0" + def.file);
7738
+ if (!entry) sites.set(def.name + "\0" + def.file, entry = { def, callers: [] });
7739
+ entry.callers.push(caller);
7740
+ };
7741
+ for (const f of scan2.files) {
7742
+ if (!f.calls?.length) continue;
7743
+ const family = familyOf(f.lang);
7744
+ const own = localDefs.get(f.rel);
7745
+ for (const c2 of f.calls) {
7746
+ const local = own.get(c2.name);
7747
+ if (local) {
7748
+ if (local.line !== c2.line)
7749
+ record(local, recall ? { file: f.rel, line: c2.line, confidence: "corroborated" } : { file: f.rel, line: c2.line });
7750
+ continue;
7608
7751
  }
7752
+ const cands = (defs.get(c2.name) ?? []).filter((d) => familyOf(d.lang) === family && d.file !== f.rel).map((d) => ({ file: d.file, lang: d.lang }));
7753
+ if (!cands.length) continue;
7754
+ const imported = cands.filter((d) => pairs.has(`${f.rel}|${d.file}`));
7755
+ const chosen = family === "js" ? imported.length ? pickCandidate(f.rel, imported) : (
7756
+ // JS/TS gate: no corroborating import → no binding. Recall mode
7757
+ // relaxes this to a unique-repo-wide name match (issue #7).
7758
+ recall && cands.length === 1 ? cands[0] : void 0
7759
+ ) : imported.length ? pickCandidate(f.rel, imported) : pickCandidate(f.rel, cands);
7760
+ if (!chosen) continue;
7761
+ const def = defs.get(c2.name).find((d) => d.file === chosen.file);
7762
+ record(
7763
+ def,
7764
+ recall ? { file: f.rel, line: c2.line, confidence: imported.length ? "corroborated" : "unique-name" } : { file: f.rel, line: c2.line }
7765
+ );
7609
7766
  }
7610
7767
  }
7611
- const fileEdges = [...fileEdgeMap.values()].sort(
7612
- (a, b) => byStr(a.from, b.from) || byStr(a.to, b.to) || byStr(a.kind, b.kind)
7613
- );
7614
- const degIn = /* @__PURE__ */ new Map();
7615
- const degOut = /* @__PURE__ */ new Map();
7616
- const fileSet = new Set(scan2.files.map((f) => f.rel));
7617
- for (const e of fileEdges) {
7618
- if (e.dangling || !fileSet.has(e.to)) continue;
7619
- degOut.set(e.from, (degOut.get(e.from) ?? 0) + 1);
7620
- degIn.set(e.to, (degIn.get(e.to) ?? 0) + 1);
7621
- }
7622
- const KIND_RANK = { import: 5, call: 4, use: 3, "doc-link": 2, mention: 1, contains: 0 };
7623
- const modEdgeMap = /* @__PURE__ */ new Map();
7624
- for (const e of fileEdges) {
7625
- if (e.dangling || !fileSet.has(e.to)) continue;
7626
- const from = moduleOf.get(e.from);
7627
- const to = moduleOf.get(e.to);
7628
- if (!from || !to || from === to) continue;
7629
- const k = `${from}\0${to}`;
7630
- const prev = modEdgeMap.get(k);
7631
- if (prev) {
7632
- prev.weight += e.weight;
7633
- if ((KIND_RANK[e.kind] ?? 0) > (KIND_RANK[prev.kind] ?? 0)) prev.kind = e.kind;
7634
- } else {
7635
- modEdgeMap.set(k, { from, to, kind: e.kind, weight: e.weight });
7636
- }
7637
- }
7638
- const moduleEdges = [...modEdgeMap.values()].sort((a, b) => byStr(a.from, b.from) || byStr(a.to, b.to));
7639
- const modDegIn = /* @__PURE__ */ new Map();
7640
- const modDegOut = /* @__PURE__ */ new Map();
7641
- for (const e of moduleEdges) {
7642
- modDegOut.set(e.from, (modDegOut.get(e.from) ?? 0) + 1);
7643
- modDegIn.set(e.to, (modDegIn.get(e.to) ?? 0) + 1);
7644
- }
7645
- const files = scan2.files.map((f) => ({
7646
- id: f.rel,
7647
- kind: "file",
7648
- rel: f.rel,
7649
- fileKind: f.kind,
7650
- lang: f.lang,
7651
- module: moduleOf.get(f.rel) ?? "root",
7652
- title: f.title,
7653
- summary: f.summary,
7654
- symbols: f.symbols.length,
7655
- lines: f.lines,
7656
- degIn: degIn.get(f.rel) ?? 0,
7657
- degOut: degOut.get(f.rel) ?? 0
7658
- })).sort((a, b) => byStr(a.rel, b.rel));
7659
- const symbolsByModule = /* @__PURE__ */ new Map();
7660
- for (const f of scan2.files) {
7661
- const slug = moduleOf.get(f.rel) ?? "root";
7662
- symbolsByModule.set(slug, (symbolsByModule.get(slug) ?? 0) + f.symbols.length);
7768
+ const index = /* @__PURE__ */ new Map();
7769
+ const keys = [...sites.keys()].sort(byStr);
7770
+ for (const key of keys) {
7771
+ const { def, callers } = sites.get(key);
7772
+ callers.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7773
+ if (!index.has(def.name)) index.set(def.name, { def, callers });
7774
+ else index.set(`${def.name}@${def.file}`, { def, callers });
7663
7775
  }
7664
- const moduleNodes = modules.map((m) => ({
7665
- id: m.slug,
7666
- kind: "module",
7667
- slug: m.slug,
7668
- path: m.path,
7669
- title: m.title,
7670
- summary: m.summary,
7671
- tier: m.tier,
7672
- members: m.members,
7673
- symbols: symbolsByModule.get(m.slug) ?? 0,
7674
- degIn: modDegIn.get(m.slug) ?? 0,
7675
- degOut: modDegOut.get(m.slug) ?? 0
7676
- })).sort((a, b) => byStr(a.slug, b.slug));
7677
- return {
7678
- schemaVersion: meta?.schemaVersion ?? SCHEMA_VERSION,
7679
- version: meta?.version ?? ENGINE_VERSION,
7680
- commit: scan2.commit,
7681
- fileCount: scan2.files.length,
7682
- languages: scan2.languages,
7683
- files,
7684
- modules: moduleNodes,
7685
- fileEdges,
7686
- moduleEdges
7687
- };
7776
+ return index;
7688
7777
  }
7689
- var REFERENCE_KINDS2, keyOf;
7690
- var init_graph = __esm({
7691
- "src/graph.ts"() {
7692
- "use strict";
7693
- init_types();
7694
- init_resolve();
7695
- init_calls();
7696
- init_walk();
7697
- init_sort();
7698
- REFERENCE_KINDS2 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7699
- keyOf = (from, to, kind) => `${from}\0${to}\0${kind}`;
7700
- }
7701
- });
7702
-
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
- };
7713
- for (const f of scan2.files) {
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
- }
7778
+ function enclosingSymbol(scan2, file, line) {
7779
+ const f = scan2.files.find((x) => x.rel === file);
7780
+ if (!f?.symbols.length) return void 0;
7781
+ return enclosingAmong(f.symbols, line);
7782
+ }
7783
+ function enclosingAmong(symbols, line) {
7784
+ let best;
7785
+ for (const s of symbols) {
7786
+ if (REFERENCE_KINDS2.has(s.kind)) continue;
7787
+ if (s.line > line) continue;
7788
+ if (s.endLine !== void 0 && line > s.endLine) continue;
7789
+ if (!best || s.line > best.line || s.line === best.line && (s.endLine ?? Infinity) <= (best.endLine ?? Infinity)) {
7790
+ best = s;
7726
7791
  }
7727
7792
  }
7728
- return refs;
7793
+ return best;
7729
7794
  }
7730
- function buildSymbolIndex(scan2, refs = /* @__PURE__ */ new Map()) {
7731
- const defsByName = /* @__PURE__ */ new Map();
7795
+ function buildRawCallerIndex(scan2) {
7796
+ const byName = /* @__PURE__ */ new Map();
7732
7797
  for (const f of scan2.files) {
7733
- for (const s of f.symbols) {
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
- });
7798
+ if (!f.calls?.length) continue;
7799
+ const symbols = f.symbols.filter((s) => !REFERENCE_KINDS2.has(s.kind));
7800
+ for (const c2 of f.calls) {
7801
+ const site = { file: f.rel, line: c2.line };
7802
+ if (c2.receiver !== void 0) site.receiver = c2.receiver;
7803
+ const enc = enclosingAmong(symbols, c2.line);
7804
+ if (enc) site.enclosingSymbol = enc;
7805
+ let arr = byName.get(c2.name);
7806
+ if (!arr) byName.set(c2.name, arr = []);
7807
+ arr.push(site);
7745
7808
  }
7746
7809
  }
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));
7750
- }
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;
7810
+ const index = /* @__PURE__ */ new Map();
7811
+ for (const name2 of [...byName.keys()].sort(byStr)) {
7812
+ const sites = byName.get(name2);
7813
+ sites.sort((a, b) => byStr(a.file, b.file) || a.line - b.line);
7814
+ index.set(name2, sites);
7755
7815
  }
7756
- return { schemaVersion: SCHEMA_VERSION, defs, refs: refsOut };
7757
- }
7758
- function renderSymbolsJson(index) {
7759
- return JSON.stringify(index, null, 2) + "\n";
7816
+ return index;
7760
7817
  }
7761
- var init_symbols_json = __esm({
7762
- "src/render/symbols-json.ts"() {
7818
+ var REFERENCE_KINDS2;
7819
+ var init_callers = __esm({
7820
+ "src/callers.ts"() {
7763
7821
  "use strict";
7764
- init_types();
7822
+ init_calls();
7823
+ init_derived();
7765
7824
  init_sort();
7766
- init_graph();
7825
+ REFERENCE_KINDS2 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
7767
7826
  }
7768
7827
  });
7769
7828
 
@@ -7941,7 +8000,7 @@ var init_bm25 = __esm({
7941
8000
  });
7942
8001
 
7943
8002
  // src/complexity.ts
7944
- import { join as join6 } from "path";
8003
+ import { join as join7 } from "path";
7945
8004
  function complexityOfSource(source) {
7946
8005
  return 1 + (source.match(BRANCH_RE) ?? []).length;
7947
8006
  }
@@ -7951,7 +8010,7 @@ function symbolComplexity(scan2, rel, top = 50) {
7951
8010
  if (f.kind !== "code") continue;
7952
8011
  if (rel && f.rel !== rel) continue;
7953
8012
  if (!f.symbols.length) continue;
7954
- const lines = readText(join6(scan2.root, f.rel)).split("\n");
8013
+ const lines = readText(join7(scan2.root, f.rel)).split("\n");
7955
8014
  for (const s of f.symbols) {
7956
8015
  if (s.kind === "reexport" || s.kind === "reexport-all") continue;
7957
8016
  const end = s.endLine ?? s.line;
@@ -7986,7 +8045,7 @@ var init_complexity = __esm({
7986
8045
  });
7987
8046
 
7988
8047
  // src/derived.ts
7989
- import { join as join7 } from "path";
8048
+ import { join as join8 } from "path";
7990
8049
  function cacheFor(scan2) {
7991
8050
  let c2 = caches.get(scan2);
7992
8051
  if (!c2) caches.set(scan2, c2 = {});
@@ -8012,6 +8071,11 @@ function importPairsFor(scan2) {
8012
8071
  }
8013
8072
  return c2.importPairs;
8014
8073
  }
8074
+ function publishImportPairs(scan2, ctx, pairs) {
8075
+ const c2 = caches.get(scan2);
8076
+ if (!c2 || c2.resolveCtx !== ctx || c2.importPairs) return;
8077
+ c2.importPairs = pairs;
8078
+ }
8015
8079
  function uniqueDefsFor(scan2) {
8016
8080
  const c2 = cacheFor(scan2);
8017
8081
  return c2.uniqueDefs ??= uniqueSymbolDefs(scan2);
@@ -8039,7 +8103,7 @@ function fileComplexityFor(scan2) {
8039
8103
  const m = /* @__PURE__ */ new Map();
8040
8104
  for (const f of scan2.files) {
8041
8105
  if (f.kind !== "code") continue;
8042
- m.set(f.rel, complexityOfSource(readText(join7(scan2.root, f.rel))));
8106
+ m.set(f.rel, complexityOfSource(readText(join8(scan2.root, f.rel))));
8043
8107
  }
8044
8108
  c2.fileComplexity = m;
8045
8109
  }
@@ -8060,129 +8124,196 @@ var init_derived = __esm({
8060
8124
  }
8061
8125
  });
8062
8126
 
8063
- // src/callers.ts
8064
- function computeImportPairs(scan2) {
8065
- return new Set(importPairsFor(scan2));
8127
+ // src/graph.ts
8128
+ import { join as join9 } from "path";
8129
+ function isDistinctive(name2) {
8130
+ if (name2.length < 5) return false;
8131
+ const internalUpper = /[a-z][A-Z]/.test(name2) || /[A-Z]{2}/.test(name2);
8132
+ return internalUpper || name2.includes("_") || /\d/.test(name2);
8066
8133
  }
8067
- function buildCallerIndex(scan2, importPairs, opts = {}) {
8068
- const pairs = importPairs ?? importPairsFor(scan2);
8069
- const recall = opts.recall === true;
8070
- const defs = /* @__PURE__ */ new Map();
8134
+ function uniqueSymbolDefs(scan2) {
8135
+ const byName = /* @__PURE__ */ new Map();
8071
8136
  for (const f of scan2.files) {
8072
- const seen = /* @__PURE__ */ new Set();
8073
8137
  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);
8138
+ if (!s.exported || REFERENCE_KINDS3.has(s.kind) || !isDistinctive(s.name)) continue;
8139
+ let set = byName.get(s.name);
8140
+ if (!set) byName.set(s.name, set = /* @__PURE__ */ new Set());
8141
+ set.add(f.rel);
8080
8142
  }
8081
8143
  }
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);
8144
+ const unique = /* @__PURE__ */ new Map();
8145
+ for (const [name2, files] of byName) if (files.size === 1) unique.set(name2, [...files][0]);
8146
+ return unique;
8147
+ }
8148
+ function collect(edges, e) {
8149
+ const k = keyOf(e.from, e.to, e.kind);
8150
+ const prev = edges.get(k);
8151
+ if (prev) {
8152
+ prev.weight += e.weight;
8153
+ return;
8089
8154
  }
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
- };
8155
+ edges.set(k, { ...e });
8156
+ }
8157
+ function buildGraph(scan2, ctx, modules, moduleOf, meta) {
8158
+ const fileEdgeMap = /* @__PURE__ */ new Map();
8159
+ const importPairs = /* @__PURE__ */ new Set();
8096
8160
  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;
8161
+ for (const ref of f.refs) {
8162
+ if (ref.kind === "doc-link") {
8163
+ const r = resolveDocLink(f.rel, ref.spec, ctx);
8164
+ if (r.kind === "external") continue;
8165
+ if (r.kind === "dangling") {
8166
+ collect(fileEdgeMap, { from: f.rel, to: ref.spec, kind: "doc-link", weight: 1, dangling: true, reason: r.reason });
8167
+ } else if (r.target !== f.rel) {
8168
+ collect(fileEdgeMap, { from: f.rel, to: r.target, kind: "doc-link", weight: 1 });
8169
+ }
8170
+ } else {
8171
+ const r = resolveImport(f.rel, f.ext, ref.spec, ctx);
8172
+ if (r.kind === "external") continue;
8173
+ if (r.kind === "dangling") {
8174
+ collect(fileEdgeMap, { from: f.rel, to: ref.spec, kind: "import", weight: 1, dangling: true, reason: r.reason });
8175
+ } else if (r.target !== f.rel) {
8176
+ collect(fileEdgeMap, { from: f.rel, to: r.target, kind: "import", weight: 1 });
8177
+ importPairs.add(`${f.rel}|${r.target}`);
8178
+ }
8106
8179
  }
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
8180
  }
8122
8181
  }
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 });
8182
+ const callPairs = /* @__PURE__ */ new Set();
8183
+ for (const e of resolveCallEdges(scan2, importPairs)) {
8184
+ collect(fileEdgeMap, e);
8185
+ callPairs.add(`${e.from}|${e.to}`);
8130
8186
  }
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;
8187
+ publishImportPairs(scan2, ctx, importPairs);
8188
+ const unique = uniqueDefsFor(scan2);
8189
+ if (unique.size) {
8190
+ for (const f of scan2.files) {
8191
+ if (f.kind !== "code" || !f.idents?.length) continue;
8192
+ const perTarget = /* @__PURE__ */ new Map();
8193
+ for (const id of f.idents) {
8194
+ const target = unique.get(id);
8195
+ if (!target || target === f.rel) continue;
8196
+ perTarget.set(target, (perTarget.get(target) ?? 0) + 1);
8197
+ }
8198
+ for (const [target, count] of perTarget) {
8199
+ const pair = `${f.rel}|${target}`;
8200
+ if (importPairs.has(pair) || callPairs.has(pair)) continue;
8201
+ collect(fileEdgeMap, { from: f.rel, to: target, kind: "use", weight: Math.min(count, 5) });
8202
+ }
8146
8203
  }
8147
8204
  }
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);
8205
+ if (unique.size) {
8206
+ for (const f of scan2.files) {
8207
+ if (f.kind !== "doc") continue;
8208
+ const content = scan2.docText.get(f.rel) ?? readText(join9(scan2.root, f.rel));
8209
+ if (!content) continue;
8210
+ const tokens = /* @__PURE__ */ new Map();
8211
+ for (const tok of content.split(/[^A-Za-z0-9_]+/)) {
8212
+ if (unique.has(tok)) tokens.set(tok, (tokens.get(tok) ?? 0) + 1);
8213
+ }
8214
+ for (const [name2, count] of tokens) {
8215
+ const target = unique.get(name2);
8216
+ if (target === f.rel) continue;
8217
+ collect(fileEdgeMap, { from: f.rel, to: target, kind: "mention", weight: Math.min(count, 5) });
8218
+ }
8163
8219
  }
8164
8220
  }
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);
8221
+ const fileEdges = [...fileEdgeMap.values()].sort(
8222
+ (a, b) => byStr(a.from, b.from) || byStr(a.to, b.to) || byStr(a.kind, b.kind)
8223
+ );
8224
+ const degIn = /* @__PURE__ */ new Map();
8225
+ const degOut = /* @__PURE__ */ new Map();
8226
+ const fileSet = new Set(scan2.files.map((f) => f.rel));
8227
+ for (const e of fileEdges) {
8228
+ if (e.dangling || !fileSet.has(e.to)) continue;
8229
+ degOut.set(e.from, (degOut.get(e.from) ?? 0) + 1);
8230
+ degIn.set(e.to, (degIn.get(e.to) ?? 0) + 1);
8170
8231
  }
8171
- return index;
8172
- }
8173
- var REFERENCE_KINDS3;
8174
- var init_callers = __esm({
8175
- "src/callers.ts"() {
8232
+ const KIND_RANK = { import: 5, call: 4, use: 3, "doc-link": 2, mention: 1, contains: 0 };
8233
+ const modEdgeMap = /* @__PURE__ */ new Map();
8234
+ for (const e of fileEdges) {
8235
+ if (e.dangling || !fileSet.has(e.to)) continue;
8236
+ const from = moduleOf.get(e.from);
8237
+ const to = moduleOf.get(e.to);
8238
+ if (!from || !to || from === to) continue;
8239
+ const k = `${from}${SEP}${to}`;
8240
+ const prev = modEdgeMap.get(k);
8241
+ if (prev) {
8242
+ prev.weight += e.weight;
8243
+ if ((KIND_RANK[e.kind] ?? 0) > (KIND_RANK[prev.kind] ?? 0)) prev.kind = e.kind;
8244
+ } else {
8245
+ modEdgeMap.set(k, { from, to, kind: e.kind, weight: e.weight });
8246
+ }
8247
+ }
8248
+ const moduleEdges = [...modEdgeMap.values()].sort((a, b) => byStr(a.from, b.from) || byStr(a.to, b.to));
8249
+ const modDegIn = /* @__PURE__ */ new Map();
8250
+ const modDegOut = /* @__PURE__ */ new Map();
8251
+ for (const e of moduleEdges) {
8252
+ modDegOut.set(e.from, (modDegOut.get(e.from) ?? 0) + 1);
8253
+ modDegIn.set(e.to, (modDegIn.get(e.to) ?? 0) + 1);
8254
+ }
8255
+ const files = scan2.files.map((f) => ({
8256
+ id: f.rel,
8257
+ kind: "file",
8258
+ rel: f.rel,
8259
+ fileKind: f.kind,
8260
+ lang: f.lang,
8261
+ module: moduleOf.get(f.rel) ?? "root",
8262
+ title: f.title,
8263
+ summary: f.summary,
8264
+ symbols: f.symbols.length,
8265
+ lines: f.lines,
8266
+ degIn: degIn.get(f.rel) ?? 0,
8267
+ degOut: degOut.get(f.rel) ?? 0
8268
+ })).sort((a, b) => byStr(a.rel, b.rel));
8269
+ const symbolsByModule = /* @__PURE__ */ new Map();
8270
+ for (const f of scan2.files) {
8271
+ const slug = moduleOf.get(f.rel) ?? "root";
8272
+ symbolsByModule.set(slug, (symbolsByModule.get(slug) ?? 0) + f.symbols.length);
8273
+ }
8274
+ const moduleNodes = modules.map((m) => ({
8275
+ id: m.slug,
8276
+ kind: "module",
8277
+ slug: m.slug,
8278
+ path: m.path,
8279
+ title: m.title,
8280
+ summary: m.summary,
8281
+ tier: m.tier,
8282
+ members: m.members,
8283
+ symbols: symbolsByModule.get(m.slug) ?? 0,
8284
+ degIn: modDegIn.get(m.slug) ?? 0,
8285
+ degOut: modDegOut.get(m.slug) ?? 0
8286
+ })).sort((a, b) => byStr(a.slug, b.slug));
8287
+ return {
8288
+ schemaVersion: meta?.schemaVersion ?? SCHEMA_VERSION,
8289
+ version: meta?.version ?? ENGINE_VERSION,
8290
+ commit: scan2.commit,
8291
+ fileCount: scan2.files.length,
8292
+ languages: scan2.languages,
8293
+ files,
8294
+ modules: moduleNodes,
8295
+ fileEdges,
8296
+ moduleEdges
8297
+ };
8298
+ }
8299
+ var REFERENCE_KINDS3, SEP, keyOf;
8300
+ var init_graph = __esm({
8301
+ "src/graph.ts"() {
8176
8302
  "use strict";
8303
+ init_types();
8304
+ init_resolve();
8177
8305
  init_calls();
8178
8306
  init_derived();
8307
+ init_walk();
8179
8308
  init_sort();
8180
8309
  REFERENCE_KINDS3 = /* @__PURE__ */ new Set(["reexport", "reexport-all", "default"]);
8310
+ SEP = "\0";
8311
+ keyOf = (from, to, kind) => `${from}${SEP}${to}${SEP}${kind}`;
8181
8312
  }
8182
8313
  });
8183
8314
 
8184
8315
  // src/query.ts
8185
- import { join as join8 } from "path";
8316
+ import { join as join10 } from "path";
8186
8317
  function symbolsOverview(scan2, rel) {
8187
8318
  const f = scan2.files.find((x) => x.rel === rel);
8188
8319
  if (!f) return [];
@@ -8213,7 +8344,7 @@ function findSymbol(scan2, namePath, opts = {}) {
8213
8344
  if (opts.includeBody) {
8214
8345
  for (const m of capped) {
8215
8346
  const end = m.endLine ?? m.line;
8216
- const content = readText(join8(scan2.root, m.file));
8347
+ const content = readText(join10(scan2.root, m.file));
8217
8348
  if (!content) continue;
8218
8349
  m.body = content.split("\n").slice(m.line - 1, end).join("\n");
8219
8350
  }
@@ -8259,8 +8390,8 @@ var init_query = __esm({
8259
8390
  });
8260
8391
 
8261
8392
  // src/edit.ts
8262
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
8263
- import { join as join9 } from "path";
8393
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
8394
+ import { join as join11 } from "path";
8264
8395
  function resolveUniqueSymbol(scan2, namePath, file) {
8265
8396
  let matches = findSymbol(scan2, namePath);
8266
8397
  if (file) matches = matches.filter((m) => m.file === file);
@@ -8273,12 +8404,12 @@ function resolveUniqueSymbol(scan2, namePath, file) {
8273
8404
  throw new Error(`"${namePath}" is ambiguous (${matches.length} matches: ${list}) \u2014 qualify with \`file\` or a Parent/name path`);
8274
8405
  }
8275
8406
  function readLines(abs) {
8276
- return readFileSync4(abs, "utf8").split("\n");
8407
+ return readFileSync5(abs, "utf8").split("\n");
8277
8408
  }
8278
8409
  function replaceSymbolBody(scan2, namePath, body2, file) {
8279
8410
  const sym = resolveUniqueSymbol(scan2, namePath, file);
8280
8411
  const end = sym.endLine ?? sym.line;
8281
- const abs = join9(scan2.root, sym.file);
8412
+ const abs = join11(scan2.root, sym.file);
8282
8413
  const lines = readLines(abs);
8283
8414
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
8284
8415
  lines.splice(sym.line - 1, end - sym.line + 1, ...newLines);
@@ -8286,7 +8417,7 @@ function replaceSymbolBody(scan2, namePath, body2, file) {
8286
8417
  return { file: sym.file, startLine: sym.line, endLine: sym.line + newLines.length - 1, lines: newLines.length };
8287
8418
  }
8288
8419
  function insertAt(scan2, sym, body2, index, blankBefore, blankAfter) {
8289
- const abs = join9(scan2.root, sym.file);
8420
+ const abs = join11(scan2.root, sym.file);
8290
8421
  const lines = readLines(abs);
8291
8422
  const minGap = SEPARATED_KINDS.has(sym.kind) ? 1 : 0;
8292
8423
  const newLines = body2.replace(/^\n+|\n+$/g, "").split("\n");
@@ -8317,8 +8448,8 @@ var init_edit = __esm({
8317
8448
  });
8318
8449
 
8319
8450
  // src/memory.ts
8320
- import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
8321
- import { dirname as dirname3, join as join10 } from "path";
8451
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync6, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
8452
+ import { dirname as dirname4, join as join12 } from "path";
8322
8453
  function sanitize(name2) {
8323
8454
  const clean = name2.replace(/^mem:/, "").replace(/\.md$/, "");
8324
8455
  if (!clean) throw new Error("memory name is empty");
@@ -8332,17 +8463,17 @@ function sanitize(name2) {
8332
8463
  return clean;
8333
8464
  }
8334
8465
  function memoryPath(repo, name2) {
8335
- return join10(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
8466
+ return join12(repo, ...MEMORY_DIR, `${sanitize(name2)}.md`);
8336
8467
  }
8337
8468
  function writeMemory(repo, name2, content) {
8338
8469
  const path = memoryPath(repo, name2);
8339
- mkdirSync2(dirname3(path), { recursive: true });
8470
+ mkdirSync2(dirname4(path), { recursive: true });
8340
8471
  writeFileSync3(path, content.endsWith("\n") ? content : content + "\n");
8341
8472
  return sanitize(name2);
8342
8473
  }
8343
8474
  function readMemory(repo, name2) {
8344
8475
  try {
8345
- return readFileSync5(memoryPath(repo, name2), "utf8");
8476
+ return readFileSync6(memoryPath(repo, name2), "utf8");
8346
8477
  } catch {
8347
8478
  return void 0;
8348
8479
  }
@@ -8350,7 +8481,7 @@ function readMemory(repo, name2) {
8350
8481
  function deleteMemory(repo, name2) {
8351
8482
  const path = memoryPath(repo, name2);
8352
8483
  try {
8353
- statSync2(path);
8484
+ statSync3(path);
8354
8485
  } catch {
8355
8486
  return false;
8356
8487
  }
@@ -8358,7 +8489,7 @@ function deleteMemory(repo, name2) {
8358
8489
  return true;
8359
8490
  }
8360
8491
  function listMemories(repo) {
8361
- const root = join10(repo, ...MEMORY_DIR);
8492
+ const root = join12(repo, ...MEMORY_DIR);
8362
8493
  const out2 = [];
8363
8494
  const walk2 = (dir, prefix) => {
8364
8495
  let entries;
@@ -8368,7 +8499,7 @@ function listMemories(repo) {
8368
8499
  return;
8369
8500
  }
8370
8501
  for (const e of entries) {
8371
- if (e.isDirectory()) walk2(join10(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
8502
+ if (e.isDirectory()) walk2(join12(dir, e.name), prefix ? `${prefix}/${e.name}` : e.name);
8372
8503
  else if (e.name.endsWith(".md")) out2.push(prefix ? `${prefix}/${e.name.slice(0, -3)}` : e.name.slice(0, -3));
8373
8504
  }
8374
8505
  };
@@ -8384,8 +8515,8 @@ var init_memory = __esm({
8384
8515
  });
8385
8516
 
8386
8517
  // src/workspaces.ts
8387
- import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
8388
- import { join as join11 } from "path";
8518
+ import { existsSync as existsSync4, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
8519
+ import { join as join13 } from "path";
8389
8520
  function readJson(path, label, warnings) {
8390
8521
  const raw = readText(path);
8391
8522
  if (!raw) return void 0;
@@ -8436,8 +8567,8 @@ function wsGlobToRegExp(pat) {
8436
8567
  return new RegExp(`^${re}($|/)`);
8437
8568
  }
8438
8569
  function probeNodePkg(root, dir, kind, warnings) {
8439
- const path = join11(root, dir, "package.json");
8440
- if (!existsSync3(path)) return void 0;
8570
+ const path = join13(root, dir, "package.json");
8571
+ if (!existsSync4(path)) return void 0;
8441
8572
  const manifest = `${dir}/package.json`;
8442
8573
  const pkg = readJson(path, manifest, warnings);
8443
8574
  const out2 = {
@@ -8450,8 +8581,8 @@ function probeNodePkg(root, dir, kind, warnings) {
8450
8581
  return out2;
8451
8582
  }
8452
8583
  function probeCargo(root, dir) {
8453
- const path = join11(root, dir, "Cargo.toml");
8454
- if (!existsSync3(path)) return void 0;
8584
+ const path = join13(root, dir, "Cargo.toml");
8585
+ if (!existsSync4(path)) return void 0;
8455
8586
  const body2 = tomlSectionBody(readText(path), "package");
8456
8587
  const out2 = {
8457
8588
  name: tomlString(body2, "name") ?? dir,
@@ -8464,19 +8595,19 @@ function probeCargo(root, dir) {
8464
8595
  return out2;
8465
8596
  }
8466
8597
  function probeGoMod(root, dir) {
8467
- const path = join11(root, dir, "go.mod");
8468
- if (!existsSync3(path)) return void 0;
8598
+ const path = join13(root, dir, "go.mod");
8599
+ if (!existsSync4(path)) return void 0;
8469
8600
  const name2 = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
8470
8601
  return { name: name2, dir, kind: "go", manifest: `${dir}/go.mod` };
8471
8602
  }
8472
8603
  function probeMaven(root, dir) {
8473
- const path = join11(root, dir, "pom.xml");
8474
- if (!existsSync3(path)) return void 0;
8604
+ const path = join13(root, dir, "pom.xml");
8605
+ if (!existsSync4(path)) return void 0;
8475
8606
  return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
8476
8607
  }
8477
8608
  function probePyproject(root, dir) {
8478
- const path = join11(root, dir, "pyproject.toml");
8479
- if (!existsSync3(path)) return void 0;
8609
+ const path = join13(root, dir, "pyproject.toml");
8610
+ if (!existsSync4(path)) return void 0;
8480
8611
  const toml = readText(path);
8481
8612
  const project = tomlSectionBody(toml, "project");
8482
8613
  const poetry = tomlSectionBody(toml, "tool.poetry");
@@ -8491,8 +8622,8 @@ function probePyproject(root, dir) {
8491
8622
  return out2;
8492
8623
  }
8493
8624
  function probeComposer(root, dir, warnings) {
8494
- const path = join11(root, dir, "composer.json");
8495
- if (!existsSync3(path)) return void 0;
8625
+ const path = join13(root, dir, "composer.json");
8626
+ if (!existsSync4(path)) return void 0;
8496
8627
  const manifest = `${dir}/composer.json`;
8497
8628
  const pkg = readJson(path, manifest, warnings);
8498
8629
  const out2 = {
@@ -8505,8 +8636,8 @@ function probeComposer(root, dir, warnings) {
8505
8636
  return out2;
8506
8637
  }
8507
8638
  function probeNxProject(root, dir, warnings) {
8508
- const path = join11(root, dir, "project.json");
8509
- if (!existsSync3(path)) return void 0;
8639
+ const path = join13(root, dir, "project.json");
8640
+ if (!existsSync4(path)) return void 0;
8510
8641
  const manifest = `${dir}/project.json`;
8511
8642
  const proj = readJson(path, manifest, warnings);
8512
8643
  return {
@@ -8518,7 +8649,7 @@ function probeNxProject(root, dir, warnings) {
8518
8649
  }
8519
8650
  function probeGradle(root, dir) {
8520
8651
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8521
- if (existsSync3(join11(root, dir, f))) {
8652
+ if (existsSync4(join13(root, dir, f))) {
8522
8653
  return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
8523
8654
  }
8524
8655
  }
@@ -8553,7 +8684,7 @@ function addPackage(root, dir, found, kind, warnings) {
8553
8684
  }
8554
8685
  function isDirAt(root, rel) {
8555
8686
  try {
8556
- return statSync3(join11(root, rel)).isDirectory();
8687
+ return statSync4(join13(root, rel)).isDirectory();
8557
8688
  } catch {
8558
8689
  return false;
8559
8690
  }
@@ -8561,7 +8692,7 @@ function isDirAt(root, rel) {
8561
8692
  function subdirsOf(root, base) {
8562
8693
  let entries;
8563
8694
  try {
8564
- entries = readdirSync3(base ? join11(root, base) : root, { withFileTypes: true });
8695
+ entries = readdirSync3(base ? join13(root, base) : root, { withFileTypes: true });
8565
8696
  } catch {
8566
8697
  return [];
8567
8698
  }
@@ -8623,14 +8754,14 @@ function npmFamilyPatterns(root, warnings) {
8623
8754
  if (t.startsWith("!")) negations.push(t.slice(1));
8624
8755
  else positives.push({ pattern: t, kind });
8625
8756
  };
8626
- const pkg = readJson(join11(root, "package.json"), "package.json", warnings);
8757
+ const pkg = readJson(join13(root, "package.json"), "package.json", warnings);
8627
8758
  const ws = pkg?.workspaces;
8628
8759
  if (Array.isArray(ws)) {
8629
8760
  for (const x of ws) if (typeof x === "string") push(x, "npm");
8630
8761
  } else if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
8631
8762
  for (const x of ws.packages) if (typeof x === "string") push(x, "npm");
8632
8763
  }
8633
- const pnpm = readText(join11(root, "pnpm-workspace.yaml"));
8764
+ const pnpm = readText(join13(root, "pnpm-workspace.yaml"));
8634
8765
  let inPackages = false;
8635
8766
  for (const line of pnpm.split(/\r?\n/)) {
8636
8767
  if (/^\S/.test(line)) {
@@ -8644,11 +8775,11 @@ function npmFamilyPatterns(root, warnings) {
8644
8775
  return { positives, negations };
8645
8776
  }
8646
8777
  function fallbackNpmPatterns(root, warnings) {
8647
- const lerna = readJson(join11(root, "lerna.json"), "lerna.json", warnings);
8778
+ const lerna = readJson(join13(root, "lerna.json"), "lerna.json", warnings);
8648
8779
  if (lerna && Array.isArray(lerna.packages)) {
8649
8780
  return lerna.packages.filter((x) => typeof x === "string").map((pattern) => ({ pattern, kind: "lerna" }));
8650
8781
  }
8651
- const nx = readJson(join11(root, "nx.json"), "nx.json", warnings);
8782
+ const nx = readJson(join13(root, "nx.json"), "nx.json", warnings);
8652
8783
  if (nx) {
8653
8784
  const layout = nx.workspaceLayout ?? {};
8654
8785
  const appsDir = typeof layout.appsDir === "string" ? layout.appsDir : "apps";
@@ -8658,7 +8789,7 @@ function fallbackNpmPatterns(root, warnings) {
8658
8789
  return [];
8659
8790
  }
8660
8791
  function detectCargoMembers(root, found, warnings) {
8661
- const toml = readText(join11(root, "Cargo.toml"));
8792
+ const toml = readText(join13(root, "Cargo.toml"));
8662
8793
  if (!toml) return;
8663
8794
  const body2 = tomlSectionBody(toml, "workspace");
8664
8795
  if (!body2) return;
@@ -8673,7 +8804,7 @@ function detectCargoMembers(root, found, warnings) {
8673
8804
  }
8674
8805
  }
8675
8806
  function detectGoWork(root, found, warnings) {
8676
- const gowork = readText(join11(root, "go.work"));
8807
+ const gowork = readText(join13(root, "go.work"));
8677
8808
  if (!gowork) return;
8678
8809
  const dirs = [];
8679
8810
  for (const block of gowork.matchAll(/^use\s*\(([\s\S]*?)\)/gm)) {
@@ -8689,7 +8820,7 @@ function detectGoWork(root, found, warnings) {
8689
8820
  }
8690
8821
  }
8691
8822
  function detectMavenModules(root, found, warnings) {
8692
- const pom = readText(join11(root, "pom.xml"));
8823
+ const pom = readText(join13(root, "pom.xml"));
8693
8824
  if (!pom) return;
8694
8825
  const modules = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1];
8695
8826
  if (!modules) return;
@@ -8698,7 +8829,7 @@ function detectMavenModules(root, found, warnings) {
8698
8829
  }
8699
8830
  }
8700
8831
  function detectUvMembers(root, found, warnings) {
8701
- const toml = readText(join11(root, "pyproject.toml"));
8832
+ const toml = readText(join13(root, "pyproject.toml"));
8702
8833
  if (!toml) return;
8703
8834
  const body2 = tomlSectionBody(toml, "tool.uv.workspace");
8704
8835
  if (!body2) return;
@@ -8713,7 +8844,7 @@ function detectUvMembers(root, found, warnings) {
8713
8844
  }
8714
8845
  }
8715
8846
  function detectComposerPathRepos(root, found, warnings) {
8716
- const composer = readJson(join11(root, "composer.json"), "composer.json", warnings);
8847
+ const composer = readJson(join13(root, "composer.json"), "composer.json", warnings);
8717
8848
  const repos = composer?.repositories;
8718
8849
  if (!Array.isArray(repos)) return;
8719
8850
  for (const r of repos) {
@@ -8724,7 +8855,7 @@ function detectComposerPathRepos(root, found, warnings) {
8724
8855
  }
8725
8856
  function detectGradleIncludes(root, found, warnings) {
8726
8857
  for (const f of ["settings.gradle", "settings.gradle.kts"]) {
8727
- const text = readText(join11(root, f));
8858
+ const text = readText(join13(root, f));
8728
8859
  if (!text) continue;
8729
8860
  for (const line of text.split(/\r?\n/)) {
8730
8861
  if (!/^\s*include[\s(]/.test(line)) continue;
@@ -8736,7 +8867,7 @@ function detectGradleIncludes(root, found, warnings) {
8736
8867
  }
8737
8868
  }
8738
8869
  function npmEdges(root, pkg, byName, warnings) {
8739
- const manifest = readJson(join11(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8870
+ const manifest = readJson(join13(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
8740
8871
  if (!manifest) return [];
8741
8872
  const edges = /* @__PURE__ */ new Set();
8742
8873
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
@@ -8759,7 +8890,7 @@ function normalizeDepPath(fromDir, rel) {
8759
8890
  return out2.join("/");
8760
8891
  }
8761
8892
  function cargoEdges(root, pkg, byName, byDir) {
8762
- const toml = readText(join11(root, pkg.dir, "Cargo.toml"));
8893
+ const toml = readText(join13(root, pkg.dir, "Cargo.toml"));
8763
8894
  if (!toml) return [];
8764
8895
  const edges = /* @__PURE__ */ new Set();
8765
8896
  for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
@@ -8783,7 +8914,7 @@ function cargoEdges(root, pkg, byName, byDir) {
8783
8914
  return [...edges];
8784
8915
  }
8785
8916
  function goPkgEdges(root, pkg, byName, byDir) {
8786
- const gomod = readText(join11(root, pkg.dir, "go.mod"));
8917
+ const gomod = readText(join13(root, pkg.dir, "go.mod"));
8787
8918
  if (!gomod) return [];
8788
8919
  const edges = /* @__PURE__ */ new Set();
8789
8920
  for (const m of gomod.matchAll(/^\s*(?:require\s+)?([^\s/(][^\s]*)\s+v[^\s]+/gm)) {
@@ -8797,7 +8928,7 @@ function goPkgEdges(root, pkg, byName, byDir) {
8797
8928
  return [...edges];
8798
8929
  }
8799
8930
  function mavenEdges(root, pkg, byName) {
8800
- const pom = readText(join11(root, pkg.dir, "pom.xml"));
8931
+ const pom = readText(join13(root, pkg.dir, "pom.xml"));
8801
8932
  if (!pom) return [];
8802
8933
  const edges = /* @__PURE__ */ new Set();
8803
8934
  for (const m of pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
@@ -8807,7 +8938,7 @@ function mavenEdges(root, pkg, byName) {
8807
8938
  return [...edges];
8808
8939
  }
8809
8940
  function uvEdges(root, pkg, byName) {
8810
- const toml = readText(join11(root, pkg.dir, "pyproject.toml"));
8941
+ const toml = readText(join13(root, pkg.dir, "pyproject.toml"));
8811
8942
  if (!toml) return [];
8812
8943
  const edges = /* @__PURE__ */ new Set();
8813
8944
  const project = tomlSectionBody(toml, "project");
@@ -8827,7 +8958,7 @@ function uvEdges(root, pkg, byName) {
8827
8958
  return [...edges];
8828
8959
  }
8829
8960
  function composerEdges(root, pkg, byName, warnings) {
8830
- const manifest = readJson(join11(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8961
+ const manifest = readJson(join13(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
8831
8962
  if (!manifest) return [];
8832
8963
  const edges = /* @__PURE__ */ new Set();
8833
8964
  for (const field of ["require", "require-dev"]) {
@@ -8841,7 +8972,7 @@ function composerEdges(root, pkg, byName, warnings) {
8841
8972
  }
8842
8973
  function gradleEdges(root, pkg, byName, byDir) {
8843
8974
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8844
- const text = readText(join11(root, pkg.dir, f));
8975
+ const text = readText(join13(root, pkg.dir, f));
8845
8976
  if (!text) continue;
8846
8977
  const edges = /* @__PURE__ */ new Set();
8847
8978
  for (const m of text.matchAll(/project\s*\(\s*["']:?([^"']+)["']\s*\)/g)) {
@@ -9565,16 +9696,16 @@ var init_grep = __esm({
9565
9696
 
9566
9697
  // src/embed/model.ts
9567
9698
  import { createHash as createHash3 } from "crypto";
9568
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
9569
- import { join as join13 } from "path";
9699
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
9700
+ import { join as join15 } from "path";
9570
9701
  function resolveEmbedModelDir(repo) {
9571
9702
  const env = process.env.CODEINDEX_EMBED_DIR;
9572
9703
  const candidates = [];
9573
9704
  if (env) candidates.push(env);
9574
- if (repo) candidates.push(join13(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9575
- candidates.push(join13(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9705
+ if (repo) candidates.push(join15(repo, ".codeindex", DEFAULT_EMBED_DIRNAME));
9706
+ candidates.push(join15(process.cwd(), ".codeindex", DEFAULT_EMBED_DIRNAME));
9576
9707
  for (const c2 of candidates) {
9577
- if (existsSync4(join13(c2, "model.json"))) return c2;
9708
+ if (existsSync5(join15(c2, "model.json"))) return c2;
9578
9709
  }
9579
9710
  return void 0;
9580
9711
  }
@@ -9607,9 +9738,9 @@ function parseEmbedModel(raw, source) {
9607
9738
  }
9608
9739
  function loadEmbedModel(dir) {
9609
9740
  if (!dir) return void 0;
9610
- const path = join13(dir, "model.json");
9611
- if (!existsSync4(path)) return void 0;
9612
- const raw = JSON.parse(readFileSync6(path, "utf8"));
9741
+ const path = join15(dir, "model.json");
9742
+ if (!existsSync5(path)) return void 0;
9743
+ const raw = JSON.parse(readFileSync7(path, "utf8"));
9613
9744
  return parseEmbedModel(raw, path);
9614
9745
  }
9615
9746
  function resolveEmbedPullUrl() {
@@ -10153,7 +10284,7 @@ function changeCoupling(dir, opts = {}) {
10153
10284
  for (const f of unique) totals.set(f, (totals.get(f) ?? 0) + 1);
10154
10285
  for (let i2 = 0; i2 < unique.length; i2++) {
10155
10286
  for (let j = i2 + 1; j < unique.length; j++) {
10156
- const key = `${unique[i2]}\0${unique[j]}`;
10287
+ const key = `${unique[i2]}${SEP2}${unique[j]}`;
10157
10288
  pairs.set(key, (pairs.get(key) ?? 0) + 1);
10158
10289
  }
10159
10290
  }
@@ -10161,7 +10292,7 @@ function changeCoupling(dir, opts = {}) {
10161
10292
  const out2 = [];
10162
10293
  for (const [key, together] of pairs) {
10163
10294
  if (together < minTogether) continue;
10164
- const [a, b] = key.split("\0");
10295
+ const [a, b] = key.split(SEP2);
10165
10296
  const totalA = totals.get(a) ?? together;
10166
10297
  const totalB = totals.get(b) ?? together;
10167
10298
  out2.push({ a, b, together, totalA, totalB, strength: Number((together / Math.min(totalA, totalB)).toFixed(3)) });
@@ -10177,11 +10308,13 @@ function rankHotspots(scan2, churn, top = 20) {
10177
10308
  out2.sort((a, b) => b.score - a.score || b.lines - a.lines || byStr(a.rel, b.rel));
10178
10309
  return out2.slice(0, top);
10179
10310
  }
10311
+ var SEP2;
10180
10312
  var init_coupling = __esm({
10181
10313
  "src/coupling.ts"() {
10182
10314
  "use strict";
10183
10315
  init_util();
10184
10316
  init_sort();
10317
+ SEP2 = "\0";
10185
10318
  }
10186
10319
  });
10187
10320
 
@@ -10282,29 +10415,78 @@ function renderMermaid(graph, opts = {}) {
10282
10415
  if (dropped) lines.push(` %% ${dropped} lighter edges omitted (maxEdges=${maxEdges})`);
10283
10416
  return lines.join("\n") + "\n";
10284
10417
  }
10285
- var sanitizeId;
10418
+ function clusterNodeId(slug) {
10419
+ return "m_" + slug.replace(/[^A-Za-z0-9_]/g, "_");
10420
+ }
10421
+ function renderMermaidClustered(graph, opts = {}) {
10422
+ const maxModules = opts.maxModules ?? CLUSTER_MAX_MODULES;
10423
+ const maxEdges = opts.maxEdges ?? CLUSTER_MAX_EDGES;
10424
+ const ranked = graph.modules.slice().sort((a, b) => degreeOf(b) - degreeOf(a) || byStr(a.slug, b.slug));
10425
+ const shown = ranked.slice(0, maxModules);
10426
+ const shownSet = new Set(shown.map((m) => m.slug));
10427
+ const edges = graph.moduleEdges.filter((e) => shownSet.has(e.from) && shownSet.has(e.to)).sort((a, b) => b.weight - a.weight || byStr(a.from, b.from) || byStr(a.to, b.to)).slice(0, maxEdges);
10428
+ const lines = [];
10429
+ lines.push(
10430
+ `%% ${opts.title ?? "module graph"} \u2014 ${shown.length} of ${graph.modules.length} modules, ${edges.length} of ${graph.moduleEdges.length} edges`
10431
+ );
10432
+ if (shown.length < graph.modules.length || edges.length < graph.moduleEdges.length) {
10433
+ lines.push("%% truncated to the most-connected modules/edges; see graph.json for the full graph");
10434
+ }
10435
+ lines.push("flowchart LR");
10436
+ for (const tier of [0, 1, 2]) {
10437
+ const inTier = shown.filter((m) => m.tier === tier);
10438
+ if (!inTier.length) continue;
10439
+ lines.push(` subgraph ${TIER_LABEL[tier]}`);
10440
+ for (const m of inTier) lines.push(` ${clusterNodeId(m.slug)}["${m.path.replace(/"/g, "'")}"]`);
10441
+ lines.push(" end");
10442
+ }
10443
+ for (const e of edges) {
10444
+ const label = e.weight > 1 ? `|${e.weight}| ` : "";
10445
+ lines.push(` ${clusterNodeId(e.from)} -->${label ? " " + label : " "}${clusterNodeId(e.to)}`);
10446
+ }
10447
+ return {
10448
+ content: "```mermaid\n" + lines.join("\n") + "\n```\n",
10449
+ shownModules: shown.length,
10450
+ totalModules: graph.modules.length,
10451
+ shownEdges: edges.length,
10452
+ totalEdges: graph.moduleEdges.length
10453
+ };
10454
+ }
10455
+ var sanitizeId, TIER_LABEL, CLUSTER_MAX_MODULES, CLUSTER_MAX_EDGES, degreeOf;
10286
10456
  var init_viz = __esm({
10287
10457
  "src/viz.ts"() {
10288
10458
  "use strict";
10289
10459
  init_sort();
10290
10460
  sanitizeId = (slug) => slug.replace(/[^\w]/g, "_");
10461
+ TIER_LABEL = { 0: "Foundations", 1: "Features", 2: "Tail" };
10462
+ CLUSTER_MAX_MODULES = 40;
10463
+ CLUSTER_MAX_EDGES = 80;
10464
+ degreeOf = (m) => m.degIn + m.degOut;
10291
10465
  }
10292
10466
  });
10293
10467
 
10294
10468
  // src/mcp.ts
10295
10469
  var mcp_exports = {};
10296
10470
  __export(mcp_exports, {
10471
+ DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
10472
+ capResponse: () => capResponse,
10297
10473
  getArtifacts: () => getArtifacts,
10298
10474
  getScan: () => getScan,
10475
+ getScanSummary: () => getScanSummary,
10299
10476
  memoizedEmbedModel: () => memoizedEmbedModel,
10300
10477
  memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
10478
+ negotiateProtocol: () => negotiateProtocol,
10479
+ resourceLinkFor: () => resourceLinkFor,
10301
10480
  runMcpServer: () => runMcpServer,
10302
10481
  scanFingerprint: () => scanFingerprint,
10303
10482
  toCacheMap: () => toCacheMap,
10304
- warmGrammarsForRepo: () => warmGrammarsForRepo
10483
+ validateArgs: () => validateArgs,
10484
+ warmGrammarsForRepo: () => warmGrammarsForRepo,
10485
+ warmGrammarsForWalk: () => warmGrammarsForWalk
10305
10486
  });
10306
- import { readFileSync as readFileSync7, statSync as statSync4 } from "fs";
10307
- import { join as join14 } from "path";
10487
+ import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
10488
+ import { isAbsolute, join as join16 } from "path";
10489
+ import { pathToFileURL as pathToFileURL2 } from "url";
10308
10490
  import { createInterface } from "readline";
10309
10491
  function str(v) {
10310
10492
  return typeof v === "string" && v ? v : void 0;
@@ -10312,6 +10494,10 @@ function str(v) {
10312
10494
  function strArray(v) {
10313
10495
  return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
10314
10496
  }
10497
+ function num(v) {
10498
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
10499
+ return Number.isFinite(n) && n > 0 ? n : void 0;
10500
+ }
10315
10501
  function errMessage(e) {
10316
10502
  return e instanceof Error ? e.message : String(e);
10317
10503
  }
@@ -10328,7 +10514,7 @@ async function memoizedEmbeddingIndex(key, build) {
10328
10514
  function memoizedEmbedModel(modelDir) {
10329
10515
  let stat;
10330
10516
  try {
10331
- stat = statSync4(join14(modelDir, "model.json"));
10517
+ stat = statSync5(join16(modelDir, "model.json"));
10332
10518
  } catch {
10333
10519
  return void 0;
10334
10520
  }
@@ -10338,6 +10524,23 @@ function memoizedEmbedModel(modelDir) {
10338
10524
  if (model) embedModelCache = { key, model };
10339
10525
  return model;
10340
10526
  }
10527
+ function sessionGet(key) {
10528
+ const i2 = sessionCaches.findIndex((e) => e.key === key);
10529
+ if (i2 < 0) return void 0;
10530
+ const [entry] = sessionCaches.splice(i2, 1);
10531
+ sessionCaches.unshift(entry);
10532
+ return entry;
10533
+ }
10534
+ function sessionPut(entry) {
10535
+ const i2 = sessionCaches.findIndex((e) => e.key === entry.key);
10536
+ if (i2 >= 0) sessionCaches.splice(i2, 1);
10537
+ sessionCaches.unshift(entry);
10538
+ sessionCaches.length = Math.min(sessionCaches.length, SESSION_CACHE_MAX);
10539
+ return entry;
10540
+ }
10541
+ function sessionClear() {
10542
+ sessionCaches.length = 0;
10543
+ }
10341
10544
  function sessionKey(repo, opts) {
10342
10545
  return repo + "\0" + JSON.stringify({
10343
10546
  scope: opts.scope,
@@ -10352,112 +10555,76 @@ function sessionKey(repo, opts) {
10352
10555
  fullHash: opts.fullHash
10353
10556
  });
10354
10557
  }
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(readFileSync7(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 = readFileSync7(join14(dir, "graph.json"));
10388
- symbolsBytes = readFileSync7(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 = {}) {
10558
+ function getScan(repo, opts = {}, walked) {
10412
10559
  const key = sessionKey(repo, opts);
10413
- if (sessionCache && sessionCache.key === key) {
10414
- const fresh = scanRepo(repo, { ...opts, cache: sessionCache.cacheMap });
10560
+ const hit = sessionGet(key);
10561
+ if (hit) {
10562
+ const fresh = scanRepo(repo, { ...opts, cache: hit.cacheMap, precomputedWalk: walked });
10415
10563
  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;
10564
+ if (fresh.cacheDirty) hit.cacheMap = toCacheMap(fresh);
10565
+ if (hit.scan.commit !== fresh.commit) hit.scan.commit = fresh.commit;
10566
+ return hit.scan;
10419
10567
  }
10420
- sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
10568
+ sessionPut({ key, scan: fresh, cacheMap: toCacheMap(fresh) });
10421
10569
  return fresh;
10422
10570
  }
10423
- const preloaded = preloadSession(repo, opts);
10571
+ const preloaded = preloadSession(repo, { ...opts, precomputedWalk: walked });
10424
10572
  if (preloaded) {
10425
- sessionCache = { key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts };
10573
+ sessionPut({ key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts });
10426
10574
  return preloaded.scan;
10427
10575
  }
10428
- const scan2 = scanRepo(repo, opts);
10429
- sessionCache = { key, scan: scan2, cacheMap: toCacheMap(scan2) };
10576
+ const scan2 = scanRepo(repo, { ...opts, precomputedWalk: walked });
10577
+ sessionPut({ key, scan: scan2, cacheMap: toCacheMap(scan2) });
10430
10578
  return scan2;
10431
10579
  }
10432
- function getArtifacts(repo, opts = {}) {
10433
- const scan2 = getScan(repo, opts);
10434
- if (sessionCache && sessionCache.scan === scan2) {
10435
- return sessionCache.arts ??= buildArtifactsFromScan(scan2, opts);
10580
+ function getScanSummary(repo, opts = {}, walked) {
10581
+ if (sessionCaches.some((e) => e.key === sessionKey(repo, opts))) {
10582
+ const scan2 = getScan(repo, opts, walked);
10583
+ return {
10584
+ root: scan2.root,
10585
+ commit: scan2.commit,
10586
+ fileCount: scan2.files.length,
10587
+ languages: scan2.languages,
10588
+ capped: scan2.capped,
10589
+ excluded: scan2.excluded
10590
+ };
10436
10591
  }
10592
+ return scanSummary(repo, { ...opts, precomputedWalk: walked });
10593
+ }
10594
+ function getArtifacts(repo, opts = {}, walked) {
10595
+ const scan2 = getScan(repo, opts, walked);
10596
+ const entry = sessionCaches.find((e) => e.scan === scan2);
10597
+ if (entry) return entry.arts ??= buildArtifactsFromScan(scan2, opts);
10437
10598
  return buildArtifactsFromScan(scan2, opts);
10438
10599
  }
10439
10600
  async function warmGrammarsForRepo(repo) {
10440
- const { files } = walk(repo, {});
10441
- await ensureGrammars(grammarKeysForExts(files.map((f) => f.ext)));
10601
+ await warmGrammarsForWalk(walk(repo, {}));
10602
+ }
10603
+ async function warmGrammarsForWalk(walked) {
10604
+ await ensureGrammars(grammarKeysForExts(walked.files.map((f) => f.ext)));
10442
10605
  }
10443
10606
  async function callTool(name2, args2, defaultRepo) {
10444
10607
  const repo = str(args2.repo) ?? defaultRepo;
10445
10608
  if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10446
10609
  const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
10447
- if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
10610
+ let walked;
10611
+ if (!SCANLESS_TOOLS.has(name2)) {
10612
+ walked = walk(repo, {});
10613
+ await warmGrammarsForWalk(walked);
10614
+ }
10448
10615
  if (name2 === "scan_summary") {
10449
- const scan2 = getScan(repo, scanOpts);
10616
+ const s = getScanSummary(repo, scanOpts, walked);
10450
10617
  return JSON.stringify(
10451
- { engineVersion: ENGINE_VERSION, commit: scan2.commit, fileCount: scan2.files.length, languages: scan2.languages, capped: scan2.capped },
10618
+ { engineVersion: ENGINE_VERSION, commit: s.commit, fileCount: s.fileCount, languages: s.languages, capped: s.capped },
10452
10619
  null,
10453
10620
  2
10454
10621
  );
10455
10622
  }
10456
10623
  if (name2 === "graph") {
10457
- return renderGraphJson(getArtifacts(repo, scanOpts).graph);
10624
+ return renderGraphJson(getArtifacts(repo, scanOpts, walked).graph);
10458
10625
  }
10459
10626
  if (name2 === "symbols") {
10460
- const { symbols } = getArtifacts(repo, scanOpts);
10627
+ const { symbols } = getArtifacts(repo, scanOpts, walked);
10461
10628
  const lookup = str(args2.name);
10462
10629
  if (lookup) {
10463
10630
  return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
@@ -10465,7 +10632,8 @@ async function callTool(name2, args2, defaultRepo) {
10465
10632
  return JSON.stringify(symbols, null, 2);
10466
10633
  }
10467
10634
  if (name2 === "callers") {
10468
- const index = buildCallerIndex(getScan(repo, scanOpts));
10635
+ const scan2 = getScan(repo, scanOpts, walked);
10636
+ const index = args2.recall === true ? buildCallerIndex(scan2, void 0, { recall: true }) : callerIndexFor(scan2);
10469
10637
  const lookup = str(args2.name);
10470
10638
  if (lookup) {
10471
10639
  const entry = index.get(lookup);
@@ -10488,30 +10656,31 @@ async function callTool(name2, args2, defaultRepo) {
10488
10656
  if (name2 === "symbols_overview") {
10489
10657
  const file = str(args2.file);
10490
10658
  if (!file) throw new Error("`file` is required");
10491
- return JSON.stringify(symbolsOverview(getScan(repo, scanOpts), file), null, 2);
10659
+ return JSON.stringify(symbolsOverview(getScan(repo, scanOpts, walked), file), null, 2);
10492
10660
  }
10493
10661
  if (name2 === "find_symbol") {
10494
10662
  const namePath = str(args2.namePath);
10495
10663
  if (!namePath) throw new Error("`namePath` is required");
10496
- const matches = findSymbol(getScan(repo, scanOpts), namePath, {
10664
+ const matches = findSymbol(getScan(repo, scanOpts, walked), namePath, {
10497
10665
  substring: args2.substring === true,
10498
- includeBody: args2.includeBody === true
10666
+ includeBody: args2.includeBody === true,
10667
+ maxResults: num(args2.maxResults)
10499
10668
  });
10500
10669
  return JSON.stringify(matches, null, 2);
10501
10670
  }
10502
10671
  if (name2 === "find_references") {
10503
10672
  const symName = str(args2.name);
10504
10673
  if (!symName) throw new Error("`name` is required");
10505
- return JSON.stringify(findReferences(getScan(repo, scanOpts), symName), null, 2);
10674
+ return JSON.stringify(findReferences(getScan(repo, scanOpts, walked), symName), null, 2);
10506
10675
  }
10507
10676
  if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
10508
10677
  const namePath = str(args2.namePath);
10509
10678
  const body2 = typeof args2.body === "string" ? args2.body : void 0;
10510
10679
  if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
10511
- const scan2 = getScan(repo, scanOpts);
10680
+ const scan2 = getScan(repo, scanOpts, walked);
10512
10681
  const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
10513
10682
  const result = fn(scan2, namePath, body2, str(args2.file));
10514
- sessionCache = void 0;
10683
+ sessionClear();
10515
10684
  return JSON.stringify(result, null, 2);
10516
10685
  }
10517
10686
  if (name2 === "write_memory") {
@@ -10536,26 +10705,29 @@ async function callTool(name2, args2, defaultRepo) {
10536
10705
  return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
10537
10706
  }
10538
10707
  if (name2 === "dead_code") {
10539
- return JSON.stringify(findDeadCode(getScan(repo, scanOpts)), null, 2);
10708
+ const all = findDeadCode(getScan(repo, scanOpts, walked));
10709
+ const limit = num(args2.limit);
10710
+ if (limit === void 0 || all.length <= limit) return JSON.stringify(all, null, 2);
10711
+ return JSON.stringify({ total: all.length, shown: limit, truncated: true, candidates: all.slice(0, limit) }, null, 2);
10540
10712
  }
10541
10713
  if (name2 === "complexity") {
10542
- const scan2 = getScan(repo, scanOpts);
10714
+ const scan2 = getScan(repo, scanOpts, walked);
10543
10715
  if (args2.risk === true) {
10544
- const { churn, ok } = gitChurn(repo);
10545
- return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2);
10716
+ const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10717
+ return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn, num(args2.top)) }, null, 2);
10546
10718
  }
10547
- return JSON.stringify(symbolComplexity(scan2, str(args2.file)), null, 2);
10719
+ return JSON.stringify(symbolComplexity(scan2, str(args2.file), num(args2.top)), null, 2);
10548
10720
  }
10549
10721
  if (name2 === "mermaid") {
10550
- const { graph } = getArtifacts(repo, scanOpts);
10551
- return renderMermaid(graph, { module: str(args2.module) });
10722
+ const { graph } = getArtifacts(repo, scanOpts, walked);
10723
+ return renderMermaid(graph, { module: str(args2.module), maxEdges: num(args2.maxEdges) });
10552
10724
  }
10553
10725
  if (name2 === "repo_map") {
10554
- const { scan: scan2, graph } = getArtifacts(repo, scanOpts);
10726
+ const { scan: scan2, graph } = getArtifacts(repo, scanOpts, walked);
10555
10727
  return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
10556
10728
  }
10557
10729
  if (name2 === "hotspots") {
10558
- const scan2 = getScan(repo, scanOpts);
10730
+ const scan2 = getScan(repo, scanOpts, walked);
10559
10731
  const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10560
10732
  return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
10561
10733
  }
@@ -10566,8 +10738,10 @@ async function callTool(name2, args2, defaultRepo) {
10566
10738
  if (name2 === "grep") {
10567
10739
  const pattern = str(args2.pattern);
10568
10740
  if (!pattern) throw new Error("`pattern` is required");
10741
+ const scope = str(args2.scope);
10742
+ const globs = strArray(args2.globs);
10569
10743
  const hits = grepRepo(repo, pattern, {
10570
- globs: strArray(args2.globs),
10744
+ globs: scope ? [...globs ?? [], `${scope.replace(/\/+$/, "")}/**`] : globs,
10571
10745
  ignoreCase: args2.ignoreCase === true,
10572
10746
  maxHits: typeof args2.maxHits === "number" ? args2.maxHits : void 0
10573
10747
  });
@@ -10576,7 +10750,7 @@ async function callTool(name2, args2, defaultRepo) {
10576
10750
  if (name2 === "search") {
10577
10751
  const query = str(args2.query);
10578
10752
  if (!query) throw new Error("`query` is required");
10579
- const scan2 = getScan(repo, scanOpts);
10753
+ const scan2 = getScan(repo, scanOpts, walked);
10580
10754
  const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10581
10755
  const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10582
10756
  if (args2.semantic === true) {
@@ -10630,32 +10804,122 @@ async function callTool(name2, args2, defaultRepo) {
10630
10804
  return JSON.stringify(status, null, 2);
10631
10805
  }
10632
10806
  if (name2 === "check_rules") {
10633
- const rules = parseRules(args2.rules);
10634
- const { graph } = getArtifacts(repo, scanOpts);
10807
+ const configPath = str(args2.configPath);
10808
+ let payload = args2.rules;
10809
+ if (payload === void 0 && configPath) {
10810
+ const abs = isAbsolute(configPath) ? configPath : join16(repo, configPath);
10811
+ try {
10812
+ payload = JSON.parse(readFileSync8(abs, "utf8"));
10813
+ } catch (e) {
10814
+ throw new Error(`cannot read rules from ${abs}: ${errMessage(e)}`);
10815
+ }
10816
+ }
10817
+ if (payload === void 0) throw new Error("`rules` (or `configPath`) is required");
10818
+ const rules = parseRules(payload);
10819
+ const { graph } = getArtifacts(repo, scanOpts, walked);
10635
10820
  return JSON.stringify(checkRules(graph, rules), null, 2);
10636
10821
  }
10637
10822
  throw new Error(`unknown tool: ${name2}`);
10638
10823
  }
10639
- function toolsFor(defaultRepo) {
10640
- if (!defaultRepo) return TOOLS;
10824
+ function validateArgs(schema, args2) {
10825
+ const props = schema.properties ?? {};
10826
+ for (const [key, value] of Object.entries(args2)) {
10827
+ if (value === void 0 || value === null) continue;
10828
+ const spec = props[key];
10829
+ if (!spec?.type) continue;
10830
+ const actual = Array.isArray(value) ? "array" : typeof value;
10831
+ if (spec.type === "number") {
10832
+ if (actual === "number") continue;
10833
+ if (actual === "string" && Number.isFinite(Number(value)) && value.trim() !== "") continue;
10834
+ return `\`${key}\` must be a number, got ${actual === "string" ? JSON.stringify(value) : actual}`;
10835
+ }
10836
+ if (spec.type === "array") {
10837
+ if (actual !== "array") return `\`${key}\` must be an array of strings, got ${actual}`;
10838
+ if (spec.items?.type === "string" && !value.every((x) => typeof x === "string")) {
10839
+ return `\`${key}\` must be an array of strings`;
10840
+ }
10841
+ continue;
10842
+ }
10843
+ if (actual !== spec.type) return `\`${key}\` must be a ${spec.type}, got ${actual}`;
10844
+ }
10845
+ return void 0;
10846
+ }
10847
+ function negotiateProtocol(requested) {
10848
+ return typeof requested === "string" && PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL;
10849
+ }
10850
+ function annotationsFor(name2) {
10851
+ const meta = TOOL_META[name2];
10852
+ if (!meta) return void 0;
10853
+ return {
10854
+ readOnlyHint: !meta.write,
10855
+ ...meta.write ? { destructiveHint: meta.destructive === true, idempotentHint: meta.idempotent === true } : {},
10856
+ openWorldHint: meta.openWorld === true
10857
+ };
10858
+ }
10859
+ function toolsFor(defaultRepo, protocolVersion = PROTOCOL_VERSIONS[0]) {
10860
+ const withAnnotations = protocolVersion >= ANNOTATIONS_SINCE;
10861
+ const withTitle = protocolVersion >= RICH_TOOLS_SINCE;
10862
+ if (!defaultRepo && !withAnnotations && !withTitle) return TOOLS;
10641
10863
  return TOOLS.map((t) => ({
10642
10864
  ...t,
10643
- inputSchema: {
10865
+ ...withTitle && TOOL_META[t.name] ? { title: TOOL_META[t.name].title } : {},
10866
+ ...withAnnotations ? { annotations: annotationsFor(t.name) } : {},
10867
+ inputSchema: !defaultRepo ? t.inputSchema : {
10644
10868
  ...t.inputSchema,
10645
10869
  properties: {
10646
10870
  ...t.inputSchema.properties,
10647
- repo: { type: "string", description: `Absolute path to the repository root (optional \u2014 defaults to ${defaultRepo})` }
10871
+ repo: {
10872
+ type: "string",
10873
+ description: `Absolute path to the repository root (optional \u2014 defaults to ${defaultRepo})`
10874
+ }
10648
10875
  },
10649
10876
  required: t.inputSchema.required.filter((r) => r !== "repo")
10650
10877
  }
10651
10878
  }));
10652
10879
  }
10880
+ function capResponse(text, tool, repo, maxBytes) {
10881
+ const bytes = Buffer.byteLength(text, "utf8");
10882
+ if (bytes <= maxBytes) return text;
10883
+ const artifact = ARTIFACT_FOR[tool] ? join16(repo, INDEX_DIR, ARTIFACT_FOR[tool]) : void 0;
10884
+ return JSON.stringify(
10885
+ {
10886
+ truncated: true,
10887
+ tool,
10888
+ bytes,
10889
+ maxBytes,
10890
+ reason: "This response exceeds the configured limit and was withheld rather than sent as an unusable partial payload.",
10891
+ narrower: NARROWER[tool] ?? "narrow the request with `scope`, `include`/`exclude`, or a `limit`",
10892
+ ...artifact && existsSync6(artifact) ? { artifact, artifactNote: "The full result is already on disk here \u2014 read it directly if you need all of it." } : artifact ? { artifactNote: `Run \`codeindex index --repo ${repo} --out ${join16(repo, INDEX_DIR)}\` to get this as a file.` } : {}
10893
+ },
10894
+ null,
10895
+ 2
10896
+ ) + "\n";
10897
+ }
10898
+ function resourceLinkFor(text, tool) {
10899
+ const artifactName = ARTIFACT_FOR[tool];
10900
+ if (!artifactName) return void 0;
10901
+ let parsed;
10902
+ try {
10903
+ parsed = JSON.parse(text);
10904
+ } catch {
10905
+ return void 0;
10906
+ }
10907
+ if (parsed.truncated !== true || typeof parsed.artifact !== "string") return void 0;
10908
+ return {
10909
+ type: "resource_link",
10910
+ uri: pathToFileURL2(parsed.artifact).href,
10911
+ name: artifactName,
10912
+ description: `The full ${tool} result this call was too large to inline.`,
10913
+ mimeType: "application/json"
10914
+ };
10915
+ }
10653
10916
  async function runMcpServer(opts = {}) {
10654
10917
  const serverInfo = {
10655
10918
  name: opts.serverInfo?.name ?? "codeindex",
10656
10919
  version: opts.serverInfo?.version ?? ENGINE_VERSION
10657
10920
  };
10658
- const tools = toolsFor(opts.defaultRepo);
10921
+ let protocolVersion = PROTOCOL_VERSIONS[0];
10922
+ let tools = toolsFor(opts.defaultRepo, protocolVersion);
10659
10923
  const send = (msg) => {
10660
10924
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
10661
10925
  };
@@ -10677,10 +10941,12 @@ async function runMcpServer(opts = {}) {
10677
10941
  if (req.id === void 0 || req.id === null) return;
10678
10942
  try {
10679
10943
  if (req.method === "initialize") {
10944
+ protocolVersion = negotiateProtocol(req.params?.protocolVersion);
10945
+ tools = toolsFor(opts.defaultRepo, protocolVersion);
10680
10946
  send({
10681
10947
  id: req.id,
10682
10948
  result: {
10683
- protocolVersion: "2024-11-05",
10949
+ protocolVersion,
10684
10950
  capabilities: { tools: {} },
10685
10951
  serverInfo
10686
10952
  }
@@ -10694,8 +10960,19 @@ async function runMcpServer(opts = {}) {
10694
10960
  const name2 = str(params.name) ?? "";
10695
10961
  const args2 = params.arguments ?? {};
10696
10962
  try {
10697
- const text = await callTool(name2, args2, opts.defaultRepo);
10698
- send({ id: req.id, result: { content: [{ type: "text", text }] } });
10963
+ const decl = tools.find(
10964
+ (t) => t.name === name2
10965
+ );
10966
+ const invalid = decl ? validateArgs(decl.inputSchema, args2) : void 0;
10967
+ if (invalid) throw new Error(invalid);
10968
+ const raw = await callTool(name2, args2, opts.defaultRepo);
10969
+ const repo = str(args2.repo) ?? opts.defaultRepo ?? "";
10970
+ const text = capResponse(raw, name2, repo, opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
10971
+ const link = text !== raw && protocolVersion >= RICH_TOOLS_SINCE ? resourceLinkFor(text, name2) : void 0;
10972
+ send({
10973
+ id: req.id,
10974
+ result: { content: link ? [{ type: "text", text }, link] : [{ type: "text", text }] }
10975
+ });
10699
10976
  } catch (e) {
10700
10977
  send({
10701
10978
  id: req.id,
@@ -10710,7 +10987,7 @@ async function runMcpServer(opts = {}) {
10710
10987
  }
10711
10988
  }
10712
10989
  }
10713
- var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, sessionCache, SCANLESS_TOOLS;
10990
+ var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, SESSION_CACHE_MAX, sessionCaches, SCANLESS_TOOLS, PROTOCOL_VERSIONS, LATEST_PROTOCOL, ANNOTATIONS_SINCE, RICH_TOOLS_SINCE, TOOL_META, DEFAULT_MAX_RESPONSE_BYTES, NARROWER, ARTIFACT_FOR;
10714
10991
  var init_mcp = __esm({
10715
10992
  "src/mcp.ts"() {
10716
10993
  "use strict";
@@ -10719,8 +10996,10 @@ var init_mcp = __esm({
10719
10996
  init_pipeline();
10720
10997
  init_graph_json();
10721
10998
  init_scan();
10999
+ init_preload();
10722
11000
  init_walk();
10723
11001
  init_callers();
11002
+ init_derived();
10724
11003
  init_workspaces();
10725
11004
  init_git();
10726
11005
  init_grep();
@@ -10770,7 +11049,14 @@ var init_mcp = __esm({
10770
11049
  description: "Who calls a function? Per-symbol caller index: each defined symbol with the exact (file, line) call sites that bind to it. Omit `name` for the full index.",
10771
11050
  inputSchema: {
10772
11051
  type: "object",
10773
- properties: { ...repoProp, name: { type: "string", description: "Symbol name to look up" } },
11052
+ properties: {
11053
+ ...repoProp,
11054
+ name: { type: "string", description: "Symbol name to look up" },
11055
+ recall: {
11056
+ type: "boolean",
11057
+ description: "Recall-oriented binding: relax the JS/TS import gate to unique repo-wide names, labelling each site corroborated|unique-name (default false = precision)"
11058
+ }
11059
+ },
10774
11060
  required: ["repo"]
10775
11061
  }
10776
11062
  },
@@ -10806,7 +11092,8 @@ var init_mcp = __esm({
10806
11092
  ...repoProp,
10807
11093
  namePath: { type: "string", description: "Symbol name or Parent/child path" },
10808
11094
  substring: { type: "boolean" },
10809
- includeBody: { type: "boolean" }
11095
+ includeBody: { type: "boolean" },
11096
+ maxResults: { type: "number", description: "Cap matches (default 50)" }
10810
11097
  },
10811
11098
  required: ["repo", "namePath"]
10812
11099
  }
@@ -10913,8 +11200,16 @@ var init_mcp = __esm({
10913
11200
  },
10914
11201
  {
10915
11202
  name: "dead_code",
10916
- description: "Dead-code candidates in two labeled tiers: 'unreferenced' (no call site binds AND nothing references the name) and 'uncalled' (referenced somewhere \u2014 re-export, type position \u2014 but never called). Exported symbols only; test files and entrypoint-looking files excluded as roots.",
10917
- inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] }
11203
+ description: "Dead-code candidates in two labeled tiers: 'unreferenced' (no call site binds AND nothing references the name) and 'uncalled' (referenced somewhere \u2014 re-export, type position \u2014 but never called). Exported symbols only; test files and entrypoint-looking files excluded as roots. On a large repo this list runs to thousands of entries \u2014 pass `limit`, or `scope` to one subdirectory.",
11204
+ inputSchema: {
11205
+ type: "object",
11206
+ properties: {
11207
+ ...repoProp,
11208
+ ...scopeProps,
11209
+ limit: { type: "number", description: "Cap entries (default: all)" }
11210
+ },
11211
+ required: ["repo"]
11212
+ }
10918
11213
  },
10919
11214
  {
10920
11215
  name: "complexity",
@@ -10942,6 +11237,7 @@ var init_mcp = __esm({
10942
11237
  properties: {
10943
11238
  ...repoProp,
10944
11239
  pattern: { type: "string", description: "Regular expression to search for" },
11240
+ scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
10945
11241
  globs: { type: "array", items: { type: "string" }, description: "Restrict to matching paths" },
10946
11242
  ignoreCase: { type: "boolean" },
10947
11243
  maxHits: { type: "number" }
@@ -10984,12 +11280,18 @@ var init_mcp = __esm({
10984
11280
  properties: {
10985
11281
  ...repoProp,
10986
11282
  ...scopeProps,
10987
- rules: { type: "array", description: "Rules array (inline JSON \u2014 see description)" }
11283
+ rules: { type: "array", description: "Rules array (inline JSON \u2014 see description)" },
11284
+ configPath: {
11285
+ type: "string",
11286
+ description: "Read the rules from this JSON file instead (repo-relative or absolute) \u2014 the CLI's --config. Ignored when `rules` is given."
11287
+ }
10988
11288
  },
10989
- required: ["repo", "rules"]
11289
+ required: ["repo"]
10990
11290
  }
10991
11291
  }
10992
11292
  ];
11293
+ SESSION_CACHE_MAX = 4;
11294
+ sessionCaches = [];
10993
11295
  SCANLESS_TOOLS = /* @__PURE__ */ new Set([
10994
11296
  "workspaces",
10995
11297
  "churn",
@@ -10999,8 +11301,54 @@ var init_mcp = __esm({
10999
11301
  "read_memory",
11000
11302
  "list_memories",
11001
11303
  "delete_memory",
11002
- "embed_status"
11304
+ "embed_status",
11305
+ // scan_summary counts and classifies by path only — it never parses, so the
11306
+ // grammar warm (a whole extra walk) would be pure overhead. When a scan is
11307
+ // already cached getScanSummary reuses it, warm grammars included.
11308
+ "scan_summary"
11003
11309
  ]);
11310
+ PROTOCOL_VERSIONS = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
11311
+ LATEST_PROTOCOL = PROTOCOL_VERSIONS[PROTOCOL_VERSIONS.length - 1];
11312
+ ANNOTATIONS_SINCE = "2025-03-26";
11313
+ RICH_TOOLS_SINCE = "2025-06-18";
11314
+ TOOL_META = {
11315
+ scan_summary: { title: "Scan summary" },
11316
+ graph: { title: "Link graph" },
11317
+ symbols: { title: "Symbol index" },
11318
+ callers: { title: "Caller index" },
11319
+ workspaces: { title: "Monorepo workspaces" },
11320
+ churn: { title: "Git churn" },
11321
+ symbols_overview: { title: "File symbol overview" },
11322
+ find_symbol: { title: "Find symbol" },
11323
+ find_references: { title: "Find references" },
11324
+ repo_map: { title: "Repository map" },
11325
+ hotspots: { title: "Hotspots" },
11326
+ coupling: { title: "Change coupling" },
11327
+ replace_symbol_body: { title: "Replace symbol body", write: true, destructive: true, idempotent: true },
11328
+ insert_after_symbol: { title: "Insert after symbol", write: true, destructive: false, idempotent: false },
11329
+ insert_before_symbol: { title: "Insert before symbol", write: true, destructive: false, idempotent: false },
11330
+ write_memory: { title: "Write memory", write: true, destructive: false, idempotent: true },
11331
+ read_memory: { title: "Read memory" },
11332
+ list_memories: { title: "List memories" },
11333
+ delete_memory: { title: "Delete memory", write: true, destructive: true, idempotent: true },
11334
+ dead_code: { title: "Dead-code candidates" },
11335
+ complexity: { title: "Complexity" },
11336
+ mermaid: { title: "Mermaid module diagram" },
11337
+ grep: { title: "Grep file contents" },
11338
+ search: { title: "Lexical search", openWorld: true },
11339
+ embed_status: { title: "Embedding tier status", openWorld: true },
11340
+ check_rules: { title: "Check architecture rules" }
11341
+ };
11342
+ DEFAULT_MAX_RESPONSE_BYTES = 1e6;
11343
+ NARROWER = {
11344
+ graph: "pass `scope` to a subdirectory, or use repo_map / mermaid for an overview",
11345
+ symbols: "pass `name` to look up one symbol, or use find_symbol / symbols_overview",
11346
+ callers: "pass `name` to look up one symbol's call sites",
11347
+ dead_code: "pass `scope` to a subdirectory",
11348
+ find_references: "the symbol is referenced very widely \u2014 narrow with `scope` on a graph query",
11349
+ check_rules: "narrow the rule set, or pass `scope` to a subdirectory"
11350
+ };
11351
+ ARTIFACT_FOR = { graph: "graph.json", symbols: "symbols.json" };
11004
11352
  }
11005
11353
  });
11006
11354
 
@@ -11123,6 +11471,140 @@ var init_rewrite = __esm({
11123
11471
  init_types();
11124
11472
  init_walk();
11125
11473
  init_scan();
11474
+ init_scan();
11475
+ init_preload();
11476
+
11477
+ // src/pool.ts
11478
+ init_hash();
11479
+ init_walk();
11480
+ init_registry();
11481
+ init_loader();
11482
+ init_scan();
11483
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
11484
+ import { availableParallelism } from "os";
11485
+ import { dirname as dirname2, join as join4 } from "path";
11486
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
11487
+ import { Worker } from "worker_threads";
11488
+ function resolveEngineUrl() {
11489
+ try {
11490
+ const here = fileURLToPath2(import.meta.url);
11491
+ if (here.endsWith("engine.mjs")) return pathToFileURL(here).href;
11492
+ const adjacent = join4(dirname2(here), "engine.mjs");
11493
+ if (existsSync2(adjacent)) return pathToFileURL(adjacent).href;
11494
+ return void 0;
11495
+ } catch {
11496
+ return void 0;
11497
+ }
11498
+ }
11499
+ var WORKER_TIMEOUT_MS = 10 * 60 * 1e3;
11500
+ function workerCount(requested) {
11501
+ const env = process.env["CODEINDEX_WORKERS"];
11502
+ const raw = requested ?? (env !== void 0 && env !== "" ? Number(env) : void 0);
11503
+ if (raw !== void 0) return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
11504
+ let cores = 1;
11505
+ try {
11506
+ cores = availableParallelism();
11507
+ } catch {
11508
+ cores = 1;
11509
+ }
11510
+ return Math.max(0, Math.min(cores - 1, 8));
11511
+ }
11512
+ async function runExtractWorker(input, post) {
11513
+ await ensureGrammars(input.grammarKeys);
11514
+ const ready = input.grammarKeys.filter((k) => grammarReady(k));
11515
+ const records = [];
11516
+ for (const job of input.jobs) {
11517
+ let size;
11518
+ let mtimeMs;
11519
+ try {
11520
+ const st = statSync2(job.abs);
11521
+ size = st.size;
11522
+ mtimeMs = st.mtimeMs;
11523
+ } catch {
11524
+ continue;
11525
+ }
11526
+ const content = readText(job.abs);
11527
+ const record = buildCodeRecord(job.rel, job.ext, size, content, sha1(content), extToLang(job.ext), {
11528
+ maxCallsPerFile: input.maxCallsPerFile
11529
+ });
11530
+ records.push({ rel: job.rel, size, mtimeMs, record });
11531
+ }
11532
+ post({ ready, records });
11533
+ }
11534
+ async function extractInParallel(jobs, grammarKeys, count, opts = {}) {
11535
+ if (count < 2 || jobs.length === 0) return void 0;
11536
+ const engineUrl = resolveEngineUrl();
11537
+ if (!engineUrl) return void 0;
11538
+ const wanted = grammarKeys.filter((k) => grammarReady(k)).sort();
11539
+ const shards = Array.from({ length: Math.min(count, jobs.length) }, () => []);
11540
+ jobs.forEach((j, i2) => shards[i2 % shards.length].push(j));
11541
+ const bootstrap = `import { runExtractWorker } from ${JSON.stringify(engineUrl)};
11542
+ import { parentPort, workerData } from "node:worker_threads";
11543
+ runExtractWorker(workerData.input, (o) => parentPort.postMessage(o)).catch((e) => parentPort.postMessage({ error: String(e) }));
11544
+ `;
11545
+ try {
11546
+ const outputs = await Promise.all(
11547
+ shards.map(
11548
+ (jobsForShard) => new Promise((resolve3, reject) => {
11549
+ const w = new Worker(bootstrap, {
11550
+ eval: true,
11551
+ workerData: { input: { jobs: jobsForShard, grammarKeys: wanted, maxCallsPerFile: opts.maxCallsPerFile } }
11552
+ });
11553
+ const timer = setTimeout(() => {
11554
+ reject(new Error("extraction worker timed out"));
11555
+ void w.terminate();
11556
+ }, WORKER_TIMEOUT_MS);
11557
+ const settle = (fn) => {
11558
+ clearTimeout(timer);
11559
+ fn();
11560
+ };
11561
+ w.once("message", (m) => {
11562
+ settle(() => resolve3(m));
11563
+ void w.terminate();
11564
+ });
11565
+ w.once("error", (e) => settle(() => reject(e)));
11566
+ w.once("exit", (code) => {
11567
+ if (code !== 0) settle(() => reject(new Error(`extraction worker exited with ${code}`)));
11568
+ });
11569
+ })
11570
+ )
11571
+ );
11572
+ const out2 = /* @__PURE__ */ new Map();
11573
+ for (const o of outputs) {
11574
+ if ("error" in o) return void 0;
11575
+ if (o.ready.slice().sort().join(",") !== wanted.join(",")) return void 0;
11576
+ for (const r of o.records) out2.set(r.rel, { size: r.size, mtimeMs: r.mtimeMs, record: r.record });
11577
+ }
11578
+ return out2;
11579
+ } catch {
11580
+ return void 0;
11581
+ }
11582
+ }
11583
+ async function scanRepoParallel(root, opts = {}) {
11584
+ const count = workerCount(opts.workers);
11585
+ if (count < 2) return scanRepo(root, opts);
11586
+ const walked = opts.precomputedWalk ?? walk(root, {
11587
+ maxFileBytes: opts.maxBytes,
11588
+ maxFiles: opts.maxFiles,
11589
+ gitignore: opts.gitignore,
11590
+ ignoreDirs: opts.ignoreDirs
11591
+ });
11592
+ const scanOpts = { ...opts, precomputedWalk: walked };
11593
+ const jobs = [];
11594
+ for (const { f } of keptCodeFiles(root, scanOpts)) {
11595
+ const cached = opts.cache?.get(f.rel);
11596
+ if (!opts.fullHash && cached && cached.size !== void 0 && cached.mtimeMs !== void 0 && cached.size === f.size && cached.mtimeMs === f.mtimeMs) {
11597
+ continue;
11598
+ }
11599
+ jobs.push({ abs: f.abs, rel: f.rel, ext: f.ext });
11600
+ }
11601
+ if (jobs.length === 0) return scanRepo(root, scanOpts);
11602
+ const grammarKeys = grammarKeysForExts(walked.files.map((f) => f.ext));
11603
+ const extracted = await extractInParallel(jobs, grammarKeys, count, { maxCallsPerFile: opts.maxCallsPerFile });
11604
+ return scanRepo(root, extracted ? { ...scanOpts, extracted } : scanOpts);
11605
+ }
11606
+
11607
+ // src/engine.ts
11126
11608
  init_glob();
11127
11609
  init_ignore();
11128
11610
  init_classify();
@@ -11260,8 +11742,8 @@ init_loader();
11260
11742
  // src/ast/grammars-pull.ts
11261
11743
  init_types();
11262
11744
  import { createHash as createHash2 } from "crypto";
11263
- import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
11264
- import { dirname as dirname2, join as join3, resolve, sep as sep2 } from "path";
11745
+ import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync } from "fs";
11746
+ import { dirname as dirname3, join as join5, resolve, sep as sep2 } from "path";
11265
11747
  import { gunzipSync } from "zlib";
11266
11748
  var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
11267
11749
  function resolveGrammarsPullTarget() {
@@ -11342,7 +11824,7 @@ function extractTarInto(rawTar, destDir) {
11342
11824
  if (dest !== root && !dest.startsWith(root + sep2)) {
11343
11825
  throw new Error(`tar entry escapes destination: ${entry.name}`);
11344
11826
  }
11345
- mkdirSync(dirname2(dest), { recursive: true });
11827
+ mkdirSync(dirname3(dest), { recursive: true });
11346
11828
  writeFileSync(dest, entry.data);
11347
11829
  written.push(rel);
11348
11830
  }
@@ -11366,12 +11848,12 @@ async function pullGrammars(cacheDir, opts = {}) {
11366
11848
  `);
11367
11849
  }
11368
11850
  }
11369
- const runtime = join3(cacheDir, "web-tree-sitter.wasm");
11370
- const markerPath = join3(dirname2(cacheDir), `${ENGINE_VERSION}.sha256`);
11371
- if (existsSync2(runtime) && expected && existsSync2(markerPath)) {
11851
+ const runtime = join5(cacheDir, "web-tree-sitter.wasm");
11852
+ const markerPath = join5(dirname3(cacheDir), `${ENGINE_VERSION}.sha256`);
11853
+ if (existsSync3(runtime) && expected && existsSync3(markerPath)) {
11372
11854
  let marker = "";
11373
11855
  try {
11374
- marker = readFileSync3(markerPath, "utf8").trim();
11856
+ marker = readFileSync4(markerPath, "utf8").trim();
11375
11857
  } catch {
11376
11858
  }
11377
11859
  if (marker === expected) {
@@ -11395,13 +11877,13 @@ async function pullGrammars(cacheDir, opts = {}) {
11395
11877
  }
11396
11878
  let tmp;
11397
11879
  try {
11398
- mkdirSync(dirname2(cacheDir), { recursive: true });
11399
- tmp = mkdtempSync(join3(dirname2(cacheDir), ".grammars-tmp-"));
11880
+ mkdirSync(dirname3(cacheDir), { recursive: true });
11881
+ tmp = mkdtempSync(join5(dirname3(cacheDir), ".grammars-tmp-"));
11400
11882
  extractGrammarsTarball(bytes, tmp);
11401
- if (!existsSync2(join3(tmp, "web-tree-sitter.wasm"))) {
11883
+ if (!existsSync3(join5(tmp, "web-tree-sitter.wasm"))) {
11402
11884
  throw new Error("archive is missing web-tree-sitter.wasm");
11403
11885
  }
11404
- if (existsSync2(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
11886
+ if (existsSync3(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
11405
11887
  renameSync(tmp, cacheDir);
11406
11888
  tmp = void 0;
11407
11889
  if (expected) writeFileSync(markerPath, expected + "\n");
@@ -11478,7 +11960,48 @@ init_graph_json();
11478
11960
  init_types();
11479
11961
  init_walk();
11480
11962
  init_sort();
11481
- import { join as join12 } from "path";
11963
+ import { join as join14 } from "path";
11964
+ var Bytes = class {
11965
+ buf;
11966
+ len = 0;
11967
+ constructor(capacity = 64) {
11968
+ this.buf = new Uint8Array(capacity);
11969
+ }
11970
+ get length() {
11971
+ return this.len;
11972
+ }
11973
+ grow(need) {
11974
+ if (this.len + need <= this.buf.length) return;
11975
+ let cap = this.buf.length * 2 || 64;
11976
+ while (cap < this.len + need) cap *= 2;
11977
+ const next = new Uint8Array(cap);
11978
+ next.set(this.buf.subarray(0, this.len));
11979
+ this.buf = next;
11980
+ }
11981
+ // Rewind without releasing the buffer, so a per-item scratch sink is
11982
+ // allocated once per document instead of once per occurrence/symbol.
11983
+ reset() {
11984
+ this.len = 0;
11985
+ }
11986
+ push(byte) {
11987
+ this.grow(1);
11988
+ this.buf[this.len++] = byte;
11989
+ }
11990
+ pushAll(src) {
11991
+ this.grow(src.length);
11992
+ if (src instanceof Uint8Array) this.buf.set(src, this.len);
11993
+ else for (let i2 = 0; i2 < src.length; i2++) this.buf[this.len + i2] = src[i2];
11994
+ this.len += src.length;
11995
+ }
11996
+ // A view over exactly the bytes written — no copy. Callers must not retain it
11997
+ // across further writes to this sink.
11998
+ view() {
11999
+ return this.buf.subarray(0, this.len);
12000
+ }
12001
+ toUint8Array() {
12002
+ return this.buf.slice(0, this.len);
12003
+ }
12004
+ };
11482
12005
  var utf8 = new TextEncoder();
11483
12006
  function pushVarint(out2, n) {
11484
12007
  if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
@@ -11498,15 +12021,18 @@ function pushVarintField(out2, field, n) {
11498
12021
  function pushLenDelim(out2, field, payload) {
11499
12022
  pushTag(out2, field, 2);
11500
12023
  pushVarint(out2, payload.length);
11501
- for (let i2 = 0; i2 < payload.length; i2++) out2.push(payload[i2]);
12024
+ out2.pushAll(payload);
12025
+ }
12026
+ function pushMessage(out2, field, payload) {
12027
+ pushLenDelim(out2, field, payload.view());
11502
12028
  }
11503
12029
  function pushString(out2, field, s) {
11504
12030
  pushLenDelim(out2, field, utf8.encode(s));
11505
12031
  }
11506
12032
  function pushPackedInt32(out2, field, values) {
11507
- const payload = [];
12033
+ const payload = new Bytes(values.length * 2);
11508
12034
  for (const v of values) pushVarint(payload, v);
11509
- pushLenDelim(out2, field, payload);
12035
+ pushMessage(out2, field, payload);
11510
12036
  }
11511
12037
  var F_INDEX_METADATA = 1;
11512
12038
  var F_INDEX_DOCUMENTS = 2;
@@ -11648,7 +12174,7 @@ function renderScip(scan2, opts = {}) {
11648
12174
  };
11649
12175
  const documents = [];
11650
12176
  for (const f of docs) {
11651
- const text = readText(join12(scan2.root, f.rel));
12177
+ const text = readText(join14(scan2.root, f.rel));
11652
12178
  const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
11653
12179
  const locate = (lineNo, name2) => {
11654
12180
  const line = lines[lineNo - 1];
@@ -11677,41 +12203,45 @@ function renderScip(scan2, opts = {}) {
11677
12203
  kind: KIND[sym.kind],
11678
12204
  enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : void 0
11679
12205
  })).sort((a, b) => byStr(a.symbol, b.symbol));
11680
- const doc = [];
12206
+ const doc = new Bytes(1024);
11681
12207
  pushString(doc, F_DOC_RELPATH, f.rel);
12208
+ const ob = new Bytes(64);
11682
12209
  for (const o of occs) {
11683
12210
  const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
11684
12211
  if (seenOcc.has(key)) continue;
11685
12212
  seenOcc.add(key);
11686
- const ob = [];
12213
+ ob.reset();
11687
12214
  pushPackedInt32(ob, F_OCC_RANGE, o.range);
11688
12215
  pushString(ob, F_OCC_SYMBOL, o.symbol);
11689
12216
  if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
11690
- pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
12217
+ pushMessage(doc, F_DOC_OCCURRENCES, ob);
11691
12218
  }
12219
+ const sb = new Bytes(64);
11692
12220
  for (const si of infos) {
11693
- const sb = [];
12221
+ sb.reset();
11694
12222
  pushString(sb, F_SI_SYMBOL, si.symbol);
11695
12223
  if (si.kind !== void 0) pushVarintField(sb, F_SI_KIND, si.kind);
11696
12224
  pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
11697
12225
  if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
11698
- pushLenDelim(doc, F_DOC_SYMBOLS, sb);
12226
+ pushMessage(doc, F_DOC_SYMBOLS, sb);
11699
12227
  }
11700
12228
  pushString(doc, F_DOC_LANGUAGE, f.lang);
11701
12229
  pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
11702
12230
  documents.push(doc);
11703
12231
  }
11704
- const toolInfo = [];
12232
+ const toolInfo = new Bytes();
11705
12233
  pushString(toolInfo, F_TOOL_NAME, "codeindex");
11706
12234
  pushString(toolInfo, F_TOOL_VERSION, toolVersion);
11707
- const metadata2 = [];
11708
- pushLenDelim(metadata2, F_META_TOOL_INFO, toolInfo);
12235
+ const metadata2 = new Bytes();
12236
+ pushMessage(metadata2, F_META_TOOL_INFO, toolInfo);
11709
12237
  pushString(metadata2, F_META_PROJECT_ROOT, projectRoot);
11710
12238
  pushVarintField(metadata2, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
11711
- const index = [];
11712
- pushLenDelim(index, F_INDEX_METADATA, metadata2);
11713
- for (const d of documents) pushLenDelim(index, F_INDEX_DOCUMENTS, d);
11714
- return Uint8Array.from(index);
12239
+ let total = 0;
12240
+ for (const d of documents) total += d.length;
12241
+ const index = new Bytes(total + metadata2.length + 16);
12242
+ pushMessage(index, F_INDEX_METADATA, metadata2);
12243
+ for (const d of documents) pushMessage(index, F_INDEX_DOCUMENTS, d);
12244
+ return index.toUint8Array();
11715
12245
  }
11716
12246
 
11717
12247
  // src/engine.ts
@@ -11730,6 +12260,375 @@ init_repomap();
11730
12260
  init_deadcode();
11731
12261
  init_complexity();
11732
12262
  init_viz();
12263
+
12264
+ // src/traverse.ts
12265
+ init_sort();
12266
+ var DEPENDS_KINDS = /* @__PURE__ */ new Set(["import", "use", "call"]);
12267
+ function hubThreshold(degrees) {
12268
+ const sorted = degrees.slice().sort((a, b) => a - b);
12269
+ const n = sorted.length;
12270
+ const p99 = n === 0 ? 0 : sorted[Math.min(n - 1, Math.floor(0.99 * n))];
12271
+ return Math.max(50, p99);
12272
+ }
12273
+ function reverseClosure(edges, seeds, depth = Infinity) {
12274
+ const dependents = /* @__PURE__ */ new Map();
12275
+ for (const e of edges) {
12276
+ if (e.dangling || !DEPENDS_KINDS.has(e.kind)) continue;
12277
+ let arr = dependents.get(e.to);
12278
+ if (!arr) dependents.set(e.to, arr = []);
12279
+ arr.push(e);
12280
+ }
12281
+ const depthOf = /* @__PURE__ */ new Map();
12282
+ const seen = new Set(seeds);
12283
+ let frontier = [...seeds];
12284
+ for (let d = 1; d <= depth && frontier.length; d++) {
12285
+ const next = [];
12286
+ for (const node of frontier) {
12287
+ for (const e of (dependents.get(node) ?? []).slice().sort((a, b) => byStr(a.from, b.from))) {
12288
+ if (seen.has(e.from)) continue;
12289
+ seen.add(e.from);
12290
+ depthOf.set(e.from, d);
12291
+ next.push(e.from);
12292
+ }
12293
+ }
12294
+ frontier = next;
12295
+ }
12296
+ return depthOf;
12297
+ }
12298
+ function impactOf(graph, target, depth = Infinity) {
12299
+ const moduleOf = new Map(graph.files.map((f) => [f.rel, f.module]));
12300
+ const mod = graph.modules.find((m) => m.slug === target);
12301
+ const file = mod ? void 0 : graph.files.find((f) => f.rel === target);
12302
+ if (!mod && !file) return void 0;
12303
+ const seeds = mod ? mod.members : [file.rel];
12304
+ const depthOf = reverseClosure(graph.fileEdges, seeds, depth);
12305
+ const files = [...depthOf.entries()].map(([rel, d]) => ({ rel, module: moduleOf.get(rel) ?? "root", depth: d })).sort((a, b) => a.depth - b.depth || byStr(a.rel, b.rel));
12306
+ const modules = [...new Set(files.map((f) => f.module).filter((m) => m !== target))].sort(byStr);
12307
+ return { target, scope: mod ? "module" : "file", seeds, files, modules };
12308
+ }
12309
+ function bfs(edges, start2, depth, kinds) {
12310
+ const out2 = /* @__PURE__ */ new Map();
12311
+ const inn = /* @__PURE__ */ new Map();
12312
+ const degree = /* @__PURE__ */ new Map();
12313
+ for (const e of edges) {
12314
+ if (e.dangling) continue;
12315
+ if (kinds && !kinds.has(e.kind)) continue;
12316
+ (out2.get(e.from) ?? out2.set(e.from, []).get(e.from)).push(e);
12317
+ (inn.get(e.to) ?? inn.set(e.to, []).get(e.to)).push(e);
12318
+ degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
12319
+ degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
12320
+ }
12321
+ const threshold = hubThreshold([...degree.values()]);
12322
+ const seen = /* @__PURE__ */ new Set([start2]);
12323
+ const links = [];
12324
+ let frontier = [start2];
12325
+ for (let d = 1; d <= depth; d++) {
12326
+ const next = [];
12327
+ for (const node of frontier) {
12328
+ if (node !== start2 && (degree.get(node) ?? 0) >= threshold) continue;
12329
+ for (const e of (out2.get(node) ?? []).slice().sort((a, b) => byStr(a.to, b.to))) {
12330
+ if (seen.has(e.to)) continue;
12331
+ links.push({ node: e.to, direction: "out", kind: e.kind, weight: e.weight, depth: d, confidence: e.confidence });
12332
+ seen.add(e.to);
12333
+ next.push(e.to);
12334
+ }
12335
+ for (const e of (inn.get(node) ?? []).slice().sort((a, b) => byStr(a.from, b.from))) {
12336
+ if (seen.has(e.from)) continue;
12337
+ links.push({ node: e.from, direction: "in", kind: e.kind, weight: e.weight, depth: d, confidence: e.confidence });
12338
+ seen.add(e.from);
12339
+ next.push(e.from);
12340
+ }
12341
+ }
12342
+ frontier = next;
12343
+ }
12344
+ return links;
12345
+ }
12346
+ function neighborsOf(graph, target, depth = 1, kinds) {
12347
+ const mod = graph.modules.find((m) => m.slug === target);
12348
+ if (mod) {
12349
+ return { target, scope: "module", links: bfs(graph.moduleEdges, target, depth, kinds), members: mod.members };
12350
+ }
12351
+ const file = graph.files.find((f) => f.rel === target);
12352
+ if (file) {
12353
+ return { target, scope: "file", links: bfs(graph.fileEdges, target, depth, kinds) };
12354
+ }
12355
+ return void 0;
12356
+ }
12357
+
12358
+ // src/delta.ts
12359
+ init_git();
12360
+ init_sort();
12361
+ init_util();
12362
+ var RISK_WEIGHTS = {
12363
+ exportedChange: 25,
12364
+ // an exported symbol changed: consumers may break
12365
+ hubHigh: 20,
12366
+ // pagerank percentile ≥ .90
12367
+ hubMed: 10,
12368
+ // pagerank percentile ≥ .75
12369
+ blastHigh: 20,
12370
+ // ≥ 20 dependent files or ≥ 5 dependent modules
12371
+ blastMed: 10,
12372
+ // ≥ 5 dependent files
12373
+ testGap: 20,
12374
+ // a testable module with no covering test
12375
+ surprise: 10,
12376
+ // the module sits on a surprising cross-community edge
12377
+ dangling: 15
12378
+ // a changed file carries a dangling import
12379
+ };
12380
+ var HIGH_MIN = 60;
12381
+ var MEDIUM_MIN = 30;
12382
+ var OPEN_CAP = 3;
12383
+ var DEFAULT_DELTA_DEPTH = 2;
12384
+ function symbolsInHunks(defs, hunks) {
12385
+ const out2 = [];
12386
+ const seen = /* @__PURE__ */ new Set();
12387
+ const push = (d, approx) => {
12388
+ const key = `${d.name}:${d.line}`;
12389
+ if (seen.has(key)) return;
12390
+ seen.add(key);
12391
+ out2.push({
12392
+ name: d.name,
12393
+ kind: d.kind,
12394
+ exported: d.exported,
12395
+ line: d.line,
12396
+ ...d.endLine !== void 0 ? { endLine: d.endLine } : {},
12397
+ ...d.parent !== void 0 ? { parent: d.parent } : {},
12398
+ ...approx ? { approx: true } : {}
12399
+ });
12400
+ };
12401
+ const span = (d) => (d.endLine ?? d.line) - d.line;
12402
+ for (const h of hunks) {
12403
+ const enclosing = defs.filter((d) => d.line <= h.end && (d.endLine ?? d.line) >= h.start);
12404
+ if (enclosing.length) {
12405
+ enclosing.sort((a, b) => span(a) - span(b) || b.line - a.line || byStr(a.name, b.name));
12406
+ for (const d of enclosing) push(d, false);
12407
+ } else {
12408
+ const above = defs.filter((d) => d.line <= h.start && d.endLine === void 0);
12409
+ const near = above[above.length - 1];
12410
+ if (near) push(near, true);
12411
+ }
12412
+ }
12413
+ return out2;
12414
+ }
12415
+ function percentile(values, mine) {
12416
+ if (values.length <= 1) return 0;
12417
+ let smaller = 0;
12418
+ for (const v of values) if (v < mine) smaller++;
12419
+ return smaller / (values.length - 1);
12420
+ }
12421
+ function computeDelta(graph, symbols, diff, depth = DEFAULT_DELTA_DEPTH) {
12422
+ const notes = [...diff.notes ?? []];
12423
+ if (!symbols) notes.push("symbol index missing \u2014 symbol-level attribution disabled");
12424
+ const fileByRel = new Map(graph.files.map((f) => [f.rel, f]));
12425
+ const defsByFile = /* @__PURE__ */ new Map();
12426
+ if (symbols) {
12427
+ for (const [name2, entries] of Object.entries(symbols.defs)) {
12428
+ for (const d of entries) {
12429
+ let arr = defsByFile.get(d.file);
12430
+ if (!arr) defsByFile.set(d.file, arr = []);
12431
+ arr.push({ name: name2, ...d });
12432
+ }
12433
+ }
12434
+ for (const arr of defsByFile.values()) arr.sort((a, b) => a.line - b.line || byStr(a.name, b.name));
12435
+ }
12436
+ const deleted = [];
12437
+ const unindexed = [];
12438
+ const changes = [];
12439
+ for (const df of [...diff.files].sort((a, b) => byStr(a.path, b.path))) {
12440
+ const carry = {
12441
+ ...df.oldPath !== void 0 ? { oldPath: df.oldPath } : {},
12442
+ ...df.binary ? { binary: true } : {},
12443
+ ...df.linesAdded !== void 0 ? { linesAdded: df.linesAdded } : {},
12444
+ ...df.linesDeleted !== void 0 ? { linesDeleted: df.linesDeleted } : {}
12445
+ };
12446
+ if (df.status === "deleted") {
12447
+ deleted.push(df.path);
12448
+ changes.push({ path: df.path, status: df.status, ...carry, hunks: [], symbols: [] });
12449
+ continue;
12450
+ }
12451
+ const node = fileByRel.get(df.path);
12452
+ if (!node) {
12453
+ unindexed.push(df.path);
12454
+ continue;
12455
+ }
12456
+ let hunks = diff.hunks.get(df.path) ?? [];
12457
+ if (!hunks.length && df.status === "added" && !df.binary) hunks = [{ start: 1, end: Math.max(node.lines, 1) }];
12458
+ const syms = df.binary ? [] : symbolsInHunks(defsByFile.get(df.path) ?? [], hunks);
12459
+ changes.push({
12460
+ path: df.path,
12461
+ status: df.status,
12462
+ ...carry,
12463
+ module: node.module,
12464
+ hunks: hunks.map((h) => ({ start: h.start, end: h.end })),
12465
+ symbols: syms
12466
+ });
12467
+ }
12468
+ const changedRels = new Set(changes.filter((c2) => c2.status !== "deleted").map((c2) => c2.path));
12469
+ const dangling = graph.fileEdges.filter((e) => e.dangling && (e.kind === "import" || e.kind === "doc-link") && changedRels.has(e.from)).map((e) => ({ from: e.from, spec: e.to, reason: e.reason ?? "unknown" })).sort((a, b) => byStr(a.from, b.from) || byStr(a.spec, b.spec));
12470
+ const byModule = /* @__PURE__ */ new Map();
12471
+ for (const c2 of changes) {
12472
+ if (c2.status === "deleted" || !c2.module) continue;
12473
+ let arr = byModule.get(c2.module);
12474
+ if (!arr) byModule.set(c2.module, arr = []);
12475
+ arr.push(c2);
12476
+ }
12477
+ const nonTestCode = /* @__PURE__ */ new Set();
12478
+ for (const f of graph.files) {
12479
+ if (f.fileKind === "code" && !f.testFile) nonTestCode.add(f.module);
12480
+ }
12481
+ const pagerankKnown = graph.modules.some((m) => m.pagerank !== void 0);
12482
+ const metricOf = (m) => pagerankKnown ? m.pagerank ?? 0 : m.degIn + m.degOut;
12483
+ const metricValues = graph.modules.map(metricOf);
12484
+ const metricName = pagerankKnown ? "pagerank" : "degree";
12485
+ const modules = [];
12486
+ for (const slug of [...byModule.keys()].sort(byStr)) {
12487
+ const m = graph.modules.find((x) => x.slug === slug);
12488
+ if (!m) continue;
12489
+ const moduleChanges = byModule.get(slug);
12490
+ const reasons = [];
12491
+ let score = 0;
12492
+ const exportedNames = [...new Set(moduleChanges.flatMap((c2) => c2.symbols.filter((s) => s.exported).map((s) => s.name)))].sort(byStr);
12493
+ if (exportedNames.length) {
12494
+ score += RISK_WEIGHTS.exportedChange;
12495
+ const shown = exportedNames.slice(0, 3).join(", ") + (exportedNames.length > 3 ? ", \u2026" : "");
12496
+ reasons.push(exportedNames.length === 1 ? `exported symbol ${shown} changed` : `exported symbols ${shown} changed`);
12497
+ }
12498
+ const pct = percentile(metricValues, metricOf(m));
12499
+ if (pct >= 0.9) {
12500
+ score += RISK_WEIGHTS.hubHigh;
12501
+ reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
12502
+ } else if (pct >= 0.75) {
12503
+ score += RISK_WEIGHTS.hubMed;
12504
+ reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
12505
+ }
12506
+ const depthByRel = /* @__PURE__ */ new Map();
12507
+ const impModules = /* @__PURE__ */ new Set();
12508
+ for (const c2 of moduleChanges) {
12509
+ const imp = impactOf(graph, c2.path, depth);
12510
+ if (!imp) continue;
12511
+ for (const f of imp.files) {
12512
+ const prev = depthByRel.get(f.rel);
12513
+ if (prev === void 0 || f.depth < prev) depthByRel.set(f.rel, f.depth);
12514
+ }
12515
+ for (const im of imp.modules) if (im !== slug) impModules.add(im);
12516
+ }
12517
+ const transitiveFiles = depthByRel.size;
12518
+ const directFiles = [...depthByRel.values()].filter((d) => d === 1).length;
12519
+ const impact = { directFiles, transitiveFiles, modules: [...impModules].sort(byStr) };
12520
+ if (transitiveFiles >= 20 || impact.modules.length >= 5) {
12521
+ score += RISK_WEIGHTS.blastHigh;
12522
+ reasons.push(`${transitiveFiles} dependent files across ${impact.modules.length} modules (depth ${depth})`);
12523
+ } else if (transitiveFiles >= 5) {
12524
+ score += RISK_WEIGHTS.blastMed;
12525
+ reasons.push(`${transitiveFiles} dependent files across ${impact.modules.length} modules (depth ${depth})`);
12526
+ }
12527
+ const testable = m.tier <= 1 && m.symbols > 0 && nonTestCode.has(slug);
12528
+ const coveredBy = m.testedBy ?? [];
12529
+ const tests = testable ? coveredBy.length ? { status: "covered", files: coveredBy } : { status: "gap", files: [] } : { status: "n/a", files: [] };
12530
+ if (tests.status === "gap") {
12531
+ score += RISK_WEIGHTS.testGap;
12532
+ reasons.push("no test covers this module");
12533
+ }
12534
+ const sup = (graph.surprises ?? []).find((s) => s.from === slug || s.to === slug);
12535
+ if (sup) {
12536
+ score += RISK_WEIGHTS.surprise;
12537
+ reasons.push(`cross-community edge to ${sup.from === slug ? sup.to : sup.from} (surprising)`);
12538
+ }
12539
+ const moduleDangling = dangling.filter((d) => moduleChanges.some((c2) => c2.path === d.from));
12540
+ if (moduleDangling.length) {
12541
+ score += RISK_WEIGHTS.dangling;
12542
+ const first = moduleDangling[0];
12543
+ const more = moduleDangling.length > 1 ? ` (+${moduleDangling.length - 1} more)` : "";
12544
+ reasons.push(`dangling import "${first.spec}" in ${first.from}${more}`);
12545
+ }
12546
+ score = Math.min(100, score);
12547
+ const changedFiles = moduleChanges.map((c2) => c2.path).sort(byStr);
12548
+ const allSyms = moduleChanges.flatMap((c2) => c2.symbols);
12549
+ const open = moduleChanges.slice().sort(
12550
+ (a, b) => b.symbols.filter((s) => s.exported).length - a.symbols.filter((s) => s.exported).length || b.symbols.length - a.symbols.length || byStr(a.path, b.path)
12551
+ ).slice(0, OPEN_CAP).map((c2) => c2.path);
12552
+ modules.push({
12553
+ slug,
12554
+ path: m.path,
12555
+ score,
12556
+ bucket: score >= HIGH_MIN ? "HIGH" : score >= MEDIUM_MIN ? "MEDIUM" : "LOW",
12557
+ reasons,
12558
+ changedFiles,
12559
+ changedSymbols: {
12560
+ total: new Set(allSyms.map((s) => `${s.name}:${s.line}`)).size,
12561
+ exported: new Set(allSyms.filter((s) => s.exported).map((s) => `${s.name}:${s.line}`)).size
12562
+ },
12563
+ impact,
12564
+ tests,
12565
+ open
12566
+ });
12567
+ }
12568
+ modules.sort((a, b) => b.score - a.score || byStr(a.slug, b.slug));
12569
+ return {
12570
+ base: diff.base,
12571
+ ...graph.commit !== void 0 ? { indexCommit: graph.commit } : {},
12572
+ depth,
12573
+ changes,
12574
+ modules,
12575
+ dangling,
12576
+ deleted: deleted.sort(byStr),
12577
+ unindexed: unindexed.sort(byStr),
12578
+ notes
12579
+ };
12580
+ }
12581
+ function deltaFor(repo, graph, symbols, opts = {}) {
12582
+ if (!have("git")) return { error: "git is required for delta and was not found on PATH" };
12583
+ if (!isGitWorktree(repo)) return { error: `delta needs a git worktree \u2014 ${repo} is not inside one` };
12584
+ const notes = [];
12585
+ let base;
12586
+ if (opts.staged) {
12587
+ const head = sh("git", ["-C", repo, "rev-parse", "HEAD"]);
12588
+ if (!head.ok) return { error: "cannot resolve HEAD \u2014 empty repository?" };
12589
+ base = { ref: "HEAD", mergeBase: head.stdout.trim(), staged: true };
12590
+ } else {
12591
+ const r = resolveBaseRef(repo, opts.base);
12592
+ if ("error" in r) return { error: r.error };
12593
+ if (r.note) notes.push(r.note);
12594
+ base = { ref: r.ref, mergeBase: r.mergeBase, staged: false };
12595
+ }
12596
+ const spec = opts.staged ? { staged: true } : { mergeBase: base.mergeBase };
12597
+ const files = diffFiles(repo, spec);
12598
+ if (!opts.staged) {
12599
+ const known = new Set(files.map((f) => f.path));
12600
+ for (const u of untrackedFiles(repo)) {
12601
+ if (!known.has(u)) files.push({ path: u, status: "added" });
12602
+ }
12603
+ }
12604
+ return computeDelta(graph, symbols, { files, hunks: diffHunks(repo, spec), base, notes }, opts.depth ?? DEFAULT_DELTA_DEPTH);
12605
+ }
12606
+ function formatDeltaPanel(res) {
12607
+ const mb = res.base.mergeBase.slice(0, 7);
12608
+ const vs = `${res.base.staged ? "staged vs " : ""}${res.base.ref}`;
12609
+ if (!res.changes.length && !res.unindexed.length) {
12610
+ return `codeindex: no changes vs ${vs} (merge-base ${mb})
12611
+ `;
12612
+ }
12613
+ const changedCount = res.changes.length + res.unindexed.length;
12614
+ const lines = [
12615
+ `codeindex: delta vs ${vs} (merge-base ${mb}) \u2014 ${changedCount} changed file(s), ${res.modules.length} module(s)${res.indexCommit ? `, index @ ${res.indexCommit}` : ""}`
12616
+ ];
12617
+ for (const n of res.notes) lines.push(` note: ${n}`);
12618
+ for (const m of res.modules) {
12619
+ lines.push(` ${m.bucket.padEnd(6)} ${m.slug} score ${m.score}${m.reasons.length ? ` \u2014 ${m.reasons.join("; ")}` : ""}`);
12620
+ const tests = m.tests.status === "gap" ? "GAP" : m.tests.status === "covered" ? `covered (${m.tests.files.length})` : "n/a";
12621
+ lines.push(` open: ${m.open.join(", ") || "\u2014"} \xB7 tests: ${tests}`);
12622
+ }
12623
+ if (res.dangling.length) {
12624
+ lines.push(` dangling: ${res.dangling.map((d) => `${d.spec} (from ${d.from})`).join(" \xB7 ")}`);
12625
+ }
12626
+ if (res.deleted.length) lines.push(` deleted: ${res.deleted.join(", ")}`);
12627
+ if (res.unindexed.length) lines.push(` unindexed: ${res.unindexed.join(", ")}`);
12628
+ return lines.join("\n") + "\n";
12629
+ }
12630
+
12631
+ // src/engine.ts
11733
12632
  init_mcp();
11734
12633
  init_rewrite();
11735
12634
  init_hash();
@@ -11740,13 +12639,14 @@ init_util();
11740
12639
  init_types();
11741
12640
  init_types();
11742
12641
  init_loader();
11743
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
11744
- import { join as join15, resolve as resolve2 } from "path";
12642
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
12643
+ import { join as join17, resolve as resolve2 } from "path";
11745
12644
  init_pipeline();
11746
12645
  init_hash();
11747
12646
  init_graph_json();
11748
12647
  init_symbols_json();
11749
12648
  init_scan();
12649
+ init_preload();
11750
12650
  init_walk();
11751
12651
  init_callers();
11752
12652
  init_workspaces();
@@ -11814,6 +12714,13 @@ Commands:
11814
12714
  complexity Cyclomatic-complexity estimates, most-complex first. Pass a file
11815
12715
  positional for one file; omit for the repo-wide top
11816
12716
  risk Complexity \xD7 git-churn ranking (JSON; --since <ref> to bound)
12717
+ delta Review panel for the git diff: changed files -> enclosing symbols ->
12718
+ blast radius -> risk score with explained reasons
12719
+ (--base <ref> | --staged, --depth <n>, --json)
12720
+ impact Reverse dependency closure of a file or module: everything that
12721
+ transitively imports/uses/calls it (--depth <n>; JSON)
12722
+ neighbors Graph neighbours of a file or module, both directions
12723
+ (--depth <n>, --kind import,call,use,doc-link,mention; JSON)
11817
12724
  mermaid Mermaid diagram of the module graph; pass a module positional to
11818
12725
  focus on one neighborhood
11819
12726
  rewrite Map an expensive tree-wide search onto its indexed equivalent:
@@ -11828,10 +12735,14 @@ Commands:
11828
12735
  check_rules, the memory quartet and the three symbolic-edit
11829
12736
  writes). Flags: --repo <dir> pins ONE repository so the per-tool
11830
12737
  repo argument becomes optional (an explicit per-call repo still
11831
- wins); --server-name <name> overrides the announced serverInfo
12738
+ wins); --server-name <name> overrides the announced serverInfo;
12739
+ --max-response-bytes <n> caps a single tool response (default 1e6;
12740
+ a response under the cap is byte-identical, one over it is
12741
+ replaced by an actionable notice instead of an unusable blob)
11832
12742
  version Print the engine version
11833
12743
 
11834
- Flags:
12744
+ Flags (accepted before OR after the subcommand: '--repo X scan' and
12745
+ 'scan --repo X' are equivalent):
11835
12746
  --repo <dir> Repo root (default: cwd)
11836
12747
  --out <file> Write output to a file instead of stdout (\`scip\`: --out -
11837
12748
  writes the binary index to stdout)
@@ -11847,6 +12758,16 @@ Flags:
11847
12758
  --max-bytes <n> Skip files above this size (default 1 MiB)
11848
12759
  --max-calls <n> Per-file call-site cap for extraction (default 512)
11849
12760
  --no-ast Skip tree-sitter grammars even when present (regex tier)
12761
+ --workers <n> \`index\`: extraction worker threads (default: cores-1,
12762
+ capped at 8; 0 or 1 forces the single-threaded path).
12763
+ Also settable with CODEINDEX_WORKERS. Artifacts are
12764
+ byte-identical either way
12765
+ --index <dir> Persisted index the READ commands reuse, relative to the
12766
+ repo (default .codeindex \u2014 i.e. what \`index --out\` wrote
12767
+ there). A fresh index turns the scan into a stat pass and,
12768
+ when it still matches the worktree, skips the pipeline
12769
+ entirely. Stale/absent/corrupt \u2192 a normal cold build
12770
+ --no-index-cache Never reuse a persisted index; always build from scratch
11850
12771
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
11851
12772
  --limit <n> Max results for \`search\` (default 20)
11852
12773
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
@@ -11870,7 +12791,7 @@ function parseFlags(args2) {
11870
12791
  if (v === void 0) throw new Error(`missing value for ${a}`);
11871
12792
  return v;
11872
12793
  };
11873
- const num = () => {
12794
+ const num2 = () => {
11874
12795
  const raw = next();
11875
12796
  const n = Number(raw);
11876
12797
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
@@ -11886,20 +12807,32 @@ function parseFlags(args2) {
11886
12807
  else if (a === "--scope") flags2.scope = next();
11887
12808
  else if (a === "--no-gitignore") flags2.gitignore = false;
11888
12809
  else if (a === "--ignore-dir") flags2.ignoreDirs.push(next());
11889
- else if (a === "--max-files") flags2.maxFiles = num();
11890
- else if (a === "--max-bytes") flags2.maxBytes = num();
11891
- else if (a === "--max-calls") flags2.maxCalls = num();
12810
+ else if (a === "--max-files") flags2.maxFiles = num2();
12811
+ else if (a === "--max-bytes") flags2.maxBytes = num2();
12812
+ else if (a === "--max-calls") flags2.maxCalls = num2();
11892
12813
  else if (a === "--ignore-case") flags2.ignoreCase = true;
11893
- else if (a === "--max-hits") flags2.maxHits = num();
11894
- else if (a === "--budget-tokens") flags2.budgetTokens = num();
12814
+ else if (a === "--max-hits") flags2.maxHits = num2();
12815
+ else if (a === "--budget-tokens") flags2.budgetTokens = num2();
11895
12816
  else if (a === "--no-ast") flags2.noAst = true;
11896
- else if (a === "--since") flags2.since = next();
12817
+ else if (a === "--index") flags2.indexDir = next();
12818
+ else if (a === "--no-index-cache") flags2.noIndexCache = true;
12819
+ else if (a === "--workers") {
12820
+ const raw = next();
12821
+ const n = Number(raw);
12822
+ if (!Number.isInteger(n) || n < 0) throw new Error(`--workers expects a non-negative integer, got "${raw}"`);
12823
+ flags2.workers = n;
12824
+ } else if (a === "--since") flags2.since = next();
11897
12825
  else if (a === "--config") flags2.config = resolve2(next());
11898
- else if (a === "--limit") flags2.limit = num();
12826
+ else if (a === "--limit") flags2.limit = num2();
11899
12827
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11900
12828
  else if (a === "--semantic") flags2.semantic = true;
11901
12829
  else if (a === "--recall") flags2.recall = true;
11902
12830
  else if (a === "--run") flags2.run = true;
12831
+ else if (a === "--base") flags2.base = next();
12832
+ else if (a === "--staged") flags2.staged = true;
12833
+ else if (a === "--depth") flags2.depth = num2();
12834
+ else if (a === "--kind") flags2.kind = next();
12835
+ else if (a === "--json") flags2.json = true;
11903
12836
  else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
11904
12837
  else throw new Error(`unknown flag: ${a}`);
11905
12838
  }
@@ -11929,6 +12862,7 @@ var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "w
11929
12862
  function parseMcpFlags(argv) {
11930
12863
  let defaultRepo;
11931
12864
  let name2;
12865
+ let maxResponseBytes;
11932
12866
  for (let i2 = 0; i2 < argv.length; i2++) {
11933
12867
  const a = argv[i2];
11934
12868
  if (a === "--repo") {
@@ -11939,14 +12873,57 @@ function parseMcpFlags(argv) {
11939
12873
  const v = argv[++i2];
11940
12874
  if (!v) throw new Error("--server-name requires a value");
11941
12875
  name2 = v;
12876
+ } else if (a === "--max-response-bytes") {
12877
+ const v = argv[++i2];
12878
+ const n = Number(v);
12879
+ if (!v || !Number.isFinite(n) || n <= 0) throw new Error("--max-response-bytes requires a positive number");
12880
+ maxResponseBytes = n;
11942
12881
  } else {
11943
12882
  throw new Error(`unknown flag for \`mcp\`: ${a}`);
11944
12883
  }
11945
12884
  }
11946
- if (defaultRepo && !existsSync5(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
11947
- return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0 };
12885
+ if (defaultRepo && !existsSync7(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
12886
+ return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0, maxResponseBytes };
12887
+ }
12888
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
12889
+ "--repo",
12890
+ "--out",
12891
+ "--project-root",
12892
+ "--include",
12893
+ "--exclude",
12894
+ "--scope",
12895
+ "--ignore-dir",
12896
+ "--max-files",
12897
+ "--max-bytes",
12898
+ "--max-calls",
12899
+ "--max-hits",
12900
+ "--budget-tokens",
12901
+ "--since",
12902
+ "--config",
12903
+ "--limit",
12904
+ "--server-name",
12905
+ "--workers",
12906
+ "--index",
12907
+ "--max-response-bytes"
12908
+ ]);
12909
+ function hoistLeadingFlags(argv) {
12910
+ const lead = [];
12911
+ let i2 = 0;
12912
+ while (i2 < argv.length) {
12913
+ const a = argv[i2];
12914
+ if (a === void 0 || !a.startsWith("-")) break;
12915
+ lead.push(a);
12916
+ i2++;
12917
+ if (VALUE_FLAGS.has(a) && i2 < argv.length) {
12918
+ lead.push(argv[i2]);
12919
+ i2++;
12920
+ }
12921
+ }
12922
+ if (lead.length === 0 || i2 >= argv.length) return argv;
12923
+ return [argv[i2], ...lead, ...argv.slice(i2 + 1)];
11948
12924
  }
11949
- async function runCli(argv) {
12925
+ async function runCli(rawArgv) {
12926
+ const argv = hoistLeadingFlags(rawArgv);
11950
12927
  const [cmd, ...rest] = argv;
11951
12928
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
11952
12929
  process.stdout.write(HELP);
@@ -11972,7 +12949,7 @@ async function runCli(argv) {
11972
12949
  return;
11973
12950
  }
11974
12951
  const flags2 = parseFlags(rest);
11975
- if (!existsSync5(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
12952
+ if (!existsSync7(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
11976
12953
  const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
11977
12954
  let precomputedWalk;
11978
12955
  if (scans && !flags2.noAst) {
@@ -11984,15 +12961,33 @@ async function runCli(argv) {
11984
12961
  });
11985
12962
  await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
11986
12963
  }
12964
+ const indexDir = flags2.indexDir ?? INDEX_DIR;
12965
+ let preloadTried = false;
12966
+ let preloaded;
12967
+ const tryPreload = () => {
12968
+ if (preloadTried) return preloaded;
12969
+ preloadTried = true;
12970
+ if (flags2.noIndexCache) return void 0;
12971
+ const p = preloadSession(flags2.repo, scanOptions(flags2, precomputedWalk), indexDir);
12972
+ if (p) preloaded = { scan: p.scan, arts: p.arts };
12973
+ return preloaded;
12974
+ };
12975
+ const readScan = () => tryPreload()?.scan ?? scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
12976
+ const readArtifacts = () => {
12977
+ const p = tryPreload();
12978
+ if (p?.arts) return p.arts;
12979
+ if (p) return buildArtifactsFromScan(p.scan, scanOptions(flags2, precomputedWalk));
12980
+ return buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
12981
+ };
11987
12982
  if (cmd === "index") {
11988
12983
  if (!flags2.out) throw new Error("index needs --out <dir>");
11989
12984
  const outDir = flags2.out;
11990
12985
  mkdirSync3(outDir, { recursive: true });
11991
- const cachePath = join15(outDir, "cache.json");
12986
+ const cachePath = join17(outDir, "cache.json");
11992
12987
  let cache;
11993
12988
  let meta = {};
11994
12989
  try {
11995
- const parsed = JSON.parse(readFileSync8(cachePath, "utf8"));
12990
+ const parsed = JSON.parse(readFileSync9(cachePath, "utf8"));
11996
12991
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
11997
12992
  cache = new Map(Object.entries(parsed.files));
11998
12993
  meta = {
@@ -12005,15 +13000,20 @@ async function runCli(argv) {
12005
13000
  }
12006
13001
  } catch {
12007
13002
  }
12008
- const scan2 = scanRepo(flags2.repo, { ...scanOptions(flags2, precomputedWalk), cache, out: outDir });
13003
+ const scan2 = await scanRepoParallel(flags2.repo, {
13004
+ ...scanOptions(flags2, precomputedWalk),
13005
+ cache,
13006
+ out: outDir,
13007
+ workers: flags2.workers
13008
+ });
12009
13009
  const modelDir = resolveEmbedModelDir(flags2.repo);
12010
13010
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
12011
- const graphPath = join15(outDir, "graph.json");
12012
- const symbolsPath = join15(outDir, "symbols.json");
12013
- const embedPath = join15(outDir, "embeddings.bin");
13011
+ const graphPath = join17(outDir, "graph.json");
13012
+ const symbolsPath = join17(outDir, "symbols.json");
13013
+ const embedPath = join17(outDir, "embeddings.bin");
12014
13014
  const artifactSha = (path) => {
12015
13015
  try {
12016
- return sha1(readFileSync8(path));
13016
+ return sha1(readFileSync9(path));
12017
13017
  } catch {
12018
13018
  return void 0;
12019
13019
  }
@@ -12068,23 +13068,23 @@ async function runCli(argv) {
12068
13068
  `);
12069
13069
  }
12070
13070
  } else if (cmd === "scan") {
12071
- const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13071
+ const s = scanSummary(flags2.repo, scanOptions(flags2, precomputedWalk));
12072
13072
  const summary = {
12073
13073
  engineVersion: ENGINE_VERSION,
12074
- commit: scan2.commit,
12075
- fileCount: scan2.files.length,
12076
- languages: scan2.languages,
12077
- capped: scan2.capped
13074
+ commit: s.commit,
13075
+ fileCount: s.fileCount,
13076
+ languages: s.languages,
13077
+ capped: s.capped
12078
13078
  };
12079
13079
  emit(JSON.stringify(summary, null, 2) + "\n", flags2.out);
12080
13080
  } else if (cmd === "graph") {
12081
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13081
+ const { graph } = readArtifacts();
12082
13082
  emit(renderGraphJson(graph), flags2.out);
12083
13083
  } else if (cmd === "symbols") {
12084
- const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13084
+ const { symbols } = readArtifacts();
12085
13085
  emit(renderSymbolsJson(symbols), flags2.out);
12086
13086
  } else if (cmd === "scip") {
12087
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13087
+ const scan2 = readScan();
12088
13088
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
12089
13089
  const out2 = flags2.out ?? resolve2("index.scip");
12090
13090
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
@@ -12094,14 +13094,14 @@ async function runCli(argv) {
12094
13094
  `);
12095
13095
  }
12096
13096
  } else if (cmd === "callers") {
12097
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13097
+ const scan2 = readScan();
12098
13098
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
12099
13099
  const obj = {};
12100
13100
  for (const [name2, entry] of index) obj[name2] = entry;
12101
13101
  emit(JSON.stringify(obj, null, 2) + "\n", flags2.out);
12102
13102
  } else if (cmd === "search") {
12103
13103
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
12104
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13104
+ const scan2 = readScan();
12105
13105
  if (flags2.semantic) {
12106
13106
  const endpoint = resolveEmbedEndpoint();
12107
13107
  const lexical = () => {
@@ -12192,16 +13192,16 @@ async function runCli(argv) {
12192
13192
  }
12193
13193
  const model = loadEmbedModel(modelDir);
12194
13194
  mkdirSync3(flags2.out, { recursive: true });
12195
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13195
+ const scan2 = readScan();
12196
13196
  const index = buildEmbeddingIndex(scan2, model);
12197
- writeFileSync4(join15(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
13197
+ writeFileSync4(join17(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
12198
13198
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
12199
13199
  `);
12200
13200
  } else if (sub === "pull") {
12201
13201
  const { url, sha256 } = resolveEmbedPullUrl();
12202
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join15(flags2.repo, ".codeindex", "models");
13202
+ const destDir = process.env.CODEINDEX_EMBED_DIR ?? join17(flags2.repo, ".codeindex", "models");
12203
13203
  mkdirSync3(destDir, { recursive: true });
12204
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join15(destDir, "model.json")}
13204
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join17(destDir, "model.json")}
12205
13205
  `);
12206
13206
  let body2;
12207
13207
  try {
@@ -12222,8 +13222,8 @@ async function runCli(argv) {
12222
13222
  process.exitCode = 1;
12223
13223
  return;
12224
13224
  }
12225
- writeFileSync4(join15(destDir, "model.json"), body2);
12226
- process.stderr.write(`codeindex: model written to ${join15(destDir, "model.json")}
13225
+ writeFileSync4(join17(destDir, "model.json"), body2);
13226
+ process.stderr.write(`codeindex: model written to ${join17(destDir, "model.json")}
12227
13227
  `);
12228
13228
  } else {
12229
13229
  throw new Error("embed needs a subcommand: status | build | pull | serve");
@@ -12233,7 +13233,7 @@ async function runCli(argv) {
12233
13233
  const cacheDir = sharedGrammarsCacheDir();
12234
13234
  if (sub === "status") {
12235
13235
  const info2 = resolveGrammarsTier();
12236
- const runtimePresent = info2.dir ? existsSync5(join15(info2.dir, "web-tree-sitter.wasm")) : false;
13236
+ const runtimePresent = info2.dir ? existsSync7(join17(info2.dir, "web-tree-sitter.wasm")) : false;
12237
13237
  const target = resolveGrammarsPullTarget();
12238
13238
  const status = {
12239
13239
  engineVersion: ENGINE_VERSION,
@@ -12254,8 +13254,8 @@ async function runCli(argv) {
12254
13254
  }
12255
13255
  } else if (cmd === "rules") {
12256
13256
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
12257
- const rules = parseRules(JSON.parse(readFileSync8(flags2.config, "utf8")));
12258
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13257
+ const rules = parseRules(JSON.parse(readFileSync9(flags2.config, "utf8")));
13258
+ const { graph } = readArtifacts();
12259
13259
  const violations = checkRules(graph, rules);
12260
13260
  const errors = violations.filter((v) => v.severity === "error").length;
12261
13261
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags2.out);
@@ -12276,26 +13276,48 @@ async function runCli(argv) {
12276
13276
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
12277
13277
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags2.out);
12278
13278
  } else if (cmd === "repomap") {
12279
- const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13279
+ const { scan: scan2, graph } = readArtifacts();
12280
13280
  emit(renderRepoMap(scan2, graph, { budgetTokens: flags2.budgetTokens }), flags2.out);
12281
13281
  } else if (cmd === "hotspots") {
12282
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13282
+ const scan2 = readScan();
12283
13283
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
12284
13284
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
12285
13285
  } else if (cmd === "coupling") {
12286
13286
  const { ok, couplings } = changeCoupling(flags2.repo, { since: flags2.since });
12287
13287
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
12288
13288
  } else if (cmd === "deadcode") {
12289
- emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk))), null, 2) + "\n", flags2.out);
13289
+ emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags2.out);
12290
13290
  } else if (cmd === "complexity") {
12291
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13291
+ const scan2 = readScan();
12292
13292
  emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
12293
13293
  } else if (cmd === "risk") {
12294
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13294
+ const scan2 = readScan();
12295
13295
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
12296
13296
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
13297
+ } else if (cmd === "delta") {
13298
+ const { graph, symbols } = readArtifacts();
13299
+ const res = deltaFor(flags2.repo, graph, symbols, {
13300
+ base: flags2.base,
13301
+ staged: flags2.staged,
13302
+ depth: flags2.depth
13303
+ });
13304
+ if ("error" in res) throw new Error(res.error);
13305
+ emit(flags2.json ? JSON.stringify(res, null, 2) + "\n" : formatDeltaPanel(res), flags2.out);
13306
+ } else if (cmd === "impact") {
13307
+ if (!flags2.positional) throw new Error("impact needs a target: cli.mjs impact <file|module> --repo <dir>");
13308
+ const { graph } = readArtifacts();
13309
+ const res = impactOf(graph, flags2.positional, flags2.depth ?? Infinity);
13310
+ if (!res) throw new Error(`no such file or module in the index: ${flags2.positional}`);
13311
+ emit(JSON.stringify(res, null, 2) + "\n", flags2.out);
13312
+ } else if (cmd === "neighbors") {
13313
+ if (!flags2.positional) throw new Error("neighbors needs a target: cli.mjs neighbors <file|module> --repo <dir>");
13314
+ const { graph } = readArtifacts();
13315
+ const kinds = flags2.kind ? new Set(flags2.kind.split(",").map((k) => k.trim()).filter(Boolean)) : void 0;
13316
+ const res = neighborsOf(graph, flags2.positional, flags2.depth ?? 1, kinds);
13317
+ if (!res) throw new Error(`no such file or module in the index: ${flags2.positional}`);
13318
+ emit(JSON.stringify(res, null, 2) + "\n", flags2.out);
12297
13319
  } else if (cmd === "mermaid") {
12298
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13320
+ const { graph } = readArtifacts();
12299
13321
  emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
12300
13322
  } else if (cmd === "grep") {
12301
13323
  if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
@@ -12315,12 +13337,15 @@ ${HELP}`);
12315
13337
  }
12316
13338
  }
12317
13339
  export {
13340
+ DEFAULT_DELTA_DEPTH,
12318
13341
  DEFAULT_GRAMMARS_URL,
12319
13342
  DEFAULT_MAX_FILES,
12320
13343
  EMBED_VERSION,
12321
13344
  ENGINE_VERSION,
12322
13345
  EXTRACTOR_VERSION,
13346
+ INDEX_DIR,
12323
13347
  MARKDOWN_EXT,
13348
+ RISK_WEIGHTS,
12324
13349
  SCHEMA_VERSION,
12325
13350
  allGrammarKeys,
12326
13351
  applyCentrality,
@@ -12328,6 +13353,7 @@ export {
12328
13353
  betweennessOf,
12329
13354
  buildArtifactsFromScan,
12330
13355
  buildCallerIndex,
13356
+ buildCodeRecord,
12331
13357
  buildEmbeddingIndex,
12332
13358
  buildEndpointIndex,
12333
13359
  buildGraph,
@@ -12348,11 +13374,13 @@ export {
12348
13374
  communityOf,
12349
13375
  compileGlobs,
12350
13376
  complexityOfSource,
13377
+ computeDelta,
12351
13378
  computeImportPairs,
12352
13379
  computeSurprises,
12353
13380
  computeSymbolRefs,
12354
13381
  computeTestMap,
12355
13382
  deleteMemory,
13383
+ deltaFor,
12356
13384
  deserializeEmbeddings,
12357
13385
  detectCommunities,
12358
13386
  detectWorkspaces,
@@ -12370,6 +13398,7 @@ export {
12370
13398
  extractAst,
12371
13399
  extractCode,
12372
13400
  extractGrammarsTarball,
13401
+ extractInParallel,
12373
13402
  extractMarkdown,
12374
13403
  extractSymbols,
12375
13404
  extractTarInto,
@@ -12379,6 +13408,7 @@ export {
12379
13408
  findReferences,
12380
13409
  findSymbol,
12381
13410
  foldText,
13411
+ formatDeltaPanel,
12382
13412
  gitChurn,
12383
13413
  grammarKeyForExt,
12384
13414
  grammarKeysForExts,
@@ -12388,6 +13418,8 @@ export {
12388
13418
  have,
12389
13419
  headCommit,
12390
13420
  healthzUrl,
13421
+ hubThreshold,
13422
+ impactOf,
12391
13423
  insertAfterSymbol,
12392
13424
  insertBeforeSymbol,
12393
13425
  intDot,
@@ -12398,22 +13430,28 @@ export {
12398
13430
  isSurprising,
12399
13431
  isTestFile,
12400
13432
  isTestPath,
13433
+ keptCodeFiles,
12401
13434
  keywords,
12402
13435
  languageOf,
12403
13436
  listMemories,
12404
13437
  loadEmbedModel,
13438
+ neighborsOf,
12405
13439
  pagerankOf,
12406
13440
  parseGitignore,
12407
13441
  parseRules,
13442
+ preloadArtifacts,
13443
+ preloadSession,
12408
13444
  probeEndpoint,
12409
13445
  pullGrammars,
12410
13446
  quantize,
12411
13447
  rankHotspots,
12412
13448
  rankedKeywords,
12413
13449
  readMemory,
13450
+ readPersistedIndex,
12414
13451
  readText,
12415
13452
  renderGraphJson,
12416
13453
  renderMermaid,
13454
+ renderMermaidClustered,
12417
13455
  renderRepoMap,
12418
13456
  renderScip,
12419
13457
  renderSymbolsJson,
@@ -12429,13 +13467,17 @@ export {
12429
13467
  resolveGrammarsTier,
12430
13468
  resolveImport,
12431
13469
  resolveUniqueSymbol,
13470
+ reverseClosure,
12432
13471
  rewriteCommand,
12433
13472
  riskHotspots,
12434
13473
  roundHalfToEven,
12435
13474
  rrf,
12436
13475
  runCli,
13476
+ runExtractWorker,
12437
13477
  runMcpServer,
12438
13478
  scanRepo,
13479
+ scanRepoParallel,
13480
+ scanSummary,
12439
13481
  searchIndex,
12440
13482
  searchSemantic,
12441
13483
  serializeEmbeddings,
@@ -12446,9 +13488,11 @@ export {
12446
13488
  slugify,
12447
13489
  subtokens,
12448
13490
  symbolComplexity,
13491
+ symbolsInHunks,
12449
13492
  symbolsOverview,
12450
13493
  testsForModule,
12451
13494
  tierForPath,
13495
+ toCacheMap,
12452
13496
  tokenize,
12453
13497
  uniqueSymbolDefs,
12454
13498
  untestedModules,
@@ -12456,6 +13500,7 @@ export {
12456
13500
  walk,
12457
13501
  warmGrammars,
12458
13502
  wordpiece,
13503
+ workerCount,
12459
13504
  writeMemory
12460
13505
  };
12461
13506
  // "Copyright" and "@license" are already caught by DIRECTIVE_RE.