sigmap 2.9.1 → 2.10.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
@@ -6,6 +6,23 @@ Format: [Semantic Versioning](https://semver.org/)
6
6
 
7
7
  ---
8
8
 
9
+ ## [2.10.0] — upcoming · [#25](https://github.com/manojmallick/sigmap/issues/25) · branch: `feat/v2.10-reporting-charts-advanced-metrics`
10
+
11
+ ### Planned additions
12
+ - **Report charts** — add chart-ready output for token reduction, signatures per file, and budget utilization trends.
13
+ - **Advanced metrics** — extend evaluation output with precision@K, recall@K, MRR, and query-level diagnostics.
14
+ - **CLI reporting mode** — introduce richer report surfaces for both human-readable tables and structured JSON artifacts.
15
+ - **Benchmark visibility** — include comparative metrics across runs to track regressions and improvements over time.
16
+ - **Docs refresh** — align roadmap and docs site references to the v2.10 milestone.
17
+
18
+ ### Go / No-go criteria
19
+ - Full test suite passes (extractor + integration).
20
+ - Report output includes chart-friendly numeric series and summary stats.
21
+ - Benchmark metrics remain stable or improve versus v2.9 baseline.
22
+ - Generated docs and release metadata are version-synced to `2.10.0`.
23
+
24
+ ---
25
+
9
26
  ## [2.9.1] — 2026-04-06 · JetBrains Marketplace Publishing
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -112,17 +112,17 @@ AI agent session starts with full context
112
112
 
113
113
  ---
114
114
 
115
- ## 🔭 What's next — v2.9 (in progress · [#23](https://github.com/manojmallick/sigmap/issues/23))
115
+ ## 🔭 What's next — v2.10 (in progress · [#25](https://github.com/manojmallick/sigmap/issues/25))
116
116
 
117
- ### v2.9IDE Expansion: JetBrains Plugin
117
+ ### v2.10Reporting: Charts + Advanced Metrics
118
118
 
119
119
  | Feature | Description |
120
120
  |---|---|
121
- | **JetBrains plugin** | Install SigMap natively in IntelliJ IDEA, WebStorm, PyCharm, GoLand, RubyMine |
122
- | **Toolbar actions** | "Regenerate Context", "Open Context File", "View Roadmap" buttons |
123
- | **Settings panel** | Configure srcDirs, exclude patterns, maxTokens, routing presets from IDE |
124
- | **File watcher** | Auto-regenerate context on file changes (opt-in) |
125
- | **Marketplace publishing** | Automated publishing to JetBrains Marketplace via GitHub Actions |
121
+ | **Charts in reports** | Visualize token reduction, signature counts, and budget usage per run |
122
+ | **Advanced retrieval metrics** | Add precision@K, recall@K, MRR trend, and query-level diagnostics |
123
+ | **Evaluation dashboard output** | Generate shareable HTML/JSON benchmark summaries from CLI runs |
124
+ | **CI-friendly metrics export** | Persist machine-readable metrics for release gates and regression tracking |
125
+ | **Release quality gates** | Add pass/fail thresholds for hit@5 and precision before publish |
126
126
 
127
127
  ---
128
128
  | **`get_impact` MCP tool** | 9th MCP tool — `{ file, depth? }` → impacted files + signatures |
@@ -722,6 +722,6 @@ MIT © 2026 [Manoj Mallick](https://github.com/manojmallick) · Made in Amsterda
722
722
 
723
723
  If SigMap saves you time — a ⭐ on [GitHub](https://github.com/manojmallick/sigmap) helps others find it.
724
724
 
725
- **[Docs](https://manojmallick.github.io/sigmap) · [Changelog](CHANGELOG.md) · [Roadmap](docs/roadmap.html) · [Repomix](https://github.com/yamadashy/repomix)**
725
+ **[Docs](https://manojmallick.github.io/sigmap) · [Changelog](CHANGELOG.md) · [Roadmap](https://manojmallick.github.io/sigmap/roadmap.html) · [Repomix](https://github.com/yamadashy/repomix)**
726
726
 
727
727
  </div>
package/gen-context.js CHANGED
@@ -1937,6 +1937,316 @@ __factories["./src/format/cache"] = function(module, exports) {
1937
1937
 
1938
1938
  };
1939
1939
 
1940
+ // ── ./src/format/dashboard ──
1941
+ __factories["./src/format/dashboard"] = function(module, exports) {
1942
+ const fs = require('fs');
1943
+ const path = require('path');
1944
+ const { readLog } = __require('./src/tracking/logger');
1945
+
1946
+ const LANGUAGE_KEYS = [
1947
+ 'typescript', 'javascript', 'python', 'java', 'kotlin', 'go', 'rust',
1948
+ 'csharp', 'cpp', 'ruby', 'php', 'swift', 'dart', 'scala', 'vue',
1949
+ 'svelte', 'html', 'css', 'yaml', 'shell', 'dockerfile',
1950
+ ];
1951
+
1952
+ function toNumber(v) {
1953
+ const n = Number(v);
1954
+ return Number.isFinite(n) ? n : null;
1955
+ }
1956
+
1957
+ function percentile(values, p) {
1958
+ if (!Array.isArray(values) || values.length === 0) return 0;
1959
+ const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
1960
+ if (sorted.length === 0) return 0;
1961
+ if (p <= 0) return sorted[0];
1962
+ if (p >= 100) return sorted[sorted.length - 1];
1963
+ const idx = (p / 100) * (sorted.length - 1);
1964
+ const lo = Math.floor(idx);
1965
+ const hi = Math.ceil(idx);
1966
+ if (lo === hi) return sorted[lo];
1967
+ const frac = idx - lo;
1968
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;
1969
+ }
1970
+
1971
+ function overBudgetStreak(entries) {
1972
+ if (!Array.isArray(entries) || entries.length === 0) return 0;
1973
+ let streak = 0;
1974
+ for (let i = entries.length - 1; i >= 0; i--) {
1975
+ if (entries[i] && entries[i].overBudget) streak++;
1976
+ else break;
1977
+ }
1978
+ return streak;
1979
+ }
1980
+
1981
+ function walkFiles(dir, maxDepth, depth, out, excludeSet) {
1982
+ if (depth > maxDepth) return;
1983
+ let entries = [];
1984
+ try {
1985
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1986
+ } catch (_) {
1987
+ return;
1988
+ }
1989
+ for (const entry of entries) {
1990
+ const abs = path.join(dir, entry.name);
1991
+ const rel = abs.replace(/\\/g, '/');
1992
+ if (excludeSet.has(entry.name) || rel.includes('/node_modules/') || rel.includes('/.git/')) continue;
1993
+ if (entry.isDirectory()) walkFiles(abs, maxDepth, depth + 1, out, excludeSet);
1994
+ else if (entry.isFile()) out.push(abs);
1995
+ }
1996
+ }
1997
+
1998
+ function detectLanguage(filePath) {
1999
+ const base = path.basename(filePath);
2000
+ const ext = path.extname(filePath).toLowerCase();
2001
+ if (base === 'Dockerfile' || /^Dockerfile\./.test(base)) return 'dockerfile';
2002
+ if (ext === '.ts' || ext === '.tsx') return 'typescript';
2003
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') return 'javascript';
2004
+ if (ext === '.py' || ext === '.pyw') return 'python';
2005
+ if (ext === '.java') return 'java';
2006
+ if (ext === '.kt' || ext === '.kts') return 'kotlin';
2007
+ if (ext === '.go') return 'go';
2008
+ if (ext === '.rs') return 'rust';
2009
+ if (ext === '.cs') return 'csharp';
2010
+ if (ext === '.cpp' || ext === '.c' || ext === '.h' || ext === '.hpp' || ext === '.cc') return 'cpp';
2011
+ if (ext === '.rb' || ext === '.rake') return 'ruby';
2012
+ if (ext === '.php') return 'php';
2013
+ if (ext === '.swift') return 'swift';
2014
+ if (ext === '.dart') return 'dart';
2015
+ if (ext === '.scala' || ext === '.sc') return 'scala';
2016
+ if (ext === '.vue') return 'vue';
2017
+ if (ext === '.svelte') return 'svelte';
2018
+ if (ext === '.html' || ext === '.htm') return 'html';
2019
+ if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') return 'css';
2020
+ if (ext === '.yml' || ext === '.yaml') return 'yaml';
2021
+ if (ext === '.sh' || ext === '.bash' || ext === '.zsh' || ext === '.fish') return 'shell';
2022
+ return null;
2023
+ }
2024
+
2025
+ function computeExtractorCoverage(cwd) {
2026
+ let cfg = {};
2027
+ try {
2028
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
2029
+ if (fs.existsSync(cfgPath)) cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
2030
+ } catch (_) {}
2031
+ const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0
2032
+ ? cfg.srcDirs
2033
+ : ['src', 'app', 'lib', 'packages', 'services', 'api'];
2034
+ const exclude = new Set(['node_modules', '.git', 'dist', 'build', 'out', '__pycache__', '.next', 'coverage', 'target', 'vendor', '.context']);
2035
+ if (Array.isArray(cfg.exclude)) for (const x of cfg.exclude) exclude.add(String(x));
2036
+ const counts = {};
2037
+ for (const k of LANGUAGE_KEYS) counts[k] = 0;
2038
+ const files = [];
2039
+ for (const relDir of srcDirs) {
2040
+ const absDir = path.join(cwd, relDir);
2041
+ if (!fs.existsSync(absDir)) continue;
2042
+ walkFiles(absDir, 8, 0, files, exclude);
2043
+ }
2044
+ for (const f of files) {
2045
+ const lang = detectLanguage(f);
2046
+ if (lang) counts[lang]++;
2047
+ }
2048
+ const covered = LANGUAGE_KEYS.filter((k) => counts[k] > 0).length;
2049
+ const supported = LANGUAGE_KEYS.length;
2050
+ const pct = supported > 0 ? parseFloat(((covered / supported) * 100).toFixed(1)) : 0;
2051
+ return { supported, covered, pct, perLanguage: counts };
2052
+ }
2053
+
2054
+ function readBenchmarkTrend(cwd) {
2055
+ const resultDir = path.join(cwd, 'benchmarks', 'results');
2056
+ if (!fs.existsSync(resultDir)) return [];
2057
+ const files = [];
2058
+ walkFiles(resultDir, 6, 0, files, new Set());
2059
+ const values = [];
2060
+ for (const filePath of files) {
2061
+ const base = path.basename(filePath).toLowerCase();
2062
+ if (!base.endsWith('.json') && !base.endsWith('.jsonl') && !base.endsWith('.ndjson')) continue;
2063
+ let raw = '';
2064
+ try { raw = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
2065
+ if (base.endsWith('.json')) {
2066
+ try {
2067
+ const obj = JSON.parse(raw);
2068
+ const v = toNumber(obj && obj.hitAt5);
2069
+ const m = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
2070
+ if (v !== null) values.push(v);
2071
+ else if (m !== null) values.push(m);
2072
+ } catch (_) {}
2073
+ continue;
2074
+ }
2075
+ for (const line of raw.split('\n').filter(Boolean)) {
2076
+ try {
2077
+ const obj = JSON.parse(line);
2078
+ const v = toNumber(obj && obj.hitAt5);
2079
+ const m = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
2080
+ if (v !== null) values.push(v);
2081
+ else if (m !== null) values.push(m);
2082
+ } catch (_) {}
2083
+ }
2084
+ }
2085
+ return values.slice(-30);
2086
+ }
2087
+
2088
+ function lineChartSvg(values, title, ySuffix) {
2089
+ const width = 760;
2090
+ const height = 210;
2091
+ const left = 38;
2092
+ const right = 18;
2093
+ const top = 22;
2094
+ const bottom = 30;
2095
+ const innerW = width - left - right;
2096
+ const innerH = height - top - bottom;
2097
+ const clean = values.filter((n) => Number.isFinite(n));
2098
+ if (clean.length === 0) {
2099
+ return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}"><rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/><text x="20" y="36" fill="#d7defa" font-size="14" font-family="monospace">${title}</text><text x="20" y="96" fill="#8ea0d9" font-size="13" font-family="monospace">No data yet. Run with --track and --benchmark.</text></svg>`;
2100
+ }
2101
+ const min = Math.min(...clean);
2102
+ const max = Math.max(...clean);
2103
+ const span = max - min || 1;
2104
+ const points = clean.map((v, i) => {
2105
+ const x = left + ((clean.length === 1 ? 0 : i / (clean.length - 1)) * innerW);
2106
+ const y = top + (1 - ((v - min) / span)) * innerH;
2107
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
2108
+ }).join(' ');
2109
+ const latest = clean[clean.length - 1];
2110
+ const grid = [];
2111
+ for (let i = 0; i <= 4; i++) {
2112
+ const gy = top + (i / 4) * innerH;
2113
+ grid.push(`<line x1="${left}" y1="${gy.toFixed(1)}" x2="${left + innerW}" y2="${gy.toFixed(1)}" stroke="#223056" stroke-width="1"/>`);
2114
+ }
2115
+ return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}"><rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/><text x="20" y="26" fill="#d7defa" font-size="13" font-family="monospace">${title}</text>${grid.join('')}<polyline fill="none" stroke="#42d392" stroke-width="2.5" points="${points}"/><text x="20" y="${height - 8}" fill="#8ea0d9" font-size="12" font-family="monospace">latest: ${latest.toFixed(2)}${ySuffix || ''}</text></svg>`;
2116
+ }
2117
+
2118
+ function barChartSvg(perLanguage) {
2119
+ const width = 760;
2120
+ const height = 260;
2121
+ const left = 20;
2122
+ const top = 34;
2123
+ const usableW = width - left * 2;
2124
+ const max = Math.max(1, ...LANGUAGE_KEYS.map((k) => perLanguage[k] || 0));
2125
+ const barW = usableW / LANGUAGE_KEYS.length;
2126
+ const labels = ['ts', 'js', 'py', 'java', 'kt', 'go', 'rs', 'cs', 'cpp', 'rb', 'php', 'swift', 'dart', 'scala', 'vue', 'sv', 'html', 'css', 'yaml', 'sh', 'df'];
2127
+ const bars = [];
2128
+ for (let i = 0; i < LANGUAGE_KEYS.length; i++) {
2129
+ const key = LANGUAGE_KEYS[i];
2130
+ const v = perLanguage[key] || 0;
2131
+ const h = (v / max) * 160;
2132
+ const x = left + i * barW + 2;
2133
+ const y = top + 160 - h;
2134
+ bars.push(`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${Math.max(2, barW - 4).toFixed(1)}" height="${h.toFixed(1)}" fill="#7aa2ff" rx="2"/>`);
2135
+ }
2136
+ const xLabels = labels.map((lbl, i) => {
2137
+ const x = left + i * barW + barW / 2;
2138
+ return `<text x="${x.toFixed(1)}" y="222" fill="#8ea0d9" font-size="9" font-family="monospace" text-anchor="middle">${lbl}</text>`;
2139
+ });
2140
+ return `<svg viewBox="0 0 760 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Extractor coverage by language"><rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/><text x="20" y="24" fill="#d7defa" font-size="13" font-family="monospace">Per-language extractor coverage (file counts)</text><line x1="20" y1="194" x2="740" y2="194" stroke="#223056" stroke-width="1"/>${bars.join('')}${xLabels.join('')}</svg>`;
2141
+ }
2142
+
2143
+ function sparkline(values) {
2144
+ const clean = values.filter((n) => Number.isFinite(n));
2145
+ if (clean.length === 0) return 'n/a';
2146
+ const ticks = '▁▂▃▄▅▆▇█';
2147
+ const min = Math.min(...clean);
2148
+ const max = Math.max(...clean);
2149
+ const span = max - min || 1;
2150
+ return clean.map((v) => {
2151
+ const idx = Math.max(0, Math.min(ticks.length - 1, Math.round(((v - min) / span) * (ticks.length - 1))));
2152
+ return ticks[idx];
2153
+ }).join('');
2154
+ }
2155
+
2156
+ function buildDashboardData(cwd, health) {
2157
+ const entries = readLog(cwd);
2158
+ const recent = entries.slice(-30);
2159
+ const tokenReductionTrend = recent.map((e) => toNumber(e.reductionPct)).filter((n) => n !== null);
2160
+ const hitAt5Trend = readBenchmarkTrend(cwd);
2161
+ const coverage = computeExtractorCoverage(cwd);
2162
+ const finals = entries.map((e) => toNumber(e.finalTokens)).filter((n) => n !== null);
2163
+ const summary = {
2164
+ grade: health.grade,
2165
+ score: health.score,
2166
+ daysSinceRegen: health.daysSinceRegen,
2167
+ totalRuns: entries.length,
2168
+ overBudgetRate: entries.length > 0 ? parseFloat(((entries.filter((e) => e.overBudget).length / entries.length) * 100).toFixed(1)) : 0,
2169
+ p50TokenCount: Math.round(percentile(finals, 50)),
2170
+ p95TokenCount: Math.round(percentile(finals, 95)),
2171
+ overBudgetStreak: overBudgetStreak(entries),
2172
+ extractorCoverage: coverage.pct,
2173
+ };
2174
+ return {
2175
+ summary,
2176
+ tokenReductionTrend,
2177
+ hitAt5Trend,
2178
+ coverage,
2179
+ charts: {
2180
+ tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
2181
+ hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
2182
+ coverageSvg: barChartSvg(coverage.perLanguage),
2183
+ },
2184
+ };
2185
+ }
2186
+
2187
+ function generateDashboardHtml(cwd, health) {
2188
+ const data = buildDashboardData(cwd, health);
2189
+ const cards = [
2190
+ { label: 'Current grade', value: `${data.summary.grade} (${data.summary.score}/100)` },
2191
+ { label: 'Days since regen', value: data.summary.daysSinceRegen === null ? 'n/a' : String(data.summary.daysSinceRegen) },
2192
+ { label: 'Total tracked runs', value: String(data.summary.totalRuns) },
2193
+ { label: 'Over-budget %', value: `${data.summary.overBudgetRate}%` },
2194
+ { label: 'p50 token count', value: String(data.summary.p50TokenCount) },
2195
+ { label: 'p95 token count', value: String(data.summary.p95TokenCount) },
2196
+ { label: 'Over-budget streak', value: String(data.summary.overBudgetStreak) },
2197
+ { label: 'Extractor coverage', value: `${data.summary.extractorCoverage}%` },
2198
+ ];
2199
+ const cardHtml = cards.map((c) => `<div class="card"><div class="label">${c.label}</div><div class="value">${c.value}</div></div>`).join('');
2200
+ const html = [
2201
+ '<!doctype html>', '<html lang="en">', '<head>', '<meta charset="utf-8"/>',
2202
+ '<meta name="viewport" content="width=device-width,initial-scale=1"/>', '<title>SigMap Dashboard</title>',
2203
+ '<style>',
2204
+ 'body{margin:0;background:#0a0f1e;color:#e6ecff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}',
2205
+ '.wrap{max-width:980px;margin:0 auto;padding:24px}',
2206
+ 'h1{font-size:22px;margin:0 0 6px 0}', '.sub{color:#8ea0d9;font-size:12px;margin-bottom:20px}',
2207
+ '.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}',
2208
+ '.card{background:#111a33;border:1px solid #223056;border-radius:10px;padding:10px}',
2209
+ '.label{font-size:11px;color:#8ea0d9;margin-bottom:6px}', '.value{font-size:16px;color:#f5f7ff}',
2210
+ '.panel{background:#111a33;border:1px solid #223056;border-radius:12px;padding:10px;margin-top:12px}',
2211
+ '@media (max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr));}}',
2212
+ '</style>', '</head>', '<body>', '<div class="wrap">',
2213
+ '<h1>SigMap v2.10 dashboard</h1>',
2214
+ '<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
2215
+ `<div class="grid">${cardHtml}</div>`,
2216
+ `<div class="panel">${data.charts.tokenReductionSvg}</div>`,
2217
+ `<div class="panel">${data.charts.hitAt5Svg}</div>`,
2218
+ `<div class="panel">${data.charts.coverageSvg}</div>`,
2219
+ '</div>', '</body>', '</html>',
2220
+ ].join('');
2221
+ return { html, data };
2222
+ }
2223
+
2224
+ function renderHistoryCharts(cwd, health) {
2225
+ const data = buildDashboardData(cwd, health);
2226
+ const lines = [
2227
+ '[sigmap] history charts:',
2228
+ ` token reduction trend : ${sparkline(data.tokenReductionTrend)}`,
2229
+ ` hit@5 trend : ${sparkline(data.hitAt5Trend)}`,
2230
+ ` extractor coverage : ${data.coverage.covered}/${data.coverage.supported} (${data.coverage.pct}%)`,
2231
+ '',
2232
+ '[sigmap] inline svg: token reduction', data.charts.tokenReductionSvg,
2233
+ '',
2234
+ '[sigmap] inline svg: hit@5', data.charts.hitAt5Svg,
2235
+ '',
2236
+ '[sigmap] inline svg: coverage', data.charts.coverageSvg,
2237
+ ];
2238
+ return {
2239
+ text: lines.join('\n'),
2240
+ tokenReductionSparkline: sparkline(data.tokenReductionTrend),
2241
+ hitAt5Sparkline: sparkline(data.hitAt5Trend),
2242
+ summary: data.summary,
2243
+ charts: data.charts,
2244
+ };
2245
+ }
2246
+
2247
+ module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak };
2248
+ };
2249
+
1940
2250
  // ── ./src/health/scorer ──
1941
2251
  __factories["./src/health/scorer"] = function(module, exports) {
1942
2252
 
@@ -1970,36 +2280,148 @@ __factories["./src/health/scorer"] = function(module, exports) {
1970
2280
  function score(cwd) {
1971
2281
  const fs = require('fs');
1972
2282
  const path = require('path');
2283
+
2284
+ function toNumber(v) {
2285
+ const n = Number(v);
2286
+ return Number.isFinite(n) ? n : null;
2287
+ }
2288
+
2289
+ function percentile(values, p) {
2290
+ if (!Array.isArray(values) || values.length === 0) return 0;
2291
+ const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
2292
+ if (sorted.length === 0) return 0;
2293
+ if (p <= 0) return sorted[0];
2294
+ if (p >= 100) return sorted[sorted.length - 1];
2295
+ const idx = (p / 100) * (sorted.length - 1);
2296
+ const lo = Math.floor(idx);
2297
+ const hi = Math.ceil(idx);
2298
+ if (lo === hi) return sorted[lo];
2299
+ const frac = idx - lo;
2300
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;
2301
+ }
2302
+
2303
+ function overBudgetStreak(entries) {
2304
+ if (!Array.isArray(entries) || entries.length === 0) return 0;
2305
+ let streak = 0;
2306
+ for (let i = entries.length - 1; i >= 0; i--) {
2307
+ if (entries[i] && entries[i].overBudget) streak++;
2308
+ else break;
2309
+ }
2310
+ return streak;
2311
+ }
2312
+
2313
+ function walkFiles(dir, maxDepth, depth, out, excludeSet) {
2314
+ if (depth > maxDepth) return;
2315
+ let entries = [];
2316
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
2317
+ for (const entry of entries) {
2318
+ const abs = path.join(dir, entry.name);
2319
+ const rel = abs.replace(/\\/g, '/');
2320
+ if (excludeSet.has(entry.name) || rel.includes('/node_modules/') || rel.includes('/.git/')) continue;
2321
+ if (entry.isDirectory()) walkFiles(abs, maxDepth, depth + 1, out, excludeSet);
2322
+ else if (entry.isFile()) out.push(abs);
2323
+ }
2324
+ }
2325
+
2326
+ function detectLanguage(filePath) {
2327
+ const base = path.basename(filePath);
2328
+ const ext = path.extname(filePath).toLowerCase();
2329
+ if (base === 'Dockerfile' || /^Dockerfile\./.test(base)) return 'dockerfile';
2330
+ if (ext === '.ts' || ext === '.tsx') return 'typescript';
2331
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') return 'javascript';
2332
+ if (ext === '.py' || ext === '.pyw') return 'python';
2333
+ if (ext === '.java') return 'java';
2334
+ if (ext === '.kt' || ext === '.kts') return 'kotlin';
2335
+ if (ext === '.go') return 'go';
2336
+ if (ext === '.rs') return 'rust';
2337
+ if (ext === '.cs') return 'csharp';
2338
+ if (ext === '.cpp' || ext === '.c' || ext === '.h' || ext === '.hpp' || ext === '.cc') return 'cpp';
2339
+ if (ext === '.rb' || ext === '.rake') return 'ruby';
2340
+ if (ext === '.php') return 'php';
2341
+ if (ext === '.swift') return 'swift';
2342
+ if (ext === '.dart') return 'dart';
2343
+ if (ext === '.scala' || ext === '.sc') return 'scala';
2344
+ if (ext === '.vue') return 'vue';
2345
+ if (ext === '.svelte') return 'svelte';
2346
+ if (ext === '.html' || ext === '.htm') return 'html';
2347
+ if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') return 'css';
2348
+ if (ext === '.yml' || ext === '.yaml') return 'yaml';
2349
+ if (ext === '.sh' || ext === '.bash' || ext === '.zsh' || ext === '.fish') return 'shell';
2350
+ return null;
2351
+ }
2352
+
2353
+ function computeExtractorCoverage(cwd, cfg) {
2354
+ const LANGUAGE_KEYS = [
2355
+ 'typescript', 'javascript', 'python', 'java', 'kotlin', 'go', 'rust',
2356
+ 'csharp', 'cpp', 'ruby', 'php', 'swift', 'dart', 'scala', 'vue',
2357
+ 'svelte', 'html', 'css', 'yaml', 'shell', 'dockerfile',
2358
+ ];
2359
+ const srcDirs = Array.isArray(cfg && cfg.srcDirs) && cfg.srcDirs.length > 0
2360
+ ? cfg.srcDirs
2361
+ : ['src', 'app', 'lib', 'packages', 'services', 'api'];
2362
+ const exclude = new Set(['node_modules', '.git', 'dist', 'build', 'out', '__pycache__', '.next', 'coverage', 'target', 'vendor', '.context']);
2363
+ if (cfg && Array.isArray(cfg.exclude)) for (const x of cfg.exclude) exclude.add(String(x));
2364
+ const counts = {};
2365
+ for (const k of LANGUAGE_KEYS) counts[k] = 0;
2366
+ const files = [];
2367
+ for (const relDir of srcDirs) {
2368
+ const absDir = path.join(cwd, relDir);
2369
+ if (!fs.existsSync(absDir)) continue;
2370
+ walkFiles(absDir, 8, 0, files, exclude);
2371
+ }
2372
+ for (const f of files) {
2373
+ const lang = detectLanguage(f);
2374
+ if (lang) counts[lang]++;
2375
+ }
2376
+ const covered = LANGUAGE_KEYS.filter((k) => counts[k] > 0).length;
2377
+ return LANGUAGE_KEYS.length > 0 ? parseFloat(((covered / LANGUAGE_KEYS.length) * 100).toFixed(1)) : 0;
2378
+ }
1973
2379
 
1974
2380
  let tokenReductionPct = null;
1975
2381
  let daysSinceRegen = null;
1976
2382
  let strategyFreshnessDays = null;
1977
2383
  let overBudgetRuns = 0;
1978
2384
  let totalRuns = 0;
2385
+ let p50TokenCount = 0;
2386
+ let p95TokenCount = 0;
2387
+ let overBudgetStreakCount = 0;
2388
+ let extractorCoverage = 0;
2389
+ let cfgObj = null;
2390
+ let entries = [];
1979
2391
 
1980
2392
  // ── Detect active strategy ──────────────────────────────────────────────
1981
2393
  let strategy = 'full';
1982
2394
  try {
1983
2395
  const cfgPath = path.join(cwd, 'gen-context.config.json');
1984
2396
  if (fs.existsSync(cfgPath)) {
1985
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1986
- strategy = cfg.strategy || 'full';
2397
+ cfgObj = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
2398
+ strategy = cfgObj.strategy || 'full';
1987
2399
  }
1988
2400
  } catch (_) {}
1989
2401
 
1990
2402
  // ── Read usage log via tracking logger ──────────────────────────────────
1991
2403
  try {
1992
2404
  const { readLog, summarize } = __require('./src/tracking/logger');
1993
- const entries = readLog(cwd);
2405
+ entries = readLog(cwd);
1994
2406
  const s = summarize(entries);
1995
2407
  // Only set tokenReductionPct when there is actual history; a brand-new/
1996
2408
  // untracked project should not be penalised for "0% reduction".
1997
2409
  if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
1998
2410
  overBudgetRuns = s.overBudgetRuns;
1999
2411
  totalRuns = s.totalRuns;
2412
+ const finals = entries.map((e) => toNumber(e.finalTokens)).filter((n) => n !== null);
2413
+ p50TokenCount = Math.round(percentile(finals, 50));
2414
+ p95TokenCount = Math.round(percentile(finals, 95));
2415
+ overBudgetStreakCount = overBudgetStreak(entries);
2000
2416
  } catch (_) {
2001
2417
  // No usage log yet — proceed with nulls
2002
2418
  }
2419
+
2420
+ try {
2421
+ extractorCoverage = computeExtractorCoverage(cwd, cfgObj || {});
2422
+ } catch (_) {
2423
+ extractorCoverage = 0;
2424
+ }
2003
2425
 
2004
2426
  // ── Days since primary context file was last regenerated ─────────────────
2005
2427
  try {
@@ -2057,7 +2479,20 @@ __factories["./src/health/scorer"] = function(module, exports) {
2057
2479
  else if (points >= 60) grade = 'C';
2058
2480
  else grade = 'D';
2059
2481
 
2060
- return { score: points, grade, strategy, tokenReductionPct, daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns };
2482
+ return {
2483
+ score: points,
2484
+ grade,
2485
+ strategy,
2486
+ tokenReductionPct,
2487
+ daysSinceRegen,
2488
+ strategyFreshnessDays,
2489
+ totalRuns,
2490
+ overBudgetRuns,
2491
+ p50TokenCount,
2492
+ p95TokenCount,
2493
+ overBudgetStreak: overBudgetStreakCount,
2494
+ extractorCoverage,
2495
+ };
2061
2496
  }
2062
2497
 
2063
2498
  module.exports = { score };
@@ -3107,7 +3542,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
3107
3542
 
3108
3543
  const SERVER_INFO = {
3109
3544
  name: 'sigmap',
3110
- version: '1.5.0',
3545
+ version: '2.10.0',
3111
3546
  description: 'SigMap MCP server — code signatures on demand',
3112
3547
  };
3113
3548
 
@@ -4304,7 +4739,7 @@ const path = require('path');
4304
4739
  const os = require('os');
4305
4740
  const { execSync } = require('child_process');
4306
4741
 
4307
- const VERSION = '2.9.1';
4742
+ const VERSION = '2.10.0';
4308
4743
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
4309
4744
 
4310
4745
  function requireSourceOrBundled(key) {
@@ -5503,6 +5938,8 @@ Usage:
5503
5938
  node gen-context.js --report Token reduction stats to stdout
5504
5939
  node gen-context.js --report --json Token report as JSON (for CI; exits 1 if over budget)
5505
5940
  node gen-context.js --report --history Print usage log summary from .context/usage.ndjson
5941
+ node gen-context.js --report --history --chart Include inline SVG charts + Unicode sparklines
5942
+ node gen-context.js --dashboard Write benchmarks/reports/dashboard.html
5506
5943
  node gen-context.js --suggest-tool "<task>" Recommend model tier for a task description
5507
5944
  node gen-context.js --suggest-tool "<task>" --json Machine-readable tier recommendation
5508
5945
  node gen-context.js --health Print composite health score
@@ -5625,6 +6062,31 @@ function main() {
5625
6062
  }
5626
6063
  console.log(` total runs : ${result.totalRuns}`);
5627
6064
  console.log(` over-budget runs: ${result.overBudgetRuns}`);
6065
+ console.log(` p50 token count : ${result.p50TokenCount}`);
6066
+ console.log(` p95 token count : ${result.p95TokenCount}`);
6067
+ console.log(` overbudget streak: ${result.overBudgetStreak}`);
6068
+ console.log(` extractor cover.: ${result.extractorCoverage}%`);
6069
+ }
6070
+ process.exit(0);
6071
+ }
6072
+
6073
+ if (args.includes('--dashboard')) {
6074
+ try {
6075
+ const { score } = __require('./src/health/scorer');
6076
+ const health = score(cwd);
6077
+ const { generateDashboardHtml } = requireSourceOrBundled('./src/format/dashboard');
6078
+ const out = generateDashboardHtml(cwd, health);
6079
+ const outPath = path.join(cwd, 'benchmarks', 'reports', 'dashboard.html');
6080
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
6081
+ fs.writeFileSync(outPath, out.html, 'utf8');
6082
+ if (args.includes('--json')) {
6083
+ process.stdout.write(JSON.stringify({ ok: true, file: path.relative(cwd, outPath), summary: out.data.summary }) + '\n');
6084
+ } else {
6085
+ console.log(`[sigmap] dashboard written: ${path.relative(cwd, outPath)}`);
6086
+ }
6087
+ } catch (err) {
6088
+ console.error(`[sigmap] dashboard error: ${err.message}`);
6089
+ process.exit(1);
5628
6090
  }
5629
6091
  process.exit(0);
5630
6092
  }
@@ -5876,8 +6338,25 @@ function main() {
5876
6338
  const { readLog, summarize } = __require('./src/tracking/logger');
5877
6339
  const entries = readLog(cwd);
5878
6340
  const summary = summarize(entries);
6341
+ let chartInfo = null;
6342
+ if (args.includes('--chart')) {
6343
+ const { score } = __require('./src/health/scorer');
6344
+ const { renderHistoryCharts } = requireSourceOrBundled('./src/format/dashboard');
6345
+ chartInfo = renderHistoryCharts(cwd, score(cwd));
6346
+ }
5879
6347
  if (args.includes('--json')) {
5880
- process.stdout.write(JSON.stringify(summary) + '\n');
6348
+ const payload = chartInfo
6349
+ ? {
6350
+ ...summary,
6351
+ chart: {
6352
+ tokenReductionSparkline: chartInfo.tokenReductionSparkline,
6353
+ hitAt5Sparkline: chartInfo.hitAt5Sparkline,
6354
+ summary: chartInfo.summary,
6355
+ charts: chartInfo.charts,
6356
+ },
6357
+ }
6358
+ : summary;
6359
+ process.stdout.write(JSON.stringify(payload) + '\n');
5881
6360
  } else {
5882
6361
  console.log('[sigmap] usage history:');
5883
6362
  console.log(` total runs : ${summary.totalRuns}`);
@@ -5886,6 +6365,10 @@ function main() {
5886
6365
  console.log(` over-budget runs: ${summary.overBudgetRuns}`);
5887
6366
  if (summary.firstRun) console.log(` first run : ${summary.firstRun}`);
5888
6367
  if (summary.lastRun) console.log(` last run : ${summary.lastRun}`);
6368
+ if (chartInfo) {
6369
+ console.log('');
6370
+ console.log(chartInfo.text);
6371
+ }
5889
6372
  }
5890
6373
  } catch (err) {
5891
6374
  console.warn(`[sigmap] tracking: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "2.9.1",
3
+ "version": "2.10.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": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -128,9 +128,9 @@ const health = score('/path/to/project');
128
128
 
129
129
  All existing CLI flags (`--generate`, `--watch`, `--mcp`, `--query`, `--analyze`, `--benchmark`, `--health`, …) are unchanged.
130
130
 
131
- ## What's next — v2.9
131
+ ## What's next — v2.10
132
132
 
133
- v2.9 adds JetBrains plugin support install SigMap natively in IntelliJ IDEA, WebStorm, PyCharm, GoLand, RubyMine. Includes toolbar actions, settings panel, file watcher integration, and automated JetBrains Marketplace publishing. See [issue #23](https://github.com/manojmallick/sigmap/issues/23).
133
+ v2.10 adds reporting charts and advanced metrics for benchmark visibility. This milestone focuses on chart-ready report output, precision@K/recall@K/MRR trends, and CI-friendly metrics artifacts. See [issue #25](https://github.com/manojmallick/sigmap/issues/25).
134
134
 
135
135
  See the full [roadmap](https://manojmallick.github.io/sigmap/roadmap.html).
136
136
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "2.9.1",
3
+ "version": "2.10.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,400 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readLog } = require('../tracking/logger');
6
+
7
+ const LANGUAGE_KEYS = [
8
+ 'typescript', 'javascript', 'python', 'java', 'kotlin', 'go', 'rust',
9
+ 'csharp', 'cpp', 'ruby', 'php', 'swift', 'dart', 'scala', 'vue',
10
+ 'svelte', 'html', 'css', 'yaml', 'shell', 'dockerfile',
11
+ ];
12
+
13
+ function toNumber(v) {
14
+ const n = Number(v);
15
+ return Number.isFinite(n) ? n : null;
16
+ }
17
+
18
+ function percentile(values, p) {
19
+ if (!Array.isArray(values) || values.length === 0) return 0;
20
+ const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
21
+ if (sorted.length === 0) return 0;
22
+ if (p <= 0) return sorted[0];
23
+ if (p >= 100) return sorted[sorted.length - 1];
24
+ const idx = (p / 100) * (sorted.length - 1);
25
+ const lo = Math.floor(idx);
26
+ const hi = Math.ceil(idx);
27
+ if (lo === hi) return sorted[lo];
28
+ const frac = idx - lo;
29
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;
30
+ }
31
+
32
+ function overBudgetStreak(entries) {
33
+ if (!Array.isArray(entries) || entries.length === 0) return 0;
34
+ let streak = 0;
35
+ for (let i = entries.length - 1; i >= 0; i--) {
36
+ if (entries[i] && entries[i].overBudget) streak++;
37
+ else break;
38
+ }
39
+ return streak;
40
+ }
41
+
42
+ function loadConfig(cwd) {
43
+ try {
44
+ const p = path.join(cwd, 'gen-context.config.json');
45
+ if (!fs.existsSync(p)) return null;
46
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
47
+ } catch (_) {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function shouldExclude(rel, excludeSet) {
53
+ if (!rel) return true;
54
+ const parts = rel.split('/');
55
+ for (const part of parts) {
56
+ if (excludeSet.has(part)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function detectLanguage(filePath) {
62
+ const base = path.basename(filePath);
63
+ const ext = path.extname(filePath).toLowerCase();
64
+ if (base === 'Dockerfile' || /^Dockerfile\./.test(base)) return 'dockerfile';
65
+ if (ext === '.ts' || ext === '.tsx') return 'typescript';
66
+ if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') return 'javascript';
67
+ if (ext === '.py' || ext === '.pyw') return 'python';
68
+ if (ext === '.java') return 'java';
69
+ if (ext === '.kt' || ext === '.kts') return 'kotlin';
70
+ if (ext === '.go') return 'go';
71
+ if (ext === '.rs') return 'rust';
72
+ if (ext === '.cs') return 'csharp';
73
+ if (ext === '.cpp' || ext === '.c' || ext === '.h' || ext === '.hpp' || ext === '.cc') return 'cpp';
74
+ if (ext === '.rb' || ext === '.rake') return 'ruby';
75
+ if (ext === '.php') return 'php';
76
+ if (ext === '.swift') return 'swift';
77
+ if (ext === '.dart') return 'dart';
78
+ if (ext === '.scala' || ext === '.sc') return 'scala';
79
+ if (ext === '.vue') return 'vue';
80
+ if (ext === '.svelte') return 'svelte';
81
+ if (ext === '.html' || ext === '.htm') return 'html';
82
+ if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') return 'css';
83
+ if (ext === '.yml' || ext === '.yaml') return 'yaml';
84
+ if (ext === '.sh' || ext === '.bash' || ext === '.zsh' || ext === '.fish') return 'shell';
85
+ return null;
86
+ }
87
+
88
+ function walkFiles(dir, maxDepth, depth, out, excludeSet) {
89
+ if (depth > maxDepth) return;
90
+ let entries = [];
91
+ try {
92
+ entries = fs.readdirSync(dir, { withFileTypes: true });
93
+ } catch (_) {
94
+ return;
95
+ }
96
+ for (const entry of entries) {
97
+ const abs = path.join(dir, entry.name);
98
+ const rel = abs.replace(/\\/g, '/');
99
+ if (shouldExclude(rel, excludeSet)) continue;
100
+ if (entry.isDirectory()) {
101
+ walkFiles(abs, maxDepth, depth + 1, out, excludeSet);
102
+ } else if (entry.isFile()) {
103
+ out.push(abs);
104
+ }
105
+ }
106
+ }
107
+
108
+ function computeExtractorCoverage(cwd) {
109
+ const cfg = loadConfig(cwd) || {};
110
+ const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0
111
+ ? cfg.srcDirs
112
+ : ['src', 'app', 'lib', 'packages', 'services', 'api'];
113
+ const exclude = new Set([
114
+ 'node_modules', '.git', 'dist', 'build', 'out', '__pycache__', '.next',
115
+ 'coverage', 'target', 'vendor', '.context', 'jetbrains-plugin/build',
116
+ ]);
117
+ if (Array.isArray(cfg.exclude)) {
118
+ for (const item of cfg.exclude) exclude.add(String(item));
119
+ }
120
+
121
+ const counts = {};
122
+ for (const key of LANGUAGE_KEYS) counts[key] = 0;
123
+
124
+ const files = [];
125
+ for (const relDir of srcDirs) {
126
+ const absDir = path.join(cwd, relDir);
127
+ if (!fs.existsSync(absDir)) continue;
128
+ walkFiles(absDir, 8, 0, files, exclude);
129
+ }
130
+
131
+ for (const f of files) {
132
+ const lang = detectLanguage(f);
133
+ if (lang) counts[lang]++;
134
+ }
135
+
136
+ const covered = LANGUAGE_KEYS.filter((k) => counts[k] > 0).length;
137
+ const supported = LANGUAGE_KEYS.length;
138
+ const pct = supported > 0 ? parseFloat(((covered / supported) * 100).toFixed(1)) : 0;
139
+ return { supported, covered, pct, perLanguage: counts };
140
+ }
141
+
142
+ function readBenchmarkTrend(cwd) {
143
+ const resultDir = path.join(cwd, 'benchmarks', 'results');
144
+ if (!fs.existsSync(resultDir)) return [];
145
+
146
+ const files = [];
147
+ walkFiles(resultDir, 6, 0, files, new Set());
148
+
149
+ const values = [];
150
+ for (const filePath of files) {
151
+ const base = path.basename(filePath).toLowerCase();
152
+ if (!base.endsWith('.json') && !base.endsWith('.jsonl') && !base.endsWith('.ndjson')) continue;
153
+ let raw = '';
154
+ try {
155
+ raw = fs.readFileSync(filePath, 'utf8');
156
+ } catch (_) {
157
+ continue;
158
+ }
159
+
160
+ if (base.endsWith('.json')) {
161
+ try {
162
+ const obj = JSON.parse(raw);
163
+ const direct = toNumber(obj && obj.hitAt5);
164
+ const nested = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
165
+ if (direct !== null) values.push(direct);
166
+ else if (nested !== null) values.push(nested);
167
+ } catch (_) {}
168
+ continue;
169
+ }
170
+
171
+ const lines = raw.split('\n').filter(Boolean);
172
+ for (const line of lines) {
173
+ try {
174
+ const obj = JSON.parse(line);
175
+ const direct = toNumber(obj && obj.hitAt5);
176
+ const nested = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
177
+ if (direct !== null) values.push(direct);
178
+ else if (nested !== null) values.push(nested);
179
+ } catch (_) {}
180
+ }
181
+ }
182
+
183
+ return values.slice(-30);
184
+ }
185
+
186
+ function lineChartSvg(values, title, ySuffix) {
187
+ const width = 760;
188
+ const height = 210;
189
+ const left = 38;
190
+ const right = 18;
191
+ const top = 22;
192
+ const bottom = 30;
193
+ const innerW = width - left - right;
194
+ const innerH = height - top - bottom;
195
+ const clean = values.filter((n) => Number.isFinite(n));
196
+
197
+ if (clean.length === 0) {
198
+ return [
199
+ `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}">`,
200
+ '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
201
+ `<text x="20" y="36" fill="#d7defa" font-size="14" font-family="monospace">${title}</text>`,
202
+ '<text x="20" y="96" fill="#8ea0d9" font-size="13" font-family="monospace">No data yet. Run with --track and --benchmark.</text>',
203
+ '</svg>',
204
+ ].join('');
205
+ }
206
+
207
+ const min = Math.min(...clean);
208
+ const max = Math.max(...clean);
209
+ const span = max - min || 1;
210
+
211
+ const points = clean.map((v, i) => {
212
+ const x = left + ((clean.length === 1 ? 0 : i / (clean.length - 1)) * innerW);
213
+ const y = top + (1 - ((v - min) / span)) * innerH;
214
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
215
+ }).join(' ');
216
+
217
+ const latest = clean[clean.length - 1];
218
+ const yLabel = ySuffix || '';
219
+ const grid = [];
220
+ for (let i = 0; i <= 4; i++) {
221
+ const gy = top + (i / 4) * innerH;
222
+ grid.push(`<line x1="${left}" y1="${gy.toFixed(1)}" x2="${left + innerW}" y2="${gy.toFixed(1)}" stroke="#223056" stroke-width="1"/>`);
223
+ }
224
+
225
+ return [
226
+ `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}">`,
227
+ '<defs><linearGradient id="lineFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#42d392" stop-opacity="0.25"/><stop offset="100%" stop-color="#42d392" stop-opacity="0"/></linearGradient></defs>',
228
+ '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
229
+ `<text x="20" y="26" fill="#d7defa" font-size="13" font-family="monospace">${title}</text>`,
230
+ grid.join(''),
231
+ `<polyline fill="none" stroke="#42d392" stroke-width="2.5" points="${points}"/>`,
232
+ `<text x="20" y="${height - 8}" fill="#8ea0d9" font-size="12" font-family="monospace">latest: ${latest.toFixed(2)}${yLabel}</text>`,
233
+ '</svg>',
234
+ ].join('');
235
+ }
236
+
237
+ function barChartSvg(perLanguage) {
238
+ const width = 760;
239
+ const height = 260;
240
+ const left = 20;
241
+ const top = 34;
242
+ const usableW = width - left * 2;
243
+ const keys = LANGUAGE_KEYS.slice();
244
+ const max = Math.max(1, ...keys.map((k) => perLanguage[k] || 0));
245
+ const barW = usableW / keys.length;
246
+
247
+ const bars = [];
248
+ for (let i = 0; i < keys.length; i++) {
249
+ const key = keys[i];
250
+ const v = perLanguage[key] || 0;
251
+ const h = (v / max) * 160;
252
+ const x = left + i * barW + 2;
253
+ const y = top + 160 - h;
254
+ bars.push(`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${Math.max(2, barW - 4).toFixed(1)}" height="${h.toFixed(1)}" fill="#7aa2ff" rx="2"/>`);
255
+ }
256
+
257
+ const labels = ['ts', 'js', 'py', 'java', 'kt', 'go', 'rs', 'cs', 'cpp', 'rb', 'php', 'swift', 'dart', 'scala', 'vue', 'sv', 'html', 'css', 'yaml', 'sh', 'df'];
258
+ const xLabels = labels.map((lbl, i) => {
259
+ const x = left + i * barW + barW / 2;
260
+ return `<text x="${x.toFixed(1)}" y="222" fill="#8ea0d9" font-size="9" font-family="monospace" text-anchor="middle">${lbl}</text>`;
261
+ });
262
+
263
+ return [
264
+ `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Extractor coverage by language">`,
265
+ '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
266
+ '<text x="20" y="24" fill="#d7defa" font-size="13" font-family="monospace">Per-language extractor coverage (file counts)</text>',
267
+ '<line x1="20" y1="194" x2="740" y2="194" stroke="#223056" stroke-width="1"/>',
268
+ bars.join(''),
269
+ xLabels.join(''),
270
+ '</svg>',
271
+ ].join('');
272
+ }
273
+
274
+ function sparkline(values) {
275
+ const clean = values.filter((n) => Number.isFinite(n));
276
+ if (clean.length === 0) return 'n/a';
277
+ const ticks = '▁▂▃▄▅▆▇█';
278
+ const min = Math.min(...clean);
279
+ const max = Math.max(...clean);
280
+ const span = max - min || 1;
281
+ return clean.map((v) => {
282
+ const idx = Math.max(0, Math.min(ticks.length - 1, Math.round(((v - min) / span) * (ticks.length - 1))));
283
+ return ticks[idx];
284
+ }).join('');
285
+ }
286
+
287
+ function buildDashboardData(cwd, health) {
288
+ const entries = readLog(cwd);
289
+ const recent = entries.slice(-30);
290
+ const tokenReductionTrend = recent.map((e) => toNumber(e.reductionPct)).filter((n) => n !== null);
291
+ const hitAt5Trend = readBenchmarkTrend(cwd);
292
+ const coverage = computeExtractorCoverage(cwd);
293
+
294
+ const finals = entries.map((e) => toNumber(e.finalTokens)).filter((n) => n !== null);
295
+ const summary = {
296
+ grade: health.grade,
297
+ score: health.score,
298
+ daysSinceRegen: health.daysSinceRegen,
299
+ totalRuns: entries.length,
300
+ overBudgetRate: entries.length > 0
301
+ ? parseFloat(((entries.filter((e) => e.overBudget).length / entries.length) * 100).toFixed(1))
302
+ : 0,
303
+ p50TokenCount: Math.round(percentile(finals, 50)),
304
+ p95TokenCount: Math.round(percentile(finals, 95)),
305
+ overBudgetStreak: overBudgetStreak(entries),
306
+ extractorCoverage: coverage.pct,
307
+ };
308
+
309
+ return {
310
+ summary,
311
+ tokenReductionTrend,
312
+ hitAt5Trend,
313
+ coverage,
314
+ charts: {
315
+ tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
316
+ hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
317
+ coverageSvg: barChartSvg(coverage.perLanguage),
318
+ },
319
+ };
320
+ }
321
+
322
+ function generateDashboardHtml(cwd, health) {
323
+ const data = buildDashboardData(cwd, health);
324
+ const cards = [
325
+ { label: 'Current grade', value: `${data.summary.grade} (${data.summary.score}/100)` },
326
+ { label: 'Days since regen', value: data.summary.daysSinceRegen === null ? 'n/a' : String(data.summary.daysSinceRegen) },
327
+ { label: 'Total tracked runs', value: String(data.summary.totalRuns) },
328
+ { label: 'Over-budget %', value: `${data.summary.overBudgetRate}%` },
329
+ { label: 'p50 token count', value: String(data.summary.p50TokenCount) },
330
+ { label: 'p95 token count', value: String(data.summary.p95TokenCount) },
331
+ { label: 'Over-budget streak', value: String(data.summary.overBudgetStreak) },
332
+ { label: 'Extractor coverage', value: `${data.summary.extractorCoverage}%` },
333
+ ];
334
+
335
+ const cardHtml = cards.map((c) => `<div class="card"><div class="label">${c.label}</div><div class="value">${c.value}</div></div>`).join('');
336
+
337
+ const html = [
338
+ '<!doctype html>',
339
+ '<html lang="en">',
340
+ '<head>',
341
+ '<meta charset="utf-8"/>',
342
+ '<meta name="viewport" content="width=device-width,initial-scale=1"/>',
343
+ '<title>SigMap Dashboard</title>',
344
+ '<style>',
345
+ 'body{margin:0;background:#0a0f1e;color:#e6ecff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}',
346
+ '.wrap{max-width:980px;margin:0 auto;padding:24px}',
347
+ 'h1{font-size:22px;margin:0 0 6px 0}',
348
+ '.sub{color:#8ea0d9;font-size:12px;margin-bottom:20px}',
349
+ '.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}',
350
+ '.card{background:#111a33;border:1px solid #223056;border-radius:10px;padding:10px}',
351
+ '.label{font-size:11px;color:#8ea0d9;margin-bottom:6px}',
352
+ '.value{font-size:16px;color:#f5f7ff}',
353
+ '.panel{background:#111a33;border:1px solid #223056;border-radius:12px;padding:10px;margin-top:12px}',
354
+ '@media (max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr));}}',
355
+ '</style>',
356
+ '</head>',
357
+ '<body>',
358
+ '<div class="wrap">',
359
+ '<h1>SigMap v2.10 dashboard</h1>',
360
+ '<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
361
+ `<div class="grid">${cardHtml}</div>`,
362
+ `<div class="panel">${data.charts.tokenReductionSvg}</div>`,
363
+ `<div class="panel">${data.charts.hitAt5Svg}</div>`,
364
+ `<div class="panel">${data.charts.coverageSvg}</div>`,
365
+ '</div>',
366
+ '</body>',
367
+ '</html>',
368
+ ].join('');
369
+
370
+ return { html, data };
371
+ }
372
+
373
+ function renderHistoryCharts(cwd, health) {
374
+ const data = buildDashboardData(cwd, health);
375
+ const lines = [
376
+ '[sigmap] history charts:',
377
+ ` token reduction trend : ${sparkline(data.tokenReductionTrend)}`,
378
+ ` hit@5 trend : ${sparkline(data.hitAt5Trend)}`,
379
+ ` extractor coverage : ${data.coverage.covered}/${data.coverage.supported} (${data.coverage.pct}%)`,
380
+ '',
381
+ '[sigmap] inline svg: token reduction',
382
+ data.charts.tokenReductionSvg,
383
+ '',
384
+ '[sigmap] inline svg: hit@5',
385
+ data.charts.hitAt5Svg,
386
+ '',
387
+ '[sigmap] inline svg: coverage',
388
+ data.charts.coverageSvg,
389
+ ];
390
+
391
+ return {
392
+ text: lines.join('\n'),
393
+ tokenReductionSparkline: sparkline(data.tokenReductionTrend),
394
+ hitAt5Sparkline: sparkline(data.hitAt5Trend),
395
+ summary: data.summary,
396
+ charts: data.charts,
397
+ };
398
+ }
399
+
400
+ module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak };
@@ -36,6 +36,10 @@ function score(cwd) {
36
36
  let strategyFreshnessDays = null;
37
37
  let overBudgetRuns = 0;
38
38
  let totalRuns = 0;
39
+ let p50TokenCount = 0;
40
+ let p95TokenCount = 0;
41
+ let overBudgetStreak = 0;
42
+ let extractorCoverage = 0;
39
43
 
40
44
  // ── Detect active strategy ────────────────────────────────────────────────
41
45
  let strategy = 'full';
@@ -50,6 +54,7 @@ function score(cwd) {
50
54
  // ── Read usage log via tracking logger ──────────────────────────────────
51
55
  try {
52
56
  const { readLog, summarize } = require('../tracking/logger');
57
+ const { percentile, overBudgetStreak: calcOverBudgetStreak } = require('../format/dashboard');
53
58
  const entries = readLog(cwd);
54
59
  const s = summarize(entries);
55
60
  // Only set tokenReductionPct when there is actual history; a brand-new/
@@ -57,10 +62,21 @@ function score(cwd) {
57
62
  if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
58
63
  overBudgetRuns = s.overBudgetRuns;
59
64
  totalRuns = s.totalRuns;
65
+ const finals = entries.map((e) => Number(e.finalTokens)).filter(Number.isFinite);
66
+ p50TokenCount = Math.round(percentile(finals, 50));
67
+ p95TokenCount = Math.round(percentile(finals, 95));
68
+ overBudgetStreak = calcOverBudgetStreak(entries);
60
69
  } catch (_) {
61
70
  // No usage log yet — proceed with nulls
62
71
  }
63
72
 
73
+ try {
74
+ const { computeExtractorCoverage } = require('../format/dashboard');
75
+ extractorCoverage = computeExtractorCoverage(cwd).pct;
76
+ } catch (_) {
77
+ extractorCoverage = 0;
78
+ }
79
+
64
80
  // ── Days since primary context file was last regenerated ─────────────────
65
81
  try {
66
82
  const ctxFile = path.join(cwd, '.github', 'copilot-instructions.md');
@@ -117,7 +133,20 @@ function score(cwd) {
117
133
  else if (points >= 60) grade = 'C';
118
134
  else grade = 'D';
119
135
 
120
- return { score: points, grade, strategy, tokenReductionPct, daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns };
136
+ return {
137
+ score: points,
138
+ grade,
139
+ strategy,
140
+ tokenReductionPct,
141
+ daysSinceRegen,
142
+ strategyFreshnessDays,
143
+ totalRuns,
144
+ overBudgetRuns,
145
+ p50TokenCount,
146
+ p95TokenCount,
147
+ overBudgetStreak,
148
+ extractorCoverage,
149
+ };
121
150
  }
122
151
 
123
152
  module.exports = { score };
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: '2.9.1',
21
+ version: '2.10.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24