sigmap 8.19.0 → 8.21.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 +26 -0
- package/README.md +10 -10
- package/gen-context.js +445 -19
- package/llms-full.txt +9 -7
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +2 -0
- package/src/extractors/go.js +31 -3
- package/src/extractors/java.js +37 -2
- package/src/extractors/javascript.js +42 -1
- package/src/extractors/rust.js +33 -5
- package/src/extractors/typescript.js +41 -1
- package/src/graph/centrality.js +61 -0
- package/src/mcp/handlers.js +6 -2
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +26 -1
- package/src/session/memory-inspect.js +86 -0
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.21.0 | Benchmark: sigmap-v8.21-main (2026-07-19)
|
|
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.21-main, 2026-07-19)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
24
|
-
| Retrieval hit@5 | 42.7% (single-shot grep) |
|
|
25
|
-
| Token reduction | — | 96.
|
|
26
|
-
| Task-success proxy (modeled) | — |
|
|
27
|
-
| Prompts per task | 2.84 | 1.
|
|
24
|
+
| Retrieval hit@5 | 42.7% (single-shot grep) | 85.6% (2.00× lift) |
|
|
25
|
+
| Token reduction | — | 96.8% average |
|
|
26
|
+
| Task-success proxy (modeled) | — | 66.7% |
|
|
27
|
+
| Prompts per task | 2.84 | 1.48 (48.0% fewer) |
|
|
28
28
|
| Supported languages | — | 33 |
|
|
29
29
|
| MCP tools | — | 20 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
@@ -123,6 +123,8 @@ sigmap ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default
|
|
|
123
123
|
sigmap evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
|
|
124
124
|
sigmap evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
|
|
125
125
|
sigmap evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
|
|
126
|
+
sigmap memory List cross-session stores (.context/) — entries, size, age
|
|
127
|
+
sigmap memory --clear <store> Clear one store: session|notes|weights|evidence|all (--json supported)
|
|
126
128
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
127
129
|
sigmap note List recent notes (also: note --list <N>)
|
|
128
130
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -343,7 +345,7 @@ testCoverage = false
|
|
|
343
345
|
testDirs = ["tests","test","__tests__","spec"]
|
|
344
346
|
sigCache = false
|
|
345
347
|
impactRadius = false
|
|
346
|
-
retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false,"surfaceEnrichment":false}
|
|
348
|
+
retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false,"centralityBlend":false,"surfaceEnrichment":false}
|
|
347
349
|
impact = {"depth":3,"includeSigs":true}
|
|
348
350
|
```
|
|
349
351
|
|
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.21.0 | Benchmark: sigmap-v8.21-main (2026-07-19)
|
|
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.21-main, 2026-07-19)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval:
|
|
29
|
-
- Token reduction: 96.
|
|
30
|
-
- Task-success proxy:
|
|
31
|
-
- Prompts per task: 1.
|
|
28
|
+
- hit@5 retrieval: 85.6% vs 42.7% single-shot grep baseline (2.00× lift)
|
|
29
|
+
- Token reduction: 96.8% average across benchmark repos
|
|
30
|
+
- Task-success proxy: 66.7% (modeled from retrieval tiers, not measured LLM sessions)
|
|
31
|
+
- Prompts per task: 1.48 vs 2.84 baseline (48.0% fewer, modeled)
|
|
32
32
|
- Languages: 33 supported · MCP tools: 20
|
|
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.21.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": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
|
|
32
32
|
"benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
|
|
33
33
|
"benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
|
|
34
|
+
"benchmark:centrality-blend": "node scripts/run-centrality-blend-benchmark.mjs --save",
|
|
34
35
|
"benchmark:surface-enrichment": "node scripts/run-surface-enrichment-benchmark.mjs --save",
|
|
35
36
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
|
36
37
|
"health": "node gen-context.js --health",
|
package/src/config/defaults.js
CHANGED
|
@@ -149,6 +149,8 @@ const DEFAULTS = {
|
|
|
149
149
|
recencyBoost: 1.5,
|
|
150
150
|
// Boost files call-graph-connected to query matches (opt-in, measure-gated)
|
|
151
151
|
callGraphBoost: false,
|
|
152
|
+
// Blend import-graph centrality into ranking as a small prior (opt-in, measure-gated)
|
|
153
|
+
centralityBlend: false,
|
|
152
154
|
// Append route pseudo-signatures to the rankable index (opt-in, measure-gated)
|
|
153
155
|
surfaceEnrichment: false,
|
|
154
156
|
},
|
package/src/extractors/go.js
CHANGED
|
@@ -12,6 +12,10 @@ const { lineAt, withAnchor } = require('./line-anchor');
|
|
|
12
12
|
function extract(src) {
|
|
13
13
|
if (!src || typeof src !== 'string') return [];
|
|
14
14
|
const sigs = [];
|
|
15
|
+
const docHints = buildDocHints(src);
|
|
16
|
+
// Append the godoc hint after the anchor as ` # <hint>` — same convention
|
|
17
|
+
// as the Python/JS extractors' doc hints.
|
|
18
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
15
19
|
|
|
16
20
|
const stripped = src
|
|
17
21
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -23,14 +27,14 @@ function extract(src) {
|
|
|
23
27
|
// Structs
|
|
24
28
|
for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
|
|
25
29
|
const end = blockEndIdx(m.index + m[0].length);
|
|
26
|
-
sigs.push(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)));
|
|
30
|
+
sigs.push(hinted(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)), m[1]));
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
// Interfaces
|
|
30
34
|
for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
|
|
31
35
|
const bodyStart = m.index + m[0].length;
|
|
32
36
|
const block = extractBlock(stripped, bodyStart);
|
|
33
|
-
sigs.push(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
37
|
+
sigs.push(hinted(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[1]));
|
|
34
38
|
for (const meth of extractInterfaceMethods(block)) {
|
|
35
39
|
sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
|
|
36
40
|
}
|
|
@@ -42,7 +46,7 @@ function extract(src) {
|
|
|
42
46
|
const retType = m[4] ? m[4].trim().replace(/\s+/g, ' ') : '';
|
|
43
47
|
const retStr = retType ? ` → ${retType.slice(0, 30)}` : '';
|
|
44
48
|
const end = blockEndIdx(m.index + m[0].length);
|
|
45
|
-
sigs.push(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)));
|
|
49
|
+
sigs.push(hinted(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)), m[2]));
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
return sigs.slice(0, 25);
|
|
@@ -78,4 +82,28 @@ function normalizeParams(params) {
|
|
|
78
82
|
return params.trim().replace(/\s+/g, ' ');
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
// Godoc: the `//` comment block directly above a top-level func/type/method
|
|
86
|
+
// declaration → first prose sentence, 60-char cap. Runs on the ORIGINAL src
|
|
87
|
+
// (extract strips comments before matching). Compiler directives (`//go:...`)
|
|
88
|
+
// carry no prose and are skipped.
|
|
89
|
+
function buildDocHints(src) {
|
|
90
|
+
const hints = new Map();
|
|
91
|
+
const re = /((?:^\/\/[^\n]*\n)+)(?:func\s+(?:\(\w+\s+[\w*]+\)\s+)?(\w+)\s*\(|type\s+(\w+)\s+(?:struct|interface)\b)/gm;
|
|
92
|
+
for (const m of src.matchAll(re)) {
|
|
93
|
+
const name = m[2] || m[3];
|
|
94
|
+
const hint = firstDocSentence(m[1]);
|
|
95
|
+
if (hint && !hints.has(name)) hints.set(name, hint);
|
|
96
|
+
}
|
|
97
|
+
return hints;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// First non-directive prose line of a `//` block → first sentence, 60-char cap.
|
|
101
|
+
function firstDocSentence(block) {
|
|
102
|
+
const line = String(block).split('\n')
|
|
103
|
+
.map((l) => l.replace(/^\/\/\s?/, '').trim())
|
|
104
|
+
.find((l) => l && !l.startsWith('go:') && !l.startsWith('nolint'));
|
|
105
|
+
if (!line) return '';
|
|
106
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
module.exports = { extract };
|
package/src/extractors/java.js
CHANGED
|
@@ -12,6 +12,10 @@ const { lineAt, withAnchor } = require('./line-anchor');
|
|
|
12
12
|
function extract(src) {
|
|
13
13
|
if (!src || typeof src !== 'string') return [];
|
|
14
14
|
const sigs = [];
|
|
15
|
+
const docHints = buildDocHints(src);
|
|
16
|
+
// Append the Javadoc hint after the anchor as ` # <hint>` — same convention
|
|
17
|
+
// as the Python/JS extractors' doc hints.
|
|
18
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
15
19
|
|
|
16
20
|
const stripped = src
|
|
17
21
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -22,9 +26,9 @@ function extract(src) {
|
|
|
22
26
|
for (const m of stripped.matchAll(typeRegex)) {
|
|
23
27
|
const bodyStart = m.index + m[0].length;
|
|
24
28
|
const block = extractBlock(stripped, bodyStart);
|
|
25
|
-
sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
29
|
+
sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
|
|
26
30
|
for (const meth of extractMembers(block)) {
|
|
27
|
-
sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
|
|
31
|
+
sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -51,6 +55,7 @@ function extractMembers(block) {
|
|
|
51
55
|
const retStr = ret ? ` → ${ret}` : '';
|
|
52
56
|
members.push({
|
|
53
57
|
text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
|
|
58
|
+
name: m[2],
|
|
54
59
|
declIdx: m.index + (m[0].length - m[0].trimStart().length),
|
|
55
60
|
endIdx: m.index + m[0].length,
|
|
56
61
|
});
|
|
@@ -68,4 +73,34 @@ function normalizeType(type) {
|
|
|
68
73
|
return type.trim().replace(/\s+/g, ' ').slice(0, 30);
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
// Javadoc: the `/** ... */` block directly above a type or public/protected
|
|
77
|
+
// member declaration → first prose sentence, 60-char cap. Runs on the
|
|
78
|
+
// ORIGINAL src (extract strips comments before matching). Annotation lines
|
|
79
|
+
// (`@Override` etc.) between the doc block and the declaration are tolerated.
|
|
80
|
+
// Body may not contain `*/` so a failed adjacency check can't expand across
|
|
81
|
+
// code to the next comment block and misattribute the hint.
|
|
82
|
+
function buildDocHints(src) {
|
|
83
|
+
const hints = new Map();
|
|
84
|
+
const patterns = [
|
|
85
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum)\s+(\w+)/g,
|
|
86
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?[\w<>\[\], ?.]+\s+(\w+)\s*\(/g,
|
|
87
|
+
];
|
|
88
|
+
for (const re of patterns) {
|
|
89
|
+
for (const m of src.matchAll(re)) {
|
|
90
|
+
const hint = firstDocSentence(m[1]);
|
|
91
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return hints;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// First non-tag prose line of a Javadoc body → first sentence, 60-char cap.
|
|
98
|
+
function firstDocSentence(body) {
|
|
99
|
+
const line = String(body).split('\n')
|
|
100
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
101
|
+
.find((l) => l && !l.startsWith('@'));
|
|
102
|
+
if (!line) return '';
|
|
103
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
module.exports = { extract };
|
|
@@ -14,7 +14,12 @@ function extract(src) {
|
|
|
14
14
|
if (!src || typeof src !== 'string') return [];
|
|
15
15
|
const sigs = [];
|
|
16
16
|
const anchors = [];
|
|
17
|
+
// docHintFor[i] is the doc-comment hint for sigs[i] (top-level functions
|
|
18
|
+
// only), appended after the anchor as ` # <hint>` — same convention as the
|
|
19
|
+
// Python extractor's extractDocHint.
|
|
20
|
+
const docHintFor = [];
|
|
17
21
|
const returnHints = buildReturnHints(src);
|
|
22
|
+
const docHints = buildDocHints(src);
|
|
18
23
|
|
|
19
24
|
// Block comments are blanked newline-by-newline (non-newline chars → spaces)
|
|
20
25
|
// so character offsets AND line numbers stay exact for anchors.
|
|
@@ -49,6 +54,7 @@ function extract(src) {
|
|
|
49
54
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
50
55
|
const startLn = lineAt(stripped, m.index);
|
|
51
56
|
sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
|
|
57
|
+
docHintFor[sigs.length - 1] = docHints.get(m[1]);
|
|
52
58
|
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -58,6 +64,7 @@ function extract(src) {
|
|
|
58
64
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
59
65
|
const startLn = lineAt(stripped, m.index);
|
|
60
66
|
sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
|
|
67
|
+
docHintFor[sigs.length - 1] = docHints.get(m[1]);
|
|
61
68
|
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
62
69
|
}
|
|
63
70
|
|
|
@@ -78,10 +85,14 @@ function extract(src) {
|
|
|
78
85
|
const retStr = formatReturnHint(returnHints.get(m[1]));
|
|
79
86
|
const startLn = lineAt(stripped, m.index);
|
|
80
87
|
sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
|
|
88
|
+
docHintFor[sigs.length - 1] = docHints.get(m[1]);
|
|
81
89
|
anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
const withAnchors = sigs.map((s, i) =>
|
|
92
|
+
const withAnchors = sigs.map((s, i) => {
|
|
93
|
+
const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
|
|
94
|
+
return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
|
|
95
|
+
});
|
|
85
96
|
return capWithNotice(withAnchors, 25, 'signatures');
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -130,6 +141,36 @@ function buildReturnHints(src) {
|
|
|
130
141
|
return hints;
|
|
131
142
|
}
|
|
132
143
|
|
|
144
|
+
// First prose sentence of the JSDoc block immediately preceding a top-level
|
|
145
|
+
// function (same three shapes as buildReturnHints). Mirrors the Python
|
|
146
|
+
// extractor's extractDocHint: first sentence only, 60-char cap.
|
|
147
|
+
function buildDocHints(src) {
|
|
148
|
+
const hints = new Map();
|
|
149
|
+
// Body may not contain `*/` — otherwise a failed adjacency check would let
|
|
150
|
+
// the match expand across a whole function to the next comment block and
|
|
151
|
+
// misattribute the hint.
|
|
152
|
+
const patterns = [
|
|
153
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g,
|
|
154
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g,
|
|
155
|
+
];
|
|
156
|
+
for (const re of patterns) {
|
|
157
|
+
for (const m of src.matchAll(re)) {
|
|
158
|
+
const hint = firstDocSentence(m[1]);
|
|
159
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return hints;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
|
|
166
|
+
function firstDocSentence(body) {
|
|
167
|
+
const line = String(body).split('\n')
|
|
168
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
169
|
+
.find((l) => l && !l.startsWith('@'));
|
|
170
|
+
if (!line) return '';
|
|
171
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
172
|
+
}
|
|
173
|
+
|
|
133
174
|
function normalizeType(type) {
|
|
134
175
|
if (!type) return '';
|
|
135
176
|
return type.trim().replace(/\s+/g, ' ').slice(0, 25);
|
package/src/extractors/rust.js
CHANGED
|
@@ -12,6 +12,10 @@ const { lineAt, withAnchor } = require('./line-anchor');
|
|
|
12
12
|
function extract(src) {
|
|
13
13
|
if (!src || typeof src !== 'string') return [];
|
|
14
14
|
const sigs = [];
|
|
15
|
+
const docHints = buildDocHints(src);
|
|
16
|
+
// Append the doc-comment hint after the anchor as ` # <hint>` — same
|
|
17
|
+
// convention as the Python/JS extractors' doc hints.
|
|
18
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
15
19
|
|
|
16
20
|
const stripped = src
|
|
17
21
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -33,19 +37,19 @@ function extract(src) {
|
|
|
33
37
|
// Structs
|
|
34
38
|
for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
35
39
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
36
|
-
sigs.push(withAnchor(`pub struct ${m[1]}`, s, e));
|
|
40
|
+
sigs.push(hinted(withAnchor(`pub struct ${m[1]}`, s, e), m[1]));
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// Enums
|
|
40
44
|
for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
41
45
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
42
|
-
sigs.push(withAnchor(`pub enum ${m[1]}`, s, e));
|
|
46
|
+
sigs.push(hinted(withAnchor(`pub enum ${m[1]}`, s, e), m[1]));
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
// Traits
|
|
46
50
|
for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
47
51
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
48
|
-
sigs.push(withAnchor(`pub trait ${m[1]}`, s, e));
|
|
52
|
+
sigs.push(hinted(withAnchor(`pub trait ${m[1]}`, s, e), m[1]));
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// impl blocks
|
|
@@ -54,7 +58,7 @@ function extract(src) {
|
|
|
54
58
|
const block = extractBlock(stripped, bodyStart);
|
|
55
59
|
sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
56
60
|
for (const fn of extractMethods(block)) {
|
|
57
|
-
sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
|
|
61
|
+
sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)), fn.name));
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -63,7 +67,7 @@ function extract(src) {
|
|
|
63
67
|
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
64
68
|
const retStr = extractReturnType(m[3]);
|
|
65
69
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
66
|
-
sigs.push(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
|
|
70
|
+
sigs.push(hinted(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e), m[1]));
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
return sigs.slice(0, 25);
|
|
@@ -87,6 +91,7 @@ function extractMethods(block) {
|
|
|
87
91
|
const retStr = extractReturnType(m[3]);
|
|
88
92
|
methods.push({
|
|
89
93
|
text: `pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`,
|
|
94
|
+
name: m[1],
|
|
90
95
|
declIdx: m.index + (m[0].length - m[0].trimStart().length),
|
|
91
96
|
endIdx: m.index + m[0].length,
|
|
92
97
|
});
|
|
@@ -107,4 +112,27 @@ function extractReturnType(afterParen) {
|
|
|
107
112
|
return ` → ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
// Rustdoc: the `///` block directly above a declaration → first prose
|
|
116
|
+
// sentence, 60-char cap. Runs on the ORIGINAL src (extract strips comments
|
|
117
|
+
// before matching). Attribute lines (`#[...]`) between the doc block and the
|
|
118
|
+
// declaration are tolerated.
|
|
119
|
+
function buildDocHints(src) {
|
|
120
|
+
const hints = new Map();
|
|
121
|
+
const re = /((?:^[ \t]*\/\/\/[^\n]*\n)+)(?:[ \t]*#\[[^\n]*\n)*[ \t]*pub(?:\s+async)?\s+(?:fn|struct|enum|trait)\s+(\w+)/gm;
|
|
122
|
+
for (const m of src.matchAll(re)) {
|
|
123
|
+
const hint = firstDocSentence(m[1]);
|
|
124
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
125
|
+
}
|
|
126
|
+
return hints;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// First prose line of a `///` block → first sentence, 60-char cap.
|
|
130
|
+
function firstDocSentence(block) {
|
|
131
|
+
const line = String(block).split('\n')
|
|
132
|
+
.map((l) => l.replace(/^[ \t]*\/\/\/\s?/, '').trim())
|
|
133
|
+
.find((l) => l);
|
|
134
|
+
if (!line) return '';
|
|
135
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
module.exports = { extract };
|
|
@@ -13,6 +13,11 @@ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
|
13
13
|
function extract(src) {
|
|
14
14
|
if (!src || typeof src !== 'string') return [];
|
|
15
15
|
const sigs = [];
|
|
16
|
+
// docHintFor[i] is the doc-comment hint for sigs[i] (exported top-level
|
|
17
|
+
// functions only), appended after the anchor as ` # <hint>` — same
|
|
18
|
+
// convention as the Python extractor's extractDocHint.
|
|
19
|
+
const docHintFor = [];
|
|
20
|
+
const docHints = buildDocHints(src);
|
|
16
21
|
// anchors[i] is [start, end] for a top-level sig, or null for an indented member.
|
|
17
22
|
// Kept parallel to `sigs` so existing push/mutation logic stays untouched;
|
|
18
23
|
// anchors are applied once at return.
|
|
@@ -80,6 +85,7 @@ function extract(src) {
|
|
|
80
85
|
const retStr = retType ? ` → ${retType}` : '';
|
|
81
86
|
const bodyStart = m.index + m[0].length;
|
|
82
87
|
sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
|
|
88
|
+
docHintFor[sigs.length - 1] = docHints.get(m[1]);
|
|
83
89
|
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
84
90
|
|
|
85
91
|
// Hooks: capture compact return object shape for use* functions.
|
|
@@ -104,6 +110,7 @@ function extract(src) {
|
|
|
104
110
|
const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
|
|
105
111
|
const params = normalizeParams(m[2]);
|
|
106
112
|
sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
|
|
113
|
+
docHintFor[sigs.length - 1] = docHints.get(m[1]);
|
|
107
114
|
const bodyStart = stripped.indexOf('{', m.index + m[0].length);
|
|
108
115
|
const endLn = bodyStart !== -1
|
|
109
116
|
? lineAt(stripped, blockEndIdx(bodyStart + 1))
|
|
@@ -153,7 +160,10 @@ function extract(src) {
|
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
const withAnchors = sigs.map((s, i) =>
|
|
163
|
+
const withAnchors = sigs.map((s, i) => {
|
|
164
|
+
const anchored = anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s;
|
|
165
|
+
return docHintFor[i] ? `${anchored} # ${docHintFor[i]}` : anchored;
|
|
166
|
+
});
|
|
157
167
|
return capWithNotice(withAnchors, 35, 'signatures');
|
|
158
168
|
}
|
|
159
169
|
|
|
@@ -218,4 +228,34 @@ function normalizeParams(params) {
|
|
|
218
228
|
return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
|
|
219
229
|
}
|
|
220
230
|
|
|
231
|
+
// First prose sentence of the JSDoc block immediately preceding an exported
|
|
232
|
+
// top-level function (function or arrow-const form). Mirrors the Python
|
|
233
|
+
// extractor's extractDocHint: first sentence only, 60-char cap.
|
|
234
|
+
function buildDocHints(src) {
|
|
235
|
+
const hints = new Map();
|
|
236
|
+
// Body may not contain `*/` — otherwise a failed adjacency check would let
|
|
237
|
+
// the match expand across a whole function to the next comment block and
|
|
238
|
+
// misattribute the hint.
|
|
239
|
+
const patterns = [
|
|
240
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/g,
|
|
241
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*export\s+const\s+(\w+)\s*[:=]/g,
|
|
242
|
+
];
|
|
243
|
+
for (const re of patterns) {
|
|
244
|
+
for (const m of src.matchAll(re)) {
|
|
245
|
+
const hint = firstDocSentence(m[1]);
|
|
246
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return hints;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// First non-tag prose line of a JSDoc body → first sentence, 60-char cap.
|
|
253
|
+
function firstDocSentence(body) {
|
|
254
|
+
const line = String(body).split('\n')
|
|
255
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
256
|
+
.find((l) => l && !l.startsWith('@'));
|
|
257
|
+
if (!line) return '';
|
|
258
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
259
|
+
}
|
|
260
|
+
|
|
221
261
|
module.exports = { extract };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-dependency import-graph centrality (Semantic Bridge II, B3).
|
|
5
|
+
*
|
|
6
|
+
* Power iteration over the forward dependency graph: rank flows from each
|
|
7
|
+
* importer to the files it imports, so heavily-referenced files accumulate
|
|
8
|
+
* centrality and one-off helpers do not. Deterministic — fixed damping,
|
|
9
|
+
* fixed iteration count, nodes processed in sorted order.
|
|
10
|
+
*
|
|
11
|
+
* The result feeds the opt-in `retrieval.centralityBlend` ranking prior
|
|
12
|
+
* (see src/retrieval/ranker.js) — a principled deepening of the existing
|
|
13
|
+
* graph-boost idea, not a replacement for query relevance.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DAMPING = 0.85;
|
|
17
|
+
const ITERATIONS = 20;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compute a normalized centrality score for every file in a dependency graph.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ forward: Map<string, string[]> }} graph - forward dependency graph
|
|
23
|
+
* (file → files it imports), as built by src/graph/builder.js
|
|
24
|
+
* @returns {Map<string, number>} file → centrality in (0, 1], max-normalized;
|
|
25
|
+
* empty Map when the graph is missing or empty
|
|
26
|
+
*/
|
|
27
|
+
function computeCentrality(graph) {
|
|
28
|
+
if (!graph || !(graph.forward instanceof Map) || graph.forward.size === 0) return new Map();
|
|
29
|
+
|
|
30
|
+
const nodes = new Set(graph.forward.keys());
|
|
31
|
+
for (const deps of graph.forward.values()) {
|
|
32
|
+
for (const dep of deps || []) nodes.add(dep);
|
|
33
|
+
}
|
|
34
|
+
const nodeList = [...nodes].sort();
|
|
35
|
+
const n = nodeList.length;
|
|
36
|
+
const indexOf = new Map(nodeList.map((file, i) => [file, i]));
|
|
37
|
+
const outLinks = nodeList.map((file) =>
|
|
38
|
+
(graph.forward.get(file) || []).map((dep) => indexOf.get(dep)).filter((i) => i !== undefined));
|
|
39
|
+
|
|
40
|
+
let ranks = new Array(n).fill(1 / n);
|
|
41
|
+
for (let iter = 0; iter < ITERATIONS; iter++) {
|
|
42
|
+
const next = new Array(n).fill((1 - DAMPING) / n);
|
|
43
|
+
let dangling = 0;
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
if (outLinks[i].length === 0) { dangling += ranks[i]; continue; }
|
|
46
|
+
const share = (DAMPING * ranks[i]) / outLinks[i].length;
|
|
47
|
+
for (const j of outLinks[i]) next[j] += share;
|
|
48
|
+
}
|
|
49
|
+
// Dangling mass (files that import nothing) is redistributed uniformly.
|
|
50
|
+
const danglingShare = (DAMPING * dangling) / n;
|
|
51
|
+
for (let i = 0; i < n; i++) next[i] += danglingShare;
|
|
52
|
+
ranks = next;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const max = Math.max(...ranks) || 1;
|
|
56
|
+
const result = new Map();
|
|
57
|
+
for (let i = 0; i < n; i++) result.set(nodeList[i], ranks[i] / max);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { computeCentrality, DAMPING, ITERATIONS };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -421,8 +421,9 @@ function queryContext(args, cwd) {
|
|
|
421
421
|
// Build dependency graph for neighbor boost — non-fatal if it fails
|
|
422
422
|
let graph = null;
|
|
423
423
|
try { graph = buildFromCwd(cwd); } catch (_) {}
|
|
424
|
-
// Opt-in call-graph neighbor boost + surface enrichment — non-fatal
|
|
424
|
+
// Opt-in call-graph neighbor boost + surface enrichment + centrality blend — non-fatal
|
|
425
425
|
let callGraph = null;
|
|
426
|
+
let centrality = null;
|
|
426
427
|
try {
|
|
427
428
|
const { loadConfig } = require('../config/loader');
|
|
428
429
|
const retrieval = loadConfig(cwd).retrieval;
|
|
@@ -432,8 +433,11 @@ function queryContext(args, cwd) {
|
|
|
432
433
|
if (retrieval && retrieval.surfaceEnrichment) {
|
|
433
434
|
require('../retrieval/enrich-from-maps').enrichWithSurfaces(index, cwd);
|
|
434
435
|
}
|
|
436
|
+
if (retrieval && retrieval.centralityBlend && graph) {
|
|
437
|
+
centrality = require('../graph/centrality').computeCentrality(graph);
|
|
438
|
+
}
|
|
435
439
|
} catch (_) {}
|
|
436
|
-
const results = rank(args.query, index, { topK, cwd, graph, callGraph });
|
|
440
|
+
const results = rank(args.query, index, { topK, cwd, graph, callGraph, centrality });
|
|
437
441
|
return formatRankTable(results, args.query);
|
|
438
442
|
} catch (err) {
|
|
439
443
|
return `_query_context failed: ${err.message}_`;
|
package/src/mcp/server.js
CHANGED
package/src/retrieval/ranker.js
CHANGED
|
@@ -40,6 +40,9 @@ const GRAPH_BOOST_AMOUNTS = {
|
|
|
40
40
|
callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// Max additive prior for import-graph centrality (opt-in retrieval.centralityBlend)
|
|
44
|
+
const CENTRALITY_BLEND_WEIGHT = 0.3;
|
|
45
|
+
|
|
43
46
|
// Intent-specific weight adjustments
|
|
44
47
|
const INTENT_WEIGHTS = {
|
|
45
48
|
search: DEFAULT_WEIGHTS,
|
|
@@ -172,6 +175,8 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
|
|
|
172
175
|
* @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
|
|
173
176
|
* @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
|
|
174
177
|
* (from buildCallFileGraph) for the opt-in call-neighbor boost
|
|
178
|
+
* @param {Map<string,number>} [opts.centrality] - absolute file → normalized
|
|
179
|
+
* centrality (from computeCentrality) for the opt-in centrality blend
|
|
175
180
|
* @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
|
|
176
181
|
*/
|
|
177
182
|
function rank(query, sigIndex, opts) {
|
|
@@ -320,6 +325,26 @@ function rank(query, sigIndex, opts) {
|
|
|
320
325
|
}
|
|
321
326
|
}
|
|
322
327
|
|
|
328
|
+
// Centrality blend (opt-in via retrieval.centralityBlend): a small additive
|
|
329
|
+
// prior from import-graph centrality so heavily-referenced files rank above
|
|
330
|
+
// one-off helpers on ambiguous queries. Applied only to positively-scored
|
|
331
|
+
// files — a tie-breaker among matches, never a way to surface non-matches.
|
|
332
|
+
const centrality = (opts && opts.centrality instanceof Map && opts.centrality.size > 0) ? opts.centrality : null;
|
|
333
|
+
if (centrality && cwd) {
|
|
334
|
+
const path = require('path');
|
|
335
|
+
for (const entry of scored) {
|
|
336
|
+
if (entry.score <= 0) continue;
|
|
337
|
+
const abs = path.resolve(cwd, entry.file);
|
|
338
|
+
// The graph builder lowercases paths (normalizePath) — probe both forms.
|
|
339
|
+
const c = centrality.get(abs) || centrality.get(abs.toLowerCase());
|
|
340
|
+
if (c) {
|
|
341
|
+
const bonus = CENTRALITY_BLEND_WEIGHT * c;
|
|
342
|
+
entry.score += bonus;
|
|
343
|
+
entry.signals.centrality = bonus;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
323
348
|
// Compute confidence levels based on score distribution
|
|
324
349
|
if (scored.length > 0) {
|
|
325
350
|
const scores = scored.map(s => s.score);
|
|
@@ -595,4 +620,4 @@ function detectIntent(query) {
|
|
|
595
620
|
return 'search';
|
|
596
621
|
}
|
|
597
622
|
|
|
598
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
|
|
623
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|