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/CHANGELOG.md +25 -0
- package/README.md +6 -6
- package/gen-context.js +468 -61
- package/llms-full.txt +4 -4
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/javascript.js +57 -27
- package/src/extractors/scan.js +91 -0
- package/src/extractors/typescript.js +96 -31
- package/src/mcp/server.js +1 -1
- package/src/verify/arity.js +180 -0
- package/src/verify/hallucination-guard.js +34 -1
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.
|
|
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.
|
|
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) |
|
|
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.
|
|
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.
|
|
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
|
+
## Core metrics (benchmark: sigmap-v8.28-main, 2026-08-18)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval:
|
|
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.
|
|
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.
|
|
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": {
|
|
@@ -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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
31
|
-
// End line for a function whose
|
|
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 =
|
|
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,
|
|
44
|
-
const block =
|
|
45
|
-
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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+)?\(
|
|
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(
|
|
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(
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
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,
|
|
71
|
-
const block =
|
|
72
|
-
const
|
|
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*\(
|
|
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(
|
|
83
|
-
const
|
|
84
|
-
const retType =
|
|
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 =
|
|
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+)?\(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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*\(
|
|
209
|
-
for (const m of
|
|
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
|
|
213
|
-
const
|
|
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(
|
|
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
|
|
219
|
-
const retType =
|
|
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(
|
|
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
|
-
|
|
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