sigmap 2.9.0 → 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,26 +6,57 @@ Format: [Semantic Versioning](https://semver.org/)
6
6
 
7
7
  ---
8
8
 
9
- ## [2.9.0] — upcoming · [#23](https://github.com/manojmallick/sigmap/issues/23) · branch: `feat/v2.9-jetbrains-plugin`
9
+ ## [2.10.0] — upcoming · [#25](https://github.com/manojmallick/sigmap/issues/25) · branch: `feat/v2.10-reporting-charts-advanced-metrics`
10
10
 
11
11
  ### Planned additions
12
- - **JetBrains plugin skeleton** — `jetbrains-plugin/` with Gradle build, plugin.xml manifest
13
- - **Context provider integration** — integrate SigMap MCP server into JetBrains Platform SDK
14
- - **Toolbar actions** — "Regenerate Context", "Open Context File", "View Roadmap" buttons
15
- - **Settings panel** — configure srcDirs, exclude patterns, maxTokens, routing presets
16
- - **File watcher integration** — auto-regenerate context on file changes (opt-in)
17
- - **Multi-IDE support** — test on IntelliJ IDEA, WebStorm, PyCharm, GoLand, RubyMine
18
- - **JetBrains Marketplace publishing** — automated release via GitHub Actions
19
- - **Documentation** — `docs/JETBRAINS_SETUP.md` with installation guide
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.
20
17
 
21
18
  ### Go / No-go criteria
22
- - Plugin installs successfully on IntelliJ IDEA 2024.1+
23
- - Context regeneration works via toolbar button
24
- - Settings panel allows configuration of all config options
25
- - File watcher detects changes and regenerates context
26
- - Plugin passes JetBrains Plugin Verifier
27
- - Published to JetBrains Marketplace
28
- - `docs/JETBRAINS_SETUP.md` covers installation and configuration
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
+
26
+ ## [2.9.1] — 2026-04-06 · JetBrains Marketplace Publishing
27
+
28
+ ### Added
29
+ - **JetBrains Marketplace publishing** — automated publishing job in GitHub Actions workflow
30
+ - **Gradle wrapper** — gradlew, gradlew.bat for consistent JetBrains plugin builds
31
+ - **Publishing guide** — comprehensive [docs/JETBRAINS_PUBLISH.md](docs/JETBRAINS_PUBLISH.md)
32
+ - **JetBrains Marketplace badge** — added to README.md
33
+ - **One-time token setup** — documented in publishing guide
34
+
35
+ ### Details
36
+ - GitHub Actions workflow now includes `publish-jetbrains` job
37
+ - Publishes to JetBrains Marketplace alongside npm, GitHub Packages, VS Code, and Open VSX
38
+ - Requires `JETBRAINS_PUBLISH_TOKEN` secret for automated publishing
39
+ - Full publishing guide with manual instructions and troubleshooting
40
+
41
+ ---
42
+
43
+ ## [2.9.0] — 2026-04-05 · IDE Expansion: JetBrains Plugin
44
+
45
+ ### Added
46
+ - **JetBrains plugin** — native support for all JetBrains IDEs (IntelliJ IDEA, WebStorm, PyCharm, GoLand, RubyMine, etc.)
47
+ - **Plugin descriptor** — `jetbrains-plugin/src/main/resources/META-INF/plugin.xml` with 3 actions + status bar widget
48
+ - **Kotlin sources** — 5 action implementations (RegenerateAction, OpenContextFileAction, ViewRoadmapAction, HealthStatusBar, Factory)
49
+ - **Toolbar actions** — "Regenerate Context" (Ctrl+Alt+G), "Open Context File", "View Roadmap"
50
+ - **Status bar widget** — shows health grade (A-F) and time since last regeneration; updates every 60s
51
+ - **Gradle build** — `jetbrains-plugin/build.gradle.kts` with IntelliJ Platform 2024.1+ compatibility
52
+ - **Setup documentation** — [docs/JETBRAINS_SETUP.md](docs/JETBRAINS_SETUP.md) with installation guide, features, troubleshooting
53
+ - **Integration tests** — `test/integration/jetbrains.test.js` with 11 structure validation tests
54
+
55
+ ### Details
56
+ - Compatible with IntelliJ IDEA 2024.1 - 2024.3 (Community & Ultimate)
57
+ - One-click context regeneration from IDE toolbar
58
+ - Automatic status bar updates every 60 seconds
59
+ - Full Kotlin/Gradle plugin with proper plugin.xml structure
29
60
 
30
61
  ---
31
62
 
package/README.md CHANGED
@@ -26,6 +26,7 @@
26
26
  [![Changelog](https://img.shields.io/badge/changelog-CHANGELOG.md-blue)](CHANGELOG.md)
27
27
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
28
28
  [![VS Code](https://img.shields.io/badge/VS%20Code-extension-0078d4?logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=manojmallick.sigmap)
29
+ [![JetBrains](https://img.shields.io/badge/JetBrains-plugin-000000?logo=jetbrains)](https://plugins.jetbrains.com/plugin/sigmap)
29
30
  [![Open VSX](https://img.shields.io/open-vsx/v/manojmallick/sigmap?color=a251e3&label=Open%20VSX&logo=vscodium)](https://open-vsx.org/extension/manojmallick/sigmap)
30
31
 
31
32
  </div>
@@ -39,6 +40,7 @@
39
40
  | [What it does](#-what-it-does) | Token reduction table, pipeline overview |
40
41
  | [Quick start](#-quick-start) | Get running in 60 seconds |
41
42
  | [VS Code extension](#-vs-code-extension) | Status bar, stale alerts, commands |
43
+ | [JetBrains plugin](#-jetbrains-plugin) | IntelliJ IDEA, WebStorm, PyCharm support |
42
44
  | [Languages supported](#-languages-supported) | 21 languages |
43
45
  | [Context strategies](#-context-strategies) | full / per-module / hot-cold |
44
46
  | [MCP server](#-mcp-server) | 8 on-demand tools |
@@ -110,17 +112,17 @@ AI agent session starts with full context
110
112
 
111
113
  ---
112
114
 
113
- ## 🔭 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))
114
116
 
115
- ### v2.9IDE Expansion: JetBrains Plugin
117
+ ### v2.10Reporting: Charts + Advanced Metrics
116
118
 
117
119
  | Feature | Description |
118
120
  |---|---|
119
- | **JetBrains plugin** | Install SigMap natively in IntelliJ IDEA, WebStorm, PyCharm, GoLand, RubyMine |
120
- | **Toolbar actions** | "Regenerate Context", "Open Context File", "View Roadmap" buttons |
121
- | **Settings panel** | Configure srcDirs, exclude patterns, maxTokens, routing presets from IDE |
122
- | **File watcher** | Auto-regenerate context on file changes (opt-in) |
123
- | **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 |
124
126
 
125
127
  ---
126
128
  | **`get_impact` MCP tool** | 9th MCP tool — `{ file, depth? }` → impacted files + signatures |
@@ -198,6 +200,28 @@ The `vscode-extension/` directory contains a first-party VS Code extension that
198
200
 
199
201
  Activate on startup (`onStartupFinished`) — loads within 3 s, never blocks editor startup.
200
202
 
203
+ **Install:** [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=manojmallick.sigmap) | [Open VSX Registry](https://open-vsx.org/extension/manojmallick/sigmap)
204
+
205
+ ---
206
+
207
+ ## 🔧 JetBrains plugin
208
+
209
+ > Introduced in v2.9 — brings SigMap to IntelliJ IDEA, WebStorm, PyCharm, and all JetBrains IDEs.
210
+
211
+ The `jetbrains-plugin/` directory contains a Kotlin-based plugin for JetBrains IDEs with the same core features as the VS Code extension.
212
+
213
+ | Feature | Detail |
214
+ |---|---|
215
+ | **Status bar widget** | Shows health grade (`A`-`F`) + time since last regen; updates every 60 s |
216
+ | **Regenerate action** | `Tools → SigMap → Regenerate Context` or **Ctrl+Alt+G** — runs `node gen-context.js` |
217
+ | **Open context action** | `Tools → SigMap → Open Context File` — opens `.github/copilot-instructions.md` |
218
+ | **View roadmap action** | `Tools → SigMap → View Roadmap` — opens roadmap in browser |
219
+ | **One-click regen** | Click status bar widget to regenerate context instantly |
220
+
221
+ Compatible with **IntelliJ IDEA 2024.1+** (Community & Ultimate), **WebStorm**, **PyCharm**, **GoLand**, **RubyMine**, **PhpStorm**, and all other IntelliJ-based IDEs.
222
+
223
+ **Install:** [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/sigmap) | [Manual setup guide](docs/JETBRAINS_SETUP.md)
224
+
201
225
  ---
202
226
 
203
227
  ## 🌐 Languages supported
@@ -698,6 +722,6 @@ MIT © 2026 [Manoj Mallick](https://github.com/manojmallick) · Made in Amsterda
698
722
 
699
723
  If SigMap saves you time — a ⭐ on [GitHub](https://github.com/manojmallick/sigmap) helps others find it.
700
724
 
701
- **[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)**
702
726
 
703
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.0';
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.0",
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.0",
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.0",
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.0',
21
+ version: '2.10.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24