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/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.32.1 | Benchmark: sigmap-v8.32-main (2026-09-12)
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.32-main, 2026-09-12)
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.32.1 | Benchmark: sigmap-v8.32-main (2026-09-12)
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.32-main, 2026-09-12)
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.32.1",
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.32.1",
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.32.1",
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([
@@ -14,47 +14,13 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
 
16
16
  // Extension → extractor name (mirrors EXT_MAP in gen-context.js)
17
- const EXT_MAP = {
18
- '.ts': 'typescript', '.tsx': 'typescript',
19
- '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
20
- '.py': 'python', '.pyw': 'python',
21
- '.java': 'java',
22
- '.kt': 'kotlin', '.kts': 'kotlin',
23
- '.go': 'go',
24
- '.rs': 'rust',
25
- '.cs': 'csharp',
26
- '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
27
- '.rb': 'ruby', '.rake': 'ruby',
28
- '.php': 'php',
29
- '.swift': 'swift',
30
- '.dart': 'dart',
31
- '.scala': 'scala', '.sc': 'scala',
32
- '.gd': 'gdscript',
33
- '.r': 'r', '.R': 'r',
34
- '.svelte': 'svelte',
35
- '.html': 'html', '.htm': 'html',
36
- '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
37
- '.yml': 'yaml', '.yaml': 'yaml',
38
- '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
39
- '.toml': 'toml',
40
- '.properties': 'properties',
41
- '.xml': 'xml',
42
- '.md': 'markdown',
43
- // Phase C specialized extractors
44
- '.tsx': 'typescript_react',
45
- '.vue': 'vue_sfc',
46
- };
47
-
48
- function isDockerfile(name) {
49
- return name === 'Dockerfile' || name.startsWith('Dockerfile.');
50
- }
17
+ // Extractor resolution goes through the dispatcher — the single source of
18
+ // truth (#591). This file previously kept its own copy, which had drifted to
19
+ // a dead duplicate `.vue` key.
20
+ const { langFor } = require('../extractors/dispatch');
51
21
 
52
22
  function getExtractorName(filePath) {
53
- const base = path.basename(filePath);
54
- const ext = path.extname(base).toLowerCase();
55
- if (EXT_MAP[ext]) return EXT_MAP[ext];
56
- if (isDockerfile(base)) return 'dockerfile';
57
- return null;
23
+ return langFor(filePath);
58
24
  }
59
25
 
60
26
  /** Rough token estimate: chars / 4 */
@@ -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'),
@@ -48,6 +49,21 @@ const EXTRACTORS = {
48
49
  generic: require('./generic'),
49
50
  };
50
51
 
52
+ /**
53
+ * Extension → extractor module name. **The single source of truth for
54
+ * extractor resolution** (#591).
55
+ *
56
+ * Anything that decides *which extractor module to load* must go through
57
+ * `langFor` rather than declaring its own copy. Three copies existed and two
58
+ * had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
59
+ * the `--diagnose-extractors` map pointed at `vue.js` after that module was
60
+ * deleted — which is how an unreachable extractor survived unnoticed (#582).
61
+ *
62
+ * Not every extension map in the codebase belongs here. `language-detector.js`
63
+ * maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
64
+ * short display *labels*. Both are correct for their purpose and deliberately
65
+ * differ from resolution — folding them in would miscount languages.
66
+ */
51
67
  const EXT_MAP = {
52
68
  '.ts': 'typescript', '.tsx': 'typescript_react',
53
69
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
@@ -63,6 +79,7 @@ const EXT_MAP = {
63
79
  '.swift': 'swift',
64
80
  '.dart': 'dart',
65
81
  '.scala': 'scala', '.sc': 'scala',
82
+ '.lua': 'lua',
66
83
  '.gd': 'gdscript',
67
84
  '.r': 'r', '.R': 'r',
68
85
  '.vue': 'vue_sfc',
@@ -108,4 +125,4 @@ function extractFile(filePathOrName, src) {
108
125
  }
109
126
  }
110
127
 
111
- module.exports = { extractFile, langFor };
128
+ module.exports = { extractFile, langFor, EXT_MAP };
@@ -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
  }
@@ -20,6 +20,45 @@ function shouldSkipFile(rel) {
20
20
  return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
21
21
  }
22
22
 
23
+ /**
24
+ * Byte offsets and prefixes of every `@Controller(...)` in a file.
25
+ * A file may declare several controllers, so each route is attributed to the
26
+ * nearest one above it rather than to a single file-wide prefix.
27
+ * @param {string} content
28
+ * @returns {Array<{index:number, prefix:string}>} ascending by index
29
+ */
30
+ function nestControllerPrefixes(content) {
31
+ const out = [];
32
+ const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
33
+ let m;
34
+ while ((m = re.exec(content)) !== null) {
35
+ out.push({ index: m.index, prefix: m[1] || '' });
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
41
+ function prefixBefore(controllers, index) {
42
+ let prefix = '';
43
+ for (const c of controllers) {
44
+ if (c.index > index) break;
45
+ prefix = c.prefix;
46
+ }
47
+ return prefix;
48
+ }
49
+
50
+ /**
51
+ * Join a controller prefix and a method path into one route path.
52
+ * Either side may be empty, absent, or carry its own slashes.
53
+ * @returns {string} always slash-prefixed; never a trailing slash except '/'
54
+ */
55
+ function joinRoute(prefix, methodPath) {
56
+ const parts = [prefix, methodPath]
57
+ .map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
58
+ .filter(Boolean);
59
+ return parts.length ? '/' + parts.join('/') : '/';
60
+ }
61
+
23
62
  /**
24
63
  * Structured route rows across the supported frameworks — the data behind
25
64
  * `analyze`, exposed for retrieval surface-enrichment (#488).
@@ -48,16 +87,14 @@ function collectRoutes(files, cwd) {
48
87
  routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
49
88
  }
50
89
 
51
- // NestJS decorators: @Get('/path') @Post('/path')
52
- const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
90
+ // NestJS: @Get(':id') / @Post() — composed with the enclosing
91
+ // @Controller('prefix'). Without the prefix the emitted path matches
92
+ // nothing real, which defeats the point of route pseudo-signatures (#585).
93
+ const controllers = nestControllerPrefixes(content);
94
+ const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
53
95
  while ((m = re2.exec(content)) !== null) {
54
- routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
55
- }
56
-
57
- // NestJS: @Get() with no path
58
- const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
59
- while ((m = re3.exec(content)) !== null) {
60
- routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
96
+ const prefix = prefixBefore(controllers, m.index);
97
+ routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
61
98
  }
62
99
  }
63
100
 
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.32.1',
21
+ version: '8.34.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24