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 +17 -0
- package/README.md +8 -8
- package/gen-context.js +490 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/README.md +2 -2
- package/packages/core/package.json +1 -1
- package/src/format/dashboard.js +400 -0
- package/src/health/scorer.js +30 -1
- package/src/mcp/server.js +1 -1
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.
|
|
115
|
+
## 🔭 What's next — v2.10 (in progress · [#25](https://github.com/manojmallick/sigmap/issues/25))
|
|
116
116
|
|
|
117
|
-
### v2.
|
|
117
|
+
### v2.10 — Reporting: Charts + Advanced Metrics
|
|
118
118
|
|
|
119
119
|
| Feature | Description |
|
|
120
120
|
|---|---|
|
|
121
|
-
| **
|
|
122
|
-
| **
|
|
123
|
-
| **
|
|
124
|
-
| **
|
|
125
|
-
| **
|
|
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](
|
|
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
|
-
|
|
1986
|
-
strategy =
|
|
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
|
-
|
|
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 {
|
|
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: '
|
|
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.
|
|
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
|
-
|
|
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
package/packages/core/README.md
CHANGED
|
@@ -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.
|
|
131
|
+
## What's next — v2.10
|
|
132
132
|
|
|
133
|
-
v2.
|
|
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
|
|
|
@@ -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 };
|
package/src/health/scorer.js
CHANGED
|
@@ -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 {
|
|
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