@holmes-lab/holmes-kit 0.10.1 → 0.12.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 +58 -1
- package/README.md +2 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.js +2 -1
- package/dist/holmes/cli/doctor.js +6 -3
- package/dist/holmes/cli/index.js +49 -0
- package/dist/holmes/cli/upgrade.d.ts +14 -0
- package/dist/holmes/cli/upgrade.js +177 -0
- package/dist/holmes/governance/approval-queue.js +3 -2
- package/dist/holmes/hooks/session-start.js +45 -0
- package/dist/holmes/hooks/stop.js +2 -1
- package/dist/holmes/mcp/elicit-approval.js +4 -3
- package/dist/holmes/mcp/handlers.d.ts +17 -1
- package/dist/holmes/mcp/handlers.js +34 -0
- package/dist/holmes/mcp/tool-schemas.js +10 -0
- package/dist/holmes/project/npx-bin.d.ts +10 -0
- package/dist/holmes/project/npx-bin.js +16 -0
- package/dist/holmes/server/cfg-view.d.ts +45 -0
- package/dist/holmes/server/cfg-view.js +57 -0
- package/dist/holmes/server/dashboard-launcher.d.ts +40 -0
- package/dist/holmes/server/dashboard-launcher.js +54 -0
- package/dist/holmes/server/dashboard.js +206 -152
- package/dist/holmes/server/rtm-matrix.d.ts +47 -0
- package/dist/holmes/server/rtm-matrix.js +83 -0
- package/dist/holmes/update/update-notice.js +4 -2
- package/dist/holmes/update/upgrade-plan.d.ts +43 -0
- package/dist/holmes/update/upgrade-plan.js +45 -0
- package/dist/holmes/update/workspaces.d.ts +35 -0
- package/dist/holmes/update/workspaces.js +109 -0
- package/package.json +1 -1
|
@@ -50,6 +50,11 @@ const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
|
50
50
|
const language_parser_1 = require("../cpg/language-parser");
|
|
51
51
|
const findings_1 = require("../review/findings");
|
|
52
52
|
const ast_mutation_1 = require("../cpg/ast-mutation");
|
|
53
|
+
const rtm_matrix_1 = require("./rtm-matrix");
|
|
54
|
+
const ast_store_1 = require("../cpg/foundation/ast-store");
|
|
55
|
+
const cfg_1 = require("../cpg/foundation/cfg");
|
|
56
|
+
const cdg_1 = require("../cpg/foundation/cdg");
|
|
57
|
+
const cfg_view_1 = require("./cfg-view");
|
|
53
58
|
/**
|
|
54
59
|
* Start a lightweight standalone Node.js HTTP server for interactive dashboard & RTM visualization.
|
|
55
60
|
*
|
|
@@ -183,6 +188,8 @@ async function startDashboardServer(options) {
|
|
|
183
188
|
const findingsLedger = new findings_1.FindingsLedger(findingsPath);
|
|
184
189
|
const allFindings = findingsScanned ? findingsLedger.list() : [];
|
|
185
190
|
const pipelines = buildPipelineRows(specs, files, allFindings, { findingsScanned });
|
|
191
|
+
// @implements A-SPEC-545.2 — seriated REQ × stage coverage matrix (additive; pipelines unchanged).
|
|
192
|
+
const matrix = (0, rtm_matrix_1.heatmapMatrix)(pipelines);
|
|
186
193
|
const body = JSON.stringify({
|
|
187
194
|
ok: true,
|
|
188
195
|
pipelineCount: pipelines.length,
|
|
@@ -190,6 +197,7 @@ async function startDashboardServer(options) {
|
|
|
190
197
|
findingsScanned,
|
|
191
198
|
mutationScoreMeasured: false,
|
|
192
199
|
pipelines,
|
|
200
|
+
matrix,
|
|
193
201
|
});
|
|
194
202
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
195
203
|
res.end(body);
|
|
@@ -202,6 +210,78 @@ async function startDashboardServer(options) {
|
|
|
202
210
|
}
|
|
203
211
|
return;
|
|
204
212
|
}
|
|
213
|
+
// @implements A-SPEC-545.4 — one function's CFG + PDG (data/control deps), from the same cfgOf/pdgOf
|
|
214
|
+
// the taint lane uses. Honest envelope: a non-CFG language is named, never faked as an empty graph.
|
|
215
|
+
if (parsedUrl === '/api/cfg') {
|
|
216
|
+
try {
|
|
217
|
+
const q = new URL(req.url || '/', 'http://localhost').searchParams;
|
|
218
|
+
const file = q.get('file');
|
|
219
|
+
const symbol = q.get('symbol');
|
|
220
|
+
if (!file || !symbol) {
|
|
221
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
222
|
+
res.end(JSON.stringify({ ok: false, reason: 'file and symbol query params are required' }));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const files = scanner.scan(root);
|
|
226
|
+
const scanned = files.find((f) => f.sourcePath === file);
|
|
227
|
+
const sym = scanned?.symbols.find((s) => s.name === symbol);
|
|
228
|
+
const lang = (0, ast_store_1.languageFor)(file)?.lang ?? null;
|
|
229
|
+
if (!scanned || !sym) {
|
|
230
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
231
|
+
res.end(JSON.stringify({ ok: false, reason: `symbol ${symbol} not found in ${file}`, file, symbol, lang }));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!lang || !cfg_1.CFG_LANGUAGES.has(lang)) {
|
|
235
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({ ok: true, file, symbol, lang, unsupported: lang ?? 'unknown' }));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
|
240
|
+
const ast = await (0, ast_store_1.parseAst)(source, file);
|
|
241
|
+
if (!ast) {
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify({ ok: false, reason: `could not parse ${file}`, file, symbol, lang }));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Pick the function whose declaration line is closest to the scanned symbol's start line
|
|
247
|
+
// (absorbs any 0/1-based drift between the scanner and the AST byte→line mapping).
|
|
248
|
+
const fns = (0, cfg_1.functionsIn)(ast);
|
|
249
|
+
let best = null;
|
|
250
|
+
let bestDist = Infinity;
|
|
251
|
+
for (const fn of fns) {
|
|
252
|
+
const node = ast.nodes[fn.nodeIndex];
|
|
253
|
+
if (!node)
|
|
254
|
+
continue;
|
|
255
|
+
const dist = Math.abs((0, cfg_view_1.lineAtByte)(source, node.start) - sym.startLine);
|
|
256
|
+
if (dist < bestDist) {
|
|
257
|
+
bestDist = dist;
|
|
258
|
+
best = fn;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!best) {
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({ ok: false, reason: `no function body for ${symbol} in ${file}`, file, symbol, lang }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const cfg = (0, cfg_1.cfgOf)(ast, best, source);
|
|
267
|
+
const pdg = (0, cdg_1.pdgOf)(ast, cfg, best, source);
|
|
268
|
+
const view = (0, cfg_view_1.buildCfgView)(cfg, pdg, ast, source);
|
|
269
|
+
if ('unsupported' in view) {
|
|
270
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
271
|
+
res.end(JSON.stringify({ ok: true, file, symbol, lang, unsupported: view.unsupported }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify({ ok: true, file, symbol, lang, cfg: view }));
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (!res.headersSent) {
|
|
279
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
280
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
205
285
|
if (parsedUrl === '/api/cpg') {
|
|
206
286
|
try {
|
|
207
287
|
const files = scanner.scan(root);
|
|
@@ -388,7 +468,57 @@ function renderDashboardHtml() {
|
|
|
388
468
|
<title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
|
|
389
469
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
390
470
|
<style>
|
|
391
|
-
|
|
471
|
+
/* @implements A-SPEC-545.2 — design tokens: sequential coverage ramp, status, surfaces, 2 fonts.
|
|
472
|
+
Dark-first; light palette under prefers-color-scheme. The green ramp reads "traced". */
|
|
473
|
+
:root {
|
|
474
|
+
--surface-0: #0d1417; --surface-1: #131e22; --surface-2: #1b2b30; --border: #26383d;
|
|
475
|
+
--ink-0: #eaf2f0; --ink-1: #9fb2b0; --ink-2: #6b807e;
|
|
476
|
+
--ramp-0: #17211d; --ramp-1: #1f3a2c; --ramp-2: #26543a; --ramp-3: #2c7449; --ramp-4: #33a35f; --ramp-5: #46d97e;
|
|
477
|
+
--ramp-ink-lo: #8ba39c; --ramp-ink-hi: #06120c;
|
|
478
|
+
--ok: #46d97e; --warn: #e0a83c; --bad: #e5595c; --accent: #46b0d9;
|
|
479
|
+
--font-ui: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
480
|
+
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
481
|
+
}
|
|
482
|
+
@media (prefers-color-scheme: light) {
|
|
483
|
+
:root {
|
|
484
|
+
--surface-0: #f4f7f6; --surface-1: #ffffff; --surface-2: #eaf0ee; --border: #d3ddda;
|
|
485
|
+
--ink-0: #12201c; --ink-1: #4a5c58; --ink-2: #7b8d89;
|
|
486
|
+
--ramp-0: #e7efe9; --ramp-1: #c3e0cd; --ramp-2: #8fc9a5; --ramp-3: #54ac77; --ramp-4: #2f8a54; --ramp-5: #166b3c;
|
|
487
|
+
--ramp-ink-lo: #5b6f69; --ramp-ink-hi: #f4faf6;
|
|
488
|
+
--ok: #166b3c; --warn: #b7791f; --bad: #c53539; --accent: #1f7fa6;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
body { font-family: var(--font-ui); background-color: var(--surface-0); color: var(--ink-0); margin: 0; padding: 24px; user-select: none; }
|
|
492
|
+
|
|
493
|
+
/* Seriated REQ × stage coverage matrix (2D grid) */
|
|
494
|
+
.hm-toolbar { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; color: var(--ink-1); font-size: 12px; }
|
|
495
|
+
.hm-legend { display: inline-flex; align-items: center; gap: 6px; }
|
|
496
|
+
.hm-legend-swatch { width: 16px; height: 12px; border-radius: 2px; display: inline-block; border: 1px solid var(--border); }
|
|
497
|
+
.hm-scroll { max-height: 74vh; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-1); }
|
|
498
|
+
table.hm-grid { border-collapse: separate; border-spacing: 0; width: 100%; font-family: var(--font-mono); }
|
|
499
|
+
table.hm-grid th, table.hm-grid td { border-bottom: 1px solid var(--surface-0); border-right: 1px solid var(--surface-0); }
|
|
500
|
+
table.hm-grid thead th { position: sticky; top: 0; z-index: 3; background: var(--surface-2); color: var(--ink-1);
|
|
501
|
+
font-family: var(--font-ui); font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
|
|
502
|
+
padding: 10px 8px; text-align: center; white-space: nowrap; }
|
|
503
|
+
table.hm-grid thead th.hm-corner { left: 0; z-index: 4; text-align: left; }
|
|
504
|
+
table.hm-grid td.hm-rowlabel, table.hm-grid th.hm-corner { position: sticky; left: 0; z-index: 2; background: var(--surface-1);
|
|
505
|
+
min-width: 260px; max-width: 320px; padding: 7px 12px; border-right: 1px solid var(--border); }
|
|
506
|
+
.hm-req-id { font-family: var(--font-mono); font-weight: 700; color: var(--ink-0); font-size: 12px; }
|
|
507
|
+
.hm-req-title { font-family: var(--font-ui); color: var(--ink-2); font-size: 11px; margin-left: 8px;
|
|
508
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
509
|
+
.hm-rowlabel-flex { display: flex; align-items: baseline; overflow: hidden; }
|
|
510
|
+
table.hm-grid td.hm-cell { text-align: center; font-variant-numeric: tabular-nums; font-size: 11px; padding: 7px 6px;
|
|
511
|
+
color: var(--ramp-ink-hi); cursor: default; min-width: 62px; }
|
|
512
|
+
table.hm-grid tbody tr:hover td.hm-cell { outline: 1px solid var(--accent); outline-offset: -2px; }
|
|
513
|
+
.hm-b0 { background: var(--ramp-0); color: var(--ramp-ink-lo); }
|
|
514
|
+
.hm-b1 { background: var(--ramp-1); color: var(--ramp-ink-lo); }
|
|
515
|
+
.hm-b2 { background: var(--ramp-2); color: var(--ramp-ink-hi); }
|
|
516
|
+
.hm-b3 { background: var(--ramp-3); color: var(--ramp-ink-hi); }
|
|
517
|
+
.hm-b4 { background: var(--ramp-4); color: var(--ramp-ink-hi); }
|
|
518
|
+
.hm-b5 { background: var(--ramp-5); color: var(--ramp-ink-hi); }
|
|
519
|
+
.hm-summary-pill { font-family: var(--font-ui); font-weight: 700; font-size: 11px; padding: 2px 8px; border-radius: 999px; }
|
|
520
|
+
.hm-pill-ok { background: color-mix(in srgb, var(--ok) 22%, transparent); color: var(--ok); border: 1px solid var(--ok); }
|
|
521
|
+
.hm-pill-warn { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); border: 1px solid var(--warn); }
|
|
392
522
|
.header { margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.12); padding-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
|
|
393
523
|
.nav-tabs { display: flex; gap: 12px; margin-bottom: 20px; }
|
|
394
524
|
.tab-btn { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }
|
|
@@ -541,10 +671,10 @@ function renderDashboardHtml() {
|
|
|
541
671
|
</div>
|
|
542
672
|
|
|
543
673
|
<div class="nav-tabs">
|
|
544
|
-
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')"
|
|
545
|
-
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')"
|
|
546
|
-
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')"
|
|
547
|
-
<button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')"
|
|
674
|
+
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">Quantitative REQ RTM</button>
|
|
675
|
+
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">Coverage Matrix</button>
|
|
676
|
+
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">Pipeline Canvas</button>
|
|
677
|
+
<button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">Unmapped Specs</button>
|
|
548
678
|
</div>
|
|
549
679
|
|
|
550
680
|
<div id="rtmTab">
|
|
@@ -570,30 +700,28 @@ function renderDashboardHtml() {
|
|
|
570
700
|
</div>
|
|
571
701
|
|
|
572
702
|
<div id="heatmapTab" style="display: none;">
|
|
573
|
-
<div class="
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
</
|
|
593
|
-
</div>
|
|
594
|
-
<div class="heatmap-wrapper">
|
|
595
|
-
<div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
|
|
703
|
+
<div class="hm-toolbar">
|
|
704
|
+
<span style="font-family: var(--font-ui); font-weight: 700; color: var(--ink-0); font-size: 13px;">Requirement × Pipeline-Stage Coverage</span>
|
|
705
|
+
<span style="color: var(--ink-2);">rows seriated by completeness · cell = share of a REQ's chains reaching that stage</span>
|
|
706
|
+
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
|
707
|
+
<label for="pipelineFilterSelect" style="color: var(--ink-1);">Rows</label>
|
|
708
|
+
<select id="pipelineFilterSelect" class="select-box" onchange="changeHeatmapLens()">
|
|
709
|
+
<option value="all" selected>All requirements</option>
|
|
710
|
+
<option value="uncovered">Incomplete only</option>
|
|
711
|
+
</select>
|
|
712
|
+
</span>
|
|
713
|
+
<span class="hm-legend">
|
|
714
|
+
<span style="color: var(--ink-2);">0%</span>
|
|
715
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-0);"></span>
|
|
716
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-1);"></span>
|
|
717
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-2);"></span>
|
|
718
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-3);"></span>
|
|
719
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-4);"></span>
|
|
720
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-5);"></span>
|
|
721
|
+
<span style="color: var(--ink-2);">100%</span>
|
|
722
|
+
</span>
|
|
596
723
|
</div>
|
|
724
|
+
<div id="heatmapContainer">Loading coverage matrix…</div>
|
|
597
725
|
</div>
|
|
598
726
|
|
|
599
727
|
<div id="graphTab" style="display: none;">
|
|
@@ -844,144 +972,70 @@ function renderDashboardHtml() {
|
|
|
844
972
|
}
|
|
845
973
|
}
|
|
846
974
|
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (!label) {
|
|
852
|
-
return '<td class="' + cls + ' stage-missing" title="No artifact for this stage">' + arrow + '— missing</td>';
|
|
853
|
-
}
|
|
854
|
-
const onclick = jumpId ? ' onclick="focusSpecInGraph(\\'' + escapeHtml(String(jumpId)) + '\\')"' : '';
|
|
855
|
-
return '<td class="' + cls + '"' + onclick + '>' + arrow + icon + ' ' + escapeHtml(String(label)) + '</td>';
|
|
856
|
-
}
|
|
857
|
-
// REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Renderer
|
|
975
|
+
// @implements A-SPEC-545.2 — seriated REQ × stage coverage matrix (2D grid), painted from the
|
|
976
|
+
// server-built data.matrix. Rows are ordered by matrix.order (completeness), each cell coloured
|
|
977
|
+
// by its ramp bucket with the coverage percent shown inline (tabular-nums), so magnitude never
|
|
978
|
+
// relies on colour alone.
|
|
858
979
|
function render6StagePipelineMatrix(data) {
|
|
859
980
|
const container = document.getElementById('heatmapContainer');
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const statusFilter =
|
|
981
|
+
const matrix = data.matrix || { reqs: [], stages: [], values: [], order: [], buckets: [] };
|
|
982
|
+
const filterEl = document.getElementById('pipelineFilterSelect');
|
|
983
|
+
const statusFilter = filterEl ? (filterEl.value || 'all') : 'all';
|
|
863
984
|
|
|
864
|
-
if (
|
|
865
|
-
container.innerHTML = '<div style="color
|
|
985
|
+
if (!matrix.reqs || matrix.reqs.length === 0) {
|
|
986
|
+
container.innerHTML = '<div style="color: var(--ink-1); font-size: 14px; padding: 16px;">No pipeline trace chains found.</div>';
|
|
866
987
|
return;
|
|
867
988
|
}
|
|
868
989
|
|
|
869
|
-
|
|
870
|
-
let
|
|
871
|
-
if (statusFilter === 'uncovered')
|
|
872
|
-
pipelines = rawPipelines.filter(p => p.status !== 'COVERED');
|
|
873
|
-
} else if (statusFilter === 'findings') {
|
|
874
|
-
pipelines = rawPipelines.filter(p => p.findingsCount > 0);
|
|
875
|
-
}
|
|
990
|
+
const isRowComplete = (i) => (matrix.values[i] || []).every(v => v >= 1);
|
|
991
|
+
let rowIdx = (matrix.order && matrix.order.length ? matrix.order : matrix.reqs.map((_, i) => i)).slice();
|
|
992
|
+
if (statusFilter === 'uncovered') rowIdx = rowIdx.filter(i => !isRowComplete(i));
|
|
876
993
|
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
pipelines.forEach(p => {
|
|
880
|
-
if (!groupedMap.has(p.reqId)) {
|
|
881
|
-
groupedMap.set(p.reqId, { reqId: p.reqId, reqTitle: p.reqTitle, items: [] });
|
|
882
|
-
}
|
|
883
|
-
groupedMap.get(p.reqId).items.push(p);
|
|
884
|
-
});
|
|
994
|
+
const fullyTraced = matrix.reqs.filter((_, i) => isRowComplete(i)).length;
|
|
995
|
+
const summaryCls = fullyTraced === matrix.reqs.length ? 'hm-pill-ok' : 'hm-pill-warn';
|
|
885
996
|
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
container.innerHTML = unscanned
|
|
889
|
-
? '<div style="color:#fbbf24; font-size:14px; padding:16px; border:1px dashed #64748b; border-radius:8px;">⚪ No findings ledger exists (<code>.ax/ledger/findings.jsonl</code>). This is <strong>not</strong> a clean result — no audit has been run.</div>'
|
|
890
|
-
: '<div style="color:#94a3b8; font-size:14px; padding:16px;">No pipelines match the selected filter.</div>';
|
|
997
|
+
if (rowIdx.length === 0) {
|
|
998
|
+
container.innerHTML = '<div style="color: var(--ink-1); font-size: 14px; padding: 16px;">Every requirement is fully traced — no incomplete rows to show.</div>';
|
|
891
999
|
return;
|
|
892
1000
|
}
|
|
893
1001
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
<span class="req-id-badge">\${escapeHtml(reqId)}</span>
|
|
915
|
-
<span class="req-title">\${escapeHtml(group.reqTitle || 'Business Requirement Pipeline')}</span>
|
|
916
|
-
\${reqStatusBadge}
|
|
917
|
-
</div>
|
|
918
|
-
<div style="display: flex; align-items: center; gap: 12px;">
|
|
919
|
-
<span style="font-size: 12px; color: #94a3b8; font-weight: bold;">\${totalItems} End-to-End Trace Chain\${totalItems > 1 ? 's' : ''}</span>
|
|
920
|
-
<button class="expand-btn">▶ Expand Pipeline Matrix</button>
|
|
1002
|
+
const pct = (v) => Math.round((v || 0) * 100);
|
|
1003
|
+
const headCells = matrix.stages.map(s => \`<th>\${escapeHtml(s)}</th>\`).join('');
|
|
1004
|
+
|
|
1005
|
+
let rows = '';
|
|
1006
|
+
rowIdx.forEach(i => {
|
|
1007
|
+
const req = matrix.reqs[i];
|
|
1008
|
+
const vals = matrix.values[i] || [];
|
|
1009
|
+
const bkts = matrix.buckets[i] || [];
|
|
1010
|
+
const cells = matrix.stages.map((stage, s) => {
|
|
1011
|
+
const v = vals[s] || 0;
|
|
1012
|
+
const b = bkts[s] || 0;
|
|
1013
|
+
const title = escapeHtml(req.id) + ' · ' + escapeHtml(stage) + ': ' + pct(v) + '% of chains reached';
|
|
1014
|
+
return \`<td class="hm-cell hm-b\${b}" title="\${title}">\${pct(v)}</td>\`;
|
|
1015
|
+
}).join('');
|
|
1016
|
+
rows += \`
|
|
1017
|
+
<tr>
|
|
1018
|
+
<td class="hm-rowlabel" onclick="focusSpecInGraph('\${escapeHtml(req.id)}')" title="\${escapeHtml(req.title || '')}">
|
|
1019
|
+
<div class="hm-rowlabel-flex">
|
|
1020
|
+
<span class="hm-req-id">\${escapeHtml(req.id)}</span>
|
|
1021
|
+
<span class="hm-req-title">\${escapeHtml(req.title || '')}</span>
|
|
921
1022
|
</div>
|
|
922
|
-
</
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
<table class="pipeline-table">
|
|
926
|
-
<thead>
|
|
927
|
-
<tr>
|
|
928
|
-
<th>Stage 1: REQ Business</th>
|
|
929
|
-
<th>Stage 2: H-SPEC Functional</th>
|
|
930
|
-
<th>Stage 3: A-SPEC Architecture</th>
|
|
931
|
-
<th>Stage 4: T-SPEC Verification</th>
|
|
932
|
-
<th>Stage 5: Source Code File</th>
|
|
933
|
-
<th>Stage 6: AST / CPG Symbol</th>
|
|
934
|
-
<th>\${extraHeader}</th>
|
|
935
|
-
</tr>
|
|
936
|
-
</thead>
|
|
937
|
-
<tbody>
|
|
938
|
-
\`;
|
|
939
|
-
|
|
940
|
-
items.forEach(p => {
|
|
941
|
-
const missing = p.missingStages || [];
|
|
942
|
-
let statusBadge = missing.length === 0
|
|
943
|
-
? '<span class="neighbor-rel badge-covered">COVERED (6/6)</span>'
|
|
944
|
-
: \`<span class="neighbor-rel badge-uncovered" title="Missing: \${escapeHtml(missing.join(', '))}">UNCOVERED (\${p.stagesComplete}/6)</span>\`;
|
|
945
|
-
|
|
946
|
-
if (lens === 'lens2') {
|
|
947
|
-
if (!p.findingsScanned) {
|
|
948
|
-
statusBadge = '<span class="neighbor-rel badge-unmeasured" title="No .ax/ledger/findings.jsonl — this chain has never been audited">⚪ Not scanned</span>';
|
|
949
|
-
} else if (p.criticalCount > 0) {
|
|
950
|
-
statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
|
|
951
|
-
} else if (p.findingsCount > 0) {
|
|
952
|
-
statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
|
|
953
|
-
} else {
|
|
954
|
-
statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
|
|
955
|
-
}
|
|
956
|
-
} else if (lens === 'lens3') {
|
|
957
|
-
// A score requires running the suite once per mutant; this endpoint only generates them
|
|
958
|
-
// statically, so it reports the mutant count and says the score is unmeasured.
|
|
959
|
-
const mutantCount = p.mutantCount || 0;
|
|
960
|
-
statusBadge = \`<span class="neighbor-rel badge-unmeasured" title="Static generation only — run mutation testing to obtain a score">🧪 \${mutantCount} Mutant\${mutantCount === 1 ? '' : 's'} · Score not measured</span>\`;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
html += \`
|
|
964
|
-
<tr class="pipeline-row">
|
|
965
|
-
\${stageCell('stage-req', '🟣', p.reqId, p.reqId, false)}
|
|
966
|
-
\${stageCell('stage-hspec', '🟣', p.hspecId, p.hspecId, true)}
|
|
967
|
-
\${stageCell('stage-aspec', '🔵', p.aspecId, p.aspecId, true)}
|
|
968
|
-
\${stageCell('stage-tspec', '🔷', p.tspecId, p.tspecId, true)}
|
|
969
|
-
\${stageCell('stage-file', '📄', p.fileId ? (p.fileId.split('/').pop() || p.fileId) : null, p.fileId, true)}
|
|
970
|
-
\${stageCell('stage-symbol', '⚡', p.symbolId, p.fileId && p.symbolId ? p.fileId + '#' + p.symbolId : null, true)}
|
|
971
|
-
<td>\${statusBadge}</td>
|
|
972
|
-
</tr>
|
|
973
|
-
\`;
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
html += \`
|
|
977
|
-
</tbody>
|
|
978
|
-
</table>
|
|
979
|
-
</div>
|
|
980
|
-
</div>
|
|
981
|
-
\`;
|
|
1023
|
+
</td>
|
|
1024
|
+
\${cells}
|
|
1025
|
+
</tr>\`;
|
|
982
1026
|
});
|
|
983
1027
|
|
|
984
|
-
container.innerHTML =
|
|
1028
|
+
container.innerHTML = \`
|
|
1029
|
+
<div style="margin-bottom: 10px;">
|
|
1030
|
+
<span class="hm-summary-pill \${summaryCls}">\${fullyTraced} / \${matrix.reqs.length} requirements fully traced</span>
|
|
1031
|
+
<span style="color: var(--ink-2); font-size: 12px; margin-left: 10px;">showing \${rowIdx.length} row\${rowIdx.length === 1 ? '' : 's'}</span>
|
|
1032
|
+
</div>
|
|
1033
|
+
<div class="hm-scroll">
|
|
1034
|
+
<table class="hm-grid">
|
|
1035
|
+
<thead><tr><th class="hm-corner">Requirement</th>\${headCells}</tr></thead>
|
|
1036
|
+
<tbody>\${rows}</tbody>
|
|
1037
|
+
</table>
|
|
1038
|
+
</div>\`;
|
|
985
1039
|
}
|
|
986
1040
|
|
|
987
1041
|
// World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** The six pipeline stages, in traversal order — the columns of the matrix. */
|
|
2
|
+
export declare const STAGES: readonly ["REQ", "H-SPEC", "A-SPEC", "T-SPEC", "File", "AST Symbol"];
|
|
3
|
+
/** One pipeline chain row from /api/rtm/heatmap (only the fields the matrix reads). */
|
|
4
|
+
export interface PipelineRow {
|
|
5
|
+
reqId: string;
|
|
6
|
+
reqTitle?: string;
|
|
7
|
+
stagesComplete: number;
|
|
8
|
+
}
|
|
9
|
+
export interface StageMatrix {
|
|
10
|
+
reqs: {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
}[];
|
|
14
|
+
stages: string[];
|
|
15
|
+
/** values[r][s] ∈ [0,1] = fraction of REQ r's chains that reached stage s. */
|
|
16
|
+
values: number[][];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @implements A-SPEC-545.1
|
|
20
|
+
* Pure: turn per-chain pipeline rows into a REQ × stage coverage matrix. Rows are REQs (first-seen
|
|
21
|
+
* order), columns the six stages; each cell is the fraction of that REQ's chains that REACHED the
|
|
22
|
+
* stage (stagesComplete >= s+1). This is the overview the 2D heatmap paints — magnitude per cell.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildStageMatrix(pipelines: PipelineRow[]): StageMatrix;
|
|
25
|
+
/**
|
|
26
|
+
* @implements A-SPEC-545.1
|
|
27
|
+
* Pure & deterministic: a row permutation that clusters similar, high-coverage rows together
|
|
28
|
+
* (Behrisch-style seriation). Key: descending row-mean, then the profile vector descending, then the
|
|
29
|
+
* original index (stable). Same input ⇒ same output.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* @implements A-SPEC-545.2
|
|
33
|
+
* Pure: map a coverage fraction v∈[0,1] to a sequential-ramp bucket index 0..5. Monotonic
|
|
34
|
+
* non-decreasing, clamped ([0,1]), NaN→0. The dashboard paints cell background = var(--ramp-<bucket>).
|
|
35
|
+
*/
|
|
36
|
+
export declare function coverageBucket(v: number): number;
|
|
37
|
+
export interface HeatmapMatrix extends StageMatrix {
|
|
38
|
+
order: number[];
|
|
39
|
+
buckets: number[][];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @implements A-SPEC-545.2
|
|
43
|
+
* Pure server-side payload builder: buildStageMatrix + seriateRows + coverageBucket in one object,
|
|
44
|
+
* so the client render is a thin paint over data.matrix.
|
|
45
|
+
*/
|
|
46
|
+
export declare function heatmapMatrix(pipelines: PipelineRow[]): HeatmapMatrix;
|
|
47
|
+
export declare function seriateRows(values: number[][]): number[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-545.1
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.STAGES = void 0;
|
|
5
|
+
exports.buildStageMatrix = buildStageMatrix;
|
|
6
|
+
exports.coverageBucket = coverageBucket;
|
|
7
|
+
exports.heatmapMatrix = heatmapMatrix;
|
|
8
|
+
exports.seriateRows = seriateRows;
|
|
9
|
+
/** The six pipeline stages, in traversal order — the columns of the matrix. */
|
|
10
|
+
exports.STAGES = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
|
|
11
|
+
/**
|
|
12
|
+
* @implements A-SPEC-545.1
|
|
13
|
+
* Pure: turn per-chain pipeline rows into a REQ × stage coverage matrix. Rows are REQs (first-seen
|
|
14
|
+
* order), columns the six stages; each cell is the fraction of that REQ's chains that REACHED the
|
|
15
|
+
* stage (stagesComplete >= s+1). This is the overview the 2D heatmap paints — magnitude per cell.
|
|
16
|
+
*/
|
|
17
|
+
function buildStageMatrix(pipelines) {
|
|
18
|
+
const order = [];
|
|
19
|
+
const groups = new Map();
|
|
20
|
+
for (const p of pipelines) {
|
|
21
|
+
let g = groups.get(p.reqId);
|
|
22
|
+
if (!g) {
|
|
23
|
+
g = { title: p.reqTitle ?? '', chains: [] };
|
|
24
|
+
groups.set(p.reqId, g);
|
|
25
|
+
order.push(p.reqId);
|
|
26
|
+
}
|
|
27
|
+
g.chains.push(p.stagesComplete);
|
|
28
|
+
}
|
|
29
|
+
const reqs = order.map((id) => ({ id, title: groups.get(id).title }));
|
|
30
|
+
const values = order.map((id) => {
|
|
31
|
+
const chains = groups.get(id).chains;
|
|
32
|
+
return exports.STAGES.map((_, s) => {
|
|
33
|
+
const reached = chains.filter((c) => c >= s + 1).length;
|
|
34
|
+
return Number((reached / chains.length).toFixed(3));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return { reqs, stages: [...exports.STAGES], values };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @implements A-SPEC-545.1
|
|
41
|
+
* Pure & deterministic: a row permutation that clusters similar, high-coverage rows together
|
|
42
|
+
* (Behrisch-style seriation). Key: descending row-mean, then the profile vector descending, then the
|
|
43
|
+
* original index (stable). Same input ⇒ same output.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @implements A-SPEC-545.2
|
|
47
|
+
* Pure: map a coverage fraction v∈[0,1] to a sequential-ramp bucket index 0..5. Monotonic
|
|
48
|
+
* non-decreasing, clamped ([0,1]), NaN→0. The dashboard paints cell background = var(--ramp-<bucket>).
|
|
49
|
+
*/
|
|
50
|
+
function coverageBucket(v) {
|
|
51
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
52
|
+
return 0; // NaN / -Inf / ≤0 → lowest
|
|
53
|
+
if (v >= 1)
|
|
54
|
+
return 5; // ≥1 → highest (upper clamp)
|
|
55
|
+
return Math.min(5, Math.floor(v * 6));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @implements A-SPEC-545.2
|
|
59
|
+
* Pure server-side payload builder: buildStageMatrix + seriateRows + coverageBucket in one object,
|
|
60
|
+
* so the client render is a thin paint over data.matrix.
|
|
61
|
+
*/
|
|
62
|
+
function heatmapMatrix(pipelines) {
|
|
63
|
+
const { reqs, stages, values } = buildStageMatrix(pipelines);
|
|
64
|
+
const order = seriateRows(values);
|
|
65
|
+
const buckets = values.map((row) => row.map(coverageBucket));
|
|
66
|
+
return { reqs, stages, values, order, buckets };
|
|
67
|
+
}
|
|
68
|
+
function seriateRows(values) {
|
|
69
|
+
const mean = (row) => row.length ? row.reduce((a, b) => a + b, 0) / row.length : 0;
|
|
70
|
+
return values
|
|
71
|
+
.map((row, i) => ({ i, m: mean(row), row }))
|
|
72
|
+
.sort((a, b) => {
|
|
73
|
+
if (b.m !== a.m)
|
|
74
|
+
return b.m - a.m; // higher coverage first
|
|
75
|
+
for (let k = 0; k < Math.max(a.row.length, b.row.length); k++) {
|
|
76
|
+
const d = (b.row[k] ?? 0) - (a.row[k] ?? 0); // then profile descending
|
|
77
|
+
if (d !== 0)
|
|
78
|
+
return d;
|
|
79
|
+
}
|
|
80
|
+
return a.i - b.i; // stable on ties
|
|
81
|
+
})
|
|
82
|
+
.map((x) => x.i);
|
|
83
|
+
}
|
|
@@ -49,6 +49,7 @@ exports.cacheIsStale = cacheIsStale;
|
|
|
49
49
|
// major.minor.patch — a full semver library would carry prerelease/build code this never runs), and
|
|
50
50
|
// the registry query (A-SPEC-531.2) uses Node's built-in https.
|
|
51
51
|
const path = __importStar(require("node:path"));
|
|
52
|
+
const npx_bin_1 = require("../project/npx-bin");
|
|
52
53
|
exports.NPM_URL = 'https://www.npmjs.com/package/@holmes-lab/holmes-kit';
|
|
53
54
|
const DEFAULT_TTL_MS = 24 * 3600_000;
|
|
54
55
|
/** Parse `major.minor.patch` to a 3-tuple; a non-numeric field becomes 0 (never throws). */
|
|
@@ -97,10 +98,11 @@ function readCache(home, readFile) {
|
|
|
97
98
|
function installModeGuide(mode, latest, current) {
|
|
98
99
|
const head = `[Holmes-Kit] Update available: ${latest} (current ${current}).`;
|
|
99
100
|
switch (mode) {
|
|
101
|
+
// @implements A-SPEC-544.1 — one command installs the latest AND re-pins every wired workspace,
|
|
102
|
+
// so the nudge no longer spells out `npm i` + `init --force`. win32 → npx.cmd (REQ-542).
|
|
100
103
|
case 'global-npx':
|
|
101
|
-
return `${head} Run: npm i -g @holmes-lab/holmes-kit@latest, then holmes-kit init --force (re-pins wiring; requires HOLMES_APPROVAL).`;
|
|
102
104
|
case 'local-dep':
|
|
103
|
-
return `${head} Run:
|
|
105
|
+
return `${head} Run: ${(0, npx_bin_1.npxBin)()} holmes-kit upgrade`;
|
|
104
106
|
case 'source':
|
|
105
107
|
return null;
|
|
106
108
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { InstallMode } from './update-notice';
|
|
2
|
+
import { Registry, WorkspaceEntry } from './workspaces';
|
|
3
|
+
/** What `holmes-kit upgrade` would do — pure facts; wording and refusals belong to the CLI. */
|
|
4
|
+
export type UpgradePlan = {
|
|
5
|
+
kind: 'noop';
|
|
6
|
+
reason: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'run';
|
|
9
|
+
current: string;
|
|
10
|
+
latest: string;
|
|
11
|
+
installMode: InstallMode;
|
|
12
|
+
repins: WorkspaceEntry[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @implements A-SPEC-543.2
|
|
16
|
+
* Pure: decide whether an upgrade runs and what it touches. Unknown latest is a noop (silence is not
|
|
17
|
+
* an install), and so is an equal or OLDER latest — upgrade never plans a downgrade. A 'run' plan
|
|
18
|
+
* carries the registry's workspaces verbatim as the re-pin list; an empty list is still a valid run
|
|
19
|
+
* (install-only, first machine).
|
|
20
|
+
*/
|
|
21
|
+
export declare function planUpgrade(a: {
|
|
22
|
+
current: string;
|
|
23
|
+
latest: string | null;
|
|
24
|
+
registry: Registry;
|
|
25
|
+
installMode: InstallMode;
|
|
26
|
+
}): UpgradePlan;
|
|
27
|
+
/** The env switch that turns SessionStart's forward self-heal re-pin OFF. */
|
|
28
|
+
export declare const NO_AUTO_REPIN_ENV = "HOLMES_NO_AUTO_REPIN";
|
|
29
|
+
/**
|
|
30
|
+
* @implements A-SPEC-543.3
|
|
31
|
+
* Pure: should a session auto re-pin this workspace's derived artifacts? Only when the pin is a real
|
|
32
|
+
* version, the installed package is strictly NEWER (forward-only — never a downgrade or a match), and
|
|
33
|
+
* the operator has not opted out. The install was the user's explicit choice; this completes it at
|
|
34
|
+
* the workspace level. A decision always says why.
|
|
35
|
+
*/
|
|
36
|
+
export declare function repinDecision(a: {
|
|
37
|
+
pinned: string | null;
|
|
38
|
+
installed: string;
|
|
39
|
+
env: NodeJS.ProcessEnv;
|
|
40
|
+
}): {
|
|
41
|
+
repin: boolean;
|
|
42
|
+
reason: string;
|
|
43
|
+
};
|