sigmap 6.11.1 → 6.12.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 +18 -0
- package/README.md +3 -3
- package/gen-context.js +251 -11
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/format/dashboard.js +105 -0
- package/src/mcp/handlers.js +58 -1
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +31 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.12.0] — 2026-06-05
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Surgical Context Phase 2 — demand-driven retrieval (#219, PR #220):**
|
|
18
|
+
- **`get_lines` MCP tool** (10th tool) — fetch an exact `{ file, start, end }` line range on demand. Lines are clamped to the file bounds, secret-scanned via the existing redactor, and sandboxed to the project root. This is the demand-driven workhorse: agents read the lines behind a `:start-end` anchor instead of re-opening whole files.
|
|
19
|
+
- **`sigmap ask --mode index`** — two-tier output that emits only symbol-header pointers (`symbol :start-end`), dropping parameter lists, return types, and bodies. Agents re-fetch bodies via `get_lines`.
|
|
20
|
+
- **`sigmap ask --since <ref>`** — delta context that restricts ranked output to files changed since a git ref.
|
|
21
|
+
- **Token Reduction dashboard panel (Surface A)** — `sigmap --report` now renders a "Token Reduction" panel (whole-file baseline vs ranked signatures vs surgical, with per-repo rows), sourced from `benchmarks/reports/token-reduction.json` — numbers are never hand-typed.
|
|
22
|
+
- New **Surgical Context** guide (`docs-vp/guide/surgical-context.md`) covering line anchors, `--mode index`, `--since`, and the `get_lines` MCP tool.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Budget-aware progressive disclosure** — when generated context exceeds `maxTokens`, the token budget now collapses signature bodies to their line anchors (keeping `symbol :start-end`) *before* dropping whole files, degrading gracefully.
|
|
27
|
+
- **CI** — added `workflow_dispatch` to the develop→main sync workflow so it can be run on demand (PR #218).
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [6.11.1] — 2026-06-04
|
|
14
32
|
|
|
15
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -87,8 +87,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
87
87
|
## Benchmark
|
|
88
88
|
|
|
89
89
|
```
|
|
90
|
-
Benchmark : sigmap-v6.
|
|
91
|
-
Date : 2026-06-
|
|
90
|
+
Benchmark : sigmap-v6.12-main (21 repositories, including R language)
|
|
91
|
+
Date : 2026-06-05
|
|
92
92
|
|
|
93
93
|
Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
|
|
94
94
|
Token reduction: 96.5% (across 21 repos)
|
|
@@ -183,7 +183,7 @@ Use SigMap with open-source tools and fully self-hosted setups:
|
|
|
183
183
|
| **JetBrains** | [Marketplace](https://plugins.jetbrains.com/plugin/31109-sigmap--ai-context-engine/) | [github.com/manojmallick/sigmap-jetbrains](https://github.com/manojmallick/sigmap-jetbrains) | IntelliJ IDEA, WebStorm, PyCharm, GoLand — tool window + actions |
|
|
184
184
|
| **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
|
|
185
185
|
|
|
186
|
-
**MCP server** —
|
|
186
|
+
**MCP server** — 10 on-demand tools for Claude Code and Cursor:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
189
|
sigmap --mcp
|
package/gen-context.js
CHANGED
|
@@ -4052,6 +4052,96 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4052
4052
|
}).join('');
|
|
4053
4053
|
}
|
|
4054
4054
|
|
|
4055
|
+
function escapeAttr(s) {
|
|
4056
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
4057
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
4058
|
+
));
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function readTokenReduction(cwd) {
|
|
4062
|
+
const p = path.join(cwd, 'benchmarks', 'reports', 'token-reduction.json');
|
|
4063
|
+
let data;
|
|
4064
|
+
try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
4065
|
+
const repos = Array.isArray(data.repos) ? data.repos : [];
|
|
4066
|
+
if (repos.length === 0) return null;
|
|
4067
|
+
let baseline = 0, signatures = 0, surgical = 0, hasSurgical = false;
|
|
4068
|
+
for (const r of repos) {
|
|
4069
|
+
baseline += toNumber(r.rawTokens) || 0;
|
|
4070
|
+
signatures += toNumber(r.finalTokens) || 0;
|
|
4071
|
+
const s = toNumber(r.surgicalTokens);
|
|
4072
|
+
if (s !== null) { surgical += s; hasSurgical = true; }
|
|
4073
|
+
}
|
|
4074
|
+
const out = {
|
|
4075
|
+
version: data.version || null,
|
|
4076
|
+
repoCount: repos.length,
|
|
4077
|
+
baseline,
|
|
4078
|
+
signatures,
|
|
4079
|
+
savedPct: baseline > 0 ? Math.round((1 - signatures / baseline) * 1000) / 10 : 0,
|
|
4080
|
+
perRepo: repos.map((r) => ({
|
|
4081
|
+
repo: r.repo, language: r.language,
|
|
4082
|
+
rawTokens: toNumber(r.rawTokens) || 0,
|
|
4083
|
+
finalTokens: toNumber(r.finalTokens) || 0,
|
|
4084
|
+
reductionPct: toNumber(r.reductionPct) || 0,
|
|
4085
|
+
})),
|
|
4086
|
+
};
|
|
4087
|
+
if (hasSurgical) {
|
|
4088
|
+
out.surgical = surgical;
|
|
4089
|
+
out.surgicalSavedPct = baseline > 0 ? Math.round((1 - surgical / baseline) * 1000) / 10 : 0;
|
|
4090
|
+
}
|
|
4091
|
+
return out;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function tokenReductionPanelHtml(tr) {
|
|
4095
|
+
if (!tr) {
|
|
4096
|
+
return '<div class="panel"><div class="label">Token Reduction</div>' +
|
|
4097
|
+
'<div class="value" style="font-size:13px">No token-reduction benchmark found — run the token benchmark to populate this panel.</div></div>';
|
|
4098
|
+
}
|
|
4099
|
+
const fmt = (n) => Number(n).toLocaleString('en-US');
|
|
4100
|
+
const tiers = [
|
|
4101
|
+
{ label: 'Whole-file baseline', value: fmt(tr.baseline) + ' tok' },
|
|
4102
|
+
{ label: 'Ranked signatures (ask)', value: fmt(tr.signatures) + ' tok' },
|
|
4103
|
+
];
|
|
4104
|
+
if (tr.surgical != null) {
|
|
4105
|
+
tiers.push({ label: 'Surgical (index + delta)', value: fmt(tr.surgical) + ' tok' });
|
|
4106
|
+
tiers.push({ label: 'Saved (surgical)', value: tr.surgicalSavedPct + '%' });
|
|
4107
|
+
} else {
|
|
4108
|
+
tiers.push({ label: 'Saved', value: tr.savedPct + '%' });
|
|
4109
|
+
}
|
|
4110
|
+
const tierHtml = tiers.map((t) =>
|
|
4111
|
+
`<div class="card"><div class="label">${escapeAttr(t.label)}</div><div class="value">${escapeAttr(t.value)}</div></div>`
|
|
4112
|
+
).join('');
|
|
4113
|
+
const sigPct = tr.baseline > 0 ? Math.max(0.4, (tr.signatures / tr.baseline) * 100) : 0;
|
|
4114
|
+
const surgPct = (tr.surgical != null && tr.baseline > 0) ? Math.max(0.4, (tr.surgical / tr.baseline) * 100) : null;
|
|
4115
|
+
const barRow = (label, pct, color) =>
|
|
4116
|
+
`<div style="margin:4px 0;font-size:11px;color:#8ea0d9">${escapeAttr(label)}</div>` +
|
|
4117
|
+
`<div style="background:#0a0f1e;border:1px solid #223056;border-radius:6px;height:14px;overflow:hidden">` +
|
|
4118
|
+
`<div style="width:${pct.toFixed(1)}%;height:100%;background:${color}"></div></div>`;
|
|
4119
|
+
const bars = [
|
|
4120
|
+
barRow('Whole-file baseline (100%)', 100, '#3a4a78'),
|
|
4121
|
+
barRow(`Ranked signatures — ${tr.savedPct}% saved`, sigPct, '#2e7d6b'),
|
|
4122
|
+
surgPct != null ? barRow(`Surgical — ${tr.surgicalSavedPct}% saved`, surgPct, '#5ad1a8') : '',
|
|
4123
|
+
].filter(Boolean).join('');
|
|
4124
|
+
const rows = tr.perRepo.slice(0, 8).map((r) =>
|
|
4125
|
+
`<tr><td>${escapeAttr(r.repo)}</td><td>${escapeAttr(r.language)}</td>` +
|
|
4126
|
+
`<td style="text-align:right">${fmt(r.rawTokens)}</td>` +
|
|
4127
|
+
`<td style="text-align:right">${fmt(r.finalTokens)}</td>` +
|
|
4128
|
+
`<td style="text-align:right">${r.reductionPct}%</td></tr>`
|
|
4129
|
+
).join('');
|
|
4130
|
+
return [
|
|
4131
|
+
'<div class="panel">',
|
|
4132
|
+
`<div class="label">Token Reduction — ${tr.repoCount} benchmark repos${tr.version ? ' · v' + escapeAttr(tr.version) : ''}</div>`,
|
|
4133
|
+
`<div class="grid" style="margin:8px 0">${tierHtml}</div>`,
|
|
4134
|
+
bars,
|
|
4135
|
+
'<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:12px">',
|
|
4136
|
+
'<thead><tr style="color:#8ea0d9;text-align:left">',
|
|
4137
|
+
'<th>Repo</th><th>Lang</th><th style="text-align:right">Baseline</th><th style="text-align:right">Signatures</th><th style="text-align:right">Saved</th>',
|
|
4138
|
+
'</tr></thead>',
|
|
4139
|
+
`<tbody>${rows}</tbody>`,
|
|
4140
|
+
'</table>',
|
|
4141
|
+
'</div>',
|
|
4142
|
+
].join('');
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4055
4145
|
function buildDashboardData(cwd, health) {
|
|
4056
4146
|
const entries = readLog(cwd);
|
|
4057
4147
|
const recent = entries.slice(-30);
|
|
@@ -4070,15 +4160,18 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4070
4160
|
overBudgetStreak: overBudgetStreak(entries),
|
|
4071
4161
|
extractorCoverage: coverage.pct,
|
|
4072
4162
|
};
|
|
4163
|
+
const tokenReduction = readTokenReduction(cwd);
|
|
4073
4164
|
return {
|
|
4074
4165
|
summary,
|
|
4075
4166
|
tokenReductionTrend,
|
|
4076
4167
|
hitAt5Trend,
|
|
4077
4168
|
coverage,
|
|
4169
|
+
tokenReduction,
|
|
4078
4170
|
charts: {
|
|
4079
4171
|
tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
|
|
4080
4172
|
hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
|
|
4081
4173
|
coverageSvg: barChartSvg(coverage.perLanguage),
|
|
4174
|
+
tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
|
|
4082
4175
|
},
|
|
4083
4176
|
};
|
|
4084
4177
|
}
|
|
@@ -4112,6 +4205,7 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4112
4205
|
'<h1>SigMap v2.10 dashboard</h1>',
|
|
4113
4206
|
'<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
|
|
4114
4207
|
`<div class="grid">${cardHtml}</div>`,
|
|
4208
|
+
data.charts.tokenSavingsPanel,
|
|
4115
4209
|
`<div class="panel">${data.charts.tokenReductionSvg}</div>`,
|
|
4116
4210
|
`<div class="panel">${data.charts.hitAt5Svg}</div>`,
|
|
4117
4211
|
`<div class="panel">${data.charts.coverageSvg}</div>`,
|
|
@@ -5719,7 +5813,56 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5719
5813
|
}
|
|
5720
5814
|
}
|
|
5721
5815
|
|
|
5722
|
-
|
|
5816
|
+
function getLines(args, cwd) {
|
|
5817
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
5818
|
+
|
|
5819
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
5820
|
+
const abs = path.resolve(cwd, rel);
|
|
5821
|
+
|
|
5822
|
+
// Sandbox: refuse paths that resolve outside the project root.
|
|
5823
|
+
const root = path.resolve(cwd);
|
|
5824
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
5825
|
+
return `Refused: ${rel} resolves outside the project root`;
|
|
5826
|
+
}
|
|
5827
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
5828
|
+
return `File not found: ${rel}`;
|
|
5829
|
+
}
|
|
5830
|
+
|
|
5831
|
+
const start = parseInt(args.start, 10);
|
|
5832
|
+
const end = parseInt(args.end, 10);
|
|
5833
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
5834
|
+
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
let lines;
|
|
5838
|
+
try {
|
|
5839
|
+
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
5840
|
+
} catch (err) {
|
|
5841
|
+
return `Could not read ${rel}: ${err.message}`;
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5844
|
+
const total = lines.length;
|
|
5845
|
+
const from = Math.max(1, Math.min(start, end));
|
|
5846
|
+
const to = Math.min(total, Math.max(start, end));
|
|
5847
|
+
if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
|
|
5848
|
+
|
|
5849
|
+
const slice = lines.slice(from - 1, to);
|
|
5850
|
+
|
|
5851
|
+
let safeLines = slice;
|
|
5852
|
+
try {
|
|
5853
|
+
const { scan } = __require('./src/security/scanner');
|
|
5854
|
+
safeLines = scan(slice, rel).safe;
|
|
5855
|
+
} catch (_) {}
|
|
5856
|
+
|
|
5857
|
+
return [
|
|
5858
|
+
`# ${rel}:${from}-${to}`,
|
|
5859
|
+
'```',
|
|
5860
|
+
...safeLines,
|
|
5861
|
+
'```',
|
|
5862
|
+
].join('\n');
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
|
|
5723
5866
|
};
|
|
5724
5867
|
|
|
5725
5868
|
// ── ./src/learning/weights ──
|
|
@@ -5890,17 +6033,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5890
6033
|
*
|
|
5891
6034
|
* Supported methods:
|
|
5892
6035
|
* initialize → serverInfo + capabilities
|
|
5893
|
-
* tools/list →
|
|
6036
|
+
* tools/list → 10 tool definitions
|
|
5894
6037
|
* tools/call → dispatch to handler, return result
|
|
5895
6038
|
*/
|
|
5896
|
-
|
|
6039
|
+
|
|
5897
6040
|
const readline = require('readline');
|
|
5898
6041
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
5899
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = __require('./src/mcp/handlers');
|
|
5900
|
-
|
|
6042
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = __require('./src/mcp/handlers');
|
|
6043
|
+
|
|
5901
6044
|
const SERVER_INFO = {
|
|
5902
6045
|
name: 'sigmap',
|
|
5903
|
-
version: '6.
|
|
6046
|
+
version: '6.12.0',
|
|
5904
6047
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5905
6048
|
};
|
|
5906
6049
|
|
|
@@ -5957,6 +6100,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5957
6100
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
5958
6101
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
5959
6102
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6103
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
5960
6104
|
else {
|
|
5961
6105
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
5962
6106
|
return;
|
|
@@ -6184,8 +6328,36 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
6184
6328
|
required: ['file'],
|
|
6185
6329
|
},
|
|
6186
6330
|
},
|
|
6331
|
+
{
|
|
6332
|
+
name: 'get_lines',
|
|
6333
|
+
description:
|
|
6334
|
+
'Fetch an exact line range from a source file on demand — the Surgical Context ' +
|
|
6335
|
+
'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
|
|
6336
|
+
'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
|
|
6337
|
+
'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
|
|
6338
|
+
inputSchema: {
|
|
6339
|
+
type: 'object',
|
|
6340
|
+
properties: {
|
|
6341
|
+
file: {
|
|
6342
|
+
type: 'string',
|
|
6343
|
+
description:
|
|
6344
|
+
'Relative path from the project root (e.g. "src/config/loader.js"). ' +
|
|
6345
|
+
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
6346
|
+
},
|
|
6347
|
+
start: {
|
|
6348
|
+
type: 'number',
|
|
6349
|
+
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
6350
|
+
},
|
|
6351
|
+
end: {
|
|
6352
|
+
type: 'number',
|
|
6353
|
+
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
6354
|
+
},
|
|
6355
|
+
},
|
|
6356
|
+
required: ['file', 'start', 'end'],
|
|
6357
|
+
},
|
|
6358
|
+
},
|
|
6187
6359
|
];
|
|
6188
|
-
|
|
6360
|
+
|
|
6189
6361
|
module.exports = { TOOLS };
|
|
6190
6362
|
|
|
6191
6363
|
};
|
|
@@ -8695,7 +8867,7 @@ const path = require('path');
|
|
|
8695
8867
|
const os = require('os');
|
|
8696
8868
|
const { execSync } = require('child_process');
|
|
8697
8869
|
|
|
8698
|
-
const VERSION = '6.
|
|
8870
|
+
const VERSION = '6.12.0';
|
|
8699
8871
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8700
8872
|
|
|
8701
8873
|
function requireSourceOrBundled(key) {
|
|
@@ -8987,8 +9159,28 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
8987
9159
|
let total = fileEntries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
|
|
8988
9160
|
if (total <= effectiveBudget) return fileEntries;
|
|
8989
9161
|
|
|
9162
|
+
// v6.12 Surgical Context — progressive disclosure: before dropping whole files,
|
|
9163
|
+
// collapse signature BODIES to their line-anchor pointers (keep `symbol :start-end`).
|
|
9164
|
+
// Only sigs that actually carry an anchor shrink; the agent can re-fetch bodies via
|
|
9165
|
+
// the get_lines MCP tool. This degrades gracefully instead of losing files outright.
|
|
9166
|
+
let working = fileEntries;
|
|
9167
|
+
const collapsed = fileEntries.map((e) => {
|
|
9168
|
+
const slim = (e.sigs || []).map((s) => {
|
|
9169
|
+
const line = toIndexLine(s);
|
|
9170
|
+
return line && /:\d+-\d+/.test(line) ? line : s; // replace only when it yields an anchor
|
|
9171
|
+
});
|
|
9172
|
+
return { ...e, sigs: slim };
|
|
9173
|
+
});
|
|
9174
|
+
const collapsedTotal = collapsed.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
|
|
9175
|
+
if (collapsedTotal < total) {
|
|
9176
|
+
console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedTotal} tokens`);
|
|
9177
|
+
working = collapsed;
|
|
9178
|
+
total = collapsedTotal;
|
|
9179
|
+
if (total <= effectiveBudget) return working; // anchor collapse alone fit the budget
|
|
9180
|
+
}
|
|
9181
|
+
|
|
8990
9182
|
// Sort by drop priority (drop first = index 0)
|
|
8991
|
-
const withPriority =
|
|
9183
|
+
const withPriority = working.map((e) => {
|
|
8992
9184
|
let priority = 0;
|
|
8993
9185
|
let dropReason = 'budget: low recency';
|
|
8994
9186
|
if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
|
|
@@ -10576,6 +10768,40 @@ function buildMiniContext(ranked, cwd) {
|
|
|
10576
10768
|
return lines.join('\n');
|
|
10577
10769
|
}
|
|
10578
10770
|
|
|
10771
|
+
// Surgical Context Phase 2 (v6.12.0): collapse one signature to a symbol-header
|
|
10772
|
+
// pointer — `<declaration head> :start-end` — dropping param lists, return types,
|
|
10773
|
+
// and any body so the agent fetches the real lines on demand via the get_lines MCP tool.
|
|
10774
|
+
function toIndexLine(sig) {
|
|
10775
|
+
if (typeof sig !== 'string') return '';
|
|
10776
|
+
// Anchor is `:start-end`, optionally followed by a `# doc hint` (Python extractor).
|
|
10777
|
+
const m = sig.match(/\s*:(\d+)-(\d+)(?:\s*#.*)?\s*$/);
|
|
10778
|
+
if (!m) {
|
|
10779
|
+
// No anchor (e.g. an indented member) — keep a trimmed head so it stays listed.
|
|
10780
|
+
return sig.trim().slice(0, 60);
|
|
10781
|
+
}
|
|
10782
|
+
const range = `:${m[1]}-${m[2]}`;
|
|
10783
|
+
let head = sig.slice(0, m.index).trim();
|
|
10784
|
+
const paren = head.indexOf('(');
|
|
10785
|
+
if (paren > 0) head = head.slice(0, paren).trim(); // drop params
|
|
10786
|
+
head = head.replace(/\s*[=→].*$/, '').trim(); // drop return type / assignment tail
|
|
10787
|
+
return head ? `${head} ${range}` : range;
|
|
10788
|
+
}
|
|
10789
|
+
|
|
10790
|
+
// Two-tier index output: emit only `file → [symbol:start-end]` headers (no bodies).
|
|
10791
|
+
function buildIndexContext(ranked, cwd) {
|
|
10792
|
+
const lines = [
|
|
10793
|
+
'# SigMap Query Context (index mode)',
|
|
10794
|
+
`Generated: ${new Date().toISOString()}`,
|
|
10795
|
+
'> Symbol index only — fetch exact lines on demand via the `get_lines` MCP tool.',
|
|
10796
|
+
'',
|
|
10797
|
+
];
|
|
10798
|
+
for (const { file, sigs } of ranked) {
|
|
10799
|
+
const idx = sigs.slice(0, 40).map(toIndexLine).filter(Boolean);
|
|
10800
|
+
lines.push(`## ${file}`, '```', ...idx, '```', '');
|
|
10801
|
+
}
|
|
10802
|
+
return lines.join('\n');
|
|
10803
|
+
}
|
|
10804
|
+
|
|
10579
10805
|
function computeCurrentRisk(cwd) {
|
|
10580
10806
|
try {
|
|
10581
10807
|
const { execSync } = require('child_process');
|
|
@@ -10746,7 +10972,7 @@ function main() {
|
|
|
10746
10972
|
const mode = modeIdx !== -1
|
|
10747
10973
|
? args[modeIdx + 1]
|
|
10748
10974
|
: (config.mode || 'default');
|
|
10749
|
-
const VALID_MODES = ['default', 'fast', 'full', 'both'];
|
|
10975
|
+
const VALID_MODES = ['default', 'fast', 'full', 'both', 'index'];
|
|
10750
10976
|
if (mode && !VALID_MODES.includes(mode)) {
|
|
10751
10977
|
console.error(`[sigmap] unknown --mode "${mode}". Valid: ${VALID_MODES.join(', ')}`);
|
|
10752
10978
|
process.exit(1);
|
|
@@ -10825,9 +11051,23 @@ function main() {
|
|
|
10825
11051
|
}
|
|
10826
11052
|
}
|
|
10827
11053
|
|
|
11054
|
+
// v6.12: Delta context — restrict to files changed since a git ref (--since <ref>).
|
|
11055
|
+
const sinceIdx = args.indexOf('--since');
|
|
11056
|
+
if (sinceIdx !== -1 && args[sinceIdx + 1]) {
|
|
11057
|
+
const baseRef = args[sinceIdx + 1];
|
|
11058
|
+
const changed = getFilesChangedSinceBase(cwd, baseRef);
|
|
11059
|
+
const before = ranked.length;
|
|
11060
|
+
ranked = ranked.filter((r) => changed.has(path.resolve(cwd, r.file)));
|
|
11061
|
+
if (!args.includes('--json')) {
|
|
11062
|
+
process.stderr.write(`[sigmap] Δ since ${baseRef}: ${ranked.length}/${before} ranked files changed\n`);
|
|
11063
|
+
}
|
|
11064
|
+
}
|
|
11065
|
+
|
|
10828
11066
|
// v6.8: Save session for future --followup calls
|
|
10829
11067
|
saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
|
|
10830
|
-
|
|
11068
|
+
// v6.12: Two-tier output — `--mode index` emits symbol pointers only (bodies via get_lines).
|
|
11069
|
+
const indexMode = mode === 'index' || args.includes('--index');
|
|
11070
|
+
const miniCtx = indexMode ? buildIndexContext(ranked, cwd) : buildMiniContext(ranked, cwd);
|
|
10831
11071
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
10832
11072
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
10833
11073
|
fs.writeFileSync(outPath, miniCtx, 'utf8');
|
package/package.json
CHANGED
package/src/format/dashboard.js
CHANGED
|
@@ -304,6 +304,106 @@ function sparkline(values) {
|
|
|
304
304
|
}).join('');
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
function escapeAttr(s) {
|
|
308
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
309
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
310
|
+
));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Surgical Context (v6.12.0): read the published token-reduction benchmark and
|
|
314
|
+
// aggregate it for the dashboard panel. Numbers are never hand-typed — they come
|
|
315
|
+
// straight from benchmarks/reports/token-reduction.json.
|
|
316
|
+
function readTokenReduction(cwd) {
|
|
317
|
+
const p = path.join(cwd, 'benchmarks', 'reports', 'token-reduction.json');
|
|
318
|
+
let data;
|
|
319
|
+
try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
320
|
+
const repos = Array.isArray(data.repos) ? data.repos : [];
|
|
321
|
+
if (repos.length === 0) return null;
|
|
322
|
+
|
|
323
|
+
let baseline = 0, signatures = 0, surgical = 0, hasSurgical = false;
|
|
324
|
+
for (const r of repos) {
|
|
325
|
+
baseline += toNumber(r.rawTokens) || 0;
|
|
326
|
+
signatures += toNumber(r.finalTokens) || 0;
|
|
327
|
+
const s = toNumber(r.surgicalTokens);
|
|
328
|
+
if (s !== null) { surgical += s; hasSurgical = true; }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const out = {
|
|
332
|
+
version: data.version || null,
|
|
333
|
+
repoCount: repos.length,
|
|
334
|
+
baseline,
|
|
335
|
+
signatures,
|
|
336
|
+
savedPct: baseline > 0 ? Math.round((1 - signatures / baseline) * 1000) / 10 : 0,
|
|
337
|
+
perRepo: repos.map((r) => ({
|
|
338
|
+
repo: r.repo,
|
|
339
|
+
language: r.language,
|
|
340
|
+
rawTokens: toNumber(r.rawTokens) || 0,
|
|
341
|
+
finalTokens: toNumber(r.finalTokens) || 0,
|
|
342
|
+
reductionPct: toNumber(r.reductionPct) || 0,
|
|
343
|
+
})),
|
|
344
|
+
};
|
|
345
|
+
if (hasSurgical) {
|
|
346
|
+
out.surgical = surgical;
|
|
347
|
+
out.surgicalSavedPct = baseline > 0 ? Math.round((1 - surgical / baseline) * 1000) / 10 : 0;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function tokenReductionPanelHtml(tr) {
|
|
353
|
+
if (!tr) {
|
|
354
|
+
return '<div class="panel"><div class="label">Token Reduction</div>' +
|
|
355
|
+
'<div class="value" style="font-size:13px">No token-reduction benchmark found — run the token benchmark to populate this panel.</div></div>';
|
|
356
|
+
}
|
|
357
|
+
const fmt = (n) => Number(n).toLocaleString('en-US');
|
|
358
|
+
const tiers = [
|
|
359
|
+
{ label: 'Whole-file baseline', value: fmt(tr.baseline) + ' tok' },
|
|
360
|
+
{ label: 'Ranked signatures (ask)', value: fmt(tr.signatures) + ' tok' },
|
|
361
|
+
];
|
|
362
|
+
if (tr.surgical != null) {
|
|
363
|
+
tiers.push({ label: 'Surgical (index + delta)', value: fmt(tr.surgical) + ' tok' });
|
|
364
|
+
tiers.push({ label: 'Saved (surgical)', value: tr.surgicalSavedPct + '%' });
|
|
365
|
+
} else {
|
|
366
|
+
tiers.push({ label: 'Saved', value: tr.savedPct + '%' });
|
|
367
|
+
}
|
|
368
|
+
const tierHtml = tiers.map((t) =>
|
|
369
|
+
`<div class="card"><div class="label">${escapeAttr(t.label)}</div><div class="value">${escapeAttr(t.value)}</div></div>`
|
|
370
|
+
).join('');
|
|
371
|
+
|
|
372
|
+
// Proportional comparison bar (baseline = full width).
|
|
373
|
+
const sigPct = tr.baseline > 0 ? Math.max(0.4, (tr.signatures / tr.baseline) * 100) : 0;
|
|
374
|
+
const surgPct = (tr.surgical != null && tr.baseline > 0) ? Math.max(0.4, (tr.surgical / tr.baseline) * 100) : null;
|
|
375
|
+
const barRow = (label, pct, color) =>
|
|
376
|
+
`<div style="margin:4px 0;font-size:11px;color:#8ea0d9">${escapeAttr(label)}</div>` +
|
|
377
|
+
`<div style="background:#0a0f1e;border:1px solid #223056;border-radius:6px;height:14px;overflow:hidden">` +
|
|
378
|
+
`<div style="width:${pct.toFixed(1)}%;height:100%;background:${color}"></div></div>`;
|
|
379
|
+
const bars = [
|
|
380
|
+
barRow('Whole-file baseline (100%)', 100, '#3a4a78'),
|
|
381
|
+
barRow(`Ranked signatures — ${tr.savedPct}% saved`, sigPct, '#2e7d6b'),
|
|
382
|
+
surgPct != null ? barRow(`Surgical — ${tr.surgicalSavedPct}% saved`, surgPct, '#5ad1a8') : '',
|
|
383
|
+
].filter(Boolean).join('');
|
|
384
|
+
|
|
385
|
+
const rows = tr.perRepo.slice(0, 8).map((r) =>
|
|
386
|
+
`<tr><td>${escapeAttr(r.repo)}</td><td>${escapeAttr(r.language)}</td>` +
|
|
387
|
+
`<td style="text-align:right">${fmt(r.rawTokens)}</td>` +
|
|
388
|
+
`<td style="text-align:right">${fmt(r.finalTokens)}</td>` +
|
|
389
|
+
`<td style="text-align:right">${r.reductionPct}%</td></tr>`
|
|
390
|
+
).join('');
|
|
391
|
+
|
|
392
|
+
return [
|
|
393
|
+
'<div class="panel">',
|
|
394
|
+
`<div class="label">Token Reduction — ${tr.repoCount} benchmark repos${tr.version ? ' · v' + escapeAttr(tr.version) : ''}</div>`,
|
|
395
|
+
`<div class="grid" style="margin:8px 0">${tierHtml}</div>`,
|
|
396
|
+
bars,
|
|
397
|
+
'<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:12px">',
|
|
398
|
+
'<thead><tr style="color:#8ea0d9;text-align:left">',
|
|
399
|
+
'<th>Repo</th><th>Lang</th><th style="text-align:right">Baseline</th><th style="text-align:right">Signatures</th><th style="text-align:right">Saved</th>',
|
|
400
|
+
'</tr></thead>',
|
|
401
|
+
`<tbody>${rows}</tbody>`,
|
|
402
|
+
'</table>',
|
|
403
|
+
'</div>',
|
|
404
|
+
].join('');
|
|
405
|
+
}
|
|
406
|
+
|
|
307
407
|
function buildDashboardData(cwd, health) {
|
|
308
408
|
const entries = readLog(cwd);
|
|
309
409
|
const recent = entries.slice(-30);
|
|
@@ -326,15 +426,19 @@ function buildDashboardData(cwd, health) {
|
|
|
326
426
|
extractorCoverage: coverage.pct,
|
|
327
427
|
};
|
|
328
428
|
|
|
429
|
+
const tokenReduction = readTokenReduction(cwd);
|
|
430
|
+
|
|
329
431
|
return {
|
|
330
432
|
summary,
|
|
331
433
|
tokenReductionTrend,
|
|
332
434
|
hitAt5Trend,
|
|
333
435
|
coverage,
|
|
436
|
+
tokenReduction,
|
|
334
437
|
charts: {
|
|
335
438
|
tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
|
|
336
439
|
hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
|
|
337
440
|
coverageSvg: barChartSvg(coverage.perLanguage),
|
|
441
|
+
tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
|
|
338
442
|
},
|
|
339
443
|
};
|
|
340
444
|
}
|
|
@@ -379,6 +483,7 @@ function generateDashboardHtml(cwd, health) {
|
|
|
379
483
|
'<h1>SigMap v2.10 dashboard</h1>',
|
|
380
484
|
'<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
|
|
381
485
|
`<div class="grid">${cardHtml}</div>`,
|
|
486
|
+
data.charts.tokenSavingsPanel,
|
|
382
487
|
`<div class="panel">${data.charts.tokenReductionSvg}</div>`,
|
|
383
488
|
`<div class="panel">${data.charts.hitAt5Svg}</div>`,
|
|
384
489
|
`<div class="panel">${data.charts.coverageSvg}</div>`,
|
package/src/mcp/handlers.js
CHANGED
|
@@ -448,4 +448,61 @@ function getImpact(args, cwd) {
|
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
/**
|
|
452
|
+
* get_lines({ file, start, end }) → string
|
|
453
|
+
*
|
|
454
|
+
* Surgical Context demand-driven fetch: returns an exact, clamped line range from a
|
|
455
|
+
* source file. The path is resolved inside the project root (no traversal escape) and
|
|
456
|
+
* the returned lines are secret-scanned via the same redactor used for signatures.
|
|
457
|
+
*/
|
|
458
|
+
function getLines(args, cwd) {
|
|
459
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
460
|
+
|
|
461
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
462
|
+
const abs = path.resolve(cwd, rel);
|
|
463
|
+
|
|
464
|
+
// Sandbox: refuse paths that resolve outside the project root.
|
|
465
|
+
const root = path.resolve(cwd);
|
|
466
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
467
|
+
return `Refused: ${rel} resolves outside the project root`;
|
|
468
|
+
}
|
|
469
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
470
|
+
return `File not found: ${rel}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const start = parseInt(args.start, 10);
|
|
474
|
+
const end = parseInt(args.end, 10);
|
|
475
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
476
|
+
return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let lines;
|
|
480
|
+
try {
|
|
481
|
+
lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
482
|
+
} catch (err) {
|
|
483
|
+
return `Could not read ${rel}: ${err.message}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const total = lines.length;
|
|
487
|
+
const from = Math.max(1, Math.min(start, end));
|
|
488
|
+
const to = Math.min(total, Math.max(start, end));
|
|
489
|
+
if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
|
|
490
|
+
|
|
491
|
+
const slice = lines.slice(from - 1, to);
|
|
492
|
+
|
|
493
|
+
// Redaction scan: reuse the signature secret scanner line-by-line.
|
|
494
|
+
let safeLines = slice;
|
|
495
|
+
try {
|
|
496
|
+
const { scan } = require('../security/scanner');
|
|
497
|
+
safeLines = scan(slice, rel).safe;
|
|
498
|
+
} catch (_) {} // non-fatal: fall back to raw slice
|
|
499
|
+
|
|
500
|
+
return [
|
|
501
|
+
`# ${rel}:${from}-${to}`,
|
|
502
|
+
'```',
|
|
503
|
+
...safeLines,
|
|
504
|
+
'```',
|
|
505
|
+
].join('\n');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
|
package/src/mcp/server.js
CHANGED
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Supported methods:
|
|
10
10
|
* initialize → serverInfo + capabilities
|
|
11
|
-
* tools/list →
|
|
11
|
+
* tools/list → 10 tool definitions
|
|
12
12
|
* tools/call → dispatch to handler, return result
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { TOOLS } = require('./tools');
|
|
17
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '6.
|
|
21
|
+
version: '6.12.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -75,6 +75,7 @@ function dispatch(msg, cwd) {
|
|
|
75
75
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
76
76
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
77
77
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
78
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
78
79
|
else {
|
|
79
80
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
80
81
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* MCP tool definitions for SigMap.
|
|
5
|
-
*
|
|
4
|
+
* MCP tool definitions for SigMap (10 tools).
|
|
5
|
+
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6
|
+
* explain_file, list_modules, query_context, get_impact, get_lines.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const TOOLS = [
|
|
@@ -168,6 +169,34 @@ const TOOLS = [
|
|
|
168
169
|
required: ['file'],
|
|
169
170
|
},
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: 'get_lines',
|
|
174
|
+
description:
|
|
175
|
+
'Fetch an exact line range from a source file on demand — the Surgical Context ' +
|
|
176
|
+
'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
|
|
177
|
+
'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
|
|
178
|
+
'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
file: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description:
|
|
185
|
+
'Relative path from the project root (e.g. "src/config/loader.js"). ' +
|
|
186
|
+
'Use the path shown in a signature anchor. Use forward slashes.',
|
|
187
|
+
},
|
|
188
|
+
start: {
|
|
189
|
+
type: 'number',
|
|
190
|
+
description: '1-based start line (inclusive). Clamped to the file bounds.',
|
|
191
|
+
},
|
|
192
|
+
end: {
|
|
193
|
+
type: 'number',
|
|
194
|
+
description: '1-based end line (inclusive). Clamped to the file bounds.',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ['file', 'start', 'end'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
171
200
|
];
|
|
172
201
|
|
|
173
202
|
module.exports = { TOOLS };
|