sigmap 8.33.0 → 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/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.34.0] — 2026-09-13
14
+
15
+ ### Added
16
+ - **Lua extractor** (Tier 3) — global and local functions, module-table functions (`function M.name`, `function M:name`), assigned functions, `require` module hints, and LDoc `---` doc comments as first-sentence hints (#540, PR #550) — thanks **@zerone0x**. The contribution shipped its own fixture and expected output, which is exactly what the #588 fixture guard now requires, contributed before that guard existed. Brought up to the disclosure convention it predates: its caps now report the true overflow rather than truncating silently, with the ceiling unchanged
17
+ - Call-graph definitions for **Kotlin and Scala** (#586, PR #602). `extractDefs` returned null for `.kt`/`.scala` and the walk did not collect them, so the graph was empty for both — and an empty graph returns no error, so `--impact` and blast radius silently read zero. That is the failure mode that cost a release in #561, and akka (Scala) sits in the gated JVM corpus, so CI exercised Scala in a way that could never detect it. Adds `jvmBodyRange` for expression bodies (`fun f() = expr`), which Java does not have. On a small Kotlin+Scala tree: 0 symbols / 0 edges → 8 / 2. Scope is pinned by test — same-file calls resolve, cross-file *receiver* calls do not yet, because receiver typing is Java-shaped
18
+
19
+ ### Fixed
20
+ - Generated `context-*.md` splits from a previous `strategy` no longer pollute retrieval (#555, PR #601). Splits are discovered by filename pattern at read time, not by consulting the config, so a file left by an earlier strategy — or by a module since dropped from `srcDirs` — kept being merged into the index and steering every query. On a 524-file Java repo a stale 376 KB split held ranks 1, 3 and 4 with generated entities, one unrelated to the query, while both files implementing the feature fell outside the top 6 — with `sig-index.json` correctly holding zero entries for it. Each strategy now declares what it wrote and the rest is pruned, and the prune is reported rather than silent
21
+
22
+ ---
23
+
13
24
  ## [8.33.0] — 2026-09-13
14
25
 
15
26
  ### Added
package/README.md CHANGED
@@ -64,7 +64,7 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
64
64
  - **62.9% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
65
  - **1.58 prompts per task** — down from 2.84 (44.4% fewer retries, modeled)
66
66
  <!--/SM:whyMetrics-->
67
- - **<!--SM:languages-->32<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
67
+ - **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
68
68
  - **No vendor lock-in** — works with any AI assistant or local LLM
69
69
  - **No API costs** — use local models (Ollama, llama.cpp, vLLM) with zero token fees
70
70
  - **Full privacy** — keep your code and context on your machine
@@ -122,7 +122,7 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.33-main (21 repositories, including R language)
125
+ Benchmark : sigmap-v8.34-main (21 repositories, including R language)
126
126
  Date : 2026-09-13
127
127
 
128
128
  Hit@5 : 78.9% (grep-agent baseline 44.0% — 1.73× lift)
@@ -332,7 +332,7 @@ sigmap --health
332
332
  | Benchmark methodology | [benchmark.html](https://sigmap.io/guide/benchmark.html) |
333
333
  | Config reference | [config.html](https://sigmap.io/guide/config.html) |
334
334
  | Roadmap | [roadmap.html](https://sigmap.io/guide/roadmap.html) |
335
- | <!--SM:languages-->32<!--/SM:languages--> languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
335
+ | <!--SM:languages-->33<!--/SM:languages--> languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
336
336
 
337
337
  ---
338
338
 
@@ -389,7 +389,7 @@ See [.github/PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md) for the
389
389
 
390
390
  ---
391
391
 
392
- ## <!--SM:languages-->32<!--/SM:languages--> languages
392
+ ## <!--SM:languages-->33<!--/SM:languages--> languages
393
393
 
394
394
  TypeScript · JavaScript · Python · Java · Kotlin · Go · Rust · C# · C/C++ · Ruby · PHP · Swift · Dart · Scala · Vue · Svelte · HTML · CSS/SCSS · YAML · Shell · SQL · GraphQL · Terraform · Protobuf · Dockerfile · TOML · XML · Properties · Markdown · R · GDScript
395
395
 
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([
@@ -5882,6 +5892,28 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5882
5892
  return [...deps].slice(0, 5);
5883
5893
  }
5884
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
+
5885
5917
  /**
5886
5918
  * Build reverse dependency map from forward map.
5887
5919
  * @param {Map<string, string[]>} forwardMap
@@ -5900,7 +5932,7 @@ __factories["./src/extractors/deps"] = function(module, exports) {
5900
5932
  return reverse;
5901
5933
  }
5902
5934
 
5903
- module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
5935
+ module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, extractLuaDeps, buildReverseDepMap };
5904
5936
 
5905
5937
  };
5906
5938
 
@@ -5935,6 +5967,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5935
5967
  swift: __require('./src/extractors/swift'),
5936
5968
  dart: __require('./src/extractors/dart'),
5937
5969
  scala: __require('./src/extractors/scala'),
5970
+ lua: __require('./src/extractors/lua'),
5938
5971
  gdscript: __require('./src/extractors/gdscript'),
5939
5972
  r: __require('./src/extractors/r'),
5940
5973
  vue_sfc: __require('./src/extractors/vue_sfc'),
@@ -5985,6 +6018,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5985
6018
  '.swift': 'swift',
5986
6019
  '.dart': 'dart',
5987
6020
  '.scala': 'scala', '.sc': 'scala',
6021
+ '.lua': 'lua',
5988
6022
  '.gd': 'gdscript',
5989
6023
  '.r': 'r', '.R': 'r',
5990
6024
  '.vue': 'vue_sfc',
@@ -7012,6 +7046,162 @@ __factories["./src/extractors/line-anchor"] = function(module, exports) {
7012
7046
 
7013
7047
  };
7014
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
+
7015
7205
  // ── ./src/extractors/markdown ──
7016
7206
  __factories["./src/extractors/markdown"] = function(module, exports) {
7017
7207
 
@@ -11977,6 +12167,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11977
12167
  const JAVA_EXTS = new Set(['.java']);
11978
12168
  const GO_EXTS = new Set(['.go']);
11979
12169
  const RS_EXTS = new Set(['.rs']);
12170
+ const KT_EXTS = new Set(['.kt', '.kts']);
12171
+ const SCALA_EXTS = new Set(['.scala', '.sc']);
11980
12172
 
11981
12173
  // Tokens that look like `name(` calls or definition headers but are language
11982
12174
  // keywords, not user symbols — never treated as a call or a definition.
@@ -12300,6 +12492,81 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12300
12492
 
12301
12493
  // Pick the masker whose comment/string syntax matches the language.
12302
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
+
12303
12570
  function maskFor(filePath, src) {
12304
12571
  const ext = path.extname(filePath).toLowerCase();
12305
12572
  if (PY_EXTS.has(ext)) return maskPy(src);
@@ -12314,6 +12581,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12314
12581
  if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
12315
12582
  if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
12316
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));
12317
12586
  return null; // unsupported language
12318
12587
  }
12319
12588
 
@@ -12403,7 +12672,8 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12403
12672
  if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
12404
12673
  else if (e.isFile()) {
12405
12674
  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);
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);
12407
12677
  }
12408
12678
  }
12409
12679
  }
@@ -15806,7 +16076,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15806
16076
 
15807
16077
  const SERVER_INFO = {
15808
16078
  name: 'sigmap',
15809
- version: '8.33.0',
16079
+ version: '8.34.0',
15810
16080
  description: 'SigMap MCP server — code signatures on demand',
15811
16081
  };
15812
16082
 
@@ -22100,7 +22370,7 @@ function __tryGit(args, opts = {}) {
22100
22370
  catch (_) { return ''; }
22101
22371
  }
22102
22372
 
22103
- const VERSION = '8.33.0';
22373
+ const VERSION = '8.34.0';
22104
22374
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
22105
22375
 
22106
22376
  function requireSourceOrBundled(key) {
@@ -22469,6 +22739,38 @@ function computeEffectiveMaxTokens(fileEntries, config) {
22469
22739
  return effective;
22470
22740
  }
22471
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
+
22472
22774
  function applyTokenBudget(fileEntries, maxTokens) {
22473
22775
  // fileEntries: [{ filePath, sigs, mtime }]
22474
22776
  // Per-file rendered overhead: "### <path>", code fences, trailing blank line.
@@ -23276,6 +23578,7 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23276
23578
  ];
23277
23579
 
23278
23580
  let totalOut = 0;
23581
+ const writtenSplits = [];
23279
23582
  for (const mod of moduleNames) {
23280
23583
  const outName = `context-${mod}.md`;
23281
23584
  const outPath = path.join(cwd, '.github', outName);
@@ -23294,7 +23597,9 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
23294
23597
  console.warn(`[sigmap] per-module: wrote .github/${outName} (~${modTokens} tokens, ${budgeted.length} files)`);
23295
23598
 
23296
23599
  overviewLines.push(`| \`${mod}\` | \`.github/${outName}\` |`);
23600
+ writtenSplits.push(outName);
23297
23601
  }
23602
+ pruneStaleContextSplits(cwd, writtenSplits);
23298
23603
 
23299
23604
  overviewLines.push('');
23300
23605
  overviewLines.push('> Inject the relevant module file into your IDE context window.');
@@ -23333,6 +23638,7 @@ function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTot
23333
23638
  const coldContent = coldHeader + formatOutput(coldEntries, cwd, false, config, null);
23334
23639
  ensureDir(coldPath);
23335
23640
  fs.writeFileSync(coldPath, coldContent, 'utf8');
23641
+ pruneStaleContextSplits(cwd, ['context-cold.md']);
23336
23642
  const coldTokens = estimateTokens(coldContent);
23337
23643
 
23338
23644
  console.warn('[sigmap] hot-cold:');
@@ -23657,6 +23963,9 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
23657
23963
  result = runHotColdStrategy(cwd, configWithBudget, fileEntries, recentFiles, inputTokenTotal);
23658
23964
  } else {
23659
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, []);
23660
23969
  fileEntries = applyTokenBudget(fileEntries, effectiveMaxTokens);
23661
23970
  const droppedCount = beforeCount - fileEntries.length;
23662
23971
  const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
package/llms-full.txt CHANGED
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.33.0 | Benchmark: sigmap-v8.33-main (2026-09-13)
14
+ # Version: 8.34.0 | Benchmark: sigmap-v8.34-main (2026-09-13)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v8.33-main, 2026-09-13)
20
+ ## Core metrics (benchmark: sigmap-v8.34-main, 2026-09-13)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
@@ -25,7 +25,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
25
25
  | Token reduction | — | 96.8% average |
26
26
  | Task-success proxy (modeled) | — | 62.9% |
27
27
  | Prompts per task | 2.84 | 1.58 (44.4% fewer) |
28
- | Supported languages | — | 32 |
28
+ | Supported languages | — | 33 |
29
29
  | MCP tools | — | 21 |
30
30
  | npm runtime dependencies | — | 0 |
31
31
 
@@ -370,9 +370,9 @@ impact = {"depth":3,"includeSigs":true}
370
370
 
371
371
  ---
372
372
 
373
- ## Supported languages (32 extractors)
373
+ ## Supported languages (33 extractors)
374
374
 
375
- cpp, csharp, css, dart, dockerfile, gdscript, go, graphql, html, java, javascript, kotlin, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue_sfc, xml, yaml
375
+ cpp, csharp, css, dart, dockerfile, gdscript, go, graphql, html, java, javascript, kotlin, lua, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue_sfc, xml, yaml
376
376
 
377
377
  ---
378
378
 
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.33.0 | Benchmark: sigmap-v8.33-main (2026-09-13)
14
+ # Version: 8.34.0 | Benchmark: sigmap-v8.34-main (2026-09-13)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,13 +23,13 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v8.33-main, 2026-09-13)
26
+ ## Core metrics (benchmark: sigmap-v8.34-main, 2026-09-13)
27
27
 
28
28
  - hit@5 retrieval: 78.9% vs 44.0% single-shot grep baseline (1.73× lift)
29
29
  - Token reduction: 96.8% average across benchmark repos
30
30
  - Task-success proxy: 62.9% (modeled from retrieval tiers, not measured LLM sessions)
31
31
  - Prompts per task: 1.58 vs 2.84 baseline (44.4% fewer, modeled)
32
- - Languages: 32 supported · MCP tools: 21
32
+ - Languages: 33 supported · MCP tools: 21
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
34
34
 
35
35
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.33.0",
3
+ "version": "8.34.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.33.0",
3
+ "version": "8.34.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -29,6 +29,7 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.lua': 'lua',
32
33
  '.gd': 'gdscript',
33
34
  '.r': 'r', '.R': 'r',
34
35
  '.vue': 'vue',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.33.0",
3
+ "version": "8.34.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -81,7 +81,7 @@ const SUPPORTED_CODE_EXTS = new Set([
81
81
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
82
82
  '.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
83
83
  '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
84
- '.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
84
+ '.swift', '.dart', '.scala', '.sc', '.lua', '.vue', '.svelte',
85
85
  '.html', '.htm', '.css', '.scss', '.sass', '.less',
86
86
  '.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
87
87
  '.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
@@ -38,7 +38,7 @@ const TOKENS_PER_FILE = 25;
38
38
  const SOURCE_EXTS = new Set([
39
39
  '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
40
40
  '.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
41
- '.scala', '.php', '.gd', '.r', '.R',
41
+ '.scala', '.php', '.lua', '.gd', '.r', '.R',
42
42
  ]);
43
43
 
44
44
  /** Raw user config file content, or null when absent/unparsable. */
@@ -19,6 +19,7 @@ const EXT_TO_LANG = {
19
19
  '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
20
20
  '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
21
21
  '.dart': 'dart', '.scala': 'scala', '.php': 'php',
22
+ '.lua': 'lua',
22
23
  '.gd': 'gdscript',
23
24
  '.r': 'r', '.R': 'r',
24
25
  };
@@ -156,12 +156,21 @@ const REGISTRY = {
156
156
  akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
157
157
  play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
158
158
  spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
159
- zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
159
+ zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
160
160
  },
161
161
  srcDirs: ['src/main/scala','src'],
162
162
  penalties: ['target'],
163
163
  },
164
164
 
165
+ lua: {
166
+ manifestFiles: ['.luarc.json', 'selene.toml', 'stylua.toml'],
167
+ frameworks: {
168
+ luarocks: { detectionFiles: ['*.rockspec'], srcDirs: ['src','lua','lib'] },
169
+ },
170
+ srcDirs: ['src','lua','lib'],
171
+ penalties: ['.luarocks','luarocks_modules'],
172
+ },
173
+
165
174
  r: {
166
175
  manifestFiles: ['DESCRIPTION','renv.lock'],
167
176
  frameworks: {
@@ -7,7 +7,7 @@ const { git } = require('../util/git');
7
7
  const CODE_EXTS = new Set([
8
8
  '.js','.mjs','.cjs','.ts','.tsx','.jsx',
9
9
  '.py','.rb','.go','.rs','.java','.kt',
10
- '.cs','.cpp','.c','.h','.swift','.dart','.scala','.php',
10
+ '.cs','.cpp','.c','.h','.swift','.dart','.scala','.php','.lua',
11
11
  ]);
12
12
 
13
13
  const AUTO_SKIP = new Set([
@@ -89,6 +89,28 @@ function extractRDeps(src) {
89
89
  return [...deps].slice(0, 5);
90
90
  }
91
91
 
92
+ /**
93
+ * Extract Lua require() module dependencies.
94
+ * Captures `require "mod"` and `require("mod")`, returning compact module
95
+ * names as they appear in source.
96
+ * @param {string} src
97
+ * @returns {string[]}
98
+ */
99
+ function extractLuaDeps(src) {
100
+ const deps = new Set();
101
+ const stripped = stripLuaComments(src || '');
102
+ for (const m of stripped.matchAll(/\brequire\s*(?:\(\s*)?["']([A-Za-z0-9_.\/-]+)["']\s*\)?/g)) {
103
+ if (m[1]) deps.add(m[1]);
104
+ }
105
+ return [...deps].slice(0, 5);
106
+ }
107
+
108
+ function stripLuaComments(src) {
109
+ return String(src || '')
110
+ .replace(/--\[\[[\s\S]*?\]\]/g, '')
111
+ .replace(/--.*$/gm, '');
112
+ }
113
+
92
114
  /**
93
115
  * Build reverse dependency map from forward map.
94
116
  * @param {Map<string, string[]>} forwardMap
@@ -107,4 +129,4 @@ function buildReverseDepMap(forwardMap) {
107
129
  return reverse;
108
130
  }
109
131
 
110
- module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
132
+ module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, extractLuaDeps, buildReverseDepMap };
@@ -28,6 +28,7 @@ const EXTRACTORS = {
28
28
  swift: require('./swift'),
29
29
  dart: require('./dart'),
30
30
  scala: require('./scala'),
31
+ lua: require('./lua'),
31
32
  gdscript: require('./gdscript'),
32
33
  r: require('./r'),
33
34
  vue_sfc: require('./vue_sfc'),
@@ -78,6 +79,7 @@ const EXT_MAP = {
78
79
  '.swift': 'swift',
79
80
  '.dart': 'dart',
80
81
  '.scala': 'scala', '.sc': 'scala',
82
+ '.lua': 'lua',
81
83
  '.gd': 'gdscript',
82
84
  '.r': 'r', '.R': 'r',
83
85
  '.vue': 'vue_sfc',
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { capWithNotice } = require('../util/truncate');
4
+
5
+ // Ceiling discloses what it drops rather than truncating silently (#583).
6
+ // Collection runs to completion so the marker reports the true overflow —
7
+ // stopping early made r.js report "+1 more" where 50 were hidden (#584).
8
+ const PER_FILE_LIMIT = 30;
9
+
10
+ /**
11
+ * Extract signatures from Lua source code.
12
+ *
13
+ * Recognised constructs:
14
+ * - Global functions: `function name(args)`
15
+ * - Module-table functions: `function M.name(args)` / `function M:name(args)`
16
+ * - Local functions: `local function name(args)`
17
+ * - Assigned functions: `name = function(args)` / `M.name = function(args)`
18
+ * - Module imports: `local mod = require("mod")` as compact hints
19
+ * - LDoc-style doc comments (`---`) as first-sentence hints
20
+ *
21
+ * The extractor is regex-only and zero-dependency, matching SigMap's Tier-3
22
+ * language extractor style.
23
+ *
24
+ * @param {string} src - Raw file content
25
+ * @returns {string[]} Array of signature strings
26
+ */
27
+ function extract(src) {
28
+ if (!src || typeof src !== 'string') return [];
29
+ const sigs = [];
30
+ const hints = collectDocHints(src);
31
+ const stripped = stripLuaComments(src);
32
+ const seen = new Set();
33
+
34
+ // local foo = require('bar.baz') — useful module-surface hint, capped low.
35
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*)\s*=\s*require\s*\(\s*['"]([A-Za-z0-9_.\/-]+)['"]\s*\)/gm)) {
36
+ pushUnique(sigs, seen, `require ${m[2]} as ${m[1]}`);
37
+ }
38
+
39
+ // local function name(args)
40
+ for (const m of stripped.matchAll(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/gm)) {
41
+ if (m[1].startsWith('_')) continue;
42
+ pushUnique(sigs, seen, `local function ${m[1]}(${normalizeParams(m[2])})${applyHint(hints, m[1])}`);
43
+ }
44
+
45
+ // function name(args), function M.name(args), function M:name(args)
46
+ for (const m of stripped.matchAll(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(([^)]*)\)/gm)) {
47
+ const name = m[1];
48
+ if (name.startsWith('_')) continue;
49
+ pushUnique(sigs, seen, `function ${name}(${normalizeParams(m[2])})${applyHint(hints, name)}`);
50
+ }
51
+
52
+ // name = function(args), M.name = function(args), M:name = function(args)
53
+ for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(([^)]*)\)/gm)) {
54
+ const name = m[1];
55
+ if (name.startsWith('_')) continue;
56
+ pushUnique(sigs, seen, `${name} = function(${normalizeParams(m[2])})${applyHint(hints, name)}`);
57
+ }
58
+
59
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
60
+ }
61
+
62
+ function pushUnique(out, seen, sig) {
63
+ if (!sig || seen.has(sig)) return;
64
+ seen.add(sig);
65
+ out.push(sig);
66
+ }
67
+
68
+ function normalizeParams(params) {
69
+ return String(params || '')
70
+ .replace(/--.*$/gm, '')
71
+ .split(',')
72
+ .map((p) => p.trim().replace(/\s+/g, ' '))
73
+ .filter(Boolean)
74
+ .join(', ');
75
+ }
76
+
77
+ function applyHint(hints, name) {
78
+ const h = hints.get(name);
79
+ return h ? ` # ${h}` : '';
80
+ }
81
+
82
+ /**
83
+ * Attach each contiguous `---` doc block to the next function-like declaration
84
+ * by its extracted symbol name.
85
+ */
86
+ function collectDocHints(src) {
87
+ const hints = new Map();
88
+ const lines = src.split('\n');
89
+ let block = [];
90
+ for (const line of lines) {
91
+ const doc = line.match(/^\s*---\s?(.*)$/);
92
+ if (doc) {
93
+ block.push(doc[1]);
94
+ } else if (block.length > 0) {
95
+ const decl = line.match(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(/)
96
+ || line.match(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(/)
97
+ || line.match(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(/);
98
+ if (decl) {
99
+ const hint = firstDocSentence(block);
100
+ if (hint) hints.set(decl[1], hint);
101
+ }
102
+ block = [];
103
+ }
104
+ }
105
+ return hints;
106
+ }
107
+
108
+ function firstDocSentence(block) {
109
+ for (const raw of block) {
110
+ const line = String(raw || '').trim();
111
+ if (!line || line.startsWith('@')) continue;
112
+ return line.replace(/\s+/g, ' ').slice(0, 60).replace(/[.,;:!?]+$/, '').trim();
113
+ }
114
+ return '';
115
+ }
116
+
117
+ /** Strip Lua line and long comments while preserving strings enough for regex scans. */
118
+ function stripLuaComments(src) {
119
+ const out = src.split('');
120
+ const blank = (a, b) => { for (let i = a; i < b; i++) if (out[i] !== '\n') out[i] = ' '; };
121
+ let i = 0;
122
+ while (i < src.length) {
123
+ const ch = src[i];
124
+ if (ch === '"' || ch === "'") {
125
+ const quote = ch;
126
+ i++;
127
+ while (i < src.length) {
128
+ if (src[i] === '\\') { i += 2; continue; }
129
+ if (src[i] === quote) { i++; break; }
130
+ if (src[i] === '\n') break;
131
+ i++;
132
+ }
133
+ continue;
134
+ }
135
+ if (src.startsWith('--[[', i)) {
136
+ const end = src.indexOf(']]', i + 4);
137
+ blank(i, end === -1 ? src.length : end + 2);
138
+ i = end === -1 ? src.length : end + 2;
139
+ continue;
140
+ }
141
+ if (src.startsWith('--', i)) {
142
+ const end = src.indexOf('\n', i + 2);
143
+ blank(i, end === -1 ? src.length : end);
144
+ i = end === -1 ? src.length : end;
145
+ continue;
146
+ }
147
+ i++;
148
+ }
149
+ return out.join('');
150
+ }
151
+
152
+ module.exports = { extract };
@@ -26,6 +26,8 @@ const PY_EXTS = new Set(['.py', '.pyw']);
26
26
  const JAVA_EXTS = new Set(['.java']);
27
27
  const GO_EXTS = new Set(['.go']);
28
28
  const RS_EXTS = new Set(['.rs']);
29
+ const KT_EXTS = new Set(['.kt', '.kts']);
30
+ const SCALA_EXTS = new Set(['.scala', '.sc']);
29
31
 
30
32
  // Tokens that look like `name(` calls or definition headers but are language
31
33
  // keywords, not user symbols — never treated as a call or a definition.
@@ -349,6 +351,81 @@ function rustDefs(masked) {
349
351
 
350
352
  // Pick the masker whose comment/string syntax matches the language.
351
353
  // Java and Go share JS syntax (Go raw strings mask like template literals).
354
+ /**
355
+ * Body range for a JVM-family member that may use either a brace body or an
356
+ * expression body (`fun f() = expr` / `def f = expr`), which Java has not.
357
+ * An expression body runs to the end of its line — enough to capture the calls
358
+ * it makes, which is all the graph needs.
359
+ * @returns {{bodyStart:number, bodyEnd:number}|null} null when neither form follows
360
+ */
361
+ function jvmBodyRange(masked, from) {
362
+ let k = from;
363
+ while (k < masked.length && masked[k] !== '{' && masked[k] !== '=' && masked[k] !== '\n' && masked[k] !== ';') k++;
364
+ if (masked[k] === '{') return { bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') };
365
+ if (masked[k] === '=') {
366
+ // `= {` is still a brace body, just written with an assignment.
367
+ let j = k + 1;
368
+ while (j < masked.length && /\s/.test(masked[j])) j++;
369
+ if (masked[j] === '{') return { bodyStart: j, bodyEnd: matchDelim(masked, j, '{', '}') };
370
+ const eol = masked.indexOf('\n', k);
371
+ return { bodyStart: k, bodyEnd: eol === -1 ? masked.length : eol };
372
+ }
373
+ return null; // abstract member or interface declaration
374
+ }
375
+
376
+ /**
377
+ * Kotlin `fun` definitions, including extension functions (`fun Foo.bar()`,
378
+ * recorded as `bar`) and expression bodies.
379
+ */
380
+ function ktDefs(masked) {
381
+ const defs = [];
382
+ const seen = new Set();
383
+ 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;
384
+ let m;
385
+ while ((m = re.exec(masked)) !== null) {
386
+ const name = m[2];
387
+ if (NON_CALL.has(name)) continue;
388
+ const paren = masked.indexOf('(', m.index + m[0].length - 1);
389
+ const close = matchDelim(masked, paren, '(', ')');
390
+ if (close < 0) continue;
391
+ const range = jvmBodyRange(masked, close + 1);
392
+ const line = lineAt(masked, m.index + 1);
393
+ const key = name + ':' + (range ? range.bodyStart : line);
394
+ if (seen.has(key)) continue;
395
+ seen.add(key);
396
+ // An abstract/interface member owns no body: it can receive edges but
397
+ // never produces them, the same treatment javaDefs gives declarations.
398
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: close, bodyEnd: close });
399
+ }
400
+ return defs;
401
+ }
402
+
403
+ /**
404
+ * Scala `def` definitions. Handles parameterless members (`def foo: Int = 1`),
405
+ * type parameters, and expression bodies.
406
+ */
407
+ function scalaDefs(masked) {
408
+ const defs = [];
409
+ const seen = new Set();
410
+ 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;
411
+ let m;
412
+ while ((m = re.exec(masked)) !== null) {
413
+ const name = m[2];
414
+ if (NON_CALL.has(name)) continue;
415
+ // Step past an optional type-parameter list, then an optional value list.
416
+ let k = m.index + m[0].length;
417
+ if (masked[k] === '[') { const e = matchDelim(masked, k, '[', ']'); if (e < 0) continue; k = e + 1; }
418
+ while (masked[k] === '(') { const e = matchDelim(masked, k, '(', ')'); if (e < 0) break; k = e + 1; }
419
+ const range = jvmBodyRange(masked, k);
420
+ const line = lineAt(masked, m.index + 1);
421
+ const key = name + ':' + (range ? range.bodyStart : line);
422
+ if (seen.has(key)) continue;
423
+ seen.add(key);
424
+ defs.push(range ? { name, line, ...range } : { name, line, bodyStart: k, bodyEnd: k });
425
+ }
426
+ return defs;
427
+ }
428
+
352
429
  function maskFor(filePath, src) {
353
430
  const ext = path.extname(filePath).toLowerCase();
354
431
  if (PY_EXTS.has(ext)) return maskPy(src);
@@ -363,6 +440,8 @@ function extractDefs(filePath, src) {
363
440
  if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
364
441
  if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
365
442
  if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
443
+ if (KT_EXTS.has(ext)) return ktDefs(maskJs(src));
444
+ if (SCALA_EXTS.has(ext)) return scalaDefs(maskJs(src));
366
445
  return null; // unsupported language
367
446
  }
368
447
 
@@ -452,7 +531,8 @@ function _walk(dir, excludeSet, out, depth, maxDepth) {
452
531
  if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
453
532
  else if (e.isFile()) {
454
533
  const ext = path.extname(e.name).toLowerCase();
455
- if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
534
+ if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
535
+ || RS_EXTS.has(ext) || KT_EXTS.has(ext) || SCALA_EXTS.has(ext)) out.push(full);
456
536
  }
457
537
  }
458
538
  }
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.33.0',
21
+ version: '8.34.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24