sigmap 8.33.0 → 8.35.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 +24 -0
- package/README.md +12 -12
- package/gen-context.js +531 -74
- package/llms-full.txt +9 -9
- package/llms.txt +7 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +1 -0
- package/packages/core/package.json +1 -1
- package/src/config/loader.js +1 -1
- package/src/config/tune.js +1 -1
- package/src/discovery/language-detector.js +1 -0
- package/src/discovery/source-root-registry.js +10 -1
- package/src/discovery/source-root-scorer.js +1 -1
- package/src/extractors/cpp.js +8 -3
- package/src/extractors/csharp.js +8 -3
- package/src/extractors/css.js +1 -1
- package/src/extractors/dart.js +8 -3
- package/src/extractors/deps.js +23 -1
- package/src/extractors/dispatch.js +2 -0
- package/src/extractors/dockerfile.js +1 -1
- package/src/extractors/gdscript.js +2 -2
- package/src/extractors/go.js +13 -5
- package/src/extractors/html.js +1 -1
- package/src/extractors/javascript.js +7 -3
- package/src/extractors/kotlin.js +8 -3
- package/src/extractors/lua.js +152 -0
- package/src/extractors/markdown.js +1 -1
- package/src/extractors/php.js +8 -3
- package/src/extractors/properties.js +1 -1
- package/src/extractors/python.js +5 -2
- package/src/extractors/python_dataclass.js +3 -1
- package/src/extractors/r.js +1 -1
- package/src/extractors/ruby.js +1 -1
- package/src/extractors/rust.js +13 -5
- package/src/extractors/scala.js +8 -3
- package/src/extractors/shell.js +1 -1
- package/src/extractors/svelte.js +1 -1
- package/src/extractors/swift.js +8 -3
- package/src/extractors/toml.js +1 -1
- package/src/extractors/typescript.js +8 -4
- package/src/extractors/typescript_react.js +1 -1
- package/src/extractors/vue_sfc.js +3 -1
- package/src/extractors/xml.js +3 -1
- package/src/extractors/yaml.js +1 -1
- package/src/graph/call-graph.js +81 -1
- package/src/mcp/server.js +46 -4
- package/src/retrieval/ranker.js +14 -5
package/src/extractors/python.js
CHANGED
|
@@ -6,7 +6,10 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
6
6
|
|
|
7
7
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
8
8
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
9
|
-
const PER_FILE_LIMIT =
|
|
9
|
+
const PER_FILE_LIMIT = 200;
|
|
10
|
+
|
|
11
|
+
// Per-class member ceiling, disclosed via capWithNotice (#576).
|
|
12
|
+
const MEMBER_LIMIT = 120;
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* 1-based line of the last source line belonging to a top-level (indent 0)
|
|
@@ -162,7 +165,7 @@ function extractClassMethods(stripped, startIndex) {
|
|
|
162
165
|
methods.push(`${asyncKw}def ${m[1]}(${params})${retStr}`);
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
return methods
|
|
168
|
+
return capWithNotice(methods, MEMBER_LIMIT, 'methods');
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
function tryExtractDataclassFields(stripped, classIndex) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { capWithNotice } = require('../util/truncate');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Extract Python dataclass, Pydantic model, and SQLAlchemy ORM metadata.
|
|
5
7
|
* Focuses on model fields, validation, and relationships.
|
|
@@ -71,7 +73,7 @@ function extract(src) {
|
|
|
71
73
|
sigs.push('config-class');
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
return Array.from(new Set(sigs))
|
|
76
|
+
return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
module.exports = { extract };
|
package/src/extractors/r.js
CHANGED
|
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
4
4
|
|
|
5
5
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
6
6
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
7
|
-
const PER_FILE_LIMIT =
|
|
7
|
+
const PER_FILE_LIMIT = 200;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract signatures from R source code.
|
package/src/extractors/ruby.js
CHANGED
|
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
4
4
|
|
|
5
5
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
6
6
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
7
|
-
const PER_FILE_LIMIT =
|
|
7
|
+
const PER_FILE_LIMIT = 200;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract signatures from Ruby source code.
|
package/src/extractors/rust.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { lineAt, withAnchor } = require('./line-anchor');
|
|
4
|
-
const { capWithNotice } = require('../util/truncate');
|
|
4
|
+
const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
5
5
|
|
|
6
6
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
7
7
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
8
|
-
|
|
8
|
+
// Class bodies are scanned to this many characters. Real classes routinely
|
|
9
|
+
// run past the old 4KB scan window — truncating there silently hid every
|
|
10
|
+
// member after ~4000 chars AND anchored class end-lines short (#576). The
|
|
11
|
+
// ceiling only guards against pathological input (Java parity, #551).
|
|
12
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
13
|
+
const PER_FILE_LIMIT = 200;
|
|
14
|
+
|
|
15
|
+
// Per-impl member ceiling, disclosed via capMembersWithNotice (#576).
|
|
16
|
+
const MEMBER_LIMIT = 120;
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Extract signatures from Rust source code.
|
|
@@ -63,7 +71,7 @@ function extract(src) {
|
|
|
63
71
|
const block = extractBlock(stripped, bodyStart);
|
|
64
72
|
sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
65
73
|
for (const fn of extractMethods(block)) {
|
|
66
|
-
sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)), fn.name));
|
|
74
|
+
sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + (fn.declIdx || 0)), lineAt(stripped, bodyStart + (fn.endIdx || 0))), fn.name));
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -80,7 +88,7 @@ function extract(src) {
|
|
|
80
88
|
|
|
81
89
|
function extractBlock(src, startIndex) {
|
|
82
90
|
let depth = 1, i = startIndex;
|
|
83
|
-
const end = Math.min(src.length, startIndex +
|
|
91
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
84
92
|
while (i < end && depth > 0) {
|
|
85
93
|
if (src[i] === '{') depth++;
|
|
86
94
|
else if (src[i] === '}') depth--;
|
|
@@ -101,7 +109,7 @@ function extractMethods(block) {
|
|
|
101
109
|
endIdx: m.index + m[0].length,
|
|
102
110
|
});
|
|
103
111
|
}
|
|
104
|
-
return methods
|
|
112
|
+
return capMembersWithNotice(methods, MEMBER_LIMIT, 'methods');
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
function normalizeParams(params) {
|
package/src/extractors/scala.js
CHANGED
|
@@ -6,8 +6,13 @@ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
|
6
6
|
// Ceilings sit above the default `maxSigsPerFile` so the configured budget
|
|
7
7
|
// governs output rather than a literal buried here, and omissions are disclosed
|
|
8
8
|
// — an undisclosed cap looks like a class that simply has eight methods (#576).
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Class bodies are scanned to this many characters. Real classes routinely
|
|
10
|
+
// run past the old 4KB scan window — truncating there silently hid every
|
|
11
|
+
// member after ~4000 chars AND anchored class end-lines short (#576). The
|
|
12
|
+
// ceiling only guards against pathological input (Java parity, #551).
|
|
13
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
14
|
+
const MEMBER_LIMIT = 120;
|
|
15
|
+
const PER_FILE_LIMIT = 200;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Extract signatures from Scala source code.
|
|
@@ -54,7 +59,7 @@ function extract(src) {
|
|
|
54
59
|
|
|
55
60
|
function extractBlock(src, startIndex) {
|
|
56
61
|
let depth = 1, i = startIndex;
|
|
57
|
-
const end = Math.min(src.length, startIndex +
|
|
62
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
58
63
|
while (i < end && depth > 0) {
|
|
59
64
|
if (src[i] === '{') depth++;
|
|
60
65
|
else if (src[i] === '}') depth--;
|
package/src/extractors/shell.js
CHANGED
|
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
4
4
|
|
|
5
5
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
6
6
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
7
|
-
const PER_FILE_LIMIT =
|
|
7
|
+
const PER_FILE_LIMIT = 200;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract signatures from shell scripts (bash, zsh, fish).
|
package/src/extractors/svelte.js
CHANGED
|
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
4
4
|
|
|
5
5
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
6
6
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
7
|
-
const PER_FILE_LIMIT =
|
|
7
|
+
const PER_FILE_LIMIT = 200;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract signatures from Svelte components.
|
package/src/extractors/swift.js
CHANGED
|
@@ -6,8 +6,13 @@ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
|
6
6
|
// Ceilings sit above the default `maxSigsPerFile` so the configured budget
|
|
7
7
|
// governs output rather than a literal buried here, and omissions are disclosed
|
|
8
8
|
// — an undisclosed cap looks like a class that simply has eight methods (#576).
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Class bodies are scanned to this many characters. Real classes routinely
|
|
10
|
+
// run past the old 4KB scan window — truncating there silently hid every
|
|
11
|
+
// member after ~4000 chars AND anchored class end-lines short (#576). The
|
|
12
|
+
// ceiling only guards against pathological input (Java parity, #551).
|
|
13
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
14
|
+
const MEMBER_LIMIT = 120;
|
|
15
|
+
const PER_FILE_LIMIT = 200;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Extract signatures from Swift source code.
|
|
@@ -61,7 +66,7 @@ function extract(src) {
|
|
|
61
66
|
|
|
62
67
|
function extractBlock(src, startIndex) {
|
|
63
68
|
let depth = 1, i = startIndex;
|
|
64
|
-
const end = Math.min(src.length, startIndex +
|
|
69
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
65
70
|
while (i < end && depth > 0) {
|
|
66
71
|
if (src[i] === '{') depth++;
|
|
67
72
|
else if (src[i] === '}') depth--;
|
package/src/extractors/toml.js
CHANGED
|
@@ -4,6 +4,10 @@ const { lineAt, withAnchor } = require('./line-anchor');
|
|
|
4
4
|
const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
5
5
|
const { stripComments, maskCode, readBalanced } = require('./scan');
|
|
6
6
|
|
|
7
|
+
// Class bodies are scanned to this many characters — guard against
|
|
8
|
+
// pathological input only; the old 4KB window silently hid members (#576).
|
|
9
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
10
|
+
|
|
7
11
|
/**
|
|
8
12
|
* Extract signatures from TypeScript source code.
|
|
9
13
|
* Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
|
|
@@ -189,13 +193,13 @@ function extract(src) {
|
|
|
189
193
|
const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
|
|
190
194
|
return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
|
|
191
195
|
});
|
|
192
|
-
return capWithNotice(withAnchors,
|
|
196
|
+
return capWithNotice(withAnchors, 200, 'signatures');
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
function extractBlock(src, startIndex) {
|
|
196
200
|
let depth = 1;
|
|
197
201
|
let i = startIndex;
|
|
198
|
-
const end = Math.min(src.length, startIndex +
|
|
202
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
199
203
|
while (i < end && depth > 0) {
|
|
200
204
|
if (src[i] === '{') depth++;
|
|
201
205
|
else if (src[i] === '}') depth--;
|
|
@@ -223,7 +227,7 @@ function extractInterfaceMembers(block) {
|
|
|
223
227
|
const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
|
|
224
228
|
members.push({ text: `${m[1]}(${normalizeParams(block.slice(openIdx + 1, closeIdx))})`, start, end: closeIdx + 1 });
|
|
225
229
|
}
|
|
226
|
-
return capMembersWithNotice(members,
|
|
230
|
+
return capMembersWithNotice(members, 120, 'members');
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
|
|
@@ -258,7 +262,7 @@ function extractClassMembers(block, maskedBlock) {
|
|
|
258
262
|
const retStr = retType ? ` → ${retType}` : '';
|
|
259
263
|
members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
|
|
260
264
|
}
|
|
261
|
-
return capMembersWithNotice(members,
|
|
265
|
+
return capMembersWithNotice(members, 120, 'methods');
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
function normalizeParams(params) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { capWithNotice } = require('../util/truncate');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Extract Vue Single-File Component (SFC) signatures from .vue files.
|
|
5
7
|
* Captures component metadata: name, props, emits, slots, composables, lifecycle.
|
|
@@ -93,7 +95,7 @@ function extract(src) {
|
|
|
93
95
|
sigs.push(`slot ${s}`);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
return Array.from(new Set(sigs))
|
|
98
|
+
return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
module.exports = { extract };
|
package/src/extractors/xml.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { capWithNotice } = require('../util/truncate');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Lightweight XML config extractor.
|
|
5
7
|
* Captures root tags, key config tags, and id/name/class attributes.
|
|
@@ -40,7 +42,7 @@ function extract(src) {
|
|
|
40
42
|
if (cls) sigs.push(`${tag} -> ${cls[1]}`);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
return Array.from(new Set(sigs))
|
|
45
|
+
return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
module.exports = { extract };
|
package/src/extractors/yaml.js
CHANGED
|
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
|
|
|
4
4
|
|
|
5
5
|
// Ceiling sits above the default `maxSigsPerFile` so the configured budget
|
|
6
6
|
// governs output rather than a literal buried here, and omissions are disclosed (#576).
|
|
7
|
-
const PER_FILE_LIMIT =
|
|
7
|
+
const PER_FILE_LIMIT = 200;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Extract signatures from YAML configuration files.
|
package/src/graph/call-graph.js
CHANGED
|
@@ -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)
|
|
534
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
|
|
535
|
+
|| RS_EXTS.has(ext) || KT_EXTS.has(ext) || SCALA_EXTS.has(ext)) out.push(full);
|
|
456
536
|
}
|
|
457
537
|
}
|
|
458
538
|
}
|
package/src/mcp/server.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* One JSON object per line on both stdin and stdout.
|
|
8
8
|
*
|
|
9
9
|
* Supported methods:
|
|
10
|
-
* initialize → serverInfo + capabilities
|
|
11
|
-
* tools/list →
|
|
10
|
+
* initialize → serverInfo + capabilities + negotiated protocolVersion
|
|
11
|
+
* tools/list → 21 tool definitions
|
|
12
12
|
* tools/call → dispatch to handler, return result
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -18,10 +18,22 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
|
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '8.
|
|
21
|
+
version: '8.35.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// Protocol revisions this server actually speaks. The tools-only surface —
|
|
26
|
+
// initialize / tools/list / tools/call plus the initialized/cancelled
|
|
27
|
+
// notifications — is identical across these revisions, and the
|
|
28
|
+
// @hasmcp/mcp-spec-test suite passes every applicable 2025-11-25 case against
|
|
29
|
+
// it. 2026-07-28 is deliberately absent: that revision requires
|
|
30
|
+
// server/discover, which is not implemented. Newest first: a client offering
|
|
31
|
+
// a version outside this list is downgraded to [0], never echoed back —
|
|
32
|
+
// echoing an unspeakable version is itself a spec violation (#544), and it
|
|
33
|
+
// made the conformance suite believe 2026-07-28 was supported, producing six
|
|
34
|
+
// phantom server/discover failures on a revision never really offered (#545).
|
|
35
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
36
|
+
|
|
25
37
|
// ---------------------------------------------------------------------------
|
|
26
38
|
// JSON-RPC helpers
|
|
27
39
|
// ---------------------------------------------------------------------------
|
|
@@ -46,9 +58,32 @@ function dispatch(msg, cwd) {
|
|
|
46
58
|
return;
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
// server/discover (spec 2026-07-28) — session-less discovery, answerable
|
|
62
|
+
// before any handshake, so a client can learn the honest version list
|
|
63
|
+
// instead of offering versions and hoping. The response is a
|
|
64
|
+
// CacheableResult: deterministic, so the TTL promise of stability holds.
|
|
65
|
+
// Note supportedVersions does NOT include 2026-07-28 — answering discover
|
|
66
|
+
// is forward-compatible plumbing, not a claim to serve that revision's
|
|
67
|
+
// whole surface (per-result envelopes, inline _meta negotiation).
|
|
68
|
+
if (method === 'server/discover') {
|
|
69
|
+
respond(id, {
|
|
70
|
+
resultType: 'complete',
|
|
71
|
+
cacheScope: 'public',
|
|
72
|
+
ttlMs: 3600000,
|
|
73
|
+
supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
74
|
+
capabilities: { tools: {} },
|
|
75
|
+
serverInfo: SERVER_INFO,
|
|
76
|
+
instructions: 'SigMap serves code signatures for the working directory. Call query_context or search_signatures to rank and fetch signature blocks instead of reading whole files.',
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
if (method === 'initialize') {
|
|
82
|
+
const offered = params && params.protocolVersion;
|
|
50
83
|
respond(id, {
|
|
51
|
-
protocolVersion: (
|
|
84
|
+
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(offered)
|
|
85
|
+
? offered
|
|
86
|
+
: SUPPORTED_PROTOCOL_VERSIONS[0],
|
|
52
87
|
serverInfo: SERVER_INFO,
|
|
53
88
|
capabilities: { tools: {} },
|
|
54
89
|
});
|
|
@@ -56,6 +91,13 @@ function dispatch(msg, cwd) {
|
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
if (method === 'tools/list') {
|
|
94
|
+
// No pagination: the full list fits one page, so any cursor a client
|
|
95
|
+
// presents is one this server never issued — reject it (-32602, per the
|
|
96
|
+
// spec's SHOULD) rather than silently restarting from page one.
|
|
97
|
+
if (params && params.cursor !== undefined) {
|
|
98
|
+
respondError(id, -32602, `Invalid cursor: ${String(params.cursor)}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
59
101
|
respond(id, { tools: TOOLS });
|
|
60
102
|
return;
|
|
61
103
|
}
|
package/src/retrieval/ranker.js
CHANGED
|
@@ -376,9 +376,15 @@ function rank(query, sigIndex, opts) {
|
|
|
376
376
|
const hop1Files = new Set(); // normalised keys that received a hop1 boost
|
|
377
377
|
const hop1Seeds = []; // original (un-normalised) paths, for hop-2 lookup
|
|
378
378
|
|
|
379
|
-
// Hop 1: direct neighbors of scored files
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Hop 1: direct neighbors of scored files. Seeds snapshotted BEFORE the
|
|
380
|
+
// loop (as the call-graph block below already does) so boosts never
|
|
381
|
+
// cascade: a file whose only score is a hop-1 boost must not become a seed
|
|
382
|
+
// itself mid-loop, or the effective seed set — and every boost total —
|
|
383
|
+
// depends on index insertion order, which git history changes via the
|
|
384
|
+
// recent-commits hoist. That made gate scores differ between a shallow CI
|
|
385
|
+
// checkout and a developer clone of the same commit (#596).
|
|
386
|
+
const hop1SeedEntries = scored.filter((e) => e.score > 0);
|
|
387
|
+
for (const entry of hop1SeedEntries) {
|
|
382
388
|
const neighbors = _graphGet(graph.forward, path.resolve(cwd, entry.file)) || [];
|
|
383
389
|
for (const neighborAbs of neighbors) {
|
|
384
390
|
const nk = path.normalize(neighborAbs);
|
|
@@ -393,7 +399,10 @@ function rank(query, sigIndex, opts) {
|
|
|
393
399
|
}
|
|
394
400
|
}
|
|
395
401
|
|
|
396
|
-
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
|
|
402
|
+
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score).
|
|
403
|
+
// Eligibility frozen after hop-1 for the same reason: a file whose first
|
|
404
|
+
// score is a hop-2 boost must not become hop-2-eligible mid-loop (#596).
|
|
405
|
+
const hop2Eligible = scored.map((e) => e.score > 0);
|
|
397
406
|
for (const hop1Key of hop1Seeds) {
|
|
398
407
|
if (_graphGet(keyToIdx, hop1Key) === undefined) continue; // skip files not in index
|
|
399
408
|
const neighbors = _graphGet(graph.forward, hop1Key) || [];
|
|
@@ -402,7 +411,7 @@ function rank(query, sigIndex, opts) {
|
|
|
402
411
|
if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
|
|
403
412
|
if (hop1Files.has(nk)) continue; // skip already hop1-boosted
|
|
404
413
|
const idx = _graphGet(keyToIdx, nk);
|
|
405
|
-
if (idx !== undefined &&
|
|
414
|
+
if (idx !== undefined && hop2Eligible[idx]) {
|
|
406
415
|
// Only boost files that have some baseline score (not noise)
|
|
407
416
|
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
|
|
408
417
|
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
|