sigmap 8.32.1 → 8.33.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,20 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.33.0] — 2026-09-13
14
+
15
+ ### Added
16
+ - The generated artifact now **says when the token budget left files out**. The `[sigmap] budget: dropped N file(s)` warning only ever went to stderr, which an agent reading the file never sees — on flask that meant 25 of 51 files present with no indication the other 26 existed, indistinguishable from a 25-file repo. `applyTokenBudget` attaches a non-enumerable summary to the array it returns, so all four call sites (full, per-module, hot-cold) gain the footer without a signature change. The wording states that the omitted files are still in the retrieval index and reachable via `sigmap ask`, and a test asserts it does not overstate the loss — replacing a misleading silence with a misleading warning would be no improvement (#587, PR #599)
17
+ - A test fixture for **every** language extractor, plus a guard that fails when one is missing (#588, PR #595). The 8 languages named in the issue were only part of it: the guard immediately found a ninth, `typescript_react` — `.tsx`, every React component, and the extractor that shipped a silent truncation cap in v8.32.0. It also caught a stale `test/expected/vue.txt` that outlived the `vue.js` deleted in v8.32.1, which had been making `--diagnose-extractors` print a silent `SKIP`. That diagnostic went from 21 passing with an unnoticed SKIP to 32 passing with none
18
+
19
+ ### Fixed
20
+ - NestJS route paths now compose the `@Controller` prefix. `@Controller('cats')` + `@Get(':id')` emitted `:id` rather than `/cats/:id`, so the route pseudo-signature matched nothing a user would ask about — defeating the purpose of the feature, which exists so a route-worded query can reach a controller whose signatures never mention the path. The prefix is attributed per controller rather than per file, since a file may declare several. The other six claimed frameworks were verified unchanged (#585, PR #598)
21
+
22
+ ### Changed
23
+ - Extractor resolution has **one source of truth**. Three places decided which extractor module to load and two had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and the `--diagnose-extractors` map still pointed at `vue.js` after that module was deleted — which is how an unreachable extractor survived unnoticed in the first place. The hand-maintained copy was also incomplete, with no `.gd` entry, so gdscript was never actually diagnosed despite having both a fixture and recorded expected output. `language-detector.js` and `dashboard.js` are deliberately **not** merged in: they map `.tsx` for language *statistics* and display *labels*, not resolution, and a test pins that distinction so a future dedup cannot quietly break language counts (#591, PR #597)
24
+
25
+ ---
26
+
13
27
  ## [8.32.1] — 2026-09-13
14
28
 
15
29
 
package/README.md CHANGED
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.32-main (21 repositories, including R language)
126
- Date : 2026-09-12
125
+ Benchmark : sigmap-v8.33-main (21 repositories, including R language)
126
+ Date : 2026-09-13
127
127
 
128
128
  Hit@5 : 78.9% (grep-agent baseline 44.0% — 1.73× lift)
129
129
  Token reduction: 96.8% (across 21 repos)
package/gen-context.js CHANGED
@@ -4151,47 +4151,13 @@ __factories["./src/eval/analyzer"] = function(module, exports) {
4151
4151
  const path = require('path');
4152
4152
 
4153
4153
  // 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
- }
4154
+ // Extractor resolution goes through the dispatcher — the single source of
4155
+ // truth (#591). This file previously kept its own copy, which had drifted to
4156
+ // a dead duplicate `.vue` key.
4157
+ const { langFor } = __require('./src/extractors/dispatch');
4188
4158
 
4189
4159
  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;
4160
+ return langFor(filePath);
4195
4161
  }
4196
4162
 
4197
4163
  /** Rough token estimate: chars / 4 */
@@ -5989,6 +5955,21 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5989
5955
  generic: __require('./src/extractors/generic'),
5990
5956
  };
5991
5957
 
5958
+ /**
5959
+ * Extension → extractor module name. **The single source of truth for
5960
+ * extractor resolution** (#591).
5961
+ *
5962
+ * Anything that decides *which extractor module to load* must go through
5963
+ * `langFor` rather than declaring its own copy. Three copies existed and two
5964
+ * had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
5965
+ * the `--diagnose-extractors` map pointed at `vue.js` after that module was
5966
+ * deleted — which is how an unreachable extractor survived unnoticed (#582).
5967
+ *
5968
+ * Not every extension map in the codebase belongs here. `language-detector.js`
5969
+ * maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
5970
+ * short display *labels*. Both are correct for their purpose and deliberately
5971
+ * differ from resolution — folding them in would miscount languages.
5972
+ */
5992
5973
  const EXT_MAP = {
5993
5974
  '.ts': 'typescript', '.tsx': 'typescript_react',
5994
5975
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
@@ -6049,7 +6030,7 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
6049
6030
  }
6050
6031
  }
6051
6032
 
6052
- module.exports = { extractFile, langFor };
6033
+ module.exports = { extractFile, langFor, EXT_MAP };
6053
6034
 
6054
6035
  };
6055
6036
 
@@ -14453,6 +14434,45 @@ __factories["./src/map/route-table"] = function(module, exports) {
14453
14434
  return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
14454
14435
  }
14455
14436
 
14437
+ /**
14438
+ * Byte offsets and prefixes of every `@Controller(...)` in a file.
14439
+ * A file may declare several controllers, so each route is attributed to the
14440
+ * nearest one above it rather than to a single file-wide prefix.
14441
+ * @param {string} content
14442
+ * @returns {Array<{index:number, prefix:string}>} ascending by index
14443
+ */
14444
+ function nestControllerPrefixes(content) {
14445
+ const out = [];
14446
+ const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
14447
+ let m;
14448
+ while ((m = re.exec(content)) !== null) {
14449
+ out.push({ index: m.index, prefix: m[1] || '' });
14450
+ }
14451
+ return out;
14452
+ }
14453
+
14454
+ /** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
14455
+ function prefixBefore(controllers, index) {
14456
+ let prefix = '';
14457
+ for (const c of controllers) {
14458
+ if (c.index > index) break;
14459
+ prefix = c.prefix;
14460
+ }
14461
+ return prefix;
14462
+ }
14463
+
14464
+ /**
14465
+ * Join a controller prefix and a method path into one route path.
14466
+ * Either side may be empty, absent, or carry its own slashes.
14467
+ * @returns {string} always slash-prefixed; never a trailing slash except '/'
14468
+ */
14469
+ function joinRoute(prefix, methodPath) {
14470
+ const parts = [prefix, methodPath]
14471
+ .map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
14472
+ .filter(Boolean);
14473
+ return parts.length ? '/' + parts.join('/') : '/';
14474
+ }
14475
+
14456
14476
  /**
14457
14477
  * Structured route rows across the supported frameworks — the data behind
14458
14478
  * `analyze`, exposed for retrieval surface-enrichment (#488).
@@ -14481,16 +14501,14 @@ __factories["./src/map/route-table"] = function(module, exports) {
14481
14501
  routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
14482
14502
  }
14483
14503
 
14484
- // NestJS decorators: @Get('/path') @Post('/path')
14485
- const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
14504
+ // NestJS: @Get(':id') / @Post() — composed with the enclosing
14505
+ // @Controller('prefix'). Without the prefix the emitted path matches
14506
+ // nothing real, which defeats the point of route pseudo-signatures (#585).
14507
+ const controllers = nestControllerPrefixes(content);
14508
+ const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
14486
14509
  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 });
14510
+ const prefix = prefixBefore(controllers, m.index);
14511
+ routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
14494
14512
  }
14495
14513
  }
14496
14514
 
@@ -15788,7 +15806,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15788
15806
 
15789
15807
  const SERVER_INFO = {
15790
15808
  name: 'sigmap',
15791
- version: '8.32.1',
15809
+ version: '8.33.0',
15792
15810
  description: 'SigMap MCP server — code signatures on demand',
15793
15811
  };
15794
15812
 
@@ -22082,7 +22100,7 @@ function __tryGit(args, opts = {}) {
22082
22100
  catch (_) { return ''; }
22083
22101
  }
22084
22102
 
22085
- const VERSION = '8.32.1';
22103
+ const VERSION = '8.33.0';
22086
22104
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
22087
22105
 
22088
22106
  function requireSourceOrBundled(key) {
@@ -22532,6 +22550,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
22532
22550
  // Restore the original file order for stable output.
22533
22551
  const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
22534
22552
 
22553
+ // Record what was omitted so the artifact itself can say so (#587). The
22554
+ // stderr warning below is invisible to an agent that only reads the file:
22555
+ // 25 sections with no notice is indistinguishable from a 25-file repo.
22556
+ // Non-enumerable so every existing consumer still sees a plain array.
22557
+ if (verboseDropped.length > 0 || collapsedCount > 0) {
22558
+ Object.defineProperty(kept, '__omissions', {
22559
+ value: { dropped: verboseDropped.length, collapsed: collapsedCount, maxTokens },
22560
+ enumerable: false, writable: false, configurable: true,
22561
+ });
22562
+ }
22563
+
22535
22564
  if (verboseDropped.length > 0 || collapsedCount > 0) {
22536
22565
  const parts = [];
22537
22566
  if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
@@ -22860,6 +22889,20 @@ function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
22860
22889
  }
22861
22890
  }
22862
22891
 
22892
+ // Say what the budget left out — an omission the reader cannot see is the
22893
+ // same failure as an undisclosed truncation cap (#587, cf. #576).
22894
+ const omitted = fileEntries && fileEntries.__omissions;
22895
+ if (omitted && (omitted.dropped > 0 || omitted.collapsed > 0)) {
22896
+ const bits = [];
22897
+ if (omitted.dropped) bits.push(`${omitted.dropped} file(s) omitted`);
22898
+ if (omitted.collapsed) bits.push(`${omitted.collapsed} collapsed to anchors`);
22899
+ lines.push('');
22900
+ lines.push(`> **Not everything is here.** ${bits.join(', ')} to stay under the `
22901
+ + `${omitted.maxTokens}-token budget (tests and configs go first). `
22902
+ + 'The retrieval index still has them all — run `sigmap ask "<question>"` '
22903
+ + 'to pull in anything missing.');
22904
+ }
22905
+
22863
22906
  return lines.join('\n');
22864
22907
  }
22865
22908
 
@@ -26849,23 +26892,16 @@ function main() {
26849
26892
  process.exit(1);
26850
26893
  }
26851
26894
 
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' };
26895
+ // Resolution goes through the dispatcher — the single source of truth
26896
+ // (#591). This map was a third copy, and it still pointed at `vue.js`
26897
+ // after that module was deleted, which is how a dead extractor survived.
26898
+ const { langFor } = requireSourceOrBundled('./src/extractors/dispatch');
26862
26899
 
26863
26900
  let passed = 0; let failed = 0;
26864
26901
  const entries = fs.readdirSync(fixturesDir).sort();
26865
26902
 
26866
26903
  for (const filename of entries) {
26867
- const ext = path.extname(filename).toLowerCase();
26868
- const lang = EXT_TO_LANG[ext] || SPECIAL[filename];
26904
+ const lang = langFor(filename);
26869
26905
  if (!lang) continue;
26870
26906
 
26871
26907
  const fixturePath = path.join(fixturesDir, filename);
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.33.0 | Benchmark: sigmap-v8.33-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.33-main, 2026-09-13)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
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.33.0 | Benchmark: sigmap-v8.33-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,7 +23,7 @@ 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.33-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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.32.1",
3
+ "version": "8.33.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.33.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.32.1",
3
+ "version": "8.33.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 */
@@ -48,6 +48,21 @@ const EXTRACTORS = {
48
48
  generic: require('./generic'),
49
49
  };
50
50
 
51
+ /**
52
+ * Extension → extractor module name. **The single source of truth for
53
+ * extractor resolution** (#591).
54
+ *
55
+ * Anything that decides *which extractor module to load* must go through
56
+ * `langFor` rather than declaring its own copy. Three copies existed and two
57
+ * had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
58
+ * the `--diagnose-extractors` map pointed at `vue.js` after that module was
59
+ * deleted — which is how an unreachable extractor survived unnoticed (#582).
60
+ *
61
+ * Not every extension map in the codebase belongs here. `language-detector.js`
62
+ * maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
63
+ * short display *labels*. Both are correct for their purpose and deliberately
64
+ * differ from resolution — folding them in would miscount languages.
65
+ */
51
66
  const EXT_MAP = {
52
67
  '.ts': 'typescript', '.tsx': 'typescript_react',
53
68
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
@@ -108,4 +123,4 @@ function extractFile(filePathOrName, src) {
108
123
  }
109
124
  }
110
125
 
111
- module.exports = { extractFile, langFor };
126
+ module.exports = { extractFile, langFor, EXT_MAP };
@@ -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.33.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24