sigmap 7.26.0 → 7.28.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 +21 -0
- package/README.md +3 -3
- package/gen-context.js +477 -7
- package/llms-full.txt +21 -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/doctor/diagnose.js +236 -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,27 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.28.0] — 2026-06-23
|
|
14
|
+
|
|
15
|
+
Minor release — **v8.0 E3:** a one-shot setup doctor so a cold user reaches a useful answer fast.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap doctor` (#381):** a one-shot diagnostic that runs seven resilient checks — git repository, config & source roots, generated context file, signature index, index freshness, coverage, and MCP wiring — and prints an **actionable fix** for anything wrong (e.g. "run: npx sigmap", "run: sigmap --setup", "increase maxTokens or expand srcDirs"). `sigmap doctor --json` emits `{ checks, ok, errors, warnings }`; the command exits **1** on a hard failure (no context file / invalid config) and **0** otherwise, so it is usable in CI. Composed from `loadConfig`, `coverageScore`, `buildSigIndex`, and the known adapter-output / MCP-config paths — zero new runtime dependencies, no system-shell spawns.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Release CI self-heals `develop` (#380):** the develop→main release PR's head branch is `develop`, which the repo's delete-branch-on-merge setting removes on every release; the "Sync develop with main" workflow now fetches `main` and recreates `develop` from it when the ref is missing (else merges `main` in), so `develop` no longer needs manual restoration after a release.
|
|
22
|
+
|
|
23
|
+
## [7.27.0] — 2026-06-22
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`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.
|
|
29
|
+
- **`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`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- 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`.
|
|
33
|
+
|
|
13
34
|
## [7.26.0] — 2026-06-22
|
|
14
35
|
|
|
15
36
|
Minor release — **v8.0 "The Evidence Pack & the Pivot" (E1):** the keystone artifact that makes SigMap consumable by machines instead of copy-paste.
|
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.28-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
|
@@ -3462,6 +3462,246 @@ __factories["./src/discovery/source-root-scorer"] = function(module, exports) {
|
|
|
3462
3462
|
|
|
3463
3463
|
};
|
|
3464
3464
|
|
|
3465
|
+
// ── ./src/doctor/diagnose ──
|
|
3466
|
+
__factories["./src/doctor/diagnose"] = function(module, exports) {
|
|
3467
|
+
|
|
3468
|
+
/**
|
|
3469
|
+
* sigmap doctor (v8.0 E3).
|
|
3470
|
+
*
|
|
3471
|
+
* One-shot diagnostic for a SigMap setup: each check reports ok/warn/fail with
|
|
3472
|
+
* an actionable fix, so a cold user can reach a useful answer in minutes. Pure,
|
|
3473
|
+
* resilient (no check ever throws), zero new runtime deps. Composes the config
|
|
3474
|
+
* loader, coverage scorer, signature index, and the known adapter-output /
|
|
3475
|
+
* MCP-config paths.
|
|
3476
|
+
*/
|
|
3477
|
+
|
|
3478
|
+
const fs = require('fs');
|
|
3479
|
+
const path = require('path');
|
|
3480
|
+
const os = require('os');
|
|
3481
|
+
|
|
3482
|
+
// Generated context files a `read_context`/`ask` flow can consume.
|
|
3483
|
+
const ADAPTER_OUTPUTS = [
|
|
3484
|
+
['.github', 'copilot-instructions.md'],
|
|
3485
|
+
['CLAUDE.md'],
|
|
3486
|
+
['AGENTS.md'],
|
|
3487
|
+
['.cursorrules'],
|
|
3488
|
+
['.windsurfrules'],
|
|
3489
|
+
['.github', 'openai-context.md'],
|
|
3490
|
+
['.github', 'gemini-context.md'],
|
|
3491
|
+
['llm-full.txt'],
|
|
3492
|
+
['llm.txt'],
|
|
3493
|
+
];
|
|
3494
|
+
|
|
3495
|
+
const EXCLUDE_DIRS = new Set([
|
|
3496
|
+
'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
|
|
3497
|
+
'.next', 'coverage', 'target', 'vendor', '.context',
|
|
3498
|
+
]);
|
|
3499
|
+
|
|
3500
|
+
const ICON = { ok: '✓', warn: '⚠', fail: '✗' };
|
|
3501
|
+
|
|
3502
|
+
function _short(p, cwd) {
|
|
3503
|
+
const rel = path.relative(cwd, p);
|
|
3504
|
+
return rel && !rel.startsWith('..') ? rel : p.replace(os.homedir(), '~');
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
function _contextFiles(cwd) {
|
|
3508
|
+
const out = [];
|
|
3509
|
+
for (const parts of ADAPTER_OUTPUTS) {
|
|
3510
|
+
const p = path.join(cwd, ...parts);
|
|
3511
|
+
try { if (fs.existsSync(p)) out.push(p); } catch (_) {}
|
|
3512
|
+
}
|
|
3513
|
+
return out;
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function _mcpTargets(cwd) {
|
|
3517
|
+
return [
|
|
3518
|
+
path.join(cwd, '.mcp.json'),
|
|
3519
|
+
path.join(cwd, '.claude', 'settings.json'),
|
|
3520
|
+
path.join(cwd, '.cursor', 'mcp.json'),
|
|
3521
|
+
path.join(cwd, '.windsurf', 'mcp.json'),
|
|
3522
|
+
path.join(cwd, '.vscode', 'mcp.json'),
|
|
3523
|
+
path.join(cwd, 'opencode.json'),
|
|
3524
|
+
path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
3525
|
+
path.join(os.homedir(), '.config', 'opencode', 'config.json'),
|
|
3526
|
+
path.join(os.homedir(), '.gemini', 'settings.json'),
|
|
3527
|
+
path.join(os.homedir(), '.codex', 'config.yaml'),
|
|
3528
|
+
path.join(os.homedir(), '.config', 'zed', 'settings.json'),
|
|
3529
|
+
];
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
/** Count code files under srcDirs modified after the context was generated. */
|
|
3533
|
+
function _countChangedSince(cwd, srcDirs, config, ctxMtime) {
|
|
3534
|
+
const { CODE_EXTS } = __require('./src/analysis/coverage-score');
|
|
3535
|
+
const exclude = new Set(EXCLUDE_DIRS);
|
|
3536
|
+
if (config && Array.isArray(config.exclude)) for (const x of config.exclude) exclude.add(String(x));
|
|
3537
|
+
|
|
3538
|
+
let changed = 0;
|
|
3539
|
+
let seen = 0;
|
|
3540
|
+
const walk = (dir, depth) => {
|
|
3541
|
+
if (depth > 8 || seen > 5000) return;
|
|
3542
|
+
let entries;
|
|
3543
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
3544
|
+
for (const e of entries) {
|
|
3545
|
+
if (exclude.has(e.name)) continue;
|
|
3546
|
+
const full = path.join(dir, e.name);
|
|
3547
|
+
if (e.isDirectory()) walk(full, depth + 1);
|
|
3548
|
+
else if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
3549
|
+
seen++;
|
|
3550
|
+
try { if (fs.statSync(full).mtimeMs > ctxMtime) changed++; } catch (_) {}
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
};
|
|
3554
|
+
for (const d of srcDirs) {
|
|
3555
|
+
const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
|
|
3556
|
+
if (fs.existsSync(abs)) walk(abs, 0);
|
|
3557
|
+
}
|
|
3558
|
+
return changed;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
/**
|
|
3562
|
+
* Run all diagnostic checks.
|
|
3563
|
+
* @param {string} cwd
|
|
3564
|
+
* @returns {{ checks: Array<{id,label,status,detail,fix}>, ok: boolean, errors: number, warnings: number }}
|
|
3565
|
+
*/
|
|
3566
|
+
function diagnose(cwd, opts = {}) {
|
|
3567
|
+
const checks = [];
|
|
3568
|
+
const add = (id, label, status, detail, fix) => checks.push({ id, label, status, detail: detail || '', fix: fix || null });
|
|
3569
|
+
|
|
3570
|
+
// 1. Git repository
|
|
3571
|
+
try {
|
|
3572
|
+
const { tryGit } = __require('./src/util/git');
|
|
3573
|
+
const inside = tryGit(['rev-parse', '--is-inside-work-tree'], { cwd });
|
|
3574
|
+
if (inside === 'true') add('git', 'Git repository', 'ok', 'recency boost + impact analysis enabled');
|
|
3575
|
+
else add('git', 'Git repository', 'warn', 'not a git repository', 'git init — enables recency boost and impact analysis');
|
|
3576
|
+
} catch (_) {
|
|
3577
|
+
add('git', 'Git repository', 'warn', 'git not available', 'install git for recency boost + impact analysis');
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
// 2. Config & source roots
|
|
3581
|
+
let config = {};
|
|
3582
|
+
try {
|
|
3583
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
3584
|
+
let configBroken = false;
|
|
3585
|
+
if (fs.existsSync(cfgPath)) {
|
|
3586
|
+
try { JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
|
|
3587
|
+
catch (e) {
|
|
3588
|
+
configBroken = true;
|
|
3589
|
+
add('config', 'Config & source roots', 'fail', `gen-context.config.json is invalid JSON: ${e.message}`, 'fix the JSON syntax, or delete the file to fall back to defaults');
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
3593
|
+
config = loadConfig(cwd) || {};
|
|
3594
|
+
if (!configBroken) {
|
|
3595
|
+
// Distinguish explicitly-configured srcDirs (validate each) from the
|
|
3596
|
+
// default candidate list (just report which ones actually exist).
|
|
3597
|
+
let explicitSrcDirs = null;
|
|
3598
|
+
if (fs.existsSync(cfgPath)) {
|
|
3599
|
+
try { const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); if (Array.isArray(raw.srcDirs)) explicitSrcDirs = raw.srcDirs; } catch (_) {}
|
|
3600
|
+
}
|
|
3601
|
+
const exists = (d) => { try { return fs.existsSync(path.isAbsolute(d) ? d : path.join(cwd, d)); } catch (_) { return false; } };
|
|
3602
|
+
const srcDirs = Array.isArray(config.srcDirs) ? config.srcDirs : [];
|
|
3603
|
+
const present = srcDirs.filter(exists);
|
|
3604
|
+
|
|
3605
|
+
if (explicitSrcDirs) {
|
|
3606
|
+
const missing = explicitSrcDirs.filter((d) => !exists(d));
|
|
3607
|
+
if (missing.length) add('config', 'Config & source roots', 'warn', `configured srcDirs not found: ${missing.join(', ')}`, 'fix "srcDirs" in gen-context.config.json');
|
|
3608
|
+
else add('config', 'Config & source roots', 'ok', `srcDirs: ${explicitSrcDirs.join(', ')}`);
|
|
3609
|
+
} else if (present.length === 0) {
|
|
3610
|
+
add('config', 'Config & source roots', 'warn', 'no source directories found (looked for src/, app/, lib/, …)', 'create a src/ dir, set "srcDirs" in gen-context.config.json, or run: sigmap roots --fix');
|
|
3611
|
+
} else {
|
|
3612
|
+
add('config', 'Config & source roots', 'ok', `source roots: ${present.slice(0, 8).join(', ')}${present.length > 8 ? `, +${present.length - 8} more` : ''}`);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
} catch (e) {
|
|
3616
|
+
if (!checks.some((c) => c.id === 'config')) add('config', 'Config & source roots', 'warn', `could not load config: ${e.message}`);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// 3. Generated context file
|
|
3620
|
+
const ctxFiles = _contextFiles(cwd);
|
|
3621
|
+
if (ctxFiles.length === 0) {
|
|
3622
|
+
add('context', 'Generated context', 'fail', 'no context file found', 'run: npx sigmap (generates the signature map)');
|
|
3623
|
+
} else {
|
|
3624
|
+
add('context', 'Generated context', 'ok', `${ctxFiles.length} file(s): ${ctxFiles.map((f) => _short(f, cwd)).join(', ')}`);
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
// 4. Signature index
|
|
3628
|
+
let indexSize = 0;
|
|
3629
|
+
try {
|
|
3630
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
3631
|
+
indexSize = buildSigIndex(cwd).size;
|
|
3632
|
+
} catch (_) {}
|
|
3633
|
+
if (indexSize === 0) {
|
|
3634
|
+
add('index', 'Signature index', ctxFiles.length === 0 ? 'fail' : 'warn', 'no signatures indexed', 'run: npx sigmap then: sigmap ask "<query>"');
|
|
3635
|
+
} else {
|
|
3636
|
+
add('index', 'Signature index', 'ok', `${indexSize} file(s) indexed`);
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// 5. Index freshness
|
|
3640
|
+
try {
|
|
3641
|
+
if (ctxFiles.length) {
|
|
3642
|
+
const ctxMtime = Math.max(...ctxFiles.map((f) => { try { return fs.statSync(f).mtimeMs; } catch (_) { return 0; } }));
|
|
3643
|
+
const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length) ? config.srcDirs : ['src', 'app', 'lib'];
|
|
3644
|
+
const changed = _countChangedSince(cwd, srcDirs, config, ctxMtime);
|
|
3645
|
+
if (changed > 0) add('freshness', 'Index freshness', 'warn', `${changed} source file(s) changed since last generate`, 'run: sigmap (or: sigmap --watch to auto-refresh)');
|
|
3646
|
+
else add('freshness', 'Index freshness', 'ok', 'index is up to date with sources');
|
|
3647
|
+
}
|
|
3648
|
+
} catch (_) {}
|
|
3649
|
+
|
|
3650
|
+
// 6. Coverage
|
|
3651
|
+
try {
|
|
3652
|
+
if (indexSize > 0) {
|
|
3653
|
+
const { coverageScore } = __require('./src/analysis/coverage-score');
|
|
3654
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
3655
|
+
const entries = [...buildSigIndex(cwd).keys()].map((rel) => ({ filePath: path.resolve(cwd, rel) }));
|
|
3656
|
+
const cov = coverageScore(cwd, entries, config);
|
|
3657
|
+
if (cov.score < 70) add('coverage', 'Coverage', 'warn', `${cov.score}% of source files in context (grade ${cov.grade})`, 'increase maxTokens or expand srcDirs in gen-context.config.json');
|
|
3658
|
+
else add('coverage', 'Coverage', 'ok', `${cov.score}% of source files in context (grade ${cov.grade})`);
|
|
3659
|
+
}
|
|
3660
|
+
} catch (_) {}
|
|
3661
|
+
|
|
3662
|
+
// 7. MCP wiring
|
|
3663
|
+
try {
|
|
3664
|
+
let wired = null;
|
|
3665
|
+
for (const t of _mcpTargets(cwd)) {
|
|
3666
|
+
try {
|
|
3667
|
+
if (!fs.existsSync(t)) continue;
|
|
3668
|
+
if (/sigmap/.test(fs.readFileSync(t, 'utf8'))) { wired = t; break; }
|
|
3669
|
+
} catch (_) {}
|
|
3670
|
+
}
|
|
3671
|
+
if (wired) add('mcp', 'MCP wiring', 'ok', `registered in ${_short(wired, cwd)}`);
|
|
3672
|
+
else add('mcp', 'MCP wiring', 'warn', 'MCP server not registered in any editor config', 'run: sigmap --setup (auto-wires Claude, Cursor, Windsurf, VS Code, …)');
|
|
3673
|
+
} catch (_) {}
|
|
3674
|
+
|
|
3675
|
+
const errors = checks.filter((c) => c.status === 'fail').length;
|
|
3676
|
+
const warnings = checks.filter((c) => c.status === 'warn').length;
|
|
3677
|
+
return { checks, ok: errors === 0, errors, warnings };
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/** Human-readable checklist. */
|
|
3681
|
+
function formatDoctor(result) {
|
|
3682
|
+
const lines = ['sigmap doctor', ''];
|
|
3683
|
+
for (const c of result.checks) {
|
|
3684
|
+
lines.push(`${ICON[c.status] || '?'} ${c.label}${c.detail ? ' — ' + c.detail : ''}`);
|
|
3685
|
+
if (c.status !== 'ok' && c.fix) lines.push(` ↳ ${c.fix}`);
|
|
3686
|
+
}
|
|
3687
|
+
lines.push('');
|
|
3688
|
+
lines.push(
|
|
3689
|
+
result.errors === 0 && result.warnings === 0
|
|
3690
|
+
? '✓ All checks passed.'
|
|
3691
|
+
: `${result.errors} error(s), ${result.warnings} warning(s).`
|
|
3692
|
+
);
|
|
3693
|
+
return lines.join('\n');
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/** Machine-readable result. */
|
|
3697
|
+
function formatDoctorJSON(result) {
|
|
3698
|
+
return JSON.stringify(result, null, 2);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
module.exports = { diagnose, formatDoctor, formatDoctorJSON };
|
|
3702
|
+
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3465
3705
|
// ── ./src/eval/analyzer ──
|
|
3466
3706
|
__factories["./src/eval/analyzer"] = function(module, exports) {
|
|
3467
3707
|
|
|
@@ -12109,7 +12349,182 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
12109
12349
|
}
|
|
12110
12350
|
}
|
|
12111
12351
|
|
|
12112
|
-
|
|
12352
|
+
/**
|
|
12353
|
+
* List the files changed in the working tree, staged area, or vs a base ref.
|
|
12354
|
+
* Shell-free (routes through src/util/git.js). Returns relative paths.
|
|
12355
|
+
*/
|
|
12356
|
+
function _changedFiles(cwd, args) {
|
|
12357
|
+
const { tryGit } = __require('./src/util/git');
|
|
12358
|
+
let out = '';
|
|
12359
|
+
if (args.base) {
|
|
12360
|
+
if (!/^[A-Za-z0-9._/\-~^]+$/.test(args.base)) return [];
|
|
12361
|
+
out = tryGit(['diff', `${args.base}..HEAD`, '--name-only'], { cwd });
|
|
12362
|
+
} else if (args.staged) {
|
|
12363
|
+
out = tryGit(['diff', '--cached', '--name-only'], { cwd });
|
|
12364
|
+
} else {
|
|
12365
|
+
out = tryGit(['diff', 'HEAD', '--name-only'], { cwd });
|
|
12366
|
+
}
|
|
12367
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
12368
|
+
}
|
|
12369
|
+
|
|
12370
|
+
/**
|
|
12371
|
+
* get_diff_context({ base?, staged?, depth? }) → string
|
|
12372
|
+
*
|
|
12373
|
+
* For each changed file: its extracted signatures + blast radius (direct
|
|
12374
|
+
* importers, transitive count, affected tests/routes) + a risk label.
|
|
12375
|
+
* Hands an agent everything a review or a safe edit needs in one call.
|
|
12376
|
+
*/
|
|
12377
|
+
function getDiffContext(args, cwd) {
|
|
12378
|
+
try {
|
|
12379
|
+
const a = args || {};
|
|
12380
|
+
const files = _changedFiles(cwd, a);
|
|
12381
|
+
if (files.length === 0) {
|
|
12382
|
+
return '_No changed files detected._ (Outside a git repo, or the working tree / selected range is clean.)';
|
|
12383
|
+
}
|
|
12384
|
+
|
|
12385
|
+
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
12386
|
+
const { analyzeImpact } = __require('./src/graph/impact');
|
|
12387
|
+
let riskLabelFor;
|
|
12388
|
+
try { ({ riskLabelFor } = __require('./src/evidence/pack')); } catch (_) { riskLabelFor = () => 'source'; }
|
|
12389
|
+
|
|
12390
|
+
const depth = Math.max(0, parseInt(a.depth, 10) || 2);
|
|
12391
|
+
const srcFiles = files.filter((f) => langFor(f));
|
|
12392
|
+
let impactByFile = new Map();
|
|
12393
|
+
try {
|
|
12394
|
+
const impacts = analyzeImpact(srcFiles, cwd, { depth });
|
|
12395
|
+
impactByFile = new Map(impacts.map((r) => [r.file, r.impact]));
|
|
12396
|
+
} catch (_) { /* graph optional */ }
|
|
12397
|
+
|
|
12398
|
+
const scope = a.base ? `vs ${a.base}` : (a.staged ? 'staged' : 'working tree');
|
|
12399
|
+
const out = [
|
|
12400
|
+
`# Diff context (${scope})`,
|
|
12401
|
+
'',
|
|
12402
|
+
`**${files.length} changed file${files.length === 1 ? '' : 's'}** · ${srcFiles.length} with extractable signatures`,
|
|
12403
|
+
'',
|
|
12404
|
+
];
|
|
12405
|
+
|
|
12406
|
+
for (const rel of files) {
|
|
12407
|
+
out.push(`## \`${rel}\``);
|
|
12408
|
+
if (!langFor(rel)) { out.push('_non-source file (no signatures)_', ''); continue; }
|
|
12409
|
+
|
|
12410
|
+
out.push(`_risk: ${riskLabelFor(rel)}_`);
|
|
12411
|
+
const impact = impactByFile.get(rel);
|
|
12412
|
+
if (impact) {
|
|
12413
|
+
out.push(
|
|
12414
|
+
`**Blast radius:** ${impact.totalImpact} file(s) impacted — ` +
|
|
12415
|
+
`${impact.direct.length} direct importer(s), ${impact.transitive.length} transitive` +
|
|
12416
|
+
(impact.tests.length ? `, ${impact.tests.length} test(s)` : '') +
|
|
12417
|
+
(impact.routes.length ? `, ${impact.routes.length} route(s)` : '')
|
|
12418
|
+
);
|
|
12419
|
+
if (impact.direct.length) {
|
|
12420
|
+
out.push(`Direct importers: ${impact.direct.slice(0, 8).map((f) => '`' + f + '`').join(', ')}` + (impact.direct.length > 8 ? ' …' : ''));
|
|
12421
|
+
}
|
|
12422
|
+
if (impact.tests.length) {
|
|
12423
|
+
out.push(`Tests to run: ${impact.tests.slice(0, 8).map((f) => '`' + f + '`').join(', ')}`);
|
|
12424
|
+
}
|
|
12425
|
+
} else {
|
|
12426
|
+
out.push('**Blast radius:** (not in dependency graph — new or leaf file)');
|
|
12427
|
+
}
|
|
12428
|
+
out.push('');
|
|
12429
|
+
|
|
12430
|
+
let src = '';
|
|
12431
|
+
try { src = fs.readFileSync(path.resolve(cwd, rel), 'utf8'); } catch (_) {}
|
|
12432
|
+
const sigs = src ? extractFile(rel, src) : [];
|
|
12433
|
+
if (sigs.length) {
|
|
12434
|
+
out.push('```');
|
|
12435
|
+
for (const s of sigs.slice(0, 40)) out.push(s);
|
|
12436
|
+
if (sigs.length > 40) out.push(`… +${sigs.length - 40} more`);
|
|
12437
|
+
out.push('```');
|
|
12438
|
+
} else {
|
|
12439
|
+
out.push('_(no signatures extracted — file may be deleted or empty)_');
|
|
12440
|
+
}
|
|
12441
|
+
out.push('');
|
|
12442
|
+
}
|
|
12443
|
+
|
|
12444
|
+
return out.join('\n');
|
|
12445
|
+
} catch (err) {
|
|
12446
|
+
return `_get_diff_context failed: ${err.message}_`;
|
|
12447
|
+
}
|
|
12448
|
+
}
|
|
12449
|
+
|
|
12450
|
+
/**
|
|
12451
|
+
* get_architecture_overview({}) → string
|
|
12452
|
+
*
|
|
12453
|
+
* A high-level map of the codebase: module breakdown (files/tokens), the most
|
|
12454
|
+
* depended-on "hub" files, dependency-cycle count, and route totals. Extends
|
|
12455
|
+
* get_map — one call to orient in an unfamiliar repo.
|
|
12456
|
+
*/
|
|
12457
|
+
function getArchitectureOverview(args, cwd) {
|
|
12458
|
+
try {
|
|
12459
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
12460
|
+
const index = buildSigIndex(cwd);
|
|
12461
|
+
const out = ['# Architecture overview', ''];
|
|
12462
|
+
|
|
12463
|
+
if (index.size === 0) {
|
|
12464
|
+
out.push('_No context file found. Run: node gen-context.js_', '');
|
|
12465
|
+
} else {
|
|
12466
|
+
const groups = {};
|
|
12467
|
+
let totalTokens = 0;
|
|
12468
|
+
let totalFiles = 0;
|
|
12469
|
+
for (const [rel, sigs] of index.entries()) {
|
|
12470
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
12471
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
12472
|
+
const tok = Math.ceil(sigs.join('\n').length / 4);
|
|
12473
|
+
if (!groups[mod]) groups[mod] = { files: 0, tokens: 0 };
|
|
12474
|
+
groups[mod].files++;
|
|
12475
|
+
groups[mod].tokens += tok;
|
|
12476
|
+
totalTokens += tok;
|
|
12477
|
+
totalFiles++;
|
|
12478
|
+
}
|
|
12479
|
+
const sorted = Object.entries(groups)
|
|
12480
|
+
.map(([mod, d]) => ({ mod, files: d.files, tokens: d.tokens }))
|
|
12481
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
12482
|
+
|
|
12483
|
+
out.push(`**${totalFiles} indexed files · ${sorted.length} modules · ~${totalTokens} tokens**`, '');
|
|
12484
|
+
out.push('## Modules', '| Module | Files | Tokens |', '|--------|-------|--------|');
|
|
12485
|
+
for (const m of sorted.slice(0, 20)) out.push(`| ${m.mod} | ${m.files} | ~${m.tokens} |`);
|
|
12486
|
+
out.push('');
|
|
12487
|
+
}
|
|
12488
|
+
|
|
12489
|
+
// Hub files + cycle count from the dependency graph (optional).
|
|
12490
|
+
try {
|
|
12491
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
12492
|
+
const { detectCycles } = __require('./src/map/import-graph');
|
|
12493
|
+
const graph = buildFromCwd(cwd);
|
|
12494
|
+
if (graph && graph.reverse && graph.reverse.size) {
|
|
12495
|
+
const hubs = [...graph.reverse.entries()]
|
|
12496
|
+
.map(([f, importers]) => ({ file: path.relative(cwd, f).replace(/\\/g, '/'), in: importers.length }))
|
|
12497
|
+
.filter((h) => h.in > 0)
|
|
12498
|
+
.sort((a, b) => b.in - a.in)
|
|
12499
|
+
.slice(0, 10);
|
|
12500
|
+
if (hubs.length) {
|
|
12501
|
+
out.push('## Hub files (most depended-on)', '| File | Importers |', '|------|-----------|');
|
|
12502
|
+
for (const h of hubs) out.push(`| \`${h.file}\` | ${h.in} |`);
|
|
12503
|
+
out.push('');
|
|
12504
|
+
}
|
|
12505
|
+
let cycleCount = 0;
|
|
12506
|
+
try { cycleCount = detectCycles(graph.forward).length; } catch (_) {}
|
|
12507
|
+
out.push(`**Dependency cycles:** ${cycleCount}` + (cycleCount ? ' _(see import graph)_' : ' — none detected'), '');
|
|
12508
|
+
}
|
|
12509
|
+
} catch (_) { /* graph optional */ }
|
|
12510
|
+
|
|
12511
|
+
// Routes from PROJECT_MAP.md if present.
|
|
12512
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
12513
|
+
if (fs.existsSync(mapPath)) {
|
|
12514
|
+
const mc = fs.readFileSync(mapPath, 'utf8');
|
|
12515
|
+
const routeCount = mc.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---')).length;
|
|
12516
|
+
out.push('## Project map', `Routes detected: ${routeCount} _(use get_map for imports/classes/routes detail)_`, '');
|
|
12517
|
+
} else {
|
|
12518
|
+
out.push('_Run `node gen-project-map.js` for routes / class-hierarchy detail (get_map)._');
|
|
12519
|
+
}
|
|
12520
|
+
|
|
12521
|
+
return out.join('\n');
|
|
12522
|
+
} catch (err) {
|
|
12523
|
+
return `_get_architecture_overview failed: ${err.message}_`;
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12526
|
+
|
|
12527
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview };
|
|
12113
12528
|
|
|
12114
12529
|
};
|
|
12115
12530
|
|
|
@@ -12124,17 +12539,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
12124
12539
|
*
|
|
12125
12540
|
* Supported methods:
|
|
12126
12541
|
* initialize → serverInfo + capabilities
|
|
12127
|
-
* tools/list →
|
|
12542
|
+
* tools/list → 17 tool definitions
|
|
12128
12543
|
* tools/call → dispatch to handler, return result
|
|
12129
12544
|
*/
|
|
12130
12545
|
|
|
12131
12546
|
const readline = require('readline');
|
|
12132
12547
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
12133
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = __require('./src/mcp/handlers');
|
|
12548
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview } = __require('./src/mcp/handlers');
|
|
12134
12549
|
|
|
12135
12550
|
const SERVER_INFO = {
|
|
12136
12551
|
name: 'sigmap',
|
|
12137
|
-
version: '7.
|
|
12552
|
+
version: '7.28.0',
|
|
12138
12553
|
description: 'SigMap MCP server — code signatures on demand',
|
|
12139
12554
|
};
|
|
12140
12555
|
|
|
@@ -12197,6 +12612,8 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
12197
12612
|
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
12198
12613
|
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
12199
12614
|
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
12615
|
+
else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
|
|
12616
|
+
else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
|
|
12200
12617
|
else {
|
|
12201
12618
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
12202
12619
|
return;
|
|
@@ -12257,11 +12674,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
12257
12674
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
12258
12675
|
|
|
12259
12676
|
/**
|
|
12260
|
-
* MCP tool definitions for SigMap (
|
|
12677
|
+
* MCP tool definitions for SigMap (17 tools).
|
|
12261
12678
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
12262
12679
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
12263
12680
|
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
12264
|
-
* sigmap_notify_file_deleted.
|
|
12681
|
+
* sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
|
|
12265
12682
|
*/
|
|
12266
12683
|
|
|
12267
12684
|
const TOOLS = [
|
|
@@ -12534,6 +12951,44 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
12534
12951
|
required: ['path'],
|
|
12535
12952
|
},
|
|
12536
12953
|
},
|
|
12954
|
+
{
|
|
12955
|
+
name: 'get_diff_context',
|
|
12956
|
+
description:
|
|
12957
|
+
'For every changed file in the working tree (or staged, or vs a base ref), return its ' +
|
|
12958
|
+
'current signatures plus blast radius — direct importers, transitive count, and affected ' +
|
|
12959
|
+
'tests/routes — with a risk label. One call gives an agent everything a code review or a ' +
|
|
12960
|
+
'safe edit needs. Lists changed files shell-free (git binary, never a shell).',
|
|
12961
|
+
inputSchema: {
|
|
12962
|
+
type: 'object',
|
|
12963
|
+
properties: {
|
|
12964
|
+
base: {
|
|
12965
|
+
type: 'string',
|
|
12966
|
+
description: 'Optional git ref to diff against (e.g. "main"). Returns files changed in `base..HEAD`. Omit for working-tree changes.',
|
|
12967
|
+
},
|
|
12968
|
+
staged: {
|
|
12969
|
+
type: 'boolean',
|
|
12970
|
+
description: 'When true (and no base), report only staged changes (`git diff --cached`).',
|
|
12971
|
+
},
|
|
12972
|
+
depth: {
|
|
12973
|
+
type: 'number',
|
|
12974
|
+
description: 'Blast-radius BFS depth limit (default: 2). Use 0 for unlimited.',
|
|
12975
|
+
},
|
|
12976
|
+
},
|
|
12977
|
+
required: [],
|
|
12978
|
+
},
|
|
12979
|
+
},
|
|
12980
|
+
{
|
|
12981
|
+
name: 'get_architecture_overview',
|
|
12982
|
+
description:
|
|
12983
|
+
'A high-level map of the codebase in one call: module breakdown (files/tokens), the most ' +
|
|
12984
|
+
'depended-on "hub" files, the dependency-cycle count, and route totals. Extends get_map — ' +
|
|
12985
|
+
'use it to orient in an unfamiliar repo before drilling in with read_context / query_context.',
|
|
12986
|
+
inputSchema: {
|
|
12987
|
+
type: 'object',
|
|
12988
|
+
properties: {},
|
|
12989
|
+
required: [],
|
|
12990
|
+
},
|
|
12991
|
+
},
|
|
12537
12992
|
];
|
|
12538
12993
|
|
|
12539
12994
|
module.exports = { TOOLS };
|
|
@@ -15923,7 +16378,7 @@ function __tryGit(args, opts = {}) {
|
|
|
15923
16378
|
catch (_) { return ''; }
|
|
15924
16379
|
}
|
|
15925
16380
|
|
|
15926
|
-
const VERSION = '7.
|
|
16381
|
+
const VERSION = '7.28.0';
|
|
15927
16382
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
15928
16383
|
|
|
15929
16384
|
function requireSourceOrBundled(key) {
|
|
@@ -17736,6 +18191,7 @@ Usage:
|
|
|
17736
18191
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
17737
18192
|
${cmd} note List recent notes (also: note --list <N>)
|
|
17738
18193
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
18194
|
+
${cmd} doctor Diagnose config, index, freshness, coverage, MCP wiring — with fixes (--json; exits 1 on hard failure)
|
|
17739
18195
|
${cmd} --init Write example config + .contextignore scaffold
|
|
17740
18196
|
${cmd} --help Show this message
|
|
17741
18197
|
${cmd} --version Show version
|
|
@@ -19160,6 +19616,20 @@ function main() {
|
|
|
19160
19616
|
process.exit(0);
|
|
19161
19617
|
}
|
|
19162
19618
|
|
|
19619
|
+
// `sigmap doctor` — diagnose config, index, freshness, coverage, and MCP
|
|
19620
|
+
// wiring; print an actionable fix for anything wrong. Exit 1 on a hard
|
|
19621
|
+
// failure (no context file / invalid config) so it is usable in CI.
|
|
19622
|
+
if (args[0] === 'doctor') {
|
|
19623
|
+
const { diagnose, formatDoctor, formatDoctorJSON } = requireSourceOrBundled('./src/doctor/diagnose');
|
|
19624
|
+
const result = diagnose(cwd);
|
|
19625
|
+
if (args.includes('--json')) {
|
|
19626
|
+
process.stdout.write(formatDoctorJSON(result) + '\n');
|
|
19627
|
+
} else {
|
|
19628
|
+
console.log(formatDoctor(result));
|
|
19629
|
+
}
|
|
19630
|
+
process.exit(result.ok ? 0 : 1);
|
|
19631
|
+
}
|
|
19632
|
+
|
|
19163
19633
|
// Layer 3: `sigmap conventions` — extract & report repo coding conventions
|
|
19164
19634
|
// (file naming, export style, test framework) for TS/JS/Python so generated
|
|
19165
19635
|
// code matches the house style. Writes .context/conventions.json.
|
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.28.0 | Benchmark: sigmap-v7.28-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.28-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
|
---
|
|
@@ -115,6 +115,7 @@ sigmap evidence "<query>" --top <n> --budget <n> --out <path> Tune ranked file
|
|
|
115
115
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
116
116
|
sigmap note List recent notes (also: note --list <N>)
|
|
117
117
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
|
118
|
+
sigmap doctor Diagnose config, index, freshness, coverage, MCP wiring — with fixes (--json; exits 1 on hard failure)
|
|
118
119
|
sigmap --init Write example config + .contextignore scaffold
|
|
119
120
|
sigmap --help Show this message
|
|
120
121
|
sigmap --version Show version
|
|
@@ -122,7 +123,7 @@ sigmap --version Show version
|
|
|
122
123
|
|
|
123
124
|
---
|
|
124
125
|
|
|
125
|
-
## MCP server —
|
|
126
|
+
## MCP server — 17 tools
|
|
126
127
|
|
|
127
128
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
128
129
|
|
|
@@ -250,6 +251,22 @@ Tell SigMap a file was deleted so its symbols are dropped from the live index.
|
|
|
250
251
|
Input: { path: string }
|
|
251
252
|
```
|
|
252
253
|
|
|
254
|
+
### get_diff_context
|
|
255
|
+
|
|
256
|
+
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).
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
Input: { base?: string, staged?: boolean, depth?: number }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### get_architecture_overview
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
Input: { } (no arguments)
|
|
268
|
+
```
|
|
269
|
+
|
|
253
270
|
---
|
|
254
271
|
|
|
255
272
|
## 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.28.0 | Benchmark: sigmap-v7.28-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.28-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.28.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,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sigmap doctor (v8.0 E3).
|
|
5
|
+
*
|
|
6
|
+
* One-shot diagnostic for a SigMap setup: each check reports ok/warn/fail with
|
|
7
|
+
* an actionable fix, so a cold user can reach a useful answer in minutes. Pure,
|
|
8
|
+
* resilient (no check ever throws), zero new runtime deps. Composes the config
|
|
9
|
+
* loader, coverage scorer, signature index, and the known adapter-output /
|
|
10
|
+
* MCP-config paths.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
// Generated context files a `read_context`/`ask` flow can consume.
|
|
18
|
+
const ADAPTER_OUTPUTS = [
|
|
19
|
+
['.github', 'copilot-instructions.md'],
|
|
20
|
+
['CLAUDE.md'],
|
|
21
|
+
['AGENTS.md'],
|
|
22
|
+
['.cursorrules'],
|
|
23
|
+
['.windsurfrules'],
|
|
24
|
+
['.github', 'openai-context.md'],
|
|
25
|
+
['.github', 'gemini-context.md'],
|
|
26
|
+
['llm-full.txt'],
|
|
27
|
+
['llm.txt'],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const EXCLUDE_DIRS = new Set([
|
|
31
|
+
'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
|
|
32
|
+
'.next', 'coverage', 'target', 'vendor', '.context',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const ICON = { ok: '✓', warn: '⚠', fail: '✗' };
|
|
36
|
+
|
|
37
|
+
function _short(p, cwd) {
|
|
38
|
+
const rel = path.relative(cwd, p);
|
|
39
|
+
return rel && !rel.startsWith('..') ? rel : p.replace(os.homedir(), '~');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _contextFiles(cwd) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const parts of ADAPTER_OUTPUTS) {
|
|
45
|
+
const p = path.join(cwd, ...parts);
|
|
46
|
+
try { if (fs.existsSync(p)) out.push(p); } catch (_) {}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _mcpTargets(cwd) {
|
|
52
|
+
return [
|
|
53
|
+
path.join(cwd, '.mcp.json'),
|
|
54
|
+
path.join(cwd, '.claude', 'settings.json'),
|
|
55
|
+
path.join(cwd, '.cursor', 'mcp.json'),
|
|
56
|
+
path.join(cwd, '.windsurf', 'mcp.json'),
|
|
57
|
+
path.join(cwd, '.vscode', 'mcp.json'),
|
|
58
|
+
path.join(cwd, 'opencode.json'),
|
|
59
|
+
path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
60
|
+
path.join(os.homedir(), '.config', 'opencode', 'config.json'),
|
|
61
|
+
path.join(os.homedir(), '.gemini', 'settings.json'),
|
|
62
|
+
path.join(os.homedir(), '.codex', 'config.yaml'),
|
|
63
|
+
path.join(os.homedir(), '.config', 'zed', 'settings.json'),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Count code files under srcDirs modified after the context was generated. */
|
|
68
|
+
function _countChangedSince(cwd, srcDirs, config, ctxMtime) {
|
|
69
|
+
const { CODE_EXTS } = require('../analysis/coverage-score');
|
|
70
|
+
const exclude = new Set(EXCLUDE_DIRS);
|
|
71
|
+
if (config && Array.isArray(config.exclude)) for (const x of config.exclude) exclude.add(String(x));
|
|
72
|
+
|
|
73
|
+
let changed = 0;
|
|
74
|
+
let seen = 0;
|
|
75
|
+
const walk = (dir, depth) => {
|
|
76
|
+
if (depth > 8 || seen > 5000) return;
|
|
77
|
+
let entries;
|
|
78
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
if (exclude.has(e.name)) continue;
|
|
81
|
+
const full = path.join(dir, e.name);
|
|
82
|
+
if (e.isDirectory()) walk(full, depth + 1);
|
|
83
|
+
else if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
84
|
+
seen++;
|
|
85
|
+
try { if (fs.statSync(full).mtimeMs > ctxMtime) changed++; } catch (_) {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
for (const d of srcDirs) {
|
|
90
|
+
const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
|
|
91
|
+
if (fs.existsSync(abs)) walk(abs, 0);
|
|
92
|
+
}
|
|
93
|
+
return changed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run all diagnostic checks.
|
|
98
|
+
* @param {string} cwd
|
|
99
|
+
* @returns {{ checks: Array<{id,label,status,detail,fix}>, ok: boolean, errors: number, warnings: number }}
|
|
100
|
+
*/
|
|
101
|
+
function diagnose(cwd, opts = {}) {
|
|
102
|
+
const checks = [];
|
|
103
|
+
const add = (id, label, status, detail, fix) => checks.push({ id, label, status, detail: detail || '', fix: fix || null });
|
|
104
|
+
|
|
105
|
+
// 1. Git repository
|
|
106
|
+
try {
|
|
107
|
+
const { tryGit } = require('../util/git');
|
|
108
|
+
const inside = tryGit(['rev-parse', '--is-inside-work-tree'], { cwd });
|
|
109
|
+
if (inside === 'true') add('git', 'Git repository', 'ok', 'recency boost + impact analysis enabled');
|
|
110
|
+
else add('git', 'Git repository', 'warn', 'not a git repository', 'git init — enables recency boost and impact analysis');
|
|
111
|
+
} catch (_) {
|
|
112
|
+
add('git', 'Git repository', 'warn', 'git not available', 'install git for recency boost + impact analysis');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Config & source roots
|
|
116
|
+
let config = {};
|
|
117
|
+
try {
|
|
118
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
119
|
+
let configBroken = false;
|
|
120
|
+
if (fs.existsSync(cfgPath)) {
|
|
121
|
+
try { JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
|
|
122
|
+
catch (e) {
|
|
123
|
+
configBroken = true;
|
|
124
|
+
add('config', 'Config & source roots', 'fail', `gen-context.config.json is invalid JSON: ${e.message}`, 'fix the JSON syntax, or delete the file to fall back to defaults');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const { loadConfig } = require('../config/loader');
|
|
128
|
+
config = loadConfig(cwd) || {};
|
|
129
|
+
if (!configBroken) {
|
|
130
|
+
// Distinguish explicitly-configured srcDirs (validate each) from the
|
|
131
|
+
// default candidate list (just report which ones actually exist).
|
|
132
|
+
let explicitSrcDirs = null;
|
|
133
|
+
if (fs.existsSync(cfgPath)) {
|
|
134
|
+
try { const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); if (Array.isArray(raw.srcDirs)) explicitSrcDirs = raw.srcDirs; } catch (_) {}
|
|
135
|
+
}
|
|
136
|
+
const exists = (d) => { try { return fs.existsSync(path.isAbsolute(d) ? d : path.join(cwd, d)); } catch (_) { return false; } };
|
|
137
|
+
const srcDirs = Array.isArray(config.srcDirs) ? config.srcDirs : [];
|
|
138
|
+
const present = srcDirs.filter(exists);
|
|
139
|
+
|
|
140
|
+
if (explicitSrcDirs) {
|
|
141
|
+
const missing = explicitSrcDirs.filter((d) => !exists(d));
|
|
142
|
+
if (missing.length) add('config', 'Config & source roots', 'warn', `configured srcDirs not found: ${missing.join(', ')}`, 'fix "srcDirs" in gen-context.config.json');
|
|
143
|
+
else add('config', 'Config & source roots', 'ok', `srcDirs: ${explicitSrcDirs.join(', ')}`);
|
|
144
|
+
} else if (present.length === 0) {
|
|
145
|
+
add('config', 'Config & source roots', 'warn', 'no source directories found (looked for src/, app/, lib/, …)', 'create a src/ dir, set "srcDirs" in gen-context.config.json, or run: sigmap roots --fix');
|
|
146
|
+
} else {
|
|
147
|
+
add('config', 'Config & source roots', 'ok', `source roots: ${present.slice(0, 8).join(', ')}${present.length > 8 ? `, +${present.length - 8} more` : ''}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if (!checks.some((c) => c.id === 'config')) add('config', 'Config & source roots', 'warn', `could not load config: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Generated context file
|
|
155
|
+
const ctxFiles = _contextFiles(cwd);
|
|
156
|
+
if (ctxFiles.length === 0) {
|
|
157
|
+
add('context', 'Generated context', 'fail', 'no context file found', 'run: npx sigmap (generates the signature map)');
|
|
158
|
+
} else {
|
|
159
|
+
add('context', 'Generated context', 'ok', `${ctxFiles.length} file(s): ${ctxFiles.map((f) => _short(f, cwd)).join(', ')}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 4. Signature index
|
|
163
|
+
let indexSize = 0;
|
|
164
|
+
try {
|
|
165
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
166
|
+
indexSize = buildSigIndex(cwd).size;
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
if (indexSize === 0) {
|
|
169
|
+
add('index', 'Signature index', ctxFiles.length === 0 ? 'fail' : 'warn', 'no signatures indexed', 'run: npx sigmap then: sigmap ask "<query>"');
|
|
170
|
+
} else {
|
|
171
|
+
add('index', 'Signature index', 'ok', `${indexSize} file(s) indexed`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 5. Index freshness
|
|
175
|
+
try {
|
|
176
|
+
if (ctxFiles.length) {
|
|
177
|
+
const ctxMtime = Math.max(...ctxFiles.map((f) => { try { return fs.statSync(f).mtimeMs; } catch (_) { return 0; } }));
|
|
178
|
+
const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length) ? config.srcDirs : ['src', 'app', 'lib'];
|
|
179
|
+
const changed = _countChangedSince(cwd, srcDirs, config, ctxMtime);
|
|
180
|
+
if (changed > 0) add('freshness', 'Index freshness', 'warn', `${changed} source file(s) changed since last generate`, 'run: sigmap (or: sigmap --watch to auto-refresh)');
|
|
181
|
+
else add('freshness', 'Index freshness', 'ok', 'index is up to date with sources');
|
|
182
|
+
}
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
|
|
185
|
+
// 6. Coverage
|
|
186
|
+
try {
|
|
187
|
+
if (indexSize > 0) {
|
|
188
|
+
const { coverageScore } = require('../analysis/coverage-score');
|
|
189
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
190
|
+
const entries = [...buildSigIndex(cwd).keys()].map((rel) => ({ filePath: path.resolve(cwd, rel) }));
|
|
191
|
+
const cov = coverageScore(cwd, entries, config);
|
|
192
|
+
if (cov.score < 70) add('coverage', 'Coverage', 'warn', `${cov.score}% of source files in context (grade ${cov.grade})`, 'increase maxTokens or expand srcDirs in gen-context.config.json');
|
|
193
|
+
else add('coverage', 'Coverage', 'ok', `${cov.score}% of source files in context (grade ${cov.grade})`);
|
|
194
|
+
}
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
|
|
197
|
+
// 7. MCP wiring
|
|
198
|
+
try {
|
|
199
|
+
let wired = null;
|
|
200
|
+
for (const t of _mcpTargets(cwd)) {
|
|
201
|
+
try {
|
|
202
|
+
if (!fs.existsSync(t)) continue;
|
|
203
|
+
if (/sigmap/.test(fs.readFileSync(t, 'utf8'))) { wired = t; break; }
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
if (wired) add('mcp', 'MCP wiring', 'ok', `registered in ${_short(wired, cwd)}`);
|
|
207
|
+
else add('mcp', 'MCP wiring', 'warn', 'MCP server not registered in any editor config', 'run: sigmap --setup (auto-wires Claude, Cursor, Windsurf, VS Code, …)');
|
|
208
|
+
} catch (_) {}
|
|
209
|
+
|
|
210
|
+
const errors = checks.filter((c) => c.status === 'fail').length;
|
|
211
|
+
const warnings = checks.filter((c) => c.status === 'warn').length;
|
|
212
|
+
return { checks, ok: errors === 0, errors, warnings };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Human-readable checklist. */
|
|
216
|
+
function formatDoctor(result) {
|
|
217
|
+
const lines = ['sigmap doctor', ''];
|
|
218
|
+
for (const c of result.checks) {
|
|
219
|
+
lines.push(`${ICON[c.status] || '?'} ${c.label}${c.detail ? ' — ' + c.detail : ''}`);
|
|
220
|
+
if (c.status !== 'ok' && c.fix) lines.push(` ↳ ${c.fix}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(
|
|
224
|
+
result.errors === 0 && result.warnings === 0
|
|
225
|
+
? '✓ All checks passed.'
|
|
226
|
+
: `${result.errors} error(s), ${result.warnings} warning(s).`
|
|
227
|
+
);
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Machine-readable result. */
|
|
232
|
+
function formatDoctorJSON(result) {
|
|
233
|
+
return JSON.stringify(result, null, 2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = { diagnose, formatDoctor, formatDoctorJSON };
|
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.28.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 };
|