sigmap 8.26.2 → 8.28.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,20 +11,20 @@ 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.26.2 | Benchmark: sigmap-v8.26-main (2026-08-18)
14
+ # Version: 8.28.0 | Benchmark: sigmap-v8.28-main (2026-08-18)
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.26-main, 2026-08-18)
20
+ ## Core metrics (benchmark: sigmap-v8.28-main, 2026-08-18)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 44.0% (single-shot grep) | 82.2% (1.76× lift) |
24
+ | Retrieval hit@5 | 44.0% (single-shot grep) | 81.1% (1.73× lift) |
25
25
  | Token reduction | — | 96.8% average |
26
26
  | Task-success proxy (modeled) | — | 64.8% |
27
- | Prompts per task | 2.84 | 1.53 (46.1% fewer) |
27
+ | Prompts per task | 2.84 | 1.54 (45.7% fewer) |
28
28
  | Supported languages | — | 33 |
29
29
  | MCP tools | — | 21 |
30
30
  | npm runtime dependencies | — | 0 |
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.26.2 | Benchmark: sigmap-v8.26-main (2026-08-18)
14
+ # Version: 8.28.0 | Benchmark: sigmap-v8.28-main (2026-08-18)
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,12 +23,12 @@ 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.26-main, 2026-08-18)
26
+ ## Core metrics (benchmark: sigmap-v8.28-main, 2026-08-18)
27
27
 
28
- - hit@5 retrieval: 82.2% vs 44.0% single-shot grep baseline (1.76× lift)
28
+ - hit@5 retrieval: 81.1% 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: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
31
- - Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
31
+ - Prompts per task: 1.54 vs 2.84 baseline (45.7% fewer, modeled)
32
32
  - Languages: 33 supported · MCP tools: 21
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.26.2",
3
+ "version": "8.28.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.26.2",
3
+ "version": "8.28.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.26.2",
3
+ "version": "8.28.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
4
  const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+ const { stripComments, maskCode, readBalanced } = require('./scan');
5
6
 
6
7
  /**
7
8
  * Extract signatures from JavaScript source code.
@@ -21,16 +22,29 @@ function extract(src) {
21
22
  const returnHints = buildReturnHints(src);
22
23
  const docHints = buildDocHints(src);
23
24
 
24
- // Block comments are blanked newline-by-newline (non-newline chars spaces)
25
- // so character offsets AND line numbers stay exact for anchors.
26
- const stripped = src
27
- .replace(/\/\/.*$/gm, '')
28
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
25
+ // stripComments is string-aware (a `//` inside a string literal survives);
26
+ // maskCode additionally blanks string/template contents so every delimiter
27
+ // found on it is structural. Both are length- and newline-preserving, so
28
+ // offsets and line anchors align across all three views (#526).
29
+ const stripped = stripComments(src);
30
+ const masked = maskCode(src);
31
+
32
+ // Full params for a declaration whose `(` sits at openIdx: depth-matched
33
+ // close over masked text; TEXT sliced from stripped so string defaults keep
34
+ // their real content. Falls back to first-`)` when unbalanced (cap hit).
35
+ const paramsFrom = (openIdx) => {
36
+ const closeIdx = readBalanced(masked, openIdx);
37
+ if (closeIdx === -1) {
38
+ const naive = stripped.indexOf(')', openIdx);
39
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
40
+ }
41
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
42
+ };
29
43
 
30
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
31
- // End line for a function whose match ends at `matchEnd` (before its body brace).
44
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
45
+ // End line for a function whose params close just before `matchEnd`.
32
46
  const fnEndLine = (matchEnd, startLn) => {
33
- const brace = stripped.indexOf('{', matchEnd);
47
+ const brace = masked.indexOf('{', matchEnd);
34
48
  return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
35
49
  };
36
50
 
@@ -39,33 +53,38 @@ function extract(src) {
39
53
  for (const m of stripped.matchAll(classRegex)) {
40
54
  const prefix = m[1] ? m[1].trim() + ' ' : '';
41
55
  const bodyStart = m.index + m[0].length;
56
+ const blockEnd = blockEndIdx(bodyStart);
42
57
  sigs.push(`${prefix}class ${m[2]}`);
43
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
44
- const block = extractBlock(stripped, bodyStart);
45
- for (const meth of extractClassMembers(block, returnHints)) {
58
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
59
+ const block = stripped.slice(bodyStart, blockEnd);
60
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
61
+ for (const meth of extractClassMembers(block, maskedBlock, returnHints)) {
46
62
  sigs.push(` ${meth.text}`);
47
63
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
48
64
  }
49
65
  }
50
66
 
51
67
  // Exported named functions
52
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
68
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
53
69
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
54
70
  const retStr = formatReturnHint(returnHints.get(m[1]));
55
71
  const startLn = lineAt(stripped, m.index);
56
- sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
72
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
73
+ sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
57
74
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
58
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
75
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
59
76
  }
60
77
 
61
78
  // Exported arrow functions
62
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
79
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/gm)) {
80
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
81
+ if (closeIdx === -1 || !/^\s*=>/.test(masked.slice(closeIdx + 1, closeIdx + 40))) continue;
63
82
  const asyncKw = m[0].includes('async') ? 'async ' : '';
64
83
  const retStr = formatReturnHint(returnHints.get(m[1]));
65
84
  const startLn = lineAt(stripped, m.index);
66
- sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
85
+ sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(params)}) =>${retStr}`);
67
86
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
68
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
87
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
69
88
  }
70
89
 
71
90
  // module.exports = { ... }
@@ -80,13 +99,14 @@ function extract(src) {
80
99
  }
81
100
 
82
101
  // Top-level named functions (non-exported)
83
- for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
102
+ for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
84
103
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
85
104
  const retStr = formatReturnHint(returnHints.get(m[1]));
86
105
  const startLn = lineAt(stripped, m.index);
87
- sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
106
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
107
+ sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
88
108
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
89
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
109
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
90
110
  }
91
111
 
92
112
  const withAnchors = sigs.map((s, i) => {
@@ -108,21 +128,31 @@ function extractBlock(src, startIndex) {
108
128
  return src.slice(startIndex, i - 1);
109
129
  }
110
130
 
131
+ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
132
+
111
133
  // Returns members as { text, start, end } where start/end are char offsets
112
134
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
113
- // per-method line anchors that span the method body.
114
- function extractClassMembers(block, returnHints) {
135
+ // per-method line anchors that span the method body. `maskedBlock` is the
136
+ // same-offset maskCode slice used for balanced-delimiter scanning.
137
+ function extractClassMembers(block, maskedBlock, returnHints) {
115
138
  const members = [];
116
- for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
139
+ for (const m of maskedBlock.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(/gm)) {
117
140
  if (/^_/.test(m[1])) continue;
118
- const bodyStart = m.index + m[0].length; // just past the opening brace
119
- const end = bodyStart + extractBlock(block, bodyStart).length;
141
+ if (_CTRL_KEYWORDS.has(m[1])) continue;
142
+ const openIdx = m.index + m[0].length - 1;
143
+ const closeIdx = readBalanced(maskedBlock, openIdx);
144
+ if (closeIdx === -1) continue;
145
+ const braceMatch = maskedBlock.slice(closeIdx + 1, closeIdx + 40).match(/^\s*\{/);
146
+ if (!braceMatch) continue;
147
+ const params = block.slice(openIdx + 1, closeIdx);
148
+ const bodyStart = closeIdx + 1 + braceMatch[0].length; // just past the opening brace
149
+ const end = bodyStart + extractBlock(maskedBlock, bodyStart).length;
120
150
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
121
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
151
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
122
152
  const isAsync = m[0].includes('async ') ? 'async ' : '';
123
153
  const isStatic = m[0].includes('static ') ? 'static ' : '';
124
154
  const retStr = formatReturnHint(returnHints.get(m[1]));
125
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
155
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
126
156
  }
127
157
  return capMembersWithNotice(members, 8, 'methods');
128
158
  }
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared tokenizer-grade scanning core (G4 increment 1, #526).
5
+ *
6
+ * Hand-rolled string/comment state + delimiter depth — generalizes the
7
+ * masking in src/graph/call-graph.js (maskJs) and the balanced reader in the
8
+ * R extractor. NOT a parser, NOT tree-sitter: three small, deterministic,
9
+ * zero-dependency passes that let extractors find real declaration
10
+ * boundaries instead of truncating at the first `)`.
11
+ *
12
+ * All transforms are length- and newline-preserving, so character offsets
13
+ * and line anchors computed on the output align 1:1 with the input.
14
+ */
15
+
16
+ /**
17
+ * Blank comments only — string-aware, so `//` or `/*` INSIDE a string
18
+ * literal survives (the naive regex strip corrupted e.g. `url = "https://x"`).
19
+ * Comment bytes become spaces; newlines and everything else are preserved.
20
+ * @param {string} src
21
+ * @returns {string} same length, comments blanked
22
+ */
23
+ function stripComments(src) {
24
+ const out = src.split('');
25
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
26
+ let i = 0; const n = src.length;
27
+ while (i < n) {
28
+ const c = src[i], d = src[i + 1];
29
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
30
+ if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
31
+ if (c === '"' || c === "'" || c === '`') {
32
+ let j = i + 1;
33
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
34
+ i = Math.min(n, j + 1); continue;
35
+ }
36
+ i++;
37
+ }
38
+ return out.join('');
39
+ }
40
+
41
+ /**
42
+ * Blank comments AND string/template contents (quotes included) — the
43
+ * boundary-scanning surface: delimiters found here are always structural.
44
+ * @param {string} src
45
+ * @returns {string} same length, comments + strings blanked
46
+ */
47
+ function maskCode(src) {
48
+ const out = src.split('');
49
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
50
+ let i = 0; const n = src.length;
51
+ while (i < n) {
52
+ const c = src[i], d = src[i + 1];
53
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
54
+ if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
55
+ if (c === '"' || c === "'" || c === '`') {
56
+ let j = i + 1;
57
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
58
+ j = Math.min(n, j + 1); blank(i, j); i = j; continue;
59
+ }
60
+ i++;
61
+ }
62
+ return out.join('');
63
+ }
64
+
65
+ /**
66
+ * Index of the delimiter that closes the one open at `openIdx`, matched by
67
+ * depth over MASKED text (strings/comments already blanked, so every
68
+ * delimiter seen is structural). -1 when unbalanced within the cap.
69
+ * @param {string} masked output of maskCode
70
+ * @param {number} openIdx index of the opening delimiter
71
+ * @param {string} [open='(']
72
+ * @param {string} [close=')']
73
+ * @param {number} [cap=4000] scan ceiling in chars
74
+ * @returns {number}
75
+ */
76
+ function readBalanced(masked, openIdx, open = '(', close = ')', cap = 4000) {
77
+ if (masked[openIdx] !== open) return -1;
78
+ let depth = 1;
79
+ const end = Math.min(masked.length, openIdx + cap);
80
+ for (let i = openIdx + 1; i < end; i++) {
81
+ const ch = masked[i];
82
+ if (ch === open) depth++;
83
+ else if (ch === close) {
84
+ depth--;
85
+ if (depth === 0) return i;
86
+ }
87
+ }
88
+ return -1;
89
+ }
90
+
91
+ module.exports = { stripComments, maskCode, readBalanced };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
4
  const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+ const { stripComments, maskCode, readBalanced } = require('./scan');
5
6
 
6
7
  /**
7
8
  * Extract signatures from TypeScript source code.
@@ -23,15 +24,25 @@ function extract(src) {
23
24
  // anchors are applied once at return.
24
25
  const anchors = [];
25
26
 
26
- // Strip comments to simplify matching. Block comments are blanked
27
- // newline-by-newline (non-newline chars spaces) so character offsets AND
28
- // line numbers stay exact. Line comments preserve their trailing newline.
29
- const stripped = src
30
- .replace(/\/\/.*$/gm, '')
31
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
27
+ // stripComments is string-aware (a `//` inside a string literal survives);
28
+ // maskCode additionally blanks string/template contents so every delimiter
29
+ // found on it is structural. Both are length- and newline-preserving, so
30
+ // offsets and line anchors align across all three views (#526).
31
+ const stripped = stripComments(src);
32
+ const masked = maskCode(src);
33
+
34
+ // Full params for a declaration whose `(` sits at openIdx (see javascript.js).
35
+ const paramsFrom = (openIdx) => {
36
+ const closeIdx = readBalanced(masked, openIdx);
37
+ if (closeIdx === -1) {
38
+ const naive = stripped.indexOf(')', openIdx);
39
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
40
+ }
41
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
42
+ };
32
43
 
33
44
  // Index of the closing brace for a block whose body starts at bodyStart.
34
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
45
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
35
46
 
36
47
  // Exported interfaces
37
48
  for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
@@ -66,10 +77,12 @@ function extract(src) {
66
77
  const prefix = m[1] ? 'export ' : '';
67
78
  const abs = m[2] ? 'abstract ' : '';
68
79
  const bodyStart = m.index + m[0].length;
80
+ const blockEnd = blockEndIdx(bodyStart);
69
81
  sigs.push(`${prefix}${abs}class ${m[3]}`);
70
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
71
- const block = extractBlock(stripped, bodyStart);
72
- const methods = extractClassMembers(block);
82
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
83
+ const block = stripped.slice(bodyStart, blockEnd);
84
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
85
+ const methods = extractClassMembers(block, maskedBlock);
73
86
  for (const meth of methods) {
74
87
  sigs.push(` ${meth.text}`);
75
88
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
@@ -77,13 +90,19 @@ function extract(src) {
77
90
  }
78
91
 
79
92
  // Exported top-level functions (not methods)
80
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
93
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
94
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
95
+ if (closeIdx === -1) continue;
96
+ // Declaration shape check + return-type capture, mirroring the old
97
+ // `\)(?:\s*:\s*[^{]+)?\s*\{` tail against the text after the real close.
98
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{]+?)?\s*\{/);
99
+ if (!tail) continue;
81
100
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
82
- const params = normalizeParams(m[2]);
83
- const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
84
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
101
+ const params = normalizeParams(rawParams);
102
+ const retRaw = tail[1] ? stripped.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
103
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 30) : '';
85
104
  const retStr = retType ? ` → ${retType}` : '';
86
- const bodyStart = m.index + m[0].length;
105
+ const bodyStart = closeIdx + 1 + tail[0].length;
87
106
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
88
107
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
89
108
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
@@ -106,15 +125,21 @@ function extract(src) {
106
125
  }
107
126
 
108
127
  // Exported arrow functions / const functions
109
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
128
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(/gm)) {
129
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
130
+ if (closeIdx === -1) continue;
131
+ // Arrow shape check, mirroring the old `\)\s*(?::\s*[^=>{]+)?\s*=>` tail.
132
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^\s*(?::\s*[^=>{]+)?\s*=>/);
133
+ if (!tail) continue;
110
134
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
111
- const params = normalizeParams(m[2]);
135
+ const params = normalizeParams(rawParams);
112
136
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
113
137
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
114
- const bodyStart = stripped.indexOf('{', m.index + m[0].length);
138
+ const matchEnd = closeIdx + 1 + tail[0].length;
139
+ const bodyStart = masked.indexOf('{', matchEnd);
115
140
  const endLn = bodyStart !== -1
116
141
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
117
- : lineAt(stripped, m.index + m[0].length);
142
+ : lineAt(stripped, matchEnd);
118
143
  anchors.push([lineAt(stripped, m.index), endLn]);
119
144
 
120
145
  // Hooks: capture compact return object shape for use* functions.
@@ -182,6 +207,7 @@ function extractBlock(src, startIndex) {
182
207
  // Returns members as { text, start, end } where start/end are char offsets
183
208
  // WITHIN `block`, so the caller can resolve member line anchors.
184
209
  function extractInterfaceMembers(block) {
210
+ const maskedBlock = maskCode(block);
185
211
  const members = [];
186
212
  for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
187
213
  const readonly = m[1] ? 'readonly ' : '';
@@ -190,9 +216,12 @@ function extractInterfaceMembers(block) {
190
216
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
191
217
  members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
192
218
  }
193
- for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
219
+ for (const m of maskedBlock.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
220
+ const openIdx = m.index + m[0].length - 1;
221
+ const closeIdx = readBalanced(maskedBlock, openIdx);
222
+ if (closeIdx === -1 || !/^\s*:/.test(maskedBlock.slice(closeIdx + 1, closeIdx + 40))) continue;
194
223
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
195
- members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
224
+ members.push({ text: `${m[1]}(${normalizeParams(block.slice(openIdx + 1, closeIdx))})`, start, end: closeIdx + 1 });
196
225
  }
197
226
  return capMembersWithNotice(members, 8, 'members');
198
227
  }
@@ -202,30 +231,66 @@ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'ca
202
231
  // Returns members as { text, start, end } where start/end are char offsets
203
232
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
204
233
  // per-method line anchors that span the method body.
205
- function extractClassMembers(block) {
234
+ function extractClassMembers(block, maskedBlock) {
235
+ const masked = maskedBlock || maskCode(block);
206
236
  const members = [];
207
237
  // Public methods (skip private/protected/_ prefixed and control-flow keywords)
208
- const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
209
- for (const m of block.matchAll(methodRe)) {
238
+ const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(/gm;
239
+ for (const m of masked.matchAll(methodRe)) {
210
240
  if (_CTRL_KEYWORDS.has(m[1])) continue;
211
241
  if (/^(private|protected|_)/.test(m[1])) continue;
212
- const bodyStart = m.index + m[0].length; // just past the opening brace
213
- const end = bodyStart + extractBlock(block, bodyStart).length;
242
+ const openIdx = m.index + m[0].length - 1;
243
+ const closeIdx = readBalanced(masked, openIdx);
244
+ if (closeIdx === -1) continue;
245
+ // Declaration tail check + return-type capture, mirroring the old
246
+ // `\)(?:\s*:\s*[^{;]+)?\s*\{` shape against the text after the real close.
247
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{;]+?)?\s*\{/);
248
+ if (!tail) continue;
249
+ const params = block.slice(openIdx + 1, closeIdx);
250
+ const bodyStart = closeIdx + 1 + tail[0].length; // just past the opening brace
251
+ const end = bodyStart + extractBlock(masked, bodyStart).length;
214
252
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
215
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
253
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
216
254
  const isAsync = m[0].includes('async ') ? 'async ' : '';
217
255
  const isStatic = m[0].includes('static ') ? 'static ' : '';
218
- const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
219
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
256
+ const retRaw = tail[1] ? block.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
257
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 20) : '';
220
258
  const retStr = retType ? ` → ${retType}` : '';
221
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
259
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
222
260
  }
223
261
  return capMembersWithNotice(members, 8, 'methods');
224
262
  }
225
263
 
226
264
  function normalizeParams(params) {
227
265
  if (!params) return '';
228
- return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
266
+ const compact = params.trim().replace(/\s+/g, ' ');
267
+ // Strip `: type` annotations at top level only — nested delimiters in types
268
+ // (e.g. `cb: (x: number) => void`, `m: Map<string, X>`) are consumed with
269
+ // their annotation instead of truncating at the first `)` or `,` (#526).
270
+ let out = '';
271
+ let depth = 0;
272
+ let inType = false;
273
+ let quote = null;
274
+ for (let i = 0; i < compact.length; i++) {
275
+ const ch = compact[i];
276
+ if (quote) {
277
+ if (ch === '\\') { if (!inType) out += ch + (compact[i + 1] || ''); i++; continue; }
278
+ if (ch === quote) quote = null;
279
+ if (!inType) out += ch;
280
+ continue;
281
+ }
282
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; if (!inType) out += ch; continue; }
283
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<') depth++;
284
+ else if (ch === ')' || ch === ']' || ch === '}' || (ch === '>' && compact[i - 1] !== '=')) depth--;
285
+ if (inType) {
286
+ if (depth <= 0 && ch === ',') { inType = false; out += ch; }
287
+ else if (depth <= 0 && ch === '=' && compact[i + 1] !== '>') { inType = false; out += ' ='; }
288
+ continue;
289
+ }
290
+ if (depth === 0 && ch === ':') { inType = true; continue; }
291
+ out += ch;
292
+ }
293
+ return out.replace(/\s*,\s*/g, ', ').replace(/\s+/g, ' ').trim();
229
294
  }
230
295
 
231
296
  // First prose sentence of the JSDoc block immediately preceding an exported
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.26.2',
21
+ version: '8.28.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24