@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.
@@ -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
- body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
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')">📊 Quantitative REQ RTM Matrix Grid</button>
545
- <button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 REQ-Grouped Pipeline Heatmap (REQ ➔ AST)</button>
546
- <button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">🕸️ End-to-End Human-Insight Pipeline Canvas</button>
547
- <button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">🏷️ Unmapped Specs (C-SPEC · JOB)</button>
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="ux-hint-banner" style="border-left-color: #f59e0b;">
574
- <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
575
- <div style="display: flex; align-items: center; gap: 8px;">
576
- <span>🔥 <strong>3-Tier Multi-Lens:</strong></span>
577
- <select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
578
- <option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ ➔ AST/CPG Core Chain)</option>
579
- <option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Findings Ledger — shows “not scanned” when absent)</option>
580
- <option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Static AST Mutants — score unmeasured)</option>
581
- </select>
582
- </div>
583
-
584
- <div style="display: flex; align-items: center; gap: 8px;">
585
- <span>🔍 <strong>Filter Status:</strong></span>
586
- <select id="pipelineFilterSelect" class="select-box" style="border-color: #38bdf8; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
587
- <option value="all" selected>All REQ Pipelines</option>
588
- <option value="uncovered">Only Uncovered / Active Traces</option>
589
- <option value="findings">Only Traces with Open Findings</option>
590
- </select>
591
- </div>
592
- </div>
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
- // Render one pipeline stage cell. A stage with no artifact renders as an inert “missing” cell
848
- // instead of a clickable id, so an empty stage can never be mistaken for a traced one.
849
- function stageCell(cls, icon, label, jumpId, withArrow) {
850
- const arrow = withArrow ? '<span class="arrow-sep">➔</span> ' : '';
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 rawPipelines = data.pipelines || [];
861
- const lens = document.getElementById('lensSelect').value || 'lens1';
862
- const statusFilter = document.getElementById('pipelineFilterSelect').value || 'all';
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 (rawPipelines.length === 0) {
865
- container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
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
- // Filter pipelines based on user status selection
870
- let pipelines = rawPipelines;
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
- // Group pipelines by reqId
878
- const groupedMap = new Map();
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 (groupedMap.size === 0) {
887
- const unscanned = statusFilter === 'findings' && data.findingsScanned === false;
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
- let extraHeader = 'Status';
895
- if (lens === 'lens2') extraHeader = heatmapData && heatmapData.findingsScanned === false ? '🛡️ Security Audit (never scanned)' : '🛡️ Live Security Findings';
896
- else if (lens === 'lens3') extraHeader = '🧪 Static AST Mutants (score unmeasured)';
897
-
898
- let html = '';
899
-
900
- groupedMap.forEach((group, reqId) => {
901
- const items = group.items;
902
- const totalItems = items.length;
903
- const coveredItems = items.filter(i => i.status === 'COVERED').length;
904
- const isReqHealthy = coveredItems === totalItems;
905
-
906
- const reqStatusBadge = isReqHealthy
907
- ? '<span class="neighbor-rel badge-covered">🟢 100% HEALTHY TRACE</span>'
908
- : '<span class="neighbor-rel badge-uncovered">🔴 CONTAINS UNCOVERED TRACES</span>';
909
-
910
- html += \`
911
- <div class="req-card" style="margin-bottom: 20px;">
912
- <div class="req-header" onclick="toggleAccordion('pipe-body-\${escapeHtml(reqId)}')">
913
- <div class="req-title-group">
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
- </div>
923
-
924
- <div class="accordion-body open" id="pipe-body-\${escapeHtml(reqId)}" style="padding:0;">
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 = html;
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: npm i -D @holmes-lab/holmes-kit@latest`;
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
+ };