sigmap 5.3.0 → 5.5.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,34 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [5.5.0] — 2026-04-17
14
+
15
+ ### Fixed
16
+
17
+ - **Coverage grade now accurate for mixed-content projects** — `coverageScore()` counts only code files (`.ts`, `.js`, `.py`, `.go`, etc.) in the denominator. Previously, `package.json`, `tsconfig.json`, `README.md`, and other non-code files were counted, causing inflated D-grades even when all code was covered (reported in discussion #81).
18
+ - **`--report` coverage label** — now reads `code files` instead of `source files`, and prints `(N non-code files skipped — json, md, config)` when non-code files were excluded.
19
+ - **`--report` actionable guidance** — modules marked `← attention needed` (<50% coverage) now show a tip block listing the three common causes and how to fix each.
20
+ - **`--health` label disambiguation** — coverage line renamed from `coverage … source files` to `file access … files accessible in srcDirs`, making it clearly distinct from the `--report` coverage metric.
21
+ - **`autoMaxTokens` silent-override warning** — when `autoMaxTokens` is active and overrides the user's `maxTokens` config value, `--report` now emits an explicit note explaining the override and how to disable it.
22
+
23
+ ### Changed
24
+
25
+ - `src/analysis/coverage-score.js` exports `CODE_EXTS` (the allowlist Set) for use by other modules and tests.
26
+ - `coverageScore()` return object gains a `nonCodeSkipped` field (number of non-code files found in srcDirs but excluded from the denominator).
27
+
28
+ ---
29
+
30
+ ## [5.4.0] — 2026-04-17
31
+
32
+ ### Added
33
+
34
+ - **Neovim plugin (`sigmap.nvim`)** — first-class Neovim integration in `neovim-plugin/`. Provides `:SigMap [args]` (async regen), `:SigMapQuery <text>` (TF-IDF retrieval in a floating window), `auto_run = true` (`BufWritePost` autocmd for source files), `require('sigmap').statusline()` for lualine/statusline widgets, and `:checkhealth sigmap` (validates Node 18+, binary presence, context file freshness).
35
+ - **Binary auto-detection** — plugin resolves the sigmap binary automatically: global `sigmap` → `npx sigmap` → local `gen-context.js` fallback; no manual config needed for most setups.
36
+ - **`release-neovim.yml` workflow** — tag `neovim-v*` to validate Lua files, run the full integration suite across Node 18/20/22, package the plugin as a `.tar.gz`, and create a GitHub Release.
37
+ - **CI now runs integration tests** — `ci.yml` runs both `node test/run.js` and `node test/integration/all.js` on every push and pull request.
38
+
39
+ ---
40
+
13
41
  ## [5.3.0] — 2026-04-17
14
42
 
15
43
  ### Added
package/README.md CHANGED
@@ -25,7 +25,7 @@ npx sigmap # 10 seconds. zero config. your AI never reads the wrong file again
25
25
  - Fewer retries (1.69 vs 2.84 prompts per task)
26
26
  - Far smaller context (~2K–4K tokens instead of ~80K)
27
27
 
28
- > Latest: **v5.3.0** — Learning engine + workflow-first release. Use `ask`, `validate`, `judge`, `learn`, `weights`, `compare`, and `share` on top of the core signature pipeline.
28
+ > Latest: **v5.5.0** — Coverage clarity + report UX plugin (`sigmap.nvim`). `:SigMap`, `:SigMapQuery`, auto-run on save, statusline widget, and `:checkhealth sigmap` for the #1 most-admired editor.
29
29
 
30
30
  **What is new in v5.2**
31
31
  - `sigmap ask` creates task-focused context in one step
@@ -99,6 +99,7 @@ Measured on 90 coding tasks across 18 real public repos. Full methodology and ra
99
99
  | [Standalone binaries](docs/readmes/binaries.md) | macOS, Linux, Windows — no Node required |
100
100
  | [VS Code extension](#-vs-code-extension) | Status bar, stale alerts, commands |
101
101
  | [JetBrains plugin](#-jetbrains-plugin) | IntelliJ IDEA, WebStorm, PyCharm support |
102
+ | [Neovim plugin](#-neovim-plugin) | `:SigMap`, `:SigMapQuery`, statusline, health check |
102
103
  | [Languages supported](#-languages-supported) | 29 languages |
103
104
  | [Context strategies](#-context-strategies) | full / per-module / hot-cold |
104
105
  | [MCP server](#-mcp-server) | 8 on-demand tools |
@@ -518,6 +519,39 @@ Compatible with **IntelliJ IDEA 2024.1+** (Community & Ultimate), **WebStorm**,
518
519
 
519
520
  ---
520
521
 
522
+ ## 🖥️ Neovim plugin
523
+
524
+ <a href="https://dotfyle.com/plugins/manojmallick/sigmap.nvim">
525
+ <img src="https://dotfyle.com/plugins/manojmallick/sigmap.nvim/shield?style=flat" />
526
+ </a>
527
+
528
+ The official SigMap Neovim plugin (`sigmap.nvim`) brings first-class integration to the #1 most-admired editor (Stack Overflow 2025, 83% admiration rate). Power users who live in the terminal get context regeneration, ranked retrieval, and health checks without leaving Neovim.
529
+
530
+ | Feature | Detail |
531
+ |---|---|
532
+ | **`:SigMap [args]`** | Regenerate your AI context file asynchronously |
533
+ | **`:SigMapQuery <text>`** | TF-IDF ranked retrieval — results appear in a centered floating window |
534
+ | **Auto-run on save** | `auto_run = true` triggers regen on `BufWritePost` for `.js/ts/py/go/rs/java/rb/lua` |
535
+ | **Statusline widget** | `require('sigmap').statusline()` returns `sm:✓` (fresh) or `sm:⚠ Nh` (stale) |
536
+ | **`:checkhealth sigmap`** | Validates Node 18+, binary presence, and context file freshness |
537
+ | **Binary auto-detection** | Finds `sigmap` → `npx sigmap` → local `gen-context.js` automatically |
538
+
539
+ **Install (lazy.nvim):**
540
+ ```lua
541
+ { 'manojmallick/sigmap.nvim',
542
+ config = function()
543
+ require('sigmap').setup({
544
+ auto_run = true, -- regenerate on save
545
+ float_query = true, -- show query results in a floating window
546
+ })
547
+ end,
548
+ }
549
+ ```
550
+
551
+ **Repo:** [manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim)
552
+
553
+ ---
554
+
521
555
  ## 🌐 Languages supported
522
556
 
523
557
  > 29 languages and formats. All implemented with zero external dependencies — pure regex + Node built-ins.
@@ -811,7 +845,7 @@ Every run now prints a coverage line alongside token reduction:
811
845
 
812
846
  ```
813
847
  ───────────────────────────────────────────
814
- SigMap v5.3.0
848
+ SigMap v5.5.0
815
849
  Files scanned : 76
816
850
  Symbols found : 332
817
851
  Token reduction: 94% (65,227 → 4,103)
@@ -830,7 +864,7 @@ sigmap --report
830
864
 
831
865
  ```
832
866
  [sigmap] report:
833
- version : 5.3.0
867
+ version : 5.5.0
834
868
  files processed : 76
835
869
  reduction : 93.7%
836
870
  coverage : A (97%) — 76 of 78 source files included
@@ -874,7 +908,7 @@ sigmap --health --json
874
908
  Every output file now carries a metadata line so you can inspect freshness at a glance:
875
909
 
876
910
  ```
877
- <!-- sigmap: version=5.3.0 confidence=HIGH coverage=97% dropped=2 commit=8540612 -->
911
+ <!-- sigmap: version=5.5.0 confidence=HIGH coverage=97% dropped=2 commit=8540612 -->
878
912
  ```
879
913
 
880
914
  ### Diff risk score
@@ -1009,6 +1043,11 @@ sigmap/
1009
1043
  │ ├── package.json ← manifest — commands, settings, activation
1010
1044
  │ └── src/extension.js ← status bar, stale notification, commands
1011
1045
 
1046
+ ├── neovim-plugin/ ← Neovim plugin — sigmap.nvim (v5.4)
1047
+ │ ├── lua/sigmap/init.lua ← M.setup(), M.run(), M.query(), M.statusline()
1048
+ │ ├── lua/sigmap/health.lua ← :checkhealth sigmap
1049
+ │ └── plugin/sigmap.lua ← :SigMap and :SigMapQuery user commands
1050
+
1012
1051
  ├── test/
1013
1052
  │ ├── fixtures/ ← one source file per language
1014
1053
  │ ├── expected/ ← expected extractor output
package/gen-context.js CHANGED
@@ -3034,7 +3034,445 @@ __factories["./src/format/cache"] = function(module, exports) {
3034
3034
  }
3035
3035
 
3036
3036
  module.exports = { formatCache, formatCachePayload };
3037
-
3037
+
3038
+ };
3039
+
3040
+ // ── ./src/format/benchmark-report ──
3041
+ __factories["./src/format/benchmark-report"] = function(module, exports) {
3042
+ 'use strict';
3043
+
3044
+ const fs = require('fs');
3045
+ const path = require('path');
3046
+
3047
+ function escapeHtml(value) {
3048
+ return String(value == null ? '' : value)
3049
+ .replace(/&/g, '&amp;')
3050
+ .replace(/</g, '&lt;')
3051
+ .replace(/>/g, '&gt;')
3052
+ .replace(/"/g, '&quot;');
3053
+ }
3054
+
3055
+ function formatInt(value) {
3056
+ const n = Number(value);
3057
+ if (!Number.isFinite(n)) return 'n/a';
3058
+ return Math.round(n).toLocaleString('en-US');
3059
+ }
3060
+
3061
+ function formatCompact(value) {
3062
+ const n = Number(value);
3063
+ if (!Number.isFinite(n)) return 'n/a';
3064
+ if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
3065
+ if (Math.abs(n) >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
3066
+ return String(Math.round(n));
3067
+ }
3068
+
3069
+ function formatPct(value, digits = 1) {
3070
+ const n = Number(value);
3071
+ if (!Number.isFinite(n)) return 'n/a';
3072
+ return `${n.toFixed(digits)}%`;
3073
+ }
3074
+
3075
+ function formatMaybePct(value, digits = 1) {
3076
+ const n = Number(value);
3077
+ if (!Number.isFinite(n)) return 'n/a';
3078
+ return `${n.toFixed(digits)}%`;
3079
+ }
3080
+
3081
+ function formatRatio(value, digits = 1) {
3082
+ const n = Number(value);
3083
+ if (!Number.isFinite(n)) return 'n/a';
3084
+ return `${n.toFixed(digits)}x`;
3085
+ }
3086
+
3087
+ function formatMoney(value) {
3088
+ const n = Number(value);
3089
+ if (!Number.isFinite(n)) return 'n/a';
3090
+ return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
3091
+ }
3092
+
3093
+ function durationLabel(ms) {
3094
+ const n = Number(ms);
3095
+ if (!Number.isFinite(n)) return 'n/a';
3096
+ const sec = n / 1000;
3097
+ if (sec < 60) return `${sec.toFixed(1)}s`;
3098
+ const min = Math.floor(sec / 60);
3099
+ const rem = sec - (min * 60);
3100
+ return `${min}m ${rem.toFixed(1)}s`;
3101
+ }
3102
+
3103
+ function maxOrZero(values) {
3104
+ if (!Array.isArray(values) || values.length === 0) return 0;
3105
+ return Math.max(...values.map((v) => (Number.isFinite(v) ? v : 0)));
3106
+ }
3107
+
3108
+ function readJson(filePath) {
3109
+ try {
3110
+ if (!fs.existsSync(filePath)) return null;
3111
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
3112
+ } catch (_) {
3113
+ return null;
3114
+ }
3115
+ }
3116
+
3117
+ function loadBenchmarkReports(cwd) {
3118
+ const reportsDir = path.join(cwd, 'benchmarks', 'reports');
3119
+ return {
3120
+ reportsDir,
3121
+ token: readJson(path.join(reportsDir, 'token-reduction.json')),
3122
+ retrieval: readJson(path.join(reportsDir, 'retrieval.json')),
3123
+ quality: readJson(path.join(reportsDir, 'quality.json')),
3124
+ task: readJson(path.join(reportsDir, 'task-benchmark.json')),
3125
+ matrix: readJson(path.join(reportsDir, 'benchmark-matrix.json')),
3126
+ };
3127
+ }
3128
+
3129
+ function buildRetrievalSummary(retrieval) {
3130
+ if (!retrieval || !Array.isArray(retrieval.repos) || retrieval.repos.length === 0) return null;
3131
+ let totalTasks = 0;
3132
+ let weightedHit = 0;
3133
+ let weightedRand = 0;
3134
+ let correct = 0;
3135
+ let partial = 0;
3136
+ let wrong = 0;
3137
+ let repoCount = 0;
3138
+
3139
+ for (const repo of retrieval.repos) {
3140
+ const tasks = Number(repo.tasks) || 0;
3141
+ repoCount++;
3142
+ totalTasks += tasks;
3143
+ weightedHit += (Number(repo.hitAt5) || 0) * tasks;
3144
+ weightedRand += (Number(repo.randomBaseline) || 0) * tasks;
3145
+ correct += Number(repo.tiers && repo.tiers.correct) || 0;
3146
+ partial += Number(repo.tiers && repo.tiers.partial) || 0;
3147
+ wrong += Number(repo.tiers && repo.tiers.wrong) || 0;
3148
+ }
3149
+
3150
+ const hitAt5 = totalTasks > 0 ? (weightedHit / totalTasks) * 100 : null;
3151
+ const randomBaseline = totalTasks > 0 ? (weightedRand / totalTasks) * 100 : null;
3152
+ const lift = hitAt5 && randomBaseline ? hitAt5 / randomBaseline : null;
3153
+
3154
+ return { repoCount, totalTasks, hitAt5, randomBaseline, lift, correct, partial, wrong };
3155
+ }
3156
+
3157
+ function buildBenchmarkSummary(reports, matrixSummary) {
3158
+ const missing = [];
3159
+ if (!reports.token) missing.push('token-reduction.json');
3160
+ if (!reports.retrieval) missing.push('retrieval.json');
3161
+ if (!reports.quality) missing.push('quality.json');
3162
+ if (!reports.task) missing.push('task-benchmark.json');
3163
+
3164
+ const retrievalSummary = buildRetrievalSummary(reports.retrieval);
3165
+ const qualitySummary = reports.quality && reports.quality.summary ? reports.quality.summary : null;
3166
+ const tokenSummary = reports.token && reports.token.summary ? reports.token.summary : null;
3167
+ const taskSummary = reports.task && reports.task.summary ? reports.task.summary : null;
3168
+ const matrix = matrixSummary || reports.matrix || null;
3169
+
3170
+ const generatedCandidates = [
3171
+ matrix && matrix.generated,
3172
+ reports.task && reports.task.generated,
3173
+ reports.retrieval && reports.retrieval.generated,
3174
+ reports.quality && reports.quality.timestamp,
3175
+ reports.token && reports.token.timestamp,
3176
+ ].filter(Boolean);
3177
+ const generatedAt = generatedCandidates
3178
+ .map((value) => ({ value, time: Date.parse(value) }))
3179
+ .filter((item) => Number.isFinite(item.time))
3180
+ .sort((a, b) => b.time - a.time)[0];
3181
+
3182
+ return {
3183
+ generatedAt: (generatedAt && generatedAt.value) || generatedCandidates[0] || new Date().toISOString(),
3184
+ missing,
3185
+ tokenSummary,
3186
+ retrievalSummary,
3187
+ qualitySummary,
3188
+ taskSummary,
3189
+ matrix,
3190
+ };
3191
+ }
3192
+
3193
+ function renderCard(label, value, hint, tone) {
3194
+ const toneClass = tone ? ` ${tone}` : '';
3195
+ return [
3196
+ `<article class="card${toneClass}">`,
3197
+ `<div class="label">${escapeHtml(label)}</div>`,
3198
+ `<div class="value">${escapeHtml(value)}</div>`,
3199
+ `<div class="hint">${escapeHtml(hint || '')}</div>`,
3200
+ '</article>',
3201
+ ].join('');
3202
+ }
3203
+
3204
+ function renderProgress(label, value, max, suffix) {
3205
+ const safeValue = Number.isFinite(value) ? value : 0;
3206
+ const safeMax = Math.max(1, Number.isFinite(max) ? max : 1);
3207
+ const width = Math.max(2, Math.min(100, (safeValue / safeMax) * 100));
3208
+ return [
3209
+ '<div class="progress-row">',
3210
+ `<div class="progress-label">${escapeHtml(label)}</div>`,
3211
+ '<div class="progress-bar"><span style="width:',
3212
+ String(width.toFixed(1)),
3213
+ '%"></span></div>',
3214
+ `<div class="progress-value">${escapeHtml(`${safeValue}${suffix || ''}`)}</div>`,
3215
+ '</div>',
3216
+ ].join('');
3217
+ }
3218
+
3219
+ function renderMatrixSection(matrix) {
3220
+ if (!matrix || !Array.isArray(matrix.steps) || matrix.steps.length === 0) return '';
3221
+ const rows = matrix.steps.map((step) => {
3222
+ const status = step.ok ? 'ok' : 'fail';
3223
+ return [
3224
+ '<tr>',
3225
+ `<td>${escapeHtml(step.name)}</td>`,
3226
+ `<td><span class="badge ${status}">${escapeHtml(step.ok ? 'ok' : `exit ${step.status}`)}</span></td>`,
3227
+ `<td>${escapeHtml(durationLabel(step.durationMs))}</td>`,
3228
+ `<td><code>${escapeHtml(['node', step.script].concat(step.args || []).join(' '))}</code></td>`,
3229
+ '</tr>',
3230
+ ].join('');
3231
+ }).join('');
3232
+
3233
+ return [
3234
+ '<section>',
3235
+ '<h2>Run matrix</h2>',
3236
+ '<p class="section-copy">This shows which benchmark jobs ran, whether they succeeded, and how long each step took.</p>',
3237
+ '<table>',
3238
+ '<thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Command</th></tr></thead>',
3239
+ `<tbody>${rows}</tbody>`,
3240
+ '</table>',
3241
+ '</section>',
3242
+ ].join('');
3243
+ }
3244
+
3245
+ function renderTokenSection(token) {
3246
+ if (!token || !Array.isArray(token.repos) || token.repos.length === 0) return '';
3247
+ const rows = token.repos
3248
+ .slice()
3249
+ .sort((a, b) => (b.reductionPct || 0) - (a.reductionPct || 0))
3250
+ .map((repo) => [
3251
+ '<tr>',
3252
+ `<td>${escapeHtml(repo.repo)}</td>`,
3253
+ `<td>${escapeHtml(repo.language || 'n/a')}</td>`,
3254
+ `<td>${escapeHtml(formatCompact(repo.rawTokens))}</td>`,
3255
+ `<td>${escapeHtml(formatCompact(repo.finalTokens))}</td>`,
3256
+ `<td>${escapeHtml(formatMaybePct(repo.reductionPct, 1))}</td>`,
3257
+ '</tr>',
3258
+ ].join(''))
3259
+ .join('');
3260
+
3261
+ return [
3262
+ '<section>',
3263
+ '<h2>Token reduction</h2>',
3264
+ '<p class="section-copy">Raw repository tokens versus SigMap output size across the benchmark repos.</p>',
3265
+ '<table>',
3266
+ '<thead><tr><th>Repo</th><th>Language</th><th>Raw tokens</th><th>Final tokens</th><th>Reduction</th></tr></thead>',
3267
+ `<tbody>${rows}</tbody>`,
3268
+ '</table>',
3269
+ '</section>',
3270
+ ].join('');
3271
+ }
3272
+
3273
+ function renderRetrievalSection(retrieval) {
3274
+ if (!retrieval || !Array.isArray(retrieval.repos) || retrieval.repos.length === 0) return '';
3275
+ const rows = retrieval.repos.map((repo) => {
3276
+ const lift = repo.randomBaseline > 0 ? (repo.hitAt5 / repo.randomBaseline) : null;
3277
+ return [
3278
+ '<tr>',
3279
+ `<td>${escapeHtml(repo.repo)}</td>`,
3280
+ `<td>${escapeHtml(formatMaybePct((repo.randomBaseline || 0) * 100, 1))}</td>`,
3281
+ `<td>${escapeHtml(formatMaybePct((repo.hitAt5 || 0) * 100, 1))}</td>`,
3282
+ `<td>${escapeHtml(formatRatio(lift, 1))}</td>`,
3283
+ `<td>${escapeHtml(String((repo.tiers && repo.tiers.correct) || 0))}</td>`,
3284
+ `<td>${escapeHtml(String((repo.tiers && repo.tiers.partial) || 0))}</td>`,
3285
+ `<td>${escapeHtml(String((repo.tiers && repo.tiers.wrong) || 0))}</td>`,
3286
+ '</tr>',
3287
+ ].join('');
3288
+ }).join('');
3289
+
3290
+ return [
3291
+ '<section>',
3292
+ '<h2>Retrieval quality</h2>',
3293
+ '<p class="section-copy">Hit@5 performance against the random baseline, plus the quality-tier mix that drives the task benchmark.</p>',
3294
+ '<table>',
3295
+ '<thead><tr><th>Repo</th><th>Random hit@5</th><th>SigMap hit@5</th><th>Lift</th><th>Correct</th><th>Partial</th><th>Wrong</th></tr></thead>',
3296
+ `<tbody>${rows}</tbody>`,
3297
+ '</table>',
3298
+ '</section>',
3299
+ ].join('');
3300
+ }
3301
+
3302
+ function renderQualitySection(quality) {
3303
+ if (!quality || !Array.isArray(quality.repos) || quality.repos.length === 0) return '';
3304
+ const rows = quality.repos.map((repo) => {
3305
+ const overflow = (repo.rawTokens || 0) > 128000 ? 'overflow' : 'fits';
3306
+ return [
3307
+ '<tr>',
3308
+ `<td>${escapeHtml(repo.repo)}</td>`,
3309
+ `<td>${escapeHtml(formatInt(repo.groundedSymbols))}</td>`,
3310
+ `<td>${escapeHtml(formatInt(repo.darkSymbols))}</td>`,
3311
+ `<td>${escapeHtml(formatMaybePct(repo.groundingPct, 0))}</td>`,
3312
+ `<td>${escapeHtml(String(repo.filesHiddenRaw || 0))}</td>`,
3313
+ `<td><span class="badge ${overflow === 'overflow' ? 'warn' : 'ok'}">${escapeHtml(overflow)}</span></td>`,
3314
+ '</tr>',
3315
+ ].join('');
3316
+ }).join('');
3317
+
3318
+ return [
3319
+ '<section>',
3320
+ '<h2>Quality and hallucination surface</h2>',
3321
+ '<p class="section-copy">How much code stays visible to the model, plus the overflow and dark-symbol risk by repo.</p>',
3322
+ '<table>',
3323
+ '<thead><tr><th>Repo</th><th>Grounded symbols</th><th>Dark symbols</th><th>Grounding</th><th>Hidden files (raw)</th><th>GPT-4o 128K</th></tr></thead>',
3324
+ `<tbody>${rows}</tbody>`,
3325
+ '</table>',
3326
+ '</section>',
3327
+ ].join('');
3328
+ }
3329
+
3330
+ function renderTaskSection(task) {
3331
+ if (!task || !Array.isArray(task.repos) || task.repos.length === 0 || !task.summary) return '';
3332
+ const summary = task.summary;
3333
+ const maxReduction = maxOrZero(task.repos.map((repo) => Number(repo.reductionPct) || 0));
3334
+ const repoBars = task.repos
3335
+ .slice()
3336
+ .sort((a, b) => (b.reductionPct || 0) - (a.reductionPct || 0))
3337
+ .slice(0, 10)
3338
+ .map((repo) => renderProgress(repo.repo, Number(repo.reductionPct) || 0, maxReduction, '%'))
3339
+ .join('');
3340
+
3341
+ return [
3342
+ '<section>',
3343
+ '<h2>Task benchmark</h2>',
3344
+ '<p class="section-copy">A prompt-reduction proxy derived from retrieval quality tiers. Lower prompts means the right file surfaces sooner.</p>',
3345
+ '<div class="split">',
3346
+ '<div class="panel">',
3347
+ '<h3>Answer quality tiers</h3>',
3348
+ renderProgress('Correct', Number(summary.correctPct) || 0, 100, '%'),
3349
+ renderProgress('Partial', Number(summary.partialPct) || 0, 100, '%'),
3350
+ renderProgress('Wrong', Number(summary.wrongPct) || 0, 100, '%'),
3351
+ '</div>',
3352
+ '<div class="panel">',
3353
+ '<h3>Best prompt reduction by repo</h3>',
3354
+ repoBars,
3355
+ '</div>',
3356
+ '</div>',
3357
+ '</section>',
3358
+ ].join('');
3359
+ }
3360
+
3361
+ function generateBenchmarkReportHtml(reports, opts = {}) {
3362
+ const summary = buildBenchmarkSummary(reports, opts.matrixSummary);
3363
+ const cards = [];
3364
+ cards.push(renderCard(
3365
+ 'Token reduction',
3366
+ summary.tokenSummary ? formatPct(summary.tokenSummary.overallReductionPct, 1) : 'n/a',
3367
+ summary.tokenSummary ? `${formatInt(summary.tokenSummary.repoCount)} repos • ${formatCompact(summary.tokenSummary.totalRawTokens)} raw -> ${formatCompact(summary.tokenSummary.totalFinalTokens)} final` : 'token-reduction.json missing',
3368
+ 'cool'
3369
+ ));
3370
+ cards.push(renderCard(
3371
+ 'Retrieval hit@5',
3372
+ summary.retrievalSummary ? formatPct(summary.retrievalSummary.hitAt5, 1) : 'n/a',
3373
+ summary.retrievalSummary ? `${formatPct(summary.retrievalSummary.randomBaseline, 1)} random baseline • ${formatRatio(summary.retrievalSummary.lift, 1)} lift` : 'retrieval.json missing',
3374
+ 'warm'
3375
+ ));
3376
+ cards.push(renderCard(
3377
+ 'Prompt reduction',
3378
+ summary.taskSummary ? formatPct(summary.taskSummary.avgReductionPct, 0) : 'n/a',
3379
+ summary.taskSummary ? `${summary.taskSummary.avgPromptsWithout} -> ${summary.taskSummary.avgPromptsWith} prompts • ${formatInt(summary.taskSummary.totalTasks)} tasks` : 'task-benchmark.json missing',
3380
+ 'neutral'
3381
+ ));
3382
+ cards.push(renderCard(
3383
+ 'Overflow risk',
3384
+ summary.qualitySummary ? `${formatInt(summary.qualitySummary.overflowGPT4oCount)} repos` : 'n/a',
3385
+ summary.qualitySummary ? `${formatInt(summary.qualitySummary.totalHiddenFiles)} hidden raw files • ${formatMoney(summary.qualitySummary.gpt4oSavedPerMonth)}/month saved` : 'quality.json missing',
3386
+ summary.qualitySummary && summary.qualitySummary.overflowGPT4oCount > 0 ? 'warn' : 'ok'
3387
+ ));
3388
+
3389
+ const missingHtml = summary.missing.length > 0
3390
+ ? `<div class="notice">Missing source reports: ${escapeHtml(summary.missing.join(', '))}. The page still renders whatever data is available.</div>`
3391
+ : '';
3392
+
3393
+ return [
3394
+ '<!doctype html>',
3395
+ '<html lang="en">',
3396
+ '<head>',
3397
+ '<meta charset="utf-8" />',
3398
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
3399
+ '<title>SigMap Benchmark Report</title>',
3400
+ '<style>',
3401
+ ':root { color-scheme: light; --bg:#f5f1e8; --panel:#fffaf2; --ink:#1f1b16; --muted:#6a6258; --line:#dccfbf; --gold:#c87f2a; --green:#2f6f52; --blue:#2f5f8f; --red:#9f4f43; --shadow:0 18px 40px rgba(54,38,14,.10);} ',
3402
+ '*{box-sizing:border-box} body{margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:linear-gradient(180deg,#f3ecdf 0%,#f7f3ed 100%);color:var(--ink)}',
3403
+ '.page{max-width:1240px;margin:0 auto;padding:28px 20px 56px}',
3404
+ 'header{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:24px}',
3405
+ 'h1{margin:0;font-size:clamp(2rem,4vw,3.6rem);line-height:1.02;letter-spacing:-.04em}',
3406
+ '.lede{max-width:760px;color:var(--muted);font-size:1rem;line-height:1.6;margin-top:10px}',
3407
+ '.stamp{font-size:.92rem;color:var(--muted);text-align:right}',
3408
+ '.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin:20px 0 24px}',
3409
+ '.card,.panel,.notice,section{background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:18px}',
3410
+ '.card{padding:18px 18px 16px}.card.cool{background:#f7f5ff}.card.warm{background:#fff4eb}.card.warn{background:#fff1eb}.card.ok{background:#eff8f1}',
3411
+ '.label{font-size:.84rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}',
3412
+ '.value{font-size:2rem;font-weight:700;letter-spacing:-.04em;margin-top:8px}',
3413
+ '.hint{font-size:.95rem;color:var(--muted);margin-top:8px;line-height:1.5}',
3414
+ '.notice{padding:14px 16px;margin-bottom:20px;color:var(--muted)}',
3415
+ 'section{padding:20px;margin-top:18px}',
3416
+ 'h2{margin:0 0 6px;font-size:1.4rem;letter-spacing:-.03em}',
3417
+ 'h3{margin:0 0 14px;font-size:1rem}',
3418
+ '.section-copy{margin:0 0 16px;color:var(--muted);line-height:1.6}',
3419
+ 'table{width:100%;border-collapse:collapse;font-size:.95rem}',
3420
+ 'th,td{padding:10px 12px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}',
3421
+ 'th{font-size:.82rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}',
3422
+ 'tbody tr:hover{background:rgba(200,127,42,.06)}',
3423
+ '.badge{display:inline-flex;align-items:center;padding:4px 8px;border-radius:999px;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em}',
3424
+ '.badge.ok{background:#e6f4ea;color:#21573f}.badge.warn{background:#fff0de;color:#8a4a17}.badge.fail{background:#fde8e5;color:#8a2e23}',
3425
+ '.split{display:grid;grid-template-columns:1fr 1fr;gap:16px}',
3426
+ '.panel{padding:16px}',
3427
+ '.progress-row{display:grid;grid-template-columns:140px 1fr 60px;gap:12px;align-items:center;margin:10px 0}',
3428
+ '.progress-label,.progress-value{font-size:.92rem}',
3429
+ '.progress-bar{height:10px;border-radius:999px;background:#efe4d5;overflow:hidden}',
3430
+ '.progress-bar span{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--gold),#ebbb61)}',
3431
+ 'code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85rem}',
3432
+ '@media (max-width: 1020px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.split{grid-template-columns:1fr}header{flex-direction:column;align-items:flex-start}.stamp{text-align:left}}',
3433
+ '@media (max-width: 640px){.grid{grid-template-columns:1fr}.progress-row{grid-template-columns:110px 1fr 52px}th:nth-child(n+5),td:nth-child(n+5){display:none}}',
3434
+ '</style>',
3435
+ '</head>',
3436
+ '<body>',
3437
+ '<div class="page">',
3438
+ '<header>',
3439
+ '<div>',
3440
+ '<h1>SigMap Benchmark Report</h1>',
3441
+ '<p class="lede">A self-contained view of token reduction, retrieval quality, hallucination surface, and task-level prompt reduction. This page reads the saved JSON benchmark artifacts so it stays easy to regenerate locally.</p>',
3442
+ '</div>',
3443
+ `<div class="stamp">Generated: ${escapeHtml(summary.generatedAt)}<br />Source directory: <code>benchmarks/reports</code></div>`,
3444
+ '</header>',
3445
+ missingHtml,
3446
+ `<div class="grid">${cards.join('')}</div>`,
3447
+ renderMatrixSection(summary.matrix),
3448
+ renderTokenSection(reports.token),
3449
+ renderRetrievalSection(reports.retrieval),
3450
+ renderQualitySection(reports.quality),
3451
+ renderTaskSection(reports.task),
3452
+ '</div>',
3453
+ '</body>',
3454
+ '</html>',
3455
+ ].join('');
3456
+ }
3457
+
3458
+ function writeBenchmarkReport(cwd, opts = {}) {
3459
+ const reports = loadBenchmarkReports(cwd);
3460
+ const html = generateBenchmarkReportHtml(reports, opts);
3461
+ const filePath = path.join(reports.reportsDir, opts.fileName || 'benchmark-report.html');
3462
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
3463
+ fs.writeFileSync(filePath, html, 'utf8');
3464
+ return {
3465
+ file: filePath,
3466
+ summary: buildBenchmarkSummary(reports, opts.matrixSummary),
3467
+ };
3468
+ }
3469
+
3470
+ module.exports = {
3471
+ loadBenchmarkReports,
3472
+ buildBenchmarkSummary,
3473
+ generateBenchmarkReportHtml,
3474
+ writeBenchmarkReport,
3475
+ };
3038
3476
  };
3039
3477
 
3040
3478
  // ── ./src/format/dashboard ──
@@ -4853,7 +5291,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
4853
5291
 
4854
5292
  const SERVER_INFO = {
4855
5293
  name: 'sigmap',
4856
- version: '5.3.0',
5294
+ version: '5.5.0',
4857
5295
  description: 'SigMap MCP server — code signatures on demand',
4858
5296
  };
4859
5297
 
@@ -6571,7 +7009,7 @@ const path = require('path');
6571
7009
  const os = require('os');
6572
7010
  const { execSync } = require('child_process');
6573
7011
 
6574
- const VERSION = '5.3.0';
7012
+ const VERSION = '5.5.0';
6575
7013
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6576
7014
 
6577
7015
  function requireSourceOrBundled(key) {
@@ -7325,7 +7763,7 @@ function _coverageBar(pct, width) {
7325
7763
  return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
7326
7764
  }
7327
7765
 
7328
- function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit, coverageResult, isAutoBudget) {
7766
+ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit, coverageResult, isAutoBudget, configuredMaxTokens) {
7329
7767
  const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
7330
7768
  const overBudget = finalTokens > (budgetLimit || 6000);
7331
7769
  if (asJson) {
@@ -7363,6 +7801,10 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
7363
7801
  const budgetLabel = isAutoBudget
7364
7802
  ? `${budgetLimit || 6000} (auto-scaled)`
7365
7803
  : `${budgetLimit || 6000} (fixed)`;
7804
+ if (isAutoBudget && configuredMaxTokens && configuredMaxTokens !== budgetLimit) {
7805
+ console.warn(`[sigmap] note: autoMaxTokens is active — your maxTokens:${configuredMaxTokens} config was overridden by auto-scaled budget (${budgetLimit})`);
7806
+ console.warn(` to use your value, set "autoMaxTokens": false in gen-context.config.json`);
7807
+ }
7366
7808
  console.log(`[sigmap] report:`);
7367
7809
  console.log(` version : ${VERSION}`);
7368
7810
  console.log(` files processed : ${fileCount}`);
@@ -7372,7 +7814,11 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
7372
7814
  console.log(` budget limit : ${budgetLabel}`);
7373
7815
  console.log(` reduction : ${reduction}%`);
7374
7816
  if (coverageResult) {
7375
- console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files included`);
7817
+ const skipNote = coverageResult.nonCodeSkipped > 0
7818
+ ? ` (${coverageResult.nonCodeSkipped} non-code files skipped — json, md, config)`
7819
+ : '';
7820
+ console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} code files included`);
7821
+ if (skipNote) console.log(` ${skipNote}`);
7376
7822
  console.log(` confidence : ${coverageResult.confidence}`);
7377
7823
  if (coverageResult.perModule && coverageResult.perModule.size > 0) {
7378
7824
  console.log('');
@@ -7383,6 +7829,15 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
7383
7829
  const attention = mod.pct < 50 ? ' \u2190 attention needed' : '';
7384
7830
  console.log(` ${dir.padEnd(18)} ${bar} ${String(mod.pct).padStart(3)}% (${mod.included}/${mod.total} files)${attention}`);
7385
7831
  }
7832
+ const lowModules = [...coverageResult.perModule.entries()].filter(([, m]) => m.pct < 50 && m.total > 0);
7833
+ if (lowModules.length > 0) {
7834
+ console.log('');
7835
+ console.log(' tip: modules marked "\u2190 attention needed" have <50% code-file coverage.');
7836
+ console.log(' Common causes:');
7837
+ console.log(' \u2022 token budget dropped files \u2014 raise maxTokens or use strategy:"per-module"');
7838
+ console.log(' \u2022 srcDir path includes non-code files \u2014 check .contextignore or exclude config');
7839
+ console.log(' \u2022 srcDir path is wrong \u2014 verify srcDirs in gen-context.config.json');
7840
+ }
7386
7841
  }
7387
7842
  }
7388
7843
  if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
@@ -7890,7 +8345,7 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7890
8345
  }
7891
8346
 
7892
8347
  if (reportMode || process.argv.includes('--report')) {
7893
- printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens);
8348
+ printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens, config.maxTokens);
7894
8349
  }
7895
8350
 
7896
8351
  // Usage tracking (v0.9) — optional append-only NDJSON log
@@ -9056,7 +9511,7 @@ function main() {
9056
9511
  console.log('[sigmap] health:');
9057
9512
  console.log(` score : ${result.score}/100 (grade ${result.grade})`);
9058
9513
  if (coverageResult) {
9059
- console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files`);
9514
+ console.log(` file access : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} files accessible in srcDirs`);
9060
9515
  }
9061
9516
  console.log(` strategy : ${result.strategy}`);
9062
9517
  console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "5.3.0",
3
+ "version": "5.5.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": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * SigMap coverage scorer — v4.0.0
4
+ * SigMap coverage scorer — v5.5.0
5
+ *
6
+ * Measures what fraction of *code* files made it into the context output
7
+ * after token-budget application. Non-code files (json, md, config) are
8
+ * counted separately as `nonCodeSkipped` so the grade reflects real coverage.
5
9
  *
6
- * Measures what fraction of source files made it into the context output
7
- * after token-budget application. This is complementary to the health score:
8
10
  * - Health score = context freshness / reduction quality / budget compliance
9
11
  * - Coverage score = how much of the codebase is represented in context
10
12
  *
@@ -19,10 +21,23 @@
19
21
  * total: number,
20
22
  * included: number,
21
23
  * dropped: number,
24
+ * nonCodeSkipped: number,
22
25
  * confidence: 'HIGH'|'MEDIUM'|'LOW',
23
26
  * perModule: Map<string, {total:number, included:number, pct:number}>,
24
27
  * }}
25
28
  */
29
+
30
+ const CODE_EXTS = new Set([
31
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
32
+ '.py', '.rb', '.go', '.rs', '.java', '.kt',
33
+ '.cs', '.cpp', '.c', '.h', '.hpp',
34
+ '.swift', '.dart', '.scala', '.php',
35
+ '.vue', '.svelte', '.css', '.scss',
36
+ '.sql', '.graphql', '.proto', '.tf',
37
+ '.lua', '.r', '.jl', '.ex', '.exs',
38
+ '.sh', '.bash', '.zsh', '.ps1',
39
+ ]);
40
+
26
41
  function coverageScore(cwd, fileEntries, config) {
27
42
  const fs = require('fs');
28
43
  const path = require('path');
@@ -41,12 +56,17 @@ function coverageScore(cwd, fileEntries, config) {
41
56
 
42
57
  const includedSet = new Set((fileEntries || []).map(f => f.filePath));
43
58
 
44
- // Walk all source files from srcDirs
59
+ // Walk srcDirs: separate code files from non-code files
60
+ const allFiles = [];
45
61
  const allSource = [];
46
62
  for (const relDir of srcDirs) {
47
63
  const absDir = path.resolve(cwd, relDir);
48
- if (fs.existsSync(absDir)) _walk(absDir, excludeSet, allSource);
64
+ if (fs.existsSync(absDir)) _walk(absDir, excludeSet, allFiles);
65
+ }
66
+ for (const f of allFiles) {
67
+ if (CODE_EXTS.has(path.extname(f).toLowerCase())) allSource.push(f);
49
68
  }
69
+ const nonCodeSkipped = allFiles.length - allSource.length;
50
70
 
51
71
  const total = allSource.length;
52
72
  const included = allSource.filter(f => includedSet.has(f)).length;
@@ -66,7 +86,7 @@ function coverageScore(cwd, fileEntries, config) {
66
86
  perModule.set(relDir, { total: modFiles.length, included: modIncl, pct: modPct });
67
87
  }
68
88
 
69
- return { score: pct, grade, total, included, dropped, confidence, perModule };
89
+ return { score: pct, grade, total, included, dropped, nonCodeSkipped, confidence, perModule };
70
90
  }
71
91
 
72
92
  function _walk(dir, excludeSet, out) {
@@ -82,4 +102,4 @@ function _walk(dir, excludeSet, out) {
82
102
  }
83
103
  }
84
104
 
85
- module.exports = { coverageScore };
105
+ module.exports = { coverageScore, CODE_EXTS };
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: '5.3.0',
21
+ version: '5.5.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24