sigmap 7.27.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 CHANGED
@@ -10,6 +10,16 @@ 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
+
13
23
  ## [7.27.0] — 2026-06-22
14
24
 
15
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.
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.27-main (21 repositories, including R language)
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)
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
 
@@ -12309,7 +12549,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
12309
12549
 
12310
12550
  const SERVER_INFO = {
12311
12551
  name: 'sigmap',
12312
- version: '7.27.0',
12552
+ version: '7.28.0',
12313
12553
  description: 'SigMap MCP server — code signatures on demand',
12314
12554
  };
12315
12555
 
@@ -16138,7 +16378,7 @@ function __tryGit(args, opts = {}) {
16138
16378
  catch (_) { return ''; }
16139
16379
  }
16140
16380
 
16141
- const VERSION = '7.27.0';
16381
+ const VERSION = '7.28.0';
16142
16382
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
16143
16383
 
16144
16384
  function requireSourceOrBundled(key) {
@@ -17951,6 +18191,7 @@ Usage:
17951
18191
  ${cmd} note "<text>" Append a note to the cross-session decision log
17952
18192
  ${cmd} note List recent notes (also: note --list <N>)
17953
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)
17954
18195
  ${cmd} --init Write example config + .contextignore scaffold
17955
18196
  ${cmd} --help Show this message
17956
18197
  ${cmd} --version Show version
@@ -19375,6 +19616,20 @@ function main() {
19375
19616
  process.exit(0);
19376
19617
  }
19377
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
+
19378
19633
  // Layer 3: `sigmap conventions` — extract & report repo coding conventions
19379
19634
  // (file naming, export style, test framework) for TS/JS/Python so generated
19380
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.27.0 | Benchmark: sigmap-v7.27-main (2026-06-22)
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.27-main, 2026-06-22)
18
+ ## Core metrics (benchmark: sigmap-v7.28-main, 2026-06-22)
19
19
 
20
20
  | Metric | Without SigMap | With SigMap |
21
21
  |--------|----------------|-------------|
@@ -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
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.27.0 | Benchmark: sigmap-v7.27-main (2026-06-22)
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,7 +21,7 @@ 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.27-main, 2026-06-22)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "7.27.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.27.0",
3
+ "version": "7.28.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.27.0",
3
+ "version": "7.28.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '7.27.0',
21
+ version: '7.28.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24