sigmap 2.9.1 → 3.0.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.
@@ -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: '3.0.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24