sigmap 5.4.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,23 @@ 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
+
13
30
  ## [5.4.0] — 2026-04-17
14
31
 
15
32
  ### 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.4.0** — Neovim plugin (`sigmap.nvim`). `:SigMap`, `:SigMapQuery`, auto-run on save, statusline widget, and `:checkhealth sigmap` for the #1 most-admired editor.
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
@@ -521,6 +521,10 @@ Compatible with **IntelliJ IDEA 2024.1+** (Community & Ultimate), **WebStorm**,
521
521
 
522
522
  ## 🖥️ Neovim plugin
523
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
+
524
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.
525
529
 
526
530
  | Feature | Detail |
@@ -534,7 +538,7 @@ The official SigMap Neovim plugin (`sigmap.nvim`) brings first-class integration
534
538
 
535
539
  **Install (lazy.nvim):**
536
540
  ```lua
537
- { 'manojmallick/sigmap',
541
+ { 'manojmallick/sigmap.nvim',
538
542
  config = function()
539
543
  require('sigmap').setup({
540
544
  auto_run = true, -- regenerate on save
@@ -544,7 +548,7 @@ The official SigMap Neovim plugin (`sigmap.nvim`) brings first-class integration
544
548
  }
545
549
  ```
546
550
 
547
- **Source:** [`neovim-plugin/`](neovim-plugin/) | **Docs:** [`neovim-plugin/README.md`](neovim-plugin/README.md)
551
+ **Repo:** [manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim)
548
552
 
549
553
  ---
550
554
 
@@ -841,7 +845,7 @@ Every run now prints a coverage line alongside token reduction:
841
845
 
842
846
  ```
843
847
  ───────────────────────────────────────────
844
- SigMap v5.4.0
848
+ SigMap v5.5.0
845
849
  Files scanned : 76
846
850
  Symbols found : 332
847
851
  Token reduction: 94% (65,227 → 4,103)
@@ -860,7 +864,7 @@ sigmap --report
860
864
 
861
865
  ```
862
866
  [sigmap] report:
863
- version : 5.4.0
867
+ version : 5.5.0
864
868
  files processed : 76
865
869
  reduction : 93.7%
866
870
  coverage : A (97%) — 76 of 78 source files included
@@ -904,7 +908,7 @@ sigmap --health --json
904
908
  Every output file now carries a metadata line so you can inspect freshness at a glance:
905
909
 
906
910
  ```
907
- <!-- sigmap: version=5.4.0 confidence=HIGH coverage=97% dropped=2 commit=8540612 -->
911
+ <!-- sigmap: version=5.5.0 confidence=HIGH coverage=97% dropped=2 commit=8540612 -->
908
912
  ```
909
913
 
910
914
  ### Diff risk score
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.4.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.4.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.4.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.4.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.4.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.4.0',
21
+ version: '5.5.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24