sigmap 8.32.1 → 8.34.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.
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([
@@ -4151,47 +4161,13 @@ __factories["./src/eval/analyzer"] = function(module, exports) {
4151
4161
  const path = require('path');
4152
4162
 
4153
4163
  // Extension → extractor name (mirrors EXT_MAP in gen-context.js)
4154
- const EXT_MAP = {
4155
- '.ts': 'typescript', '.tsx': 'typescript',
4156
- '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
4157
- '.py': 'python', '.pyw': 'python',
4158
- '.java': 'java',
4159
- '.kt': 'kotlin', '.kts': 'kotlin',
4160
- '.go': 'go',
4161
- '.rs': 'rust',
4162
- '.cs': 'csharp',
4163
- '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
4164
- '.rb': 'ruby', '.rake': 'ruby',
4165
- '.php': 'php',
4166
- '.swift': 'swift',
4167
- '.dart': 'dart',
4168
- '.scala': 'scala', '.sc': 'scala',
4169
- '.gd': 'gdscript',
4170
- '.r': 'r', '.R': 'r',
4171
- '.svelte': 'svelte',
4172
- '.html': 'html', '.htm': 'html',
4173
- '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
4174
- '.yml': 'yaml', '.yaml': 'yaml',
4175
- '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
4176
- '.toml': 'toml',
4177
- '.properties': 'properties',
4178
- '.xml': 'xml',
4179
- '.md': 'markdown',
4180
- // Phase C specialized extractors
4181
- '.tsx': 'typescript_react',
4182
- '.vue': 'vue_sfc',
4183
- };
4184
-
4185
- function isDockerfile(name) {
4186
- return name === 'Dockerfile' || name.startsWith('Dockerfile.');
4187
- }
4164
+ // Extractor resolution goes through the dispatcher — the single source of
4165
+ // truth (#591). This file previously kept its own copy, which had drifted to
4166
+ // a dead duplicate `.vue` key.
4167
+ const { langFor } = __require('./src/extractors/dispatch');
4188
4168
 
4189
4169
  function getExtractorName(filePath) {
4190
- const base = path.basename(filePath);
4191
- const ext = path.extname(base).toLowerCase();
4192
- if (EXT_MAP[ext]) return EXT_MAP[ext];
4193
- if (isDockerfile(base)) return 'dockerfile';
4194
- return null;
4170
+ return langFor(filePath);
4195
4171
  }
4196
4172
 
4197
4173
  /** Rough token estimate: chars / 4 */
@@ -5916,6 +5892,28 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5916
5892
  return [...deps].slice(0, 5);
5917
5893
  }
5918
5894
 
5895
+ /**
5896
+ * Extract Lua require() module dependencies.
5897
+ * Captures `require "mod"` and `require("mod")`, returning compact module
5898
+ * names as they appear in source.
5899
+ * @param {string} src
5900
+ * @returns {string[]}
5901
+ */
5902
+ function extractLuaDeps(src) {
5903
+ const deps = new Set();
5904
+ const stripped = stripLuaComments(src || '');
5905
+ for (const m of stripped.matchAll(/\brequire\s*(?:\(\s*)?["']([A-Za-z0-9_.\/-]+)["']\s*\)?/g)) {
5906
+ if (m[1]) deps.add(m[1]);
5907
+ }
5908
+ return [...deps].slice(0, 5);
5909
+ }
5910
+
5911
+ function stripLuaComments(src) {
5912
+ return String(src || '')
5913
+ .replace(/--\[\[[\s\S]*?\]\]/g, '')
5914
+ .replace(/--.*$/gm, '');
5915
+ }
5916
+
5919
5917
  /**
5920
5918
  * Build reverse dependency map from forward map.
5921
5919
  * @param {Map<string, string[]>} forwardMap
@@ -5934,7 +5932,7 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5934
5932
  return reverse;
5935
5933
  }
5936
5934
 
5937
- module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
5935
+ module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, extractLuaDeps, buildReverseDepMap };
5938
5936
 
5939
5937
  };
5940
5938
 
@@ -5969,6 +5967,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5969
5967
  swift: __require('./src/extractors/swift'),
5970
5968
  dart: __require('./src/extractors/dart'),
5971
5969
  scala: __require('./src/extractors/scala'),
5970
+ lua: __require('./src/extractors/lua'),
5972
5971
  gdscript: __require('./src/extractors/gdscript'),
5973
5972
  r: __require('./src/extractors/r'),
5974
5973
  vue_sfc: __require('./src/extractors/vue_sfc'),
@@ -5989,6 +5988,21 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5989
5988
  generic: __require('./src/extractors/generic'),
5990
5989
  };
5991
5990
 
5991
+ /**
5992
+ * Extension → extractor module name. **The single source of truth for
5993
+ * extractor resolution** (#591).
5994
+ *
5995
+ * Anything that decides *which extractor module to load* must go through
5996
+ * `langFor` rather than declaring its own copy. Three copies existed and two
5997
+ * had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
5998
+ * the `--diagnose-extractors` map pointed at `vue.js` after that module was
5999
+ * deleted — which is how an unreachable extractor survived unnoticed (#582).
6000
+ *
6001
+ * Not every extension map in the codebase belongs here. `language-detector.js`
6002
+ * maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
6003
+ * short display *labels*. Both are correct for their purpose and deliberately
6004
+ * differ from resolution — folding them in would miscount languages.
6005
+ */
5992
6006
  const EXT_MAP = {
5993
6007
  '.ts': 'typescript', '.tsx': 'typescript_react',
5994
6008
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
@@ -6004,6 +6018,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
6004
6018
  '.swift': 'swift',
6005
6019
  '.dart': 'dart',
6006
6020
  '.scala': 'scala', '.sc': 'scala',
6021
+ '.lua': 'lua',
6007
6022
  '.gd': 'gdscript',
6008
6023
  '.r': 'r', '.R': 'r',
6009
6024
  '.vue': 'vue_sfc',
@@ -6049,7 +6064,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
6049
6064
  }
6050
6065
  }
6051
6066
 
6052
- module.exports = { extractFile, langFor };
6067
+ module.exports = { extractFile, langFor, EXT_MAP };
6053
6068
 
6054
6069
  };
6055
6070
 
@@ -7031,6 +7046,162 @@ __factories["./src/extractors/line-anchor"] = function(module, exports) {
7031
7046
 
7032
7047
  };
7033
7048
 
7049
+ // ── ./src/extractors/lua ──
7050
+ __factories["./src/extractors/lua"] = function(module, exports) {
7051
+
7052
+ const { capWithNotice } = __require('./src/util/truncate');
7053
+
7054
+ // Ceiling discloses what it drops rather than truncating silently (#583).
7055
+ // Collection runs to completion so the marker reports the true overflow —
7056
+ // stopping early made r.js report "+1 more" where 50 were hidden (#584).
7057
+ const PER_FILE_LIMIT = 30;
7058
+
7059
+ /**
7060
+ * Extract signatures from Lua source code.
7061
+ *
7062
+ * Recognised constructs:
7063
+ * - Global functions: `function name(args)`
7064
+ * - Module-table functions: `function M.name(args)` / `function M:name(args)`
7065
+ * - Local functions: `local function name(args)`
7066
+ * - Assigned functions: `name = function(args)` / `M.name = function(args)`
7067
+ * - Module imports: `local mod = require("mod")` as compact hints
7068
+ * - LDoc-style doc comments (`---`) as first-sentence hints
7069
+ *
7070
+ * The extractor is regex-only and zero-dependency, matching SigMap's Tier-3
7071
+ * language extractor style.
7072
+ *
7073
+ * @param {string} src - Raw file content
7074
+ * @returns {string[]} Array of signature strings
7075
+ */
7076
+ function extract(src) {
7077
+ if (!src || typeof src !== 'string') return [];
7078
+ const sigs = [];
7079
+ const hints = collectDocHints(src);
7080
+ const stripped = stripLuaComments(src);
7081
+ const seen = new Set();
7082
+
7083
+ // local foo = require('bar.baz') — useful module-surface hint, capped low.
7084
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*)\s*=\s*require\s*\(\s*['"]([A-Za-z0-9_.\/-]+)['"]\s*\)/gm)) {
7085
+ pushUnique(sigs, seen, `require ${m[2]} as ${m[1]}`);
7086
+ }
7087
+
7088
+ // local function name(args)
7089
+ for (const m of stripped.matchAll(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/gm)) {
7090
+ if (m[1].startsWith('_')) continue;
7091
+ pushUnique(sigs, seen, `local function ${m[1]}(${normalizeParams(m[2])})${applyHint(hints, m[1])}`);
7092
+ }
7093
+
7094
+ // function name(args), function M.name(args), function M:name(args)
7095
+ for (const m of stripped.matchAll(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(([^)]*)\)/gm)) {
7096
+ const name = m[1];
7097
+ if (name.startsWith('_')) continue;
7098
+ pushUnique(sigs, seen, `function ${name}(${normalizeParams(m[2])})${applyHint(hints, name)}`);
7099
+ }
7100
+
7101
+ // name = function(args), M.name = function(args), M:name = function(args)
7102
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(([^)]*)\)/gm)) {
7103
+ const name = m[1];
7104
+ if (name.startsWith('_')) continue;
7105
+ pushUnique(sigs, seen, `${name} = function(${normalizeParams(m[2])})${applyHint(hints, name)}`);
7106
+ }
7107
+
7108
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
7109
+ }
7110
+
7111
+ function pushUnique(out, seen, sig) {
7112
+ if (!sig || seen.has(sig)) return;
7113
+ seen.add(sig);
7114
+ out.push(sig);
7115
+ }
7116
+
7117
+ function normalizeParams(params) {
7118
+ return String(params || '')
7119
+ .replace(/--.*$/gm, '')
7120
+ .split(',')
7121
+ .map((p) => p.trim().replace(/\s+/g, ' '))
7122
+ .filter(Boolean)
7123
+ .join(', ');
7124
+ }
7125
+
7126
+ function applyHint(hints, name) {
7127
+ const h = hints.get(name);
7128
+ return h ? ` # ${h}` : '';
7129
+ }
7130
+
7131
+ /**
7132
+ * Attach each contiguous `---` doc block to the next function-like declaration
7133
+ * by its extracted symbol name.
7134
+ */
7135
+ function collectDocHints(src) {
7136
+ const hints = new Map();
7137
+ const lines = src.split('\n');
7138
+ let block = [];
7139
+ for (const line of lines) {
7140
+ const doc = line.match(/^\s*---\s?(.*)$/);
7141
+ if (doc) {
7142
+ block.push(doc[1]);
7143
+ } else if (block.length > 0) {
7144
+ const decl = line.match(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(/)
7145
+ || line.match(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(/)
7146
+ || line.match(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(/);
7147
+ if (decl) {
7148
+ const hint = firstDocSentence(block);
7149
+ if (hint) hints.set(decl[1], hint);
7150
+ }
7151
+ block = [];
7152
+ }
7153
+ }
7154
+ return hints;
7155
+ }
7156
+
7157
+ function firstDocSentence(block) {
7158
+ for (const raw of block) {
7159
+ const line = String(raw || '').trim();
7160
+ if (!line || line.startsWith('@')) continue;
7161
+ return line.replace(/\s+/g, ' ').slice(0, 60).replace(/[.,;:!?]+$/, '').trim();
7162
+ }
7163
+ return '';
7164
+ }
7165
+
7166
+ /** Strip Lua line and long comments while preserving strings enough for regex scans. */
7167
+ function stripLuaComments(src) {
7168
+ const out = src.split('');
7169
+ const blank = (a, b) => { for (let i = a; i < b; i++) if (out[i] !== '\n') out[i] = ' '; };
7170
+ let i = 0;
7171
+ while (i < src.length) {
7172
+ const ch = src[i];
7173
+ if (ch === '"' || ch === "'") {
7174
+ const quote = ch;
7175
+ i++;
7176
+ while (i < src.length) {
7177
+ if (src[i] === '\\') { i += 2; continue; }
7178
+ if (src[i] === quote) { i++; break; }
7179
+ if (src[i] === '\n') break;
7180
+ i++;
7181
+ }
7182
+ continue;
7183
+ }
7184
+ if (src.startsWith('--[[', i)) {
7185
+ const end = src.indexOf(']]', i + 4);
7186
+ blank(i, end === -1 ? src.length : end + 2);
7187
+ i = end === -1 ? src.length : end + 2;
7188
+ continue;
7189
+ }
7190
+ if (src.startsWith('--', i)) {
7191
+ const end = src.indexOf('\n', i + 2);
7192
+ blank(i, end === -1 ? src.length : end);
7193
+ i = end === -1 ? src.length : end;
7194
+ continue;
7195
+ }
7196
+ i++;
7197
+ }
7198
+ return out.join('');
7199
+ }
7200
+
7201
+ module.exports = { extract };
7202
+
7203
+ };
7204
+
7034
7205
  // ── ./src/extractors/markdown ──
7035
7206
  __factories["./src/extractors/markdown"] = function(module, exports) {
7036
7207
 
@@ -11996,6 +12167,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11996
12167
  const JAVA_EXTS = new Set(['.java']);
11997
12168
  const GO_EXTS = new Set(['.go']);
11998
12169
  const RS_EXTS = new Set(['.rs']);
12170
+ const KT_EXTS = new Set(['.kt', '.kts']);
12171
+ const SCALA_EXTS = new Set(['.scala', '.sc']);
11999
12172
 
12000
12173
  // Tokens that look like `name(` calls or definition headers but are language
12001
12174
  // keywords, not user symbols — never treated as a call or a definition.
@@ -12319,6 +12492,81 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12319
12492
 
12320
12493
  // Pick the masker whose comment/string syntax matches the language.
12321
12494
  // Java and Go share JS syntax (Go raw strings mask like template literals).
12495
+ /**
12496
+ * Body range for a JVM-family member that may use either a brace body or an
12497
+ * expression body (`fun f() = expr` / `def f = expr`), which Java has not.
12498
+ * An expression body runs to the end of its line — enough to capture the calls
12499
+ * it makes, which is all the graph needs.
12500
+ * @returns {{bodyStart:number, bodyEnd:number}|null} null when neither form follows
12501
+ */
12502
+ function jvmBodyRange(masked, from) {
12503
+ let k = from;
12504
+ while (k < masked.length && masked[k] !== '{' && masked[k] !== '=' && masked[k] !== '\n' && masked[k] !== ';') k++;
12505
+ if (masked[k] === '{') return { bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') };
12506
+ if (masked[k] === '=') {
12507
+ // `= {` is still a brace body, just written with an assignment.
12508
+ let j = k + 1;
12509
+ while (j < masked.length && /\s/.test(masked[j])) j++;
12510
+ if (masked[j] === '{') return { bodyStart: j, bodyEnd: matchDelim(masked, j, '{', '}') };
12511
+ const eol = masked.indexOf('\n', k);
12512
+ return { bodyStart: k, bodyEnd: eol === -1 ? masked.length : eol };
12513
+ }
12514
+ return null; // abstract member or interface declaration
12515
+ }
12516
+
12517
+ /**
12518
+ * Kotlin `fun` definitions, including extension functions (`fun Foo.bar()`,
12519
+ * recorded as `bar`) and expression bodies.
12520
+ */
12521
+ function ktDefs(masked) {
12522
+ const defs = [];
12523
+ const seen = new Set();
12524
+ 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;
12525
+ let m;
12526
+ while ((m = re.exec(masked)) !== null) {
12527
+ const name = m[2];
12528
+ if (NON_CALL.has(name)) continue;
12529
+ const paren = masked.indexOf('(', m.index + m[0].length - 1);
12530
+ const close = matchDelim(masked, paren, '(', ')');
12531
+ if (close < 0) continue;
12532
+ const range = jvmBodyRange(masked, close + 1);
12533
+ const line = lineAt(masked, m.index + 1);
12534
+ const key = name + ':' + (range ? range.bodyStart : line);
12535
+ if (seen.has(key)) continue;
12536
+ seen.add(key);
12537
+ // An abstract/interface member owns no body: it can receive edges but
12538
+ // never produces them, the same treatment javaDefs gives declarations.
12539
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: close, bodyEnd: close });
12540
+ }
12541
+ return defs;
12542
+ }
12543
+
12544
+ /**
12545
+ * Scala `def` definitions. Handles parameterless members (`def foo: Int = 1`),
12546
+ * type parameters, and expression bodies.
12547
+ */
12548
+ function scalaDefs(masked) {
12549
+ const defs = [];
12550
+ const seen = new Set();
12551
+ 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;
12552
+ let m;
12553
+ while ((m = re.exec(masked)) !== null) {
12554
+ const name = m[2];
12555
+ if (NON_CALL.has(name)) continue;
12556
+ // Step past an optional type-parameter list, then an optional value list.
12557
+ let k = m.index + m[0].length;
12558
+ if (masked[k] === '[') { const e = matchDelim(masked, k, '[', ']'); if (e < 0) continue; k = e + 1; }
12559
+ while (masked[k] === '(') { const e = matchDelim(masked, k, '(', ')'); if (e < 0) break; k = e + 1; }
12560
+ const range = jvmBodyRange(masked, k);
12561
+ const line = lineAt(masked, m.index + 1);
12562
+ const key = name + ':' + (range ? range.bodyStart : line);
12563
+ if (seen.has(key)) continue;
12564
+ seen.add(key);
12565
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: k, bodyEnd: k });
12566
+ }
12567
+ return defs;
12568
+ }
12569
+
12322
12570
  function maskFor(filePath, src) {
12323
12571
  const ext = path.extname(filePath).toLowerCase();
12324
12572
  if (PY_EXTS.has(ext)) return maskPy(src);
@@ -12333,6 +12581,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12333
12581
  if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
12334
12582
  if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
12335
12583
  if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
12584
+ if (KT_EXTS.has(ext)) return ktDefs(maskJs(src));
12585
+ if (SCALA_EXTS.has(ext)) return scalaDefs(maskJs(src));
12336
12586
  return null; // unsupported language
12337
12587
  }
12338
12588
 
@@ -12422,7 +12672,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12422
12672
  if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
12423
12673
  else if (e.isFile()) {
12424
12674
  const ext = path.extname(e.name).toLowerCase();
12425
- if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
12675
+ if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
12676
+ || RS_EXTS.has(ext) || KT_EXTS.has(ext) || SCALA_EXTS.has(ext)) out.push(full);
12426
12677
  }
12427
12678
  }
12428
12679
  }
@@ -14453,6 +14704,45 @@ __factories["./src/map/route-table"] = function(module, exports) {
14453
14704
  return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
14454
14705
  }
14455
14706
 
14707
+ /**
14708
+ * Byte offsets and prefixes of every `@Controller(...)` in a file.
14709
+ * A file may declare several controllers, so each route is attributed to the
14710
+ * nearest one above it rather than to a single file-wide prefix.
14711
+ * @param {string} content
14712
+ * @returns {Array<{index:number, prefix:string}>} ascending by index
14713
+ */
14714
+ function nestControllerPrefixes(content) {
14715
+ const out = [];
14716
+ const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
14717
+ let m;
14718
+ while ((m = re.exec(content)) !== null) {
14719
+ out.push({ index: m.index, prefix: m[1] || '' });
14720
+ }
14721
+ return out;
14722
+ }
14723
+
14724
+ /** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
14725
+ function prefixBefore(controllers, index) {
14726
+ let prefix = '';
14727
+ for (const c of controllers) {
14728
+ if (c.index > index) break;
14729
+ prefix = c.prefix;
14730
+ }
14731
+ return prefix;
14732
+ }
14733
+
14734
+ /**
14735
+ * Join a controller prefix and a method path into one route path.
14736
+ * Either side may be empty, absent, or carry its own slashes.
14737
+ * @returns {string} always slash-prefixed; never a trailing slash except '/'
14738
+ */
14739
+ function joinRoute(prefix, methodPath) {
14740
+ const parts = [prefix, methodPath]
14741
+ .map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
14742
+ .filter(Boolean);
14743
+ return parts.length ? '/' + parts.join('/') : '/';
14744
+ }
14745
+
14456
14746
  /**
14457
14747
  * Structured route rows across the supported frameworks — the data behind
14458
14748
  * `analyze`, exposed for retrieval surface-enrichment (#488).
@@ -14481,16 +14771,14 @@ __factories["./src/map/route-table"] = function(module, exports) {
14481
14771
  routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
14482
14772
  }
14483
14773
 
14484
- // NestJS decorators: @Get('/path') @Post('/path')
14485
- const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
14774
+ // NestJS: @Get(':id') / @Post() — composed with the enclosing
14775
+ // @Controller('prefix'). Without the prefix the emitted path matches
14776
+ // nothing real, which defeats the point of route pseudo-signatures (#585).
14777
+ const controllers = nestControllerPrefixes(content);
14778
+ const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
14486
14779
  while ((m = re2.exec(content)) !== null) {
14487
- routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
14488
- }
14489
-
14490
- // NestJS: @Get() with no path
14491
- const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
14492
- while ((m = re3.exec(content)) !== null) {
14493
- routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
14780
+ const prefix = prefixBefore(controllers, m.index);
14781
+ routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
14494
14782
  }
14495
14783
  }
14496
14784
 
@@ -15788,7 +16076,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15788
16076
 
15789
16077
  const SERVER_INFO = {
15790
16078
  name: 'sigmap',
15791
- version: '8.32.1',
16079
+ version: '8.34.0',
15792
16080
  description: 'SigMap MCP server — code signatures on demand',
15793
16081
  };
15794
16082
 
@@ -22082,7 +22370,7 @@ function __tryGit(args, opts = {}) {
22082
22370
  catch (_) { return ''; }
22083
22371
  }
22084
22372
 
22085
- const VERSION = '8.32.1';
22373
+ const VERSION = '8.34.0';
22086
22374
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
22087
22375
 
22088
22376
  function requireSourceOrBundled(key) {
@@ -22451,6 +22739,38 @@ function computeEffectiveMaxTokens(fileEntries, config) {
22451
22739
  return effective;
22452
22740
  }
22453
22741
 
22742
+ /**
22743
+ * Delete `.github/context-*.md` split files this run did not write (#555).
22744
+ *
22745
+ * Splits are discovered by filename pattern at read time, not by consulting
22746
+ * the config, so a file left behind by a previous `strategy` — or by a module
22747
+ * since dropped from `srcDirs` — keeps being merged into the retrieval index
22748
+ * and silently steers every query. Observed on a 524-file Java repo: a stale
22749
+ * 376 KB `context-mall-mbg.md` kept generated entities at ranks 1, 3 and 4
22750
+ * while both files implementing the feature fell outside the top 6, even
22751
+ * though `sig-index.json` correctly held zero entries for that module.
22752
+ *
22753
+ * @param {string} cwd
22754
+ * @param {string[]} keep basenames this run wrote, e.g. ['context-core.md']
22755
+ * @returns {string[]} basenames removed
22756
+ */
22757
+ function pruneStaleContextSplits(cwd, keep) {
22758
+ const ghDir = path.join(cwd, '.github');
22759
+ const kept = new Set(keep);
22760
+ const removed = [];
22761
+ let entries;
22762
+ try { entries = fs.readdirSync(ghDir); } catch (_) { return removed; }
22763
+ for (const f of entries) {
22764
+ if (!/^context-[\w.-]+\.md$/.test(f) || kept.has(f)) continue;
22765
+ try { fs.unlinkSync(path.join(ghDir, f)); removed.push(f); } catch (_) { /* best effort */ }
22766
+ }
22767
+ if (removed.length) {
22768
+ console.warn(`[sigmap] pruned ${removed.length} stale context split(s) from a previous `
22769
+ + `strategy/srcDirs: ${removed.join(', ')}`);
22770
+ }
22771
+ return removed;
22772
+ }
22773
+
22454
22774
  function applyTokenBudget(fileEntries, maxTokens) {
22455
22775
  // fileEntries: [{ filePath, sigs, mtime }]
22456
22776
  // Per-file rendered overhead: "### <path>", code fences, trailing blank line.
@@ -22532,6 +22852,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
22532
22852
  // Restore the original file order for stable output.
22533
22853
  const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
22534
22854
 
22855
+ // Record what was omitted so the artifact itself can say so (#587). The
22856
+ // stderr warning below is invisible to an agent that only reads the file:
22857
+ // 25 sections with no notice is indistinguishable from a 25-file repo.
22858
+ // Non-enumerable so every existing consumer still sees a plain array.
22859
+ if (verboseDropped.length > 0 || collapsedCount > 0) {
22860
+ Object.defineProperty(kept, '__omissions', {
22861
+ value: { dropped: verboseDropped.length, collapsed: collapsedCount, maxTokens },
22862
+ enumerable: false, writable: false, configurable: true,
22863
+ });
22864
+ }
22865
+
22535
22866
  if (verboseDropped.length > 0 || collapsedCount > 0) {
22536
22867
  const parts = [];
22537
22868
  if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
@@ -22860,6 +23191,20 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
22860
23191
  }
22861
23192
  }
22862
23193
 
23194
+ // Say what the budget left out — an omission the reader cannot see is the
23195
+ // same failure as an undisclosed truncation cap (#587, cf. #576).
23196
+ const omitted = fileEntries && fileEntries.__omissions;
23197
+ if (omitted && (omitted.dropped > 0 || omitted.collapsed > 0)) {
23198
+ const bits = [];
23199
+ if (omitted.dropped) bits.push(`${omitted.dropped} file(s) omitted`);
23200
+ if (omitted.collapsed) bits.push(`${omitted.collapsed} collapsed to anchors`);
23201
+ lines.push('');
23202
+ lines.push(`> **Not everything is here.** ${bits.join(', ')} to stay under the `
23203
+ + `${omitted.maxTokens}-token budget (tests and configs go first). `
23204
+ + 'The retrieval index still has them all — run `sigmap ask "<question>"` '
23205
+ + 'to pull in anything missing.');
23206
+ }
23207
+
22863
23208
  return lines.join('\n');
22864
23209
  }
22865
23210
 
@@ -23233,6 +23578,7 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23233
23578
  ];
23234
23579
 
23235
23580
  let totalOut = 0;
23581
+ const writtenSplits = [];
23236
23582
  for (const mod of moduleNames) {
23237
23583
  const outName = `context-${mod}.md`;
23238
23584
  const outPath = path.join(cwd, '.github', outName);
@@ -23251,7 +23597,9 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23251
23597
  console.warn(`[sigmap] per-module: wrote .github/${outName} (~${modTokens} tokens, ${budgeted.length} files)`);
23252
23598
 
23253
23599
  overviewLines.push(`| \`${mod}\` | \`.github/${outName}\` |`);
23600
+ writtenSplits.push(outName);
23254
23601
  }
23602
+ pruneStaleContextSplits(cwd, writtenSplits);
23255
23603
 
23256
23604
  overviewLines.push('');
23257
23605
  overviewLines.push('> Inject the relevant module file into your IDE context window.');
@@ -23290,6 +23638,7 @@ function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTot
23290
23638
  const coldContent = coldHeader + formatOutput(coldEntries, cwd, false, config, null);
23291
23639
  ensureDir(coldPath);
23292
23640
  fs.writeFileSync(coldPath, coldContent, 'utf8');
23641
+ pruneStaleContextSplits(cwd, ['context-cold.md']);
23293
23642
  const coldTokens = estimateTokens(coldContent);
23294
23643
 
23295
23644
  console.warn('[sigmap] hot-cold:');
@@ -23614,6 +23963,9 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
23614
23963
  result = runHotColdStrategy(cwd, configWithBudget, fileEntries, recentFiles, inputTokenTotal);
23615
23964
  } else {
23616
23965
  // 'full' — original behaviour
23966
+ // 'full' writes no split files — anything left from a previous
23967
+ // strategy would still be merged into the index at read time (#555).
23968
+ pruneStaleContextSplits(cwd, []);
23617
23969
  fileEntries = applyTokenBudget(fileEntries, effectiveMaxTokens);
23618
23970
  const droppedCount = beforeCount - fileEntries.length;
23619
23971
  const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
@@ -26849,23 +27201,16 @@ function main() {
26849
27201
  process.exit(1);
26850
27202
  }
26851
27203
 
26852
- const EXT_TO_LANG = {
26853
- '.ts': 'typescript', '.js': 'javascript', '.py': 'python',
26854
- '.java': 'java', '.kt': 'kotlin', '.go': 'go', '.rs': 'rust',
26855
- '.cs': 'csharp', '.cpp': 'cpp', '.rb': 'ruby', '.php': 'php',
26856
- '.swift': 'swift', '.dart': 'dart', '.scala': 'scala',
26857
- '.r': 'r', '.R': 'r',
26858
- '.vue': 'vue_sfc', '.svelte': 'svelte', '.html': 'html',
26859
- '.css': 'css', '.yml': 'yaml', '.sh': 'shell',
26860
- };
26861
- const SPECIAL = { 'Dockerfile': 'dockerfile' };
27204
+ // Resolution goes through the dispatcher — the single source of truth
27205
+ // (#591). This map was a third copy, and it still pointed at `vue.js`
27206
+ // after that module was deleted, which is how a dead extractor survived.
27207
+ const { langFor } = requireSourceOrBundled('./src/extractors/dispatch');
26862
27208
 
26863
27209
  let passed = 0; let failed = 0;
26864
27210
  const entries = fs.readdirSync(fixturesDir).sort();
26865
27211
 
26866
27212
  for (const filename of entries) {
26867
- const ext = path.extname(filename).toLowerCase();
26868
- const lang = EXT_TO_LANG[ext] || SPECIAL[filename];
27213
+ const lang = langFor(filename);
26869
27214
  if (!lang) continue;
26870
27215
 
26871
27216
  const fixturePath = path.join(fixturesDir, filename);