sigmap 7.25.2 → 7.27.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 +20 -0
- package/README.md +3 -3
- package/gen-context.js +556 -7
- package/llms-full.txt +23 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/evidence/pack.js +267 -0
- package/src/mcp/handlers.js +176 -1
- package/src/mcp/server.js +5 -3
- package/src/mcp/tools.js +40 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,26 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.27.0] — 2026-06-22
|
|
14
|
+
|
|
15
|
+
Minor release — **v8.0 D3:** two new MCP tools, taking the server from 15 to 17 tools. Both are composed from data SigMap already computes — zero new runtime dependencies, no system-shell spawns.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`get_diff_context` MCP tool (#376):** for every changed file (working tree, staged via `staged`, or vs a base ref via `base`) returns its current **signatures** + **blast radius** (direct importers, transitive count, affected tests/routes) + a risk label — one call gives an agent everything a review or a safe edit needs. Lists changed files **shell-free** through `src/util/git.js` (no `/bin/sh`). Optional `depth` controls the blast-radius BFS.
|
|
19
|
+
- **`get_architecture_overview` MCP tool (#376):** a one-call codebase map — module breakdown (files/tokens), the most depended-on **hub files**, the dependency-**cycle** count, and route totals. Extends `get_map` for orienting in an unfamiliar repo. Composed from `buildSigIndex`, `buildFromCwd`, and `detectCycles`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- MCP surface count is now **17 tools** across `--help`, README, `docs-vp/guide/mcp.md`, `version.json` (`mcp_tools`), and the generated `llms.txt`/`llms-full.txt`.
|
|
23
|
+
|
|
24
|
+
## [7.26.0] — 2026-06-22
|
|
25
|
+
|
|
26
|
+
Minor release — **v8.0 "The Evidence Pack & the Pivot" (E1):** the keystone artifact that makes SigMap consumable by machines instead of copy-paste.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Evidence Pack JSON v1 (#372):** new `sigmap evidence "<query>"` command emits a deterministic, machine-consumable signature-and-evidence map — a byte-stable JSON artifact (plus a `--markdown`/`--md` handoff rendering) that an agent or CI can ingest directly, every entry anchored to a real file, symbol, and line range. Schema v1: `{ schemaVersion, query, intent, files:[{ path, symbols, reason, confidence, sourceLines, relatedTests, riskLabel }], tokenBudget, droppedFiles, grounding:{ symbolCount, anchoredSymbols, anchorCoverage, contextHash, deterministic } }`. Composed entirely from shipped zero-dep modules (ranker, line-anchor parsing, security scanner, sha256 grounding hash). The pack carries **no wall-clock timestamp** — an unchanged repo yields byte-identical output and a stable `grounding.contextHash`, so the artifact is auditable. CLI flags: `--top`, `--budget`, `--out`; always writes `.context/evidence-pack.json`. `riskLabel` ∈ {generated, test, config, security, source} and `relatedTests` are best-effort v1 (measured test-discovery and richer labels land in v8.5).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
13
33
|
## [7.25.2] — 2026-06-22
|
|
14
34
|
|
|
15
35
|
Patch release — **Trust Hygiene (H2):** reproducible bundle build. Completes the v7.25.x "Trust Hygiene" milestone (H1+H2+H3+H4 all shipped).
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
91
91
|
|
|
92
92
|
<!--SM:benchmarkBlock-->
|
|
93
93
|
```
|
|
94
|
-
Benchmark : sigmap-v7.
|
|
94
|
+
Benchmark : sigmap-v7.27-main (21 repositories, including R language)
|
|
95
95
|
Date : 2026-06-22
|
|
96
96
|
|
|
97
97
|
Hit@5 : 75.6% (baseline 13.6% — 5.6× lift)
|
|
@@ -191,13 +191,13 @@ Use SigMap with open-source tools and fully self-hosted setups:
|
|
|
191
191
|
| **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 |
|
|
192
192
|
| **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
|
|
193
193
|
|
|
194
|
-
**MCP server** —
|
|
194
|
+
**MCP server** — 17 on-demand tools for Claude Code and Cursor:
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
197
|
sigmap --mcp
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
Tools: `read_context`, `search_signatures`, `get_map`, `create_checkpoint`, `get_routing`, `explain_file`, `list_modules`, `query_context`, `get_impact`, `get_lines`, `read_memory`, `get_callee_signatures`, plus the live-index notifications `sigmap_notify_file_created`, `sigmap_notify_symbol_added`, and `sigmap_notify_file_deleted`. Full reference: [llms-full.txt](llms-full.txt).
|
|
200
|
+
Tools: `read_context`, `search_signatures`, `get_map`, `create_checkpoint`, `get_routing`, `explain_file`, `list_modules`, `query_context`, `get_impact`, `get_lines`, `read_memory`, `get_callee_signatures`, `get_diff_context` (changed files + signatures + blast radius), `get_architecture_overview` (modules, hub files, cycles), plus the live-index notifications `sigmap_notify_file_created`, `sigmap_notify_symbol_added`, and `sigmap_notify_file_deleted`. Full reference: [llms-full.txt](llms-full.txt).
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
package/gen-context.js
CHANGED
|
@@ -4385,6 +4385,277 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
|
|
|
4385
4385
|
|
|
4386
4386
|
};
|
|
4387
4387
|
|
|
4388
|
+
// ── ./src/evidence/pack ──
|
|
4389
|
+
__factories["./src/evidence/pack"] = function(module, exports) {
|
|
4390
|
+
|
|
4391
|
+
/**
|
|
4392
|
+
* Evidence Pack v1 (v8.0 E1).
|
|
4393
|
+
*
|
|
4394
|
+
* A deterministic, machine-consumable signature-and-evidence map. Replaces the
|
|
4395
|
+
* "paste this into your prompt" workflow with a byte-stable JSON artifact that
|
|
4396
|
+
* an agent or CI can ingest directly — every entry anchored to a real file,
|
|
4397
|
+
* symbol, and line range.
|
|
4398
|
+
*
|
|
4399
|
+
* Composed entirely from shipped zero-dep modules:
|
|
4400
|
+
* - retrieval/ranker → ranked files, scores, signals
|
|
4401
|
+
* - extractors/line-anchor → `:start-end` suffix parsing (sourceLines)
|
|
4402
|
+
* - security/scanner → secret redaction of symbols
|
|
4403
|
+
* - crypto (node builtin) → sha256 grounding hash
|
|
4404
|
+
*
|
|
4405
|
+
* Determinism: the pack carries NO wall-clock timestamp. Given an unchanged
|
|
4406
|
+
* repository, `buildEvidencePack` returns a byte-identical object, and
|
|
4407
|
+
* `grounding.contextHash` is stable. This is the point — the pack is auditable.
|
|
4408
|
+
*/
|
|
4409
|
+
|
|
4410
|
+
const fs = require('fs');
|
|
4411
|
+
const path = require('path');
|
|
4412
|
+
const crypto = require('crypto');
|
|
4413
|
+
|
|
4414
|
+
const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
|
|
4415
|
+
const { scan } = __require('./src/security/scanner');
|
|
4416
|
+
|
|
4417
|
+
const SCHEMA_VERSION = '1.0';
|
|
4418
|
+
const DEFAULT_BUDGET = 6000;
|
|
4419
|
+
const DEFAULT_TOP = 12;
|
|
4420
|
+
|
|
4421
|
+
const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
|
|
4422
|
+
const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
|
|
4423
|
+
const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
|
|
4424
|
+
const SECURITY_RE = /(^|\/|[._-])(auth|authn|authz|login|password|passwd|secret|credential|token|session|crypto|cipher|payment|billing|checkout|oauth|jwt|permission|acl|rbac)([._-]|\/|$)/i;
|
|
4425
|
+
|
|
4426
|
+
/**
|
|
4427
|
+
* Split a signature's ` :start-end` line anchor from its symbol text.
|
|
4428
|
+
* @param {string} sig
|
|
4429
|
+
* @returns {{ symbol: string, start: number|null, end: number|null }}
|
|
4430
|
+
*/
|
|
4431
|
+
function parseAnchor(sig) {
|
|
4432
|
+
const m = /\s*:(\d+)-(\d+)\s*$/.exec(sig);
|
|
4433
|
+
if (!m) return { symbol: sig.trim(), start: null, end: null };
|
|
4434
|
+
return {
|
|
4435
|
+
symbol: sig.slice(0, m.index).trim(),
|
|
4436
|
+
start: parseInt(m[1], 10),
|
|
4437
|
+
end: parseInt(m[2], 10),
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
/**
|
|
4442
|
+
* Classify a file into a coarse risk label. Path-based heuristic (v1) — the
|
|
4443
|
+
* richer label set (C3) lands in v8.5.
|
|
4444
|
+
* @param {string} relPath
|
|
4445
|
+
* @returns {'generated'|'test'|'config'|'security'|'source'}
|
|
4446
|
+
*/
|
|
4447
|
+
function riskLabelFor(relPath) {
|
|
4448
|
+
const p = relPath.replace(/\\/g, '/');
|
|
4449
|
+
if (GENERATED_RE.test(p)) return 'generated';
|
|
4450
|
+
if (TEST_RE.test(p)) return 'test';
|
|
4451
|
+
if (SECURITY_RE.test(p)) return 'security';
|
|
4452
|
+
if (CONFIG_RE.test(p)) return 'config';
|
|
4453
|
+
return 'source';
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
/** Filename stem (basename minus the first extension chain). */
|
|
4457
|
+
function stemOf(relPath) {
|
|
4458
|
+
const base = path.basename(relPath);
|
|
4459
|
+
return base.replace(/\.[^.]+$/, '').replace(/\.(test|spec)$/i, '');
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
/**
|
|
4463
|
+
* Best-effort impl→test discovery (v1). Matches test files whose stem equals
|
|
4464
|
+
* the implementation file's stem, by common convention. Deterministic. The
|
|
4465
|
+
* accuracy-measured discovery (C2) lands in v8.5.
|
|
4466
|
+
* @param {string} relPath
|
|
4467
|
+
* @param {string[]} allFiles - universe of indexed files (relative paths)
|
|
4468
|
+
* @returns {string[]}
|
|
4469
|
+
*/
|
|
4470
|
+
function findRelatedTests(relPath, allFiles) {
|
|
4471
|
+
if (riskLabelFor(relPath) === 'test') return [];
|
|
4472
|
+
const stem = stemOf(relPath).toLowerCase();
|
|
4473
|
+
if (!stem) return [];
|
|
4474
|
+
const out = [];
|
|
4475
|
+
for (const f of allFiles) {
|
|
4476
|
+
if (f === relPath) continue;
|
|
4477
|
+
if (riskLabelFor(f) !== 'test') continue;
|
|
4478
|
+
if (stemOf(f).toLowerCase() === stem) out.push(f);
|
|
4479
|
+
}
|
|
4480
|
+
return out.sort();
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
/** Map a ranker `signals` object into a short human-readable reason string. */
|
|
4484
|
+
function reasonFor(signals) {
|
|
4485
|
+
if (!signals) return 'ranked match';
|
|
4486
|
+
const parts = [];
|
|
4487
|
+
if (signals.symbolMatch > 0) parts.push('symbol-name match');
|
|
4488
|
+
if (signals.exactToken > 0) parts.push('exact token match');
|
|
4489
|
+
if (signals.prefixMatch > 0) parts.push('prefix match');
|
|
4490
|
+
if (signals.pathMatch > 0) parts.push('path match');
|
|
4491
|
+
if (signals.graphBoost > 0) parts.push('dependency-graph neighbor');
|
|
4492
|
+
if (signals.recencyBoost > 1) parts.push('recently changed');
|
|
4493
|
+
if (signals.learnedWeights && signals.learnedWeights !== 1) parts.push('learned weight');
|
|
4494
|
+
return parts.length ? parts.join('; ') : 'ranked match';
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
/** Token estimate for a signature block (matches the ranker's heuristic). */
|
|
4498
|
+
function sigTokens(sigs) {
|
|
4499
|
+
return Math.ceil(sigs.join('\n').length / 4);
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
/**
|
|
4503
|
+
* Stable stringify with recursively sorted object keys, for hashing.
|
|
4504
|
+
* @param {*} value
|
|
4505
|
+
* @returns {string}
|
|
4506
|
+
*/
|
|
4507
|
+
function canonicalize(value) {
|
|
4508
|
+
return JSON.stringify(sortKeys(value));
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
function sortKeys(value) {
|
|
4512
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
4513
|
+
if (value && typeof value === 'object') {
|
|
4514
|
+
const out = {};
|
|
4515
|
+
for (const k of Object.keys(value).sort()) out[k] = sortKeys(value[k]);
|
|
4516
|
+
return out;
|
|
4517
|
+
}
|
|
4518
|
+
return value;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
/**
|
|
4522
|
+
* Build an Evidence Pack for a query.
|
|
4523
|
+
*
|
|
4524
|
+
* @param {string} query
|
|
4525
|
+
* @param {string} cwd
|
|
4526
|
+
* @param {object} [opts]
|
|
4527
|
+
* @param {number} [opts.budget=6000] - token budget for included files
|
|
4528
|
+
* @param {number} [opts.top=12] - max ranked files to consider
|
|
4529
|
+
* @param {Map<string,string[]>} [opts.sigIndex] - pre-built index (else built from cwd)
|
|
4530
|
+
* @returns {object} Evidence Pack v1
|
|
4531
|
+
*/
|
|
4532
|
+
function buildEvidencePack(query, cwd, opts = {}) {
|
|
4533
|
+
const budget = Number.isFinite(opts.budget) ? opts.budget : DEFAULT_BUDGET;
|
|
4534
|
+
const top = Number.isFinite(opts.top) ? opts.top : DEFAULT_TOP;
|
|
4535
|
+
|
|
4536
|
+
const sigIndex = opts.sigIndex instanceof Map ? opts.sigIndex : buildSigIndex(cwd);
|
|
4537
|
+
const intent = detectIntent(query);
|
|
4538
|
+
const allFiles = Array.from(sigIndex.keys());
|
|
4539
|
+
|
|
4540
|
+
const ranked = rank(query, sigIndex, { topK: top, cwd })
|
|
4541
|
+
.filter((r) => r.score > 0 || ranked0Empty(query));
|
|
4542
|
+
const maxScore = ranked.reduce((m, r) => Math.max(m, r.score), 0);
|
|
4543
|
+
|
|
4544
|
+
// Greedy budget fill in rank order; the remainder is reported as dropped.
|
|
4545
|
+
const files = [];
|
|
4546
|
+
const droppedFiles = [];
|
|
4547
|
+
let used = 0;
|
|
4548
|
+
|
|
4549
|
+
for (const r of ranked) {
|
|
4550
|
+
const tokens = sigTokens(r.sigs);
|
|
4551
|
+
if (files.length > 0 && used + tokens > budget) {
|
|
4552
|
+
droppedFiles.push({ path: r.file, reason: `budget: would exceed ${budget}-token limit` });
|
|
4553
|
+
continue;
|
|
4554
|
+
}
|
|
4555
|
+
used += tokens;
|
|
4556
|
+
|
|
4557
|
+
const safe = scan(r.sigs, r.file).safe;
|
|
4558
|
+
const symbols = [];
|
|
4559
|
+
const sourceLines = [];
|
|
4560
|
+
for (const sig of safe) {
|
|
4561
|
+
const { symbol, start, end } = parseAnchor(sig);
|
|
4562
|
+
symbols.push(symbol);
|
|
4563
|
+
if (start !== null) sourceLines.push({ symbol, start, end });
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
files.push({
|
|
4567
|
+
path: r.file,
|
|
4568
|
+
symbols,
|
|
4569
|
+
reason: reasonFor(r.signals),
|
|
4570
|
+
confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
|
|
4571
|
+
sourceLines,
|
|
4572
|
+
relatedTests: findRelatedTests(r.file, allFiles),
|
|
4573
|
+
riskLabel: riskLabelFor(r.file),
|
|
4574
|
+
});
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
const symbolCount = files.reduce((n, f) => n + f.symbols.length, 0);
|
|
4578
|
+
const anchoredSymbols = files.reduce((n, f) => n + f.sourceLines.length, 0);
|
|
4579
|
+
|
|
4580
|
+
const pack = {
|
|
4581
|
+
schemaVersion: SCHEMA_VERSION,
|
|
4582
|
+
query,
|
|
4583
|
+
intent,
|
|
4584
|
+
files,
|
|
4585
|
+
tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
|
|
4586
|
+
droppedFiles,
|
|
4587
|
+
grounding: {
|
|
4588
|
+
symbolCount,
|
|
4589
|
+
anchoredSymbols,
|
|
4590
|
+
anchorCoverage: symbolCount > 0 ? Math.round((anchoredSymbols / symbolCount) * 1000) / 1000 : 0,
|
|
4591
|
+
contextHash: null,
|
|
4592
|
+
deterministic: true,
|
|
4593
|
+
},
|
|
4594
|
+
};
|
|
4595
|
+
|
|
4596
|
+
// Hash everything except the hash field itself.
|
|
4597
|
+
const forHash = Object.assign({}, pack, {
|
|
4598
|
+
grounding: Object.assign({}, pack.grounding, { contextHash: undefined }),
|
|
4599
|
+
});
|
|
4600
|
+
pack.grounding.contextHash = 'sha256:' + crypto.createHash('sha256').update(canonicalize(forHash)).digest('hex');
|
|
4601
|
+
|
|
4602
|
+
return pack;
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
// rank() returns [] for an empty/whitespace query; keep the filter readable.
|
|
4606
|
+
function ranked0Empty(query) {
|
|
4607
|
+
return !query || !query.trim();
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
/** Pretty-printed canonical JSON rendering of a pack. */
|
|
4611
|
+
function formatJSON(pack) {
|
|
4612
|
+
return JSON.stringify(pack, null, 2);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
/** Markdown handoff rendering of a pack. */
|
|
4616
|
+
function formatMarkdown(pack) {
|
|
4617
|
+
const L = [];
|
|
4618
|
+
L.push(`# Evidence Pack — \`${pack.query}\``);
|
|
4619
|
+
L.push('');
|
|
4620
|
+
L.push(`- **Schema:** v${pack.schemaVersion}`);
|
|
4621
|
+
L.push(`- **Intent:** ${pack.intent}`);
|
|
4622
|
+
L.push(`- **Budget:** ${pack.tokenBudget.used} / ${pack.tokenBudget.limit} tokens used (${pack.tokenBudget.remaining} remaining)`);
|
|
4623
|
+
L.push(`- **Grounding:** ${pack.grounding.anchoredSymbols}/${pack.grounding.symbolCount} symbols anchored (${Math.round(pack.grounding.anchorCoverage * 100)}%)`);
|
|
4624
|
+
L.push(`- **Hash:** \`${pack.grounding.contextHash}\``);
|
|
4625
|
+
L.push('');
|
|
4626
|
+
|
|
4627
|
+
for (const f of pack.files) {
|
|
4628
|
+
L.push(`## \`${f.path}\` _(${f.riskLabel}, confidence ${f.confidence})_`);
|
|
4629
|
+
L.push(`_${f.reason}_`);
|
|
4630
|
+
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
|
|
4631
|
+
L.push('');
|
|
4632
|
+
L.push('```');
|
|
4633
|
+
for (const s of f.symbols) L.push(s);
|
|
4634
|
+
L.push('```');
|
|
4635
|
+
L.push('');
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
if (pack.droppedFiles.length) {
|
|
4639
|
+
L.push('## Dropped (over budget)');
|
|
4640
|
+
for (const d of pack.droppedFiles) L.push(`- \`${d.path}\` — ${d.reason}`);
|
|
4641
|
+
L.push('');
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
return L.join('\n');
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
module.exports = {
|
|
4648
|
+
buildEvidencePack,
|
|
4649
|
+
formatJSON,
|
|
4650
|
+
formatMarkdown,
|
|
4651
|
+
parseAnchor,
|
|
4652
|
+
riskLabelFor,
|
|
4653
|
+
findRelatedTests,
|
|
4654
|
+
SCHEMA_VERSION,
|
|
4655
|
+
};
|
|
4656
|
+
|
|
4657
|
+
};
|
|
4658
|
+
|
|
4388
4659
|
// ── ./src/extractors/coverage ──
|
|
4389
4660
|
__factories["./src/extractors/coverage"] = function(module, exports) {
|
|
4390
4661
|
|
|
@@ -11838,7 +12109,182 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
11838
12109
|
}
|
|
11839
12110
|
}
|
|
11840
12111
|
|
|
11841
|
-
|
|
12112
|
+
/**
|
|
12113
|
+
* List the files changed in the working tree, staged area, or vs a base ref.
|
|
12114
|
+
* Shell-free (routes through src/util/git.js). Returns relative paths.
|
|
12115
|
+
*/
|
|
12116
|
+
function _changedFiles(cwd, args) {
|
|
12117
|
+
const { tryGit } = __require('./src/util/git');
|
|
12118
|
+
let out = '';
|
|
12119
|
+
if (args.base) {
|
|
12120
|
+
if (!/^[A-Za-z0-9._/\-~^]+$/.test(args.base)) return [];
|
|
12121
|
+
out = tryGit(['diff', `${args.base}..HEAD`, '--name-only'], { cwd });
|
|
12122
|
+
} else if (args.staged) {
|
|
12123
|
+
out = tryGit(['diff', '--cached', '--name-only'], { cwd });
|
|
12124
|
+
} else {
|
|
12125
|
+
out = tryGit(['diff', 'HEAD', '--name-only'], { cwd });
|
|
12126
|
+
}
|
|
12127
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
12128
|
+
}
|
|
12129
|
+
|
|
12130
|
+
/**
|
|
12131
|
+
* get_diff_context({ base?, staged?, depth? }) → string
|
|
12132
|
+
*
|
|
12133
|
+
* For each changed file: its extracted signatures + blast radius (direct
|
|
12134
|
+
* importers, transitive count, affected tests/routes) + a risk label.
|
|
12135
|
+
* Hands an agent everything a review or a safe edit needs in one call.
|
|
12136
|
+
*/
|
|
12137
|
+
function getDiffContext(args, cwd) {
|
|
12138
|
+
try {
|
|
12139
|
+
const a = args || {};
|
|
12140
|
+
const files = _changedFiles(cwd, a);
|
|
12141
|
+
if (files.length === 0) {
|
|
12142
|
+
return '_No changed files detected._ (Outside a git repo, or the working tree / selected range is clean.)';
|
|
12143
|
+
}
|
|
12144
|
+
|
|
12145
|
+
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
12146
|
+
const { analyzeImpact } = __require('./src/graph/impact');
|
|
12147
|
+
let riskLabelFor;
|
|
12148
|
+
try { ({ riskLabelFor } = __require('./src/evidence/pack')); } catch (_) { riskLabelFor = () => 'source'; }
|
|
12149
|
+
|
|
12150
|
+
const depth = Math.max(0, parseInt(a.depth, 10) || 2);
|
|
12151
|
+
const srcFiles = files.filter((f) => langFor(f));
|
|
12152
|
+
let impactByFile = new Map();
|
|
12153
|
+
try {
|
|
12154
|
+
const impacts = analyzeImpact(srcFiles, cwd, { depth });
|
|
12155
|
+
impactByFile = new Map(impacts.map((r) => [r.file, r.impact]));
|
|
12156
|
+
} catch (_) { /* graph optional */ }
|
|
12157
|
+
|
|
12158
|
+
const scope = a.base ? `vs ${a.base}` : (a.staged ? 'staged' : 'working tree');
|
|
12159
|
+
const out = [
|
|
12160
|
+
`# Diff context (${scope})`,
|
|
12161
|
+
'',
|
|
12162
|
+
`**${files.length} changed file${files.length === 1 ? '' : 's'}** · ${srcFiles.length} with extractable signatures`,
|
|
12163
|
+
'',
|
|
12164
|
+
];
|
|
12165
|
+
|
|
12166
|
+
for (const rel of files) {
|
|
12167
|
+
out.push(`## \`${rel}\``);
|
|
12168
|
+
if (!langFor(rel)) { out.push('_non-source file (no signatures)_', ''); continue; }
|
|
12169
|
+
|
|
12170
|
+
out.push(`_risk: ${riskLabelFor(rel)}_`);
|
|
12171
|
+
const impact = impactByFile.get(rel);
|
|
12172
|
+
if (impact) {
|
|
12173
|
+
out.push(
|
|
12174
|
+
`**Blast radius:** ${impact.totalImpact} file(s) impacted — ` +
|
|
12175
|
+
`${impact.direct.length} direct importer(s), ${impact.transitive.length} transitive` +
|
|
12176
|
+
(impact.tests.length ? `, ${impact.tests.length} test(s)` : '') +
|
|
12177
|
+
(impact.routes.length ? `, ${impact.routes.length} route(s)` : '')
|
|
12178
|
+
);
|
|
12179
|
+
if (impact.direct.length) {
|
|
12180
|
+
out.push(`Direct importers: ${impact.direct.slice(0, 8).map((f) => '`' + f + '`').join(', ')}` + (impact.direct.length > 8 ? ' …' : ''));
|
|
12181
|
+
}
|
|
12182
|
+
if (impact.tests.length) {
|
|
12183
|
+
out.push(`Tests to run: ${impact.tests.slice(0, 8).map((f) => '`' + f + '`').join(', ')}`);
|
|
12184
|
+
}
|
|
12185
|
+
} else {
|
|
12186
|
+
out.push('**Blast radius:** (not in dependency graph — new or leaf file)');
|
|
12187
|
+
}
|
|
12188
|
+
out.push('');
|
|
12189
|
+
|
|
12190
|
+
let src = '';
|
|
12191
|
+
try { src = fs.readFileSync(path.resolve(cwd, rel), 'utf8'); } catch (_) {}
|
|
12192
|
+
const sigs = src ? extractFile(rel, src) : [];
|
|
12193
|
+
if (sigs.length) {
|
|
12194
|
+
out.push('```');
|
|
12195
|
+
for (const s of sigs.slice(0, 40)) out.push(s);
|
|
12196
|
+
if (sigs.length > 40) out.push(`… +${sigs.length - 40} more`);
|
|
12197
|
+
out.push('```');
|
|
12198
|
+
} else {
|
|
12199
|
+
out.push('_(no signatures extracted — file may be deleted or empty)_');
|
|
12200
|
+
}
|
|
12201
|
+
out.push('');
|
|
12202
|
+
}
|
|
12203
|
+
|
|
12204
|
+
return out.join('\n');
|
|
12205
|
+
} catch (err) {
|
|
12206
|
+
return `_get_diff_context failed: ${err.message}_`;
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
|
|
12210
|
+
/**
|
|
12211
|
+
* get_architecture_overview({}) → string
|
|
12212
|
+
*
|
|
12213
|
+
* A high-level map of the codebase: module breakdown (files/tokens), the most
|
|
12214
|
+
* depended-on "hub" files, dependency-cycle count, and route totals. Extends
|
|
12215
|
+
* get_map — one call to orient in an unfamiliar repo.
|
|
12216
|
+
*/
|
|
12217
|
+
function getArchitectureOverview(args, cwd) {
|
|
12218
|
+
try {
|
|
12219
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
12220
|
+
const index = buildSigIndex(cwd);
|
|
12221
|
+
const out = ['# Architecture overview', ''];
|
|
12222
|
+
|
|
12223
|
+
if (index.size === 0) {
|
|
12224
|
+
out.push('_No context file found. Run: node gen-context.js_', '');
|
|
12225
|
+
} else {
|
|
12226
|
+
const groups = {};
|
|
12227
|
+
let totalTokens = 0;
|
|
12228
|
+
let totalFiles = 0;
|
|
12229
|
+
for (const [rel, sigs] of index.entries()) {
|
|
12230
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
12231
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
12232
|
+
const tok = Math.ceil(sigs.join('\n').length / 4);
|
|
12233
|
+
if (!groups[mod]) groups[mod] = { files: 0, tokens: 0 };
|
|
12234
|
+
groups[mod].files++;
|
|
12235
|
+
groups[mod].tokens += tok;
|
|
12236
|
+
totalTokens += tok;
|
|
12237
|
+
totalFiles++;
|
|
12238
|
+
}
|
|
12239
|
+
const sorted = Object.entries(groups)
|
|
12240
|
+
.map(([mod, d]) => ({ mod, files: d.files, tokens: d.tokens }))
|
|
12241
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
12242
|
+
|
|
12243
|
+
out.push(`**${totalFiles} indexed files · ${sorted.length} modules · ~${totalTokens} tokens**`, '');
|
|
12244
|
+
out.push('## Modules', '| Module | Files | Tokens |', '|--------|-------|--------|');
|
|
12245
|
+
for (const m of sorted.slice(0, 20)) out.push(`| ${m.mod} | ${m.files} | ~${m.tokens} |`);
|
|
12246
|
+
out.push('');
|
|
12247
|
+
}
|
|
12248
|
+
|
|
12249
|
+
// Hub files + cycle count from the dependency graph (optional).
|
|
12250
|
+
try {
|
|
12251
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
12252
|
+
const { detectCycles } = __require('./src/map/import-graph');
|
|
12253
|
+
const graph = buildFromCwd(cwd);
|
|
12254
|
+
if (graph && graph.reverse && graph.reverse.size) {
|
|
12255
|
+
const hubs = [...graph.reverse.entries()]
|
|
12256
|
+
.map(([f, importers]) => ({ file: path.relative(cwd, f).replace(/\\/g, '/'), in: importers.length }))
|
|
12257
|
+
.filter((h) => h.in > 0)
|
|
12258
|
+
.sort((a, b) => b.in - a.in)
|
|
12259
|
+
.slice(0, 10);
|
|
12260
|
+
if (hubs.length) {
|
|
12261
|
+
out.push('## Hub files (most depended-on)', '| File | Importers |', '|------|-----------|');
|
|
12262
|
+
for (const h of hubs) out.push(`| \`${h.file}\` | ${h.in} |`);
|
|
12263
|
+
out.push('');
|
|
12264
|
+
}
|
|
12265
|
+
let cycleCount = 0;
|
|
12266
|
+
try { cycleCount = detectCycles(graph.forward).length; } catch (_) {}
|
|
12267
|
+
out.push(`**Dependency cycles:** ${cycleCount}` + (cycleCount ? ' _(see import graph)_' : ' — none detected'), '');
|
|
12268
|
+
}
|
|
12269
|
+
} catch (_) { /* graph optional */ }
|
|
12270
|
+
|
|
12271
|
+
// Routes from PROJECT_MAP.md if present.
|
|
12272
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
12273
|
+
if (fs.existsSync(mapPath)) {
|
|
12274
|
+
const mc = fs.readFileSync(mapPath, 'utf8');
|
|
12275
|
+
const routeCount = mc.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---')).length;
|
|
12276
|
+
out.push('## Project map', `Routes detected: ${routeCount} _(use get_map for imports/classes/routes detail)_`, '');
|
|
12277
|
+
} else {
|
|
12278
|
+
out.push('_Run `node gen-project-map.js` for routes / class-hierarchy detail (get_map)._');
|
|
12279
|
+
}
|
|
12280
|
+
|
|
12281
|
+
return out.join('\n');
|
|
12282
|
+
} catch (err) {
|
|
12283
|
+
return `_get_architecture_overview failed: ${err.message}_`;
|
|
12284
|
+
}
|
|
12285
|
+
}
|
|
12286
|
+
|
|
12287
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview };
|
|
11842
12288
|
|
|
11843
12289
|
};
|
|
11844
12290
|
|
|
@@ -11853,17 +12299,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
11853
12299
|
*
|
|
11854
12300
|
* Supported methods:
|
|
11855
12301
|
* initialize → serverInfo + capabilities
|
|
11856
|
-
* tools/list →
|
|
12302
|
+
* tools/list → 17 tool definitions
|
|
11857
12303
|
* tools/call → dispatch to handler, return result
|
|
11858
12304
|
*/
|
|
11859
12305
|
|
|
11860
12306
|
const readline = require('readline');
|
|
11861
12307
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
11862
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = __require('./src/mcp/handlers');
|
|
12308
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview } = __require('./src/mcp/handlers');
|
|
11863
12309
|
|
|
11864
12310
|
const SERVER_INFO = {
|
|
11865
12311
|
name: 'sigmap',
|
|
11866
|
-
version: '7.
|
|
12312
|
+
version: '7.27.0',
|
|
11867
12313
|
description: 'SigMap MCP server — code signatures on demand',
|
|
11868
12314
|
};
|
|
11869
12315
|
|
|
@@ -11926,6 +12372,8 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
11926
12372
|
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
11927
12373
|
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
11928
12374
|
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
12375
|
+
else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
|
|
12376
|
+
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
11929
12377
|
else {
|
|
11930
12378
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
11931
12379
|
return;
|
|
@@ -11986,11 +12434,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
11986
12434
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
11987
12435
|
|
|
11988
12436
|
/**
|
|
11989
|
-
* MCP tool definitions for SigMap (
|
|
12437
|
+
* MCP tool definitions for SigMap (17 tools).
|
|
11990
12438
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
11991
12439
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
11992
12440
|
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
11993
|
-
* sigmap_notify_file_deleted.
|
|
12441
|
+
* sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
|
|
11994
12442
|
*/
|
|
11995
12443
|
|
|
11996
12444
|
const TOOLS = [
|
|
@@ -12263,6 +12711,44 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
12263
12711
|
required: ['path'],
|
|
12264
12712
|
},
|
|
12265
12713
|
},
|
|
12714
|
+
{
|
|
12715
|
+
name: 'get_diff_context',
|
|
12716
|
+
description:
|
|
12717
|
+
'For every changed file in the working tree (or staged, or vs a base ref), return its ' +
|
|
12718
|
+
'current signatures plus blast radius — direct importers, transitive count, and affected ' +
|
|
12719
|
+
'tests/routes — with a risk label. One call gives an agent everything a code review or a ' +
|
|
12720
|
+
'safe edit needs. Lists changed files shell-free (git binary, never a shell).',
|
|
12721
|
+
inputSchema: {
|
|
12722
|
+
type: 'object',
|
|
12723
|
+
properties: {
|
|
12724
|
+
base: {
|
|
12725
|
+
type: 'string',
|
|
12726
|
+
description: 'Optional git ref to diff against (e.g. "main"). Returns files changed in `base..HEAD`. Omit for working-tree changes.',
|
|
12727
|
+
},
|
|
12728
|
+
staged: {
|
|
12729
|
+
type: 'boolean',
|
|
12730
|
+
description: 'When true (and no base), report only staged changes (`git diff --cached`).',
|
|
12731
|
+
},
|
|
12732
|
+
depth: {
|
|
12733
|
+
type: 'number',
|
|
12734
|
+
description: 'Blast-radius BFS depth limit (default: 2). Use 0 for unlimited.',
|
|
12735
|
+
},
|
|
12736
|
+
},
|
|
12737
|
+
required: [],
|
|
12738
|
+
},
|
|
12739
|
+
},
|
|
12740
|
+
{
|
|
12741
|
+
name: 'get_architecture_overview',
|
|
12742
|
+
description:
|
|
12743
|
+
'A high-level map of the codebase in one call: module breakdown (files/tokens), the most ' +
|
|
12744
|
+
'depended-on "hub" files, the dependency-cycle count, and route totals. Extends get_map — ' +
|
|
12745
|
+
'use it to orient in an unfamiliar repo before drilling in with read_context / query_context.',
|
|
12746
|
+
inputSchema: {
|
|
12747
|
+
type: 'object',
|
|
12748
|
+
properties: {},
|
|
12749
|
+
required: [],
|
|
12750
|
+
},
|
|
12751
|
+
},
|
|
12266
12752
|
];
|
|
12267
12753
|
|
|
12268
12754
|
module.exports = { TOOLS };
|
|
@@ -15652,7 +16138,7 @@ function __tryGit(args, opts = {}) {
|
|
|
15652
16138
|
catch (_) { return ''; }
|
|
15653
16139
|
}
|
|
15654
16140
|
|
|
15655
|
-
const VERSION = '7.
|
|
16141
|
+
const VERSION = '7.27.0';
|
|
15656
16142
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
15657
16143
|
|
|
15658
16144
|
function requireSourceOrBundled(key) {
|
|
@@ -17459,6 +17945,9 @@ Usage:
|
|
|
17459
17945
|
${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
|
|
17460
17946
|
${cmd} ask "<query>" --no-squeeze Disable input minimization entirely
|
|
17461
17947
|
${cmd} ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
|
|
17948
|
+
${cmd} evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
|
|
17949
|
+
${cmd} evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
|
|
17950
|
+
${cmd} evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
|
|
17462
17951
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
17463
17952
|
${cmd} note List recent notes (also: note --list <N>)
|
|
17464
17953
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -18031,6 +18520,66 @@ function main() {
|
|
|
18031
18520
|
process.exit(0);
|
|
18032
18521
|
}
|
|
18033
18522
|
|
|
18523
|
+
// `sigmap evidence "<query>"` — Evidence Pack v1 (v8.0 E1).
|
|
18524
|
+
// Deterministic, machine-consumable signature+evidence map. Always writes the
|
|
18525
|
+
// JSON artifact to .context/evidence-pack.json; stdout carries the requested
|
|
18526
|
+
// mode (JSON default, or Markdown handoff with --markdown/--md).
|
|
18527
|
+
if (args[0] === 'evidence') {
|
|
18528
|
+
const query = args[1];
|
|
18529
|
+
if (!query || query.startsWith('--')) {
|
|
18530
|
+
console.error('[sigmap] Usage: sigmap evidence "<query>" [--markdown] [--top <n>] [--budget <n>] [--out <path>]');
|
|
18531
|
+
console.error(' Example: sigmap evidence "how does auth work" --markdown');
|
|
18532
|
+
process.exit(1);
|
|
18533
|
+
}
|
|
18534
|
+
|
|
18535
|
+
const { buildEvidencePack, formatJSON, formatMarkdown } = requireSourceOrBundled('./src/evidence/pack');
|
|
18536
|
+
|
|
18537
|
+
const opts = {};
|
|
18538
|
+
const topIdx = args.indexOf('--top');
|
|
18539
|
+
if (topIdx !== -1 && args[topIdx + 1]) opts.top = parseInt(args[topIdx + 1], 10);
|
|
18540
|
+
const budgetIdx = args.indexOf('--budget');
|
|
18541
|
+
if (budgetIdx !== -1 && args[budgetIdx + 1]) opts.budget = parseInt(args[budgetIdx + 1], 10);
|
|
18542
|
+
else opts.budget = (config && config.maxTokens) || 6000;
|
|
18543
|
+
|
|
18544
|
+
let pack;
|
|
18545
|
+
try {
|
|
18546
|
+
pack = buildEvidencePack(query, cwd, opts);
|
|
18547
|
+
} catch (e) {
|
|
18548
|
+
console.error('[sigmap] evidence: ' + e.message);
|
|
18549
|
+
process.exit(1);
|
|
18550
|
+
}
|
|
18551
|
+
|
|
18552
|
+
if (pack.files.length === 0) {
|
|
18553
|
+
process.stderr.write('[sigmap] ⚠ no matching files indexed. Run: sigmap (to generate context first)\n');
|
|
18554
|
+
}
|
|
18555
|
+
|
|
18556
|
+
const jsonText = formatJSON(pack);
|
|
18557
|
+
const artifactPath = path.join(cwd, '.context', 'evidence-pack.json');
|
|
18558
|
+
try {
|
|
18559
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
18560
|
+
fs.writeFileSync(artifactPath, jsonText, 'utf8');
|
|
18561
|
+
process.stderr.write(`[sigmap] evidence pack → ${path.relative(cwd, artifactPath)} (${pack.files.length} files, ${pack.grounding.symbolCount} symbols)\n`);
|
|
18562
|
+
} catch (_) { /* artifact write is best-effort */ }
|
|
18563
|
+
|
|
18564
|
+
const markdown = args.includes('--markdown') || args.includes('--md');
|
|
18565
|
+
const rendered = markdown ? formatMarkdown(pack) : jsonText;
|
|
18566
|
+
|
|
18567
|
+
const outIdx = args.indexOf('--out');
|
|
18568
|
+
if (outIdx !== -1 && args[outIdx + 1]) {
|
|
18569
|
+
const outPath = path.resolve(cwd, args[outIdx + 1]);
|
|
18570
|
+
try {
|
|
18571
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
18572
|
+
fs.writeFileSync(outPath, rendered + '\n', 'utf8');
|
|
18573
|
+
} catch (e) {
|
|
18574
|
+
console.error('[sigmap] evidence: could not write --out ' + outPath + ': ' + e.message);
|
|
18575
|
+
process.exit(1);
|
|
18576
|
+
}
|
|
18577
|
+
}
|
|
18578
|
+
|
|
18579
|
+
process.stdout.write(rendered + '\n');
|
|
18580
|
+
process.exit(0);
|
|
18581
|
+
}
|
|
18582
|
+
|
|
18034
18583
|
// `sigmap gain` — token-savings dashboard (totals, by-operation, trends).
|
|
18035
18584
|
if (args[0] === 'gain') {
|
|
18036
18585
|
const valOf = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
|
package/llms-full.txt
CHANGED
|
@@ -9,13 +9,13 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.27.0 | Benchmark: sigmap-v7.27-main (2026-06-22)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## Core metrics (benchmark: sigmap-v7.
|
|
18
|
+
## Core metrics (benchmark: sigmap-v7.27-main, 2026-06-22)
|
|
19
19
|
|
|
20
20
|
| Metric | Without SigMap | With SigMap |
|
|
21
21
|
|--------|----------------|-------------|
|
|
@@ -24,7 +24,7 @@ Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
24
24
|
| Task success proxy | 10% | 52.2% |
|
|
25
25
|
| Prompts per task | 2.84 | 1.72 (39.4% fewer) |
|
|
26
26
|
| Supported languages | — | 33 |
|
|
27
|
-
| MCP tools | — |
|
|
27
|
+
| MCP tools | — | 17 |
|
|
28
28
|
| npm runtime dependencies | — | 0 |
|
|
29
29
|
|
|
30
30
|
---
|
|
@@ -109,6 +109,9 @@ sigmap squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSO
|
|
|
109
109
|
sigmap ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
|
|
110
110
|
sigmap ask "<query>" --no-squeeze Disable input minimization entirely
|
|
111
111
|
sigmap ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
|
|
112
|
+
sigmap evidence "<query>" Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
|
|
113
|
+
sigmap evidence "<query>" --markdown Emit the Markdown handoff rendering to stdout
|
|
114
|
+
sigmap evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked files / token budget / write rendered output
|
|
112
115
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
113
116
|
sigmap note List recent notes (also: note --list <N>)
|
|
114
117
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -119,7 +122,7 @@ sigmap --version Show version
|
|
|
119
122
|
|
|
120
123
|
---
|
|
121
124
|
|
|
122
|
-
## MCP server —
|
|
125
|
+
## MCP server — 17 tools
|
|
123
126
|
|
|
124
127
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
125
128
|
|
|
@@ -247,6 +250,22 @@ Tell SigMap a file was deleted so its symbols are dropped from the live index.
|
|
|
247
250
|
Input: { path: string }
|
|
248
251
|
```
|
|
249
252
|
|
|
253
|
+
### get_diff_context
|
|
254
|
+
|
|
255
|
+
For every changed file in the working tree (or staged, or vs a base ref), return its current signatures plus blast radius — direct importers, transitive count, and affected tests/routes — with a risk label. One call gives an agent everything a code review or a safe edit needs. Lists changed files shell-free (git binary, never a shell).
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
Input: { base?: string, staged?: boolean, depth?: number }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### get_architecture_overview
|
|
262
|
+
|
|
263
|
+
A high-level map of the codebase in one call: module breakdown (files/tokens), the most depended-on "hub" files, the dependency-cycle count, and route totals. Extends get_map — use it to orient in an unfamiliar repo before drilling in with read_context / query_context.
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
Input: { } (no arguments)
|
|
267
|
+
```
|
|
268
|
+
|
|
250
269
|
---
|
|
251
270
|
|
|
252
271
|
## Configuration (gen-context.config.json)
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.27.0 | Benchmark: sigmap-v7.27-main (2026-06-22)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -21,13 +21,13 @@ Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
21
21
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
22
22
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
23
23
|
|
|
24
|
-
## Core metrics (benchmark: sigmap-v7.
|
|
24
|
+
## Core metrics (benchmark: sigmap-v7.27-main, 2026-06-22)
|
|
25
25
|
|
|
26
26
|
- hit@5 retrieval: 75.6% vs 13.6% random baseline (5.6× lift)
|
|
27
27
|
- Token reduction: 97.0% average across benchmark repos
|
|
28
28
|
- Task success: 52.2% vs 10% without SigMap
|
|
29
29
|
- Prompts per task: 1.72 vs 2.84 baseline (39.4% fewer)
|
|
30
|
-
- Languages: 33 supported · MCP tools:
|
|
30
|
+
- Languages: 33 supported · MCP tools: 17
|
|
31
31
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
32
32
|
|
|
33
33
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.27.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Evidence Pack v1 (v8.0 E1).
|
|
5
|
+
*
|
|
6
|
+
* A deterministic, machine-consumable signature-and-evidence map. Replaces the
|
|
7
|
+
* "paste this into your prompt" workflow with a byte-stable JSON artifact that
|
|
8
|
+
* an agent or CI can ingest directly — every entry anchored to a real file,
|
|
9
|
+
* symbol, and line range.
|
|
10
|
+
*
|
|
11
|
+
* Composed entirely from shipped zero-dep modules:
|
|
12
|
+
* - retrieval/ranker → ranked files, scores, signals
|
|
13
|
+
* - extractors/line-anchor → `:start-end` suffix parsing (sourceLines)
|
|
14
|
+
* - security/scanner → secret redaction of symbols
|
|
15
|
+
* - crypto (node builtin) → sha256 grounding hash
|
|
16
|
+
*
|
|
17
|
+
* Determinism: the pack carries NO wall-clock timestamp. Given an unchanged
|
|
18
|
+
* repository, `buildEvidencePack` returns a byte-identical object, and
|
|
19
|
+
* `grounding.contextHash` is stable. This is the point — the pack is auditable.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
const { buildSigIndex, rank, detectIntent } = require('../retrieval/ranker');
|
|
27
|
+
const { scan } = require('../security/scanner');
|
|
28
|
+
|
|
29
|
+
const SCHEMA_VERSION = '1.0';
|
|
30
|
+
const DEFAULT_BUDGET = 6000;
|
|
31
|
+
const DEFAULT_TOP = 12;
|
|
32
|
+
|
|
33
|
+
const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
|
|
34
|
+
const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
|
|
35
|
+
const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
|
|
36
|
+
const SECURITY_RE = /(^|\/|[._-])(auth|authn|authz|login|password|passwd|secret|credential|token|session|crypto|cipher|payment|billing|checkout|oauth|jwt|permission|acl|rbac)([._-]|\/|$)/i;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Split a signature's ` :start-end` line anchor from its symbol text.
|
|
40
|
+
* @param {string} sig
|
|
41
|
+
* @returns {{ symbol: string, start: number|null, end: number|null }}
|
|
42
|
+
*/
|
|
43
|
+
function parseAnchor(sig) {
|
|
44
|
+
const m = /\s*:(\d+)-(\d+)\s*$/.exec(sig);
|
|
45
|
+
if (!m) return { symbol: sig.trim(), start: null, end: null };
|
|
46
|
+
return {
|
|
47
|
+
symbol: sig.slice(0, m.index).trim(),
|
|
48
|
+
start: parseInt(m[1], 10),
|
|
49
|
+
end: parseInt(m[2], 10),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Classify a file into a coarse risk label. Path-based heuristic (v1) — the
|
|
55
|
+
* richer label set (C3) lands in v8.5.
|
|
56
|
+
* @param {string} relPath
|
|
57
|
+
* @returns {'generated'|'test'|'config'|'security'|'source'}
|
|
58
|
+
*/
|
|
59
|
+
function riskLabelFor(relPath) {
|
|
60
|
+
const p = relPath.replace(/\\/g, '/');
|
|
61
|
+
if (GENERATED_RE.test(p)) return 'generated';
|
|
62
|
+
if (TEST_RE.test(p)) return 'test';
|
|
63
|
+
if (SECURITY_RE.test(p)) return 'security';
|
|
64
|
+
if (CONFIG_RE.test(p)) return 'config';
|
|
65
|
+
return 'source';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Filename stem (basename minus the first extension chain). */
|
|
69
|
+
function stemOf(relPath) {
|
|
70
|
+
const base = path.basename(relPath);
|
|
71
|
+
return base.replace(/\.[^.]+$/, '').replace(/\.(test|spec)$/i, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Best-effort impl→test discovery (v1). Matches test files whose stem equals
|
|
76
|
+
* the implementation file's stem, by common convention. Deterministic. The
|
|
77
|
+
* accuracy-measured discovery (C2) lands in v8.5.
|
|
78
|
+
* @param {string} relPath
|
|
79
|
+
* @param {string[]} allFiles - universe of indexed files (relative paths)
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function findRelatedTests(relPath, allFiles) {
|
|
83
|
+
if (riskLabelFor(relPath) === 'test') return [];
|
|
84
|
+
const stem = stemOf(relPath).toLowerCase();
|
|
85
|
+
if (!stem) return [];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const f of allFiles) {
|
|
88
|
+
if (f === relPath) continue;
|
|
89
|
+
if (riskLabelFor(f) !== 'test') continue;
|
|
90
|
+
if (stemOf(f).toLowerCase() === stem) out.push(f);
|
|
91
|
+
}
|
|
92
|
+
return out.sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Map a ranker `signals` object into a short human-readable reason string. */
|
|
96
|
+
function reasonFor(signals) {
|
|
97
|
+
if (!signals) return 'ranked match';
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (signals.symbolMatch > 0) parts.push('symbol-name match');
|
|
100
|
+
if (signals.exactToken > 0) parts.push('exact token match');
|
|
101
|
+
if (signals.prefixMatch > 0) parts.push('prefix match');
|
|
102
|
+
if (signals.pathMatch > 0) parts.push('path match');
|
|
103
|
+
if (signals.graphBoost > 0) parts.push('dependency-graph neighbor');
|
|
104
|
+
if (signals.recencyBoost > 1) parts.push('recently changed');
|
|
105
|
+
if (signals.learnedWeights && signals.learnedWeights !== 1) parts.push('learned weight');
|
|
106
|
+
return parts.length ? parts.join('; ') : 'ranked match';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Token estimate for a signature block (matches the ranker's heuristic). */
|
|
110
|
+
function sigTokens(sigs) {
|
|
111
|
+
return Math.ceil(sigs.join('\n').length / 4);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stable stringify with recursively sorted object keys, for hashing.
|
|
116
|
+
* @param {*} value
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function canonicalize(value) {
|
|
120
|
+
return JSON.stringify(sortKeys(value));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sortKeys(value) {
|
|
124
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
125
|
+
if (value && typeof value === 'object') {
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const k of Object.keys(value).sort()) out[k] = sortKeys(value[k]);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build an Evidence Pack for a query.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} query
|
|
137
|
+
* @param {string} cwd
|
|
138
|
+
* @param {object} [opts]
|
|
139
|
+
* @param {number} [opts.budget=6000] - token budget for included files
|
|
140
|
+
* @param {number} [opts.top=12] - max ranked files to consider
|
|
141
|
+
* @param {Map<string,string[]>} [opts.sigIndex] - pre-built index (else built from cwd)
|
|
142
|
+
* @returns {object} Evidence Pack v1
|
|
143
|
+
*/
|
|
144
|
+
function buildEvidencePack(query, cwd, opts = {}) {
|
|
145
|
+
const budget = Number.isFinite(opts.budget) ? opts.budget : DEFAULT_BUDGET;
|
|
146
|
+
const top = Number.isFinite(opts.top) ? opts.top : DEFAULT_TOP;
|
|
147
|
+
|
|
148
|
+
const sigIndex = opts.sigIndex instanceof Map ? opts.sigIndex : buildSigIndex(cwd);
|
|
149
|
+
const intent = detectIntent(query);
|
|
150
|
+
const allFiles = Array.from(sigIndex.keys());
|
|
151
|
+
|
|
152
|
+
const ranked = rank(query, sigIndex, { topK: top, cwd })
|
|
153
|
+
.filter((r) => r.score > 0 || ranked0Empty(query));
|
|
154
|
+
const maxScore = ranked.reduce((m, r) => Math.max(m, r.score), 0);
|
|
155
|
+
|
|
156
|
+
// Greedy budget fill in rank order; the remainder is reported as dropped.
|
|
157
|
+
const files = [];
|
|
158
|
+
const droppedFiles = [];
|
|
159
|
+
let used = 0;
|
|
160
|
+
|
|
161
|
+
for (const r of ranked) {
|
|
162
|
+
const tokens = sigTokens(r.sigs);
|
|
163
|
+
if (files.length > 0 && used + tokens > budget) {
|
|
164
|
+
droppedFiles.push({ path: r.file, reason: `budget: would exceed ${budget}-token limit` });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
used += tokens;
|
|
168
|
+
|
|
169
|
+
const safe = scan(r.sigs, r.file).safe;
|
|
170
|
+
const symbols = [];
|
|
171
|
+
const sourceLines = [];
|
|
172
|
+
for (const sig of safe) {
|
|
173
|
+
const { symbol, start, end } = parseAnchor(sig);
|
|
174
|
+
symbols.push(symbol);
|
|
175
|
+
if (start !== null) sourceLines.push({ symbol, start, end });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
files.push({
|
|
179
|
+
path: r.file,
|
|
180
|
+
symbols,
|
|
181
|
+
reason: reasonFor(r.signals),
|
|
182
|
+
confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
|
|
183
|
+
sourceLines,
|
|
184
|
+
relatedTests: findRelatedTests(r.file, allFiles),
|
|
185
|
+
riskLabel: riskLabelFor(r.file),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const symbolCount = files.reduce((n, f) => n + f.symbols.length, 0);
|
|
190
|
+
const anchoredSymbols = files.reduce((n, f) => n + f.sourceLines.length, 0);
|
|
191
|
+
|
|
192
|
+
const pack = {
|
|
193
|
+
schemaVersion: SCHEMA_VERSION,
|
|
194
|
+
query,
|
|
195
|
+
intent,
|
|
196
|
+
files,
|
|
197
|
+
tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
|
|
198
|
+
droppedFiles,
|
|
199
|
+
grounding: {
|
|
200
|
+
symbolCount,
|
|
201
|
+
anchoredSymbols,
|
|
202
|
+
anchorCoverage: symbolCount > 0 ? Math.round((anchoredSymbols / symbolCount) * 1000) / 1000 : 0,
|
|
203
|
+
contextHash: null,
|
|
204
|
+
deterministic: true,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Hash everything except the hash field itself.
|
|
209
|
+
const forHash = Object.assign({}, pack, {
|
|
210
|
+
grounding: Object.assign({}, pack.grounding, { contextHash: undefined }),
|
|
211
|
+
});
|
|
212
|
+
pack.grounding.contextHash = 'sha256:' + crypto.createHash('sha256').update(canonicalize(forHash)).digest('hex');
|
|
213
|
+
|
|
214
|
+
return pack;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// rank() returns [] for an empty/whitespace query; keep the filter readable.
|
|
218
|
+
function ranked0Empty(query) {
|
|
219
|
+
return !query || !query.trim();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Pretty-printed canonical JSON rendering of a pack. */
|
|
223
|
+
function formatJSON(pack) {
|
|
224
|
+
return JSON.stringify(pack, null, 2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Markdown handoff rendering of a pack. */
|
|
228
|
+
function formatMarkdown(pack) {
|
|
229
|
+
const L = [];
|
|
230
|
+
L.push(`# Evidence Pack — \`${pack.query}\``);
|
|
231
|
+
L.push('');
|
|
232
|
+
L.push(`- **Schema:** v${pack.schemaVersion}`);
|
|
233
|
+
L.push(`- **Intent:** ${pack.intent}`);
|
|
234
|
+
L.push(`- **Budget:** ${pack.tokenBudget.used} / ${pack.tokenBudget.limit} tokens used (${pack.tokenBudget.remaining} remaining)`);
|
|
235
|
+
L.push(`- **Grounding:** ${pack.grounding.anchoredSymbols}/${pack.grounding.symbolCount} symbols anchored (${Math.round(pack.grounding.anchorCoverage * 100)}%)`);
|
|
236
|
+
L.push(`- **Hash:** \`${pack.grounding.contextHash}\``);
|
|
237
|
+
L.push('');
|
|
238
|
+
|
|
239
|
+
for (const f of pack.files) {
|
|
240
|
+
L.push(`## \`${f.path}\` _(${f.riskLabel}, confidence ${f.confidence})_`);
|
|
241
|
+
L.push(`_${f.reason}_`);
|
|
242
|
+
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
|
|
243
|
+
L.push('');
|
|
244
|
+
L.push('```');
|
|
245
|
+
for (const s of f.symbols) L.push(s);
|
|
246
|
+
L.push('```');
|
|
247
|
+
L.push('');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (pack.droppedFiles.length) {
|
|
251
|
+
L.push('## Dropped (over budget)');
|
|
252
|
+
for (const d of pack.droppedFiles) L.push(`- \`${d.path}\` — ${d.reason}`);
|
|
253
|
+
L.push('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return L.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
buildEvidencePack,
|
|
261
|
+
formatJSON,
|
|
262
|
+
formatMarkdown,
|
|
263
|
+
parseAnchor,
|
|
264
|
+
riskLabelFor,
|
|
265
|
+
findRelatedTests,
|
|
266
|
+
SCHEMA_VERSION,
|
|
267
|
+
};
|
package/src/mcp/handlers.js
CHANGED
|
@@ -674,4 +674,179 @@ function notifyFileDeleted(args, cwd) {
|
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
-
|
|
677
|
+
/**
|
|
678
|
+
* List the files changed in the working tree, staged area, or vs a base ref.
|
|
679
|
+
* Shell-free (routes through src/util/git.js). Returns relative paths.
|
|
680
|
+
*/
|
|
681
|
+
function _changedFiles(cwd, args) {
|
|
682
|
+
const { tryGit } = require('../util/git');
|
|
683
|
+
let out = '';
|
|
684
|
+
if (args.base) {
|
|
685
|
+
if (!/^[A-Za-z0-9._/\-~^]+$/.test(args.base)) return [];
|
|
686
|
+
out = tryGit(['diff', `${args.base}..HEAD`, '--name-only'], { cwd });
|
|
687
|
+
} else if (args.staged) {
|
|
688
|
+
out = tryGit(['diff', '--cached', '--name-only'], { cwd });
|
|
689
|
+
} else {
|
|
690
|
+
out = tryGit(['diff', 'HEAD', '--name-only'], { cwd });
|
|
691
|
+
}
|
|
692
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* get_diff_context({ base?, staged?, depth? }) → string
|
|
697
|
+
*
|
|
698
|
+
* For each changed file: its extracted signatures + blast radius (direct
|
|
699
|
+
* importers, transitive count, affected tests/routes) + a risk label.
|
|
700
|
+
* Hands an agent everything a review or a safe edit needs in one call.
|
|
701
|
+
*/
|
|
702
|
+
function getDiffContext(args, cwd) {
|
|
703
|
+
try {
|
|
704
|
+
const a = args || {};
|
|
705
|
+
const files = _changedFiles(cwd, a);
|
|
706
|
+
if (files.length === 0) {
|
|
707
|
+
return '_No changed files detected._ (Outside a git repo, or the working tree / selected range is clean.)';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const { extractFile, langFor } = require('../extractors/dispatch');
|
|
711
|
+
const { analyzeImpact } = require('../graph/impact');
|
|
712
|
+
let riskLabelFor;
|
|
713
|
+
try { ({ riskLabelFor } = require('../evidence/pack')); } catch (_) { riskLabelFor = () => 'source'; }
|
|
714
|
+
|
|
715
|
+
const depth = Math.max(0, parseInt(a.depth, 10) || 2);
|
|
716
|
+
const srcFiles = files.filter((f) => langFor(f));
|
|
717
|
+
let impactByFile = new Map();
|
|
718
|
+
try {
|
|
719
|
+
const impacts = analyzeImpact(srcFiles, cwd, { depth });
|
|
720
|
+
impactByFile = new Map(impacts.map((r) => [r.file, r.impact]));
|
|
721
|
+
} catch (_) { /* graph optional */ }
|
|
722
|
+
|
|
723
|
+
const scope = a.base ? `vs ${a.base}` : (a.staged ? 'staged' : 'working tree');
|
|
724
|
+
const out = [
|
|
725
|
+
`# Diff context (${scope})`,
|
|
726
|
+
'',
|
|
727
|
+
`**${files.length} changed file${files.length === 1 ? '' : 's'}** · ${srcFiles.length} with extractable signatures`,
|
|
728
|
+
'',
|
|
729
|
+
];
|
|
730
|
+
|
|
731
|
+
for (const rel of files) {
|
|
732
|
+
out.push(`## \`${rel}\``);
|
|
733
|
+
if (!langFor(rel)) { out.push('_non-source file (no signatures)_', ''); continue; }
|
|
734
|
+
|
|
735
|
+
out.push(`_risk: ${riskLabelFor(rel)}_`);
|
|
736
|
+
const impact = impactByFile.get(rel);
|
|
737
|
+
if (impact) {
|
|
738
|
+
out.push(
|
|
739
|
+
`**Blast radius:** ${impact.totalImpact} file(s) impacted — ` +
|
|
740
|
+
`${impact.direct.length} direct importer(s), ${impact.transitive.length} transitive` +
|
|
741
|
+
(impact.tests.length ? `, ${impact.tests.length} test(s)` : '') +
|
|
742
|
+
(impact.routes.length ? `, ${impact.routes.length} route(s)` : '')
|
|
743
|
+
);
|
|
744
|
+
if (impact.direct.length) {
|
|
745
|
+
out.push(`Direct importers: ${impact.direct.slice(0, 8).map((f) => '`' + f + '`').join(', ')}` + (impact.direct.length > 8 ? ' …' : ''));
|
|
746
|
+
}
|
|
747
|
+
if (impact.tests.length) {
|
|
748
|
+
out.push(`Tests to run: ${impact.tests.slice(0, 8).map((f) => '`' + f + '`').join(', ')}`);
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
out.push('**Blast radius:** (not in dependency graph — new or leaf file)');
|
|
752
|
+
}
|
|
753
|
+
out.push('');
|
|
754
|
+
|
|
755
|
+
let src = '';
|
|
756
|
+
try { src = fs.readFileSync(path.resolve(cwd, rel), 'utf8'); } catch (_) {}
|
|
757
|
+
const sigs = src ? extractFile(rel, src) : [];
|
|
758
|
+
if (sigs.length) {
|
|
759
|
+
out.push('```');
|
|
760
|
+
for (const s of sigs.slice(0, 40)) out.push(s);
|
|
761
|
+
if (sigs.length > 40) out.push(`… +${sigs.length - 40} more`);
|
|
762
|
+
out.push('```');
|
|
763
|
+
} else {
|
|
764
|
+
out.push('_(no signatures extracted — file may be deleted or empty)_');
|
|
765
|
+
}
|
|
766
|
+
out.push('');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return out.join('\n');
|
|
770
|
+
} catch (err) {
|
|
771
|
+
return `_get_diff_context failed: ${err.message}_`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* get_architecture_overview({}) → string
|
|
777
|
+
*
|
|
778
|
+
* A high-level map of the codebase: module breakdown (files/tokens), the most
|
|
779
|
+
* depended-on "hub" files, dependency-cycle count, and route totals. Extends
|
|
780
|
+
* get_map — one call to orient in an unfamiliar repo.
|
|
781
|
+
*/
|
|
782
|
+
function getArchitectureOverview(args, cwd) {
|
|
783
|
+
try {
|
|
784
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
785
|
+
const index = buildSigIndex(cwd);
|
|
786
|
+
const out = ['# Architecture overview', ''];
|
|
787
|
+
|
|
788
|
+
if (index.size === 0) {
|
|
789
|
+
out.push('_No context file found. Run: node gen-context.js_', '');
|
|
790
|
+
} else {
|
|
791
|
+
const groups = {};
|
|
792
|
+
let totalTokens = 0;
|
|
793
|
+
let totalFiles = 0;
|
|
794
|
+
for (const [rel, sigs] of index.entries()) {
|
|
795
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
796
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
797
|
+
const tok = Math.ceil(sigs.join('\n').length / 4);
|
|
798
|
+
if (!groups[mod]) groups[mod] = { files: 0, tokens: 0 };
|
|
799
|
+
groups[mod].files++;
|
|
800
|
+
groups[mod].tokens += tok;
|
|
801
|
+
totalTokens += tok;
|
|
802
|
+
totalFiles++;
|
|
803
|
+
}
|
|
804
|
+
const sorted = Object.entries(groups)
|
|
805
|
+
.map(([mod, d]) => ({ mod, files: d.files, tokens: d.tokens }))
|
|
806
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
807
|
+
|
|
808
|
+
out.push(`**${totalFiles} indexed files · ${sorted.length} modules · ~${totalTokens} tokens**`, '');
|
|
809
|
+
out.push('## Modules', '| Module | Files | Tokens |', '|--------|-------|--------|');
|
|
810
|
+
for (const m of sorted.slice(0, 20)) out.push(`| ${m.mod} | ${m.files} | ~${m.tokens} |`);
|
|
811
|
+
out.push('');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Hub files + cycle count from the dependency graph (optional).
|
|
815
|
+
try {
|
|
816
|
+
const { buildFromCwd } = require('../graph/builder');
|
|
817
|
+
const { detectCycles } = require('../map/import-graph');
|
|
818
|
+
const graph = buildFromCwd(cwd);
|
|
819
|
+
if (graph && graph.reverse && graph.reverse.size) {
|
|
820
|
+
const hubs = [...graph.reverse.entries()]
|
|
821
|
+
.map(([f, importers]) => ({ file: path.relative(cwd, f).replace(/\\/g, '/'), in: importers.length }))
|
|
822
|
+
.filter((h) => h.in > 0)
|
|
823
|
+
.sort((a, b) => b.in - a.in)
|
|
824
|
+
.slice(0, 10);
|
|
825
|
+
if (hubs.length) {
|
|
826
|
+
out.push('## Hub files (most depended-on)', '| File | Importers |', '|------|-----------|');
|
|
827
|
+
for (const h of hubs) out.push(`| \`${h.file}\` | ${h.in} |`);
|
|
828
|
+
out.push('');
|
|
829
|
+
}
|
|
830
|
+
let cycleCount = 0;
|
|
831
|
+
try { cycleCount = detectCycles(graph.forward).length; } catch (_) {}
|
|
832
|
+
out.push(`**Dependency cycles:** ${cycleCount}` + (cycleCount ? ' _(see import graph)_' : ' — none detected'), '');
|
|
833
|
+
}
|
|
834
|
+
} catch (_) { /* graph optional */ }
|
|
835
|
+
|
|
836
|
+
// Routes from PROJECT_MAP.md if present.
|
|
837
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
838
|
+
if (fs.existsSync(mapPath)) {
|
|
839
|
+
const mc = fs.readFileSync(mapPath, 'utf8');
|
|
840
|
+
const routeCount = mc.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---')).length;
|
|
841
|
+
out.push('## Project map', `Routes detected: ${routeCount} _(use get_map for imports/classes/routes detail)_`, '');
|
|
842
|
+
} else {
|
|
843
|
+
out.push('_Run `node gen-project-map.js` for routes / class-hierarchy detail (get_map)._');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return out.join('\n');
|
|
847
|
+
} catch (err) {
|
|
848
|
+
return `_get_architecture_overview failed: ${err.message}_`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview };
|
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 → 17 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, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '7.
|
|
21
|
+
version: '7.27.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -81,6 +81,8 @@ function dispatch(msg, cwd) {
|
|
|
81
81
|
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
82
82
|
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
83
83
|
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
84
|
+
else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
|
|
85
|
+
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
84
86
|
else {
|
|
85
87
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
86
88
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* MCP tool definitions for SigMap (
|
|
4
|
+
* MCP tool definitions for SigMap (17 tools).
|
|
5
5
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6
6
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
7
7
|
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
8
|
-
* sigmap_notify_file_deleted.
|
|
8
|
+
* sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const TOOLS = [
|
|
@@ -278,6 +278,44 @@ const TOOLS = [
|
|
|
278
278
|
required: ['path'],
|
|
279
279
|
},
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: 'get_diff_context',
|
|
283
|
+
description:
|
|
284
|
+
'For every changed file in the working tree (or staged, or vs a base ref), return its ' +
|
|
285
|
+
'current signatures plus blast radius — direct importers, transitive count, and affected ' +
|
|
286
|
+
'tests/routes — with a risk label. One call gives an agent everything a code review or a ' +
|
|
287
|
+
'safe edit needs. Lists changed files shell-free (git binary, never a shell).',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
base: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'Optional git ref to diff against (e.g. "main"). Returns files changed in `base..HEAD`. Omit for working-tree changes.',
|
|
294
|
+
},
|
|
295
|
+
staged: {
|
|
296
|
+
type: 'boolean',
|
|
297
|
+
description: 'When true (and no base), report only staged changes (`git diff --cached`).',
|
|
298
|
+
},
|
|
299
|
+
depth: {
|
|
300
|
+
type: 'number',
|
|
301
|
+
description: 'Blast-radius BFS depth limit (default: 2). Use 0 for unlimited.',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
required: [],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'get_architecture_overview',
|
|
309
|
+
description:
|
|
310
|
+
'A high-level map of the codebase in one call: module breakdown (files/tokens), the most ' +
|
|
311
|
+
'depended-on "hub" files, the dependency-cycle count, and route totals. Extends get_map — ' +
|
|
312
|
+
'use it to orient in an unfamiliar repo before drilling in with read_context / query_context.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {},
|
|
316
|
+
required: [],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
281
319
|
];
|
|
282
320
|
|
|
283
321
|
module.exports = { TOOLS };
|