@holmes-lab/holmes-kit 0.1.8 → 0.1.9

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/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 9a778c2-mt0p7fwt
1
+ ef6b2ad-mt1pqqxf
@@ -28,3 +28,58 @@ export interface GraphEdge {
28
28
  * @implements A-SPEC-219
29
29
  */
30
30
  export declare function startDashboardServer(options: DashboardOptions): Promise<DashboardServerHandle>;
31
+ /**
32
+ * One end-to-end 6-stage trace row: REQ -> H-SPEC -> A-SPEC -> T-SPEC -> File -> AST Symbol.
33
+ *
34
+ * Every stage is nullable ON PURPOSE. A stage that does not exist in the repository is reported as
35
+ * `null` and named in `missingStages`; it is never back-filled with a synthesized spec id or with a
36
+ * placeholder file/symbol, because a traceability matrix that invents its own evidence cannot be
37
+ * audited against the repository it claims to describe.
38
+ *
39
+ * @implements A-SPEC-219
40
+ */
41
+ export interface PipelineRow {
42
+ reqId: string;
43
+ reqTitle: string;
44
+ hspecId: string | null;
45
+ aspecId: string | null;
46
+ tspecId: string | null;
47
+ fileId: string | null;
48
+ symbolId: string | null;
49
+ symbolLine: number | null;
50
+ /** How many of the 6 stages are backed by a real artifact (1..6; REQ itself always counts). */
51
+ stagesComplete: number;
52
+ /** Human-readable names of the stages with no artifact, in pipeline order. */
53
+ missingStages: string[];
54
+ /** COVERED only when all 6 stages resolve to a real artifact. */
55
+ status: 'COVERED' | 'UNCOVERED';
56
+ findingsCount: number;
57
+ criticalCount: number;
58
+ /**
59
+ * false when no findings ledger exists. `findingsCount: 0` then means "not scanned", never
60
+ * "scanned and clean", and the audit lens must render the two differently.
61
+ */
62
+ findingsScanned: boolean;
63
+ mutantCount: number;
64
+ /**
65
+ * `null` — never a number. A mutation SCORE requires executing the test suite once per mutant;
66
+ * this endpoint only generates mutants statically, so it has no basis for a score and says so
67
+ * rather than reporting a constant that reads as a measurement.
68
+ */
69
+ mutationScore: null;
70
+ mutationScoreMeasured: false;
71
+ }
72
+ export declare function isCanonicalStatus(status: unknown): boolean;
73
+ /** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
74
+ export declare function specKindOf(id: string): string;
75
+ /**
76
+ * Build the 6-stage pipeline matrix from the repository's real specs, scanned files and findings.
77
+ *
78
+ * Pure and exported so the truthfulness invariants can be asserted directly, without an HTTP round
79
+ * trip: no row may name an artifact that the inputs do not contain, and no symbol may be dropped.
80
+ *
81
+ * @implements A-SPEC-219
82
+ */
83
+ export declare function buildPipelineRows(specs: any[], files: any[], allFindings: any[], options?: {
84
+ findingsScanned?: boolean;
85
+ }): PipelineRow[];
@@ -34,8 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.startDashboardServer = startDashboardServer;
37
+ exports.isCanonicalStatus = isCanonicalStatus;
38
+ exports.specKindOf = specKindOf;
39
+ exports.buildPipelineRows = buildPipelineRows;
37
40
  // @implements A-SPEC-215
38
41
  // @implements A-SPEC-219
42
+ const fs = __importStar(require("node:fs"));
39
43
  const http = __importStar(require("node:http"));
40
44
  const path = __importStar(require("node:path"));
41
45
  const spec_store_1 = require("../spec/spec-store");
@@ -83,12 +87,45 @@ async function startDashboardServer(options) {
83
87
  const enrichedSpecs = specs.map((s) => {
84
88
  const isA = s.id.startsWith('A-SPEC');
85
89
  const covered = isA ? implementedSpecIds.has(s.id) : s.status === 'approved';
86
- return { ...s, covered };
90
+ return { ...s, covered, legacyStatus: !isCanonicalStatus(s.status) };
87
91
  });
92
+ // Two axes, never one number. Approval (a spec was signed off) and implementation (code is
93
+ // anchored to it) answer different questions, and a spec whose status is outside the
94
+ // canonical vocabulary answers neither — it is excluded and counted separately instead of
95
+ // being silently scored 0, which read as "47 uncovered specs" when it meant "47 unmapped".
96
+ const canonical = enrichedSpecs.filter((s) => !s.legacyStatus);
97
+ const approvedCount = canonical.filter((s) => s.status === 'approved').length;
98
+ const anchorable = enrichedSpecs.filter((s) => s.id.startsWith('A-SPEC') && !s.legacyStatus);
99
+ const anchoredCount = anchorable.filter((s) => s.covered).length;
100
+ const legacySpecs = enrichedSpecs.filter((s) => s.legacyStatus);
101
+ const legacyByKind = {};
102
+ for (const s of legacySpecs) {
103
+ const kind = specKindOf(s.id);
104
+ legacyByKind[kind] = (legacyByKind[kind] || 0) + 1;
105
+ }
106
+ const approval = {
107
+ total: canonical.length,
108
+ approved: approvedCount,
109
+ pct: canonical.length > 0 ? Math.round((approvedCount / canonical.length) * 100) : 0,
110
+ };
111
+ const implementation = {
112
+ total: anchorable.length,
113
+ anchored: anchoredCount,
114
+ pct: anchorable.length > 0 ? Math.round((anchoredCount / anchorable.length) * 100) : 0,
115
+ };
116
+ const legacy = { count: legacySpecs.length, byKind: legacyByKind };
88
117
  const coveredCount = enrichedSpecs.filter((s) => s.covered).length;
89
118
  const totalCount = enrichedSpecs.length;
90
- const coveragePct = totalCount > 0 ? Math.round((coveredCount / totalCount) * 100) : 100;
91
- const body = JSON.stringify({ ok: true, totalCount, coveredCount, coveragePct, specs: enrichedSpecs });
119
+ const body = JSON.stringify({
120
+ ok: true,
121
+ totalCount,
122
+ coveredCount,
123
+ approval,
124
+ implementation,
125
+ legacy,
126
+ specs: enrichedSpecs,
127
+ files,
128
+ });
92
129
  res.writeHead(200, { 'Content-Type': 'application/json' });
93
130
  res.end(body);
94
131
  }
@@ -105,80 +142,21 @@ async function startDashboardServer(options) {
105
142
  try {
106
143
  const specs = await store.list();
107
144
  const files = scanner.scan(root);
108
- const findingsLedger = new findings_1.FindingsLedger(path.join(root, '.ax', 'ledger', 'findings.jsonl'));
109
- const allFindings = findingsLedger.list();
110
- const reqs = specs.filter((s) => s.id.startsWith('REQ-'));
111
- const pipelines = [];
112
- for (const reqItem of reqs) {
113
- const numPart = reqItem.id.replace('REQ-', '').split('.')[0];
114
- const hspecs = specs.filter((s) => s.id.startsWith(`H-SPEC-${numPart}`));
115
- const aspecs = specs.filter((s) => s.id.startsWith(`A-SPEC-${numPart}`));
116
- const tspecs = specs.filter((s) => s.id.startsWith(`T-SPEC-${numPart}`));
117
- const matchingFiles = files.filter((f) => f.implementsSpecs.some((specId) => specId.includes(numPart) || specId === reqItem.id));
118
- if (matchingFiles.length === 0) {
119
- const specFinding = allFindings.filter((f) => f.specRef?.includes(numPart) && f.status === 'open');
120
- pipelines.push({
121
- reqId: reqItem.id,
122
- reqTitle: reqItem.title,
123
- hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
124
- aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
125
- tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
126
- fileId: 'src/holmes/server/dashboard.ts',
127
- symbolId: 'startDashboardServer',
128
- status: reqItem.status === 'approved' ? 'COVERED' : 'UNCOVERED',
129
- findingsCount: specFinding.length,
130
- criticalCount: specFinding.filter((f) => f.severity === 'critical').length,
131
- mutantCount: 0,
132
- mutationScore: 100,
133
- });
134
- }
135
- else {
136
- for (const f of matchingFiles) {
137
- const fileId = f.sourcePath || f.path;
138
- const syms = f.symbols || [];
139
- const fileMutants = (0, ast_mutation_1.generateAstMutants)([f]);
140
- const fileFindings = allFindings.filter((fd) => (fd.file === fileId || fd.specRef?.includes(numPart)) && fd.status === 'open');
141
- if (syms.length === 0) {
142
- pipelines.push({
143
- reqId: reqItem.id,
144
- reqTitle: reqItem.title,
145
- hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
146
- aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
147
- tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
148
- fileId,
149
- symbolId: '(File Module)',
150
- status: 'COVERED',
151
- findingsCount: fileFindings.length,
152
- criticalCount: fileFindings.filter((fd) => fd.severity === 'critical').length,
153
- mutantCount: fileMutants.length,
154
- mutationScore: 100,
155
- });
156
- }
157
- else {
158
- for (const sym of syms.slice(0, 3)) {
159
- const symMutants = fileMutants.filter((m) => m.symbolName === sym.name);
160
- pipelines.push({
161
- reqId: reqItem.id,
162
- reqTitle: reqItem.title,
163
- hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
164
- aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
165
- tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
166
- fileId,
167
- symbolId: sym.name,
168
- status: 'COVERED',
169
- findingsCount: fileFindings.length,
170
- criticalCount: fileFindings.filter((fd) => fd.severity === 'critical').length,
171
- mutantCount: symMutants.length,
172
- mutationScore: 100,
173
- });
174
- }
175
- }
176
- }
177
- }
178
- }
145
+ // FindingsLedger.list() returns [] for a missing file, which is indistinguishable from a
146
+ // clean scan. The dashboard therefore asks the filesystem directly: an audit lens that
147
+ // renders "0 open findings" over a ledger that was never written is a green wall, not a
148
+ // verdict.
149
+ const findingsPath = path.join(root, '.ax', 'ledger', 'findings.jsonl');
150
+ const findingsScanned = fs.existsSync(findingsPath);
151
+ const findingsLedger = new findings_1.FindingsLedger(findingsPath);
152
+ const allFindings = findingsScanned ? findingsLedger.list() : [];
153
+ const pipelines = buildPipelineRows(specs, files, allFindings, { findingsScanned });
179
154
  const body = JSON.stringify({
180
155
  ok: true,
181
156
  pipelineCount: pipelines.length,
157
+ completeCount: pipelines.filter((p) => p.status === 'COVERED').length,
158
+ findingsScanned,
159
+ mutationScoreMeasured: false,
182
160
  pipelines,
183
161
  });
184
162
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -374,7 +352,7 @@ function renderDashboardHtml() {
374
352
  <html lang="en">
375
353
  <head>
376
354
  <meta charset="UTF-8">
377
- <title>Holmes-Kit World Top-Tier 3-Tier Multi-Lens RTM & Graph Canvas</title>
355
+ <title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
378
356
  <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
379
357
  <style>
380
358
  body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
@@ -384,18 +362,28 @@ function renderDashboardHtml() {
384
362
  .tab-btn.active { background: #3b82f6; color: #ffffff; border-color: #3b82f6; }
385
363
  .search-box { width: 100%; max-width: 400px; padding: 10px 14px; border-radius: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; margin-bottom: 16px; font-size: 14px; }
386
364
 
387
- /* REQ Hierarchical Drill-Down Accordion Matrix Styling */
365
+ /* Quantitative REQ SDLC Health Matrix Grid Styling */
388
366
  .rtm-container { display: flex; flex-direction: column; gap: 16px; }
389
- .req-card { background: #1e293b; border: 1.5px solid #334155; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.4); transition: border-color 0.2s ease; }
367
+ .req-card { background: #1e293b; border: 1.5px solid #334155; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.4); transition: border-color 0.2s ease; margin-bottom: 16px; }
390
368
  .req-card:hover { border-color: #ec4899; }
391
369
  .req-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; background: rgba(236,72,153,0.06); cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); }
392
370
  .req-title-group { display: flex; align-items: center; gap: 12px; }
393
371
  .req-id-badge { background: #ec4899; color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; }
394
372
  .req-title { font-size: 15px; font-weight: bold; color: #f8fafc; }
395
- .req-progress-bar { width: 140px; height: 8px; background: #334155; border-radius: 4px; overflow: hidden; display: inline-block; margin-right: 10px; }
396
- .req-progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #34d399); }
373
+
374
+ /* 6-Stage Quantitative Metrics Bar inside REQ Header */
375
+ .quant-grid { display: flex; gap: 8px; align-items: center; background: #0f172a; border: 1px solid #334155; padding: 6px 12px; border-radius: 8px; font-size: 11px; }
376
+ .quant-badge { display: flex; align-items: center; gap: 4px; padding: 2px 6px; border-radius: 4px; font-weight: bold; }
377
+ .quant-hspec { background: rgba(139,92,246,0.2); color: #c084fc; border: 1px solid #8b5cf6; }
378
+ .quant-aspec { background: rgba(59,130,246,0.2); color: #60a5fa; border: 1px solid #3b82f6; }
379
+ .quant-tspec { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid #06b6d4; }
380
+ .quant-file { background: rgba(16,185,129,0.2); color: #34d399; border: 1px solid #10b981; }
381
+ .quant-symbol { background: rgba(245,158,11,0.2); color: #fbbf24; border: 1px solid #f59e0b; }
382
+
397
383
  .expand-btn { background: #334155; color: #38bdf8; border: none; padding: 6px 14px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 12px; transition: all 0.15s ease; }
398
384
  .expand-btn:hover { background: #3b82f6; color: #fff; }
385
+ .view-graph-btn { background: #8b5cf6; color: #fff; border: none; padding: 6px 14px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 12px; }
386
+ .view-graph-btn:hover { background: #7c3aed; }
399
387
 
400
388
  .accordion-body { padding: 16px 20px; display: none; background: #0f172a; border-top: 1px solid #334155; flex-direction: column; gap: 10px; }
401
389
  .accordion-body.open { display: flex; }
@@ -410,9 +398,9 @@ function renderDashboardHtml() {
410
398
  .badge-uncovered { background-color: #991b1b; color: #f87171; }
411
399
  .stat-badge { font-size: 24px; font-weight: 800; color: #f59e0b; }
412
400
 
413
- /* 6-Stage Sequential Pipeline RTM Heatmap Matrix Styling */
414
- .heatmap-wrapper { width: 100%; overflow-x: auto; background: #0f172a; border-radius: 10px; border: 1.5px solid #334155; padding: 16px; box-sizing: border-box; }
415
- .pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%; min-width: 1000px; }
401
+ /* REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Styling */
402
+ .heatmap-wrapper { width: 100%; background: #0f172a; border-radius: 10px; border: 1.5px solid #334155; padding: 20px; box-sizing: border-box; }
403
+ .pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%; }
416
404
  .pipeline-table th, .pipeline-table td { border: 1px solid #334155; padding: 10px 12px; text-align: left; }
417
405
  .pipeline-table th { background: #1e293b; color: #f8fafc; font-weight: bold; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
418
406
  .pipeline-row:hover { background: #1e293b; }
@@ -479,9 +467,23 @@ function renderDashboardHtml() {
479
467
  .legend-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
480
468
  .controls { margin-bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
481
469
  .select-box { padding: 8px 12px; border-radius: 6px; border: 1px solid #334155; background: #1e293b; color: #fff; font-size: 13px; }
482
- .popover-card { position: absolute; top: 16px; right: 420px; width: 320px; background: #0f172a; border: 1px solid #3b82f6; border-radius: 8px; padding: 14px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 100; font-size: 12px; }
470
+
471
+ .axis-stats { display: flex; align-items: center; gap: 18px; font-size: 13px; color: #94a3b8; }
472
+ .axis-stat { display: inline-flex; align-items: center; gap: 6px; }
473
+ .legacy-chip { cursor: pointer; border: 1px dashed #64748b; border-radius: 999px; padding: 4px 10px; }
474
+ .legacy-chip:hover { border-color: #f59e0b; color: #f59e0b; }
475
+ .scope-notice { font-size: 12px; color: #94a3b8; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
476
+ .scope-notice-truncated { color: #fbbf24; border-color: #b45309; border-style: dashed; }
477
+ .stage-missing { color: #64748b; font-style: italic; cursor: default; background: repeating-linear-gradient(45deg, rgba(100,116,139,0.06) 0 6px, transparent 6px 12px); }
478
+ .badge-unmeasured { background: #334155; color: #cbd5e1; border: 1px dashed #64748b; }
479
+ /* World Top-Tier High-Density Rich Tooltip Popover Card Styling */
480
+ .popover-card { position: absolute; top: 16px; right: 420px; width: 360px; background: #0f172a; border: 1.5px solid #3b82f6; border-radius: 10px; padding: 16px; box-shadow: 0 12px 35px rgba(0,0,0,0.8); pointer-events: none; opacity: 0; transition: opacity 0.15s ease; z-index: 100; font-size: 12px; box-sizing: border-box; backdrop-filter: blur(10px); }
483
481
  .popover-card.active { opacity: 1; pointer-events: auto; }
484
- .popover-title { font-weight: bold; font-size: 14px; color: #38bdf8; margin-bottom: 6px; word-break: break-all; }
482
+ .popover-badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-weight: 800; font-size: 11px; color: #fff; margin-bottom: 8px; }
483
+ .popover-title { font-weight: bold; font-size: 15px; color: #f8fafc; margin-bottom: 6px; word-break: break-all; }
484
+ .popover-section { margin-top: 10px; padding-top: 8px; border-top: 1px solid #334155; display: flex; flex-direction: column; gap: 4px; }
485
+ .popover-label { color: #94a3b8; font-size: 11px; font-weight: bold; text-transform: uppercase; }
486
+ .popover-value { color: #cbd5e1; font-size: 12px; }
485
487
 
486
488
  .canvas-toolbar { position: absolute; bottom: 20px; right: 420px; display: flex; flex-direction: column; gap: 8px; z-index: 90; background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; padding: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
487
489
  .tool-btn { background: #1e293b; color: #f8fafc; border: 1px solid #334155; width: 38px; height: 38px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; }
@@ -492,39 +494,62 @@ function renderDashboardHtml() {
492
494
  <body>
493
495
  <div class="header">
494
496
  <div>
495
- <h1 style="margin:0; font-size: 24px;">🔥 3-Tier Multi-Lens RTM Pipeline & Graph Canvas</h1>
496
- <div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Layered Multi-Lens ArchitectureTruthful Real-Time Entity Traceability</div>
497
+ <h1 style="margin:0; font-size: 24px;">🔥 REQ-Grouped Pipeline Matrix & AST/CPG Canvas</h1>
498
+ <div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">REQ Master Section Blocks • Multi-Lens Quick Filter Bar Full-collection CPG call graph (canvas shows a bounded subgraph)</div>
499
+ </div>
500
+ <div class="axis-stats">
501
+ <span class="axis-stat" title="Specs signed off, over specs using the canonical status vocabulary">📋 Approval <span id="approvalStat" class="stat-badge">--%</span></span>
502
+ <span class="axis-stat" title="A-SPECs with code anchored to them, over canonical A-SPECs">⚙️ Implementation <span id="implStat" class="stat-badge">--%</span></span>
503
+ <span class="axis-stat legacy-chip" id="legacyChip" title="Specs whose status is outside draft|review|approved|outdated — excluded from both axes, not scored zero" onclick="switchTab('legacy')">🏷️ <span id="legacyStat">--</span></span>
497
504
  </div>
498
- <div>Specification Coverage: <span id="coverageStat" class="stat-badge">--%</span></div>
499
505
  </div>
500
506
 
501
507
  <div class="nav-tabs">
502
- <button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">📋 RTM Traceability Grid (REQ Drill-Down)</button>
503
- <button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 6-Stage Pipeline Heatmap (REQ ➔ AST/CPG)</button>
508
+ <button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">📊 Quantitative REQ RTM Matrix Grid</button>
509
+ <button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 REQ-Grouped Pipeline Heatmap (REQ ➔ AST)</button>
504
510
  <button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">🕸️ End-to-End Human-Insight Pipeline Canvas</button>
511
+ <button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">🏷️ Unmapped Specs (C-SPEC · JOB)</button>
505
512
  </div>
506
513
 
507
514
  <div id="rtmTab">
508
515
  <div class="ux-hint-banner">
509
- <span>💡 <strong>REQ Hierarchical Accordion Matrix:</strong> Displaying primary <strong>REQ Business Requirements</strong>. Click <strong>▶ Expand</strong> to drill down into downstream specs, files, and AST symbols!</span>
516
+ <span>💡 <strong>Quantitative REQ SDLC Health Matrix Grid:</strong> Every business requirement displays exact 6-Stage Quantitative Metrics (H/A/T-SPEC, Code Files, AST Symbols, CPG Edges). Click <strong>▶ Expand</strong> for itemized status!</span>
510
517
  </div>
511
518
  <input type="text" id="searchInput" class="search-box" placeholder="Search business REQ requirements by ID or title..." />
512
519
  <div id="specGrid" class="rtm-container">Loading REQ specifications...</div>
513
520
  </div>
514
521
 
522
+ <div id="legacyTab" style="display: none;">
523
+ <div class="ux-hint-banner" style="border-left-color: #64748b;">
524
+ <span>🏷️ <strong>Unmapped Specs:</strong> these documents use a status vocabulary outside <code>draft | review | approved | outdated</code>, so no governance gate has scored them. They are <strong>excluded</strong> from both headline axes — an unmapped vocabulary is not the same as an uncovered spec.</span>
525
+ </div>
526
+ <div id="legacyGrid" class="rtm-container">Loading unmapped specifications...</div>
527
+ </div>
528
+
515
529
  <div id="heatmapTab" style="display: none;">
516
530
  <div class="ux-hint-banner" style="border-left-color: #f59e0b;">
517
- <div style="display: flex; align-items: center; gap: 12px;">
518
- <span>🔥 <strong>3-Tier Multi-Lens Insight Switcher:</strong> Select Lens to Overlay Truthful Real-Time Entity Data:</span>
519
- <select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
520
- <option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ ➔ AST/CPG Core Chain)</option>
521
- <option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Live Findings + Provenance Ledger)</option>
522
- <option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Live AST Mutants + Score)</option>
523
- </select>
531
+ <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
532
+ <div style="display: flex; align-items: center; gap: 8px;">
533
+ <span>🔥 <strong>3-Tier Multi-Lens:</strong></span>
534
+ <select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
535
+ <option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ AST/CPG Core Chain)</option>
536
+ <option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Findings Ledger shows “not scanned” when absent)</option>
537
+ <option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Static AST Mutants — score unmeasured)</option>
538
+ </select>
539
+ </div>
540
+
541
+ <div style="display: flex; align-items: center; gap: 8px;">
542
+ <span>🔍 <strong>Filter Status:</strong></span>
543
+ <select id="pipelineFilterSelect" class="select-box" style="border-color: #38bdf8; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
544
+ <option value="all" selected>All REQ Pipelines</option>
545
+ <option value="uncovered">Only Uncovered / Active Traces</option>
546
+ <option value="findings">Only Traces with Open Findings</option>
547
+ </select>
548
+ </div>
524
549
  </div>
525
550
  </div>
526
551
  <div class="heatmap-wrapper">
527
- <div id="heatmapContainer">Loading 6-stage pipeline heatmap...</div>
552
+ <div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
528
553
  </div>
529
554
  </div>
530
555
 
@@ -565,6 +590,7 @@ function renderDashboardHtml() {
565
590
  </select>
566
591
  </div>
567
592
 
593
+ <div id="graphScopeNotice" class="scope-notice"></div>
568
594
  <div id="graphCanvasContainer">
569
595
  <svg id="graphSvg" class="graph-svg"></svg>
570
596
 
@@ -591,11 +617,12 @@ function renderDashboardHtml() {
591
617
  <button class="tool-btn" onclick="resetZoom()" title="Reset Zoom to 100%">🔄</button>
592
618
  </div>
593
619
 
620
+ <!-- World Top-Tier Rich Hover Popover Card -->
594
621
  <div id="popoverCard" class="popover-card">
622
+ <div id="popoverBadge" class="popover-badge">--</div>
595
623
  <div id="popoverTitle" class="popover-title">--</div>
596
- <div id="popoverType" style="color:#94a3b8; margin-bottom:4px;">--</div>
597
- <div id="popoverStatus" style="font-weight:bold; margin-bottom:8px;">--</div>
598
- <div id="popoverStats" style="color:#cbd5e1; font-size:11px;">--</div>
624
+ <div id="popoverSection1" class="popover-section"></div>
625
+ <div id="popoverSection2" class="popover-section"></div>
599
626
  </div>
600
627
  </div>
601
628
  <div class="legend">
@@ -645,6 +672,7 @@ function renderDashboardHtml() {
645
672
  }
646
673
 
647
674
  let allSpecs = [];
675
+ let allFiles = [];
648
676
  let graphData = null;
649
677
  let heatmapData = null;
650
678
  let currentZoom = null;
@@ -652,14 +680,53 @@ function renderDashboardHtml() {
652
680
  let traversalHistory = [];
653
681
  let selectedRootNodeId = 'A-SPEC-219';
654
682
 
683
+ // Specs excluded from both axes get their own surface. Previously 47 documents were invisible
684
+ // on every tab while still sitting in the headline denominator, which read as 0% coverage.
685
+ function renderLegacySpecGrid(specs) {
686
+ const container = document.getElementById('legacyGrid');
687
+ if (!container) return;
688
+ const legacy = specs.filter(s => s.legacyStatus);
689
+ if (legacy.length === 0) {
690
+ container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">Every spec uses the canonical status vocabulary.</div>';
691
+ return;
692
+ }
693
+ const byKind = new Map();
694
+ legacy.forEach(s => {
695
+ const kind = (s.id.match(/^([A-Za-z-]+?)-\d/) || [null, 'UNKNOWN'])[1];
696
+ if (!byKind.has(kind)) byKind.set(kind, []);
697
+ byKind.get(kind).push(s);
698
+ });
699
+ let html = '';
700
+ byKind.forEach((items, kind) => {
701
+ const statuses = {};
702
+ items.forEach(s => { statuses[s.status] = (statuses[s.status] || 0) + 1; });
703
+ const statusSummary = Object.keys(statuses).map(k => escapeHtml(k) + ' × ' + statuses[k]).join(' · ');
704
+ html += '<div class="req-card" style="margin-bottom:20px;">'
705
+ + '<div class="req-header" onclick="toggleAccordion(\'legacy-body-' + escapeHtml(kind) + '\')">'
706
+ + '<div class="req-title-group"><span class="req-id-badge">' + escapeHtml(kind) + '</span>'
707
+ + '<span class="req-title">' + items.length + ' unmapped document' + (items.length === 1 ? '' : 's') + '</span>'
708
+ + '<span class="neighbor-rel badge-unmeasured">' + statusSummary + '</span></div>'
709
+ + '<button class="expand-btn">▶ Expand</button></div>'
710
+ + '<div class="accordion-body open" id="legacy-body-' + escapeHtml(kind) + '">'
711
+ + items.map(s =>
712
+ '<div class="tree-row tree-row-file"><div class="tree-label">🏷️ <strong>' + escapeHtml(s.id) + '</strong>: ' + escapeHtml(s.title || '') + '</div>'
713
+ + '<div><span class="neighbor-rel badge-unmeasured" title="Outside draft|review|approved|outdated">status: ' + escapeHtml(String(s.status)) + '</span></div></div>'
714
+ ).join('')
715
+ + '</div></div>';
716
+ });
717
+ container.innerHTML = html;
718
+ }
719
+
655
720
  function switchTab(tab) {
656
721
  document.getElementById('tabRtmBtn').classList.toggle('active', tab === 'rtm');
657
722
  document.getElementById('tabHeatmapBtn').classList.toggle('active', tab === 'heatmap');
658
723
  document.getElementById('tabGraphBtn').classList.toggle('active', tab === 'graph');
724
+ document.getElementById('tabLegacyBtn').classList.toggle('active', tab === 'legacy');
659
725
 
660
726
  document.getElementById('rtmTab').style.display = tab === 'rtm' ? 'block' : 'none';
661
727
  document.getElementById('heatmapTab').style.display = tab === 'heatmap' ? 'block' : 'none';
662
728
  document.getElementById('graphTab').style.display = tab === 'graph' ? 'block' : 'none';
729
+ document.getElementById('legacyTab').style.display = tab === 'legacy' ? 'block' : 'none';
663
730
 
664
731
  if (tab === 'graph' && !graphData) {
665
732
  loadGraphCanvas();
@@ -674,8 +741,16 @@ function renderDashboardHtml() {
674
741
  const data = await res.json();
675
742
  if (data.ok) {
676
743
  allSpecs = data.specs;
677
- document.getElementById('coverageStat').innerText = data.coveragePct + '% (' + data.coveredCount + '/' + data.totalCount + ')';
678
- renderReqAccordionMatrix(allSpecs);
744
+ allFiles = data.files || [];
745
+ const ap = data.approval || { pct: 0, approved: 0, total: 0 };
746
+ const im = data.implementation || { pct: 0, anchored: 0, total: 0 };
747
+ const lg = data.legacy || { count: 0, byKind: {} };
748
+ document.getElementById('approvalStat').innerText = ap.pct + '% (' + ap.approved + '/' + ap.total + ')';
749
+ document.getElementById('implStat').innerText = im.pct + '% (' + im.anchored + '/' + im.total + ')';
750
+ const kinds = Object.keys(lg.byKind || {}).map(k => k + ' ' + lg.byKind[k]).join(' · ');
751
+ document.getElementById('legacyStat').innerText = lg.count + ' unmapped' + (kinds ? ' (' + kinds + ')' : '');
752
+ renderLegacySpecGrid(data.specs || []);
753
+ renderReqQuantitativeGrid(allSpecs, allFiles);
679
754
  }
680
755
  } catch (err) {
681
756
  document.getElementById('specGrid').innerText = 'Failed to load specs: ' + escapeHtml(err.message || err);
@@ -701,76 +776,148 @@ function renderDashboardHtml() {
701
776
  }
702
777
  }
703
778
 
704
- // World Top-Tier Truthful Real-Time 3-Tier Multi-Lens Pipeline Heatmap Renderer
779
+ // Render one pipeline stage cell. A stage with no artifact renders as an inert “missing” cell
780
+ // instead of a clickable id, so an empty stage can never be mistaken for a traced one.
781
+ function stageCell(cls, icon, label, jumpId, withArrow) {
782
+ const arrow = withArrow ? '<span class="arrow-sep">➔</span> ' : '';
783
+ if (!label) {
784
+ return '<td class="' + cls + ' stage-missing" title="No artifact for this stage">' + arrow + '— missing</td>';
785
+ }
786
+ const onclick = jumpId ? ' onclick="focusSpecInGraph(\'' + escapeHtml(String(jumpId)) + '\')"' : '';
787
+ return '<td class="' + cls + '"' + onclick + '>' + arrow + icon + ' ' + escapeHtml(String(label)) + '</td>';
788
+ }
789
+ // REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Renderer
705
790
  function render6StagePipelineMatrix(data) {
706
791
  const container = document.getElementById('heatmapContainer');
707
- const pipelines = data.pipelines || [];
792
+ const rawPipelines = data.pipelines || [];
708
793
  const lens = document.getElementById('lensSelect').value || 'lens1';
794
+ const statusFilter = document.getElementById('pipelineFilterSelect').value || 'all';
709
795
 
710
- if (pipelines.length === 0) {
796
+ if (rawPipelines.length === 0) {
711
797
  container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
712
798
  return;
713
799
  }
714
800
 
715
- let extraHeader = 'Status';
716
- if (lens === 'lens2') extraHeader = '🛡️ Live Security Findings';
717
- else if (lens === 'lens3') extraHeader = '🧪 Real AST Mutants Score';
718
-
719
- let html = \`
720
- <table class="pipeline-table">
721
- <thead>
722
- <tr>
723
- <th>Stage 1: REQ Business</th>
724
- <th>Stage 2: H-SPEC Functional</th>
725
- <th>Stage 3: A-SPEC Architecture</th>
726
- <th>Stage 4: T-SPEC Verification</th>
727
- <th>Stage 5: Source Code File</th>
728
- <th>Stage 6: AST / CPG Symbol</th>
729
- <th>\${extraHeader}</th>
730
- </tr>
731
- </thead>
732
- <tbody>
733
- \`;
801
+ // Filter pipelines based on user status selection
802
+ let pipelines = rawPipelines;
803
+ if (statusFilter === 'uncovered') {
804
+ pipelines = rawPipelines.filter(p => p.status !== 'COVERED');
805
+ } else if (statusFilter === 'findings') {
806
+ pipelines = rawPipelines.filter(p => p.findingsCount > 0);
807
+ }
734
808
 
809
+ // Group pipelines by reqId
810
+ const groupedMap = new Map();
735
811
  pipelines.forEach(p => {
736
- let statusBadge = \`<span class="neighbor-rel \${p.status === 'COVERED' ? 'badge-covered' : 'badge-uncovered'}">\${p.status}</span>\`;
737
-
738
- if (lens === 'lens2') {
739
- if (p.criticalCount > 0) {
740
- statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
741
- } else if (p.findingsCount > 0) {
742
- statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
743
- } else {
744
- statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
745
- }
746
- } else if (lens === 'lens3') {
747
- const mutantCount = p.mutantCount || 0;
748
- if (mutantCount > 0) {
749
- statusBadge = \`<span class="neighbor-rel badge-covered">🧪 100% Score (\${mutantCount} Mutants)</span>\`;
750
- } else {
751
- statusBadge = '<span class="neighbor-rel badge-covered">🧪 100% Score (0 Mutants)</span>';
752
- }
812
+ if (!groupedMap.has(p.reqId)) {
813
+ groupedMap.set(p.reqId, { reqId: p.reqId, reqTitle: p.reqTitle, items: [] });
753
814
  }
815
+ groupedMap.get(p.reqId).items.push(p);
816
+ });
817
+
818
+ if (groupedMap.size === 0) {
819
+ const unscanned = statusFilter === 'findings' && data.findingsScanned === false;
820
+ container.innerHTML = unscanned
821
+ ? '<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>'
822
+ : '<div style="color:#94a3b8; font-size:14px; padding:16px;">No pipelines match the selected filter.</div>';
823
+ return;
824
+ }
825
+
826
+ let extraHeader = 'Status';
827
+ if (lens === 'lens2') extraHeader = heatmapData && heatmapData.findingsScanned === false ? '🛡️ Security Audit (never scanned)' : '🛡️ Live Security Findings';
828
+ else if (lens === 'lens3') extraHeader = '🧪 Static AST Mutants (score unmeasured)';
829
+
830
+ let html = '';
831
+
832
+ groupedMap.forEach((group, reqId) => {
833
+ const items = group.items;
834
+ const totalItems = items.length;
835
+ const coveredItems = items.filter(i => i.status === 'COVERED').length;
836
+ const isReqHealthy = coveredItems === totalItems;
837
+
838
+ const reqStatusBadge = isReqHealthy
839
+ ? '<span class="neighbor-rel badge-covered">🟢 100% HEALTHY TRACE</span>'
840
+ : '<span class="neighbor-rel badge-uncovered">🔴 CONTAINS UNCOVERED TRACES</span>';
754
841
 
755
842
  html += \`
756
- <tr class="pipeline-row">
757
- <td class="stage-req" onclick="focusSpecInGraph('\${escapeHtml(p.reqId)}')">🟣 \${escapeHtml(p.reqId)}</td>
758
- <td class="stage-hspec" onclick="focusSpecInGraph('\${escapeHtml(p.hspecId)}')"><span class="arrow-sep">➔</span> 🟣 \${escapeHtml(p.hspecId)}</td>
759
- <td class="stage-aspec" onclick="focusSpecInGraph('\${escapeHtml(p.aspecId)}')"><span class="arrow-sep">➔</span> 🔵 \${escapeHtml(p.aspecId)}</td>
760
- <td class="stage-tspec" onclick="focusSpecInGraph('\${escapeHtml(p.tspecId)}')"><span class="arrow-sep">➔</span> 🔷 \${escapeHtml(p.tspecId)}</td>
761
- <td class="stage-file" onclick="focusSpecInGraph('\${escapeHtml(p.fileId)}')"><span class="arrow-sep">➔</span> 📄 \${escapeHtml(p.fileId.split('/').pop() || p.fileId)}</td>
762
- <td class="stage-symbol" onclick="focusSpecInGraph('\${escapeHtml(p.fileId + '#' + p.symbolId)}')"><span class="arrow-sep">➔</span> ⚡ \${escapeHtml(p.symbolId)}</td>
763
- <td>\${statusBadge}</td>
764
- </tr>
843
+ <div class="req-card" style="margin-bottom: 20px;">
844
+ <div class="req-header" onclick="toggleAccordion('pipe-body-\${escapeHtml(reqId)}')">
845
+ <div class="req-title-group">
846
+ <span class="req-id-badge">\${escapeHtml(reqId)}</span>
847
+ <span class="req-title">\${escapeHtml(group.reqTitle || 'Business Requirement Pipeline')}</span>
848
+ \${reqStatusBadge}
849
+ </div>
850
+ <div style="display: flex; align-items: center; gap: 12px;">
851
+ <span style="font-size: 12px; color: #94a3b8; font-weight: bold;">\${totalItems} End-to-End Trace Chain\${totalItems > 1 ? 's' : ''}</span>
852
+ <button class="expand-btn">▶ Expand Pipeline Matrix</button>
853
+ </div>
854
+ </div>
855
+
856
+ <div class="accordion-body open" id="pipe-body-\${escapeHtml(reqId)}" style="padding:0;">
857
+ <table class="pipeline-table">
858
+ <thead>
859
+ <tr>
860
+ <th>Stage 1: REQ Business</th>
861
+ <th>Stage 2: H-SPEC Functional</th>
862
+ <th>Stage 3: A-SPEC Architecture</th>
863
+ <th>Stage 4: T-SPEC Verification</th>
864
+ <th>Stage 5: Source Code File</th>
865
+ <th>Stage 6: AST / CPG Symbol</th>
866
+ <th>\${extraHeader}</th>
867
+ </tr>
868
+ </thead>
869
+ <tbody>
870
+ \`;
871
+
872
+ items.forEach(p => {
873
+ const missing = p.missingStages || [];
874
+ let statusBadge = missing.length === 0
875
+ ? '<span class="neighbor-rel badge-covered">COVERED (6/6)</span>'
876
+ : \`<span class="neighbor-rel badge-uncovered" title="Missing: \${escapeHtml(missing.join(', '))}">UNCOVERED (\${p.stagesComplete}/6)</span>\`;
877
+
878
+ if (lens === 'lens2') {
879
+ if (!p.findingsScanned) {
880
+ statusBadge = '<span class="neighbor-rel badge-unmeasured" title="No .ax/ledger/findings.jsonl — this chain has never been audited">⚪ Not scanned</span>';
881
+ } else if (p.criticalCount > 0) {
882
+ statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
883
+ } else if (p.findingsCount > 0) {
884
+ statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
885
+ } else {
886
+ statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
887
+ }
888
+ } else if (lens === 'lens3') {
889
+ // A score requires running the suite once per mutant; this endpoint only generates them
890
+ // statically, so it reports the mutant count and says the score is unmeasured.
891
+ const mutantCount = p.mutantCount || 0;
892
+ 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>\`;
893
+ }
894
+
895
+ html += \`
896
+ <tr class="pipeline-row">
897
+ \${stageCell('stage-req', '🟣', p.reqId, p.reqId, false)}
898
+ \${stageCell('stage-hspec', '🟣', p.hspecId, p.hspecId, true)}
899
+ \${stageCell('stage-aspec', '🔵', p.aspecId, p.aspecId, true)}
900
+ \${stageCell('stage-tspec', '🔷', p.tspecId, p.tspecId, true)}
901
+ \${stageCell('stage-file', '📄', p.fileId ? (p.fileId.split('/').pop() || p.fileId) : null, p.fileId, true)}
902
+ \${stageCell('stage-symbol', '⚡', p.symbolId, p.fileId && p.symbolId ? p.fileId + '#' + p.symbolId : null, true)}
903
+ <td>\${statusBadge}</td>
904
+ </tr>
905
+ \`;
906
+ });
907
+
908
+ html += \`
909
+ </tbody>
910
+ </table>
911
+ </div>
912
+ </div>
765
913
  \`;
766
914
  });
767
915
 
768
- html += '</tbody></table>';
769
916
  container.innerHTML = html;
770
917
  }
771
918
 
772
- // World Top-Tier REQ Hierarchical Drill-Down Accordion Renderer
773
- function renderReqAccordionMatrix(specs) {
919
+ // World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
920
+ function renderReqQuantitativeGrid(specs, files) {
774
921
  const container = document.getElementById('specGrid');
775
922
  const reqList = specs.filter(s => s.id.startsWith('REQ-'));
776
923
 
@@ -781,13 +928,33 @@ function renderDashboardHtml() {
781
928
 
782
929
  container.innerHTML = reqList.map(req => {
783
930
  const numPart = req.id.replace('REQ-', '').split('.')[0];
784
- const hspecs = specs.filter(s => s.id.startsWith('H-SPEC-' + numPart));
785
- const aspecs = specs.filter(s => s.id.startsWith('A-SPEC-' + numPart));
786
- const tspecs = specs.filter(s => s.id.startsWith('T-SPEC-' + numPart));
931
+ const belongs = (id, kind) => id === kind + '-' + numPart || id.startsWith(kind + '-' + numPart + '.');
932
+ const hspecs = specs.filter(s => belongs(s.id, 'H-SPEC'));
933
+ const aspecs = specs.filter(s => belongs(s.id, 'A-SPEC'));
934
+ const tspecs = specs.filter(s => belongs(s.id, 'T-SPEC'));
935
+
936
+ const matchingFiles = files.filter(f =>
937
+ f.implementsSpecs.some(specId =>
938
+ specId === req.id || belongs(specId, 'H-SPEC') || belongs(specId, 'A-SPEC') || belongs(specId, 'T-SPEC')
939
+ )
940
+ );
941
+
942
+ let totalSymbols = 0;
943
+ let totalCallEdges = 0;
944
+ matchingFiles.forEach(f => {
945
+ totalSymbols += (f.symbols || []).length;
946
+ totalCallEdges += (f.edges || []).filter(e => e.rel === 'calls').length;
947
+ });
787
948
 
788
- const totalDownstream = hspecs.length + aspecs.length + tspecs.length;
789
- const coveredDownstream = [...hspecs, ...aspecs, ...tspecs].filter(s => s.covered).length;
790
- const progressPct = totalDownstream > 0 ? Math.round((coveredDownstream / totalDownstream) * 100) : (req.covered ? 100 : 0);
949
+ // Every stage must be backed by a real artifact. Counting only specs would call a REQ with
950
+ // no code "6-Stage Complete", which is the claim this grid exists to verify.
951
+ const stagePresent = [true, hspecs.length > 0, aspecs.length > 0, tspecs.length > 0, matchingFiles.length > 0, totalSymbols > 0];
952
+ const stageNames = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
953
+ const missingStages = stageNames.filter((_, i) => !stagePresent[i]);
954
+ const stagesComplete = stagePresent.filter(Boolean).length;
955
+ const healthBadge = missingStages.length === 0
956
+ ? '<span class="neighbor-rel badge-covered" style="font-size:11px; font-weight:800;">🟢 Fully Traced (6/6 Stages)</span>'
957
+ : '<span class="neighbor-rel badge-uncovered" style="font-size:11px; font-weight:800;" title="Missing: ' + escapeHtml(missingStages.join(', ')) + '">🟡 ' + stagesComplete + '/6 Stages · missing ' + escapeHtml(missingStages.join(', ')) + '</span>';
791
958
 
792
959
  return \`
793
960
  <div class="req-card" id="card-\${escapeHtml(req.id)}">
@@ -795,15 +962,18 @@ function renderDashboardHtml() {
795
962
  <div class="req-title-group">
796
963
  <span class="req-id-badge">\${escapeHtml(req.id)}</span>
797
964
  <span class="req-title">\${escapeHtml(req.title || 'Business Requirement')}</span>
965
+ \${healthBadge}
798
966
  </div>
799
- <div style="display: flex; align-items: center; gap: 12px;">
800
- <div style="font-size: 11px; color: #94a3b8;">
801
- <div class="req-progress-bar">
802
- <div class="req-progress-fill" style="width: \${progressPct}%;"></div>
803
- </div>
804
- <strong>\${progressPct}% Covered</strong> (\${coveredDownstream}/\${totalDownstream})
967
+
968
+ <div style="display: flex; align-items: center; gap: 14px;">
969
+ <div class="quant-grid">
970
+ <span class="quant-badge quant-hspec" title="Functional Specs Count">🟣 H: \${hspecs.length}</span>
971
+ <span class="quant-badge quant-aspec" title="Architecture Specs Count">🔵 A: \${aspecs.length}</span>
972
+ <span class="quant-badge quant-tspec" title="Test Specs Count">🔷 T: \${tspecs.length}</span>
973
+ <span class="quant-badge quant-file" title="Code Files Linked">📄 File: \${matchingFiles.length}</span>
974
+ <span class="quant-badge quant-symbol" title="AST Symbols Mapped">⚡ AST: \${totalSymbols}</span>
805
975
  </div>
806
- <button class="expand-btn">▶ Expand Downstream Tree</button>
976
+ <button class="expand-btn">▶ Expand Matrix Tree</button>
807
977
  <button class="view-graph-btn" onclick="event.stopPropagation(); focusSpecInGraph('\${escapeHtml(req.id)}')">🕸️ View in Graph</button>
808
978
  </div>
809
979
  </div>
@@ -838,6 +1008,29 @@ function renderDashboardHtml() {
838
1008
  </div>
839
1009
  </div>
840
1010
  \`).join('')}
1011
+
1012
+ \${matchingFiles.map(f => {
1013
+ const fId = f.sourcePath || f.path;
1014
+ const symList = f.symbols || [];
1015
+ return \`
1016
+ <div class="tree-row tree-row-file">
1017
+ <div class="tree-label">📄 <strong>\${escapeHtml(fId)}</strong> (\${symList.length} AST Symbols)</div>
1018
+ <div>
1019
+ <span class="neighbor-rel badge-covered" style="margin-right:8px;">LINKED</span>
1020
+ <button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(fId)}')">🕸️ View File</button>
1021
+ </div>
1022
+ </div>
1023
+ \${symList.map(sym => \`
1024
+ <div class="tree-row tree-row-symbol">
1025
+ <div class="tree-label">⚡ <strong>\${escapeHtml(sym.name)}</strong> (Line \${sym.startLine || 1})</div>
1026
+ <div>
1027
+ <span class="neighbor-rel quant-symbol" style="margin-right:8px;">AST SYMBOL</span>
1028
+ <button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(fId + '#' + sym.name)}')">⚡ View Symbol</button>
1029
+ </div>
1030
+ </div>
1031
+ \`).join('')}
1032
+ \`;
1033
+ }).join('')}
841
1034
  </div>
842
1035
  </div>
843
1036
  \`;
@@ -862,7 +1055,7 @@ function renderDashboardHtml() {
862
1055
  document.getElementById('searchInput').addEventListener('input', (e) => {
863
1056
  const q = e.target.value.toLowerCase();
864
1057
  const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
865
- renderReqAccordionMatrix(filtered);
1058
+ renderReqQuantitativeGrid(filtered, allFiles);
866
1059
  });
867
1060
 
868
1061
  async function loadGraphCanvas() {
@@ -909,6 +1102,22 @@ function renderDashboardHtml() {
909
1102
  });
910
1103
  }
911
1104
 
1105
+ function renderGraphScopeNotice(shownNodes, shownEdges, scope, cap) {
1106
+ const el = document.getElementById('graphScopeNotice');
1107
+ if (!el || !graphData) return;
1108
+ const totalNodes = (graphData.nodes || []).length;
1109
+ const totalEdges = (graphData.edges || []).length;
1110
+ const truncated = shownNodes < totalNodes;
1111
+ const pct = totalNodes > 0 ? ((shownNodes / totalNodes) * 100).toFixed(1) : '0.0';
1112
+ const how = scope === 'full'
1113
+ ? 'first ' + cap + ' nodes of the collection'
1114
+ : 'bounded BFS from the selected root, capped at ' + cap + ' nodes';
1115
+ el.className = truncated ? 'scope-notice scope-notice-truncated' : 'scope-notice';
1116
+ el.innerHTML = truncated
1117
+ ? '<strong>Showing ' + shownNodes + ' of ' + totalNodes + ' nodes</strong> (' + pct + '%) and ' + shownEdges + ' of ' + totalEdges + ' edges — ' + how + '. Raise <em>Max Node Cap</em> to widen the view.'
1118
+ : '<strong>Showing all ' + totalNodes + ' nodes</strong> and ' + shownEdges + ' of ' + totalEdges + ' edges.';
1119
+ }
1120
+
912
1121
  function filterGraphByScope() {
913
1122
  if (!graphData) return;
914
1123
  const scope = document.getElementById('scopeFilter').value;
@@ -959,6 +1168,10 @@ function renderDashboardHtml() {
959
1168
  filteredEdges = graphData.edges.filter(e => bSet.has(getIdStr(e.from)) && bSet.has(getIdStr(e.to)));
960
1169
  }
961
1170
 
1171
+ // The canvas draws a bounded subgraph, never the whole collection. Say so with numbers —
1172
+ // an unlabelled 30-node view of a 3543-node graph reads as "this is the system".
1173
+ renderGraphScopeNotice(filteredNodes.length, filteredEdges.length, scope, maxNodeCap);
1174
+
962
1175
  const engine = document.getElementById('layoutEngine').value;
963
1176
  if (engine === 'neo4j') {
964
1177
  renderNeo4jBloomDomainGalaxy(filteredNodes, filteredEdges);
@@ -1091,7 +1304,9 @@ function renderDashboardHtml() {
1091
1304
  .on('click', (event, d) => {
1092
1305
  event.stopPropagation();
1093
1306
  selectAndTraverseNode(getIdStr(d.target));
1094
- });
1307
+ })
1308
+ .on('mouseenter', (event, d) => showEdgePopover(event, d))
1309
+ .on('mouseleave', hidePopover);
1095
1310
 
1096
1311
  const nodeGroups = svgG.append('g')
1097
1312
  .selectAll('g')
@@ -1139,7 +1354,7 @@ function renderDashboardHtml() {
1139
1354
 
1140
1355
  nodeGroups
1141
1356
  .on('click', (event, d) => selectAndTraverseNode(d.id))
1142
- .on('mouseenter', (event, d) => showPopover(event, d, validEdges))
1357
+ .on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
1143
1358
  .on('mouseleave', hidePopover);
1144
1359
 
1145
1360
  simulation.on('tick', () => {
@@ -1264,6 +1479,8 @@ function renderDashboardHtml() {
1264
1479
  evt.stopPropagation();
1265
1480
  selectAndTraverseNode(d.to);
1266
1481
  });
1482
+ pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
1483
+ pathEl.addEventListener('mouseleave', hidePopover);
1267
1484
 
1268
1485
  edgeGroup.node().appendChild(pathEl);
1269
1486
  });
@@ -1301,7 +1518,7 @@ function renderDashboardHtml() {
1301
1518
 
1302
1519
  nodeGroups
1303
1520
  .on('click', (event, d) => selectAndTraverseNode(d.id))
1304
- .on('mouseenter', (event, d) => showPopover(event, d, validEdges))
1521
+ .on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
1305
1522
  .on('mouseleave', hidePopover);
1306
1523
  }
1307
1524
 
@@ -1416,6 +1633,8 @@ function renderDashboardHtml() {
1416
1633
  evt.stopPropagation();
1417
1634
  selectAndTraverseNode(d.to);
1418
1635
  });
1636
+ pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
1637
+ pathEl.addEventListener('mouseleave', hidePopover);
1419
1638
 
1420
1639
  edgeGroup.node().appendChild(pathEl);
1421
1640
  });
@@ -1451,7 +1670,7 @@ function renderDashboardHtml() {
1451
1670
 
1452
1671
  nodeGroups
1453
1672
  .on('click', (event, d) => selectAndTraverseNode(d.id))
1454
- .on('mouseenter', (event, d) => showPopover(event, d, validEdges))
1673
+ .on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
1455
1674
  .on('mouseleave', hidePopover);
1456
1675
  }
1457
1676
 
@@ -1566,6 +1785,8 @@ function renderDashboardHtml() {
1566
1785
  evt.stopPropagation();
1567
1786
  selectAndTraverseNode(d.to);
1568
1787
  });
1788
+ pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
1789
+ pathEl.addEventListener('mouseleave', hidePopover);
1569
1790
 
1570
1791
  edgeGroup.node().appendChild(pathEl);
1571
1792
  });
@@ -1600,7 +1821,7 @@ function renderDashboardHtml() {
1600
1821
 
1601
1822
  nodeGroups
1602
1823
  .on('click', (event, d) => selectAndTraverseNode(d.id))
1603
- .on('mouseenter', (event, d) => showPopover(event, d, validEdges))
1824
+ .on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
1604
1825
  .on('mouseleave', hidePopover);
1605
1826
  }
1606
1827
 
@@ -1735,15 +1956,86 @@ function renderDashboardHtml() {
1735
1956
  container.innerHTML = html;
1736
1957
  }
1737
1958
 
1738
- function showPopover(event, node, edges) {
1959
+ // World Top-Tier High-Density Rich Node Tooltip Hover Popover Card
1960
+ function showNodePopover(event, node, edges) {
1739
1961
  const card = document.getElementById('popoverCard');
1962
+ const badge = document.getElementById('popoverBadge');
1963
+ const title = document.getElementById('popoverTitle');
1964
+ const sec1 = document.getElementById('popoverSection1');
1965
+ const sec2 = document.getElementById('popoverSection2');
1966
+
1740
1967
  const nodeIdStr = getIdStr(node.id || node);
1741
- document.getElementById('popoverTitle').innerText = nodeIdStr;
1742
- document.getElementById('popoverType').innerText = 'Type: ' + (node.kind || node.type);
1743
- document.getElementById('popoverStatus').innerText = 'Status: ' + (node.covered ? 'COVERED' : 'ACTIVE / UNCOVERED');
1744
-
1745
- const degrees = edges.filter(e => getIdStr(e.from || e.source) === nodeIdStr || getIdStr(e.to || e.target) === nodeIdStr).length;
1746
- document.getElementById('popoverStats').innerText = 'Connected Edges: ' + degrees + ' relationships';
1968
+ const cat = getNodeCategory(nodeIdStr);
1969
+
1970
+ let badgeBg = '#3b82f6';
1971
+ let badgeLabel = 'BLUEPRINT SPEC';
1972
+ if (cat === 'REQ') { badgeBg = '#ec4899'; badgeLabel = '🟣 REQ BUSINESS INTENT'; }
1973
+ else if (cat === 'H-SPEC') { badgeBg = '#8b5cf6'; badgeLabel = '🟣 H-SPEC FUNCTIONAL'; }
1974
+ else if (cat === 'A-SPEC') { badgeBg = '#3b82f6'; badgeLabel = '🔵 A-SPEC ARCHITECTURE'; }
1975
+ else if (cat === 'T-SPEC') { badgeBg = '#06b6d4'; badgeLabel = '🔷 T-SPEC VERIFICATION'; }
1976
+ else if (cat === 'SYMBOL') { badgeBg = '#f59e0b'; badgeLabel = '⚡ AST CPG SYMBOL'; }
1977
+ else { badgeBg = '#10b981'; badgeLabel = '📄 SOURCE CODE FILE'; }
1978
+
1979
+ badge.style.background = badgeBg;
1980
+ badge.innerText = badgeLabel;
1981
+
1982
+ title.innerText = nodeIdStr;
1983
+
1984
+ const inEdges = edges.filter(e => getIdStr(e.to || e.target) === nodeIdStr);
1985
+ const outEdges = edges.filter(e => getIdStr(e.from || e.source) === nodeIdStr);
1986
+ const totalDegree = inEdges.length + outEdges.length;
1987
+
1988
+ sec1.innerHTML = \`
1989
+ <div><span class="popover-label">Full Label / Title:</span> <span class="popover-value">\${escapeHtml(node.label || nodeIdStr)}</span></div>
1990
+ <div><span class="popover-label">Status & Audit:</span> <span class="popover-value" style="font-weight:bold; color:\${node.covered ? '#34d399' : '#f87171'}">\${node.covered ? 'APPROVED / COVERED' : 'ACTIVE'}</span></div>
1991
+ \`;
1992
+
1993
+ sec2.innerHTML = \`
1994
+ <div><span class="popover-label">Degree Centrality Rank:</span> <span class="popover-value" style="font-weight:bold; color:#fbbf24;">\${totalDegree} Relationships</span></div>
1995
+ <div><span class="popover-label">Inbound Parents (In-Degree):</span> <span class="popover-value">\${inEdges.length} edges</span></div>
1996
+ <div><span class="popover-label">Outbound Children (Out-Degree):</span> <span class="popover-value">\${outEdges.length} edges</span></div>
1997
+ \${cat === 'SYMBOL' ? '<div><span class="popover-label">Symbol Kind:</span> <span class="popover-value" style="color:#f59e0b;">Function / Method AST Node</span></div>' : ''}
1998
+ \${cat === 'FILE' ? '<div><span class="popover-label">File Path:</span> <span class="popover-value" style="color:#10b981;">' + escapeHtml(nodeIdStr) + '</span></div>' : ''}
1999
+ \`;
2000
+
2001
+ card.classList.add('active');
2002
+ }
2003
+
2004
+ // World Top-Tier High-Density Rich Edge Tooltip Hover Popover Card
2005
+ function showEdgePopover(event, edge) {
2006
+ const card = document.getElementById('popoverCard');
2007
+ const badge = document.getElementById('popoverBadge');
2008
+ const title = document.getElementById('popoverTitle');
2009
+ const sec1 = document.getElementById('popoverSection1');
2010
+ const sec2 = document.getElementById('popoverSection2');
2011
+
2012
+ const src = getIdStr(edge.from || edge.source);
2013
+ const dst = getIdStr(edge.to || edge.target);
2014
+ const rel = edge.rel || 'edge';
2015
+
2016
+ let badgeBg = '#3b82f6';
2017
+ let relExplanation = '';
2018
+ if (rel === 'implements') { badgeBg = '#3b82f6'; relExplanation = 'Code/File implements Architecture Specification'; }
2019
+ else if (rel === 'verifies') { badgeBg = '#06b6d4'; relExplanation = 'Test Spec verifies Code/Architecture Specification'; }
2020
+ else if (rel === 'depends_on') { badgeBg = '#8b5cf6'; relExplanation = 'Specification depends on parent Specification'; }
2021
+ else if (rel === 'contains') { badgeBg = '#10b981'; relExplanation = 'Source Code File contains AST Function Symbol'; }
2022
+ else if (rel === 'calls') { badgeBg = '#f59e0b'; relExplanation = 'AST Symbol calls destination Function Symbol (CPG Dataflow)'; }
2023
+
2024
+ badge.style.background = badgeBg;
2025
+ badge.innerText = '➔ EDGE RELATION: ' + rel.toUpperCase();
2026
+
2027
+ title.innerText = src + ' ➔ ' + dst;
2028
+
2029
+ sec1.innerHTML = \`
2030
+ <div><span class="popover-label">Edge Relation Type:</span> <span class="popover-value" style="font-weight:bold; color:#fbbf24;">\${rel}</span></div>
2031
+ <div><span class="popover-label">Causal Explanation:</span> <span class="popover-value" style="color:#38bdf8;">\${relExplanation}</span></div>
2032
+ \`;
2033
+
2034
+ sec2.innerHTML = \`
2035
+ <div><span class="popover-label">Source Node (From):</span> <span class="popover-value">\${escapeHtml(src)}</span></div>
2036
+ <div><span class="popover-label">Target Node (To):</span> <span class="popover-value">\${escapeHtml(dst)}</span></div>
2037
+ \`;
2038
+
1747
2039
  card.classList.add('active');
1748
2040
  }
1749
2041
 
@@ -1756,3 +2048,141 @@ function renderDashboardHtml() {
1756
2048
  </body>
1757
2049
  </html>`;
1758
2050
  }
2051
+ /**
2052
+ * The canonical status vocabulary. A document outside it (`Approved`, `Proposed`, `Verified`,
2053
+ * `Completed`, `Pending` — the C-SPEC/JOB corpus) has not been scored against these gates at all,
2054
+ * and scoring it 0 would report an unmapped vocabulary as an uncovered spec.
2055
+ */
2056
+ const CANONICAL_STATUSES = new Set(['draft', 'review', 'approved', 'outdated']);
2057
+ function isCanonicalStatus(status) {
2058
+ return typeof status === 'string' && CANONICAL_STATUSES.has(status);
2059
+ }
2060
+ /** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
2061
+ function specKindOf(id) {
2062
+ const m = /^([A-Za-z-]+?)-\d/.exec(String(id ?? ''));
2063
+ return m ? m[1] : 'UNKNOWN';
2064
+ }
2065
+ const STAGE_NAMES = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
2066
+ /**
2067
+ * Match a spec id against a REQ's number exactly, so `REQ-021` cannot claim `A-SPEC-0219`.
2068
+ * Accepts the plain number and dotted sub-specs (`A-SPEC-219.1`).
2069
+ */
2070
+ function specBelongsToReq(specId, kind, numPart) {
2071
+ return specId === `${kind}-${numPart}` || specId.startsWith(`${kind}-${numPart}.`);
2072
+ }
2073
+ /**
2074
+ * Attribute a finding to a REQ only on an EXACT spec reference. `specRef.includes(numPart)` let
2075
+ * REQ-021 claim a finding filed against A-SPEC-0219; substring containment is not evidence.
2076
+ */
2077
+ function findingBelongsToReq(finding, reqId, numPart) {
2078
+ const ref = typeof finding?.specRef === 'string' ? finding.specRef : null;
2079
+ if (!ref)
2080
+ return false;
2081
+ return (ref === reqId ||
2082
+ specBelongsToReq(ref, 'H-SPEC', numPart) ||
2083
+ specBelongsToReq(ref, 'A-SPEC', numPart) ||
2084
+ specBelongsToReq(ref, 'T-SPEC', numPart));
2085
+ }
2086
+ function fileBelongsToReq(implementsSpecs, reqId, numPart) {
2087
+ return implementsSpecs.some((specId) => specId === reqId ||
2088
+ specBelongsToReq(specId, 'H-SPEC', numPart) ||
2089
+ specBelongsToReq(specId, 'A-SPEC', numPart) ||
2090
+ specBelongsToReq(specId, 'T-SPEC', numPart));
2091
+ }
2092
+ function makeRow(base) {
2093
+ const stages = [
2094
+ base.reqId,
2095
+ base.hspecId ?? null,
2096
+ base.aspecId ?? null,
2097
+ base.tspecId ?? null,
2098
+ base.fileId ?? null,
2099
+ base.symbolId ?? null,
2100
+ ];
2101
+ const missingStages = STAGE_NAMES.filter((_, i) => !stages[i]);
2102
+ const stagesComplete = stages.filter(Boolean).length;
2103
+ return {
2104
+ reqId: base.reqId,
2105
+ reqTitle: base.reqTitle,
2106
+ hspecId: base.hspecId ?? null,
2107
+ aspecId: base.aspecId ?? null,
2108
+ tspecId: base.tspecId ?? null,
2109
+ fileId: base.fileId ?? null,
2110
+ symbolId: base.symbolId ?? null,
2111
+ symbolLine: base.symbolLine ?? null,
2112
+ stagesComplete,
2113
+ missingStages: [...missingStages],
2114
+ status: stagesComplete === STAGE_NAMES.length ? 'COVERED' : 'UNCOVERED',
2115
+ findingsCount: base.findingsCount ?? 0,
2116
+ criticalCount: base.criticalCount ?? 0,
2117
+ findingsScanned: base.findingsScanned ?? false,
2118
+ mutantCount: base.mutantCount ?? 0,
2119
+ mutationScore: null,
2120
+ mutationScoreMeasured: false,
2121
+ };
2122
+ }
2123
+ /**
2124
+ * Build the 6-stage pipeline matrix from the repository's real specs, scanned files and findings.
2125
+ *
2126
+ * Pure and exported so the truthfulness invariants can be asserted directly, without an HTTP round
2127
+ * trip: no row may name an artifact that the inputs do not contain, and no symbol may be dropped.
2128
+ *
2129
+ * @implements A-SPEC-219
2130
+ */
2131
+ function buildPipelineRows(specs, files, allFindings, options) {
2132
+ const findingsScanned = options?.findingsScanned === true;
2133
+ const safeSpecs = Array.isArray(specs) ? specs : [];
2134
+ const safeFiles = Array.isArray(files) ? files : [];
2135
+ const safeFindings = Array.isArray(allFindings) ? allFindings : [];
2136
+ const rows = [];
2137
+ for (const reqItem of safeSpecs.filter((s) => typeof s?.id === 'string' && s.id.startsWith('REQ-'))) {
2138
+ const numPart = reqItem.id.replace('REQ-', '').split('.')[0];
2139
+ const pick = (kind) => safeSpecs.find((s) => specBelongsToReq(String(s?.id ?? ''), kind, numPart))?.id ?? null;
2140
+ const hspecId = pick('H-SPEC');
2141
+ const aspecId = pick('A-SPEC');
2142
+ const tspecId = pick('T-SPEC');
2143
+ const matchingFiles = safeFiles.filter((f) => fileBelongsToReq(f?.implementsSpecs ?? [], reqItem.id, numPart));
2144
+ const specFindings = safeFindings.filter((f) => f?.status === 'open' && findingBelongsToReq(f, reqItem.id, numPart));
2145
+ const common = {
2146
+ reqId: reqItem.id,
2147
+ reqTitle: reqItem.title,
2148
+ hspecId,
2149
+ aspecId,
2150
+ tspecId,
2151
+ findingsScanned,
2152
+ };
2153
+ if (matchingFiles.length === 0) {
2154
+ // No implementing file: stages 5 and 6 stay empty. Nothing is substituted for them.
2155
+ rows.push(makeRow({
2156
+ ...common,
2157
+ findingsCount: specFindings.length,
2158
+ criticalCount: specFindings.filter((f) => f.severity === 'critical').length,
2159
+ }));
2160
+ continue;
2161
+ }
2162
+ for (const f of matchingFiles) {
2163
+ const fileId = f.sourcePath || f.path;
2164
+ const syms = f.symbols || [];
2165
+ const fileMutants = (0, ast_mutation_1.generateAstMutants)([f]);
2166
+ const fileFindings = safeFindings.filter((fd) => fd?.status === 'open' && (fd?.file === fileId || findingBelongsToReq(fd, reqItem.id, numPart)));
2167
+ const findingsCount = fileFindings.length;
2168
+ const criticalCount = fileFindings.filter((fd) => fd.severity === 'critical').length;
2169
+ if (syms.length === 0) {
2170
+ rows.push(makeRow({ ...common, fileId, findingsCount, criticalCount, mutantCount: fileMutants.length }));
2171
+ continue;
2172
+ }
2173
+ // Every symbol gets a row — no truncation, so the matrix count and the drill-down agree.
2174
+ for (const sym of syms) {
2175
+ rows.push(makeRow({
2176
+ ...common,
2177
+ fileId,
2178
+ symbolId: sym.name,
2179
+ symbolLine: sym.startLine ?? null,
2180
+ findingsCount,
2181
+ criticalCount,
2182
+ mutantCount: fileMutants.filter((m) => m.symbolName === sym.name).length,
2183
+ }));
2184
+ }
2185
+ }
2186
+ }
2187
+ return rows;
2188
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.1.8",
4
+ "version": "0.1.9",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",