sigmap 8.29.0 → 8.30.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 +10 -0
- package/README.md +2 -2
- package/gen-context.js +111 -13
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/java.js +20 -4
- package/src/mcp/install.js +29 -2
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +35 -4
- package/src/skills/skills.js +25 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.30.0] — 2026-09-07
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `sigmap-task` — an invokable prompt skill that drives the full grounding loop from the CLI, for environments where MCP is unavailable. Installs to `.github/prompts/sigmap-task.prompt.md` for Copilot (`/sigmap-task`) and through the normal skill path for other clients (#554)
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Java extractor: lifted three hard-coded caps that hid 85% of the API surface on real Java repos — an 8-member-per-class limit applied silently, a 25-signature-per-file cap that shadowed `maxSigsPerFile`, and a 5,000-character class-body scan limit. Omissions are now disclosed with a `… +N more` marker, matching the JS/TS path (#552)
|
|
20
|
+
- Retrieval: generated data holders no longer outrank real logic. A file whose members are overwhelmingly trivial accessors is demoted like other generated code, with an escape hatch when the query asks for an entity, model, DTO or accessor. Entities remain retrievable by their own symbols (#552)
|
|
21
|
+
- `sigmap mcp install vscode` wrote a config VS Code ignores — it emitted a top-level `mcpServers` key where VS Code requires `servers` with an explicit `type`. A config written by an earlier version is now migrated rather than left in place (#557)
|
|
22
|
+
|
|
13
23
|
## [8.29.0] — 2026-09-01
|
|
14
24
|
|
|
15
25
|
Minor release — **"Retrieval Index Split" (v8.29)**: the ranker stops reading the token-budgeted prompt artifact, and the ranking features that were silently inert start executing. Plus the first benchmark corpus this project did not author itself.
|
package/README.md
CHANGED
|
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-09-
|
|
125
|
+
Benchmark : sigmap-v8.30-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-09-07
|
|
127
127
|
|
|
128
128
|
Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -6424,6 +6424,19 @@ __factories["./src/extractors/html"] = function(module, exports) {
|
|
|
6424
6424
|
__factories["./src/extractors/java"] = function(module, exports) {
|
|
6425
6425
|
|
|
6426
6426
|
const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
|
|
6427
|
+
const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
|
|
6428
|
+
|
|
6429
|
+
// Class bodies are scanned to this many characters. Generated JVM sources
|
|
6430
|
+
// (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
|
|
6431
|
+
// against pathological input rather than trimming ordinary classes.
|
|
6432
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
6433
|
+
|
|
6434
|
+
// Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
|
|
6435
|
+
// caller's configured budget governs the output rather than this file.
|
|
6436
|
+
const MAX_MEMBERS_PER_CLASS = 120;
|
|
6437
|
+
|
|
6438
|
+
// Per-file signature ceiling, likewise above the configured default.
|
|
6439
|
+
const MAX_SIGS_PER_FILE = 200;
|
|
6427
6440
|
|
|
6428
6441
|
/**
|
|
6429
6442
|
* Extract signatures from Java source code.
|
|
@@ -6451,17 +6464,20 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6451
6464
|
const block = extractBlock(stripped, bodyStart);
|
|
6452
6465
|
sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
|
|
6453
6466
|
for (const meth of extractMembers(block)) {
|
|
6454
|
-
|
|
6467
|
+
// The disclosure marker carries no offsets; anchor it at the class body.
|
|
6468
|
+
const declIdx = meth.declIdx || 0;
|
|
6469
|
+
const endIdx = meth.endIdx || 0;
|
|
6470
|
+
sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
|
|
6455
6471
|
}
|
|
6456
6472
|
}
|
|
6457
6473
|
|
|
6458
|
-
return sigs
|
|
6474
|
+
return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
|
|
6459
6475
|
}
|
|
6460
6476
|
|
|
6461
6477
|
function extractBlock(src, startIndex) {
|
|
6462
6478
|
let depth = 1;
|
|
6463
6479
|
let i = startIndex;
|
|
6464
|
-
const end = Math.min(src.length, startIndex +
|
|
6480
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
6465
6481
|
while (i < end && depth > 0) {
|
|
6466
6482
|
if (src[i] === '{') depth++;
|
|
6467
6483
|
else if (src[i] === '}') depth--;
|
|
@@ -6483,7 +6499,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
|
|
|
6483
6499
|
endIdx: m.index + m[0].length,
|
|
6484
6500
|
});
|
|
6485
6501
|
}
|
|
6486
|
-
return members
|
|
6502
|
+
return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
|
|
6487
6503
|
}
|
|
6488
6504
|
|
|
6489
6505
|
function normalizeParams(params) {
|
|
@@ -15250,6 +15266,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
|
|
|
15250
15266
|
|
|
15251
15267
|
// Config shapes the supported clients use.
|
|
15252
15268
|
// - 'json' → { mcpServers: { sigmap: { command, args } } }
|
|
15269
|
+
// - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
|
|
15253
15270
|
// - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
|
|
15254
15271
|
// - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
|
|
15255
15272
|
const CLIENTS = {
|
|
@@ -15258,7 +15275,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
|
|
|
15258
15275
|
windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
|
|
15259
15276
|
project: ['.windsurf', 'mcp.json'],
|
|
15260
15277
|
global: ['.codeium', 'windsurf', 'mcp_config.json'] },
|
|
15261
|
-
vscode: { label: 'VS Code', format: '
|
|
15278
|
+
vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
|
|
15262
15279
|
opencode: { label: 'OpenCode', format: 'json', scope: 'both',
|
|
15263
15280
|
project: ['opencode.json'],
|
|
15264
15281
|
global: ['.config', 'opencode', 'config.json'] },
|
|
@@ -15313,6 +15330,31 @@ __factories["./src/mcp/install"] = function(module, exports) {
|
|
|
15313
15330
|
return 'installed';
|
|
15314
15331
|
}
|
|
15315
15332
|
|
|
15333
|
+
/**
|
|
15334
|
+
* Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
|
|
15335
|
+
* (not `mcpServers`) and expects an explicit transport `type`. A config written
|
|
15336
|
+
* by an older SigMap under `mcpServers` is migrated rather than left in place,
|
|
15337
|
+
* so re-running repairs it instead of leaving two entries VS Code cannot read.
|
|
15338
|
+
*/
|
|
15339
|
+
function _installVscode(filePath, scriptPath) {
|
|
15340
|
+
let settings = {};
|
|
15341
|
+
if (fs.existsSync(filePath)) {
|
|
15342
|
+
try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
|
|
15343
|
+
catch (_) { settings = {}; }
|
|
15344
|
+
}
|
|
15345
|
+
const stale = settings.mcpServers && settings.mcpServers.sigmap;
|
|
15346
|
+
if (stale) {
|
|
15347
|
+
delete settings.mcpServers.sigmap;
|
|
15348
|
+
if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
|
|
15349
|
+
}
|
|
15350
|
+
if (!settings.servers) settings.servers = {};
|
|
15351
|
+
if (settings.servers.sigmap && !stale) return 'already';
|
|
15352
|
+
settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
|
|
15353
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15354
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
|
|
15355
|
+
return stale ? 'updated' : 'installed';
|
|
15356
|
+
}
|
|
15357
|
+
|
|
15316
15358
|
/** Install into Zed's `context_servers` config (create file/dir if absent). */
|
|
15317
15359
|
function _installZed(filePath, scriptPath) {
|
|
15318
15360
|
let settings = {};
|
|
@@ -15365,7 +15407,8 @@ __factories["./src/mcp/install"] = function(module, exports) {
|
|
|
15365
15407
|
const filePath = resolveTarget(spec, cwd, home, opts.global);
|
|
15366
15408
|
|
|
15367
15409
|
let status;
|
|
15368
|
-
if (spec.format === '
|
|
15410
|
+
if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
|
|
15411
|
+
else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
|
|
15369
15412
|
else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
|
|
15370
15413
|
else status = _installJson(filePath, scriptPath);
|
|
15371
15414
|
|
|
@@ -15397,7 +15440,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15397
15440
|
|
|
15398
15441
|
const SERVER_INFO = {
|
|
15399
15442
|
name: 'sigmap',
|
|
15400
|
-
version: '8.
|
|
15443
|
+
version: '8.30.0',
|
|
15401
15444
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15402
15445
|
};
|
|
15403
15446
|
|
|
@@ -16760,26 +16803,52 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16760
16803
|
generatedCode: 0.3, // dist/build/.next in path
|
|
16761
16804
|
docsFile: 0.2, // docs/doc/README in path
|
|
16762
16805
|
nodeModules: 0.0, // node_modules (zero score)
|
|
16806
|
+
dataHolder: 0.3, // generated POJO/entity: almost entirely accessors
|
|
16763
16807
|
};
|
|
16764
16808
|
|
|
16809
|
+
// A file whose members are overwhelmingly trivial accessors is a data holder,
|
|
16810
|
+
// not logic. Path-based detection cannot see these: generated JPA/MyBatis
|
|
16811
|
+
// entities live in ordinary source trees. They match a query on any column
|
|
16812
|
+
// name they happen to carry (`getNote`/`setNote` matches "note" as strongly as
|
|
16813
|
+
// the service that actually implements order notes), so on an entity-heavy
|
|
16814
|
+
// repo they crowd real code out of the top results.
|
|
16815
|
+
const ACCESSOR_RE = /^\s*(get|set|is)[A-Z]\w*\s*\(/;
|
|
16816
|
+
const DATA_HOLDER_RATIO = 0.8;
|
|
16817
|
+
const DATA_HOLDER_MIN_MEMBERS = 6;
|
|
16818
|
+
|
|
16765
16819
|
// Query terms that mean the penalised category IS the target. Read from the
|
|
16766
16820
|
// query tokens directly, NOT via detectIntent: that classifier is first-match-
|
|
16767
16821
|
// wins over its pattern object, and `debug` precedes `test`, so "fix the failing
|
|
16768
16822
|
// test" classifies as debug and never reaches the test branch.
|
|
16769
16823
|
const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
|
|
16770
16824
|
const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
|
|
16825
|
+
const WANTS_MODELS = new Set(['entity', 'entities', 'model', 'models', 'pojo', 'dto', 'bean', 'getter', 'getters', 'setter', 'setters', 'accessor', 'accessors', 'field', 'fields', 'column', 'columns', 'schema']);
|
|
16771
16826
|
|
|
16772
16827
|
/** Which penalised categories the query is explicitly asking for. */
|
|
16773
16828
|
function _queryWants(queryTokens) {
|
|
16774
|
-
const wants = { tests: false, docs: false };
|
|
16829
|
+
const wants = { tests: false, docs: false, models: false };
|
|
16775
16830
|
for (const t of queryTokens || []) {
|
|
16776
16831
|
if (WANTS_TESTS.has(t)) wants.tests = true;
|
|
16777
16832
|
if (WANTS_DOCS.has(t)) wants.docs = true;
|
|
16833
|
+
if (WANTS_MODELS.has(t)) wants.models = true;
|
|
16778
16834
|
}
|
|
16779
16835
|
return wants;
|
|
16780
16836
|
}
|
|
16781
16837
|
|
|
16782
|
-
|
|
16838
|
+
/**
|
|
16839
|
+
* True when a file's members are overwhelmingly trivial accessors — a generated
|
|
16840
|
+
* entity or POJO rather than logic. Type declarations are excluded from the
|
|
16841
|
+
* ratio so a small class is not misjudged by its own `class X` line.
|
|
16842
|
+
*/
|
|
16843
|
+
function _isDataHolder(sigs) {
|
|
16844
|
+
if (!Array.isArray(sigs)) return false;
|
|
16845
|
+
const members = sigs.filter((line) => /^\s/.test(line) || !/^(class|interface|enum|struct|function|module\.exports)\b/.test(line));
|
|
16846
|
+
if (members.length < DATA_HOLDER_MIN_MEMBERS) return false;
|
|
16847
|
+
const accessors = members.filter((line) => ACCESSOR_RE.test(line)).length;
|
|
16848
|
+
return accessors / members.length >= DATA_HOLDER_RATIO;
|
|
16849
|
+
}
|
|
16850
|
+
|
|
16851
|
+
function _computePenalty(filePath, wants, sigs) {
|
|
16783
16852
|
const pathLower = filePath.toLowerCase();
|
|
16784
16853
|
if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
|
|
16785
16854
|
// A penalty must never fire on the very thing the user asked for. Before
|
|
@@ -16792,6 +16861,11 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16792
16861
|
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
|
|
16793
16862
|
return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
|
|
16794
16863
|
}
|
|
16864
|
+
// Content-based, and last: a data holder is still a real source file, so it
|
|
16865
|
+
// is only demoted once the path-based categories have had their say.
|
|
16866
|
+
if (_isDataHolder(sigs)) {
|
|
16867
|
+
return (wants && wants.models) ? 1.0 : PENALTY_SIGNALS.dataHolder;
|
|
16868
|
+
}
|
|
16795
16869
|
return 1.0;
|
|
16796
16870
|
}
|
|
16797
16871
|
|
|
@@ -16849,7 +16923,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16849
16923
|
if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
|
|
16850
16924
|
|
|
16851
16925
|
const w = weights || DEFAULT_WEIGHTS;
|
|
16852
|
-
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
|
|
16926
|
+
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants, sigs) };
|
|
16853
16927
|
|
|
16854
16928
|
// Module-doc prose is excluded here on purpose. This signal measures overlap
|
|
16855
16929
|
// with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
|
|
@@ -17445,7 +17519,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
17445
17519
|
return detectIntents(query)[0];
|
|
17446
17520
|
}
|
|
17447
17521
|
|
|
17448
|
-
module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
17522
|
+
module.exports = { rank, buildSigIndex, scoreFile, _queryWants, _isDataHolder, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
17449
17523
|
|
|
17450
17524
|
};
|
|
17451
17525
|
|
|
@@ -18738,6 +18812,24 @@ __factories["./src/skills/skills"] = function(module, exports) {
|
|
|
18738
18812
|
'6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
|
|
18739
18813
|
].join('\n'),
|
|
18740
18814
|
},
|
|
18815
|
+
'sigmap-task': {
|
|
18816
|
+
title: 'SigMap task loop',
|
|
18817
|
+
kind: 'prompt',
|
|
18818
|
+
description: 'Do a coding task grounded in SigMap: look up before reading, edit by line anchor, verify before reporting.',
|
|
18819
|
+
argumentHint: 'the change you want, in plain words',
|
|
18820
|
+
body: [
|
|
18821
|
+
'Work through these steps **in order**. Do not open any file before step 2.',
|
|
18822
|
+
'Every command runs from the integrated terminal — do not ask the user to run them for you.',
|
|
18823
|
+
'',
|
|
18824
|
+
'1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
|
|
18825
|
+
'2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
|
|
18826
|
+
'3. **Open only the anchored ranges.** A signature ending `:425-425` means read line 425, not the whole file. Never read a file in full when you hold an anchor for it.',
|
|
18827
|
+
'4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
|
|
18828
|
+
'5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
|
|
18829
|
+
'6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
|
|
18830
|
+
'7. **Report.** The files you changed, the ranges you actually read, the step-1 token count, and the step-5 verify result. Say so if you fell back to searching or if verify flagged something.',
|
|
18831
|
+
].join('\n'),
|
|
18832
|
+
},
|
|
18741
18833
|
'sigmap-config-optimizer': {
|
|
18742
18834
|
title: 'SigMap config optimizer',
|
|
18743
18835
|
description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
|
|
@@ -18762,7 +18854,9 @@ __factories["./src/skills/skills"] = function(module, exports) {
|
|
|
18762
18854
|
windsurf: { label: 'Windsurf', parent: ['.windsurf'],
|
|
18763
18855
|
target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
|
|
18764
18856
|
copilot: { label: 'GitHub Copilot', parent: ['.github'],
|
|
18765
|
-
target: (cwd, skill) =>
|
|
18857
|
+
target: (cwd, skill) => (SKILLS[skill] && SKILLS[skill].kind === 'prompt'
|
|
18858
|
+
? path.join(cwd, '.github', 'prompts', `${skill}.prompt.md`)
|
|
18859
|
+
: path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`)) },
|
|
18766
18860
|
codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
|
|
18767
18861
|
target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
|
|
18768
18862
|
};
|
|
@@ -18783,6 +18877,10 @@ __factories["./src/skills/skills"] = function(module, exports) {
|
|
|
18783
18877
|
return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
|
|
18784
18878
|
}
|
|
18785
18879
|
if (client === 'copilot') {
|
|
18880
|
+
if (skill.kind === 'prompt') {
|
|
18881
|
+
return `---\nname: ${skillName}\nagent: 'agent'\ndescription: ${skill.description}\n`
|
|
18882
|
+
+ `argument-hint: ${skill.argumentHint}\n---\n\n${body}`;
|
|
18883
|
+
}
|
|
18786
18884
|
return `---\napplyTo: "**"\n---\n\n${body}`;
|
|
18787
18885
|
}
|
|
18788
18886
|
return body; // windsurf: plain markdown
|
|
@@ -21636,7 +21734,7 @@ function __tryGit(args, opts = {}) {
|
|
|
21636
21734
|
catch (_) { return ''; }
|
|
21637
21735
|
}
|
|
21638
21736
|
|
|
21639
|
-
const VERSION = '8.
|
|
21737
|
+
const VERSION = '8.30.0';
|
|
21640
21738
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21641
21739
|
|
|
21642
21740
|
function requireSourceOrBundled(key) {
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ 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.30.0 | Benchmark: sigmap-v8.30-main (2026-09-07)
|
|
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.30-main, 2026-09-07)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
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.30.0 | Benchmark: sigmap-v8.30-main (2026-09-07)
|
|
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,7 +23,7 @@ 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.30-main, 2026-09-07)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 81.1% vs 44.0% single-shot grep baseline (1.73× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.30.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": {
|
package/src/extractors/java.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { lineAt, withAnchor } = require('./line-anchor');
|
|
4
|
+
const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
|
|
5
|
+
|
|
6
|
+
// Class bodies are scanned to this many characters. Generated JVM sources
|
|
7
|
+
// (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
|
|
8
|
+
// against pathological input rather than trimming ordinary classes.
|
|
9
|
+
const MAX_CLASS_BODY_CHARS = 200000;
|
|
10
|
+
|
|
11
|
+
// Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
|
|
12
|
+
// caller's configured budget governs the output rather than this file.
|
|
13
|
+
const MAX_MEMBERS_PER_CLASS = 120;
|
|
14
|
+
|
|
15
|
+
// Per-file signature ceiling, likewise above the configured default.
|
|
16
|
+
const MAX_SIGS_PER_FILE = 200;
|
|
4
17
|
|
|
5
18
|
/**
|
|
6
19
|
* Extract signatures from Java source code.
|
|
@@ -28,17 +41,20 @@ function extract(src) {
|
|
|
28
41
|
const block = extractBlock(stripped, bodyStart);
|
|
29
42
|
sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
|
|
30
43
|
for (const meth of extractMembers(block)) {
|
|
31
|
-
|
|
44
|
+
// The disclosure marker carries no offsets; anchor it at the class body.
|
|
45
|
+
const declIdx = meth.declIdx || 0;
|
|
46
|
+
const endIdx = meth.endIdx || 0;
|
|
47
|
+
sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
return sigs
|
|
51
|
+
return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
function extractBlock(src, startIndex) {
|
|
39
55
|
let depth = 1;
|
|
40
56
|
let i = startIndex;
|
|
41
|
-
const end = Math.min(src.length, startIndex +
|
|
57
|
+
const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
|
|
42
58
|
while (i < end && depth > 0) {
|
|
43
59
|
if (src[i] === '{') depth++;
|
|
44
60
|
else if (src[i] === '}') depth--;
|
|
@@ -60,7 +76,7 @@ function extractMembers(block) {
|
|
|
60
76
|
endIdx: m.index + m[0].length,
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
|
-
return members
|
|
79
|
+
return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
function normalizeParams(params) {
|
package/src/mcp/install.js
CHANGED
|
@@ -17,6 +17,7 @@ const os = require('os');
|
|
|
17
17
|
|
|
18
18
|
// Config shapes the supported clients use.
|
|
19
19
|
// - 'json' → { mcpServers: { sigmap: { command, args } } }
|
|
20
|
+
// - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
|
|
20
21
|
// - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
|
|
21
22
|
// - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
|
|
22
23
|
const CLIENTS = {
|
|
@@ -25,7 +26,7 @@ const CLIENTS = {
|
|
|
25
26
|
windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
|
|
26
27
|
project: ['.windsurf', 'mcp.json'],
|
|
27
28
|
global: ['.codeium', 'windsurf', 'mcp_config.json'] },
|
|
28
|
-
vscode: { label: 'VS Code', format: '
|
|
29
|
+
vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
|
|
29
30
|
opencode: { label: 'OpenCode', format: 'json', scope: 'both',
|
|
30
31
|
project: ['opencode.json'],
|
|
31
32
|
global: ['.config', 'opencode', 'config.json'] },
|
|
@@ -80,6 +81,31 @@ function _installJson(filePath, scriptPath) {
|
|
|
80
81
|
return 'installed';
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
|
|
86
|
+
* (not `mcpServers`) and expects an explicit transport `type`. A config written
|
|
87
|
+
* by an older SigMap under `mcpServers` is migrated rather than left in place,
|
|
88
|
+
* so re-running repairs it instead of leaving two entries VS Code cannot read.
|
|
89
|
+
*/
|
|
90
|
+
function _installVscode(filePath, scriptPath) {
|
|
91
|
+
let settings = {};
|
|
92
|
+
if (fs.existsSync(filePath)) {
|
|
93
|
+
try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
|
|
94
|
+
catch (_) { settings = {}; }
|
|
95
|
+
}
|
|
96
|
+
const stale = settings.mcpServers && settings.mcpServers.sigmap;
|
|
97
|
+
if (stale) {
|
|
98
|
+
delete settings.mcpServers.sigmap;
|
|
99
|
+
if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
|
|
100
|
+
}
|
|
101
|
+
if (!settings.servers) settings.servers = {};
|
|
102
|
+
if (settings.servers.sigmap && !stale) return 'already';
|
|
103
|
+
settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
|
|
104
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
105
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
|
|
106
|
+
return stale ? 'updated' : 'installed';
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
/** Install into Zed's `context_servers` config (create file/dir if absent). */
|
|
84
110
|
function _installZed(filePath, scriptPath) {
|
|
85
111
|
let settings = {};
|
|
@@ -132,7 +158,8 @@ function installClient(client, opts = {}) {
|
|
|
132
158
|
const filePath = resolveTarget(spec, cwd, home, opts.global);
|
|
133
159
|
|
|
134
160
|
let status;
|
|
135
|
-
if (spec.format === '
|
|
161
|
+
if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
|
|
162
|
+
else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
|
|
136
163
|
else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
|
|
137
164
|
else status = _installJson(filePath, scriptPath);
|
|
138
165
|
|
package/src/mcp/server.js
CHANGED
package/src/retrieval/ranker.js
CHANGED
|
@@ -72,26 +72,52 @@ const PENALTY_SIGNALS = {
|
|
|
72
72
|
generatedCode: 0.3, // dist/build/.next in path
|
|
73
73
|
docsFile: 0.2, // docs/doc/README in path
|
|
74
74
|
nodeModules: 0.0, // node_modules (zero score)
|
|
75
|
+
dataHolder: 0.3, // generated POJO/entity: almost entirely accessors
|
|
75
76
|
};
|
|
76
77
|
|
|
78
|
+
// A file whose members are overwhelmingly trivial accessors is a data holder,
|
|
79
|
+
// not logic. Path-based detection cannot see these: generated JPA/MyBatis
|
|
80
|
+
// entities live in ordinary source trees. They match a query on any column
|
|
81
|
+
// name they happen to carry (`getNote`/`setNote` matches "note" as strongly as
|
|
82
|
+
// the service that actually implements order notes), so on an entity-heavy
|
|
83
|
+
// repo they crowd real code out of the top results.
|
|
84
|
+
const ACCESSOR_RE = /^\s*(get|set|is)[A-Z]\w*\s*\(/;
|
|
85
|
+
const DATA_HOLDER_RATIO = 0.8;
|
|
86
|
+
const DATA_HOLDER_MIN_MEMBERS = 6;
|
|
87
|
+
|
|
77
88
|
// Query terms that mean the penalised category IS the target. Read from the
|
|
78
89
|
// query tokens directly, NOT via detectIntent: that classifier is first-match-
|
|
79
90
|
// wins over its pattern object, and `debug` precedes `test`, so "fix the failing
|
|
80
91
|
// test" classifies as debug and never reaches the test branch.
|
|
81
92
|
const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
|
|
82
93
|
const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
|
|
94
|
+
const WANTS_MODELS = new Set(['entity', 'entities', 'model', 'models', 'pojo', 'dto', 'bean', 'getter', 'getters', 'setter', 'setters', 'accessor', 'accessors', 'field', 'fields', 'column', 'columns', 'schema']);
|
|
83
95
|
|
|
84
96
|
/** Which penalised categories the query is explicitly asking for. */
|
|
85
97
|
function _queryWants(queryTokens) {
|
|
86
|
-
const wants = { tests: false, docs: false };
|
|
98
|
+
const wants = { tests: false, docs: false, models: false };
|
|
87
99
|
for (const t of queryTokens || []) {
|
|
88
100
|
if (WANTS_TESTS.has(t)) wants.tests = true;
|
|
89
101
|
if (WANTS_DOCS.has(t)) wants.docs = true;
|
|
102
|
+
if (WANTS_MODELS.has(t)) wants.models = true;
|
|
90
103
|
}
|
|
91
104
|
return wants;
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
|
|
107
|
+
/**
|
|
108
|
+
* True when a file's members are overwhelmingly trivial accessors — a generated
|
|
109
|
+
* entity or POJO rather than logic. Type declarations are excluded from the
|
|
110
|
+
* ratio so a small class is not misjudged by its own `class X` line.
|
|
111
|
+
*/
|
|
112
|
+
function _isDataHolder(sigs) {
|
|
113
|
+
if (!Array.isArray(sigs)) return false;
|
|
114
|
+
const members = sigs.filter((line) => /^\s/.test(line) || !/^(class|interface|enum|struct|function|module\.exports)\b/.test(line));
|
|
115
|
+
if (members.length < DATA_HOLDER_MIN_MEMBERS) return false;
|
|
116
|
+
const accessors = members.filter((line) => ACCESSOR_RE.test(line)).length;
|
|
117
|
+
return accessors / members.length >= DATA_HOLDER_RATIO;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _computePenalty(filePath, wants, sigs) {
|
|
95
121
|
const pathLower = filePath.toLowerCase();
|
|
96
122
|
if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
|
|
97
123
|
// A penalty must never fire on the very thing the user asked for. Before
|
|
@@ -104,6 +130,11 @@ function _computePenalty(filePath, wants) {
|
|
|
104
130
|
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
|
|
105
131
|
return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
|
|
106
132
|
}
|
|
133
|
+
// Content-based, and last: a data holder is still a real source file, so it
|
|
134
|
+
// is only demoted once the path-based categories have had their say.
|
|
135
|
+
if (_isDataHolder(sigs)) {
|
|
136
|
+
return (wants && wants.models) ? 1.0 : PENALTY_SIGNALS.dataHolder;
|
|
137
|
+
}
|
|
107
138
|
return 1.0;
|
|
108
139
|
}
|
|
109
140
|
|
|
@@ -161,7 +192,7 @@ function scoreFile(filePath, sigs, queryTokens, weights, wants) {
|
|
|
161
192
|
if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
|
|
162
193
|
|
|
163
194
|
const w = weights || DEFAULT_WEIGHTS;
|
|
164
|
-
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
|
|
195
|
+
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants, sigs) };
|
|
165
196
|
|
|
166
197
|
// Module-doc prose is excluded here on purpose. This signal measures overlap
|
|
167
198
|
// with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
|
|
@@ -757,4 +788,4 @@ function detectIntent(query) {
|
|
|
757
788
|
return detectIntents(query)[0];
|
|
758
789
|
}
|
|
759
790
|
|
|
760
|
-
module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
|
791
|
+
module.exports = { rank, buildSigIndex, scoreFile, _queryWants, _isDataHolder, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
|
package/src/skills/skills.js
CHANGED
|
@@ -34,6 +34,24 @@ const SKILLS = {
|
|
|
34
34
|
'6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
|
|
35
35
|
].join('\n'),
|
|
36
36
|
},
|
|
37
|
+
'sigmap-task': {
|
|
38
|
+
title: 'SigMap task loop',
|
|
39
|
+
kind: 'prompt',
|
|
40
|
+
description: 'Do a coding task grounded in SigMap: look up before reading, edit by line anchor, verify before reporting.',
|
|
41
|
+
argumentHint: 'the change you want, in plain words',
|
|
42
|
+
body: [
|
|
43
|
+
'Work through these steps **in order**. Do not open any file before step 2.',
|
|
44
|
+
'Every command runs from the integrated terminal — do not ask the user to run them for you.',
|
|
45
|
+
'',
|
|
46
|
+
'1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
|
|
47
|
+
'2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
|
|
48
|
+
'3. **Open only the anchored ranges.** A signature ending `:425-425` means read line 425, not the whole file. Never read a file in full when you hold an anchor for it.',
|
|
49
|
+
'4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
|
|
50
|
+
'5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
|
|
51
|
+
'6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
|
|
52
|
+
'7. **Report.** The files you changed, the ranges you actually read, the step-1 token count, and the step-5 verify result. Say so if you fell back to searching or if verify flagged something.',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
},
|
|
37
55
|
'sigmap-config-optimizer': {
|
|
38
56
|
title: 'SigMap config optimizer',
|
|
39
57
|
description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
|
|
@@ -58,7 +76,9 @@ const SKILL_CLIENTS = {
|
|
|
58
76
|
windsurf: { label: 'Windsurf', parent: ['.windsurf'],
|
|
59
77
|
target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
|
|
60
78
|
copilot: { label: 'GitHub Copilot', parent: ['.github'],
|
|
61
|
-
target: (cwd, skill) =>
|
|
79
|
+
target: (cwd, skill) => (SKILLS[skill] && SKILLS[skill].kind === 'prompt'
|
|
80
|
+
? path.join(cwd, '.github', 'prompts', `${skill}.prompt.md`)
|
|
81
|
+
: path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`)) },
|
|
62
82
|
codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
|
|
63
83
|
target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
|
|
64
84
|
};
|
|
@@ -79,6 +99,10 @@ function renderSkill(client, skillName, version) {
|
|
|
79
99
|
return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
|
|
80
100
|
}
|
|
81
101
|
if (client === 'copilot') {
|
|
102
|
+
if (skill.kind === 'prompt') {
|
|
103
|
+
return `---\nname: ${skillName}\nagent: 'agent'\ndescription: ${skill.description}\n`
|
|
104
|
+
+ `argument-hint: ${skill.argumentHint}\n---\n\n${body}`;
|
|
105
|
+
}
|
|
82
106
|
return `---\napplyTo: "**"\n---\n\n${body}`;
|
|
83
107
|
}
|
|
84
108
|
return body; // windsurf: plain markdown
|