@maxgfr/codeindex 2.17.1 → 2.19.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.1";
17
+ ENGINE_VERSION = "2.19.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();
8071
- for (const f of scan2.files) {
8072
- const seen = /* @__PURE__ */ new Set();
8073
- for (const s of f.symbols) {
8074
- if (!s.exported || REFERENCE_KINDS3.has(s.kind)) continue;
8075
- if (seen.has(s.name)) continue;
8076
- seen.add(s.name);
8077
- let arr = defs.get(s.name);
8078
- if (!arr) defs.set(s.name, arr = []);
8079
- arr.push(s);
8080
- }
8081
- }
8082
- const localDefs = /* @__PURE__ */ new Map();
8134
+ function uniqueSymbolDefs(scan2) {
8135
+ const byName = /* @__PURE__ */ new Map();
8083
8136
  for (const f of scan2.files) {
8084
- const byName = /* @__PURE__ */ new Map();
8085
8137
  for (const s of f.symbols) {
8086
- if (!REFERENCE_KINDS3.has(s.kind) && !byName.has(s.name)) byName.set(s.name, 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);
8087
8142
  }
8088
- localDefs.set(f.rel, byName);
8089
8143
  }
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
- };
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;
8154
+ }
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;
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
+ };
8172
8298
  }
8173
- var REFERENCE_KINDS3;
8174
- var init_callers = __esm({
8175
- "src/callers.ts"() {
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,487 +10415,216 @@ 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
- // src/mcp.ts
10295
- var mcp_exports = {};
10296
- __export(mcp_exports, {
10297
- getArtifacts: () => getArtifacts,
10298
- getScan: () => getScan,
10299
- memoizedEmbedModel: () => memoizedEmbedModel,
10300
- memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
10301
- runMcpServer: () => runMcpServer,
10302
- scanFingerprint: () => scanFingerprint,
10303
- toCacheMap: () => toCacheMap,
10304
- warmGrammarsForRepo: () => warmGrammarsForRepo
10305
- });
10306
- import { readFileSync as readFileSync7, statSync as statSync4 } from "fs";
10307
- import { join as join14 } from "path";
10308
- import { createInterface } from "readline";
10309
- function str(v) {
10310
- return typeof v === "string" && v ? v : void 0;
10468
+ // src/mcp/protocol.ts
10469
+ import { existsSync as existsSync6 } from "fs";
10470
+ import { join as join16 } from "path";
10471
+ import { pathToFileURL as pathToFileURL2 } from "url";
10472
+ function validateArgs(schema, args2) {
10473
+ const props = schema.properties ?? {};
10474
+ for (const [key, value] of Object.entries(args2)) {
10475
+ if (value === void 0 || value === null) continue;
10476
+ const spec = props[key];
10477
+ if (!spec?.type) continue;
10478
+ const actual = Array.isArray(value) ? "array" : typeof value;
10479
+ if (spec.type === "number") {
10480
+ if (actual === "number") continue;
10481
+ if (actual === "string" && Number.isFinite(Number(value)) && value.trim() !== "") continue;
10482
+ return `\`${key}\` must be a number, got ${actual === "string" ? JSON.stringify(value) : actual}`;
10483
+ }
10484
+ if (spec.type === "array") {
10485
+ if (actual !== "array") return `\`${key}\` must be an array of strings, got ${actual}`;
10486
+ if (spec.items?.type === "string" && !value.every((x) => typeof x === "string")) {
10487
+ return `\`${key}\` must be an array of strings`;
10488
+ }
10489
+ continue;
10490
+ }
10491
+ if (actual !== spec.type) return `\`${key}\` must be a ${spec.type}, got ${actual}`;
10492
+ }
10493
+ return void 0;
10311
10494
  }
10312
- function strArray(v) {
10313
- return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
10314
- }
10315
- function errMessage(e) {
10316
- return e instanceof Error ? e.message : String(e);
10317
- }
10318
- function scanFingerprint(scan2) {
10319
- return sha1(scan2.files.map((f) => `${f.rel}:${f.hash}`).join("\n"));
10320
- }
10321
- async function memoizedEmbeddingIndex(key, build) {
10322
- const cacheKey = `${key.mode}:${key.identity}:${scanFingerprint(key.scan)}`;
10323
- if (embeddingIndexCache && embeddingIndexCache.key === cacheKey) return embeddingIndexCache.index;
10324
- const index = await build();
10325
- embeddingIndexCache = { key: cacheKey, index };
10326
- return index;
10327
- }
10328
- function memoizedEmbedModel(modelDir) {
10329
- let stat;
10495
+ function structuredContentFor(text, capped, hasSchema) {
10496
+ if (capped || !hasSchema) return void 0;
10497
+ let parsed;
10330
10498
  try {
10331
- stat = statSync4(join14(modelDir, "model.json"));
10499
+ parsed = JSON.parse(text);
10332
10500
  } catch {
10333
10501
  return void 0;
10334
10502
  }
10335
- const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
10336
- if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
10337
- const model = loadEmbedModel(modelDir);
10338
- if (model) embedModelCache = { key, model };
10339
- return model;
10340
- }
10341
- function sessionKey(repo, opts) {
10342
- return repo + "\0" + JSON.stringify({
10343
- scope: opts.scope,
10344
- include: opts.include,
10345
- exclude: opts.exclude,
10346
- gitignore: opts.gitignore,
10347
- ignoreDirs: opts.ignoreDirs,
10348
- maxBytes: opts.maxBytes,
10349
- maxFiles: opts.maxFiles,
10350
- maxCallsPerFile: opts.maxCallsPerFile,
10351
- out: opts.out,
10352
- fullHash: opts.fullHash
10353
- });
10503
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
10504
+ return parsed;
10354
10505
  }
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;
10506
+ function negotiateProtocol(requested) {
10507
+ return typeof requested === "string" && PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL;
10359
10508
  }
10360
- function readPersistedIndex(repo) {
10509
+ function capResponse(text, tool, repo, maxBytes) {
10510
+ const bytes = Buffer.byteLength(text, "utf8");
10511
+ if (bytes <= maxBytes) return text;
10512
+ const artifact = ARTIFACT_FOR[tool] ? join16(repo, INDEX_DIR, ARTIFACT_FOR[tool]) : void 0;
10513
+ return JSON.stringify(
10514
+ {
10515
+ truncated: true,
10516
+ tool,
10517
+ bytes,
10518
+ maxBytes,
10519
+ reason: "This response exceeds the configured limit and was withheld rather than sent as an unusable partial payload.",
10520
+ narrower: NARROWER[tool] ?? "narrow the request with `scope`, `include`/`exclude`, or a `limit`",
10521
+ ...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.` } : {}
10522
+ },
10523
+ null,
10524
+ 2
10525
+ ) + "\n";
10526
+ }
10527
+ function resourceLinkFor(text, tool) {
10528
+ const artifactName = ARTIFACT_FOR[tool];
10529
+ if (!artifactName) return void 0;
10361
10530
  let parsed;
10362
10531
  try {
10363
- parsed = JSON.parse(readFileSync7(join14(repo, ".codeindex", "cache.json"), "utf8"));
10532
+ parsed = JSON.parse(text);
10364
10533
  } catch {
10365
10534
  return void 0;
10366
10535
  }
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
10536
+ if (parsed.truncated !== true || typeof parsed.artifact !== "string") return void 0;
10537
+ return {
10538
+ type: "resource_link",
10539
+ uri: pathToFileURL2(parsed.artifact).href,
10540
+ name: artifactName,
10541
+ description: `The full ${tool} result this call was too large to inline.`,
10542
+ mimeType: "application/json"
10376
10543
  };
10377
- return { cacheMap, meta };
10378
10544
  }
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;
10545
+ var PROTOCOL_VERSIONS, LATEST_PROTOCOL, ANNOTATIONS_SINCE, RICH_TOOLS_SINCE, DEFAULT_MAX_RESPONSE_BYTES, NARROWER, ARTIFACT_FOR;
10546
+ var init_protocol = __esm({
10547
+ "src/mcp/protocol.ts"() {
10548
+ "use strict";
10549
+ init_preload();
10550
+ PROTOCOL_VERSIONS = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
10551
+ LATEST_PROTOCOL = PROTOCOL_VERSIONS[PROTOCOL_VERSIONS.length - 1];
10552
+ ANNOTATIONS_SINCE = "2025-03-26";
10553
+ RICH_TOOLS_SINCE = "2025-06-18";
10554
+ DEFAULT_MAX_RESPONSE_BYTES = 1e6;
10555
+ NARROWER = {
10556
+ graph: "pass `scope` to a subdirectory, or use repo_map / mermaid for an overview",
10557
+ symbols: "pass `name` to look up one symbol, or use find_symbol / symbols_overview",
10558
+ callers: "pass `name` to look up one symbol's call sites",
10559
+ dead_code: "pass `scope` to a subdirectory",
10560
+ find_references: "the symbol is referenced very widely \u2014 narrow with `scope` on a graph query",
10561
+ check_rules: "narrow the rule set, or pass `scope` to a subdirectory"
10562
+ };
10563
+ ARTIFACT_FOR = { graph: "graph.json", symbols: "symbols.json" };
10402
10564
  }
10565
+ });
10566
+
10567
+ // src/mcp/tools.ts
10568
+ function annotationsFor(name2) {
10569
+ const meta = TOOL_META[name2];
10570
+ if (!meta) return void 0;
10571
+ return {
10572
+ readOnlyHint: !meta.write,
10573
+ ...meta.write ? { destructiveHint: meta.destructive === true, idempotentHint: meta.idempotent === true } : {},
10574
+ openWorldHint: meta.openWorld === true
10575
+ };
10403
10576
  }
10404
- function preloadSession(repo, opts) {
10405
- const persisted = readPersistedIndex(repo);
10406
- if (!persisted) return void 0;
10407
- const scan2 = scanRepo(repo, { ...opts, cache: persisted.cacheMap });
10408
- const arts = preloadArtifacts(repo, scan2, persisted.meta);
10409
- return { scan: scan2, cacheMap: toCacheMap(scan2), arts };
10410
- }
10411
- function getScan(repo, opts = {}) {
10412
- const key = sessionKey(repo, opts);
10413
- if (sessionCache && sessionCache.key === key) {
10414
- const fresh = scanRepo(repo, { ...opts, cache: sessionCache.cacheMap });
10415
- if (fresh.contentUnchanged) {
10416
- if (fresh.cacheDirty) sessionCache.cacheMap = toCacheMap(fresh);
10417
- if (sessionCache.scan.commit !== fresh.commit) sessionCache.scan.commit = fresh.commit;
10418
- return sessionCache.scan;
10577
+ function toolsFor(defaultRepo, protocolVersion = PROTOCOL_VERSIONS[0]) {
10578
+ const withAnnotations = protocolVersion >= ANNOTATIONS_SINCE;
10579
+ const withRich = protocolVersion >= RICH_TOOLS_SINCE;
10580
+ if (!defaultRepo && !withAnnotations && !withRich) return TOOLS;
10581
+ return TOOLS.map((t) => ({
10582
+ ...t,
10583
+ ...withRich && TOOL_META[t.name] ? { title: TOOL_META[t.name].title } : {},
10584
+ ...withRich && OUTPUT_SCHEMAS[t.name] ? { outputSchema: OUTPUT_SCHEMAS[t.name] } : {},
10585
+ ...withAnnotations ? { annotations: annotationsFor(t.name) } : {},
10586
+ inputSchema: !defaultRepo ? t.inputSchema : {
10587
+ ...t.inputSchema,
10588
+ properties: {
10589
+ ...t.inputSchema.properties,
10590
+ repo: {
10591
+ type: "string",
10592
+ description: `Absolute path to the repository root (optional \u2014 defaults to ${defaultRepo})`
10593
+ }
10594
+ },
10595
+ required: t.inputSchema.required.filter((r) => r !== "repo")
10419
10596
  }
10420
- sessionCache = { key, scan: fresh, cacheMap: toCacheMap(fresh) };
10421
- return fresh;
10422
- }
10423
- const preloaded = preloadSession(repo, opts);
10424
- if (preloaded) {
10425
- sessionCache = { key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts };
10426
- return preloaded.scan;
10427
- }
10428
- const scan2 = scanRepo(repo, opts);
10429
- sessionCache = { key, scan: scan2, cacheMap: toCacheMap(scan2) };
10430
- return scan2;
10431
- }
10432
- function getArtifacts(repo, opts = {}) {
10433
- const scan2 = getScan(repo, opts);
10434
- if (sessionCache && sessionCache.scan === scan2) {
10435
- return sessionCache.arts ??= buildArtifactsFromScan(scan2, opts);
10436
- }
10437
- return buildArtifactsFromScan(scan2, opts);
10438
- }
10439
- async function warmGrammarsForRepo(repo) {
10440
- const { files } = walk(repo, {});
10441
- await ensureGrammars(grammarKeysForExts(files.map((f) => f.ext)));
10597
+ }));
10442
10598
  }
10443
- async function callTool(name2, args2, defaultRepo) {
10444
- const repo = str(args2.repo) ?? defaultRepo;
10445
- if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
10446
- const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
10447
- if (!SCANLESS_TOOLS.has(name2)) await warmGrammarsForRepo(repo);
10448
- if (name2 === "scan_summary") {
10449
- const scan2 = getScan(repo, scanOpts);
10450
- return JSON.stringify(
10451
- { engineVersion: ENGINE_VERSION, commit: scan2.commit, fileCount: scan2.files.length, languages: scan2.languages, capped: scan2.capped },
10452
- null,
10453
- 2
10454
- );
10455
- }
10456
- if (name2 === "graph") {
10457
- return renderGraphJson(getArtifacts(repo, scanOpts).graph);
10458
- }
10459
- if (name2 === "symbols") {
10460
- const { symbols } = getArtifacts(repo, scanOpts);
10461
- const lookup = str(args2.name);
10462
- if (lookup) {
10463
- return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
10464
- }
10465
- return JSON.stringify(symbols, null, 2);
10466
- }
10467
- if (name2 === "callers") {
10468
- const index = buildCallerIndex(getScan(repo, scanOpts));
10469
- const lookup = str(args2.name);
10470
- if (lookup) {
10471
- const entry = index.get(lookup);
10472
- return JSON.stringify(entry ?? { error: `no tracked callers for "${lookup}"` }, null, 2);
10473
- }
10474
- const obj = {};
10475
- for (const [k, v] of index) obj[k] = v;
10476
- return JSON.stringify(obj, null, 2);
10477
- }
10478
- if (name2 === "workspaces") {
10479
- const info2 = detectWorkspaces(repo);
10480
- return JSON.stringify({ packages: info2.packages, cycle: info2.cycle ?? null, topoOrder: info2.topoOrder }, null, 2);
10481
- }
10482
- if (name2 === "churn") {
10483
- const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10484
- const sorted = {};
10485
- for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
10486
- return JSON.stringify({ ok, churn: sorted }, null, 2);
10487
- }
10488
- if (name2 === "symbols_overview") {
10489
- const file = str(args2.file);
10490
- if (!file) throw new Error("`file` is required");
10491
- return JSON.stringify(symbolsOverview(getScan(repo, scanOpts), file), null, 2);
10492
- }
10493
- if (name2 === "find_symbol") {
10494
- const namePath = str(args2.namePath);
10495
- if (!namePath) throw new Error("`namePath` is required");
10496
- const matches = findSymbol(getScan(repo, scanOpts), namePath, {
10497
- substring: args2.substring === true,
10498
- includeBody: args2.includeBody === true
10499
- });
10500
- return JSON.stringify(matches, null, 2);
10501
- }
10502
- if (name2 === "find_references") {
10503
- const symName = str(args2.name);
10504
- if (!symName) throw new Error("`name` is required");
10505
- return JSON.stringify(findReferences(getScan(repo, scanOpts), symName), null, 2);
10506
- }
10507
- if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
10508
- const namePath = str(args2.namePath);
10509
- const body2 = typeof args2.body === "string" ? args2.body : void 0;
10510
- if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
10511
- const scan2 = getScan(repo, scanOpts);
10512
- const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
10513
- const result = fn(scan2, namePath, body2, str(args2.file));
10514
- sessionCache = void 0;
10515
- return JSON.stringify(result, null, 2);
10516
- }
10517
- if (name2 === "write_memory") {
10518
- const memName = str(args2.name);
10519
- const content = typeof args2.content === "string" ? args2.content : void 0;
10520
- if (!memName || content === void 0) throw new Error("`name` and `content` are required");
10521
- return JSON.stringify({ written: writeMemory(repo, memName, content) }, null, 2);
10522
- }
10523
- if (name2 === "read_memory") {
10524
- const memName = str(args2.name);
10525
- if (!memName) throw new Error("`name` is required");
10526
- const content = readMemory(repo, memName);
10527
- if (content === void 0) throw new Error(`no memory named "${memName}" \u2014 see list_memories`);
10528
- return content;
10529
- }
10530
- if (name2 === "list_memories") {
10531
- return JSON.stringify(listMemories(repo), null, 2);
10532
- }
10533
- if (name2 === "delete_memory") {
10534
- const memName = str(args2.name);
10535
- if (!memName) throw new Error("`name` is required");
10536
- return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
10537
- }
10538
- if (name2 === "dead_code") {
10539
- return JSON.stringify(findDeadCode(getScan(repo, scanOpts)), null, 2);
10540
- }
10541
- if (name2 === "complexity") {
10542
- const scan2 = getScan(repo, scanOpts);
10543
- if (args2.risk === true) {
10544
- const { churn, ok } = gitChurn(repo);
10545
- return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2);
10546
- }
10547
- return JSON.stringify(symbolComplexity(scan2, str(args2.file)), null, 2);
10548
- }
10549
- if (name2 === "mermaid") {
10550
- const { graph } = getArtifacts(repo, scanOpts);
10551
- return renderMermaid(graph, { module: str(args2.module) });
10552
- }
10553
- if (name2 === "repo_map") {
10554
- const { scan: scan2, graph } = getArtifacts(repo, scanOpts);
10555
- return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
10556
- }
10557
- if (name2 === "hotspots") {
10558
- const scan2 = getScan(repo, scanOpts);
10559
- const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
10560
- return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
10561
- }
10562
- if (name2 === "coupling") {
10563
- const { ok, couplings } = changeCoupling(repo, { since: str(args2.since) });
10564
- return JSON.stringify({ ok, couplings }, null, 2);
10565
- }
10566
- if (name2 === "grep") {
10567
- const pattern = str(args2.pattern);
10568
- if (!pattern) throw new Error("`pattern` is required");
10569
- const hits = grepRepo(repo, pattern, {
10570
- globs: strArray(args2.globs),
10571
- ignoreCase: args2.ignoreCase === true,
10572
- maxHits: typeof args2.maxHits === "number" ? args2.maxHits : void 0
10573
- });
10574
- return JSON.stringify(hits, null, 2);
10575
- }
10576
- if (name2 === "search") {
10577
- const query = str(args2.query);
10578
- if (!query) throw new Error("`query` is required");
10579
- const scan2 = getScan(repo, scanOpts);
10580
- const limit = typeof args2.limit === "number" ? args2.limit : void 0;
10581
- const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
10582
- if (args2.semantic === true) {
10583
- const endpoint = resolveEmbedEndpoint();
10584
- if (endpoint) {
10585
- try {
10586
- const index = await memoizedEmbeddingIndex({ mode: "endpoint", identity: endpoint, scan: scan2 }, () => buildEndpointIndex(scan2));
10587
- const queryVec = await encodeQueryViaEndpoint(query);
10588
- const results2 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
10589
- return JSON.stringify({ results: results2, tier: "endpoint" }, null, 2);
10590
- } catch (e) {
10591
- const results2 = searchIndex(scan2, query, { limit, fuzzy });
10592
- return JSON.stringify(
10593
- { results: results2, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
10594
- null,
10595
- 2
10596
- );
10597
- }
10598
- }
10599
- const modelDir = resolveEmbedModelDir(repo);
10600
- const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
10601
- if (model) {
10602
- const index = await memoizedEmbeddingIndex(
10603
- { mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
10604
- () => buildEmbeddingIndex(scan2, model)
10605
- );
10606
- const results2 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
10607
- return JSON.stringify({ results: results2, tier: "static" }, null, 2);
10608
- }
10609
- const results = searchIndex(scan2, query, { limit, fuzzy });
10610
- return JSON.stringify(
10611
- { results, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
10612
- null,
10613
- 2
10614
- );
10615
- }
10616
- return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
10617
- }
10618
- if (name2 === "embed_status") {
10619
- const modelDir = resolveEmbedModelDir(repo);
10620
- const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
10621
- const endpoint = resolveEmbedEndpoint();
10622
- const mode = endpoint ? "endpoint" : model ? "static" : "none";
10623
- const status = {
10624
- embedVersion: EMBED_VERSION,
10625
- mode,
10626
- model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
10627
- endpoint: endpoint ?? null
10628
- };
10629
- if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
10630
- return JSON.stringify(status, null, 2);
10631
- }
10632
- if (name2 === "check_rules") {
10633
- const rules = parseRules(args2.rules);
10634
- const { graph } = getArtifacts(repo, scanOpts);
10635
- return JSON.stringify(checkRules(graph, rules), null, 2);
10636
- }
10637
- throw new Error(`unknown tool: ${name2}`);
10638
- }
10639
- function toolsFor(defaultRepo) {
10640
- if (!defaultRepo) return TOOLS;
10641
- return TOOLS.map((t) => ({
10642
- ...t,
10643
- inputSchema: {
10644
- ...t.inputSchema,
10645
- properties: {
10646
- ...t.inputSchema.properties,
10647
- repo: { type: "string", description: `Absolute path to the repository root (optional \u2014 defaults to ${defaultRepo})` }
10648
- },
10649
- required: t.inputSchema.required.filter((r) => r !== "repo")
10650
- }
10651
- }));
10652
- }
10653
- async function runMcpServer(opts = {}) {
10654
- const serverInfo = {
10655
- name: opts.serverInfo?.name ?? "codeindex",
10656
- version: opts.serverInfo?.version ?? ENGINE_VERSION
10657
- };
10658
- const tools = toolsFor(opts.defaultRepo);
10659
- const send = (msg) => {
10660
- process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
10661
- };
10662
- const rl = createInterface({ input: process.stdin, terminal: false });
10663
- for await (const line of rl) {
10664
- const trimmed = line.trim();
10665
- if (!trimmed) continue;
10666
- let parsed;
10667
- try {
10668
- parsed = JSON.parse(trimmed);
10669
- } catch {
10670
- send({ id: null, error: { code: -32700, message: "parse error" } });
10671
- continue;
10672
- }
10673
- const requests = Array.isArray(parsed) ? parsed : [parsed];
10674
- for (const req of requests) await handle2(req);
10675
- }
10676
- async function handle2(req) {
10677
- if (req.id === void 0 || req.id === null) return;
10678
- try {
10679
- if (req.method === "initialize") {
10680
- send({
10681
- id: req.id,
10682
- result: {
10683
- protocolVersion: "2024-11-05",
10684
- capabilities: { tools: {} },
10685
- serverInfo
10686
- }
10687
- });
10688
- } else if (req.method === "ping") {
10689
- send({ id: req.id, result: {} });
10690
- } else if (req.method === "tools/list") {
10691
- send({ id: req.id, result: { tools } });
10692
- } else if (req.method === "tools/call") {
10693
- const params = req.params ?? {};
10694
- const name2 = str(params.name) ?? "";
10695
- const args2 = params.arguments ?? {};
10696
- try {
10697
- const text = await callTool(name2, args2, opts.defaultRepo);
10698
- send({ id: req.id, result: { content: [{ type: "text", text }] } });
10699
- } catch (e) {
10700
- send({
10701
- id: req.id,
10702
- result: { content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }], isError: true }
10703
- });
10704
- }
10705
- } else {
10706
- send({ id: req.id, error: { code: -32601, message: `method not found: ${req.method}` } });
10707
- }
10708
- } catch (e) {
10709
- send({ id: req.id, error: { code: -32603, message: e instanceof Error ? e.message : String(e) } });
10710
- }
10711
- }
10712
- }
10713
- var repoProp, scopeProps, TOOLS, embeddingIndexCache, embedModelCache, sessionCache, SCANLESS_TOOLS;
10714
- var init_mcp = __esm({
10715
- "src/mcp.ts"() {
10716
- "use strict";
10717
- init_types();
10718
- init_loader();
10719
- init_pipeline();
10720
- init_graph_json();
10721
- init_scan();
10722
- init_walk();
10723
- init_callers();
10724
- init_workspaces();
10725
- init_git();
10726
- init_grep();
10727
- init_coupling();
10728
- init_repomap();
10729
- init_deadcode();
10730
- init_complexity();
10731
- init_viz();
10732
- init_query();
10733
- init_edit();
10734
- init_memory();
10735
- init_bm25();
10736
- init_rules();
10737
- init_model();
10738
- init_embed();
10739
- init_search();
10740
- init_endpoint();
10741
- init_hash();
10742
- repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
10743
- scopeProps = {
10744
- scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
10745
- include: { type: "array", items: { type: "string" }, description: "Include globs" },
10746
- exclude: { type: "array", items: { type: "string" }, description: "Exclude globs" }
10747
- };
10748
- TOOLS = [
10749
- {
10750
- name: "scan_summary",
10751
- description: "Deterministically scan a repository: file count, per-language file histogram, HEAD commit, and whether the walk was capped. Fast first look at any codebase.",
10752
- inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] }
10753
- },
10754
- {
10755
- name: "graph",
10756
- description: "Build the full typed cross-file link-graph (import/call/use/doc-link/mention edges, module grouping, PageRank centrality, Louvain communities, tests-map). Returns graph.json. Large on big repos \u2014 prefer scan_summary/symbols/callers for targeted questions.",
10757
- inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] }
10758
- },
10759
- {
10760
- name: "symbols",
10761
- description: "Where is a symbol defined and which files reference it? Returns the definition sites (file, line, kind, exported) and referencing files. Omit `name` for the full symbol index.",
10762
- inputSchema: {
10763
- type: "object",
10764
- properties: { ...repoProp, name: { type: "string", description: "Symbol name to look up" } },
10765
- required: ["repo"]
10599
+ var repoProp, scopeProps, TOOLS, strArr, anyObj, OUTPUT_SCHEMAS, TOOL_META;
10600
+ var init_tools = __esm({
10601
+ "src/mcp/tools.ts"() {
10602
+ "use strict";
10603
+ init_protocol();
10604
+ repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
10605
+ scopeProps = {
10606
+ scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
10607
+ include: { type: "array", items: { type: "string" }, description: "Include globs" },
10608
+ exclude: { type: "array", items: { type: "string" }, description: "Exclude globs" }
10609
+ };
10610
+ TOOLS = [
10611
+ {
10612
+ name: "scan_summary",
10613
+ description: "Deterministically scan a repository: file count, per-language file histogram, HEAD commit, and whether the walk was capped. Fast first look at any codebase.",
10614
+ inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] }
10615
+ },
10616
+ {
10617
+ name: "graph",
10618
+ description: "Build the full typed cross-file link-graph (import/call/use/doc-link/mention edges, module grouping, PageRank centrality, Louvain communities, tests-map). Returns graph.json. Large on big repos \u2014 prefer scan_summary/symbols/callers for targeted questions.",
10619
+ inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] }
10620
+ },
10621
+ {
10622
+ name: "symbols",
10623
+ description: "Where is a symbol defined and which files reference it? Returns the definition sites (file, line, kind, exported) and referencing files. Omit `name` for the full symbol index.",
10624
+ inputSchema: {
10625
+ type: "object",
10626
+ properties: { ...repoProp, name: { type: "string", description: "Symbol name to look up" } },
10627
+ required: ["repo"]
10766
10628
  }
10767
10629
  },
10768
10630
  {
@@ -10770,7 +10632,14 @@ var init_mcp = __esm({
10770
10632
  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
10633
  inputSchema: {
10772
10634
  type: "object",
10773
- properties: { ...repoProp, name: { type: "string", description: "Symbol name to look up" } },
10635
+ properties: {
10636
+ ...repoProp,
10637
+ name: { type: "string", description: "Symbol name to look up" },
10638
+ recall: {
10639
+ type: "boolean",
10640
+ description: "Recall-oriented binding: relax the JS/TS import gate to unique repo-wide names, labelling each site corroborated|unique-name (default false = precision)"
10641
+ }
10642
+ },
10774
10643
  required: ["repo"]
10775
10644
  }
10776
10645
  },
@@ -10806,7 +10675,8 @@ var init_mcp = __esm({
10806
10675
  ...repoProp,
10807
10676
  namePath: { type: "string", description: "Symbol name or Parent/child path" },
10808
10677
  substring: { type: "boolean" },
10809
- includeBody: { type: "boolean" }
10678
+ includeBody: { type: "boolean" },
10679
+ maxResults: { type: "number", description: "Cap matches (default 50)" }
10810
10680
  },
10811
10681
  required: ["repo", "namePath"]
10812
10682
  }
@@ -10913,8 +10783,16 @@ var init_mcp = __esm({
10913
10783
  },
10914
10784
  {
10915
10785
  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"] }
10786
+ 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.",
10787
+ inputSchema: {
10788
+ type: "object",
10789
+ properties: {
10790
+ ...repoProp,
10791
+ ...scopeProps,
10792
+ limit: { type: "number", description: "Cap entries (default: all)" }
10793
+ },
10794
+ required: ["repo"]
10795
+ }
10918
10796
  },
10919
10797
  {
10920
10798
  name: "complexity",
@@ -10942,6 +10820,7 @@ var init_mcp = __esm({
10942
10820
  properties: {
10943
10821
  ...repoProp,
10944
10822
  pattern: { type: "string", description: "Regular expression to search for" },
10823
+ scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
10945
10824
  globs: { type: "array", items: { type: "string" }, description: "Restrict to matching paths" },
10946
10825
  ignoreCase: { type: "boolean" },
10947
10826
  maxHits: { type: "number" }
@@ -10967,29 +10846,675 @@ var init_mcp = __esm({
10967
10846
  type: "boolean",
10968
10847
  description: 'RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. The response reports the effective tier as a top-level `tier` field ("endpoint"/"static" on success, "lexical" plus `degradedReason` when neither is available/reachable) instead of degrading silently \u2014 see embed_status.'
10969
10848
  }
10970
- },
10971
- required: ["repo", "query"]
10972
- }
10973
- },
10974
- {
10975
- name: "embed_status",
10976
- description: "Report the embedding tier: the effective mode (none/static/endpoint; endpoint > static model), the resolved model (opt-in, never shipped in the package) with its modelId/dim, EMBED_VERSION, and the configured HTTP endpoint with its reachability. Use to check whether `search` with semantic:true will fuse embeddings or degrade to lexical.",
10977
- inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
10978
- },
10979
- {
10980
- name: "check_rules",
10981
- description: 'Validate dependency-cruiser-style architecture rules against the link-graph. Rules (inline JSON array): forbidden edges {name, from, to, kind?, severity?, comment?} with glob paths, plus builtins {name, builtin: "cycles"|"orphans"} (module-level import cycles; edge-less code files). Returns deterministic violations with severity error|warn \u2014 a CI gate.',
10982
- inputSchema: {
10983
- type: "object",
10984
- properties: {
10985
- ...repoProp,
10986
- ...scopeProps,
10987
- rules: { type: "array", description: "Rules array (inline JSON \u2014 see description)" }
10988
- },
10989
- required: ["repo", "rules"]
10849
+ },
10850
+ required: ["repo", "query"]
10851
+ }
10852
+ },
10853
+ {
10854
+ name: "embed_status",
10855
+ description: "Report the embedding tier: the effective mode (none/static/endpoint; endpoint > static model), the resolved model (opt-in, never shipped in the package) with its modelId/dim, EMBED_VERSION, and the configured HTTP endpoint with its reachability. Use to check whether `search` with semantic:true will fuse embeddings or degrade to lexical.",
10856
+ inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] }
10857
+ },
10858
+ {
10859
+ name: "check_rules",
10860
+ description: 'Validate dependency-cruiser-style architecture rules against the link-graph. Rules (inline JSON array): forbidden edges {name, from, to, kind?, severity?, comment?} with glob paths, plus builtins {name, builtin: "cycles"|"orphans"} (module-level import cycles; edge-less code files). Returns deterministic violations with severity error|warn \u2014 a CI gate.',
10861
+ inputSchema: {
10862
+ type: "object",
10863
+ properties: {
10864
+ ...repoProp,
10865
+ ...scopeProps,
10866
+ rules: { type: "array", description: "Rules array (inline JSON \u2014 see description)" },
10867
+ configPath: {
10868
+ type: "string",
10869
+ description: "Read the rules from this JSON file instead (repo-relative or absolute) \u2014 the CLI's --config. Ignored when `rules` is given."
10870
+ }
10871
+ },
10872
+ required: ["repo"]
10873
+ }
10874
+ }
10875
+ ];
10876
+ strArr = { type: "array", items: { type: "string" } };
10877
+ anyObj = { type: "object" };
10878
+ OUTPUT_SCHEMAS = {
10879
+ scan_summary: {
10880
+ type: "object",
10881
+ properties: {
10882
+ engineVersion: { type: "string" },
10883
+ commit: { type: "string" },
10884
+ fileCount: { type: "integer" },
10885
+ languages: { type: "object", additionalProperties: { type: "integer" } },
10886
+ capped: { type: "boolean" }
10887
+ },
10888
+ required: ["engineVersion", "fileCount", "languages", "capped"]
10889
+ },
10890
+ graph: {
10891
+ type: "object",
10892
+ properties: {
10893
+ schemaVersion: { type: "integer" },
10894
+ version: { type: "string" },
10895
+ commit: { type: "string" },
10896
+ fileCount: { type: "integer" },
10897
+ languages: { type: "object", additionalProperties: { type: "integer" } },
10898
+ files: { type: "array", items: anyObj },
10899
+ modules: { type: "array", items: anyObj },
10900
+ fileEdges: { type: "array", items: anyObj },
10901
+ moduleEdges: { type: "array", items: anyObj }
10902
+ },
10903
+ required: ["schemaVersion", "files", "fileEdges", "modules", "moduleEdges"]
10904
+ },
10905
+ // Two shapes, both objects: the whole index, or one symbol's entry.
10906
+ symbols: {
10907
+ oneOf: [
10908
+ {
10909
+ type: "object",
10910
+ properties: { schemaVersion: { type: "integer" }, defs: anyObj, refs: anyObj },
10911
+ required: ["schemaVersion", "defs"]
10912
+ },
10913
+ {
10914
+ type: "object",
10915
+ properties: { name: { type: "string" }, defs: { type: "array", items: anyObj }, refs: strArr },
10916
+ required: ["name", "defs", "refs"]
10917
+ }
10918
+ ]
10919
+ },
10920
+ // The whole index (symbol name -> entry), one entry, or the not-found notice.
10921
+ callers: {
10922
+ oneOf: [
10923
+ { type: "object", additionalProperties: anyObj },
10924
+ { type: "object", properties: { error: { type: "string" } }, required: ["error"] }
10925
+ ]
10926
+ },
10927
+ workspaces: {
10928
+ type: "object",
10929
+ properties: {
10930
+ packages: { type: "array", items: anyObj },
10931
+ cycle: { type: ["array", "null"], items: { type: "string" } },
10932
+ topoOrder: strArr
10933
+ },
10934
+ required: ["packages", "topoOrder"]
10935
+ },
10936
+ churn: {
10937
+ type: "object",
10938
+ properties: { ok: { type: "boolean" }, churn: { type: "object", additionalProperties: { type: "integer" } } },
10939
+ required: ["ok", "churn"]
10940
+ },
10941
+ find_references: {
10942
+ type: "object",
10943
+ properties: {
10944
+ defs: { type: "array", items: anyObj },
10945
+ callSites: { type: "array", items: anyObj },
10946
+ referencingFiles: strArr
10947
+ },
10948
+ required: ["defs", "callSites", "referencingFiles"]
10949
+ },
10950
+ hotspots: {
10951
+ type: "object",
10952
+ properties: { churnOk: { type: "boolean" }, hotspots: { type: "array", items: anyObj } },
10953
+ required: ["churnOk", "hotspots"]
10954
+ },
10955
+ coupling: {
10956
+ type: "object",
10957
+ properties: { ok: { type: "boolean" }, couplings: { type: "array", items: anyObj } },
10958
+ required: ["ok", "couplings"]
10959
+ },
10960
+ embed_status: {
10961
+ type: "object",
10962
+ properties: {
10963
+ embedVersion: { type: "integer" },
10964
+ mode: { type: "string", enum: ["none", "static", "endpoint"] },
10965
+ model: {},
10966
+ endpoint: {},
10967
+ endpointReachable: { type: "boolean" }
10968
+ },
10969
+ required: ["embedVersion", "mode"]
10970
+ },
10971
+ write_memory: {
10972
+ type: "object",
10973
+ properties: { written: { type: "string" } },
10974
+ required: ["written"]
10975
+ },
10976
+ delete_memory: {
10977
+ type: "object",
10978
+ properties: { deleted: { type: "boolean" } },
10979
+ required: ["deleted"]
10980
+ }
10981
+ };
10982
+ for (const name2 of ["replace_symbol_body", "insert_after_symbol", "insert_before_symbol"]) {
10983
+ OUTPUT_SCHEMAS[name2] = {
10984
+ type: "object",
10985
+ properties: {
10986
+ file: { type: "string" },
10987
+ symbol: { type: "string" },
10988
+ startLine: { type: "integer" },
10989
+ endLine: { type: "integer" }
10990
+ },
10991
+ required: ["file"]
10992
+ };
10993
+ }
10994
+ TOOL_META = {
10995
+ scan_summary: { title: "Scan summary" },
10996
+ graph: { title: "Link graph" },
10997
+ symbols: { title: "Symbol index" },
10998
+ callers: { title: "Caller index" },
10999
+ workspaces: { title: "Monorepo workspaces" },
11000
+ churn: { title: "Git churn" },
11001
+ symbols_overview: { title: "File symbol overview" },
11002
+ find_symbol: { title: "Find symbol" },
11003
+ find_references: { title: "Find references" },
11004
+ repo_map: { title: "Repository map" },
11005
+ hotspots: { title: "Hotspots" },
11006
+ coupling: { title: "Change coupling" },
11007
+ replace_symbol_body: { title: "Replace symbol body", write: true, destructive: true, idempotent: true },
11008
+ insert_after_symbol: { title: "Insert after symbol", write: true, destructive: false, idempotent: false },
11009
+ insert_before_symbol: { title: "Insert before symbol", write: true, destructive: false, idempotent: false },
11010
+ write_memory: { title: "Write memory", write: true, destructive: false, idempotent: true },
11011
+ read_memory: { title: "Read memory" },
11012
+ list_memories: { title: "List memories" },
11013
+ delete_memory: { title: "Delete memory", write: true, destructive: true, idempotent: true },
11014
+ dead_code: { title: "Dead-code candidates" },
11015
+ complexity: { title: "Complexity" },
11016
+ mermaid: { title: "Mermaid module diagram" },
11017
+ grep: { title: "Grep file contents" },
11018
+ search: { title: "Lexical search", openWorld: true },
11019
+ embed_status: { title: "Embedding tier status", openWorld: true },
11020
+ check_rules: { title: "Check architecture rules" }
11021
+ };
11022
+ }
11023
+ });
11024
+
11025
+ // src/mcp/session.ts
11026
+ import { statSync as statSync5 } from "fs";
11027
+ import { join as join17 } from "path";
11028
+ function scanFingerprint(scan2) {
11029
+ return sha1(scan2.files.map((f) => `${f.rel}:${f.hash}`).join("\n"));
11030
+ }
11031
+ async function memoizedEmbeddingIndex(key, build) {
11032
+ const cacheKey = `${key.mode}:${key.identity}:${scanFingerprint(key.scan)}`;
11033
+ if (embeddingIndexCache && embeddingIndexCache.key === cacheKey) return embeddingIndexCache.index;
11034
+ const index = await build();
11035
+ embeddingIndexCache = { key: cacheKey, index };
11036
+ return index;
11037
+ }
11038
+ function memoizedEmbedModel(modelDir) {
11039
+ let stat;
11040
+ try {
11041
+ stat = statSync5(join17(modelDir, "model.json"));
11042
+ } catch {
11043
+ return void 0;
11044
+ }
11045
+ const key = `${modelDir}:${stat.mtimeMs}:${stat.size}`;
11046
+ if (embedModelCache && embedModelCache.key === key) return embedModelCache.model;
11047
+ const model = loadEmbedModel(modelDir);
11048
+ if (model) embedModelCache = { key, model };
11049
+ return model;
11050
+ }
11051
+ function sessionGet(key) {
11052
+ const i2 = sessionCaches.findIndex((e) => e.key === key);
11053
+ if (i2 < 0) return void 0;
11054
+ const [entry] = sessionCaches.splice(i2, 1);
11055
+ sessionCaches.unshift(entry);
11056
+ return entry;
11057
+ }
11058
+ function sessionPut(entry) {
11059
+ const i2 = sessionCaches.findIndex((e) => e.key === entry.key);
11060
+ if (i2 >= 0) sessionCaches.splice(i2, 1);
11061
+ sessionCaches.unshift(entry);
11062
+ sessionCaches.length = Math.min(sessionCaches.length, SESSION_CACHE_MAX);
11063
+ return entry;
11064
+ }
11065
+ function sessionClear() {
11066
+ sessionCaches.length = 0;
11067
+ }
11068
+ function sessionKey(repo, opts) {
11069
+ return repo + "\0" + JSON.stringify({
11070
+ scope: opts.scope,
11071
+ include: opts.include,
11072
+ exclude: opts.exclude,
11073
+ gitignore: opts.gitignore,
11074
+ ignoreDirs: opts.ignoreDirs,
11075
+ maxBytes: opts.maxBytes,
11076
+ maxFiles: opts.maxFiles,
11077
+ maxCallsPerFile: opts.maxCallsPerFile,
11078
+ out: opts.out,
11079
+ fullHash: opts.fullHash
11080
+ });
11081
+ }
11082
+ function getScan(repo, opts = {}, walked) {
11083
+ const key = sessionKey(repo, opts);
11084
+ const hit = sessionGet(key);
11085
+ if (hit) {
11086
+ const fresh = scanRepo(repo, { ...opts, cache: hit.cacheMap, precomputedWalk: walked });
11087
+ if (fresh.contentUnchanged) {
11088
+ if (fresh.cacheDirty) hit.cacheMap = toCacheMap(fresh);
11089
+ if (hit.scan.commit !== fresh.commit) hit.scan.commit = fresh.commit;
11090
+ return hit.scan;
11091
+ }
11092
+ sessionPut({ key, scan: fresh, cacheMap: toCacheMap(fresh) });
11093
+ return fresh;
11094
+ }
11095
+ const preloaded = preloadSession(repo, { ...opts, precomputedWalk: walked });
11096
+ if (preloaded) {
11097
+ sessionPut({ key, scan: preloaded.scan, cacheMap: preloaded.cacheMap, arts: preloaded.arts });
11098
+ return preloaded.scan;
11099
+ }
11100
+ const scan2 = scanRepo(repo, { ...opts, precomputedWalk: walked });
11101
+ sessionPut({ key, scan: scan2, cacheMap: toCacheMap(scan2) });
11102
+ return scan2;
11103
+ }
11104
+ function getScanSummary(repo, opts = {}, walked) {
11105
+ if (sessionCaches.some((e) => e.key === sessionKey(repo, opts))) {
11106
+ const scan2 = getScan(repo, opts, walked);
11107
+ return {
11108
+ root: scan2.root,
11109
+ commit: scan2.commit,
11110
+ fileCount: scan2.files.length,
11111
+ languages: scan2.languages,
11112
+ capped: scan2.capped,
11113
+ excluded: scan2.excluded
11114
+ };
11115
+ }
11116
+ return scanSummary(repo, { ...opts, precomputedWalk: walked });
11117
+ }
11118
+ function getArtifacts(repo, opts = {}, walked) {
11119
+ const scan2 = getScan(repo, opts, walked);
11120
+ const entry = sessionCaches.find((e) => e.scan === scan2);
11121
+ if (entry) return entry.arts ??= buildArtifactsFromScan(scan2, opts);
11122
+ return buildArtifactsFromScan(scan2, opts);
11123
+ }
11124
+ async function warmGrammarsForRepo(repo) {
11125
+ await warmGrammarsForWalk(walk(repo, {}));
11126
+ }
11127
+ async function warmGrammarsForWalk(walked) {
11128
+ await ensureGrammars(grammarKeysForExts(walked.files.map((f) => f.ext)));
11129
+ }
11130
+ var embeddingIndexCache, embedModelCache, SESSION_CACHE_MAX, sessionCaches;
11131
+ var init_session = __esm({
11132
+ "src/mcp/session.ts"() {
11133
+ "use strict";
11134
+ init_pipeline();
11135
+ init_scan();
11136
+ init_preload();
11137
+ init_walk();
11138
+ init_loader();
11139
+ init_model();
11140
+ init_embed();
11141
+ init_hash();
11142
+ SESSION_CACHE_MAX = 4;
11143
+ sessionCaches = [];
11144
+ }
11145
+ });
11146
+
11147
+ // src/mcp.ts
11148
+ var mcp_exports = {};
11149
+ __export(mcp_exports, {
11150
+ DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
11151
+ OUTPUT_SCHEMAS: () => OUTPUT_SCHEMAS,
11152
+ PROTOCOL_VERSIONS: () => PROTOCOL_VERSIONS,
11153
+ TOOLS: () => TOOLS,
11154
+ TOOL_META: () => TOOL_META,
11155
+ annotationsFor: () => annotationsFor,
11156
+ capResponse: () => capResponse,
11157
+ getArtifacts: () => getArtifacts,
11158
+ getScan: () => getScan,
11159
+ getScanSummary: () => getScanSummary,
11160
+ memoizedEmbedModel: () => memoizedEmbedModel,
11161
+ memoizedEmbeddingIndex: () => memoizedEmbeddingIndex,
11162
+ negotiateProtocol: () => negotiateProtocol,
11163
+ resourceLinkFor: () => resourceLinkFor,
11164
+ runMcpServer: () => runMcpServer,
11165
+ scanFingerprint: () => scanFingerprint,
11166
+ structuredContentFor: () => structuredContentFor,
11167
+ toCacheMap: () => toCacheMap,
11168
+ toolsFor: () => toolsFor,
11169
+ validateArgs: () => validateArgs,
11170
+ warmGrammarsForRepo: () => warmGrammarsForRepo,
11171
+ warmGrammarsForWalk: () => warmGrammarsForWalk
11172
+ });
11173
+ import { readFileSync as readFileSync8 } from "fs";
11174
+ import { isAbsolute, join as join18 } from "path";
11175
+ import { createInterface } from "readline";
11176
+ function str(v) {
11177
+ return typeof v === "string" && v ? v : void 0;
11178
+ }
11179
+ function strArray(v) {
11180
+ return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length ? v : void 0;
11181
+ }
11182
+ function num(v) {
11183
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
11184
+ return Number.isFinite(n) && n > 0 ? n : void 0;
11185
+ }
11186
+ function errMessage(e) {
11187
+ return e instanceof Error ? e.message : String(e);
11188
+ }
11189
+ async function callTool(name2, args2, defaultRepo) {
11190
+ const repo = str(args2.repo) ?? defaultRepo;
11191
+ if (!repo) throw new Error("`repo` is required (absolute path to the repository root)");
11192
+ const scanOpts = { scope: str(args2.scope), include: strArray(args2.include), exclude: strArray(args2.exclude) };
11193
+ let walked;
11194
+ if (!SCANLESS_TOOLS.has(name2)) {
11195
+ walked = walk(repo, {});
11196
+ await warmGrammarsForWalk(walked);
11197
+ }
11198
+ if (name2 === "scan_summary") {
11199
+ const s = getScanSummary(repo, scanOpts, walked);
11200
+ return JSON.stringify(
11201
+ { engineVersion: ENGINE_VERSION, commit: s.commit, fileCount: s.fileCount, languages: s.languages, capped: s.capped },
11202
+ null,
11203
+ 2
11204
+ );
11205
+ }
11206
+ if (name2 === "graph") {
11207
+ return renderGraphJson(getArtifacts(repo, scanOpts, walked).graph);
11208
+ }
11209
+ if (name2 === "symbols") {
11210
+ const { symbols } = getArtifacts(repo, scanOpts, walked);
11211
+ const lookup = str(args2.name);
11212
+ if (lookup) {
11213
+ return JSON.stringify({ name: lookup, defs: symbols.defs[lookup] ?? [], refs: symbols.refs[lookup] ?? [] }, null, 2);
11214
+ }
11215
+ return JSON.stringify(symbols, null, 2);
11216
+ }
11217
+ if (name2 === "callers") {
11218
+ const scan2 = getScan(repo, scanOpts, walked);
11219
+ const index = args2.recall === true ? buildCallerIndex(scan2, void 0, { recall: true }) : callerIndexFor(scan2);
11220
+ const lookup = str(args2.name);
11221
+ if (lookup) {
11222
+ const entry = index.get(lookup);
11223
+ return JSON.stringify(entry ?? { error: `no tracked callers for "${lookup}"` }, null, 2);
11224
+ }
11225
+ const obj = {};
11226
+ for (const [k, v] of index) obj[k] = v;
11227
+ return JSON.stringify(obj, null, 2);
11228
+ }
11229
+ if (name2 === "workspaces") {
11230
+ const info2 = detectWorkspaces(repo);
11231
+ return JSON.stringify({ packages: info2.packages, cycle: info2.cycle ?? null, topoOrder: info2.topoOrder }, null, 2);
11232
+ }
11233
+ if (name2 === "churn") {
11234
+ const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
11235
+ const sorted = {};
11236
+ for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
11237
+ return JSON.stringify({ ok, churn: sorted }, null, 2);
11238
+ }
11239
+ if (name2 === "symbols_overview") {
11240
+ const file = str(args2.file);
11241
+ if (!file) throw new Error("`file` is required");
11242
+ return JSON.stringify(symbolsOverview(getScan(repo, scanOpts, walked), file), null, 2);
11243
+ }
11244
+ if (name2 === "find_symbol") {
11245
+ const namePath = str(args2.namePath);
11246
+ if (!namePath) throw new Error("`namePath` is required");
11247
+ const matches = findSymbol(getScan(repo, scanOpts, walked), namePath, {
11248
+ substring: args2.substring === true,
11249
+ includeBody: args2.includeBody === true,
11250
+ maxResults: num(args2.maxResults)
11251
+ });
11252
+ return JSON.stringify(matches, null, 2);
11253
+ }
11254
+ if (name2 === "find_references") {
11255
+ const symName = str(args2.name);
11256
+ if (!symName) throw new Error("`name` is required");
11257
+ return JSON.stringify(findReferences(getScan(repo, scanOpts, walked), symName), null, 2);
11258
+ }
11259
+ if (name2 === "replace_symbol_body" || name2 === "insert_after_symbol" || name2 === "insert_before_symbol") {
11260
+ const namePath = str(args2.namePath);
11261
+ const body2 = typeof args2.body === "string" ? args2.body : void 0;
11262
+ if (!namePath || body2 === void 0) throw new Error("`namePath` and `body` are required");
11263
+ const scan2 = getScan(repo, scanOpts, walked);
11264
+ const fn = name2 === "replace_symbol_body" ? replaceSymbolBody : name2 === "insert_after_symbol" ? insertAfterSymbol : insertBeforeSymbol;
11265
+ const result = fn(scan2, namePath, body2, str(args2.file));
11266
+ sessionClear();
11267
+ return JSON.stringify(result, null, 2);
11268
+ }
11269
+ if (name2 === "write_memory") {
11270
+ const memName = str(args2.name);
11271
+ const content = typeof args2.content === "string" ? args2.content : void 0;
11272
+ if (!memName || content === void 0) throw new Error("`name` and `content` are required");
11273
+ return JSON.stringify({ written: writeMemory(repo, memName, content) }, null, 2);
11274
+ }
11275
+ if (name2 === "read_memory") {
11276
+ const memName = str(args2.name);
11277
+ if (!memName) throw new Error("`name` is required");
11278
+ const content = readMemory(repo, memName);
11279
+ if (content === void 0) throw new Error(`no memory named "${memName}" \u2014 see list_memories`);
11280
+ return content;
11281
+ }
11282
+ if (name2 === "list_memories") {
11283
+ return JSON.stringify(listMemories(repo), null, 2);
11284
+ }
11285
+ if (name2 === "delete_memory") {
11286
+ const memName = str(args2.name);
11287
+ if (!memName) throw new Error("`name` is required");
11288
+ return JSON.stringify({ deleted: deleteMemory(repo, memName) }, null, 2);
11289
+ }
11290
+ if (name2 === "dead_code") {
11291
+ const all = findDeadCode(getScan(repo, scanOpts, walked));
11292
+ const limit = num(args2.limit);
11293
+ if (limit === void 0 || all.length <= limit) return JSON.stringify(all, null, 2);
11294
+ return JSON.stringify({ total: all.length, shown: limit, truncated: true, candidates: all.slice(0, limit) }, null, 2);
11295
+ }
11296
+ if (name2 === "complexity") {
11297
+ const scan2 = getScan(repo, scanOpts, walked);
11298
+ if (args2.risk === true) {
11299
+ const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
11300
+ return JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn, num(args2.top)) }, null, 2);
11301
+ }
11302
+ return JSON.stringify(symbolComplexity(scan2, str(args2.file), num(args2.top)), null, 2);
11303
+ }
11304
+ if (name2 === "mermaid") {
11305
+ const { graph } = getArtifacts(repo, scanOpts, walked);
11306
+ return renderMermaid(graph, { module: str(args2.module), maxEdges: num(args2.maxEdges) });
11307
+ }
11308
+ if (name2 === "repo_map") {
11309
+ const { scan: scan2, graph } = getArtifacts(repo, scanOpts, walked);
11310
+ return renderRepoMap(scan2, graph, { budgetTokens: typeof args2.budgetTokens === "number" ? args2.budgetTokens : void 0 });
11311
+ }
11312
+ if (name2 === "hotspots") {
11313
+ const scan2 = getScan(repo, scanOpts, walked);
11314
+ const { churn, ok } = gitChurn(repo, { since: str(args2.since) });
11315
+ return JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2);
11316
+ }
11317
+ if (name2 === "coupling") {
11318
+ const { ok, couplings } = changeCoupling(repo, { since: str(args2.since) });
11319
+ return JSON.stringify({ ok, couplings }, null, 2);
11320
+ }
11321
+ if (name2 === "grep") {
11322
+ const pattern = str(args2.pattern);
11323
+ if (!pattern) throw new Error("`pattern` is required");
11324
+ const scope = str(args2.scope);
11325
+ const globs = strArray(args2.globs);
11326
+ const hits = grepRepo(repo, pattern, {
11327
+ globs: scope ? [...globs ?? [], `${scope.replace(/\/+$/, "")}/**`] : globs,
11328
+ ignoreCase: args2.ignoreCase === true,
11329
+ maxHits: typeof args2.maxHits === "number" ? args2.maxHits : void 0
11330
+ });
11331
+ return JSON.stringify(hits, null, 2);
11332
+ }
11333
+ if (name2 === "search") {
11334
+ const query = str(args2.query);
11335
+ if (!query) throw new Error("`query` is required");
11336
+ const scan2 = getScan(repo, scanOpts, walked);
11337
+ const limit = typeof args2.limit === "number" ? args2.limit : void 0;
11338
+ const fuzzy = typeof args2.fuzzy === "boolean" ? args2.fuzzy : void 0;
11339
+ if (args2.semantic === true) {
11340
+ const endpoint = resolveEmbedEndpoint();
11341
+ if (endpoint) {
11342
+ try {
11343
+ const index = await memoizedEmbeddingIndex({ mode: "endpoint", identity: endpoint, scan: scan2 }, () => buildEndpointIndex(scan2));
11344
+ const queryVec = await encodeQueryViaEndpoint(query);
11345
+ const results2 = searchSemantic(scan2, query, index, { queryVec, limit, fuzzy });
11346
+ return JSON.stringify({ results: results2, tier: "endpoint" }, null, 2);
11347
+ } catch (e) {
11348
+ const results2 = searchIndex(scan2, query, { limit, fuzzy });
11349
+ return JSON.stringify(
11350
+ { results: results2, tier: "lexical", degradedReason: `embedding endpoint failed: ${errMessage(e)}` },
11351
+ null,
11352
+ 2
11353
+ );
11354
+ }
11355
+ }
11356
+ const modelDir = resolveEmbedModelDir(repo);
11357
+ const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
11358
+ if (model) {
11359
+ const index = await memoizedEmbeddingIndex(
11360
+ { mode: "static", identity: `${modelDir}#${model.modelId}`, scan: scan2 },
11361
+ () => buildEmbeddingIndex(scan2, model)
11362
+ );
11363
+ const results2 = searchSemantic(scan2, query, index, { model, limit, fuzzy });
11364
+ return JSON.stringify({ results: results2, tier: "static" }, null, 2);
11365
+ }
11366
+ const results = searchIndex(scan2, query, { limit, fuzzy });
11367
+ return JSON.stringify(
11368
+ { results, tier: "lexical", degradedReason: "no embedding endpoint or static model configured \u2014 see embed_status" },
11369
+ null,
11370
+ 2
11371
+ );
11372
+ }
11373
+ return JSON.stringify(searchIndex(scan2, query, { limit, fuzzy }), null, 2);
11374
+ }
11375
+ if (name2 === "embed_status") {
11376
+ const modelDir = resolveEmbedModelDir(repo);
11377
+ const model = modelDir ? memoizedEmbedModel(modelDir) : void 0;
11378
+ const endpoint = resolveEmbedEndpoint();
11379
+ const mode = endpoint ? "endpoint" : model ? "static" : "none";
11380
+ const status = {
11381
+ embedVersion: EMBED_VERSION,
11382
+ mode,
11383
+ model: model ? { present: true, dir: modelDir, modelId: model.modelId, dim: model.dim, vocabSize: model.vocabSize } : { present: false },
11384
+ endpoint: endpoint ?? null
11385
+ };
11386
+ if (endpoint) status.endpointReachable = await probeEndpoint(endpoint);
11387
+ return JSON.stringify(status, null, 2);
11388
+ }
11389
+ if (name2 === "check_rules") {
11390
+ const configPath = str(args2.configPath);
11391
+ let payload = args2.rules;
11392
+ if (payload === void 0 && configPath) {
11393
+ const abs = isAbsolute(configPath) ? configPath : join18(repo, configPath);
11394
+ try {
11395
+ payload = JSON.parse(readFileSync8(abs, "utf8"));
11396
+ } catch (e) {
11397
+ throw new Error(`cannot read rules from ${abs}: ${errMessage(e)}`);
11398
+ }
11399
+ }
11400
+ if (payload === void 0) throw new Error("`rules` (or `configPath`) is required");
11401
+ const rules = parseRules(payload);
11402
+ const { graph } = getArtifacts(repo, scanOpts, walked);
11403
+ return JSON.stringify(checkRules(graph, rules), null, 2);
11404
+ }
11405
+ throw new Error(`unknown tool: ${name2}`);
11406
+ }
11407
+ async function runMcpServer(opts = {}) {
11408
+ const serverInfo = {
11409
+ name: opts.serverInfo?.name ?? "codeindex",
11410
+ version: opts.serverInfo?.version ?? ENGINE_VERSION
11411
+ };
11412
+ let protocolVersion = PROTOCOL_VERSIONS[0];
11413
+ let tools = toolsFor(opts.defaultRepo, protocolVersion);
11414
+ const send = (msg) => {
11415
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...msg }) + "\n");
11416
+ };
11417
+ const rl = createInterface({ input: process.stdin, terminal: false });
11418
+ for await (const line of rl) {
11419
+ const trimmed = line.trim();
11420
+ if (!trimmed) continue;
11421
+ let parsed;
11422
+ try {
11423
+ parsed = JSON.parse(trimmed);
11424
+ } catch {
11425
+ send({ id: null, error: { code: -32700, message: "parse error" } });
11426
+ continue;
11427
+ }
11428
+ const requests = Array.isArray(parsed) ? parsed : [parsed];
11429
+ for (const req of requests) await handle2(req);
11430
+ }
11431
+ async function handle2(req) {
11432
+ if (req.id === void 0 || req.id === null) return;
11433
+ try {
11434
+ if (req.method === "initialize") {
11435
+ protocolVersion = negotiateProtocol(req.params?.protocolVersion);
11436
+ tools = toolsFor(opts.defaultRepo, protocolVersion);
11437
+ send({
11438
+ id: req.id,
11439
+ result: {
11440
+ protocolVersion,
11441
+ capabilities: { tools: {} },
11442
+ serverInfo
11443
+ }
11444
+ });
11445
+ } else if (req.method === "ping") {
11446
+ send({ id: req.id, result: {} });
11447
+ } else if (req.method === "tools/list") {
11448
+ send({ id: req.id, result: { tools } });
11449
+ } else if (req.method === "tools/call") {
11450
+ const params = req.params ?? {};
11451
+ const name2 = str(params.name) ?? "";
11452
+ const args2 = params.arguments ?? {};
11453
+ try {
11454
+ const decl = tools.find(
11455
+ (t) => t.name === name2
11456
+ );
11457
+ const invalid = decl ? validateArgs(decl.inputSchema, args2) : void 0;
11458
+ if (invalid) throw new Error(invalid);
11459
+ const raw = await callTool(name2, args2, opts.defaultRepo);
11460
+ const repo = str(args2.repo) ?? opts.defaultRepo ?? "";
11461
+ const text = capResponse(raw, name2, repo, opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES);
11462
+ const capped = text !== raw;
11463
+ const link = capped && protocolVersion >= RICH_TOOLS_SINCE ? resourceLinkFor(text, name2) : void 0;
11464
+ const structured = protocolVersion >= RICH_TOOLS_SINCE ? structuredContentFor(text, capped, OUTPUT_SCHEMAS[name2] !== void 0) : void 0;
11465
+ send({
11466
+ id: req.id,
11467
+ result: {
11468
+ content: link ? [{ type: "text", text }, link] : [{ type: "text", text }],
11469
+ ...structured ? { structuredContent: structured } : {}
11470
+ }
11471
+ });
11472
+ } catch (e) {
11473
+ send({
11474
+ id: req.id,
11475
+ result: { content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }], isError: true }
11476
+ });
10990
11477
  }
11478
+ } else {
11479
+ send({ id: req.id, error: { code: -32601, message: `method not found: ${req.method}` } });
10991
11480
  }
10992
- ];
11481
+ } catch (e) {
11482
+ send({ id: req.id, error: { code: -32603, message: e instanceof Error ? e.message : String(e) } });
11483
+ }
11484
+ }
11485
+ }
11486
+ var SCANLESS_TOOLS;
11487
+ var init_mcp = __esm({
11488
+ "src/mcp.ts"() {
11489
+ "use strict";
11490
+ init_types();
11491
+ init_graph_json();
11492
+ init_callers();
11493
+ init_derived();
11494
+ init_workspaces();
11495
+ init_git();
11496
+ init_grep();
11497
+ init_coupling();
11498
+ init_repomap();
11499
+ init_deadcode();
11500
+ init_complexity();
11501
+ init_viz();
11502
+ init_query();
11503
+ init_edit();
11504
+ init_memory();
11505
+ init_bm25();
11506
+ init_rules();
11507
+ init_model();
11508
+ init_embed();
11509
+ init_search();
11510
+ init_endpoint();
11511
+ init_walk();
11512
+ init_tools();
11513
+ init_protocol();
11514
+ init_session();
11515
+ init_tools();
11516
+ init_protocol();
11517
+ init_session();
10993
11518
  SCANLESS_TOOLS = /* @__PURE__ */ new Set([
10994
11519
  "workspaces",
10995
11520
  "churn",
@@ -10999,7 +11524,11 @@ var init_mcp = __esm({
10999
11524
  "read_memory",
11000
11525
  "list_memories",
11001
11526
  "delete_memory",
11002
- "embed_status"
11527
+ "embed_status",
11528
+ // scan_summary counts and classifies by path only — it never parses, so the
11529
+ // grammar warm (a whole extra walk) would be pure overhead. When a scan is
11530
+ // already cached getScanSummary reuses it, warm grammars included.
11531
+ "scan_summary"
11003
11532
  ]);
11004
11533
  }
11005
11534
  });
@@ -11123,6 +11652,140 @@ var init_rewrite = __esm({
11123
11652
  init_types();
11124
11653
  init_walk();
11125
11654
  init_scan();
11655
+ init_scan();
11656
+ init_preload();
11657
+
11658
+ // src/pool.ts
11659
+ init_hash();
11660
+ init_walk();
11661
+ init_registry();
11662
+ init_loader();
11663
+ init_scan();
11664
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
11665
+ import { availableParallelism } from "os";
11666
+ import { dirname as dirname2, join as join4 } from "path";
11667
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
11668
+ import { Worker } from "worker_threads";
11669
+ function resolveEngineUrl() {
11670
+ try {
11671
+ const here = fileURLToPath2(import.meta.url);
11672
+ if (here.endsWith("engine.mjs")) return pathToFileURL(here).href;
11673
+ const adjacent = join4(dirname2(here), "engine.mjs");
11674
+ if (existsSync2(adjacent)) return pathToFileURL(adjacent).href;
11675
+ return void 0;
11676
+ } catch {
11677
+ return void 0;
11678
+ }
11679
+ }
11680
+ var WORKER_TIMEOUT_MS = 10 * 60 * 1e3;
11681
+ function workerCount(requested) {
11682
+ const env = process.env["CODEINDEX_WORKERS"];
11683
+ const raw = requested ?? (env !== void 0 && env !== "" ? Number(env) : void 0);
11684
+ if (raw !== void 0) return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
11685
+ let cores = 1;
11686
+ try {
11687
+ cores = availableParallelism();
11688
+ } catch {
11689
+ cores = 1;
11690
+ }
11691
+ return Math.max(0, Math.min(cores - 1, 8));
11692
+ }
11693
+ async function runExtractWorker(input, post) {
11694
+ await ensureGrammars(input.grammarKeys);
11695
+ const ready = input.grammarKeys.filter((k) => grammarReady(k));
11696
+ const records = [];
11697
+ for (const job of input.jobs) {
11698
+ let size;
11699
+ let mtimeMs;
11700
+ try {
11701
+ const st = statSync2(job.abs);
11702
+ size = st.size;
11703
+ mtimeMs = st.mtimeMs;
11704
+ } catch {
11705
+ continue;
11706
+ }
11707
+ const content = readText(job.abs);
11708
+ const record = buildCodeRecord(job.rel, job.ext, size, content, sha1(content), extToLang(job.ext), {
11709
+ maxCallsPerFile: input.maxCallsPerFile
11710
+ });
11711
+ records.push({ rel: job.rel, size, mtimeMs, record });
11712
+ }
11713
+ post({ ready, records });
11714
+ }
11715
+ async function extractInParallel(jobs, grammarKeys, count, opts = {}) {
11716
+ if (count < 2 || jobs.length === 0) return void 0;
11717
+ const engineUrl = resolveEngineUrl();
11718
+ if (!engineUrl) return void 0;
11719
+ const wanted = grammarKeys.filter((k) => grammarReady(k)).sort();
11720
+ const shards = Array.from({ length: Math.min(count, jobs.length) }, () => []);
11721
+ jobs.forEach((j, i2) => shards[i2 % shards.length].push(j));
11722
+ const bootstrap = `import { runExtractWorker } from ${JSON.stringify(engineUrl)};
11723
+ import { parentPort, workerData } from "node:worker_threads";
11724
+ runExtractWorker(workerData.input, (o) => parentPort.postMessage(o)).catch((e) => parentPort.postMessage({ error: String(e) }));
11725
+ `;
11726
+ try {
11727
+ const outputs = await Promise.all(
11728
+ shards.map(
11729
+ (jobsForShard) => new Promise((resolve3, reject) => {
11730
+ const w = new Worker(bootstrap, {
11731
+ eval: true,
11732
+ workerData: { input: { jobs: jobsForShard, grammarKeys: wanted, maxCallsPerFile: opts.maxCallsPerFile } }
11733
+ });
11734
+ const timer = setTimeout(() => {
11735
+ reject(new Error("extraction worker timed out"));
11736
+ void w.terminate();
11737
+ }, WORKER_TIMEOUT_MS);
11738
+ const settle = (fn) => {
11739
+ clearTimeout(timer);
11740
+ fn();
11741
+ };
11742
+ w.once("message", (m) => {
11743
+ settle(() => resolve3(m));
11744
+ void w.terminate();
11745
+ });
11746
+ w.once("error", (e) => settle(() => reject(e)));
11747
+ w.once("exit", (code) => {
11748
+ if (code !== 0) settle(() => reject(new Error(`extraction worker exited with ${code}`)));
11749
+ });
11750
+ })
11751
+ )
11752
+ );
11753
+ const out2 = /* @__PURE__ */ new Map();
11754
+ for (const o of outputs) {
11755
+ if ("error" in o) return void 0;
11756
+ if (o.ready.slice().sort().join(",") !== wanted.join(",")) return void 0;
11757
+ for (const r of o.records) out2.set(r.rel, { size: r.size, mtimeMs: r.mtimeMs, record: r.record });
11758
+ }
11759
+ return out2;
11760
+ } catch {
11761
+ return void 0;
11762
+ }
11763
+ }
11764
+ async function scanRepoParallel(root, opts = {}) {
11765
+ const count = workerCount(opts.workers);
11766
+ if (count < 2) return scanRepo(root, opts);
11767
+ const walked = opts.precomputedWalk ?? walk(root, {
11768
+ maxFileBytes: opts.maxBytes,
11769
+ maxFiles: opts.maxFiles,
11770
+ gitignore: opts.gitignore,
11771
+ ignoreDirs: opts.ignoreDirs
11772
+ });
11773
+ const scanOpts = { ...opts, precomputedWalk: walked };
11774
+ const jobs = [];
11775
+ for (const { f } of keptCodeFiles(root, scanOpts)) {
11776
+ const cached = opts.cache?.get(f.rel);
11777
+ if (!opts.fullHash && cached && cached.size !== void 0 && cached.mtimeMs !== void 0 && cached.size === f.size && cached.mtimeMs === f.mtimeMs) {
11778
+ continue;
11779
+ }
11780
+ jobs.push({ abs: f.abs, rel: f.rel, ext: f.ext });
11781
+ }
11782
+ if (jobs.length === 0) return scanRepo(root, scanOpts);
11783
+ const grammarKeys = grammarKeysForExts(walked.files.map((f) => f.ext));
11784
+ const extracted = await extractInParallel(jobs, grammarKeys, count, { maxCallsPerFile: opts.maxCallsPerFile });
11785
+ return scanRepo(root, extracted ? { ...scanOpts, extracted } : scanOpts);
11786
+ }
11787
+
11788
+ // src/engine.ts
11126
11789
  init_glob();
11127
11790
  init_ignore();
11128
11791
  init_classify();
@@ -11260,8 +11923,8 @@ init_loader();
11260
11923
  // src/ast/grammars-pull.ts
11261
11924
  init_types();
11262
11925
  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";
11926
+ import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync } from "fs";
11927
+ import { dirname as dirname3, join as join5, resolve, sep as sep2 } from "path";
11265
11928
  import { gunzipSync } from "zlib";
11266
11929
  var DEFAULT_GRAMMARS_URL = `https://github.com/maxgfr/codeindex/releases/download/v${ENGINE_VERSION}/grammars-${ENGINE_VERSION}.tar.gz`;
11267
11930
  function resolveGrammarsPullTarget() {
@@ -11342,7 +12005,7 @@ function extractTarInto(rawTar, destDir) {
11342
12005
  if (dest !== root && !dest.startsWith(root + sep2)) {
11343
12006
  throw new Error(`tar entry escapes destination: ${entry.name}`);
11344
12007
  }
11345
- mkdirSync(dirname2(dest), { recursive: true });
12008
+ mkdirSync(dirname3(dest), { recursive: true });
11346
12009
  writeFileSync(dest, entry.data);
11347
12010
  written.push(rel);
11348
12011
  }
@@ -11366,12 +12029,12 @@ async function pullGrammars(cacheDir, opts = {}) {
11366
12029
  `);
11367
12030
  }
11368
12031
  }
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)) {
12032
+ const runtime = join5(cacheDir, "web-tree-sitter.wasm");
12033
+ const markerPath = join5(dirname3(cacheDir), `${ENGINE_VERSION}.sha256`);
12034
+ if (existsSync3(runtime) && expected && existsSync3(markerPath)) {
11372
12035
  let marker = "";
11373
12036
  try {
11374
- marker = readFileSync3(markerPath, "utf8").trim();
12037
+ marker = readFileSync4(markerPath, "utf8").trim();
11375
12038
  } catch {
11376
12039
  }
11377
12040
  if (marker === expected) {
@@ -11395,13 +12058,13 @@ async function pullGrammars(cacheDir, opts = {}) {
11395
12058
  }
11396
12059
  let tmp;
11397
12060
  try {
11398
- mkdirSync(dirname2(cacheDir), { recursive: true });
11399
- tmp = mkdtempSync(join3(dirname2(cacheDir), ".grammars-tmp-"));
12061
+ mkdirSync(dirname3(cacheDir), { recursive: true });
12062
+ tmp = mkdtempSync(join5(dirname3(cacheDir), ".grammars-tmp-"));
11400
12063
  extractGrammarsTarball(bytes, tmp);
11401
- if (!existsSync2(join3(tmp, "web-tree-sitter.wasm"))) {
12064
+ if (!existsSync3(join5(tmp, "web-tree-sitter.wasm"))) {
11402
12065
  throw new Error("archive is missing web-tree-sitter.wasm");
11403
12066
  }
11404
- if (existsSync2(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
12067
+ if (existsSync3(cacheDir)) rmSync(cacheDir, { recursive: true, force: true });
11405
12068
  renameSync(tmp, cacheDir);
11406
12069
  tmp = void 0;
11407
12070
  if (expected) writeFileSync(markerPath, expected + "\n");
@@ -11478,7 +12141,48 @@ init_graph_json();
11478
12141
  init_types();
11479
12142
  init_walk();
11480
12143
  init_sort();
11481
- import { join as join12 } from "path";
12144
+ import { join as join14 } from "path";
12145
+ var Bytes = class {
12146
+ buf;
12147
+ len = 0;
12148
+ constructor(capacity = 64) {
12149
+ this.buf = new Uint8Array(capacity);
12150
+ }
12151
+ get length() {
12152
+ return this.len;
12153
+ }
12154
+ grow(need) {
12155
+ if (this.len + need <= this.buf.length) return;
12156
+ let cap = this.buf.length * 2 || 64;
12157
+ while (cap < this.len + need) cap *= 2;
12158
+ const next = new Uint8Array(cap);
12159
+ next.set(this.buf.subarray(0, this.len));
12160
+ this.buf = next;
12161
+ }
12162
+ // Rewind without releasing the buffer, so a per-item scratch sink is
12163
+ // allocated once per document instead of once per occurrence/symbol.
12164
+ reset() {
12165
+ this.len = 0;
12166
+ }
12167
+ push(byte) {
12168
+ this.grow(1);
12169
+ this.buf[this.len++] = byte;
12170
+ }
12171
+ pushAll(src) {
12172
+ this.grow(src.length);
12173
+ if (src instanceof Uint8Array) this.buf.set(src, this.len);
12174
+ else for (let i2 = 0; i2 < src.length; i2++) this.buf[this.len + i2] = src[i2];
12175
+ this.len += src.length;
12176
+ }
12177
+ // A view over exactly the bytes written — no copy. Callers must not retain it
12178
+ // across further writes to this sink.
12179
+ view() {
12180
+ return this.buf.subarray(0, this.len);
12181
+ }
12182
+ toUint8Array() {
12183
+ return this.buf.slice(0, this.len);
12184
+ }
12185
+ };
11482
12186
  var utf8 = new TextEncoder();
11483
12187
  function pushVarint(out2, n) {
11484
12188
  if (n < 0) throw new Error(`pushVarint: negative input ${n} is not a valid unsigned varint`);
@@ -11498,15 +12202,18 @@ function pushVarintField(out2, field, n) {
11498
12202
  function pushLenDelim(out2, field, payload) {
11499
12203
  pushTag(out2, field, 2);
11500
12204
  pushVarint(out2, payload.length);
11501
- for (let i2 = 0; i2 < payload.length; i2++) out2.push(payload[i2]);
12205
+ out2.pushAll(payload);
12206
+ }
12207
+ function pushMessage(out2, field, payload) {
12208
+ pushLenDelim(out2, field, payload.view());
11502
12209
  }
11503
12210
  function pushString(out2, field, s) {
11504
12211
  pushLenDelim(out2, field, utf8.encode(s));
11505
12212
  }
11506
12213
  function pushPackedInt32(out2, field, values) {
11507
- const payload = [];
12214
+ const payload = new Bytes(values.length * 2);
11508
12215
  for (const v of values) pushVarint(payload, v);
11509
- pushLenDelim(out2, field, payload);
12216
+ pushMessage(out2, field, payload);
11510
12217
  }
11511
12218
  var F_INDEX_METADATA = 1;
11512
12219
  var F_INDEX_DOCUMENTS = 2;
@@ -11648,7 +12355,7 @@ function renderScip(scan2, opts = {}) {
11648
12355
  };
11649
12356
  const documents = [];
11650
12357
  for (const f of docs) {
11651
- const text = readText(join12(scan2.root, f.rel));
12358
+ const text = readText(join14(scan2.root, f.rel));
11652
12359
  const lines = text.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
11653
12360
  const locate = (lineNo, name2) => {
11654
12361
  const line = lines[lineNo - 1];
@@ -11677,41 +12384,45 @@ function renderScip(scan2, opts = {}) {
11677
12384
  kind: KIND[sym.kind],
11678
12385
  enclosing: sym.parent ? enclosingSymbolOf(f.rel, sym.parent) : void 0
11679
12386
  })).sort((a, b) => byStr(a.symbol, b.symbol));
11680
- const doc = [];
12387
+ const doc = new Bytes(1024);
11681
12388
  pushString(doc, F_DOC_RELPATH, f.rel);
12389
+ const ob = new Bytes(64);
11682
12390
  for (const o of occs) {
11683
12391
  const key = `${o.range.join(",")} ${o.roles} ${o.symbol}`;
11684
12392
  if (seenOcc.has(key)) continue;
11685
12393
  seenOcc.add(key);
11686
- const ob = [];
12394
+ ob.reset();
11687
12395
  pushPackedInt32(ob, F_OCC_RANGE, o.range);
11688
12396
  pushString(ob, F_OCC_SYMBOL, o.symbol);
11689
12397
  if (o.roles !== 0) pushVarintField(ob, F_OCC_ROLES, o.roles);
11690
- pushLenDelim(doc, F_DOC_OCCURRENCES, ob);
12398
+ pushMessage(doc, F_DOC_OCCURRENCES, ob);
11691
12399
  }
12400
+ const sb = new Bytes(64);
11692
12401
  for (const si of infos) {
11693
- const sb = [];
12402
+ sb.reset();
11694
12403
  pushString(sb, F_SI_SYMBOL, si.symbol);
11695
12404
  if (si.kind !== void 0) pushVarintField(sb, F_SI_KIND, si.kind);
11696
12405
  pushString(sb, F_SI_DISPLAY_NAME, si.displayName);
11697
12406
  if (si.enclosing) pushString(sb, F_SI_ENCLOSING, si.enclosing);
11698
- pushLenDelim(doc, F_DOC_SYMBOLS, sb);
12407
+ pushMessage(doc, F_DOC_SYMBOLS, sb);
11699
12408
  }
11700
12409
  pushString(doc, F_DOC_LANGUAGE, f.lang);
11701
12410
  pushVarintField(doc, F_DOC_POSITION_ENCODING, POSITION_ENCODING_UTF16);
11702
12411
  documents.push(doc);
11703
12412
  }
11704
- const toolInfo = [];
12413
+ const toolInfo = new Bytes();
11705
12414
  pushString(toolInfo, F_TOOL_NAME, "codeindex");
11706
12415
  pushString(toolInfo, F_TOOL_VERSION, toolVersion);
11707
- const metadata2 = [];
11708
- pushLenDelim(metadata2, F_META_TOOL_INFO, toolInfo);
12416
+ const metadata2 = new Bytes();
12417
+ pushMessage(metadata2, F_META_TOOL_INFO, toolInfo);
11709
12418
  pushString(metadata2, F_META_PROJECT_ROOT, projectRoot);
11710
12419
  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);
12420
+ let total = 0;
12421
+ for (const d of documents) total += d.length;
12422
+ const index = new Bytes(total + metadata2.length + 16);
12423
+ pushMessage(index, F_INDEX_METADATA, metadata2);
12424
+ for (const d of documents) pushMessage(index, F_INDEX_DOCUMENTS, d);
12425
+ return index.toUint8Array();
11715
12426
  }
11716
12427
 
11717
12428
  // src/engine.ts
@@ -11730,6 +12441,375 @@ init_repomap();
11730
12441
  init_deadcode();
11731
12442
  init_complexity();
11732
12443
  init_viz();
12444
+
12445
+ // src/traverse.ts
12446
+ init_sort();
12447
+ var DEPENDS_KINDS = /* @__PURE__ */ new Set(["import", "use", "call"]);
12448
+ function hubThreshold(degrees) {
12449
+ const sorted = degrees.slice().sort((a, b) => a - b);
12450
+ const n = sorted.length;
12451
+ const p99 = n === 0 ? 0 : sorted[Math.min(n - 1, Math.floor(0.99 * n))];
12452
+ return Math.max(50, p99);
12453
+ }
12454
+ function reverseClosure(edges, seeds, depth = Infinity) {
12455
+ const dependents = /* @__PURE__ */ new Map();
12456
+ for (const e of edges) {
12457
+ if (e.dangling || !DEPENDS_KINDS.has(e.kind)) continue;
12458
+ let arr = dependents.get(e.to);
12459
+ if (!arr) dependents.set(e.to, arr = []);
12460
+ arr.push(e);
12461
+ }
12462
+ const depthOf = /* @__PURE__ */ new Map();
12463
+ const seen = new Set(seeds);
12464
+ let frontier = [...seeds];
12465
+ for (let d = 1; d <= depth && frontier.length; d++) {
12466
+ const next = [];
12467
+ for (const node of frontier) {
12468
+ for (const e of (dependents.get(node) ?? []).slice().sort((a, b) => byStr(a.from, b.from))) {
12469
+ if (seen.has(e.from)) continue;
12470
+ seen.add(e.from);
12471
+ depthOf.set(e.from, d);
12472
+ next.push(e.from);
12473
+ }
12474
+ }
12475
+ frontier = next;
12476
+ }
12477
+ return depthOf;
12478
+ }
12479
+ function impactOf(graph, target, depth = Infinity) {
12480
+ const moduleOf = new Map(graph.files.map((f) => [f.rel, f.module]));
12481
+ const mod = graph.modules.find((m) => m.slug === target);
12482
+ const file = mod ? void 0 : graph.files.find((f) => f.rel === target);
12483
+ if (!mod && !file) return void 0;
12484
+ const seeds = mod ? mod.members : [file.rel];
12485
+ const depthOf = reverseClosure(graph.fileEdges, seeds, depth);
12486
+ 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));
12487
+ const modules = [...new Set(files.map((f) => f.module).filter((m) => m !== target))].sort(byStr);
12488
+ return { target, scope: mod ? "module" : "file", seeds, files, modules };
12489
+ }
12490
+ function bfs(edges, start2, depth, kinds) {
12491
+ const out2 = /* @__PURE__ */ new Map();
12492
+ const inn = /* @__PURE__ */ new Map();
12493
+ const degree = /* @__PURE__ */ new Map();
12494
+ for (const e of edges) {
12495
+ if (e.dangling) continue;
12496
+ if (kinds && !kinds.has(e.kind)) continue;
12497
+ (out2.get(e.from) ?? out2.set(e.from, []).get(e.from)).push(e);
12498
+ (inn.get(e.to) ?? inn.set(e.to, []).get(e.to)).push(e);
12499
+ degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
12500
+ degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
12501
+ }
12502
+ const threshold = hubThreshold([...degree.values()]);
12503
+ const seen = /* @__PURE__ */ new Set([start2]);
12504
+ const links = [];
12505
+ let frontier = [start2];
12506
+ for (let d = 1; d <= depth; d++) {
12507
+ const next = [];
12508
+ for (const node of frontier) {
12509
+ if (node !== start2 && (degree.get(node) ?? 0) >= threshold) continue;
12510
+ for (const e of (out2.get(node) ?? []).slice().sort((a, b) => byStr(a.to, b.to))) {
12511
+ if (seen.has(e.to)) continue;
12512
+ links.push({ node: e.to, direction: "out", kind: e.kind, weight: e.weight, depth: d, confidence: e.confidence });
12513
+ seen.add(e.to);
12514
+ next.push(e.to);
12515
+ }
12516
+ for (const e of (inn.get(node) ?? []).slice().sort((a, b) => byStr(a.from, b.from))) {
12517
+ if (seen.has(e.from)) continue;
12518
+ links.push({ node: e.from, direction: "in", kind: e.kind, weight: e.weight, depth: d, confidence: e.confidence });
12519
+ seen.add(e.from);
12520
+ next.push(e.from);
12521
+ }
12522
+ }
12523
+ frontier = next;
12524
+ }
12525
+ return links;
12526
+ }
12527
+ function neighborsOf(graph, target, depth = 1, kinds) {
12528
+ const mod = graph.modules.find((m) => m.slug === target);
12529
+ if (mod) {
12530
+ return { target, scope: "module", links: bfs(graph.moduleEdges, target, depth, kinds), members: mod.members };
12531
+ }
12532
+ const file = graph.files.find((f) => f.rel === target);
12533
+ if (file) {
12534
+ return { target, scope: "file", links: bfs(graph.fileEdges, target, depth, kinds) };
12535
+ }
12536
+ return void 0;
12537
+ }
12538
+
12539
+ // src/delta.ts
12540
+ init_git();
12541
+ init_sort();
12542
+ init_util();
12543
+ var RISK_WEIGHTS = {
12544
+ exportedChange: 25,
12545
+ // an exported symbol changed: consumers may break
12546
+ hubHigh: 20,
12547
+ // pagerank percentile ≥ .90
12548
+ hubMed: 10,
12549
+ // pagerank percentile ≥ .75
12550
+ blastHigh: 20,
12551
+ // ≥ 20 dependent files or ≥ 5 dependent modules
12552
+ blastMed: 10,
12553
+ // ≥ 5 dependent files
12554
+ testGap: 20,
12555
+ // a testable module with no covering test
12556
+ surprise: 10,
12557
+ // the module sits on a surprising cross-community edge
12558
+ dangling: 15
12559
+ // a changed file carries a dangling import
12560
+ };
12561
+ var HIGH_MIN = 60;
12562
+ var MEDIUM_MIN = 30;
12563
+ var OPEN_CAP = 3;
12564
+ var DEFAULT_DELTA_DEPTH = 2;
12565
+ function symbolsInHunks(defs, hunks) {
12566
+ const out2 = [];
12567
+ const seen = /* @__PURE__ */ new Set();
12568
+ const push = (d, approx) => {
12569
+ const key = `${d.name}:${d.line}`;
12570
+ if (seen.has(key)) return;
12571
+ seen.add(key);
12572
+ out2.push({
12573
+ name: d.name,
12574
+ kind: d.kind,
12575
+ exported: d.exported,
12576
+ line: d.line,
12577
+ ...d.endLine !== void 0 ? { endLine: d.endLine } : {},
12578
+ ...d.parent !== void 0 ? { parent: d.parent } : {},
12579
+ ...approx ? { approx: true } : {}
12580
+ });
12581
+ };
12582
+ const span = (d) => (d.endLine ?? d.line) - d.line;
12583
+ for (const h of hunks) {
12584
+ const enclosing = defs.filter((d) => d.line <= h.end && (d.endLine ?? d.line) >= h.start);
12585
+ if (enclosing.length) {
12586
+ enclosing.sort((a, b) => span(a) - span(b) || b.line - a.line || byStr(a.name, b.name));
12587
+ for (const d of enclosing) push(d, false);
12588
+ } else {
12589
+ const above = defs.filter((d) => d.line <= h.start && d.endLine === void 0);
12590
+ const near = above[above.length - 1];
12591
+ if (near) push(near, true);
12592
+ }
12593
+ }
12594
+ return out2;
12595
+ }
12596
+ function percentile(values, mine) {
12597
+ if (values.length <= 1) return 0;
12598
+ let smaller = 0;
12599
+ for (const v of values) if (v < mine) smaller++;
12600
+ return smaller / (values.length - 1);
12601
+ }
12602
+ function computeDelta(graph, symbols, diff, depth = DEFAULT_DELTA_DEPTH) {
12603
+ const notes = [...diff.notes ?? []];
12604
+ if (!symbols) notes.push("symbol index missing \u2014 symbol-level attribution disabled");
12605
+ const fileByRel = new Map(graph.files.map((f) => [f.rel, f]));
12606
+ const defsByFile = /* @__PURE__ */ new Map();
12607
+ if (symbols) {
12608
+ for (const [name2, entries] of Object.entries(symbols.defs)) {
12609
+ for (const d of entries) {
12610
+ let arr = defsByFile.get(d.file);
12611
+ if (!arr) defsByFile.set(d.file, arr = []);
12612
+ arr.push({ name: name2, ...d });
12613
+ }
12614
+ }
12615
+ for (const arr of defsByFile.values()) arr.sort((a, b) => a.line - b.line || byStr(a.name, b.name));
12616
+ }
12617
+ const deleted = [];
12618
+ const unindexed = [];
12619
+ const changes = [];
12620
+ for (const df of [...diff.files].sort((a, b) => byStr(a.path, b.path))) {
12621
+ const carry = {
12622
+ ...df.oldPath !== void 0 ? { oldPath: df.oldPath } : {},
12623
+ ...df.binary ? { binary: true } : {},
12624
+ ...df.linesAdded !== void 0 ? { linesAdded: df.linesAdded } : {},
12625
+ ...df.linesDeleted !== void 0 ? { linesDeleted: df.linesDeleted } : {}
12626
+ };
12627
+ if (df.status === "deleted") {
12628
+ deleted.push(df.path);
12629
+ changes.push({ path: df.path, status: df.status, ...carry, hunks: [], symbols: [] });
12630
+ continue;
12631
+ }
12632
+ const node = fileByRel.get(df.path);
12633
+ if (!node) {
12634
+ unindexed.push(df.path);
12635
+ continue;
12636
+ }
12637
+ let hunks = diff.hunks.get(df.path) ?? [];
12638
+ if (!hunks.length && df.status === "added" && !df.binary) hunks = [{ start: 1, end: Math.max(node.lines, 1) }];
12639
+ const syms = df.binary ? [] : symbolsInHunks(defsByFile.get(df.path) ?? [], hunks);
12640
+ changes.push({
12641
+ path: df.path,
12642
+ status: df.status,
12643
+ ...carry,
12644
+ module: node.module,
12645
+ hunks: hunks.map((h) => ({ start: h.start, end: h.end })),
12646
+ symbols: syms
12647
+ });
12648
+ }
12649
+ const changedRels = new Set(changes.filter((c2) => c2.status !== "deleted").map((c2) => c2.path));
12650
+ 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));
12651
+ const byModule = /* @__PURE__ */ new Map();
12652
+ for (const c2 of changes) {
12653
+ if (c2.status === "deleted" || !c2.module) continue;
12654
+ let arr = byModule.get(c2.module);
12655
+ if (!arr) byModule.set(c2.module, arr = []);
12656
+ arr.push(c2);
12657
+ }
12658
+ const nonTestCode = /* @__PURE__ */ new Set();
12659
+ for (const f of graph.files) {
12660
+ if (f.fileKind === "code" && !f.testFile) nonTestCode.add(f.module);
12661
+ }
12662
+ const pagerankKnown = graph.modules.some((m) => m.pagerank !== void 0);
12663
+ const metricOf = (m) => pagerankKnown ? m.pagerank ?? 0 : m.degIn + m.degOut;
12664
+ const metricValues = graph.modules.map(metricOf);
12665
+ const metricName = pagerankKnown ? "pagerank" : "degree";
12666
+ const modules = [];
12667
+ for (const slug of [...byModule.keys()].sort(byStr)) {
12668
+ const m = graph.modules.find((x) => x.slug === slug);
12669
+ if (!m) continue;
12670
+ const moduleChanges = byModule.get(slug);
12671
+ const reasons = [];
12672
+ let score = 0;
12673
+ const exportedNames = [...new Set(moduleChanges.flatMap((c2) => c2.symbols.filter((s) => s.exported).map((s) => s.name)))].sort(byStr);
12674
+ if (exportedNames.length) {
12675
+ score += RISK_WEIGHTS.exportedChange;
12676
+ const shown = exportedNames.slice(0, 3).join(", ") + (exportedNames.length > 3 ? ", \u2026" : "");
12677
+ reasons.push(exportedNames.length === 1 ? `exported symbol ${shown} changed` : `exported symbols ${shown} changed`);
12678
+ }
12679
+ const pct = percentile(metricValues, metricOf(m));
12680
+ if (pct >= 0.9) {
12681
+ score += RISK_WEIGHTS.hubHigh;
12682
+ reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
12683
+ } else if (pct >= 0.75) {
12684
+ score += RISK_WEIGHTS.hubMed;
12685
+ reasons.push(`${metricName} p${Math.round(pct * 100)} hub`);
12686
+ }
12687
+ const depthByRel = /* @__PURE__ */ new Map();
12688
+ const impModules = /* @__PURE__ */ new Set();
12689
+ for (const c2 of moduleChanges) {
12690
+ const imp = impactOf(graph, c2.path, depth);
12691
+ if (!imp) continue;
12692
+ for (const f of imp.files) {
12693
+ const prev = depthByRel.get(f.rel);
12694
+ if (prev === void 0 || f.depth < prev) depthByRel.set(f.rel, f.depth);
12695
+ }
12696
+ for (const im of imp.modules) if (im !== slug) impModules.add(im);
12697
+ }
12698
+ const transitiveFiles = depthByRel.size;
12699
+ const directFiles = [...depthByRel.values()].filter((d) => d === 1).length;
12700
+ const impact = { directFiles, transitiveFiles, modules: [...impModules].sort(byStr) };
12701
+ if (transitiveFiles >= 20 || impact.modules.length >= 5) {
12702
+ score += RISK_WEIGHTS.blastHigh;
12703
+ reasons.push(`${transitiveFiles} dependent files across ${impact.modules.length} modules (depth ${depth})`);
12704
+ } else if (transitiveFiles >= 5) {
12705
+ score += RISK_WEIGHTS.blastMed;
12706
+ reasons.push(`${transitiveFiles} dependent files across ${impact.modules.length} modules (depth ${depth})`);
12707
+ }
12708
+ const testable = m.tier <= 1 && m.symbols > 0 && nonTestCode.has(slug);
12709
+ const coveredBy = m.testedBy ?? [];
12710
+ const tests = testable ? coveredBy.length ? { status: "covered", files: coveredBy } : { status: "gap", files: [] } : { status: "n/a", files: [] };
12711
+ if (tests.status === "gap") {
12712
+ score += RISK_WEIGHTS.testGap;
12713
+ reasons.push("no test covers this module");
12714
+ }
12715
+ const sup = (graph.surprises ?? []).find((s) => s.from === slug || s.to === slug);
12716
+ if (sup) {
12717
+ score += RISK_WEIGHTS.surprise;
12718
+ reasons.push(`cross-community edge to ${sup.from === slug ? sup.to : sup.from} (surprising)`);
12719
+ }
12720
+ const moduleDangling = dangling.filter((d) => moduleChanges.some((c2) => c2.path === d.from));
12721
+ if (moduleDangling.length) {
12722
+ score += RISK_WEIGHTS.dangling;
12723
+ const first = moduleDangling[0];
12724
+ const more = moduleDangling.length > 1 ? ` (+${moduleDangling.length - 1} more)` : "";
12725
+ reasons.push(`dangling import "${first.spec}" in ${first.from}${more}`);
12726
+ }
12727
+ score = Math.min(100, score);
12728
+ const changedFiles = moduleChanges.map((c2) => c2.path).sort(byStr);
12729
+ const allSyms = moduleChanges.flatMap((c2) => c2.symbols);
12730
+ const open = moduleChanges.slice().sort(
12731
+ (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)
12732
+ ).slice(0, OPEN_CAP).map((c2) => c2.path);
12733
+ modules.push({
12734
+ slug,
12735
+ path: m.path,
12736
+ score,
12737
+ bucket: score >= HIGH_MIN ? "HIGH" : score >= MEDIUM_MIN ? "MEDIUM" : "LOW",
12738
+ reasons,
12739
+ changedFiles,
12740
+ changedSymbols: {
12741
+ total: new Set(allSyms.map((s) => `${s.name}:${s.line}`)).size,
12742
+ exported: new Set(allSyms.filter((s) => s.exported).map((s) => `${s.name}:${s.line}`)).size
12743
+ },
12744
+ impact,
12745
+ tests,
12746
+ open
12747
+ });
12748
+ }
12749
+ modules.sort((a, b) => b.score - a.score || byStr(a.slug, b.slug));
12750
+ return {
12751
+ base: diff.base,
12752
+ ...graph.commit !== void 0 ? { indexCommit: graph.commit } : {},
12753
+ depth,
12754
+ changes,
12755
+ modules,
12756
+ dangling,
12757
+ deleted: deleted.sort(byStr),
12758
+ unindexed: unindexed.sort(byStr),
12759
+ notes
12760
+ };
12761
+ }
12762
+ function deltaFor(repo, graph, symbols, opts = {}) {
12763
+ if (!have("git")) return { error: "git is required for delta and was not found on PATH" };
12764
+ if (!isGitWorktree(repo)) return { error: `delta needs a git worktree \u2014 ${repo} is not inside one` };
12765
+ const notes = [];
12766
+ let base;
12767
+ if (opts.staged) {
12768
+ const head = sh("git", ["-C", repo, "rev-parse", "HEAD"]);
12769
+ if (!head.ok) return { error: "cannot resolve HEAD \u2014 empty repository?" };
12770
+ base = { ref: "HEAD", mergeBase: head.stdout.trim(), staged: true };
12771
+ } else {
12772
+ const r = resolveBaseRef(repo, opts.base);
12773
+ if ("error" in r) return { error: r.error };
12774
+ if (r.note) notes.push(r.note);
12775
+ base = { ref: r.ref, mergeBase: r.mergeBase, staged: false };
12776
+ }
12777
+ const spec = opts.staged ? { staged: true } : { mergeBase: base.mergeBase };
12778
+ const files = diffFiles(repo, spec);
12779
+ if (!opts.staged) {
12780
+ const known = new Set(files.map((f) => f.path));
12781
+ for (const u of untrackedFiles(repo)) {
12782
+ if (!known.has(u)) files.push({ path: u, status: "added" });
12783
+ }
12784
+ }
12785
+ return computeDelta(graph, symbols, { files, hunks: diffHunks(repo, spec), base, notes }, opts.depth ?? DEFAULT_DELTA_DEPTH);
12786
+ }
12787
+ function formatDeltaPanel(res) {
12788
+ const mb = res.base.mergeBase.slice(0, 7);
12789
+ const vs = `${res.base.staged ? "staged vs " : ""}${res.base.ref}`;
12790
+ if (!res.changes.length && !res.unindexed.length) {
12791
+ return `codeindex: no changes vs ${vs} (merge-base ${mb})
12792
+ `;
12793
+ }
12794
+ const changedCount = res.changes.length + res.unindexed.length;
12795
+ const lines = [
12796
+ `codeindex: delta vs ${vs} (merge-base ${mb}) \u2014 ${changedCount} changed file(s), ${res.modules.length} module(s)${res.indexCommit ? `, index @ ${res.indexCommit}` : ""}`
12797
+ ];
12798
+ for (const n of res.notes) lines.push(` note: ${n}`);
12799
+ for (const m of res.modules) {
12800
+ lines.push(` ${m.bucket.padEnd(6)} ${m.slug} score ${m.score}${m.reasons.length ? ` \u2014 ${m.reasons.join("; ")}` : ""}`);
12801
+ const tests = m.tests.status === "gap" ? "GAP" : m.tests.status === "covered" ? `covered (${m.tests.files.length})` : "n/a";
12802
+ lines.push(` open: ${m.open.join(", ") || "\u2014"} \xB7 tests: ${tests}`);
12803
+ }
12804
+ if (res.dangling.length) {
12805
+ lines.push(` dangling: ${res.dangling.map((d) => `${d.spec} (from ${d.from})`).join(" \xB7 ")}`);
12806
+ }
12807
+ if (res.deleted.length) lines.push(` deleted: ${res.deleted.join(", ")}`);
12808
+ if (res.unindexed.length) lines.push(` unindexed: ${res.unindexed.join(", ")}`);
12809
+ return lines.join("\n") + "\n";
12810
+ }
12811
+
12812
+ // src/engine.ts
11733
12813
  init_mcp();
11734
12814
  init_rewrite();
11735
12815
  init_hash();
@@ -11740,13 +12820,14 @@ init_util();
11740
12820
  init_types();
11741
12821
  init_types();
11742
12822
  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";
12823
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
12824
+ import { join as join19, resolve as resolve2 } from "path";
11745
12825
  init_pipeline();
11746
12826
  init_hash();
11747
12827
  init_graph_json();
11748
12828
  init_symbols_json();
11749
12829
  init_scan();
12830
+ init_preload();
11750
12831
  init_walk();
11751
12832
  init_callers();
11752
12833
  init_workspaces();
@@ -11814,6 +12895,13 @@ Commands:
11814
12895
  complexity Cyclomatic-complexity estimates, most-complex first. Pass a file
11815
12896
  positional for one file; omit for the repo-wide top
11816
12897
  risk Complexity \xD7 git-churn ranking (JSON; --since <ref> to bound)
12898
+ delta Review panel for the git diff: changed files -> enclosing symbols ->
12899
+ blast radius -> risk score with explained reasons
12900
+ (--base <ref> | --staged, --depth <n>, --json)
12901
+ impact Reverse dependency closure of a file or module: everything that
12902
+ transitively imports/uses/calls it (--depth <n>; JSON)
12903
+ neighbors Graph neighbours of a file or module, both directions
12904
+ (--depth <n>, --kind import,call,use,doc-link,mention; JSON)
11817
12905
  mermaid Mermaid diagram of the module graph; pass a module positional to
11818
12906
  focus on one neighborhood
11819
12907
  rewrite Map an expensive tree-wide search onto its indexed equivalent:
@@ -11828,7 +12916,10 @@ Commands:
11828
12916
  check_rules, the memory quartet and the three symbolic-edit
11829
12917
  writes). Flags: --repo <dir> pins ONE repository so the per-tool
11830
12918
  repo argument becomes optional (an explicit per-call repo still
11831
- wins); --server-name <name> overrides the announced serverInfo
12919
+ wins); --server-name <name> overrides the announced serverInfo;
12920
+ --max-response-bytes <n> caps a single tool response (default 1e6;
12921
+ a response under the cap is byte-identical, one over it is
12922
+ replaced by an actionable notice instead of an unusable blob)
11832
12923
  version Print the engine version
11833
12924
 
11834
12925
  Flags (accepted before OR after the subcommand: '--repo X scan' and
@@ -11848,6 +12939,16 @@ Flags (accepted before OR after the subcommand: '--repo X scan' and
11848
12939
  --max-bytes <n> Skip files above this size (default 1 MiB)
11849
12940
  --max-calls <n> Per-file call-site cap for extraction (default 512)
11850
12941
  --no-ast Skip tree-sitter grammars even when present (regex tier)
12942
+ --workers <n> \`index\`: extraction worker threads (default: cores-1,
12943
+ capped at 8; 0 or 1 forces the single-threaded path).
12944
+ Also settable with CODEINDEX_WORKERS. Artifacts are
12945
+ byte-identical either way
12946
+ --index <dir> Persisted index the READ commands reuse, relative to the
12947
+ repo (default .codeindex \u2014 i.e. what \`index --out\` wrote
12948
+ there). A fresh index turns the scan into a stat pass and,
12949
+ when it still matches the worktree, skips the pipeline
12950
+ entirely. Stale/absent/corrupt \u2192 a normal cold build
12951
+ --no-index-cache Never reuse a persisted index; always build from scratch
11851
12952
  --config <file> Rules config for \`rules\` (JSON: [{name, from, to, \u2026}])
11852
12953
  --limit <n> Max results for \`search\` (default 20)
11853
12954
  --no-fuzzy \`search\`: disable trigram fuzzy fallback for query terms
@@ -11871,7 +12972,7 @@ function parseFlags(args2) {
11871
12972
  if (v === void 0) throw new Error(`missing value for ${a}`);
11872
12973
  return v;
11873
12974
  };
11874
- const num = () => {
12975
+ const num2 = () => {
11875
12976
  const raw = next();
11876
12977
  const n = Number(raw);
11877
12978
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${a} expects a positive number, got "${raw}"`);
@@ -11887,20 +12988,32 @@ function parseFlags(args2) {
11887
12988
  else if (a === "--scope") flags2.scope = next();
11888
12989
  else if (a === "--no-gitignore") flags2.gitignore = false;
11889
12990
  else if (a === "--ignore-dir") flags2.ignoreDirs.push(next());
11890
- else if (a === "--max-files") flags2.maxFiles = num();
11891
- else if (a === "--max-bytes") flags2.maxBytes = num();
11892
- else if (a === "--max-calls") flags2.maxCalls = num();
12991
+ else if (a === "--max-files") flags2.maxFiles = num2();
12992
+ else if (a === "--max-bytes") flags2.maxBytes = num2();
12993
+ else if (a === "--max-calls") flags2.maxCalls = num2();
11893
12994
  else if (a === "--ignore-case") flags2.ignoreCase = true;
11894
- else if (a === "--max-hits") flags2.maxHits = num();
11895
- else if (a === "--budget-tokens") flags2.budgetTokens = num();
12995
+ else if (a === "--max-hits") flags2.maxHits = num2();
12996
+ else if (a === "--budget-tokens") flags2.budgetTokens = num2();
11896
12997
  else if (a === "--no-ast") flags2.noAst = true;
11897
- else if (a === "--since") flags2.since = next();
12998
+ else if (a === "--index") flags2.indexDir = next();
12999
+ else if (a === "--no-index-cache") flags2.noIndexCache = true;
13000
+ else if (a === "--workers") {
13001
+ const raw = next();
13002
+ const n = Number(raw);
13003
+ if (!Number.isInteger(n) || n < 0) throw new Error(`--workers expects a non-negative integer, got "${raw}"`);
13004
+ flags2.workers = n;
13005
+ } else if (a === "--since") flags2.since = next();
11898
13006
  else if (a === "--config") flags2.config = resolve2(next());
11899
- else if (a === "--limit") flags2.limit = num();
13007
+ else if (a === "--limit") flags2.limit = num2();
11900
13008
  else if (a === "--no-fuzzy") flags2.fuzzy = false;
11901
13009
  else if (a === "--semantic") flags2.semantic = true;
11902
13010
  else if (a === "--recall") flags2.recall = true;
11903
13011
  else if (a === "--run") flags2.run = true;
13012
+ else if (a === "--base") flags2.base = next();
13013
+ else if (a === "--staged") flags2.staged = true;
13014
+ else if (a === "--depth") flags2.depth = num2();
13015
+ else if (a === "--kind") flags2.kind = next();
13016
+ else if (a === "--json") flags2.json = true;
11904
13017
  else if (!a.startsWith("--") && flags2.positional === void 0) flags2.positional = a;
11905
13018
  else throw new Error(`unknown flag: ${a}`);
11906
13019
  }
@@ -11930,6 +13043,7 @@ var SCANLESS_COMMANDS = /* @__PURE__ */ new Set(["grep", "churn", "coupling", "w
11930
13043
  function parseMcpFlags(argv) {
11931
13044
  let defaultRepo;
11932
13045
  let name2;
13046
+ let maxResponseBytes;
11933
13047
  for (let i2 = 0; i2 < argv.length; i2++) {
11934
13048
  const a = argv[i2];
11935
13049
  if (a === "--repo") {
@@ -11940,12 +13054,17 @@ function parseMcpFlags(argv) {
11940
13054
  const v = argv[++i2];
11941
13055
  if (!v) throw new Error("--server-name requires a value");
11942
13056
  name2 = v;
13057
+ } else if (a === "--max-response-bytes") {
13058
+ const v = argv[++i2];
13059
+ const n = Number(v);
13060
+ if (!v || !Number.isFinite(n) || n <= 0) throw new Error("--max-response-bytes requires a positive number");
13061
+ maxResponseBytes = n;
11943
13062
  } else {
11944
13063
  throw new Error(`unknown flag for \`mcp\`: ${a}`);
11945
13064
  }
11946
13065
  }
11947
- if (defaultRepo && !existsSync5(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
11948
- return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0 };
13066
+ if (defaultRepo && !existsSync7(defaultRepo)) throw new Error(`--repo path does not exist: ${defaultRepo}`);
13067
+ return { defaultRepo, serverInfo: name2 ? { name: name2 } : void 0, maxResponseBytes };
11949
13068
  }
11950
13069
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
11951
13070
  "--repo",
@@ -11963,7 +13082,10 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
11963
13082
  "--since",
11964
13083
  "--config",
11965
13084
  "--limit",
11966
- "--server-name"
13085
+ "--server-name",
13086
+ "--workers",
13087
+ "--index",
13088
+ "--max-response-bytes"
11967
13089
  ]);
11968
13090
  function hoistLeadingFlags(argv) {
11969
13091
  const lead = [];
@@ -12008,7 +13130,7 @@ async function runCli(rawArgv) {
12008
13130
  return;
12009
13131
  }
12010
13132
  const flags2 = parseFlags(rest);
12011
- if (!existsSync5(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
13133
+ if (!existsSync7(flags2.repo)) throw new Error(`--repo path does not exist: ${flags2.repo}`);
12012
13134
  const scans = !SCANLESS_COMMANDS.has(cmd) && !(cmd === "embed" && flags2.positional !== "build");
12013
13135
  let precomputedWalk;
12014
13136
  if (scans && !flags2.noAst) {
@@ -12020,15 +13142,33 @@ async function runCli(rawArgv) {
12020
13142
  });
12021
13143
  await ensureGrammars(grammarKeysForExts(precomputedWalk.files.map((f) => f.ext)));
12022
13144
  }
13145
+ const indexDir = flags2.indexDir ?? INDEX_DIR;
13146
+ let preloadTried = false;
13147
+ let preloaded;
13148
+ const tryPreload = () => {
13149
+ if (preloadTried) return preloaded;
13150
+ preloadTried = true;
13151
+ if (flags2.noIndexCache) return void 0;
13152
+ const p = preloadSession(flags2.repo, scanOptions(flags2, precomputedWalk), indexDir);
13153
+ if (p) preloaded = { scan: p.scan, arts: p.arts };
13154
+ return preloaded;
13155
+ };
13156
+ const readScan = () => tryPreload()?.scan ?? scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13157
+ const readArtifacts = () => {
13158
+ const p = tryPreload();
13159
+ if (p?.arts) return p.arts;
13160
+ if (p) return buildArtifactsFromScan(p.scan, scanOptions(flags2, precomputedWalk));
13161
+ return buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13162
+ };
12023
13163
  if (cmd === "index") {
12024
13164
  if (!flags2.out) throw new Error("index needs --out <dir>");
12025
13165
  const outDir = flags2.out;
12026
13166
  mkdirSync3(outDir, { recursive: true });
12027
- const cachePath = join15(outDir, "cache.json");
13167
+ const cachePath = join19(outDir, "cache.json");
12028
13168
  let cache;
12029
13169
  let meta = {};
12030
13170
  try {
12031
- const parsed = JSON.parse(readFileSync8(cachePath, "utf8"));
13171
+ const parsed = JSON.parse(readFileSync9(cachePath, "utf8"));
12032
13172
  if (parsed.schemaVersion === SCHEMA_VERSION && parsed.extractorVersion === EXTRACTOR_VERSION) {
12033
13173
  cache = new Map(Object.entries(parsed.files));
12034
13174
  meta = {
@@ -12041,15 +13181,20 @@ async function runCli(rawArgv) {
12041
13181
  }
12042
13182
  } catch {
12043
13183
  }
12044
- const scan2 = scanRepo(flags2.repo, { ...scanOptions(flags2, precomputedWalk), cache, out: outDir });
13184
+ const scan2 = await scanRepoParallel(flags2.repo, {
13185
+ ...scanOptions(flags2, precomputedWalk),
13186
+ cache,
13187
+ out: outDir,
13188
+ workers: flags2.workers
13189
+ });
12045
13190
  const modelDir = resolveEmbedModelDir(flags2.repo);
12046
13191
  const model = modelDir ? loadEmbedModel(modelDir) : void 0;
12047
- const graphPath = join15(outDir, "graph.json");
12048
- const symbolsPath = join15(outDir, "symbols.json");
12049
- const embedPath = join15(outDir, "embeddings.bin");
13192
+ const graphPath = join19(outDir, "graph.json");
13193
+ const symbolsPath = join19(outDir, "symbols.json");
13194
+ const embedPath = join19(outDir, "embeddings.bin");
12050
13195
  const artifactSha = (path) => {
12051
13196
  try {
12052
- return sha1(readFileSync8(path));
13197
+ return sha1(readFileSync9(path));
12053
13198
  } catch {
12054
13199
  return void 0;
12055
13200
  }
@@ -12104,23 +13249,23 @@ async function runCli(rawArgv) {
12104
13249
  `);
12105
13250
  }
12106
13251
  } else if (cmd === "scan") {
12107
- const { scan: scan2 } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13252
+ const s = scanSummary(flags2.repo, scanOptions(flags2, precomputedWalk));
12108
13253
  const summary = {
12109
13254
  engineVersion: ENGINE_VERSION,
12110
- commit: scan2.commit,
12111
- fileCount: scan2.files.length,
12112
- languages: scan2.languages,
12113
- capped: scan2.capped
13255
+ commit: s.commit,
13256
+ fileCount: s.fileCount,
13257
+ languages: s.languages,
13258
+ capped: s.capped
12114
13259
  };
12115
13260
  emit(JSON.stringify(summary, null, 2) + "\n", flags2.out);
12116
13261
  } else if (cmd === "graph") {
12117
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13262
+ const { graph } = readArtifacts();
12118
13263
  emit(renderGraphJson(graph), flags2.out);
12119
13264
  } else if (cmd === "symbols") {
12120
- const { symbols } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13265
+ const { symbols } = readArtifacts();
12121
13266
  emit(renderSymbolsJson(symbols), flags2.out);
12122
13267
  } else if (cmd === "scip") {
12123
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13268
+ const scan2 = readScan();
12124
13269
  const bytes = renderScip(scan2, { projectRoot: flags2.projectRoot });
12125
13270
  const out2 = flags2.out ?? resolve2("index.scip");
12126
13271
  if (out2 === "-") process.stdout.write(Buffer.from(bytes));
@@ -12130,14 +13275,14 @@ async function runCli(rawArgv) {
12130
13275
  `);
12131
13276
  }
12132
13277
  } else if (cmd === "callers") {
12133
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13278
+ const scan2 = readScan();
12134
13279
  const index = buildCallerIndex(scan2, void 0, { recall: flags2.recall });
12135
13280
  const obj = {};
12136
13281
  for (const [name2, entry] of index) obj[name2] = entry;
12137
13282
  emit(JSON.stringify(obj, null, 2) + "\n", flags2.out);
12138
13283
  } else if (cmd === "search") {
12139
13284
  if (!flags2.positional) throw new Error('search needs a query: cli.mjs search "<query>" --repo <dir>');
12140
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13285
+ const scan2 = readScan();
12141
13286
  if (flags2.semantic) {
12142
13287
  const endpoint = resolveEmbedEndpoint();
12143
13288
  const lexical = () => {
@@ -12228,16 +13373,16 @@ async function runCli(rawArgv) {
12228
13373
  }
12229
13374
  const model = loadEmbedModel(modelDir);
12230
13375
  mkdirSync3(flags2.out, { recursive: true });
12231
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13376
+ const scan2 = readScan();
12232
13377
  const index = buildEmbeddingIndex(scan2, model);
12233
- writeFileSync4(join15(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
13378
+ writeFileSync4(join19(flags2.out, "embeddings.bin"), serializeEmbeddings(index));
12234
13379
  process.stderr.write(`codeindex: ${index.records.length} embedding records \u2192 ${flags2.out}/embeddings.bin (model ${model.modelId})
12235
13380
  `);
12236
13381
  } else if (sub === "pull") {
12237
13382
  const { url, sha256 } = resolveEmbedPullUrl();
12238
- const destDir = process.env.CODEINDEX_EMBED_DIR ?? join15(flags2.repo, ".codeindex", "models");
13383
+ const destDir = process.env.CODEINDEX_EMBED_DIR ?? join19(flags2.repo, ".codeindex", "models");
12239
13384
  mkdirSync3(destDir, { recursive: true });
12240
- process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join15(destDir, "model.json")}
13385
+ process.stderr.write(`codeindex: fetching model from ${url} \u2192 ${join19(destDir, "model.json")}
12241
13386
  `);
12242
13387
  let body2;
12243
13388
  try {
@@ -12258,8 +13403,8 @@ async function runCli(rawArgv) {
12258
13403
  process.exitCode = 1;
12259
13404
  return;
12260
13405
  }
12261
- writeFileSync4(join15(destDir, "model.json"), body2);
12262
- process.stderr.write(`codeindex: model written to ${join15(destDir, "model.json")}
13406
+ writeFileSync4(join19(destDir, "model.json"), body2);
13407
+ process.stderr.write(`codeindex: model written to ${join19(destDir, "model.json")}
12263
13408
  `);
12264
13409
  } else {
12265
13410
  throw new Error("embed needs a subcommand: status | build | pull | serve");
@@ -12269,7 +13414,7 @@ async function runCli(rawArgv) {
12269
13414
  const cacheDir = sharedGrammarsCacheDir();
12270
13415
  if (sub === "status") {
12271
13416
  const info2 = resolveGrammarsTier();
12272
- const runtimePresent = info2.dir ? existsSync5(join15(info2.dir, "web-tree-sitter.wasm")) : false;
13417
+ const runtimePresent = info2.dir ? existsSync7(join19(info2.dir, "web-tree-sitter.wasm")) : false;
12273
13418
  const target = resolveGrammarsPullTarget();
12274
13419
  const status = {
12275
13420
  engineVersion: ENGINE_VERSION,
@@ -12290,8 +13435,8 @@ async function runCli(rawArgv) {
12290
13435
  }
12291
13436
  } else if (cmd === "rules") {
12292
13437
  if (!flags2.config) throw new Error("rules needs --config <codeindex.rules.json>");
12293
- const rules = parseRules(JSON.parse(readFileSync8(flags2.config, "utf8")));
12294
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13438
+ const rules = parseRules(JSON.parse(readFileSync9(flags2.config, "utf8")));
13439
+ const { graph } = readArtifacts();
12295
13440
  const violations = checkRules(graph, rules);
12296
13441
  const errors = violations.filter((v) => v.severity === "error").length;
12297
13442
  emit(JSON.stringify({ errors, warnings: violations.length - errors, violations }, null, 2) + "\n", flags2.out);
@@ -12312,26 +13457,48 @@ async function runCli(rawArgv) {
12312
13457
  for (const k of [...churn.keys()].sort()) sorted[k] = churn.get(k);
12313
13458
  emit(JSON.stringify({ ok, churn: sorted }, null, 2) + "\n", flags2.out);
12314
13459
  } else if (cmd === "repomap") {
12315
- const { scan: scan2, graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13460
+ const { scan: scan2, graph } = readArtifacts();
12316
13461
  emit(renderRepoMap(scan2, graph, { budgetTokens: flags2.budgetTokens }), flags2.out);
12317
13462
  } else if (cmd === "hotspots") {
12318
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13463
+ const scan2 = readScan();
12319
13464
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
12320
13465
  emit(JSON.stringify({ churnOk: ok, hotspots: rankHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
12321
13466
  } else if (cmd === "coupling") {
12322
13467
  const { ok, couplings } = changeCoupling(flags2.repo, { since: flags2.since });
12323
13468
  emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
12324
13469
  } else if (cmd === "deadcode") {
12325
- emit(JSON.stringify(findDeadCode(scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk))), null, 2) + "\n", flags2.out);
13470
+ emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags2.out);
12326
13471
  } else if (cmd === "complexity") {
12327
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13472
+ const scan2 = readScan();
12328
13473
  emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
12329
13474
  } else if (cmd === "risk") {
12330
- const scan2 = scanRepo(flags2.repo, scanOptions(flags2, precomputedWalk));
13475
+ const scan2 = readScan();
12331
13476
  const { churn, ok } = gitChurn(flags2.repo, { since: flags2.since });
12332
13477
  emit(JSON.stringify({ churnOk: ok, risks: riskHotspots(scan2, churn) }, null, 2) + "\n", flags2.out);
13478
+ } else if (cmd === "delta") {
13479
+ const { graph, symbols } = readArtifacts();
13480
+ const res = deltaFor(flags2.repo, graph, symbols, {
13481
+ base: flags2.base,
13482
+ staged: flags2.staged,
13483
+ depth: flags2.depth
13484
+ });
13485
+ if ("error" in res) throw new Error(res.error);
13486
+ emit(flags2.json ? JSON.stringify(res, null, 2) + "\n" : formatDeltaPanel(res), flags2.out);
13487
+ } else if (cmd === "impact") {
13488
+ if (!flags2.positional) throw new Error("impact needs a target: cli.mjs impact <file|module> --repo <dir>");
13489
+ const { graph } = readArtifacts();
13490
+ const res = impactOf(graph, flags2.positional, flags2.depth ?? Infinity);
13491
+ if (!res) throw new Error(`no such file or module in the index: ${flags2.positional}`);
13492
+ emit(JSON.stringify(res, null, 2) + "\n", flags2.out);
13493
+ } else if (cmd === "neighbors") {
13494
+ if (!flags2.positional) throw new Error("neighbors needs a target: cli.mjs neighbors <file|module> --repo <dir>");
13495
+ const { graph } = readArtifacts();
13496
+ const kinds = flags2.kind ? new Set(flags2.kind.split(",").map((k) => k.trim()).filter(Boolean)) : void 0;
13497
+ const res = neighborsOf(graph, flags2.positional, flags2.depth ?? 1, kinds);
13498
+ if (!res) throw new Error(`no such file or module in the index: ${flags2.positional}`);
13499
+ emit(JSON.stringify(res, null, 2) + "\n", flags2.out);
12333
13500
  } else if (cmd === "mermaid") {
12334
- const { graph } = buildIndexArtifacts(flags2.repo, scanOptions(flags2, precomputedWalk));
13501
+ const { graph } = readArtifacts();
12335
13502
  emit(renderMermaid(graph, { module: flags2.positional }), flags2.out);
12336
13503
  } else if (cmd === "grep") {
12337
13504
  if (!flags2.positional) throw new Error("grep needs a pattern: cli.mjs grep <pattern> --repo <dir>");
@@ -12351,12 +13518,15 @@ ${HELP}`);
12351
13518
  }
12352
13519
  }
12353
13520
  export {
13521
+ DEFAULT_DELTA_DEPTH,
12354
13522
  DEFAULT_GRAMMARS_URL,
12355
13523
  DEFAULT_MAX_FILES,
12356
13524
  EMBED_VERSION,
12357
13525
  ENGINE_VERSION,
12358
13526
  EXTRACTOR_VERSION,
13527
+ INDEX_DIR,
12359
13528
  MARKDOWN_EXT,
13529
+ RISK_WEIGHTS,
12360
13530
  SCHEMA_VERSION,
12361
13531
  allGrammarKeys,
12362
13532
  applyCentrality,
@@ -12364,6 +13534,7 @@ export {
12364
13534
  betweennessOf,
12365
13535
  buildArtifactsFromScan,
12366
13536
  buildCallerIndex,
13537
+ buildCodeRecord,
12367
13538
  buildEmbeddingIndex,
12368
13539
  buildEndpointIndex,
12369
13540
  buildGraph,
@@ -12384,11 +13555,13 @@ export {
12384
13555
  communityOf,
12385
13556
  compileGlobs,
12386
13557
  complexityOfSource,
13558
+ computeDelta,
12387
13559
  computeImportPairs,
12388
13560
  computeSurprises,
12389
13561
  computeSymbolRefs,
12390
13562
  computeTestMap,
12391
13563
  deleteMemory,
13564
+ deltaFor,
12392
13565
  deserializeEmbeddings,
12393
13566
  detectCommunities,
12394
13567
  detectWorkspaces,
@@ -12406,6 +13579,7 @@ export {
12406
13579
  extractAst,
12407
13580
  extractCode,
12408
13581
  extractGrammarsTarball,
13582
+ extractInParallel,
12409
13583
  extractMarkdown,
12410
13584
  extractSymbols,
12411
13585
  extractTarInto,
@@ -12415,6 +13589,7 @@ export {
12415
13589
  findReferences,
12416
13590
  findSymbol,
12417
13591
  foldText,
13592
+ formatDeltaPanel,
12418
13593
  gitChurn,
12419
13594
  grammarKeyForExt,
12420
13595
  grammarKeysForExts,
@@ -12424,6 +13599,8 @@ export {
12424
13599
  have,
12425
13600
  headCommit,
12426
13601
  healthzUrl,
13602
+ hubThreshold,
13603
+ impactOf,
12427
13604
  insertAfterSymbol,
12428
13605
  insertBeforeSymbol,
12429
13606
  intDot,
@@ -12434,22 +13611,28 @@ export {
12434
13611
  isSurprising,
12435
13612
  isTestFile,
12436
13613
  isTestPath,
13614
+ keptCodeFiles,
12437
13615
  keywords,
12438
13616
  languageOf,
12439
13617
  listMemories,
12440
13618
  loadEmbedModel,
13619
+ neighborsOf,
12441
13620
  pagerankOf,
12442
13621
  parseGitignore,
12443
13622
  parseRules,
13623
+ preloadArtifacts,
13624
+ preloadSession,
12444
13625
  probeEndpoint,
12445
13626
  pullGrammars,
12446
13627
  quantize,
12447
13628
  rankHotspots,
12448
13629
  rankedKeywords,
12449
13630
  readMemory,
13631
+ readPersistedIndex,
12450
13632
  readText,
12451
13633
  renderGraphJson,
12452
13634
  renderMermaid,
13635
+ renderMermaidClustered,
12453
13636
  renderRepoMap,
12454
13637
  renderScip,
12455
13638
  renderSymbolsJson,
@@ -12465,13 +13648,17 @@ export {
12465
13648
  resolveGrammarsTier,
12466
13649
  resolveImport,
12467
13650
  resolveUniqueSymbol,
13651
+ reverseClosure,
12468
13652
  rewriteCommand,
12469
13653
  riskHotspots,
12470
13654
  roundHalfToEven,
12471
13655
  rrf,
12472
13656
  runCli,
13657
+ runExtractWorker,
12473
13658
  runMcpServer,
12474
13659
  scanRepo,
13660
+ scanRepoParallel,
13661
+ scanSummary,
12475
13662
  searchIndex,
12476
13663
  searchSemantic,
12477
13664
  serializeEmbeddings,
@@ -12482,9 +13669,11 @@ export {
12482
13669
  slugify,
12483
13670
  subtokens,
12484
13671
  symbolComplexity,
13672
+ symbolsInHunks,
12485
13673
  symbolsOverview,
12486
13674
  testsForModule,
12487
13675
  tierForPath,
13676
+ toCacheMap,
12488
13677
  tokenize,
12489
13678
  uniqueSymbolDefs,
12490
13679
  untestedModules,
@@ -12492,6 +13681,7 @@ export {
12492
13681
  walk,
12493
13682
  warmGrammars,
12494
13683
  wordpiece,
13684
+ workerCount,
12495
13685
  writeMemory
12496
13686
  };
12497
13687
  // "Copyright" and "@license" are already caught by DIRECTIVE_RE.