sigmap 8.20.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 +13 -0
- package/README.md +5 -5
- package/gen-context.js +220 -17
- package/llms-full.txt +5 -5
- package/llms.txt +4 -4
- 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/rust.js +33 -5
- 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/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,19 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.21.0] — 2026-07-19
|
|
14
|
+
|
|
15
|
+
Minor release — **"Semantic Bridge II" (v8.21)**: doc-comment hints reach Go, Rust, and Java, and the import graph gains a principled centrality prior for ranking — flag-gated and measured.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Go/Rust/Java doc-comment hints (#501, PR #502):** `buildDocHints` in the Go extractor (godoc `//` blocks above top-level `func`/`type`, compiler directives `//go:`/`nolint` skipped), the Rust extractor (`///` blocks above `pub fn`/`struct`/`enum`/`trait` and impl methods, `#[attr]` lines between doc and declaration tolerated), and the Java extractor (Javadoc on type declarations **and** public/protected members; tag-only blocks produce no hint). First prose sentence, 60-char cap, appended after the line anchor as ` # <hint>` — byte-format identical to the Python/JS/TS hints. Hints are mined from the original source since `extract()` strips comments before matching; undocumented signatures are byte-identical to before.
|
|
19
|
+
- **Centrality rank blend (#501, PR #502, opt-in `retrieval.centralityBlend`):** new `src/graph/centrality.js` — zero-dependency power iteration over the forward import graph (damping 0.85, 20 iterations, sorted nodes, dangling mass redistributed; deterministic), max-normalized to (0,1]. `rank()` blends `0.3 × centrality` as a small additive prior onto **positively-scored files only** (`signals.centrality`) — a tie-breaker among matches, never a way to surface non-matches. Wired like `callGraphBoost`: MCP `query_context` + CLI `ask`/`--query`, all non-fatal. New A/B measure gate `scripts/run-centrality-blend-benchmark.mjs` (`npm run benchmark:centrality-blend`).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Measured and gated off by default:** the centrality A/B over 90 tasks / 18 repos scored both arms at 77.8% hit@5 (+0 tasks) — non-regressing but neutral on the lexical-favoring corpus, so `retrieval.centralityBlend` ships **off** per the measure gate; the v8.22 hard-split corpus (A3) is the next chance to show a real delta. 11 new integration tests (127 test files).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
13
26
|
## [8.20.0] — 2026-07-19
|
|
14
27
|
|
|
15
28
|
Minor release — **"Semantic Bridge I" (v8.20)**: the JS/TS extractors gain the same doc-comment hints Python has carried for releases, and the cross-session stores get a single inspect/prune surface.
|
package/README.md
CHANGED
|
@@ -61,8 +61,8 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
|
|
|
61
61
|
<!--SM:whyMetrics-->
|
|
62
62
|
- **85.6% hit@5** — right file in top 5 results (vs 42.7% single-shot grep baseline — 2.00× lift)
|
|
63
63
|
- **96.8% token reduction** — average across 21 real repos
|
|
64
|
-
- **
|
|
65
|
-
- **1.
|
|
64
|
+
- **66.7% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
|
|
65
|
+
- **1.48 prompts per task** — down from 2.84 (48.0% fewer retries, modeled)
|
|
66
66
|
<!--/SM:whyMetrics-->
|
|
67
67
|
- **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
|
|
68
68
|
- **No vendor lock-in** — works with any AI assistant or local LLM
|
|
@@ -122,13 +122,13 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
125
|
+
Benchmark : sigmap-v8.21-main (21 repositories, including R language)
|
|
126
126
|
Date : 2026-07-19
|
|
127
127
|
|
|
128
128
|
Hit@5 : 85.6% (grep-agent baseline 42.7% — 2.00× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
|
130
|
-
Prompt reduction : 48.
|
|
131
|
-
Task success :
|
|
130
|
+
Prompt reduction : 48.0% (2.84 → 1.48 prompts per task, modeled)
|
|
131
|
+
Task success : 66.7% (proxy — modeled from retrieval tiers)
|
|
132
132
|
Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
|
|
133
133
|
```
|
|
134
134
|
<!--/SM:benchmarkBlock-->
|
package/gen-context.js
CHANGED
|
@@ -1533,6 +1533,8 @@ __factories["./src/config/defaults"] = function(module, exports) {
|
|
|
1533
1533
|
recencyBoost: 1.5,
|
|
1534
1534
|
// Boost files call-graph-connected to query matches (opt-in, measure-gated)
|
|
1535
1535
|
callGraphBoost: false,
|
|
1536
|
+
// Blend import-graph centrality into ranking as a small prior (opt-in, measure-gated)
|
|
1537
|
+
centralityBlend: false,
|
|
1536
1538
|
// Append route pseudo-signatures to the rankable index (opt-in, measure-gated)
|
|
1537
1539
|
surfaceEnrichment: false,
|
|
1538
1540
|
},
|
|
@@ -5928,6 +5930,10 @@ __factories["./src/extractors/go"] = function(module, exports) {
|
|
|
5928
5930
|
function extract(src) {
|
|
5929
5931
|
if (!src || typeof src !== 'string') return [];
|
|
5930
5932
|
const sigs = [];
|
|
5933
|
+
const docHints = buildDocHints(src);
|
|
5934
|
+
// Append the godoc hint after the anchor as ` # <hint>` — same convention
|
|
5935
|
+
// as the Python/JS extractors' doc hints.
|
|
5936
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
5931
5937
|
|
|
5932
5938
|
const stripped = src
|
|
5933
5939
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -5939,14 +5945,14 @@ __factories["./src/extractors/go"] = function(module, exports) {
|
|
|
5939
5945
|
// Structs
|
|
5940
5946
|
for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
|
|
5941
5947
|
const end = blockEndIdx(m.index + m[0].length);
|
|
5942
|
-
sigs.push(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)));
|
|
5948
|
+
sigs.push(hinted(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)), m[1]));
|
|
5943
5949
|
}
|
|
5944
5950
|
|
|
5945
5951
|
// Interfaces
|
|
5946
5952
|
for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
|
|
5947
5953
|
const bodyStart = m.index + m[0].length;
|
|
5948
5954
|
const block = extractBlock(stripped, bodyStart);
|
|
5949
|
-
sigs.push(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
5955
|
+
sigs.push(hinted(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[1]));
|
|
5950
5956
|
for (const meth of extractInterfaceMethods(block)) {
|
|
5951
5957
|
sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
|
|
5952
5958
|
}
|
|
@@ -5958,7 +5964,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
|
|
|
5958
5964
|
const retType = m[4] ? m[4].trim().replace(/\s+/g, ' ') : '';
|
|
5959
5965
|
const retStr = retType ? ` → ${retType.slice(0, 30)}` : '';
|
|
5960
5966
|
const end = blockEndIdx(m.index + m[0].length);
|
|
5961
|
-
sigs.push(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)));
|
|
5967
|
+
sigs.push(hinted(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)), m[2]));
|
|
5962
5968
|
}
|
|
5963
5969
|
|
|
5964
5970
|
return sigs.slice(0, 25);
|
|
@@ -5994,6 +6000,30 @@ __factories["./src/extractors/go"] = function(module, exports) {
|
|
|
5994
6000
|
return params.trim().replace(/\s+/g, ' ');
|
|
5995
6001
|
}
|
|
5996
6002
|
|
|
6003
|
+
// Godoc: the `//` comment block directly above a top-level func/type/method
|
|
6004
|
+
// declaration → first prose sentence, 60-char cap. Runs on the ORIGINAL src
|
|
6005
|
+
// (extract strips comments before matching). Compiler directives (`//go:...`)
|
|
6006
|
+
// carry no prose and are skipped.
|
|
6007
|
+
function buildDocHints(src) {
|
|
6008
|
+
const hints = new Map();
|
|
6009
|
+
const re = /((?:^\/\/[^\n]*\n)+)(?:func\s+(?:\(\w+\s+[\w*]+\)\s+)?(\w+)\s*\(|type\s+(\w+)\s+(?:struct|interface)\b)/gm;
|
|
6010
|
+
for (const m of src.matchAll(re)) {
|
|
6011
|
+
const name = m[2] || m[3];
|
|
6012
|
+
const hint = firstDocSentence(m[1]);
|
|
6013
|
+
if (hint && !hints.has(name)) hints.set(name, hint);
|
|
6014
|
+
}
|
|
6015
|
+
return hints;
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
// First non-directive prose line of a `//` block → first sentence, 60-char cap.
|
|
6019
|
+
function firstDocSentence(block) {
|
|
6020
|
+
const line = String(block).split('\n')
|
|
6021
|
+
.map((l) => l.replace(/^\/\/\s?/, '').trim())
|
|
6022
|
+
.find((l) => l && !l.startsWith('go:') && !l.startsWith('nolint'));
|
|
6023
|
+
if (!line) return '';
|
|
6024
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
6025
|
+
}
|
|
6026
|
+
|
|
5997
6027
|
module.exports = { extract };
|
|
5998
6028
|
|
|
5999
6029
|
};
|
|
@@ -6126,6 +6156,10 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6126
6156
|
function extract(src) {
|
|
6127
6157
|
if (!src || typeof src !== 'string') return [];
|
|
6128
6158
|
const sigs = [];
|
|
6159
|
+
const docHints = buildDocHints(src);
|
|
6160
|
+
// Append the Javadoc hint after the anchor as ` # <hint>` — same convention
|
|
6161
|
+
// as the Python/JS extractors' doc hints.
|
|
6162
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
6129
6163
|
|
|
6130
6164
|
const stripped = src
|
|
6131
6165
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -6136,9 +6170,9 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6136
6170
|
for (const m of stripped.matchAll(typeRegex)) {
|
|
6137
6171
|
const bodyStart = m.index + m[0].length;
|
|
6138
6172
|
const block = extractBlock(stripped, bodyStart);
|
|
6139
|
-
sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
6173
|
+
sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
|
|
6140
6174
|
for (const meth of extractMembers(block)) {
|
|
6141
|
-
sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
|
|
6175
|
+
sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
|
|
6142
6176
|
}
|
|
6143
6177
|
}
|
|
6144
6178
|
|
|
@@ -6165,6 +6199,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6165
6199
|
const retStr = ret ? ` → ${ret}` : '';
|
|
6166
6200
|
members.push({
|
|
6167
6201
|
text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
|
|
6202
|
+
name: m[2],
|
|
6168
6203
|
declIdx: m.index + (m[0].length - m[0].trimStart().length),
|
|
6169
6204
|
endIdx: m.index + m[0].length,
|
|
6170
6205
|
});
|
|
@@ -6182,6 +6217,36 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6182
6217
|
return type.trim().replace(/\s+/g, ' ').slice(0, 30);
|
|
6183
6218
|
}
|
|
6184
6219
|
|
|
6220
|
+
// Javadoc: the `/** ... */` block directly above a type or public/protected
|
|
6221
|
+
// member declaration → first prose sentence, 60-char cap. Runs on the
|
|
6222
|
+
// ORIGINAL src (extract strips comments before matching). Annotation lines
|
|
6223
|
+
// (`@Override` etc.) between the doc block and the declaration are tolerated.
|
|
6224
|
+
// Body may not contain `*/` so a failed adjacency check can't expand across
|
|
6225
|
+
// code to the next comment block and misattribute the hint.
|
|
6226
|
+
function buildDocHints(src) {
|
|
6227
|
+
const hints = new Map();
|
|
6228
|
+
const patterns = [
|
|
6229
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(?:class|interface|enum)\s+(\w+)/g,
|
|
6230
|
+
/\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?[\w<>\[\], ?.]+\s+(\w+)\s*\(/g,
|
|
6231
|
+
];
|
|
6232
|
+
for (const re of patterns) {
|
|
6233
|
+
for (const m of src.matchAll(re)) {
|
|
6234
|
+
const hint = firstDocSentence(m[1]);
|
|
6235
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
return hints;
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
// First non-tag prose line of a Javadoc body → first sentence, 60-char cap.
|
|
6242
|
+
function firstDocSentence(body) {
|
|
6243
|
+
const line = String(body).split('\n')
|
|
6244
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
6245
|
+
.find((l) => l && !l.startsWith('@'));
|
|
6246
|
+
if (!line) return '';
|
|
6247
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6185
6250
|
module.exports = { extract };
|
|
6186
6251
|
|
|
6187
6252
|
};
|
|
@@ -7680,6 +7745,10 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7680
7745
|
function extract(src) {
|
|
7681
7746
|
if (!src || typeof src !== 'string') return [];
|
|
7682
7747
|
const sigs = [];
|
|
7748
|
+
const docHints = buildDocHints(src);
|
|
7749
|
+
// Append the doc-comment hint after the anchor as ` # <hint>` — same
|
|
7750
|
+
// convention as the Python/JS extractors' doc hints.
|
|
7751
|
+
const hinted = (sig, name) => (docHints.has(name) ? `${sig} # ${docHints.get(name)}` : sig);
|
|
7683
7752
|
|
|
7684
7753
|
const stripped = src
|
|
7685
7754
|
.replace(/\/\/.*$/gm, '')
|
|
@@ -7701,19 +7770,19 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7701
7770
|
// Structs
|
|
7702
7771
|
for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
7703
7772
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
7704
|
-
sigs.push(withAnchor(`pub struct ${m[1]}`, s, e));
|
|
7773
|
+
sigs.push(hinted(withAnchor(`pub struct ${m[1]}`, s, e), m[1]));
|
|
7705
7774
|
}
|
|
7706
7775
|
|
|
7707
7776
|
// Enums
|
|
7708
7777
|
for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
7709
7778
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
7710
|
-
sigs.push(withAnchor(`pub enum ${m[1]}`, s, e));
|
|
7779
|
+
sigs.push(hinted(withAnchor(`pub enum ${m[1]}`, s, e), m[1]));
|
|
7711
7780
|
}
|
|
7712
7781
|
|
|
7713
7782
|
// Traits
|
|
7714
7783
|
for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
7715
7784
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
7716
|
-
sigs.push(withAnchor(`pub trait ${m[1]}`, s, e));
|
|
7785
|
+
sigs.push(hinted(withAnchor(`pub trait ${m[1]}`, s, e), m[1]));
|
|
7717
7786
|
}
|
|
7718
7787
|
|
|
7719
7788
|
// impl blocks
|
|
@@ -7722,7 +7791,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7722
7791
|
const block = extractBlock(stripped, bodyStart);
|
|
7723
7792
|
sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
|
|
7724
7793
|
for (const fn of extractMethods(block)) {
|
|
7725
|
-
sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
|
|
7794
|
+
sigs.push(hinted(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)), fn.name));
|
|
7726
7795
|
}
|
|
7727
7796
|
}
|
|
7728
7797
|
|
|
@@ -7731,7 +7800,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7731
7800
|
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
7732
7801
|
const retStr = extractReturnType(m[3]);
|
|
7733
7802
|
const [s, e] = rangeFor(m.index, m.index + m[0].length);
|
|
7734
|
-
sigs.push(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
|
|
7803
|
+
sigs.push(hinted(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e), m[1]));
|
|
7735
7804
|
}
|
|
7736
7805
|
|
|
7737
7806
|
return sigs.slice(0, 25);
|
|
@@ -7755,6 +7824,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7755
7824
|
const retStr = extractReturnType(m[3]);
|
|
7756
7825
|
methods.push({
|
|
7757
7826
|
text: `pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`,
|
|
7827
|
+
name: m[1],
|
|
7758
7828
|
declIdx: m.index + (m[0].length - m[0].trimStart().length),
|
|
7759
7829
|
endIdx: m.index + m[0].length,
|
|
7760
7830
|
});
|
|
@@ -7775,6 +7845,29 @@ __factories["./src/extractors/rust"] = function(module, exports) {
|
|
|
7775
7845
|
return ` → ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
|
|
7776
7846
|
}
|
|
7777
7847
|
|
|
7848
|
+
// Rustdoc: the `///` block directly above a declaration → first prose
|
|
7849
|
+
// sentence, 60-char cap. Runs on the ORIGINAL src (extract strips comments
|
|
7850
|
+
// before matching). Attribute lines (`#[...]`) between the doc block and the
|
|
7851
|
+
// declaration are tolerated.
|
|
7852
|
+
function buildDocHints(src) {
|
|
7853
|
+
const hints = new Map();
|
|
7854
|
+
const re = /((?:^[ \t]*\/\/\/[^\n]*\n)+)(?:[ \t]*#\[[^\n]*\n)*[ \t]*pub(?:\s+async)?\s+(?:fn|struct|enum|trait)\s+(\w+)/gm;
|
|
7855
|
+
for (const m of src.matchAll(re)) {
|
|
7856
|
+
const hint = firstDocSentence(m[1]);
|
|
7857
|
+
if (hint && !hints.has(m[2])) hints.set(m[2], hint);
|
|
7858
|
+
}
|
|
7859
|
+
return hints;
|
|
7860
|
+
}
|
|
7861
|
+
|
|
7862
|
+
// First prose line of a `///` block → first sentence, 60-char cap.
|
|
7863
|
+
function firstDocSentence(block) {
|
|
7864
|
+
const line = String(block).split('\n')
|
|
7865
|
+
.map((l) => l.replace(/^[ \t]*\/\/\/\s?/, '').trim())
|
|
7866
|
+
.find((l) => l);
|
|
7867
|
+
if (!line) return '';
|
|
7868
|
+
return line.split(/[.!?]/)[0].trim().slice(0, 60);
|
|
7869
|
+
}
|
|
7870
|
+
|
|
7778
7871
|
module.exports = { extract };
|
|
7779
7872
|
|
|
7780
7873
|
};
|
|
@@ -11806,6 +11899,71 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11806
11899
|
|
|
11807
11900
|
};
|
|
11808
11901
|
|
|
11902
|
+
// ── ./src/graph/centrality ──
|
|
11903
|
+
__factories["./src/graph/centrality"] = function(module, exports) {
|
|
11904
|
+
|
|
11905
|
+
/**
|
|
11906
|
+
* Zero-dependency import-graph centrality (Semantic Bridge II, B3).
|
|
11907
|
+
*
|
|
11908
|
+
* Power iteration over the forward dependency graph: rank flows from each
|
|
11909
|
+
* importer to the files it imports, so heavily-referenced files accumulate
|
|
11910
|
+
* centrality and one-off helpers do not. Deterministic — fixed damping,
|
|
11911
|
+
* fixed iteration count, nodes processed in sorted order.
|
|
11912
|
+
*
|
|
11913
|
+
* The result feeds the opt-in `retrieval.centralityBlend` ranking prior
|
|
11914
|
+
* (see src/retrieval/ranker.js) — a principled deepening of the existing
|
|
11915
|
+
* graph-boost idea, not a replacement for query relevance.
|
|
11916
|
+
*/
|
|
11917
|
+
|
|
11918
|
+
const DAMPING = 0.85;
|
|
11919
|
+
const ITERATIONS = 20;
|
|
11920
|
+
|
|
11921
|
+
/**
|
|
11922
|
+
* Compute a normalized centrality score for every file in a dependency graph.
|
|
11923
|
+
*
|
|
11924
|
+
* @param {{ forward: Map<string, string[]> }} graph - forward dependency graph
|
|
11925
|
+
* (file → files it imports), as built by src/graph/builder.js
|
|
11926
|
+
* @returns {Map<string, number>} file → centrality in (0, 1], max-normalized;
|
|
11927
|
+
* empty Map when the graph is missing or empty
|
|
11928
|
+
*/
|
|
11929
|
+
function computeCentrality(graph) {
|
|
11930
|
+
if (!graph || !(graph.forward instanceof Map) || graph.forward.size === 0) return new Map();
|
|
11931
|
+
|
|
11932
|
+
const nodes = new Set(graph.forward.keys());
|
|
11933
|
+
for (const deps of graph.forward.values()) {
|
|
11934
|
+
for (const dep of deps || []) nodes.add(dep);
|
|
11935
|
+
}
|
|
11936
|
+
const nodeList = [...nodes].sort();
|
|
11937
|
+
const n = nodeList.length;
|
|
11938
|
+
const indexOf = new Map(nodeList.map((file, i) => [file, i]));
|
|
11939
|
+
const outLinks = nodeList.map((file) =>
|
|
11940
|
+
(graph.forward.get(file) || []).map((dep) => indexOf.get(dep)).filter((i) => i !== undefined));
|
|
11941
|
+
|
|
11942
|
+
let ranks = new Array(n).fill(1 / n);
|
|
11943
|
+
for (let iter = 0; iter < ITERATIONS; iter++) {
|
|
11944
|
+
const next = new Array(n).fill((1 - DAMPING) / n);
|
|
11945
|
+
let dangling = 0;
|
|
11946
|
+
for (let i = 0; i < n; i++) {
|
|
11947
|
+
if (outLinks[i].length === 0) { dangling += ranks[i]; continue; }
|
|
11948
|
+
const share = (DAMPING * ranks[i]) / outLinks[i].length;
|
|
11949
|
+
for (const j of outLinks[i]) next[j] += share;
|
|
11950
|
+
}
|
|
11951
|
+
// Dangling mass (files that import nothing) is redistributed uniformly.
|
|
11952
|
+
const danglingShare = (DAMPING * dangling) / n;
|
|
11953
|
+
for (let i = 0; i < n; i++) next[i] += danglingShare;
|
|
11954
|
+
ranks = next;
|
|
11955
|
+
}
|
|
11956
|
+
|
|
11957
|
+
const max = Math.max(...ranks) || 1;
|
|
11958
|
+
const result = new Map();
|
|
11959
|
+
for (let i = 0; i < n; i++) result.set(nodeList[i], ranks[i] / max);
|
|
11960
|
+
return result;
|
|
11961
|
+
}
|
|
11962
|
+
|
|
11963
|
+
module.exports = { computeCentrality, DAMPING, ITERATIONS };
|
|
11964
|
+
|
|
11965
|
+
};
|
|
11966
|
+
|
|
11809
11967
|
// ── ./src/graph/impact ──
|
|
11810
11968
|
__factories["./src/graph/impact"] = function(module, exports) {
|
|
11811
11969
|
|
|
@@ -13963,8 +14121,9 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13963
14121
|
// Build dependency graph for neighbor boost — non-fatal if it fails
|
|
13964
14122
|
let graph = null;
|
|
13965
14123
|
try { graph = buildFromCwd(cwd); } catch (_) {}
|
|
13966
|
-
// Opt-in call-graph neighbor boost + surface enrichment — non-fatal
|
|
14124
|
+
// Opt-in call-graph neighbor boost + surface enrichment + centrality blend — non-fatal
|
|
13967
14125
|
let callGraph = null;
|
|
14126
|
+
let centrality = null;
|
|
13968
14127
|
try {
|
|
13969
14128
|
const { loadConfig } = __require('./src/config/loader');
|
|
13970
14129
|
const retrieval = loadConfig(cwd).retrieval;
|
|
@@ -13974,8 +14133,11 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13974
14133
|
if (retrieval && retrieval.surfaceEnrichment) {
|
|
13975
14134
|
__require('./src/retrieval/enrich-from-maps').enrichWithSurfaces(index, cwd);
|
|
13976
14135
|
}
|
|
14136
|
+
if (retrieval && retrieval.centralityBlend && graph) {
|
|
14137
|
+
centrality = __require('./src/graph/centrality').computeCentrality(graph);
|
|
14138
|
+
}
|
|
13977
14139
|
} catch (_) {}
|
|
13978
|
-
const results = rank(args.query, index, { topK, cwd, graph, callGraph });
|
|
14140
|
+
const results = rank(args.query, index, { topK, cwd, graph, callGraph, centrality });
|
|
13979
14141
|
return formatRankTable(results, args.query);
|
|
13980
14142
|
} catch (err) {
|
|
13981
14143
|
return `_query_context failed: ${err.message}_`;
|
|
@@ -14688,7 +14850,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14688
14850
|
|
|
14689
14851
|
const SERVER_INFO = {
|
|
14690
14852
|
name: 'sigmap',
|
|
14691
|
-
version: '8.
|
|
14853
|
+
version: '8.21.0',
|
|
14692
14854
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14693
14855
|
};
|
|
14694
14856
|
|
|
@@ -15818,6 +15980,9 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
15818
15980
|
callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
|
|
15819
15981
|
};
|
|
15820
15982
|
|
|
15983
|
+
// Max additive prior for import-graph centrality (opt-in retrieval.centralityBlend)
|
|
15984
|
+
const CENTRALITY_BLEND_WEIGHT = 0.3;
|
|
15985
|
+
|
|
15821
15986
|
// Intent-specific weight adjustments
|
|
15822
15987
|
const INTENT_WEIGHTS = {
|
|
15823
15988
|
search: DEFAULT_WEIGHTS,
|
|
@@ -15950,6 +16115,8 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
15950
16115
|
* @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
|
|
15951
16116
|
* @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
|
|
15952
16117
|
* (from buildCallFileGraph) for the opt-in call-neighbor boost
|
|
16118
|
+
* @param {Map<string,number>} [opts.centrality] - absolute file → normalized
|
|
16119
|
+
* centrality (from computeCentrality) for the opt-in centrality blend
|
|
15953
16120
|
* @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
|
|
15954
16121
|
*/
|
|
15955
16122
|
function rank(query, sigIndex, opts) {
|
|
@@ -16098,6 +16265,26 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16098
16265
|
}
|
|
16099
16266
|
}
|
|
16100
16267
|
|
|
16268
|
+
// Centrality blend (opt-in via retrieval.centralityBlend): a small additive
|
|
16269
|
+
// prior from import-graph centrality so heavily-referenced files rank above
|
|
16270
|
+
// one-off helpers on ambiguous queries. Applied only to positively-scored
|
|
16271
|
+
// files — a tie-breaker among matches, never a way to surface non-matches.
|
|
16272
|
+
const centrality = (opts && opts.centrality instanceof Map && opts.centrality.size > 0) ? opts.centrality : null;
|
|
16273
|
+
if (centrality && cwd) {
|
|
16274
|
+
const path = require('path');
|
|
16275
|
+
for (const entry of scored) {
|
|
16276
|
+
if (entry.score <= 0) continue;
|
|
16277
|
+
const abs = path.resolve(cwd, entry.file);
|
|
16278
|
+
// The graph builder lowercases paths (normalizePath) — probe both forms.
|
|
16279
|
+
const c = centrality.get(abs) || centrality.get(abs.toLowerCase());
|
|
16280
|
+
if (c) {
|
|
16281
|
+
const bonus = CENTRALITY_BLEND_WEIGHT * c;
|
|
16282
|
+
entry.score += bonus;
|
|
16283
|
+
entry.signals.centrality = bonus;
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
}
|
|
16287
|
+
|
|
16101
16288
|
// Compute confidence levels based on score distribution
|
|
16102
16289
|
if (scored.length > 0) {
|
|
16103
16290
|
const scores = scored.map(s => s.score);
|
|
@@ -16373,7 +16560,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16373
16560
|
return 'search';
|
|
16374
16561
|
}
|
|
16375
16562
|
|
|
16376
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
|
|
16563
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
16377
16564
|
|
|
16378
16565
|
};
|
|
16379
16566
|
|
|
@@ -19881,7 +20068,7 @@ function __tryGit(args, opts = {}) {
|
|
|
19881
20068
|
catch (_) { return ''; }
|
|
19882
20069
|
}
|
|
19883
20070
|
|
|
19884
|
-
const VERSION = '8.
|
|
20071
|
+
const VERSION = '8.21.0';
|
|
19885
20072
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
19886
20073
|
|
|
19887
20074
|
function requireSourceOrBundled(key) {
|
|
@@ -22215,8 +22402,16 @@ function main() {
|
|
|
22215
22402
|
if (config && config.retrieval && config.retrieval.surfaceEnrichment) {
|
|
22216
22403
|
try { requireSourceOrBundled('./src/retrieval/enrich-from-maps').enrichWithSurfaces(sigIndex, cwd); } catch (_) {}
|
|
22217
22404
|
}
|
|
22405
|
+
// Opt-in import-graph centrality blend (retrieval.centralityBlend) — non-fatal
|
|
22406
|
+
let askCentrality = null;
|
|
22407
|
+
if (config && config.retrieval && config.retrieval.centralityBlend) {
|
|
22408
|
+
try {
|
|
22409
|
+
const askCentralityGraph = requireSourceOrBundled('./src/graph/builder').buildFromCwd(cwd);
|
|
22410
|
+
askCentrality = requireSourceOrBundled('./src/graph/centrality').computeCentrality(askCentralityGraph);
|
|
22411
|
+
} catch (_) {}
|
|
22412
|
+
}
|
|
22218
22413
|
|
|
22219
|
-
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph });
|
|
22414
|
+
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph, centrality: askCentrality });
|
|
22220
22415
|
|
|
22221
22416
|
// v6.10: Workspace scoping — infer package from query and apply boost
|
|
22222
22417
|
const workspaces = detectWorkspaces(cwd);
|
|
@@ -24447,7 +24642,15 @@ function main() {
|
|
|
24447
24642
|
if (config && config.retrieval && config.retrieval.surfaceEnrichment) {
|
|
24448
24643
|
try { requireSourceOrBundled('./src/retrieval/enrich-from-maps').enrichWithSurfaces(index, cwd); } catch (_) {}
|
|
24449
24644
|
}
|
|
24450
|
-
|
|
24645
|
+
// Opt-in import-graph centrality blend (retrieval.centralityBlend) — non-fatal
|
|
24646
|
+
let queryCentrality = null;
|
|
24647
|
+
if (config && config.retrieval && config.retrieval.centralityBlend) {
|
|
24648
|
+
try {
|
|
24649
|
+
const centralityGraph = requireSourceOrBundled('./src/graph/builder').buildFromCwd(cwd);
|
|
24650
|
+
queryCentrality = requireSourceOrBundled('./src/graph/centrality').computeCentrality(centralityGraph);
|
|
24651
|
+
} catch (_) {}
|
|
24652
|
+
}
|
|
24653
|
+
const results = rank(query, index, { topK, recencyBoost, cwd, callGraph: queryCallGraph, centrality: queryCentrality });
|
|
24451
24654
|
if (args.includes('--context')) {
|
|
24452
24655
|
const miniCtx = buildMiniContext(results, cwd);
|
|
24453
24656
|
const ctxOut = path.join(cwd, '.context', 'query-context.md');
|
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
24
|
| Retrieval hit@5 | 42.7% (single-shot grep) | 85.6% (2.00× lift) |
|
|
25
25
|
| Token reduction | — | 96.8% average |
|
|
26
|
-
| Task-success proxy (modeled) | — |
|
|
27
|
-
| Prompts per task | 2.84 | 1.
|
|
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 |
|
|
@@ -345,7 +345,7 @@ testCoverage = false
|
|
|
345
345
|
testDirs = ["tests","test","__tests__","spec"]
|
|
346
346
|
sigCache = false
|
|
347
347
|
impactRadius = false
|
|
348
|
-
retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false,"surfaceEnrichment":false}
|
|
348
|
+
retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false,"centralityBlend":false,"surfaceEnrichment":false}
|
|
349
349
|
impact = {"depth":3,"includeSigs":true}
|
|
350
350
|
```
|
|
351
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
28
|
- hit@5 retrieval: 85.6% vs 42.7% single-shot grep baseline (2.00× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
|
30
|
-
- Task-success proxy:
|
|
31
|
-
- Prompts per task: 1.
|
|
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 };
|
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 };
|
|
@@ -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 };
|