sigmap 6.11.0 → 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 +26 -0
- package/README.md +10 -10
- package/gen-context.js +374 -94
- 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,32 @@ 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
|
+
|
|
31
|
+
## [6.11.1] — 2026-06-04
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **MCP hot-cold cold signatures in bundled server** — the bundled MCP server now includes the hot-cold "cold" signatures, so context lookups return complete results under the hot-cold strategy (closes #201, PR #216). Thanks @rudi193-cmd.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
13
39
|
## [6.11.0] — 2026-06-03
|
|
14
40
|
|
|
15
41
|
### Added
|
package/README.md
CHANGED
|
@@ -47,10 +47,10 @@ SigMap extracts function and class signatures from your codebase and feeds the r
|
|
|
47
47
|
|
|
48
48
|
## Why SigMap?
|
|
49
49
|
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **1.66 prompts per task** — down from 2.84 (
|
|
50
|
+
- **81.1% hit@5** — right file found in top 5 results (vs 13.6% baseline)
|
|
51
|
+
- **96.5% token reduction** — average across 21 real repos
|
|
52
|
+
- **53.3% task success rate** — up from 10% without context
|
|
53
|
+
- **1.66 prompts per task** — down from 2.84 (41.8% fewer retries)
|
|
54
54
|
- **31 languages supported** — TypeScript, Python, Go, Rust, Java, R, and 25 others
|
|
55
55
|
- **No vendor lock-in** — works with any AI assistant or local LLM
|
|
56
56
|
- **No API costs** — use local models (Ollama, llama.cpp, vLLM) with zero token fees
|
|
@@ -63,7 +63,7 @@ SigMap extracts function and class signatures from your codebase and feeds the r
|
|
|
63
63
|
|
|
64
64
|
| Without SigMap | With SigMap |
|
|
65
65
|
|---|---|
|
|
66
|
-
| ❌ Guessing which files are relevant | ✅ Right file in context —
|
|
66
|
+
| ❌ Guessing which files are relevant | ✅ Right file in context — 81% of the time |
|
|
67
67
|
| ❌ Sending the full repo to your AI | ✅ Minimal context — only what matters |
|
|
68
68
|
| ❌ Embeddings / vector DB required | ✅ Grounded answers, no infra needed |
|
|
69
69
|
|
|
@@ -87,12 +87,12 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
87
87
|
## Benchmark
|
|
88
88
|
|
|
89
89
|
```
|
|
90
|
-
Benchmark : sigmap-v6.
|
|
91
|
-
Date : 2026-05
|
|
90
|
+
Benchmark : sigmap-v6.12-main (21 repositories, including R language)
|
|
91
|
+
Date : 2026-06-05
|
|
92
92
|
|
|
93
|
-
Hit@5 :
|
|
93
|
+
Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
|
|
94
94
|
Token reduction: 96.5% (across 21 repos)
|
|
95
|
-
Prompt reduction : 41.
|
|
95
|
+
Prompt reduction : 41.8% (2.84 → 1.66 prompts per task)
|
|
96
96
|
Task success : 53.3% (baseline 10%)
|
|
97
97
|
Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
|
|
98
98
|
```
|
|
@@ -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
|
@@ -1174,6 +1174,30 @@ __factories["./src/extractors/php"] = function(module, exports) {
|
|
|
1174
1174
|
|
|
1175
1175
|
};
|
|
1176
1176
|
|
|
1177
|
+
// ── ./src/extractors/line-anchor ──
|
|
1178
|
+
__factories["./src/extractors/line-anchor"] = function(module, exports) {
|
|
1179
|
+
|
|
1180
|
+
function lineAt(src, idx) {
|
|
1181
|
+
let line = 1;
|
|
1182
|
+
const end = Math.min(idx, src.length);
|
|
1183
|
+
for (let i = 0; i < end; i++) {
|
|
1184
|
+
if (src.charCodeAt(i) === 10) line++;
|
|
1185
|
+
}
|
|
1186
|
+
return line;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function anchor(start, end) {
|
|
1190
|
+
return ` :${start}-${end}`;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function withAnchor(sig, start, end) {
|
|
1194
|
+
return `${sig}${anchor(start, end)}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
module.exports = { lineAt, anchor, withAnchor };
|
|
1198
|
+
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1177
1201
|
// ── ./src/extractors/python ──
|
|
1178
1202
|
__factories["./src/extractors/python"] = function(module, exports) {
|
|
1179
1203
|
|
|
@@ -4028,6 +4052,96 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4028
4052
|
}).join('');
|
|
4029
4053
|
}
|
|
4030
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
|
+
|
|
4031
4145
|
function buildDashboardData(cwd, health) {
|
|
4032
4146
|
const entries = readLog(cwd);
|
|
4033
4147
|
const recent = entries.slice(-30);
|
|
@@ -4046,15 +4160,18 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4046
4160
|
overBudgetStreak: overBudgetStreak(entries),
|
|
4047
4161
|
extractorCoverage: coverage.pct,
|
|
4048
4162
|
};
|
|
4163
|
+
const tokenReduction = readTokenReduction(cwd);
|
|
4049
4164
|
return {
|
|
4050
4165
|
summary,
|
|
4051
4166
|
tokenReductionTrend,
|
|
4052
4167
|
hitAt5Trend,
|
|
4053
4168
|
coverage,
|
|
4169
|
+
tokenReduction,
|
|
4054
4170
|
charts: {
|
|
4055
4171
|
tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
|
|
4056
4172
|
hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
|
|
4057
4173
|
coverageSvg: barChartSvg(coverage.perLanguage),
|
|
4174
|
+
tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
|
|
4058
4175
|
},
|
|
4059
4176
|
};
|
|
4060
4177
|
}
|
|
@@ -4088,6 +4205,7 @@ __factories["./src/format/benchmark-report"] = function(module, exports) {
|
|
|
4088
4205
|
'<h1>SigMap v2.10 dashboard</h1>',
|
|
4089
4206
|
'<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
|
|
4090
4207
|
`<div class="grid">${cardHtml}</div>`,
|
|
4208
|
+
data.charts.tokenSavingsPanel,
|
|
4091
4209
|
`<div class="panel">${data.charts.tokenReductionSvg}</div>`,
|
|
4092
4210
|
`<div class="panel">${data.charts.hitAt5Svg}</div>`,
|
|
4093
4211
|
`<div class="panel">${data.charts.coverageSvg}</div>`,
|
|
@@ -5339,42 +5457,29 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5339
5457
|
*/
|
|
5340
5458
|
function searchSignatures(args, cwd) {
|
|
5341
5459
|
if (!args || !args.query) return 'Missing required argument: query';
|
|
5342
|
-
|
|
5343
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
5344
|
-
if (!fs.existsSync(contextPath)) {
|
|
5345
|
-
return 'No context file found. Run: node gen-context.js';
|
|
5346
|
-
}
|
|
5347
|
-
|
|
5348
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
5349
5460
|
const query = args.query.toLowerCase();
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
for (const line of lines) {
|
|
5357
|
-
if (line.startsWith('### ')) {
|
|
5358
|
-
currentFile = line.slice(4).trim();
|
|
5359
|
-
fileHeaderAdded = false;
|
|
5360
|
-
continue;
|
|
5361
|
-
}
|
|
5362
|
-
// Skip markdown fences and top-level headers
|
|
5363
|
-
if (line.startsWith('```') || line.startsWith('## ') || line.startsWith('# ') || line.startsWith('<!--')) {
|
|
5364
|
-
continue;
|
|
5461
|
+
|
|
5462
|
+
try {
|
|
5463
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5464
|
+
const index = buildSigIndex(cwd);
|
|
5465
|
+
if (index.size === 0) {
|
|
5466
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5365
5467
|
}
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
result.push(
|
|
5468
|
+
|
|
5469
|
+
const result = [];
|
|
5470
|
+
for (const [file, sigs] of index.entries()) {
|
|
5471
|
+
const hits = sigs.filter((s) => s.toLowerCase().includes(query));
|
|
5472
|
+
if (hits.length === 0) continue;
|
|
5473
|
+
if (result.length > 0) result.push('');
|
|
5474
|
+
result.push(`### ${file}`);
|
|
5475
|
+
result.push(...hits);
|
|
5373
5476
|
}
|
|
5477
|
+
|
|
5478
|
+
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
5479
|
+
return result.join('\n');
|
|
5480
|
+
} catch (err) {
|
|
5481
|
+
return `_search_signatures failed: ${err.message}_`;
|
|
5374
5482
|
}
|
|
5375
|
-
|
|
5376
|
-
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
5377
|
-
return result.join('\n');
|
|
5378
5483
|
}
|
|
5379
5484
|
|
|
5380
5485
|
/**
|
|
@@ -5640,56 +5745,44 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5640
5745
|
}
|
|
5641
5746
|
|
|
5642
5747
|
function listModules(args, cwd) {
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
let blockBuf = [];
|
|
5653
|
-
|
|
5654
|
-
function flushBlock() {
|
|
5655
|
-
if (currentGroup === null || blockBuf.length === 0) return;
|
|
5656
|
-
if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
|
|
5657
|
-
groups[currentGroup].fileCount++;
|
|
5658
|
-
groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
|
|
5659
|
-
blockBuf = [];
|
|
5660
|
-
}
|
|
5661
|
-
|
|
5662
|
-
for (const line of ctxLines) {
|
|
5663
|
-
if (line.startsWith('### ')) {
|
|
5664
|
-
flushBlock();
|
|
5665
|
-
const rel = line.slice(4).trim().replace(/\\/g, '/');
|
|
5748
|
+
try {
|
|
5749
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5750
|
+
const index = buildSigIndex(cwd);
|
|
5751
|
+
if (index.size === 0) {
|
|
5752
|
+
return 'No context file found. Run: node gen-context.js';
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
const groups = {};
|
|
5756
|
+
for (const [rel, sigs] of index.entries()) {
|
|
5666
5757
|
const parts = rel.split('/');
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5758
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
5759
|
+
if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
|
|
5760
|
+
groups[mod].fileCount++;
|
|
5761
|
+
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
5670
5762
|
}
|
|
5763
|
+
|
|
5764
|
+
const sorted = Object.entries(groups)
|
|
5765
|
+
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
5766
|
+
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
5767
|
+
|
|
5768
|
+
if (sorted.length === 0) return 'No modules found in context file.';
|
|
5769
|
+
|
|
5770
|
+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
5771
|
+
|
|
5772
|
+
return [
|
|
5773
|
+
'# Modules',
|
|
5774
|
+
'',
|
|
5775
|
+
'| Module | Files | Tokens |',
|
|
5776
|
+
'|--------|-------|--------|',
|
|
5777
|
+
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
|
|
5778
|
+
'',
|
|
5779
|
+
`**Total context tokens: ~${total}**`,
|
|
5780
|
+
'',
|
|
5781
|
+
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
5782
|
+
].join('\n');
|
|
5783
|
+
} catch (err) {
|
|
5784
|
+
return `_list_modules failed: ${err.message}_`;
|
|
5671
5785
|
}
|
|
5672
|
-
flushBlock();
|
|
5673
|
-
|
|
5674
|
-
const sorted = Object.entries(groups)
|
|
5675
|
-
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
5676
|
-
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
5677
|
-
|
|
5678
|
-
if (sorted.length === 0) return 'No modules found in context file.';
|
|
5679
|
-
|
|
5680
|
-
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
5681
|
-
|
|
5682
|
-
return [
|
|
5683
|
-
'# Modules',
|
|
5684
|
-
'',
|
|
5685
|
-
'| Module | Files | Tokens |',
|
|
5686
|
-
'|--------|-------|--------|',
|
|
5687
|
-
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
|
|
5688
|
-
'',
|
|
5689
|
-
`**Total context tokens: ~${total}**`,
|
|
5690
|
-
'',
|
|
5691
|
-
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
5692
|
-
].join('\n');
|
|
5693
5786
|
}
|
|
5694
5787
|
|
|
5695
5788
|
function queryContext(args, cwd) {
|
|
@@ -5720,7 +5813,56 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5720
5813
|
}
|
|
5721
5814
|
}
|
|
5722
5815
|
|
|
5723
|
-
|
|
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 };
|
|
5724
5866
|
};
|
|
5725
5867
|
|
|
5726
5868
|
// ── ./src/learning/weights ──
|
|
@@ -5891,17 +6033,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5891
6033
|
*
|
|
5892
6034
|
* Supported methods:
|
|
5893
6035
|
* initialize → serverInfo + capabilities
|
|
5894
|
-
* tools/list →
|
|
6036
|
+
* tools/list → 10 tool definitions
|
|
5895
6037
|
* tools/call → dispatch to handler, return result
|
|
5896
6038
|
*/
|
|
5897
|
-
|
|
6039
|
+
|
|
5898
6040
|
const readline = require('readline');
|
|
5899
6041
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
5900
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = __require('./src/mcp/handlers');
|
|
5901
|
-
|
|
6042
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = __require('./src/mcp/handlers');
|
|
6043
|
+
|
|
5902
6044
|
const SERVER_INFO = {
|
|
5903
6045
|
name: 'sigmap',
|
|
5904
|
-
version: '6.
|
|
6046
|
+
version: '6.12.0',
|
|
5905
6047
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5906
6048
|
};
|
|
5907
6049
|
|
|
@@ -5958,6 +6100,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5958
6100
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
5959
6101
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
5960
6102
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
6103
|
+
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
5961
6104
|
else {
|
|
5962
6105
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
5963
6106
|
return;
|
|
@@ -6185,8 +6328,36 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
6185
6328
|
required: ['file'],
|
|
6186
6329
|
},
|
|
6187
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
|
+
},
|
|
6188
6359
|
];
|
|
6189
|
-
|
|
6360
|
+
|
|
6190
6361
|
module.exports = { TOOLS };
|
|
6191
6362
|
|
|
6192
6363
|
};
|
|
@@ -6968,9 +7139,50 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
6968
7139
|
if (currentFile !== null) index.set(currentFile, sigs);
|
|
6969
7140
|
return index;
|
|
6970
7141
|
}
|
|
7142
|
+
|
|
7143
|
+
function _mergeSigIndex(target, source) {
|
|
7144
|
+
for (const [file, sigs] of source.entries()) {
|
|
7145
|
+
if (!sigs || sigs.length === 0) continue;
|
|
7146
|
+
if (!target.has(file) || target.get(file).length < sigs.length) {
|
|
7147
|
+
target.set(file, sigs);
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
7150
|
+
return target;
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
function _buildSigIndexFromCache(cwd) {
|
|
7154
|
+
const fs = require('fs');
|
|
7155
|
+
const path = require('path');
|
|
7156
|
+
const index = new Map();
|
|
7157
|
+
try {
|
|
7158
|
+
const { loadCache } = require('../cache/sig-cache');
|
|
7159
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
7160
|
+
let version = '0.0.0';
|
|
7161
|
+
if (fs.existsSync(pkgPath)) {
|
|
7162
|
+
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || version;
|
|
7163
|
+
}
|
|
7164
|
+
const cache = loadCache(cwd, version);
|
|
7165
|
+
for (const [absPath, entry] of cache.entries()) {
|
|
7166
|
+
if (!entry || !entry.sigs || entry.sigs.length === 0) continue;
|
|
7167
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, '/');
|
|
7168
|
+
if (!rel || rel.startsWith('..')) continue;
|
|
7169
|
+
index.set(rel, entry.sigs);
|
|
7170
|
+
}
|
|
7171
|
+
} catch (_) {}
|
|
7172
|
+
return index;
|
|
7173
|
+
}
|
|
7174
|
+
|
|
7175
|
+
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
7176
|
+
const path = require('path');
|
|
7177
|
+
const coldPath = path.join(cwd, '.github', 'context-cold.md');
|
|
7178
|
+
_mergeSigIndex(index, _parseContextFile(coldPath));
|
|
7179
|
+
_mergeSigIndex(index, _buildSigIndexFromCache(cwd));
|
|
7180
|
+
return index;
|
|
7181
|
+
}
|
|
7182
|
+
|
|
6971
7183
|
function buildSigIndex(cwd, opts) {
|
|
6972
7184
|
const fs = require('fs'); const path = require('path');
|
|
6973
|
-
if (opts && opts.contextPath) return _parseContextFile(opts.contextPath);
|
|
7185
|
+
if (opts && opts.contextPath) return _enrichSigIndexFromStrategy(cwd, _parseContextFile(opts.contextPath));
|
|
6974
7186
|
// Check gen-context.config.json for a persisted customOutput path.
|
|
6975
7187
|
try {
|
|
6976
7188
|
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
@@ -6978,16 +7190,16 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
6978
7190
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
6979
7191
|
if (cfg.customOutput) {
|
|
6980
7192
|
const idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
|
|
6981
|
-
if (idx.size > 0) return idx;
|
|
7193
|
+
if (idx.size > 0) return _enrichSigIndexFromStrategy(cwd, idx);
|
|
6982
7194
|
}
|
|
6983
7195
|
}
|
|
6984
7196
|
} catch (_) {}
|
|
6985
7197
|
for (const parts of ADAPTER_OUTPUT_PATHS) {
|
|
6986
7198
|
const contextPath = path.join(cwd, ...parts);
|
|
6987
7199
|
const index = _parseContextFile(contextPath);
|
|
6988
|
-
if (index.size > 0) return index;
|
|
7200
|
+
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
6989
7201
|
}
|
|
6990
|
-
return new Map();
|
|
7202
|
+
return _enrichSigIndexFromStrategy(cwd, new Map());
|
|
6991
7203
|
}
|
|
6992
7204
|
function formatRankTable(results, query) {
|
|
6993
7205
|
if (!results || results.length === 0) return `No matching files found for query: "${query}"\n`;
|
|
@@ -8655,7 +8867,7 @@ const path = require('path');
|
|
|
8655
8867
|
const os = require('os');
|
|
8656
8868
|
const { execSync } = require('child_process');
|
|
8657
8869
|
|
|
8658
|
-
const VERSION = '6.
|
|
8870
|
+
const VERSION = '6.12.0';
|
|
8659
8871
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8660
8872
|
|
|
8661
8873
|
function requireSourceOrBundled(key) {
|
|
@@ -8947,8 +9159,28 @@ function applyTokenBudget(fileEntries, maxTokens) {
|
|
|
8947
9159
|
let total = fileEntries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
|
|
8948
9160
|
if (total <= effectiveBudget) return fileEntries;
|
|
8949
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
|
+
|
|
8950
9182
|
// Sort by drop priority (drop first = index 0)
|
|
8951
|
-
const withPriority =
|
|
9183
|
+
const withPriority = working.map((e) => {
|
|
8952
9184
|
let priority = 0;
|
|
8953
9185
|
let dropReason = 'budget: low recency';
|
|
8954
9186
|
if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
|
|
@@ -10536,6 +10768,40 @@ function buildMiniContext(ranked, cwd) {
|
|
|
10536
10768
|
return lines.join('\n');
|
|
10537
10769
|
}
|
|
10538
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
|
+
|
|
10539
10805
|
function computeCurrentRisk(cwd) {
|
|
10540
10806
|
try {
|
|
10541
10807
|
const { execSync } = require('child_process');
|
|
@@ -10706,7 +10972,7 @@ function main() {
|
|
|
10706
10972
|
const mode = modeIdx !== -1
|
|
10707
10973
|
? args[modeIdx + 1]
|
|
10708
10974
|
: (config.mode || 'default');
|
|
10709
|
-
const VALID_MODES = ['default', 'fast', 'full', 'both'];
|
|
10975
|
+
const VALID_MODES = ['default', 'fast', 'full', 'both', 'index'];
|
|
10710
10976
|
if (mode && !VALID_MODES.includes(mode)) {
|
|
10711
10977
|
console.error(`[sigmap] unknown --mode "${mode}". Valid: ${VALID_MODES.join(', ')}`);
|
|
10712
10978
|
process.exit(1);
|
|
@@ -10785,9 +11051,23 @@ function main() {
|
|
|
10785
11051
|
}
|
|
10786
11052
|
}
|
|
10787
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
|
+
|
|
10788
11066
|
// v6.8: Save session for future --followup calls
|
|
10789
11067
|
saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
|
|
10790
|
-
|
|
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);
|
|
10791
11071
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
10792
11072
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
10793
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 };
|