sigmap 8.33.0 → 8.35.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +12 -12
  3. package/gen-context.js +531 -74
  4. package/llms-full.txt +9 -9
  5. package/llms.txt +7 -7
  6. package/package.json +1 -1
  7. package/packages/cli/package.json +1 -1
  8. package/packages/core/index.js +1 -0
  9. package/packages/core/package.json +1 -1
  10. package/src/config/loader.js +1 -1
  11. package/src/config/tune.js +1 -1
  12. package/src/discovery/language-detector.js +1 -0
  13. package/src/discovery/source-root-registry.js +10 -1
  14. package/src/discovery/source-root-scorer.js +1 -1
  15. package/src/extractors/cpp.js +8 -3
  16. package/src/extractors/csharp.js +8 -3
  17. package/src/extractors/css.js +1 -1
  18. package/src/extractors/dart.js +8 -3
  19. package/src/extractors/deps.js +23 -1
  20. package/src/extractors/dispatch.js +2 -0
  21. package/src/extractors/dockerfile.js +1 -1
  22. package/src/extractors/gdscript.js +2 -2
  23. package/src/extractors/go.js +13 -5
  24. package/src/extractors/html.js +1 -1
  25. package/src/extractors/javascript.js +7 -3
  26. package/src/extractors/kotlin.js +8 -3
  27. package/src/extractors/lua.js +152 -0
  28. package/src/extractors/markdown.js +1 -1
  29. package/src/extractors/php.js +8 -3
  30. package/src/extractors/properties.js +1 -1
  31. package/src/extractors/python.js +5 -2
  32. package/src/extractors/python_dataclass.js +3 -1
  33. package/src/extractors/r.js +1 -1
  34. package/src/extractors/ruby.js +1 -1
  35. package/src/extractors/rust.js +13 -5
  36. package/src/extractors/scala.js +8 -3
  37. package/src/extractors/shell.js +1 -1
  38. package/src/extractors/svelte.js +1 -1
  39. package/src/extractors/swift.js +8 -3
  40. package/src/extractors/toml.js +1 -1
  41. package/src/extractors/typescript.js +8 -4
  42. package/src/extractors/typescript_react.js +1 -1
  43. package/src/extractors/vue_sfc.js +3 -1
  44. package/src/extractors/xml.js +3 -1
  45. package/src/extractors/yaml.js +1 -1
  46. package/src/graph/call-graph.js +81 -1
  47. package/src/mcp/server.js +46 -4
  48. package/src/retrieval/ranker.js +14 -5
package/gen-context.js CHANGED
@@ -1644,7 +1644,7 @@ __factories["./src/config/loader"] = function(module, exports) {
1644
1644
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
1645
1645
  '.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
1646
1646
  '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
1647
- '.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
1647
+ '.swift', '.dart', '.scala', '.sc', '.lua', '.vue', '.svelte',
1648
1648
  '.html', '.htm', '.css', '.scss', '.sass', '.less',
1649
1649
  '.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
1650
1650
  '.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
@@ -1968,7 +1968,7 @@ __factories["./src/config/tune"] = function(module, exports) {
1968
1968
  const SOURCE_EXTS = new Set([
1969
1969
  '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
1970
1970
  '.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
1971
- '.scala', '.php', '.gd', '.r', '.R',
1971
+ '.scala', '.php', '.lua', '.gd', '.r', '.R',
1972
1972
  ]);
1973
1973
 
1974
1974
  /** Raw user config file content, or null when absent/unparsable. */
@@ -3128,6 +3128,7 @@ __factories["./src/discovery/language-detector"] = function(module, exports) {
3128
3128
  '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
3129
3129
  '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
3130
3130
  '.dart': 'dart', '.scala': 'scala', '.php': 'php',
3131
+ '.lua': 'lua',
3131
3132
  '.gd': 'gdscript',
3132
3133
  '.r': 'r', '.R': 'r',
3133
3134
  };
@@ -3558,12 +3559,21 @@ __factories["./src/discovery/source-root-registry"] = function(module, exports)
3558
3559
  akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
3559
3560
  play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
3560
3561
  spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
3561
- zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
3562
+ zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
3562
3563
  },
3563
3564
  srcDirs: ['src/main/scala','src'],
3564
3565
  penalties: ['target'],
3565
3566
  },
3566
3567
 
3568
+ lua: {
3569
+ manifestFiles: ['.luarc.json', 'selene.toml', 'stylua.toml'],
3570
+ frameworks: {
3571
+ luarocks: { detectionFiles: ['*.rockspec'], srcDirs: ['src','lua','lib'] },
3572
+ },
3573
+ srcDirs: ['src','lua','lib'],
3574
+ penalties: ['.luarocks','luarocks_modules'],
3575
+ },
3576
+
3567
3577
  r: {
3568
3578
  manifestFiles: ['DESCRIPTION','renv.lock'],
3569
3579
  frameworks: {
@@ -3797,7 +3807,7 @@ __factories["./src/discovery/source-root-scorer"] = function(module, exports) {
3797
3807
  const CODE_EXTS = new Set([
3798
3808
  '.js','.mjs','.cjs','.ts','.tsx','.jsx',
3799
3809
  '.py','.rb','.go','.rs','.java','.kt',
3800
- '.cs','.cpp','.c','.h','.swift','.dart','.scala','.php',
3810
+ '.cs','.cpp','.c','.h','.swift','.dart','.scala','.php','.lua',
3801
3811
  ]);
3802
3812
 
3803
3813
  const AUTO_SKIP = new Set([
@@ -5459,8 +5469,13 @@ __factories["./src/extractors/cpp"] = function(module, exports) {
5459
5469
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5460
5470
  // governs output rather than a literal buried here, and omissions are disclosed
5461
5471
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
5462
- const MEMBER_LIMIT = 8;
5463
- const PER_FILE_LIMIT = 25;
5472
+ // Class bodies are scanned to this many characters. Real classes routinely
5473
+ // run past the old 4KB scan window — truncating there silently hid every
5474
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
5475
+ // ceiling only guards against pathological input (Java parity, #551).
5476
+ const MAX_CLASS_BODY_CHARS = 200000;
5477
+ const MEMBER_LIMIT = 120;
5478
+ const PER_FILE_LIMIT = 200;
5464
5479
 
5465
5480
  /**
5466
5481
  * Extract signatures from C/C++ source code.
@@ -5497,7 +5512,7 @@ __factories["./src/extractors/cpp"] = function(module, exports) {
5497
5512
 
5498
5513
  function extractBlock(src, startIndex) {
5499
5514
  let depth = 1, i = startIndex;
5500
- const end = Math.min(src.length, startIndex + 4000);
5515
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
5501
5516
  while (i < end && depth > 0) {
5502
5517
  if (src[i] === '{') depth++;
5503
5518
  else if (src[i] === '}') depth--;
@@ -5541,8 +5556,13 @@ __factories["./src/extractors/csharp"] = function(module, exports) {
5541
5556
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5542
5557
  // governs output rather than a literal buried here, and omissions are disclosed
5543
5558
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
5544
- const MEMBER_LIMIT = 8;
5545
- const PER_FILE_LIMIT = 25;
5559
+ // Class bodies are scanned to this many characters. Real classes routinely
5560
+ // run past the old 4KB scan window — truncating there silently hid every
5561
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
5562
+ // ceiling only guards against pathological input (Java parity, #551).
5563
+ const MAX_CLASS_BODY_CHARS = 200000;
5564
+ const MEMBER_LIMIT = 120;
5565
+ const PER_FILE_LIMIT = 200;
5546
5566
 
5547
5567
  /**
5548
5568
  * Extract signatures from C# source code.
@@ -5577,7 +5597,7 @@ __factories["./src/extractors/csharp"] = function(module, exports) {
5577
5597
 
5578
5598
  function extractBlock(src, startIndex) {
5579
5599
  let depth = 1, i = startIndex;
5580
- const end = Math.min(src.length, startIndex + 5000);
5600
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
5581
5601
  while (i < end && depth > 0) {
5582
5602
  if (src[i] === '{') depth++;
5583
5603
  else if (src[i] === '}') depth--;
@@ -5622,7 +5642,7 @@ __factories["./src/extractors/css"] = function(module, exports) {
5622
5642
 
5623
5643
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
5624
5644
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
5625
- const PER_FILE_LIMIT = 25;
5645
+ const PER_FILE_LIMIT = 200;
5626
5646
 
5627
5647
  /**
5628
5648
  * Extract signatures from CSS/SCSS/SASS/Less source code.
@@ -5703,8 +5723,13 @@ __factories["./src/extractors/dart"] = function(module, exports) {
5703
5723
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5704
5724
  // governs output rather than a literal buried here, and omissions are disclosed
5705
5725
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
5706
- const MEMBER_LIMIT = 8;
5707
- const PER_FILE_LIMIT = 25;
5726
+ // Class bodies are scanned to this many characters. Real classes routinely
5727
+ // run past the old 4KB scan window — truncating there silently hid every
5728
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
5729
+ // ceiling only guards against pathological input (Java parity, #551).
5730
+ const MAX_CLASS_BODY_CHARS = 200000;
5731
+ const MEMBER_LIMIT = 120;
5732
+ const PER_FILE_LIMIT = 200;
5708
5733
 
5709
5734
  /**
5710
5735
  * Extract signatures from Dart source code.
@@ -5758,7 +5783,7 @@ __factories["./src/extractors/dart"] = function(module, exports) {
5758
5783
 
5759
5784
  function extractBlock(src, startIndex) {
5760
5785
  let depth = 1, i = startIndex;
5761
- const end = Math.min(src.length, startIndex + 4000);
5786
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
5762
5787
  while (i < end && depth > 0) {
5763
5788
  if (src[i] === '{') depth++;
5764
5789
  else if (src[i] === '}') depth--;
@@ -5882,6 +5907,28 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5882
5907
  return [...deps].slice(0, 5);
5883
5908
  }
5884
5909
 
5910
+ /**
5911
+ * Extract Lua require() module dependencies.
5912
+ * Captures `require "mod"` and `require("mod")`, returning compact module
5913
+ * names as they appear in source.
5914
+ * @param {string} src
5915
+ * @returns {string[]}
5916
+ */
5917
+ function extractLuaDeps(src) {
5918
+ const deps = new Set();
5919
+ const stripped = stripLuaComments(src || '');
5920
+ for (const m of stripped.matchAll(/\brequire\s*(?:\(\s*)?["']([A-Za-z0-9_.\/-]+)["']\s*\)?/g)) {
5921
+ if (m[1]) deps.add(m[1]);
5922
+ }
5923
+ return [...deps].slice(0, 5);
5924
+ }
5925
+
5926
+ function stripLuaComments(src) {
5927
+ return String(src || '')
5928
+ .replace(/--\[\[[\s\S]*?\]\]/g, '')
5929
+ .replace(/--.*$/gm, '');
5930
+ }
5931
+
5885
5932
  /**
5886
5933
  * Build reverse dependency map from forward map.
5887
5934
  * @param {Map<string, string[]>} forwardMap
@@ -5900,7 +5947,7 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5900
5947
  return reverse;
5901
5948
  }
5902
5949
 
5903
- module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
5950
+ module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, extractLuaDeps, buildReverseDepMap };
5904
5951
 
5905
5952
  };
5906
5953
 
@@ -5935,6 +5982,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5935
5982
  swift: __require('./src/extractors/swift'),
5936
5983
  dart: __require('./src/extractors/dart'),
5937
5984
  scala: __require('./src/extractors/scala'),
5985
+ lua: __require('./src/extractors/lua'),
5938
5986
  gdscript: __require('./src/extractors/gdscript'),
5939
5987
  r: __require('./src/extractors/r'),
5940
5988
  vue_sfc: __require('./src/extractors/vue_sfc'),
@@ -5985,6 +6033,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5985
6033
  '.swift': 'swift',
5986
6034
  '.dart': 'dart',
5987
6035
  '.scala': 'scala', '.sc': 'scala',
6036
+ '.lua': 'lua',
5988
6037
  '.gd': 'gdscript',
5989
6038
  '.r': 'r', '.R': 'r',
5990
6039
  '.vue': 'vue_sfc',
@@ -6041,7 +6090,7 @@ __factories["./src/extractors/dockerfile"] = function(module, exports) {
6041
6090
 
6042
6091
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6043
6092
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
6044
- const PER_FILE_LIMIT = 25;
6093
+ const PER_FILE_LIMIT = 200;
6045
6094
 
6046
6095
  /**
6047
6096
  * Extract signatures from Dockerfiles.
@@ -6100,10 +6149,10 @@ __factories["./src/extractors/gdscript"] = function(module, exports) {
6100
6149
 
6101
6150
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6102
6151
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
6103
- const PER_FILE_LIMIT = 25;
6152
+ const PER_FILE_LIMIT = 200;
6104
6153
 
6105
6154
  // Ceilings disclose what they drop rather than truncating silently (#576).
6106
- const MEMBER_LIMIT = 6;
6155
+ const MEMBER_LIMIT = 120;
6107
6156
  const ENUM_LIMIT = 24;
6108
6157
 
6109
6158
  /**
@@ -6273,11 +6322,19 @@ __factories["./src/extractors/generic"] = function(module, exports) {
6273
6322
  __factories["./src/extractors/go"] = function(module, exports) {
6274
6323
 
6275
6324
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6276
- const { capWithNotice } = __require('./src/util/truncate');
6325
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6277
6326
 
6278
6327
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6279
6328
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
6280
- const PER_FILE_LIMIT = 25;
6329
+ // Class bodies are scanned to this many characters. Real classes routinely
6330
+ // run past the old 4KB scan window — truncating there silently hid every
6331
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
6332
+ // ceiling only guards against pathological input (Java parity, #551).
6333
+ const MAX_CLASS_BODY_CHARS = 200000;
6334
+ const PER_FILE_LIMIT = 200;
6335
+
6336
+ // Per-interface member ceiling, disclosed via capMembersWithNotice (#576).
6337
+ const MEMBER_LIMIT = 120;
6281
6338
 
6282
6339
  /**
6283
6340
  * Extract signatures from Go source code.
@@ -6313,7 +6370,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
6313
6370
  const block = extractBlock(stripped, bodyStart);
6314
6371
  sigs.push(hinted(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[1]));
6315
6372
  for (const meth of extractInterfaceMethods(block)) {
6316
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
6373
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + (meth.declIdx || 0)), lineAt(stripped, bodyStart + (meth.endIdx || 0))));
6317
6374
  }
6318
6375
  }
6319
6376
 
@@ -6331,7 +6388,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
6331
6388
 
6332
6389
  function extractBlock(src, startIndex) {
6333
6390
  let depth = 1, i = startIndex;
6334
- const end = Math.min(src.length, startIndex + 2000);
6391
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
6335
6392
  while (i < end && depth > 0) {
6336
6393
  if (src[i] === '{') depth++;
6337
6394
  else if (src[i] === '}') depth--;
@@ -6351,7 +6408,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
6351
6408
  endIdx: m.index + m[0].length,
6352
6409
  });
6353
6410
  }
6354
- return methods.slice(0, 8);
6411
+ return capMembersWithNotice(methods, MEMBER_LIMIT, 'methods');
6355
6412
  }
6356
6413
 
6357
6414
  function normalizeParams(params) {
@@ -6464,7 +6521,7 @@ __factories["./src/extractors/html"] = function(module, exports) {
6464
6521
 
6465
6522
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6466
6523
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
6467
- const PER_FILE_LIMIT = 25;
6524
+ const PER_FILE_LIMIT = 200;
6468
6525
 
6469
6526
  /**
6470
6527
  * Extract signatures from HTML files.
@@ -6639,6 +6696,10 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6639
6696
  const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6640
6697
  const { stripComments, maskCode, readBalanced } = __require('./src/extractors/scan');
6641
6698
 
6699
+ // Class bodies are scanned to this many characters — guard against
6700
+ // pathological input only; the old 4KB window silently hid members (#576).
6701
+ const MAX_CLASS_BODY_CHARS = 200000;
6702
+
6642
6703
  /**
6643
6704
  * Extract signatures from JavaScript source code.
6644
6705
  * Top-level declarations and class members carry a `:start-end` line anchor
@@ -6748,13 +6809,13 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6748
6809
  const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
6749
6810
  return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
6750
6811
  });
6751
- return capWithNotice(withAnchors, 25, 'signatures');
6812
+ return capWithNotice(withAnchors, 200, 'signatures');
6752
6813
  }
6753
6814
 
6754
6815
  function extractBlock(src, startIndex) {
6755
6816
  let depth = 1;
6756
6817
  let i = startIndex;
6757
- const end = Math.min(src.length, startIndex + 4000);
6818
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
6758
6819
  while (i < end && depth > 0) {
6759
6820
  if (src[i] === '{') depth++;
6760
6821
  else if (src[i] === '}') depth--;
@@ -6789,7 +6850,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6789
6850
  const retStr = formatReturnHint(returnHints.get(m[1]));
6790
6851
  members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
6791
6852
  }
6792
- return capMembersWithNotice(members, 8, 'methods');
6853
+ return capMembersWithNotice(members, 120, 'methods');
6793
6854
  }
6794
6855
 
6795
6856
  function buildReturnHints(src) {
@@ -6863,8 +6924,13 @@ __factories["./src/extractors/kotlin"] = function(module, exports) {
6863
6924
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
6864
6925
  // governs output rather than a literal buried here, and omissions are disclosed
6865
6926
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
6866
- const MEMBER_LIMIT = 8;
6867
- const PER_FILE_LIMIT = 25;
6927
+ // Class bodies are scanned to this many characters. Real classes routinely
6928
+ // run past the old 4KB scan window — truncating there silently hid every
6929
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
6930
+ // ceiling only guards against pathological input (Java parity, #551).
6931
+ const MAX_CLASS_BODY_CHARS = 200000;
6932
+ const MEMBER_LIMIT = 120;
6933
+ const PER_FILE_LIMIT = 200;
6868
6934
 
6869
6935
  /**
6870
6936
  * Extract signatures from Kotlin source code.
@@ -6918,7 +6984,7 @@ __factories["./src/extractors/kotlin"] = function(module, exports) {
6918
6984
 
6919
6985
  function extractBlock(src, startIndex) {
6920
6986
  let depth = 1, i = startIndex;
6921
- const end = Math.min(src.length, startIndex + 4000);
6987
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
6922
6988
  while (i < end && depth > 0) {
6923
6989
  if (src[i] === '{') depth++;
6924
6990
  else if (src[i] === '}') depth--;
@@ -7012,13 +7078,169 @@ __factories["./src/extractors/line-anchor"] = function(module, exports) {
7012
7078
 
7013
7079
  };
7014
7080
 
7081
+ // ── ./src/extractors/lua ──
7082
+ __factories["./src/extractors/lua"] = function(module, exports) {
7083
+
7084
+ const { capWithNotice } = __require('./src/util/truncate');
7085
+
7086
+ // Ceiling discloses what it drops rather than truncating silently (#583).
7087
+ // Collection runs to completion so the marker reports the true overflow —
7088
+ // stopping early made r.js report "+1 more" where 50 were hidden (#584).
7089
+ const PER_FILE_LIMIT = 200;
7090
+
7091
+ /**
7092
+ * Extract signatures from Lua source code.
7093
+ *
7094
+ * Recognised constructs:
7095
+ * - Global functions: `function name(args)`
7096
+ * - Module-table functions: `function M.name(args)` / `function M:name(args)`
7097
+ * - Local functions: `local function name(args)`
7098
+ * - Assigned functions: `name = function(args)` / `M.name = function(args)`
7099
+ * - Module imports: `local mod = require("mod")` as compact hints
7100
+ * - LDoc-style doc comments (`---`) as first-sentence hints
7101
+ *
7102
+ * The extractor is regex-only and zero-dependency, matching SigMap's Tier-3
7103
+ * language extractor style.
7104
+ *
7105
+ * @param {string} src - Raw file content
7106
+ * @returns {string[]} Array of signature strings
7107
+ */
7108
+ function extract(src) {
7109
+ if (!src || typeof src !== 'string') return [];
7110
+ const sigs = [];
7111
+ const hints = collectDocHints(src);
7112
+ const stripped = stripLuaComments(src);
7113
+ const seen = new Set();
7114
+
7115
+ // local foo = require('bar.baz') — useful module-surface hint, capped low.
7116
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*)\s*=\s*require\s*\(\s*['"]([A-Za-z0-9_.\/-]+)['"]\s*\)/gm)) {
7117
+ pushUnique(sigs, seen, `require ${m[2]} as ${m[1]}`);
7118
+ }
7119
+
7120
+ // local function name(args)
7121
+ for (const m of stripped.matchAll(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/gm)) {
7122
+ if (m[1].startsWith('_')) continue;
7123
+ pushUnique(sigs, seen, `local function ${m[1]}(${normalizeParams(m[2])})${applyHint(hints, m[1])}`);
7124
+ }
7125
+
7126
+ // function name(args), function M.name(args), function M:name(args)
7127
+ for (const m of stripped.matchAll(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(([^)]*)\)/gm)) {
7128
+ const name = m[1];
7129
+ if (name.startsWith('_')) continue;
7130
+ pushUnique(sigs, seen, `function ${name}(${normalizeParams(m[2])})${applyHint(hints, name)}`);
7131
+ }
7132
+
7133
+ // name = function(args), M.name = function(args), M:name = function(args)
7134
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(([^)]*)\)/gm)) {
7135
+ const name = m[1];
7136
+ if (name.startsWith('_')) continue;
7137
+ pushUnique(sigs, seen, `${name} = function(${normalizeParams(m[2])})${applyHint(hints, name)}`);
7138
+ }
7139
+
7140
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
7141
+ }
7142
+
7143
+ function pushUnique(out, seen, sig) {
7144
+ if (!sig || seen.has(sig)) return;
7145
+ seen.add(sig);
7146
+ out.push(sig);
7147
+ }
7148
+
7149
+ function normalizeParams(params) {
7150
+ return String(params || '')
7151
+ .replace(/--.*$/gm, '')
7152
+ .split(',')
7153
+ .map((p) => p.trim().replace(/\s+/g, ' '))
7154
+ .filter(Boolean)
7155
+ .join(', ');
7156
+ }
7157
+
7158
+ function applyHint(hints, name) {
7159
+ const h = hints.get(name);
7160
+ return h ? ` # ${h}` : '';
7161
+ }
7162
+
7163
+ /**
7164
+ * Attach each contiguous `---` doc block to the next function-like declaration
7165
+ * by its extracted symbol name.
7166
+ */
7167
+ function collectDocHints(src) {
7168
+ const hints = new Map();
7169
+ const lines = src.split('\n');
7170
+ let block = [];
7171
+ for (const line of lines) {
7172
+ const doc = line.match(/^\s*---\s?(.*)$/);
7173
+ if (doc) {
7174
+ block.push(doc[1]);
7175
+ } else if (block.length > 0) {
7176
+ const decl = line.match(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(/)
7177
+ || line.match(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(/)
7178
+ || line.match(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(/);
7179
+ if (decl) {
7180
+ const hint = firstDocSentence(block);
7181
+ if (hint) hints.set(decl[1], hint);
7182
+ }
7183
+ block = [];
7184
+ }
7185
+ }
7186
+ return hints;
7187
+ }
7188
+
7189
+ function firstDocSentence(block) {
7190
+ for (const raw of block) {
7191
+ const line = String(raw || '').trim();
7192
+ if (!line || line.startsWith('@')) continue;
7193
+ return line.replace(/\s+/g, ' ').slice(0, 60).replace(/[.,;:!?]+$/, '').trim();
7194
+ }
7195
+ return '';
7196
+ }
7197
+
7198
+ /** Strip Lua line and long comments while preserving strings enough for regex scans. */
7199
+ function stripLuaComments(src) {
7200
+ const out = src.split('');
7201
+ const blank = (a, b) => { for (let i = a; i < b; i++) if (out[i] !== '\n') out[i] = ' '; };
7202
+ let i = 0;
7203
+ while (i < src.length) {
7204
+ const ch = src[i];
7205
+ if (ch === '"' || ch === "'") {
7206
+ const quote = ch;
7207
+ i++;
7208
+ while (i < src.length) {
7209
+ if (src[i] === '\\') { i += 2; continue; }
7210
+ if (src[i] === quote) { i++; break; }
7211
+ if (src[i] === '\n') break;
7212
+ i++;
7213
+ }
7214
+ continue;
7215
+ }
7216
+ if (src.startsWith('--[[', i)) {
7217
+ const end = src.indexOf(']]', i + 4);
7218
+ blank(i, end === -1 ? src.length : end + 2);
7219
+ i = end === -1 ? src.length : end + 2;
7220
+ continue;
7221
+ }
7222
+ if (src.startsWith('--', i)) {
7223
+ const end = src.indexOf('\n', i + 2);
7224
+ blank(i, end === -1 ? src.length : end);
7225
+ i = end === -1 ? src.length : end;
7226
+ continue;
7227
+ }
7228
+ i++;
7229
+ }
7230
+ return out.join('');
7231
+ }
7232
+
7233
+ module.exports = { extract };
7234
+
7235
+ };
7236
+
7015
7237
  // ── ./src/extractors/markdown ──
7016
7238
  __factories["./src/extractors/markdown"] = function(module, exports) {
7017
7239
 
7018
7240
  const { capWithNotice } = __require('./src/util/truncate');
7019
7241
 
7020
7242
  // Ceiling discloses what it drops rather than truncating silently (#583).
7021
- const PER_FILE_LIMIT = 40;
7243
+ const PER_FILE_LIMIT = 200;
7022
7244
 
7023
7245
  /**
7024
7246
  * Lightweight markdown technical indexer.
@@ -7199,8 +7421,13 @@ __factories["./src/extractors/php"] = function(module, exports) {
7199
7421
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
7200
7422
  // governs output rather than a literal buried here, and omissions are disclosed
7201
7423
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
7202
- const MEMBER_LIMIT = 8;
7203
- const PER_FILE_LIMIT = 25;
7424
+ // Class bodies are scanned to this many characters. Real classes routinely
7425
+ // run past the old 4KB scan window — truncating there silently hid every
7426
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
7427
+ // ceiling only guards against pathological input (Java parity, #551).
7428
+ const MAX_CLASS_BODY_CHARS = 200000;
7429
+ const MEMBER_LIMIT = 120;
7430
+ const PER_FILE_LIMIT = 200;
7204
7431
 
7205
7432
  /**
7206
7433
  * Extract signatures from PHP source code.
@@ -7258,7 +7485,7 @@ __factories["./src/extractors/php"] = function(module, exports) {
7258
7485
 
7259
7486
  function extractBlock(src, startIndex) {
7260
7487
  let depth = 1, i = startIndex;
7261
- const end = Math.min(src.length, startIndex + 4000);
7488
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
7262
7489
  while (i < end && depth > 0) {
7263
7490
  if (src[i] === '{') depth++;
7264
7491
  else if (src[i] === '}') depth--;
@@ -7378,7 +7605,7 @@ __factories["./src/extractors/properties"] = function(module, exports) {
7378
7605
  const { capWithNotice } = __require('./src/util/truncate');
7379
7606
 
7380
7607
  // Ceiling discloses what it drops rather than truncating silently (#583).
7381
- const PER_FILE_LIMIT = 50;
7608
+ const PER_FILE_LIMIT = 200;
7382
7609
 
7383
7610
  /**
7384
7611
  * Extract signatures from .properties configuration files.
@@ -7494,7 +7721,10 @@ __factories["./src/extractors/python"] = function(module, exports) {
7494
7721
 
7495
7722
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
7496
7723
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
7497
- const PER_FILE_LIMIT = 30;
7724
+ const PER_FILE_LIMIT = 200;
7725
+
7726
+ // Per-class member ceiling, disclosed via capWithNotice (#576).
7727
+ const MEMBER_LIMIT = 120;
7498
7728
 
7499
7729
  /**
7500
7730
  * 1-based line of the last source line belonging to a top-level (indent 0)
@@ -7650,7 +7880,7 @@ __factories["./src/extractors/python"] = function(module, exports) {
7650
7880
  methods.push(`${asyncKw}def ${m[1]}(${params})${retStr}`);
7651
7881
  }
7652
7882
  }
7653
- return methods.slice(0, 8);
7883
+ return capWithNotice(methods, MEMBER_LIMIT, 'methods');
7654
7884
  }
7655
7885
 
7656
7886
  function tryExtractDataclassFields(stripped, classIndex) {
@@ -7759,6 +7989,8 @@ __factories["./src/extractors/python"] = function(module, exports) {
7759
7989
  // ── ./src/extractors/python_dataclass ──
7760
7990
  __factories["./src/extractors/python_dataclass"] = function(module, exports) {
7761
7991
 
7992
+ const { capWithNotice } = __require('./src/util/truncate');
7993
+
7762
7994
  /**
7763
7995
  * Extract Python dataclass, Pydantic model, and SQLAlchemy ORM metadata.
7764
7996
  * Focuses on model fields, validation, and relationships.
@@ -7830,7 +8062,7 @@ __factories["./src/extractors/python_dataclass"] = function(module, exports) {
7830
8062
  sigs.push('config-class');
7831
8063
  }
7832
8064
 
7833
- return Array.from(new Set(sigs)).slice(0, 50);
8065
+ return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
7834
8066
  }
7835
8067
 
7836
8068
  module.exports = { extract };
@@ -7844,7 +8076,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7844
8076
 
7845
8077
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
7846
8078
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
7847
- const PER_FILE_LIMIT = 30;
8079
+ const PER_FILE_LIMIT = 200;
7848
8080
 
7849
8081
  /**
7850
8082
  * Extract signatures from R source code.
@@ -8123,7 +8355,7 @@ __factories["./src/extractors/ruby"] = function(module, exports) {
8123
8355
 
8124
8356
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8125
8357
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
8126
- const PER_FILE_LIMIT = 25;
8358
+ const PER_FILE_LIMIT = 200;
8127
8359
 
8128
8360
  /**
8129
8361
  * Extract signatures from Ruby source code.
@@ -8184,11 +8416,19 @@ __factories["./src/extractors/ruby"] = function(module, exports) {
8184
8416
  __factories["./src/extractors/rust"] = function(module, exports) {
8185
8417
 
8186
8418
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
8187
- const { capWithNotice } = __require('./src/util/truncate');
8419
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
8188
8420
 
8189
8421
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8190
8422
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
8191
- const PER_FILE_LIMIT = 25;
8423
+ // Class bodies are scanned to this many characters. Real classes routinely
8424
+ // run past the old 4KB scan window — truncating there silently hid every
8425
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
8426
+ // ceiling only guards against pathological input (Java parity, #551).
8427
+ const MAX_CLASS_BODY_CHARS = 200000;
8428
+ const PER_FILE_LIMIT = 200;
8429
+
8430
+ // Per-impl member ceiling, disclosed via capMembersWithNotice (#576).
8431
+ const MEMBER_LIMIT = 120;
8192
8432
 
8193
8433
  /**
8194
8434
  * Extract signatures from Rust source code.
@@ -8246,7 +8486,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
8246
8486
  const block = extractBlock(stripped, bodyStart);
8247
8487
  sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
8248
8488
  for (const fn of extractMethods(block)) {
8249
- sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)), fn.name));
8489
+ sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + (fn.declIdx || 0)), lineAt(stripped, bodyStart + (fn.endIdx || 0))), fn.name));
8250
8490
  }
8251
8491
  }
8252
8492
 
@@ -8263,7 +8503,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
8263
8503
 
8264
8504
  function extractBlock(src, startIndex) {
8265
8505
  let depth = 1, i = startIndex;
8266
- const end = Math.min(src.length, startIndex + 5000);
8506
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
8267
8507
  while (i < end && depth > 0) {
8268
8508
  if (src[i] === '{') depth++;
8269
8509
  else if (src[i] === '}') depth--;
@@ -8284,7 +8524,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
8284
8524
  endIdx: m.index + m[0].length,
8285
8525
  });
8286
8526
  }
8287
- return methods.slice(0, 8);
8527
+ return capMembersWithNotice(methods, MEMBER_LIMIT, 'methods');
8288
8528
  }
8289
8529
 
8290
8530
  function normalizeParams(params) {
@@ -8336,8 +8576,13 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8336
8576
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
8337
8577
  // governs output rather than a literal buried here, and omissions are disclosed
8338
8578
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
8339
- const MEMBER_LIMIT = 8;
8340
- const PER_FILE_LIMIT = 25;
8579
+ // Class bodies are scanned to this many characters. Real classes routinely
8580
+ // run past the old 4KB scan window — truncating there silently hid every
8581
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
8582
+ // ceiling only guards against pathological input (Java parity, #551).
8583
+ const MAX_CLASS_BODY_CHARS = 200000;
8584
+ const MEMBER_LIMIT = 120;
8585
+ const PER_FILE_LIMIT = 200;
8341
8586
 
8342
8587
  /**
8343
8588
  * Extract signatures from Scala source code.
@@ -8384,7 +8629,7 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8384
8629
 
8385
8630
  function extractBlock(src, startIndex) {
8386
8631
  let depth = 1, i = startIndex;
8387
- const end = Math.min(src.length, startIndex + 4000);
8632
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
8388
8633
  while (i < end && depth > 0) {
8389
8634
  if (src[i] === '{') depth++;
8390
8635
  else if (src[i] === '}') depth--;
@@ -8529,7 +8774,7 @@ __factories["./src/extractors/shell"] = function(module, exports) {
8529
8774
 
8530
8775
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8531
8776
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
8532
- const PER_FILE_LIMIT = 25;
8777
+ const PER_FILE_LIMIT = 200;
8533
8778
 
8534
8779
  /**
8535
8780
  * Extract signatures from shell scripts (bash, zsh, fish).
@@ -8679,7 +8924,7 @@ __factories["./src/extractors/svelte"] = function(module, exports) {
8679
8924
 
8680
8925
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8681
8926
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
8682
- const PER_FILE_LIMIT = 25;
8927
+ const PER_FILE_LIMIT = 200;
8683
8928
 
8684
8929
  /**
8685
8930
  * Extract signatures from Svelte components.
@@ -8749,8 +8994,13 @@ __factories["./src/extractors/swift"] = function(module, exports) {
8749
8994
  // Ceilings sit above the default `maxSigsPerFile` so the configured budget
8750
8995
  // governs output rather than a literal buried here, and omissions are disclosed
8751
8996
  // — an undisclosed cap looks like a class that simply has eight methods (#576).
8752
- const MEMBER_LIMIT = 8;
8753
- const PER_FILE_LIMIT = 25;
8997
+ // Class bodies are scanned to this many characters. Real classes routinely
8998
+ // run past the old 4KB scan window — truncating there silently hid every
8999
+ // member after ~4000 chars AND anchored class end-lines short (#576). The
9000
+ // ceiling only guards against pathological input (Java parity, #551).
9001
+ const MAX_CLASS_BODY_CHARS = 200000;
9002
+ const MEMBER_LIMIT = 120;
9003
+ const PER_FILE_LIMIT = 200;
8754
9004
 
8755
9005
  /**
8756
9006
  * Extract signatures from Swift source code.
@@ -8804,7 +9054,7 @@ __factories["./src/extractors/swift"] = function(module, exports) {
8804
9054
 
8805
9055
  function extractBlock(src, startIndex) {
8806
9056
  let depth = 1, i = startIndex;
8807
- const end = Math.min(src.length, startIndex + 4000);
9057
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
8808
9058
  while (i < end && depth > 0) {
8809
9059
  if (src[i] === '{') depth++;
8810
9060
  else if (src[i] === '}') depth--;
@@ -8963,7 +9213,7 @@ __factories["./src/extractors/toml"] = function(module, exports) {
8963
9213
  const { capWithNotice } = __require('./src/util/truncate');
8964
9214
 
8965
9215
  // Ceiling discloses what it drops rather than truncating silently (#583).
8966
- const PER_FILE_LIMIT = 40;
9216
+ const PER_FILE_LIMIT = 200;
8967
9217
 
8968
9218
  /**
8969
9219
  * Extract signatures from TOML configuration files.
@@ -9015,6 +9265,10 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
9015
9265
  const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
9016
9266
  const { stripComments, maskCode, readBalanced } = __require('./src/extractors/scan');
9017
9267
 
9268
+ // Class bodies are scanned to this many characters — guard against
9269
+ // pathological input only; the old 4KB window silently hid members (#576).
9270
+ const MAX_CLASS_BODY_CHARS = 200000;
9271
+
9018
9272
  /**
9019
9273
  * Extract signatures from TypeScript source code.
9020
9274
  * Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
@@ -9200,13 +9454,13 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
9200
9454
  const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
9201
9455
  return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
9202
9456
  });
9203
- return capWithNotice(withAnchors, 35, 'signatures');
9457
+ return capWithNotice(withAnchors, 200, 'signatures');
9204
9458
  }
9205
9459
 
9206
9460
  function extractBlock(src, startIndex) {
9207
9461
  let depth = 1;
9208
9462
  let i = startIndex;
9209
- const end = Math.min(src.length, startIndex + 4000);
9463
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
9210
9464
  while (i < end && depth > 0) {
9211
9465
  if (src[i] === '{') depth++;
9212
9466
  else if (src[i] === '}') depth--;
@@ -9234,7 +9488,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
9234
9488
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
9235
9489
  members.push({ text: `${m[1]}(${normalizeParams(block.slice(openIdx + 1, closeIdx))})`, start, end: closeIdx + 1 });
9236
9490
  }
9237
- return capMembersWithNotice(members, 8, 'members');
9491
+ return capMembersWithNotice(members, 120, 'members');
9238
9492
  }
9239
9493
 
9240
9494
  const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
@@ -9269,7 +9523,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
9269
9523
  const retStr = retType ? ` → ${retType}` : '';
9270
9524
  members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
9271
9525
  }
9272
- return capMembersWithNotice(members, 8, 'methods');
9526
+ return capMembersWithNotice(members, 120, 'methods');
9273
9527
  }
9274
9528
 
9275
9529
  function normalizeParams(params) {
@@ -9344,7 +9598,7 @@ __factories["./src/extractors/typescript_react"] = function(module, exports) {
9344
9598
  const { capWithNotice } = __require('./src/util/truncate');
9345
9599
 
9346
9600
  // Ceiling discloses what it drops rather than truncating silently (#583).
9347
- const PER_FILE_LIMIT = 50;
9601
+ const PER_FILE_LIMIT = 200;
9348
9602
 
9349
9603
  /**
9350
9604
  * Extract React component signatures from .tsx files.
@@ -9410,6 +9664,8 @@ __factories["./src/extractors/typescript_react"] = function(module, exports) {
9410
9664
  // ── ./src/extractors/vue_sfc ──
9411
9665
  __factories["./src/extractors/vue_sfc"] = function(module, exports) {
9412
9666
 
9667
+ const { capWithNotice } = __require('./src/util/truncate');
9668
+
9413
9669
  /**
9414
9670
  * Extract Vue Single-File Component (SFC) signatures from .vue files.
9415
9671
  * Captures component metadata: name, props, emits, slots, composables, lifecycle.
@@ -9503,7 +9759,7 @@ __factories["./src/extractors/vue_sfc"] = function(module, exports) {
9503
9759
  sigs.push(`slot ${s}`);
9504
9760
  }
9505
9761
 
9506
- return Array.from(new Set(sigs)).slice(0, 50);
9762
+ return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
9507
9763
  }
9508
9764
 
9509
9765
  module.exports = { extract };
@@ -9513,6 +9769,8 @@ __factories["./src/extractors/vue_sfc"] = function(module, exports) {
9513
9769
  // ── ./src/extractors/xml ──
9514
9770
  __factories["./src/extractors/xml"] = function(module, exports) {
9515
9771
 
9772
+ const { capWithNotice } = __require('./src/util/truncate');
9773
+
9516
9774
  /**
9517
9775
  * Lightweight XML config extractor.
9518
9776
  * Captures root tags, key config tags, and id/name/class attributes.
@@ -9553,7 +9811,7 @@ __factories["./src/extractors/xml"] = function(module, exports) {
9553
9811
  if (cls) sigs.push(`${tag} -> ${cls[1]}`);
9554
9812
  }
9555
9813
 
9556
- return Array.from(new Set(sigs)).slice(0, 50);
9814
+ return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
9557
9815
  }
9558
9816
 
9559
9817
  module.exports = { extract };
@@ -9567,7 +9825,7 @@ __factories["./src/extractors/yaml"] = function(module, exports) {
9567
9825
 
9568
9826
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
9569
9827
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
9570
- const PER_FILE_LIMIT = 25;
9828
+ const PER_FILE_LIMIT = 200;
9571
9829
 
9572
9830
  /**
9573
9831
  * Extract signatures from YAML configuration files.
@@ -11977,6 +12235,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11977
12235
  const JAVA_EXTS = new Set(['.java']);
11978
12236
  const GO_EXTS = new Set(['.go']);
11979
12237
  const RS_EXTS = new Set(['.rs']);
12238
+ const KT_EXTS = new Set(['.kt', '.kts']);
12239
+ const SCALA_EXTS = new Set(['.scala', '.sc']);
11980
12240
 
11981
12241
  // Tokens that look like `name(` calls or definition headers but are language
11982
12242
  // keywords, not user symbols — never treated as a call or a definition.
@@ -12300,6 +12560,81 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12300
12560
 
12301
12561
  // Pick the masker whose comment/string syntax matches the language.
12302
12562
  // Java and Go share JS syntax (Go raw strings mask like template literals).
12563
+ /**
12564
+ * Body range for a JVM-family member that may use either a brace body or an
12565
+ * expression body (`fun f() = expr` / `def f = expr`), which Java has not.
12566
+ * An expression body runs to the end of its line — enough to capture the calls
12567
+ * it makes, which is all the graph needs.
12568
+ * @returns {{bodyStart:number, bodyEnd:number}|null} null when neither form follows
12569
+ */
12570
+ function jvmBodyRange(masked, from) {
12571
+ let k = from;
12572
+ while (k < masked.length && masked[k] !== '{' && masked[k] !== '=' && masked[k] !== '\n' && masked[k] !== ';') k++;
12573
+ if (masked[k] === '{') return { bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') };
12574
+ if (masked[k] === '=') {
12575
+ // `= {` is still a brace body, just written with an assignment.
12576
+ let j = k + 1;
12577
+ while (j < masked.length && /\s/.test(masked[j])) j++;
12578
+ if (masked[j] === '{') return { bodyStart: j, bodyEnd: matchDelim(masked, j, '{', '}') };
12579
+ const eol = masked.indexOf('\n', k);
12580
+ return { bodyStart: k, bodyEnd: eol === -1 ? masked.length : eol };
12581
+ }
12582
+ return null; // abstract member or interface declaration
12583
+ }
12584
+
12585
+ /**
12586
+ * Kotlin `fun` definitions, including extension functions (`fun Foo.bar()`,
12587
+ * recorded as `bar`) and expression bodies.
12588
+ */
12589
+ function ktDefs(masked) {
12590
+ const defs = [];
12591
+ const seen = new Set();
12592
+ const re = /(?:^|\n)[ \t]*((?:(?:public|private|protected|internal|open|override|abstract|final|suspend|inline|operator|infix|tailrec|external|expect|actual|inner|companion)\s+)*)fun\s+(?:<[^>\n]{0,80}>\s*)?(?:[A-Za-z_][\w.<>]*\.)?([A-Za-z_][\w]*)\s*\(/g;
12593
+ let m;
12594
+ while ((m = re.exec(masked)) !== null) {
12595
+ const name = m[2];
12596
+ if (NON_CALL.has(name)) continue;
12597
+ const paren = masked.indexOf('(', m.index + m[0].length - 1);
12598
+ const close = matchDelim(masked, paren, '(', ')');
12599
+ if (close < 0) continue;
12600
+ const range = jvmBodyRange(masked, close + 1);
12601
+ const line = lineAt(masked, m.index + 1);
12602
+ const key = name + ':' + (range ? range.bodyStart : line);
12603
+ if (seen.has(key)) continue;
12604
+ seen.add(key);
12605
+ // An abstract/interface member owns no body: it can receive edges but
12606
+ // never produces them, the same treatment javaDefs gives declarations.
12607
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: close, bodyEnd: close });
12608
+ }
12609
+ return defs;
12610
+ }
12611
+
12612
+ /**
12613
+ * Scala `def` definitions. Handles parameterless members (`def foo: Int = 1`),
12614
+ * type parameters, and expression bodies.
12615
+ */
12616
+ function scalaDefs(masked) {
12617
+ const defs = [];
12618
+ const seen = new Set();
12619
+ const re = /(?:^|\n)[ \t]*((?:(?:private|protected|override|implicit|final|lazy|sealed|abstract|inline)(?:\s*\[[^\]\n]{0,60}\])?\s+)*)def\s+([A-Za-z_][\w]*)\s*(?=[\[\(:=])/g;
12620
+ let m;
12621
+ while ((m = re.exec(masked)) !== null) {
12622
+ const name = m[2];
12623
+ if (NON_CALL.has(name)) continue;
12624
+ // Step past an optional type-parameter list, then an optional value list.
12625
+ let k = m.index + m[0].length;
12626
+ if (masked[k] === '[') { const e = matchDelim(masked, k, '[', ']'); if (e < 0) continue; k = e + 1; }
12627
+ while (masked[k] === '(') { const e = matchDelim(masked, k, '(', ')'); if (e < 0) break; k = e + 1; }
12628
+ const range = jvmBodyRange(masked, k);
12629
+ const line = lineAt(masked, m.index + 1);
12630
+ const key = name + ':' + (range ? range.bodyStart : line);
12631
+ if (seen.has(key)) continue;
12632
+ seen.add(key);
12633
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: k, bodyEnd: k });
12634
+ }
12635
+ return defs;
12636
+ }
12637
+
12303
12638
  function maskFor(filePath, src) {
12304
12639
  const ext = path.extname(filePath).toLowerCase();
12305
12640
  if (PY_EXTS.has(ext)) return maskPy(src);
@@ -12314,6 +12649,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12314
12649
  if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
12315
12650
  if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
12316
12651
  if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
12652
+ if (KT_EXTS.has(ext)) return ktDefs(maskJs(src));
12653
+ if (SCALA_EXTS.has(ext)) return scalaDefs(maskJs(src));
12317
12654
  return null; // unsupported language
12318
12655
  }
12319
12656
 
@@ -12403,7 +12740,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12403
12740
  if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
12404
12741
  else if (e.isFile()) {
12405
12742
  const ext = path.extname(e.name).toLowerCase();
12406
- if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
12743
+ if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
12744
+ || RS_EXTS.has(ext) || KT_EXTS.has(ext) || SCALA_EXTS.has(ext)) out.push(full);
12407
12745
  }
12408
12746
  }
12409
12747
  }
@@ -15795,8 +16133,8 @@ __factories["./src/mcp/server"] = function(module, exports) {
15795
16133
  * One JSON object per line on both stdin and stdout.
15796
16134
  *
15797
16135
  * Supported methods:
15798
- * initialize → serverInfo + capabilities
15799
- * tools/list → 19 tool definitions
16136
+ * initialize → serverInfo + capabilities + negotiated protocolVersion
16137
+ * tools/list → 21 tool definitions
15800
16138
  * tools/call → dispatch to handler, return result
15801
16139
  */
15802
16140
 
@@ -15806,10 +16144,22 @@ __factories["./src/mcp/server"] = function(module, exports) {
15806
16144
 
15807
16145
  const SERVER_INFO = {
15808
16146
  name: 'sigmap',
15809
- version: '8.33.0',
16147
+ version: '8.35.0',
15810
16148
  description: 'SigMap MCP server — code signatures on demand',
15811
16149
  };
15812
16150
 
16151
+ // Protocol revisions this server actually speaks. The tools-only surface —
16152
+ // initialize / tools/list / tools/call plus the initialized/cancelled
16153
+ // notifications — is identical across these revisions, and the
16154
+ // @hasmcp/mcp-spec-test suite passes every applicable 2025-11-25 case against
16155
+ // it. 2026-07-28 is deliberately absent: that revision requires
16156
+ // server/discover, which is not implemented. Newest first: a client offering
16157
+ // a version outside this list is downgraded to [0], never echoed back —
16158
+ // echoing an unspeakable version is itself a spec violation (#544), and it
16159
+ // made the conformance suite believe 2026-07-28 was supported, producing six
16160
+ // phantom server/discover failures on a revision never really offered (#545).
16161
+ const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
16162
+
15813
16163
  // ---------------------------------------------------------------------------
15814
16164
  // JSON-RPC helpers
15815
16165
  // ---------------------------------------------------------------------------
@@ -15834,9 +16184,32 @@ __factories["./src/mcp/server"] = function(module, exports) {
15834
16184
  return;
15835
16185
  }
15836
16186
 
16187
+ // server/discover (spec 2026-07-28) — session-less discovery, answerable
16188
+ // before any handshake, so a client can learn the honest version list
16189
+ // instead of offering versions and hoping. The response is a
16190
+ // CacheableResult: deterministic, so the TTL promise of stability holds.
16191
+ // Note supportedVersions does NOT include 2026-07-28 — answering discover
16192
+ // is forward-compatible plumbing, not a claim to serve that revision's
16193
+ // whole surface (per-result envelopes, inline _meta negotiation).
16194
+ if (method === 'server/discover') {
16195
+ respond(id, {
16196
+ resultType: 'complete',
16197
+ cacheScope: 'public',
16198
+ ttlMs: 3600000,
16199
+ supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
16200
+ capabilities: { tools: {} },
16201
+ serverInfo: SERVER_INFO,
16202
+ instructions: 'SigMap serves code signatures for the working directory. Call query_context or search_signatures to rank and fetch signature blocks instead of reading whole files.',
16203
+ });
16204
+ return;
16205
+ }
16206
+
15837
16207
  if (method === 'initialize') {
16208
+ const offered = params && params.protocolVersion;
15838
16209
  respond(id, {
15839
- protocolVersion: (params && params.protocolVersion) || '2024-11-05',
16210
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(offered)
16211
+ ? offered
16212
+ : SUPPORTED_PROTOCOL_VERSIONS[0],
15840
16213
  serverInfo: SERVER_INFO,
15841
16214
  capabilities: { tools: {} },
15842
16215
  });
@@ -15844,6 +16217,13 @@ __factories["./src/mcp/server"] = function(module, exports) {
15844
16217
  }
15845
16218
 
15846
16219
  if (method === 'tools/list') {
16220
+ // No pagination: the full list fits one page, so any cursor a client
16221
+ // presents is one this server never issued — reject it (-32602, per the
16222
+ // spec's SHOULD) rather than silently restarting from page one.
16223
+ if (params && params.cursor !== undefined) {
16224
+ respondError(id, -32602, `Invalid cursor: ${String(params.cursor)}`);
16225
+ return;
16226
+ }
15847
16227
  respond(id, { tools: TOOLS });
15848
16228
  return;
15849
16229
  }
@@ -17473,9 +17853,15 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
17473
17853
  const hop1Files = new Set(); // normalised keys that received a hop1 boost
17474
17854
  const hop1Seeds = []; // original (un-normalised) paths, for hop-2 lookup
17475
17855
 
17476
- // Hop 1: direct neighbors of scored files
17477
- for (const entry of scored) {
17478
- if (entry.score <= 0) continue;
17856
+ // Hop 1: direct neighbors of scored files. Seeds snapshotted BEFORE the
17857
+ // loop (as the call-graph block below already does) so boosts never
17858
+ // cascade: a file whose only score is a hop-1 boost must not become a seed
17859
+ // itself mid-loop, or the effective seed set — and every boost total —
17860
+ // depends on index insertion order, which git history changes via the
17861
+ // recent-commits hoist. That made gate scores differ between a shallow CI
17862
+ // checkout and a developer clone of the same commit (#596).
17863
+ const hop1SeedEntries = scored.filter((e) => e.score > 0);
17864
+ for (const entry of hop1SeedEntries) {
17479
17865
  const neighbors = _graphGet(graph.forward, path.resolve(cwd, entry.file)) || [];
17480
17866
  for (const neighborAbs of neighbors) {
17481
17867
  const nk = path.normalize(neighborAbs);
@@ -17490,7 +17876,10 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
17490
17876
  }
17491
17877
  }
17492
17878
 
17493
- // Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
17879
+ // Hop 2: neighbors of hop1 files (only if they didn't get a direct score).
17880
+ // Eligibility frozen after hop-1 for the same reason: a file whose first
17881
+ // score is a hop-2 boost must not become hop-2-eligible mid-loop (#596).
17882
+ const hop2Eligible = scored.map((e) => e.score > 0);
17494
17883
  for (const hop1Key of hop1Seeds) {
17495
17884
  if (_graphGet(keyToIdx, hop1Key) === undefined) continue; // skip files not in index
17496
17885
  const neighbors = _graphGet(graph.forward, hop1Key) || [];
@@ -17499,7 +17888,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
17499
17888
  if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
17500
17889
  if (hop1Files.has(nk)) continue; // skip already hop1-boosted
17501
17890
  const idx = _graphGet(keyToIdx, nk);
17502
- if (idx !== undefined && scored[idx].score > 0) {
17891
+ if (idx !== undefined && hop2Eligible[idx]) {
17503
17892
  // Only boost files that have some baseline score (not noise)
17504
17893
  scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
17505
17894
  scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
@@ -22100,7 +22489,7 @@ function __tryGit(args, opts = {}) {
22100
22489
  catch (_) { return ''; }
22101
22490
  }
22102
22491
 
22103
- const VERSION = '8.33.0';
22492
+ const VERSION = '8.35.0';
22104
22493
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
22105
22494
 
22106
22495
  function requireSourceOrBundled(key) {
@@ -22395,7 +22784,20 @@ function estimateTokens(str) {
22395
22784
  }
22396
22785
 
22397
22786
  function isTestFile(filePath) {
22398
- return /\.(test|spec)\.[a-z]+$/.test(filePath) || /_test\.[a-z]+$/.test(filePath);
22787
+ const p = filePath.replace(/\\/g, '/');
22788
+ // Filename conventions: foo.test.ts / foo.spec.js / foo_test.go / test_foo.py,
22789
+ // plus the PascalCase JVM/C#/Swift family (FooTest.java, FooTests.kt,
22790
+ // FooSpec.scala). The [a-z0-9] guard keeps `contest.java` and `Latest.java`
22791
+ // out; the suffix must be a real case boundary.
22792
+ if (/\.(test|spec)\.[a-z]+$/.test(p) || /_test\.[a-z]+$/.test(p)) return true;
22793
+ if (/(^|\/)test_[^/]+\.[a-z]+$/.test(p)) return true;
22794
+ if (/[a-z0-9](Test|Spec)s?\.(java|kt|kts|scala|groovy|cs|swift)$/.test(p)) return true;
22795
+ // Path-segment conventions (src/test/java/**, tests/, spec/, __tests__/, e2e/)
22796
+ // — the form the entire JVM world uses. Without this, "drop test files first"
22797
+ // ran INVERTED on JVM repos: spring-petclinic's budget kept all 17 src/test/
22798
+ // files while dropping the application entry point, the owner entities and
22799
+ // every owner template (#592).
22800
+ return /(^|\/)(test|tests|spec|specs|__tests__|e2e)(\/|$)/i.test(p);
22399
22801
  }
22400
22802
 
22401
22803
  function isConfigFile(filePath) {
@@ -22414,6 +22816,21 @@ function isMockFile(filePath) {
22414
22816
  /mock\.(ts|js|tsx|jsx)$/.test(p);
22415
22817
  }
22416
22818
 
22819
+ // The application entry point is the single most orientation-valuable file in
22820
+ // a repo, yet it is often tiny (a main class with 2-3 signatures), so the
22821
+ // fewest-sigs tie-break dropped it while keeping bulkier files: petclinic's
22822
+ // budget dropped PetClinicApplication.java and an agent asking for the startup
22823
+ // entry point could not reach it at all (#592). Deliberately narrow — bare
22824
+ // main/app/application names plus the Spring `*Application.java` and C#
22825
+ // `Program.cs` conventions. `index.*` is excluded: JS barrel files are hubs,
22826
+ // not entry points, and protecting every one would crowd out real code.
22827
+ function isEntryPointFile(filePath) {
22828
+ const base = path.basename(filePath.replace(/\\/g, '/'));
22829
+ return /^(main|app|application|__main__)\.[a-z]+$/i.test(base)
22830
+ || /Application\.(java|kt|kts)$/.test(base)
22831
+ || /^Program\.cs$/.test(base);
22832
+ }
22833
+
22417
22834
  /**
22418
22835
  * Compute the effective token budget based on repo size and config.
22419
22836
  *
@@ -22469,6 +22886,38 @@ function computeEffectiveMaxTokens(fileEntries, config) {
22469
22886
  return effective;
22470
22887
  }
22471
22888
 
22889
+ /**
22890
+ * Delete `.github/context-*.md` split files this run did not write (#555).
22891
+ *
22892
+ * Splits are discovered by filename pattern at read time, not by consulting
22893
+ * the config, so a file left behind by a previous `strategy` — or by a module
22894
+ * since dropped from `srcDirs` — keeps being merged into the retrieval index
22895
+ * and silently steers every query. Observed on a 524-file Java repo: a stale
22896
+ * 376 KB `context-mall-mbg.md` kept generated entities at ranks 1, 3 and 4
22897
+ * while both files implementing the feature fell outside the top 6, even
22898
+ * though `sig-index.json` correctly held zero entries for that module.
22899
+ *
22900
+ * @param {string} cwd
22901
+ * @param {string[]} keep basenames this run wrote, e.g. ['context-core.md']
22902
+ * @returns {string[]} basenames removed
22903
+ */
22904
+ function pruneStaleContextSplits(cwd, keep) {
22905
+ const ghDir = path.join(cwd, '.github');
22906
+ const kept = new Set(keep);
22907
+ const removed = [];
22908
+ let entries;
22909
+ try { entries = fs.readdirSync(ghDir); } catch (_) { return removed; }
22910
+ for (const f of entries) {
22911
+ if (!/^context-[\w.-]+\.md$/.test(f) || kept.has(f)) continue;
22912
+ try { fs.unlinkSync(path.join(ghDir, f)); removed.push(f); } catch (_) { /* best effort */ }
22913
+ }
22914
+ if (removed.length) {
22915
+ console.warn(`[sigmap] pruned ${removed.length} stale context split(s) from a previous `
22916
+ + `strategy/srcDirs: ${removed.join(', ')}`);
22917
+ }
22918
+ return removed;
22919
+ }
22920
+
22472
22921
  function applyTokenBudget(fileEntries, maxTokens) {
22473
22922
  // fileEntries: [{ filePath, sigs, mtime }]
22474
22923
  // Per-file rendered overhead: "### <path>", code fences, trailing blank line.
@@ -22501,6 +22950,7 @@ function applyTokenBudget(fileEntries, maxTokens) {
22501
22950
  else if (isMockFile(e.filePath)) { priority = 9; dropReason = 'budget: mock file'; }
22502
22951
  else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
22503
22952
  else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
22953
+ else if (isEntryPointFile(e.filePath)) { priority = 3; dropReason = 'budget: entry point'; }
22504
22954
  else priority = 4;
22505
22955
  const loc = e.content ? e.content.split('\n').length : 1;
22506
22956
  const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
@@ -23276,6 +23726,7 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23276
23726
  ];
23277
23727
 
23278
23728
  let totalOut = 0;
23729
+ const writtenSplits = [];
23279
23730
  for (const mod of moduleNames) {
23280
23731
  const outName = `context-${mod}.md`;
23281
23732
  const outPath = path.join(cwd, '.github', outName);
@@ -23294,7 +23745,9 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23294
23745
  console.warn(`[sigmap] per-module: wrote .github/${outName} (~${modTokens} tokens, ${budgeted.length} files)`);
23295
23746
 
23296
23747
  overviewLines.push(`| \`${mod}\` | \`.github/${outName}\` |`);
23748
+ writtenSplits.push(outName);
23297
23749
  }
23750
+ pruneStaleContextSplits(cwd, writtenSplits);
23298
23751
 
23299
23752
  overviewLines.push('');
23300
23753
  overviewLines.push('> Inject the relevant module file into your IDE context window.');
@@ -23333,6 +23786,7 @@ function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTot
23333
23786
  const coldContent = coldHeader + formatOutput(coldEntries, cwd, false, config, null);
23334
23787
  ensureDir(coldPath);
23335
23788
  fs.writeFileSync(coldPath, coldContent, 'utf8');
23789
+ pruneStaleContextSplits(cwd, ['context-cold.md']);
23336
23790
  const coldTokens = estimateTokens(coldContent);
23337
23791
 
23338
23792
  console.warn('[sigmap] hot-cold:');
@@ -23657,6 +24111,9 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
23657
24111
  result = runHotColdStrategy(cwd, configWithBudget, fileEntries, recentFiles, inputTokenTotal);
23658
24112
  } else {
23659
24113
  // 'full' — original behaviour
24114
+ // 'full' writes no split files — anything left from a previous
24115
+ // strategy would still be merged into the index at read time (#555).
24116
+ pruneStaleContextSplits(cwd, []);
23660
24117
  fileEntries = applyTokenBudget(fileEntries, effectiveMaxTokens);
23661
24118
  const droppedCount = beforeCount - fileEntries.length;
23662
24119
  const routingEnabled = !!(config.routing || process.argv.includes('--routing'));