@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 +1 -1
- package/dist/holmes/server/dashboard.d.ts +55 -0
- package/dist/holmes/server/dashboard.js +610 -180
- package/package.json +1 -1
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
|
91
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
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
|
|
414
|
-
.heatmap-wrapper { width: 100%;
|
|
415
|
-
.pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%;
|
|
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
|
-
|
|
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-
|
|
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;">🔥
|
|
496
|
-
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">
|
|
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')"
|
|
503
|
-
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥
|
|
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
|
|
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:
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
<
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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="
|
|
597
|
-
<div id="
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
757
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
|
773
|
-
function
|
|
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
|
|
785
|
-
const
|
|
786
|
-
const
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
const
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
</
|
|
804
|
-
<
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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.
|
|
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",
|