@holmes-lab/holmes-kit 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +48 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/test-platform.d.ts +25 -0
- package/dist/holmes/cli/test-platform.js +38 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
- package/dist/holmes/cpg/cpg-scanner.js +53 -1
- package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
- package/dist/holmes/cpg/forbidden-edges.js +140 -0
- package/dist/holmes/cpg/hash-cache.js +13 -5
- package/dist/holmes/cpg/language-parser-walk.js +70 -4
- package/dist/holmes/cpg/proposed-content.d.ts +51 -0
- package/dist/holmes/cpg/proposed-content.js +72 -0
- package/dist/holmes/cpg/required-calls.d.ts +62 -0
- package/dist/holmes/cpg/required-calls.js +93 -0
- package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
- package/dist/holmes/guardrail/cspec-change.js +70 -0
- package/dist/holmes/guardrail/risk-classifier.js +122 -0
- package/dist/holmes/guardrail/write-target.d.ts +42 -0
- package/dist/holmes/guardrail/write-target.js +69 -18
- package/dist/holmes/hooks/pre-tool-use.js +90 -5
- package/dist/holmes/hooks/stop.d.ts +17 -0
- package/dist/holmes/hooks/stop.js +39 -2
- package/dist/holmes/mcp/handlers.d.ts +41 -0
- package/dist/holmes/mcp/handlers.js +173 -3
- package/dist/holmes/mcp/tool-schemas.js +12 -0
- package/dist/holmes/project/dependencies.d.ts +15 -0
- package/dist/holmes/project/dependencies.js +58 -0
- package/dist/holmes/project/json-state.d.ts +24 -0
- package/dist/holmes/project/json-state.js +30 -0
- package/dist/holmes/reverse/scan.js +8 -1
- package/dist/holmes/review/scope.d.ts +29 -0
- package/dist/holmes/review/scope.js +44 -0
- package/dist/holmes/rtm/test-scope.d.ts +44 -0
- package/dist/holmes/rtm/test-scope.js +92 -2
- package/dist/holmes/server/dashboard.d.ts +77 -0
- package/dist/holmes/server/dashboard.js +703 -183
- package/dist/holmes/spec/approval-blockers.d.ts +21 -5
- package/dist/holmes/spec/approval-blockers.js +49 -6
- package/dist/holmes/spec/legacy-format.d.ts +14 -0
- package/dist/holmes/spec/legacy-format.js +15 -1
- package/dist/holmes/spec/nonfunctional.d.ts +70 -0
- package/dist/holmes/spec/nonfunctional.js +119 -0
- package/dist/holmes/spec/spec-parser.d.ts +25 -0
- package/dist/holmes/spec/spec-parser.js +46 -2
- package/dist/holmes/spec/spec-types.d.ts +4 -1
- package/dist/holmes/spec/spec-types.js +13 -1
- package/dist/holmes/testing/effects.d.ts +54 -0
- package/dist/holmes/testing/effects.js +107 -0
- package/package.json +3 -2
- package/playbooks/promote-slice/PLAYBOOK.md +20 -0
|
@@ -33,9 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SPEC_KINDS = void 0;
|
|
36
37
|
exports.startDashboardServer = startDashboardServer;
|
|
38
|
+
exports.isCanonicalStatus = isCanonicalStatus;
|
|
39
|
+
exports.activeSpecs = activeSpecs;
|
|
40
|
+
exports.specKindOf = specKindOf;
|
|
41
|
+
exports.buildPipelineRows = buildPipelineRows;
|
|
37
42
|
// @implements A-SPEC-215
|
|
38
43
|
// @implements A-SPEC-219
|
|
44
|
+
// @implements A-SPEC-222.2
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
39
46
|
const http = __importStar(require("node:http"));
|
|
40
47
|
const path = __importStar(require("node:path"));
|
|
41
48
|
const spec_store_1 = require("../spec/spec-store");
|
|
@@ -73,7 +80,9 @@ async function startDashboardServer(options) {
|
|
|
73
80
|
}
|
|
74
81
|
if (parsedUrl === '/api/rtm') {
|
|
75
82
|
try {
|
|
76
|
-
const
|
|
83
|
+
const allSpecs = await store.list();
|
|
84
|
+
const retiredSpecs = allSpecs.filter((s) => s.status === 'outdated');
|
|
85
|
+
const specs = activeSpecs(allSpecs);
|
|
77
86
|
const files = scanner.scan(root);
|
|
78
87
|
const implementedSpecIds = new Set();
|
|
79
88
|
for (const f of files) {
|
|
@@ -83,12 +92,71 @@ async function startDashboardServer(options) {
|
|
|
83
92
|
const enrichedSpecs = specs.map((s) => {
|
|
84
93
|
const isA = s.id.startsWith('A-SPEC');
|
|
85
94
|
const covered = isA ? implementedSpecIds.has(s.id) : s.status === 'approved';
|
|
86
|
-
return { ...s, covered };
|
|
95
|
+
return { ...s, covered, legacyStatus: !isCanonicalStatus(s.status) };
|
|
87
96
|
});
|
|
97
|
+
// Two axes, never one number. Approval (a spec was signed off) and implementation (code is
|
|
98
|
+
// anchored to it) answer different questions, and a spec whose status is outside the
|
|
99
|
+
// canonical vocabulary answers neither — it is excluded and counted separately instead of
|
|
100
|
+
// being silently scored 0, which read as "47 uncovered specs" when it meant "47 unmapped".
|
|
101
|
+
const canonical = enrichedSpecs.filter((s) => !s.legacyStatus && exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
102
|
+
const approvedCount = canonical.filter((s) => s.status === 'approved').length;
|
|
103
|
+
const anchorable = enrichedSpecs.filter((s) => s.id.startsWith('A-SPEC') && !s.legacyStatus);
|
|
104
|
+
const anchoredCount = anchorable.filter((s) => s.covered).length;
|
|
105
|
+
// @implements A-SPEC-219.1
|
|
106
|
+
// Two populations sit outside the axes, and merging them into one number misleads: a reader
|
|
107
|
+
// of "legacy 49" concludes 49 documents need cleaning up, when 23 of them will never become
|
|
108
|
+
// specs no matter how long anyone waits. The population that prompted this split was
|
|
109
|
+
// `06_job/JOB-*`, work orders carrying an `assignee`; those were deleted under REQ-223
|
|
110
|
+
// once measurement showed all 23 pointed at retired REQs and at 46 source paths of which
|
|
111
|
+
// none still existed. The split stays because it is the guard: the next stray document
|
|
112
|
+
// lands here instead of in the cleanup backlog.
|
|
113
|
+
const outside = enrichedSpecs.filter((s) => s.legacyStatus || !exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
114
|
+
const unmappedSpecs = outside.filter((s) => exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
115
|
+
const nonSpecDocs = outside.filter((s) => !exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
116
|
+
const tally = (rows) => {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const s of rows) {
|
|
119
|
+
const kind = specKindOf(s.id);
|
|
120
|
+
out[kind] = (out[kind] || 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
};
|
|
124
|
+
const approval = {
|
|
125
|
+
total: canonical.length,
|
|
126
|
+
approved: approvedCount,
|
|
127
|
+
pct: canonical.length > 0 ? Math.round((approvedCount / canonical.length) * 100) : 0,
|
|
128
|
+
};
|
|
129
|
+
const implementation = {
|
|
130
|
+
total: anchorable.length,
|
|
131
|
+
anchored: anchoredCount,
|
|
132
|
+
pct: anchorable.length > 0 ? Math.round((anchoredCount / anchorable.length) * 100) : 0,
|
|
133
|
+
};
|
|
134
|
+
// Reported, never silently dropped: without this the 132 excluded documents vanish and the
|
|
135
|
+
// screen cannot say where they went.
|
|
136
|
+
const retiredByKind = {};
|
|
137
|
+
for (const s of retiredSpecs) {
|
|
138
|
+
const kind = specKindOf(s.id);
|
|
139
|
+
retiredByKind[kind] = (retiredByKind[kind] || 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
// Count and kinds only. Shipping the ids would put retired documents back into the very
|
|
142
|
+
// response this slice removes them from, and nothing on the screen renders them.
|
|
143
|
+
const retired = { count: retiredSpecs.length, byKind: retiredByKind };
|
|
144
|
+
const unmapped = { count: unmappedSpecs.length, byKind: tally(unmappedSpecs) };
|
|
145
|
+
const nonSpec = { count: nonSpecDocs.length, byKind: tally(nonSpecDocs) };
|
|
88
146
|
const coveredCount = enrichedSpecs.filter((s) => s.covered).length;
|
|
89
147
|
const totalCount = enrichedSpecs.length;
|
|
90
|
-
const
|
|
91
|
-
|
|
148
|
+
const body = JSON.stringify({
|
|
149
|
+
ok: true,
|
|
150
|
+
totalCount,
|
|
151
|
+
coveredCount,
|
|
152
|
+
approval,
|
|
153
|
+
implementation,
|
|
154
|
+
unmapped,
|
|
155
|
+
nonSpec,
|
|
156
|
+
retired,
|
|
157
|
+
specs: enrichedSpecs,
|
|
158
|
+
files,
|
|
159
|
+
});
|
|
92
160
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
93
161
|
res.end(body);
|
|
94
162
|
}
|
|
@@ -103,82 +171,24 @@ async function startDashboardServer(options) {
|
|
|
103
171
|
// @implements A-SPEC-219
|
|
104
172
|
if (parsedUrl === '/api/rtm/heatmap') {
|
|
105
173
|
try {
|
|
106
|
-
|
|
174
|
+
// @implements A-SPEC-222.2 — retired documents never reach a rendered view.
|
|
175
|
+
const specs = activeSpecs(await store.list());
|
|
107
176
|
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
|
-
}
|
|
177
|
+
// FindingsLedger.list() returns [] for a missing file, which is indistinguishable from a
|
|
178
|
+
// clean scan. The dashboard therefore asks the filesystem directly: an audit lens that
|
|
179
|
+
// renders "0 open findings" over a ledger that was never written is a green wall, not a
|
|
180
|
+
// verdict.
|
|
181
|
+
const findingsPath = path.join(root, '.ax', 'ledger', 'findings.jsonl');
|
|
182
|
+
const findingsScanned = fs.existsSync(findingsPath);
|
|
183
|
+
const findingsLedger = new findings_1.FindingsLedger(findingsPath);
|
|
184
|
+
const allFindings = findingsScanned ? findingsLedger.list() : [];
|
|
185
|
+
const pipelines = buildPipelineRows(specs, files, allFindings, { findingsScanned });
|
|
179
186
|
const body = JSON.stringify({
|
|
180
187
|
ok: true,
|
|
181
188
|
pipelineCount: pipelines.length,
|
|
189
|
+
completeCount: pipelines.filter((p) => p.status === 'COVERED').length,
|
|
190
|
+
findingsScanned,
|
|
191
|
+
mutationScoreMeasured: false,
|
|
182
192
|
pipelines,
|
|
183
193
|
});
|
|
184
194
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -210,7 +220,8 @@ async function startDashboardServer(options) {
|
|
|
210
220
|
// @implements A-SPEC-219
|
|
211
221
|
if (parsedUrl === '/api/graph') {
|
|
212
222
|
try {
|
|
213
|
-
|
|
223
|
+
// @implements A-SPEC-222.2 — retired documents never reach a rendered view.
|
|
224
|
+
const specs = activeSpecs(await store.list());
|
|
214
225
|
const files = scanner.scan(root);
|
|
215
226
|
const nodes = [];
|
|
216
227
|
const edges = [];
|
|
@@ -374,7 +385,7 @@ function renderDashboardHtml() {
|
|
|
374
385
|
<html lang="en">
|
|
375
386
|
<head>
|
|
376
387
|
<meta charset="UTF-8">
|
|
377
|
-
<title>Holmes-Kit World Top-Tier
|
|
388
|
+
<title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
|
|
378
389
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
379
390
|
<style>
|
|
380
391
|
body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
|
|
@@ -384,18 +395,28 @@ function renderDashboardHtml() {
|
|
|
384
395
|
.tab-btn.active { background: #3b82f6; color: #ffffff; border-color: #3b82f6; }
|
|
385
396
|
.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
397
|
|
|
387
|
-
/* REQ
|
|
398
|
+
/* Quantitative REQ SDLC Health Matrix Grid Styling */
|
|
388
399
|
.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; }
|
|
400
|
+
.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
401
|
.req-card:hover { border-color: #ec4899; }
|
|
391
402
|
.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
403
|
.req-title-group { display: flex; align-items: center; gap: 12px; }
|
|
393
404
|
.req-id-badge { background: #ec4899; color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; }
|
|
394
405
|
.req-title { font-size: 15px; font-weight: bold; color: #f8fafc; }
|
|
395
|
-
|
|
396
|
-
|
|
406
|
+
|
|
407
|
+
/* 6-Stage Quantitative Metrics Bar inside REQ Header */
|
|
408
|
+
.quant-grid { display: flex; gap: 8px; align-items: center; background: #0f172a; border: 1px solid #334155; padding: 6px 12px; border-radius: 8px; font-size: 11px; }
|
|
409
|
+
.quant-badge { display: flex; align-items: center; gap: 4px; padding: 2px 6px; border-radius: 4px; font-weight: bold; }
|
|
410
|
+
.quant-hspec { background: rgba(139,92,246,0.2); color: #c084fc; border: 1px solid #8b5cf6; }
|
|
411
|
+
.quant-aspec { background: rgba(59,130,246,0.2); color: #60a5fa; border: 1px solid #3b82f6; }
|
|
412
|
+
.quant-tspec { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid #06b6d4; }
|
|
413
|
+
.quant-file { background: rgba(16,185,129,0.2); color: #34d399; border: 1px solid #10b981; }
|
|
414
|
+
.quant-symbol { background: rgba(245,158,11,0.2); color: #fbbf24; border: 1px solid #f59e0b; }
|
|
415
|
+
|
|
397
416
|
.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
417
|
.expand-btn:hover { background: #3b82f6; color: #fff; }
|
|
418
|
+
.view-graph-btn { background: #8b5cf6; color: #fff; border: none; padding: 6px 14px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 12px; }
|
|
419
|
+
.view-graph-btn:hover { background: #7c3aed; }
|
|
399
420
|
|
|
400
421
|
.accordion-body { padding: 16px 20px; display: none; background: #0f172a; border-top: 1px solid #334155; flex-direction: column; gap: 10px; }
|
|
401
422
|
.accordion-body.open { display: flex; }
|
|
@@ -410,9 +431,9 @@ function renderDashboardHtml() {
|
|
|
410
431
|
.badge-uncovered { background-color: #991b1b; color: #f87171; }
|
|
411
432
|
.stat-badge { font-size: 24px; font-weight: 800; color: #f59e0b; }
|
|
412
433
|
|
|
413
|
-
/* 6-Stage
|
|
414
|
-
.heatmap-wrapper { width: 100%;
|
|
415
|
-
.pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%;
|
|
434
|
+
/* REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Styling */
|
|
435
|
+
.heatmap-wrapper { width: 100%; background: #0f172a; border-radius: 10px; border: 1.5px solid #334155; padding: 20px; box-sizing: border-box; }
|
|
436
|
+
.pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%; }
|
|
416
437
|
.pipeline-table th, .pipeline-table td { border: 1px solid #334155; padding: 10px 12px; text-align: left; }
|
|
417
438
|
.pipeline-table th { background: #1e293b; color: #f8fafc; font-weight: bold; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
|
|
418
439
|
.pipeline-row:hover { background: #1e293b; }
|
|
@@ -479,9 +500,25 @@ function renderDashboardHtml() {
|
|
|
479
500
|
.legend-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
|
480
501
|
.controls { margin-bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
481
502
|
.select-box { padding: 8px 12px; border-radius: 6px; border: 1px solid #334155; background: #1e293b; color: #fff; font-size: 13px; }
|
|
482
|
-
|
|
503
|
+
|
|
504
|
+
.axis-stats { display: flex; align-items: center; gap: 18px; font-size: 13px; color: #94a3b8; }
|
|
505
|
+
.axis-stat { display: inline-flex; align-items: center; gap: 6px; }
|
|
506
|
+
.legacy-chip { cursor: pointer; border: 1px dashed #64748b; border-radius: 999px; padding: 4px 10px; }
|
|
507
|
+
.legacy-chip:hover { border-color: #f59e0b; color: #f59e0b; }
|
|
508
|
+
.nonspec-chip { cursor: pointer; border: 1px solid #334155; border-radius: 999px; padding: 4px 10px; color: #64748b; }
|
|
509
|
+
.nonspec-chip:hover { border-color: #94a3b8; color: #94a3b8; }
|
|
510
|
+
.scope-notice { font-size: 12px; color: #94a3b8; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
|
|
511
|
+
.scope-notice-truncated { color: #fbbf24; border-color: #b45309; border-style: dashed; }
|
|
512
|
+
.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); }
|
|
513
|
+
.badge-unmeasured { background: #334155; color: #cbd5e1; border: 1px dashed #64748b; }
|
|
514
|
+
/* World Top-Tier High-Density Rich Tooltip Popover Card Styling */
|
|
515
|
+
.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
516
|
.popover-card.active { opacity: 1; pointer-events: auto; }
|
|
484
|
-
.popover-
|
|
517
|
+
.popover-badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-weight: 800; font-size: 11px; color: #fff; margin-bottom: 8px; }
|
|
518
|
+
.popover-title { font-weight: bold; font-size: 15px; color: #f8fafc; margin-bottom: 6px; word-break: break-all; }
|
|
519
|
+
.popover-section { margin-top: 10px; padding-top: 8px; border-top: 1px solid #334155; display: flex; flex-direction: column; gap: 4px; }
|
|
520
|
+
.popover-label { color: #94a3b8; font-size: 11px; font-weight: bold; text-transform: uppercase; }
|
|
521
|
+
.popover-value { color: #cbd5e1; font-size: 12px; }
|
|
485
522
|
|
|
486
523
|
.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
524
|
.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 +529,70 @@ function renderDashboardHtml() {
|
|
|
492
529
|
<body>
|
|
493
530
|
<div class="header">
|
|
494
531
|
<div>
|
|
495
|
-
<h1 style="margin:0; font-size: 24px;">🔥
|
|
496
|
-
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">
|
|
532
|
+
<h1 style="margin:0; font-size: 24px;">🔥 REQ-Grouped Pipeline Matrix & AST/CPG Canvas</h1>
|
|
533
|
+
<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>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="axis-stats">
|
|
536
|
+
<span class="axis-stat" title="Specs signed off, over specs using the canonical status vocabulary">📋 Approval <span id="approvalStat" class="stat-badge">--%</span></span>
|
|
537
|
+
<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>
|
|
538
|
+
<span class="axis-stat legacy-chip" id="unmappedChip" title="Spec documents whose status is outside draft|review|approved|outdated — these are the cleanup backlog" onclick="switchTab('legacy')">🏷️ <span id="unmappedStat">--</span></span>
|
|
539
|
+
<span class="axis-stat nonspec-chip" id="nonSpecChip" title="Documents in the spec store that are not a governed spec kind — never a cleanup target" onclick="switchTab('legacy')">📋 <span id="nonSpecStat">--</span></span>
|
|
497
540
|
</div>
|
|
498
|
-
<div>Specification Coverage: <span id="coverageStat" class="stat-badge">--%</span></div>
|
|
499
541
|
</div>
|
|
500
542
|
|
|
501
543
|
<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')">🔥
|
|
544
|
+
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">📊 Quantitative REQ RTM Matrix Grid</button>
|
|
545
|
+
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 REQ-Grouped Pipeline Heatmap (REQ ➔ AST)</button>
|
|
504
546
|
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">🕸️ End-to-End Human-Insight Pipeline Canvas</button>
|
|
547
|
+
<button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">🏷️ Unmapped Specs (C-SPEC · JOB)</button>
|
|
505
548
|
</div>
|
|
506
549
|
|
|
507
550
|
<div id="rtmTab">
|
|
508
551
|
<div class="ux-hint-banner">
|
|
509
|
-
<span>💡 <strong>REQ
|
|
552
|
+
<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
553
|
</div>
|
|
511
554
|
<input type="text" id="searchInput" class="search-box" placeholder="Search business REQ requirements by ID or title..." />
|
|
512
555
|
<div id="specGrid" class="rtm-container">Loading REQ specifications...</div>
|
|
513
556
|
</div>
|
|
514
557
|
|
|
558
|
+
<div id="legacyTab" style="display: none;">
|
|
559
|
+
<div class="ux-hint-banner" style="border-left-color: #f59e0b;">
|
|
560
|
+
<span>🏷️ <strong>Cleanup backlog:</strong> spec documents whose <code>status</code> falls outside <code>draft | review | approved | outdated</code>, so no governance gate has scored them. Each is waiting for <code>spec_upgrade</code> → <code>spec_retire</code>, or a rewrite into the current format. Excluded from both headline axes — an unmapped vocabulary is not an uncovered spec.</span>
|
|
561
|
+
</div>
|
|
562
|
+
<div id="unmappedGrid" class="rtm-container">Loading…</div>
|
|
563
|
+
|
|
564
|
+
<div id="nonSpecSection" style="display: none;">
|
|
565
|
+
<div class="ux-hint-banner" style="border-left-color: #334155; margin-top: 28px;">
|
|
566
|
+
<span>📋 <strong>Not specifications:</strong> documents that live in the spec store but are not a governed spec kind. They are <strong>not</strong> a cleanup target and never will be, and they have always sat outside both axes. Nothing should normally appear here — <code>spec-store-boundary.test.ts</code> fails the build when it does.</span>
|
|
567
|
+
</div>
|
|
568
|
+
<div id="nonSpecGrid" class="rtm-container"></div>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
515
572
|
<div id="heatmapTab" style="display: none;">
|
|
516
573
|
<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
|
-
|
|
574
|
+
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
|
|
575
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
576
|
+
<span>🔥 <strong>3-Tier Multi-Lens:</strong></span>
|
|
577
|
+
<select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
|
|
578
|
+
<option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ ➔ AST/CPG Core Chain)</option>
|
|
579
|
+
<option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Findings Ledger — shows “not scanned” when absent)</option>
|
|
580
|
+
<option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Static AST Mutants — score unmeasured)</option>
|
|
581
|
+
</select>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
585
|
+
<span>🔍 <strong>Filter Status:</strong></span>
|
|
586
|
+
<select id="pipelineFilterSelect" class="select-box" style="border-color: #38bdf8; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
|
|
587
|
+
<option value="all" selected>All REQ Pipelines</option>
|
|
588
|
+
<option value="uncovered">Only Uncovered / Active Traces</option>
|
|
589
|
+
<option value="findings">Only Traces with Open Findings</option>
|
|
590
|
+
</select>
|
|
591
|
+
</div>
|
|
524
592
|
</div>
|
|
525
593
|
</div>
|
|
526
594
|
<div class="heatmap-wrapper">
|
|
527
|
-
<div id="heatmapContainer">Loading 6-stage pipeline heatmap...</div>
|
|
595
|
+
<div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
|
|
528
596
|
</div>
|
|
529
597
|
</div>
|
|
530
598
|
|
|
@@ -565,6 +633,7 @@ function renderDashboardHtml() {
|
|
|
565
633
|
</select>
|
|
566
634
|
</div>
|
|
567
635
|
|
|
636
|
+
<div id="graphScopeNotice" class="scope-notice"></div>
|
|
568
637
|
<div id="graphCanvasContainer">
|
|
569
638
|
<svg id="graphSvg" class="graph-svg"></svg>
|
|
570
639
|
|
|
@@ -591,11 +660,12 @@ function renderDashboardHtml() {
|
|
|
591
660
|
<button class="tool-btn" onclick="resetZoom()" title="Reset Zoom to 100%">🔄</button>
|
|
592
661
|
</div>
|
|
593
662
|
|
|
663
|
+
<!-- World Top-Tier Rich Hover Popover Card -->
|
|
594
664
|
<div id="popoverCard" class="popover-card">
|
|
665
|
+
<div id="popoverBadge" class="popover-badge">--</div>
|
|
595
666
|
<div id="popoverTitle" class="popover-title">--</div>
|
|
596
|
-
<div id="
|
|
597
|
-
<div id="
|
|
598
|
-
<div id="popoverStats" style="color:#cbd5e1; font-size:11px;">--</div>
|
|
667
|
+
<div id="popoverSection1" class="popover-section"></div>
|
|
668
|
+
<div id="popoverSection2" class="popover-section"></div>
|
|
599
669
|
</div>
|
|
600
670
|
</div>
|
|
601
671
|
<div class="legend">
|
|
@@ -645,6 +715,7 @@ function renderDashboardHtml() {
|
|
|
645
715
|
}
|
|
646
716
|
|
|
647
717
|
let allSpecs = [];
|
|
718
|
+
let allFiles = [];
|
|
648
719
|
let graphData = null;
|
|
649
720
|
let heatmapData = null;
|
|
650
721
|
let currentZoom = null;
|
|
@@ -652,14 +723,73 @@ function renderDashboardHtml() {
|
|
|
652
723
|
let traversalHistory = [];
|
|
653
724
|
let selectedRootNodeId = 'A-SPEC-219';
|
|
654
725
|
|
|
726
|
+
// Specs excluded from both axes get their own surface. Previously 47 documents were invisible
|
|
727
|
+
// on every tab while still sitting in the headline denominator, which read as 0% coverage.
|
|
728
|
+
// @implements A-SPEC-219.1 — two populations, two destinations. Summing them is what misled:
|
|
729
|
+
// "49" read as "49 to clean up" while 23 of them will never become specs.
|
|
730
|
+
const SPEC_KINDS_UI = ['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC'];
|
|
731
|
+
const kindOfId = (id) => (String(id).match(/^([A-Za-z-]+?)-\d/) || [null, 'UNKNOWN'])[1];
|
|
732
|
+
|
|
733
|
+
function renderUnmappedGrid(specs) {
|
|
734
|
+
const outside = specs.filter(s => s.legacyStatus || SPEC_KINDS_UI.indexOf(kindOfId(s.id)) === -1);
|
|
735
|
+
renderDocGroup(
|
|
736
|
+
document.getElementById('unmappedGrid'),
|
|
737
|
+
outside.filter(s => SPEC_KINDS_UI.indexOf(kindOfId(s.id)) !== -1),
|
|
738
|
+
'Every spec uses the canonical status vocabulary.',
|
|
739
|
+
);
|
|
740
|
+
// @implements A-SPEC-223 — an absent category is not explained. T-SPEC-219.1 asked for this
|
|
741
|
+
// as a boundary case and it was never implemented; the store is now empty of such documents,
|
|
742
|
+
// so the banner would describe something that does not exist.
|
|
743
|
+
const nonSpec = outside.filter(s => SPEC_KINDS_UI.indexOf(kindOfId(s.id)) === -1);
|
|
744
|
+
const section = document.getElementById('nonSpecSection');
|
|
745
|
+
if (section) section.style.display = nonSpec.length > 0 ? 'block' : 'none';
|
|
746
|
+
renderDocGroup(document.getElementById('nonSpecGrid'), nonSpec, '');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderDocGroup(container, rows, emptyMessage) {
|
|
750
|
+
if (!container) return;
|
|
751
|
+
const legacy = rows;
|
|
752
|
+
if (legacy.length === 0) {
|
|
753
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">' + emptyMessage + '</div>';
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const byKind = new Map();
|
|
757
|
+
legacy.forEach(s => {
|
|
758
|
+
const kind = (s.id.match(/^([A-Za-z-]+?)-\d/) || [null, 'UNKNOWN'])[1];
|
|
759
|
+
if (!byKind.has(kind)) byKind.set(kind, []);
|
|
760
|
+
byKind.get(kind).push(s);
|
|
761
|
+
});
|
|
762
|
+
let html = '';
|
|
763
|
+
byKind.forEach((items, kind) => {
|
|
764
|
+
const statuses = {};
|
|
765
|
+
items.forEach(s => { statuses[s.status] = (statuses[s.status] || 0) + 1; });
|
|
766
|
+
const statusSummary = Object.keys(statuses).map(k => escapeHtml(k) + ' × ' + statuses[k]).join(' · ');
|
|
767
|
+
html += '<div class="req-card" style="margin-bottom:20px;">'
|
|
768
|
+
+ '<div class="req-header" onclick="toggleAccordion(\'legacy-body-' + escapeHtml(kind) + '\')">'
|
|
769
|
+
+ '<div class="req-title-group"><span class="req-id-badge">' + escapeHtml(kind) + '</span>'
|
|
770
|
+
+ '<span class="req-title">' + items.length + ' unmapped document' + (items.length === 1 ? '' : 's') + '</span>'
|
|
771
|
+
+ '<span class="neighbor-rel badge-unmeasured">' + statusSummary + '</span></div>'
|
|
772
|
+
+ '<button class="expand-btn">▶ Expand</button></div>'
|
|
773
|
+
+ '<div class="accordion-body open" id="legacy-body-' + escapeHtml(kind) + '">'
|
|
774
|
+
+ items.map(s =>
|
|
775
|
+
'<div class="tree-row tree-row-file"><div class="tree-label">🏷️ <strong>' + escapeHtml(s.id) + '</strong>: ' + escapeHtml(s.title || '') + '</div>'
|
|
776
|
+
+ '<div><span class="neighbor-rel badge-unmeasured" title="Outside draft|review|approved|outdated">status: ' + escapeHtml(String(s.status)) + '</span></div></div>'
|
|
777
|
+
).join('')
|
|
778
|
+
+ '</div></div>';
|
|
779
|
+
});
|
|
780
|
+
container.innerHTML = html;
|
|
781
|
+
}
|
|
782
|
+
|
|
655
783
|
function switchTab(tab) {
|
|
656
784
|
document.getElementById('tabRtmBtn').classList.toggle('active', tab === 'rtm');
|
|
657
785
|
document.getElementById('tabHeatmapBtn').classList.toggle('active', tab === 'heatmap');
|
|
658
786
|
document.getElementById('tabGraphBtn').classList.toggle('active', tab === 'graph');
|
|
787
|
+
document.getElementById('tabLegacyBtn').classList.toggle('active', tab === 'legacy');
|
|
659
788
|
|
|
660
789
|
document.getElementById('rtmTab').style.display = tab === 'rtm' ? 'block' : 'none';
|
|
661
790
|
document.getElementById('heatmapTab').style.display = tab === 'heatmap' ? 'block' : 'none';
|
|
662
791
|
document.getElementById('graphTab').style.display = tab === 'graph' ? 'block' : 'none';
|
|
792
|
+
document.getElementById('legacyTab').style.display = tab === 'legacy' ? 'block' : 'none';
|
|
663
793
|
|
|
664
794
|
if (tab === 'graph' && !graphData) {
|
|
665
795
|
loadGraphCanvas();
|
|
@@ -674,8 +804,21 @@ function renderDashboardHtml() {
|
|
|
674
804
|
const data = await res.json();
|
|
675
805
|
if (data.ok) {
|
|
676
806
|
allSpecs = data.specs;
|
|
677
|
-
|
|
678
|
-
|
|
807
|
+
allFiles = data.files || [];
|
|
808
|
+
const ap = data.approval || { pct: 0, approved: 0, total: 0 };
|
|
809
|
+
const im = data.implementation || { pct: 0, anchored: 0, total: 0 };
|
|
810
|
+
const um = data.unmapped || { count: 0, byKind: {} };
|
|
811
|
+
const ns = data.nonSpec || { count: 0, byKind: {} };
|
|
812
|
+
document.getElementById('approvalStat').innerText = ap.pct + '% (' + ap.approved + '/' + ap.total + ')';
|
|
813
|
+
document.getElementById('implStat').innerText = im.pct + '% (' + im.anchored + '/' + im.total + ')';
|
|
814
|
+
const umKinds = Object.keys(um.byKind || {}).map(k => k + ' ' + um.byKind[k]).join(' · ');
|
|
815
|
+
const nsKinds = Object.keys(ns.byKind || {}).map(k => k + ' ' + ns.byKind[k]).join(' · ');
|
|
816
|
+
document.getElementById('unmappedStat').innerText = um.count + ' to clean up' + (umKinds ? ' (' + umKinds + ')' : '');
|
|
817
|
+
const nsChip = document.getElementById('nonSpecChip');
|
|
818
|
+
if (nsChip) nsChip.style.display = ns.count > 0 ? 'inline-flex' : 'none';
|
|
819
|
+
document.getElementById('nonSpecStat').innerText = ns.count + ' not specs' + (nsKinds ? ' (' + nsKinds + ')' : '');
|
|
820
|
+
renderUnmappedGrid(data.specs || []);
|
|
821
|
+
renderReqQuantitativeGrid(allSpecs, allFiles);
|
|
679
822
|
}
|
|
680
823
|
} catch (err) {
|
|
681
824
|
document.getElementById('specGrid').innerText = 'Failed to load specs: ' + escapeHtml(err.message || err);
|
|
@@ -701,76 +844,148 @@ function renderDashboardHtml() {
|
|
|
701
844
|
}
|
|
702
845
|
}
|
|
703
846
|
|
|
704
|
-
//
|
|
847
|
+
// Render one pipeline stage cell. A stage with no artifact renders as an inert “missing” cell
|
|
848
|
+
// instead of a clickable id, so an empty stage can never be mistaken for a traced one.
|
|
849
|
+
function stageCell(cls, icon, label, jumpId, withArrow) {
|
|
850
|
+
const arrow = withArrow ? '<span class="arrow-sep">➔</span> ' : '';
|
|
851
|
+
if (!label) {
|
|
852
|
+
return '<td class="' + cls + ' stage-missing" title="No artifact for this stage">' + arrow + '— missing</td>';
|
|
853
|
+
}
|
|
854
|
+
const onclick = jumpId ? ' onclick="focusSpecInGraph(\'' + escapeHtml(String(jumpId)) + '\')"' : '';
|
|
855
|
+
return '<td class="' + cls + '"' + onclick + '>' + arrow + icon + ' ' + escapeHtml(String(label)) + '</td>';
|
|
856
|
+
}
|
|
857
|
+
// REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Renderer
|
|
705
858
|
function render6StagePipelineMatrix(data) {
|
|
706
859
|
const container = document.getElementById('heatmapContainer');
|
|
707
|
-
const
|
|
860
|
+
const rawPipelines = data.pipelines || [];
|
|
708
861
|
const lens = document.getElementById('lensSelect').value || 'lens1';
|
|
862
|
+
const statusFilter = document.getElementById('pipelineFilterSelect').value || 'all';
|
|
709
863
|
|
|
710
|
-
if (
|
|
864
|
+
if (rawPipelines.length === 0) {
|
|
711
865
|
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
|
|
712
866
|
return;
|
|
713
867
|
}
|
|
714
868
|
|
|
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
|
-
\`;
|
|
869
|
+
// Filter pipelines based on user status selection
|
|
870
|
+
let pipelines = rawPipelines;
|
|
871
|
+
if (statusFilter === 'uncovered') {
|
|
872
|
+
pipelines = rawPipelines.filter(p => p.status !== 'COVERED');
|
|
873
|
+
} else if (statusFilter === 'findings') {
|
|
874
|
+
pipelines = rawPipelines.filter(p => p.findingsCount > 0);
|
|
875
|
+
}
|
|
734
876
|
|
|
877
|
+
// Group pipelines by reqId
|
|
878
|
+
const groupedMap = new Map();
|
|
735
879
|
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
|
-
}
|
|
880
|
+
if (!groupedMap.has(p.reqId)) {
|
|
881
|
+
groupedMap.set(p.reqId, { reqId: p.reqId, reqTitle: p.reqTitle, items: [] });
|
|
753
882
|
}
|
|
883
|
+
groupedMap.get(p.reqId).items.push(p);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
if (groupedMap.size === 0) {
|
|
887
|
+
const unscanned = statusFilter === 'findings' && data.findingsScanned === false;
|
|
888
|
+
container.innerHTML = unscanned
|
|
889
|
+
? '<div style="color:#fbbf24; font-size:14px; padding:16px; border:1px dashed #64748b; border-radius:8px;">⚪ No findings ledger exists (<code>.ax/ledger/findings.jsonl</code>). This is <strong>not</strong> a clean result — no audit has been run.</div>'
|
|
890
|
+
: '<div style="color:#94a3b8; font-size:14px; padding:16px;">No pipelines match the selected filter.</div>';
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let extraHeader = 'Status';
|
|
895
|
+
if (lens === 'lens2') extraHeader = heatmapData && heatmapData.findingsScanned === false ? '🛡️ Security Audit (never scanned)' : '🛡️ Live Security Findings';
|
|
896
|
+
else if (lens === 'lens3') extraHeader = '🧪 Static AST Mutants (score unmeasured)';
|
|
897
|
+
|
|
898
|
+
let html = '';
|
|
899
|
+
|
|
900
|
+
groupedMap.forEach((group, reqId) => {
|
|
901
|
+
const items = group.items;
|
|
902
|
+
const totalItems = items.length;
|
|
903
|
+
const coveredItems = items.filter(i => i.status === 'COVERED').length;
|
|
904
|
+
const isReqHealthy = coveredItems === totalItems;
|
|
905
|
+
|
|
906
|
+
const reqStatusBadge = isReqHealthy
|
|
907
|
+
? '<span class="neighbor-rel badge-covered">🟢 100% HEALTHY TRACE</span>'
|
|
908
|
+
: '<span class="neighbor-rel badge-uncovered">🔴 CONTAINS UNCOVERED TRACES</span>';
|
|
909
|
+
|
|
910
|
+
html += \`
|
|
911
|
+
<div class="req-card" style="margin-bottom: 20px;">
|
|
912
|
+
<div class="req-header" onclick="toggleAccordion('pipe-body-\${escapeHtml(reqId)}')">
|
|
913
|
+
<div class="req-title-group">
|
|
914
|
+
<span class="req-id-badge">\${escapeHtml(reqId)}</span>
|
|
915
|
+
<span class="req-title">\${escapeHtml(group.reqTitle || 'Business Requirement Pipeline')}</span>
|
|
916
|
+
\${reqStatusBadge}
|
|
917
|
+
</div>
|
|
918
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
919
|
+
<span style="font-size: 12px; color: #94a3b8; font-weight: bold;">\${totalItems} End-to-End Trace Chain\${totalItems > 1 ? 's' : ''}</span>
|
|
920
|
+
<button class="expand-btn">▶ Expand Pipeline Matrix</button>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
|
|
924
|
+
<div class="accordion-body open" id="pipe-body-\${escapeHtml(reqId)}" style="padding:0;">
|
|
925
|
+
<table class="pipeline-table">
|
|
926
|
+
<thead>
|
|
927
|
+
<tr>
|
|
928
|
+
<th>Stage 1: REQ Business</th>
|
|
929
|
+
<th>Stage 2: H-SPEC Functional</th>
|
|
930
|
+
<th>Stage 3: A-SPEC Architecture</th>
|
|
931
|
+
<th>Stage 4: T-SPEC Verification</th>
|
|
932
|
+
<th>Stage 5: Source Code File</th>
|
|
933
|
+
<th>Stage 6: AST / CPG Symbol</th>
|
|
934
|
+
<th>\${extraHeader}</th>
|
|
935
|
+
</tr>
|
|
936
|
+
</thead>
|
|
937
|
+
<tbody>
|
|
938
|
+
\`;
|
|
939
|
+
|
|
940
|
+
items.forEach(p => {
|
|
941
|
+
const missing = p.missingStages || [];
|
|
942
|
+
let statusBadge = missing.length === 0
|
|
943
|
+
? '<span class="neighbor-rel badge-covered">COVERED (6/6)</span>'
|
|
944
|
+
: \`<span class="neighbor-rel badge-uncovered" title="Missing: \${escapeHtml(missing.join(', '))}">UNCOVERED (\${p.stagesComplete}/6)</span>\`;
|
|
945
|
+
|
|
946
|
+
if (lens === 'lens2') {
|
|
947
|
+
if (!p.findingsScanned) {
|
|
948
|
+
statusBadge = '<span class="neighbor-rel badge-unmeasured" title="No .ax/ledger/findings.jsonl — this chain has never been audited">⚪ Not scanned</span>';
|
|
949
|
+
} else if (p.criticalCount > 0) {
|
|
950
|
+
statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
|
|
951
|
+
} else if (p.findingsCount > 0) {
|
|
952
|
+
statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
|
|
953
|
+
} else {
|
|
954
|
+
statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
|
|
955
|
+
}
|
|
956
|
+
} else if (lens === 'lens3') {
|
|
957
|
+
// A score requires running the suite once per mutant; this endpoint only generates them
|
|
958
|
+
// statically, so it reports the mutant count and says the score is unmeasured.
|
|
959
|
+
const mutantCount = p.mutantCount || 0;
|
|
960
|
+
statusBadge = \`<span class="neighbor-rel badge-unmeasured" title="Static generation only — run mutation testing to obtain a score">🧪 \${mutantCount} Mutant\${mutantCount === 1 ? '' : 's'} · Score not measured</span>\`;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
html += \`
|
|
964
|
+
<tr class="pipeline-row">
|
|
965
|
+
\${stageCell('stage-req', '🟣', p.reqId, p.reqId, false)}
|
|
966
|
+
\${stageCell('stage-hspec', '🟣', p.hspecId, p.hspecId, true)}
|
|
967
|
+
\${stageCell('stage-aspec', '🔵', p.aspecId, p.aspecId, true)}
|
|
968
|
+
\${stageCell('stage-tspec', '🔷', p.tspecId, p.tspecId, true)}
|
|
969
|
+
\${stageCell('stage-file', '📄', p.fileId ? (p.fileId.split('/').pop() || p.fileId) : null, p.fileId, true)}
|
|
970
|
+
\${stageCell('stage-symbol', '⚡', p.symbolId, p.fileId && p.symbolId ? p.fileId + '#' + p.symbolId : null, true)}
|
|
971
|
+
<td>\${statusBadge}</td>
|
|
972
|
+
</tr>
|
|
973
|
+
\`;
|
|
974
|
+
});
|
|
754
975
|
|
|
755
976
|
html += \`
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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>
|
|
977
|
+
</tbody>
|
|
978
|
+
</table>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
765
981
|
\`;
|
|
766
982
|
});
|
|
767
983
|
|
|
768
|
-
html += '</tbody></table>';
|
|
769
984
|
container.innerHTML = html;
|
|
770
985
|
}
|
|
771
986
|
|
|
772
|
-
// World Top-Tier REQ
|
|
773
|
-
function
|
|
987
|
+
// World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
|
|
988
|
+
function renderReqQuantitativeGrid(specs, files) {
|
|
774
989
|
const container = document.getElementById('specGrid');
|
|
775
990
|
const reqList = specs.filter(s => s.id.startsWith('REQ-'));
|
|
776
991
|
|
|
@@ -781,13 +996,33 @@ function renderDashboardHtml() {
|
|
|
781
996
|
|
|
782
997
|
container.innerHTML = reqList.map(req => {
|
|
783
998
|
const numPart = req.id.replace('REQ-', '').split('.')[0];
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
const
|
|
999
|
+
const belongs = (id, kind) => id === kind + '-' + numPart || id.startsWith(kind + '-' + numPart + '.');
|
|
1000
|
+
const hspecs = specs.filter(s => belongs(s.id, 'H-SPEC'));
|
|
1001
|
+
const aspecs = specs.filter(s => belongs(s.id, 'A-SPEC'));
|
|
1002
|
+
const tspecs = specs.filter(s => belongs(s.id, 'T-SPEC'));
|
|
1003
|
+
|
|
1004
|
+
const matchingFiles = files.filter(f =>
|
|
1005
|
+
f.implementsSpecs.some(specId =>
|
|
1006
|
+
specId === req.id || belongs(specId, 'H-SPEC') || belongs(specId, 'A-SPEC') || belongs(specId, 'T-SPEC')
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
let totalSymbols = 0;
|
|
1011
|
+
let totalCallEdges = 0;
|
|
1012
|
+
matchingFiles.forEach(f => {
|
|
1013
|
+
totalSymbols += (f.symbols || []).length;
|
|
1014
|
+
totalCallEdges += (f.edges || []).filter(e => e.rel === 'calls').length;
|
|
1015
|
+
});
|
|
787
1016
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const
|
|
1017
|
+
// Every stage must be backed by a real artifact. Counting only specs would call a REQ with
|
|
1018
|
+
// no code "6-Stage Complete", which is the claim this grid exists to verify.
|
|
1019
|
+
const stagePresent = [true, hspecs.length > 0, aspecs.length > 0, tspecs.length > 0, matchingFiles.length > 0, totalSymbols > 0];
|
|
1020
|
+
const stageNames = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
|
|
1021
|
+
const missingStages = stageNames.filter((_, i) => !stagePresent[i]);
|
|
1022
|
+
const stagesComplete = stagePresent.filter(Boolean).length;
|
|
1023
|
+
const healthBadge = missingStages.length === 0
|
|
1024
|
+
? '<span class="neighbor-rel badge-covered" style="font-size:11px; font-weight:800;">🟢 Fully Traced (6/6 Stages)</span>'
|
|
1025
|
+
: '<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
1026
|
|
|
792
1027
|
return \`
|
|
793
1028
|
<div class="req-card" id="card-\${escapeHtml(req.id)}">
|
|
@@ -795,15 +1030,18 @@ function renderDashboardHtml() {
|
|
|
795
1030
|
<div class="req-title-group">
|
|
796
1031
|
<span class="req-id-badge">\${escapeHtml(req.id)}</span>
|
|
797
1032
|
<span class="req-title">\${escapeHtml(req.title || 'Business Requirement')}</span>
|
|
1033
|
+
\${healthBadge}
|
|
798
1034
|
</div>
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
</
|
|
804
|
-
<
|
|
1035
|
+
|
|
1036
|
+
<div style="display: flex; align-items: center; gap: 14px;">
|
|
1037
|
+
<div class="quant-grid">
|
|
1038
|
+
<span class="quant-badge quant-hspec" title="Functional Specs Count">🟣 H: \${hspecs.length}</span>
|
|
1039
|
+
<span class="quant-badge quant-aspec" title="Architecture Specs Count">🔵 A: \${aspecs.length}</span>
|
|
1040
|
+
<span class="quant-badge quant-tspec" title="Test Specs Count">🔷 T: \${tspecs.length}</span>
|
|
1041
|
+
<span class="quant-badge quant-file" title="Code Files Linked">📄 File: \${matchingFiles.length}</span>
|
|
1042
|
+
<span class="quant-badge quant-symbol" title="AST Symbols Mapped">⚡ AST: \${totalSymbols}</span>
|
|
805
1043
|
</div>
|
|
806
|
-
<button class="expand-btn">▶ Expand
|
|
1044
|
+
<button class="expand-btn">▶ Expand Matrix Tree</button>
|
|
807
1045
|
<button class="view-graph-btn" onclick="event.stopPropagation(); focusSpecInGraph('\${escapeHtml(req.id)}')">🕸️ View in Graph</button>
|
|
808
1046
|
</div>
|
|
809
1047
|
</div>
|
|
@@ -838,6 +1076,29 @@ function renderDashboardHtml() {
|
|
|
838
1076
|
</div>
|
|
839
1077
|
</div>
|
|
840
1078
|
\`).join('')}
|
|
1079
|
+
|
|
1080
|
+
\${matchingFiles.map(f => {
|
|
1081
|
+
const fId = f.sourcePath || f.path;
|
|
1082
|
+
const symList = f.symbols || [];
|
|
1083
|
+
return \`
|
|
1084
|
+
<div class="tree-row tree-row-file">
|
|
1085
|
+
<div class="tree-label">📄 <strong>\${escapeHtml(fId)}</strong> (\${symList.length} AST Symbols)</div>
|
|
1086
|
+
<div>
|
|
1087
|
+
<span class="neighbor-rel badge-covered" style="margin-right:8px;">LINKED</span>
|
|
1088
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(fId)}')">🕸️ View File</button>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
\${symList.map(sym => \`
|
|
1092
|
+
<div class="tree-row tree-row-symbol">
|
|
1093
|
+
<div class="tree-label">⚡ <strong>\${escapeHtml(sym.name)}</strong> (Line \${sym.startLine || 1})</div>
|
|
1094
|
+
<div>
|
|
1095
|
+
<span class="neighbor-rel quant-symbol" style="margin-right:8px;">AST SYMBOL</span>
|
|
1096
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(fId + '#' + sym.name)}')">⚡ View Symbol</button>
|
|
1097
|
+
</div>
|
|
1098
|
+
</div>
|
|
1099
|
+
\`).join('')}
|
|
1100
|
+
\`;
|
|
1101
|
+
}).join('')}
|
|
841
1102
|
</div>
|
|
842
1103
|
</div>
|
|
843
1104
|
\`;
|
|
@@ -862,7 +1123,7 @@ function renderDashboardHtml() {
|
|
|
862
1123
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
863
1124
|
const q = e.target.value.toLowerCase();
|
|
864
1125
|
const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
|
|
865
|
-
|
|
1126
|
+
renderReqQuantitativeGrid(filtered, allFiles);
|
|
866
1127
|
});
|
|
867
1128
|
|
|
868
1129
|
async function loadGraphCanvas() {
|
|
@@ -909,6 +1170,22 @@ function renderDashboardHtml() {
|
|
|
909
1170
|
});
|
|
910
1171
|
}
|
|
911
1172
|
|
|
1173
|
+
function renderGraphScopeNotice(shownNodes, shownEdges, scope, cap) {
|
|
1174
|
+
const el = document.getElementById('graphScopeNotice');
|
|
1175
|
+
if (!el || !graphData) return;
|
|
1176
|
+
const totalNodes = (graphData.nodes || []).length;
|
|
1177
|
+
const totalEdges = (graphData.edges || []).length;
|
|
1178
|
+
const truncated = shownNodes < totalNodes;
|
|
1179
|
+
const pct = totalNodes > 0 ? ((shownNodes / totalNodes) * 100).toFixed(1) : '0.0';
|
|
1180
|
+
const how = scope === 'full'
|
|
1181
|
+
? 'first ' + cap + ' nodes of the collection'
|
|
1182
|
+
: 'bounded BFS from the selected root, capped at ' + cap + ' nodes';
|
|
1183
|
+
el.className = truncated ? 'scope-notice scope-notice-truncated' : 'scope-notice';
|
|
1184
|
+
el.innerHTML = truncated
|
|
1185
|
+
? '<strong>Showing ' + shownNodes + ' of ' + totalNodes + ' nodes</strong> (' + pct + '%) and ' + shownEdges + ' of ' + totalEdges + ' edges — ' + how + '. Raise <em>Max Node Cap</em> to widen the view.'
|
|
1186
|
+
: '<strong>Showing all ' + totalNodes + ' nodes</strong> and ' + shownEdges + ' of ' + totalEdges + ' edges.';
|
|
1187
|
+
}
|
|
1188
|
+
|
|
912
1189
|
function filterGraphByScope() {
|
|
913
1190
|
if (!graphData) return;
|
|
914
1191
|
const scope = document.getElementById('scopeFilter').value;
|
|
@@ -959,6 +1236,10 @@ function renderDashboardHtml() {
|
|
|
959
1236
|
filteredEdges = graphData.edges.filter(e => bSet.has(getIdStr(e.from)) && bSet.has(getIdStr(e.to)));
|
|
960
1237
|
}
|
|
961
1238
|
|
|
1239
|
+
// The canvas draws a bounded subgraph, never the whole collection. Say so with numbers —
|
|
1240
|
+
// an unlabelled 30-node view of a 3543-node graph reads as "this is the system".
|
|
1241
|
+
renderGraphScopeNotice(filteredNodes.length, filteredEdges.length, scope, maxNodeCap);
|
|
1242
|
+
|
|
962
1243
|
const engine = document.getElementById('layoutEngine').value;
|
|
963
1244
|
if (engine === 'neo4j') {
|
|
964
1245
|
renderNeo4jBloomDomainGalaxy(filteredNodes, filteredEdges);
|
|
@@ -1091,7 +1372,9 @@ function renderDashboardHtml() {
|
|
|
1091
1372
|
.on('click', (event, d) => {
|
|
1092
1373
|
event.stopPropagation();
|
|
1093
1374
|
selectAndTraverseNode(getIdStr(d.target));
|
|
1094
|
-
})
|
|
1375
|
+
})
|
|
1376
|
+
.on('mouseenter', (event, d) => showEdgePopover(event, d))
|
|
1377
|
+
.on('mouseleave', hidePopover);
|
|
1095
1378
|
|
|
1096
1379
|
const nodeGroups = svgG.append('g')
|
|
1097
1380
|
.selectAll('g')
|
|
@@ -1139,7 +1422,7 @@ function renderDashboardHtml() {
|
|
|
1139
1422
|
|
|
1140
1423
|
nodeGroups
|
|
1141
1424
|
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1142
|
-
.on('mouseenter', (event, d) =>
|
|
1425
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1143
1426
|
.on('mouseleave', hidePopover);
|
|
1144
1427
|
|
|
1145
1428
|
simulation.on('tick', () => {
|
|
@@ -1264,6 +1547,8 @@ function renderDashboardHtml() {
|
|
|
1264
1547
|
evt.stopPropagation();
|
|
1265
1548
|
selectAndTraverseNode(d.to);
|
|
1266
1549
|
});
|
|
1550
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1551
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1267
1552
|
|
|
1268
1553
|
edgeGroup.node().appendChild(pathEl);
|
|
1269
1554
|
});
|
|
@@ -1301,7 +1586,7 @@ function renderDashboardHtml() {
|
|
|
1301
1586
|
|
|
1302
1587
|
nodeGroups
|
|
1303
1588
|
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1304
|
-
.on('mouseenter', (event, d) =>
|
|
1589
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1305
1590
|
.on('mouseleave', hidePopover);
|
|
1306
1591
|
}
|
|
1307
1592
|
|
|
@@ -1416,6 +1701,8 @@ function renderDashboardHtml() {
|
|
|
1416
1701
|
evt.stopPropagation();
|
|
1417
1702
|
selectAndTraverseNode(d.to);
|
|
1418
1703
|
});
|
|
1704
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1705
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1419
1706
|
|
|
1420
1707
|
edgeGroup.node().appendChild(pathEl);
|
|
1421
1708
|
});
|
|
@@ -1451,7 +1738,7 @@ function renderDashboardHtml() {
|
|
|
1451
1738
|
|
|
1452
1739
|
nodeGroups
|
|
1453
1740
|
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1454
|
-
.on('mouseenter', (event, d) =>
|
|
1741
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1455
1742
|
.on('mouseleave', hidePopover);
|
|
1456
1743
|
}
|
|
1457
1744
|
|
|
@@ -1566,6 +1853,8 @@ function renderDashboardHtml() {
|
|
|
1566
1853
|
evt.stopPropagation();
|
|
1567
1854
|
selectAndTraverseNode(d.to);
|
|
1568
1855
|
});
|
|
1856
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1857
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1569
1858
|
|
|
1570
1859
|
edgeGroup.node().appendChild(pathEl);
|
|
1571
1860
|
});
|
|
@@ -1600,7 +1889,7 @@ function renderDashboardHtml() {
|
|
|
1600
1889
|
|
|
1601
1890
|
nodeGroups
|
|
1602
1891
|
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1603
|
-
.on('mouseenter', (event, d) =>
|
|
1892
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1604
1893
|
.on('mouseleave', hidePopover);
|
|
1605
1894
|
}
|
|
1606
1895
|
|
|
@@ -1735,15 +2024,86 @@ function renderDashboardHtml() {
|
|
|
1735
2024
|
container.innerHTML = html;
|
|
1736
2025
|
}
|
|
1737
2026
|
|
|
1738
|
-
|
|
2027
|
+
// World Top-Tier High-Density Rich Node Tooltip Hover Popover Card
|
|
2028
|
+
function showNodePopover(event, node, edges) {
|
|
1739
2029
|
const card = document.getElementById('popoverCard');
|
|
2030
|
+
const badge = document.getElementById('popoverBadge');
|
|
2031
|
+
const title = document.getElementById('popoverTitle');
|
|
2032
|
+
const sec1 = document.getElementById('popoverSection1');
|
|
2033
|
+
const sec2 = document.getElementById('popoverSection2');
|
|
2034
|
+
|
|
1740
2035
|
const nodeIdStr = getIdStr(node.id || node);
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2036
|
+
const cat = getNodeCategory(nodeIdStr);
|
|
2037
|
+
|
|
2038
|
+
let badgeBg = '#3b82f6';
|
|
2039
|
+
let badgeLabel = 'BLUEPRINT SPEC';
|
|
2040
|
+
if (cat === 'REQ') { badgeBg = '#ec4899'; badgeLabel = '🟣 REQ BUSINESS INTENT'; }
|
|
2041
|
+
else if (cat === 'H-SPEC') { badgeBg = '#8b5cf6'; badgeLabel = '🟣 H-SPEC FUNCTIONAL'; }
|
|
2042
|
+
else if (cat === 'A-SPEC') { badgeBg = '#3b82f6'; badgeLabel = '🔵 A-SPEC ARCHITECTURE'; }
|
|
2043
|
+
else if (cat === 'T-SPEC') { badgeBg = '#06b6d4'; badgeLabel = '🔷 T-SPEC VERIFICATION'; }
|
|
2044
|
+
else if (cat === 'SYMBOL') { badgeBg = '#f59e0b'; badgeLabel = '⚡ AST CPG SYMBOL'; }
|
|
2045
|
+
else { badgeBg = '#10b981'; badgeLabel = '📄 SOURCE CODE FILE'; }
|
|
2046
|
+
|
|
2047
|
+
badge.style.background = badgeBg;
|
|
2048
|
+
badge.innerText = badgeLabel;
|
|
2049
|
+
|
|
2050
|
+
title.innerText = nodeIdStr;
|
|
2051
|
+
|
|
2052
|
+
const inEdges = edges.filter(e => getIdStr(e.to || e.target) === nodeIdStr);
|
|
2053
|
+
const outEdges = edges.filter(e => getIdStr(e.from || e.source) === nodeIdStr);
|
|
2054
|
+
const totalDegree = inEdges.length + outEdges.length;
|
|
2055
|
+
|
|
2056
|
+
sec1.innerHTML = \`
|
|
2057
|
+
<div><span class="popover-label">Full Label / Title:</span> <span class="popover-value">\${escapeHtml(node.label || nodeIdStr)}</span></div>
|
|
2058
|
+
<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>
|
|
2059
|
+
\`;
|
|
2060
|
+
|
|
2061
|
+
sec2.innerHTML = \`
|
|
2062
|
+
<div><span class="popover-label">Degree Centrality Rank:</span> <span class="popover-value" style="font-weight:bold; color:#fbbf24;">\${totalDegree} Relationships</span></div>
|
|
2063
|
+
<div><span class="popover-label">Inbound Parents (In-Degree):</span> <span class="popover-value">\${inEdges.length} edges</span></div>
|
|
2064
|
+
<div><span class="popover-label">Outbound Children (Out-Degree):</span> <span class="popover-value">\${outEdges.length} edges</span></div>
|
|
2065
|
+
\${cat === 'SYMBOL' ? '<div><span class="popover-label">Symbol Kind:</span> <span class="popover-value" style="color:#f59e0b;">Function / Method AST Node</span></div>' : ''}
|
|
2066
|
+
\${cat === 'FILE' ? '<div><span class="popover-label">File Path:</span> <span class="popover-value" style="color:#10b981;">' + escapeHtml(nodeIdStr) + '</span></div>' : ''}
|
|
2067
|
+
\`;
|
|
2068
|
+
|
|
2069
|
+
card.classList.add('active');
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// World Top-Tier High-Density Rich Edge Tooltip Hover Popover Card
|
|
2073
|
+
function showEdgePopover(event, edge) {
|
|
2074
|
+
const card = document.getElementById('popoverCard');
|
|
2075
|
+
const badge = document.getElementById('popoverBadge');
|
|
2076
|
+
const title = document.getElementById('popoverTitle');
|
|
2077
|
+
const sec1 = document.getElementById('popoverSection1');
|
|
2078
|
+
const sec2 = document.getElementById('popoverSection2');
|
|
2079
|
+
|
|
2080
|
+
const src = getIdStr(edge.from || edge.source);
|
|
2081
|
+
const dst = getIdStr(edge.to || edge.target);
|
|
2082
|
+
const rel = edge.rel || 'edge';
|
|
2083
|
+
|
|
2084
|
+
let badgeBg = '#3b82f6';
|
|
2085
|
+
let relExplanation = '';
|
|
2086
|
+
if (rel === 'implements') { badgeBg = '#3b82f6'; relExplanation = 'Code/File implements Architecture Specification'; }
|
|
2087
|
+
else if (rel === 'verifies') { badgeBg = '#06b6d4'; relExplanation = 'Test Spec verifies Code/Architecture Specification'; }
|
|
2088
|
+
else if (rel === 'depends_on') { badgeBg = '#8b5cf6'; relExplanation = 'Specification depends on parent Specification'; }
|
|
2089
|
+
else if (rel === 'contains') { badgeBg = '#10b981'; relExplanation = 'Source Code File contains AST Function Symbol'; }
|
|
2090
|
+
else if (rel === 'calls') { badgeBg = '#f59e0b'; relExplanation = 'AST Symbol calls destination Function Symbol (CPG Dataflow)'; }
|
|
2091
|
+
|
|
2092
|
+
badge.style.background = badgeBg;
|
|
2093
|
+
badge.innerText = '➔ EDGE RELATION: ' + rel.toUpperCase();
|
|
2094
|
+
|
|
2095
|
+
title.innerText = src + ' ➔ ' + dst;
|
|
2096
|
+
|
|
2097
|
+
sec1.innerHTML = \`
|
|
2098
|
+
<div><span class="popover-label">Edge Relation Type:</span> <span class="popover-value" style="font-weight:bold; color:#fbbf24;">\${rel}</span></div>
|
|
2099
|
+
<div><span class="popover-label">Causal Explanation:</span> <span class="popover-value" style="color:#38bdf8;">\${relExplanation}</span></div>
|
|
2100
|
+
\`;
|
|
2101
|
+
|
|
2102
|
+
sec2.innerHTML = \`
|
|
2103
|
+
<div><span class="popover-label">Source Node (From):</span> <span class="popover-value">\${escapeHtml(src)}</span></div>
|
|
2104
|
+
<div><span class="popover-label">Target Node (To):</span> <span class="popover-value">\${escapeHtml(dst)}</span></div>
|
|
2105
|
+
\`;
|
|
2106
|
+
|
|
1747
2107
|
card.classList.add('active');
|
|
1748
2108
|
}
|
|
1749
2109
|
|
|
@@ -1756,3 +2116,163 @@ function renderDashboardHtml() {
|
|
|
1756
2116
|
</body>
|
|
1757
2117
|
</html>`;
|
|
1758
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* The canonical status vocabulary. A document outside it (`Approved`, `Proposed`, `Verified`,
|
|
2121
|
+
* `Completed`, `Pending` — the C-SPEC/JOB corpus) has not been scored against these gates at all,
|
|
2122
|
+
* and scoring it 0 would report an unmapped vocabulary as an uncovered spec.
|
|
2123
|
+
*/
|
|
2124
|
+
const CANONICAL_STATUSES = new Set(['draft', 'review', 'approved', 'outdated']);
|
|
2125
|
+
function isCanonicalStatus(status) {
|
|
2126
|
+
return typeof status === 'string' && CANONICAL_STATUSES.has(status);
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* The kinds that are governed specs at all.
|
|
2130
|
+
*
|
|
2131
|
+
* @implements A-SPEC-219.1
|
|
2132
|
+
* Held here rather than imported from SPEC_TYPES because REQ is a spec kind that SPEC_TYPES does
|
|
2133
|
+
* list — the distinction this set draws is "document in the spec store that the pipeline governs"
|
|
2134
|
+
* versus "document that lives there but never will", and JOB is the whole of the second group.
|
|
2135
|
+
*/
|
|
2136
|
+
exports.SPEC_KINDS = new Set(['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC']);
|
|
2137
|
+
/**
|
|
2138
|
+
* Drop retired documents before anything reads the corpus.
|
|
2139
|
+
*
|
|
2140
|
+
* @implements A-SPEC-222.2
|
|
2141
|
+
* A-SPEC-222.1 excluded `outdated` from `isGoverned` and claimed every other check would follow.
|
|
2142
|
+
* `tspec-mirror.test.ts` refuted that, and the spec was corrected to name one known site. Measured
|
|
2143
|
+
* 2026-08-22 it was three: /api/rtm exposed 132 retired ids, the heatmap 108, the graph 132 — a
|
|
2144
|
+
* screen still pointing at documents retired the day before. Stated once here so the three sites
|
|
2145
|
+
* cannot drift, with the suite enumerating the endpoints so a fourth cannot be added silently.
|
|
2146
|
+
*/
|
|
2147
|
+
function activeSpecs(specs) {
|
|
2148
|
+
return specs.filter((s) => s.status !== 'outdated');
|
|
2149
|
+
}
|
|
2150
|
+
/** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
|
|
2151
|
+
function specKindOf(id) {
|
|
2152
|
+
const m = /^([A-Za-z-]+?)-\d/.exec(String(id ?? ''));
|
|
2153
|
+
return m ? m[1] : 'UNKNOWN';
|
|
2154
|
+
}
|
|
2155
|
+
const STAGE_NAMES = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
|
|
2156
|
+
/**
|
|
2157
|
+
* Match a spec id against a REQ's number exactly, so `REQ-021` cannot claim `A-SPEC-0219`.
|
|
2158
|
+
* Accepts the plain number and dotted sub-specs (`A-SPEC-219.1`).
|
|
2159
|
+
*/
|
|
2160
|
+
function specBelongsToReq(specId, kind, numPart) {
|
|
2161
|
+
return specId === `${kind}-${numPart}` || specId.startsWith(`${kind}-${numPart}.`);
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Attribute a finding to a REQ only on an EXACT spec reference. `specRef.includes(numPart)` let
|
|
2165
|
+
* REQ-021 claim a finding filed against A-SPEC-0219; substring containment is not evidence.
|
|
2166
|
+
*/
|
|
2167
|
+
function findingBelongsToReq(finding, reqId, numPart) {
|
|
2168
|
+
const ref = typeof finding?.specRef === 'string' ? finding.specRef : null;
|
|
2169
|
+
if (!ref)
|
|
2170
|
+
return false;
|
|
2171
|
+
return (ref === reqId ||
|
|
2172
|
+
specBelongsToReq(ref, 'H-SPEC', numPart) ||
|
|
2173
|
+
specBelongsToReq(ref, 'A-SPEC', numPart) ||
|
|
2174
|
+
specBelongsToReq(ref, 'T-SPEC', numPart));
|
|
2175
|
+
}
|
|
2176
|
+
function fileBelongsToReq(implementsSpecs, reqId, numPart) {
|
|
2177
|
+
return implementsSpecs.some((specId) => specId === reqId ||
|
|
2178
|
+
specBelongsToReq(specId, 'H-SPEC', numPart) ||
|
|
2179
|
+
specBelongsToReq(specId, 'A-SPEC', numPart) ||
|
|
2180
|
+
specBelongsToReq(specId, 'T-SPEC', numPart));
|
|
2181
|
+
}
|
|
2182
|
+
function makeRow(base) {
|
|
2183
|
+
const stages = [
|
|
2184
|
+
base.reqId,
|
|
2185
|
+
base.hspecId ?? null,
|
|
2186
|
+
base.aspecId ?? null,
|
|
2187
|
+
base.tspecId ?? null,
|
|
2188
|
+
base.fileId ?? null,
|
|
2189
|
+
base.symbolId ?? null,
|
|
2190
|
+
];
|
|
2191
|
+
const missingStages = STAGE_NAMES.filter((_, i) => !stages[i]);
|
|
2192
|
+
const stagesComplete = stages.filter(Boolean).length;
|
|
2193
|
+
return {
|
|
2194
|
+
reqId: base.reqId,
|
|
2195
|
+
reqTitle: base.reqTitle,
|
|
2196
|
+
hspecId: base.hspecId ?? null,
|
|
2197
|
+
aspecId: base.aspecId ?? null,
|
|
2198
|
+
tspecId: base.tspecId ?? null,
|
|
2199
|
+
fileId: base.fileId ?? null,
|
|
2200
|
+
symbolId: base.symbolId ?? null,
|
|
2201
|
+
symbolLine: base.symbolLine ?? null,
|
|
2202
|
+
stagesComplete,
|
|
2203
|
+
missingStages: [...missingStages],
|
|
2204
|
+
status: stagesComplete === STAGE_NAMES.length ? 'COVERED' : 'UNCOVERED',
|
|
2205
|
+
findingsCount: base.findingsCount ?? 0,
|
|
2206
|
+
criticalCount: base.criticalCount ?? 0,
|
|
2207
|
+
findingsScanned: base.findingsScanned ?? false,
|
|
2208
|
+
mutantCount: base.mutantCount ?? 0,
|
|
2209
|
+
mutationScore: null,
|
|
2210
|
+
mutationScoreMeasured: false,
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Build the 6-stage pipeline matrix from the repository's real specs, scanned files and findings.
|
|
2215
|
+
*
|
|
2216
|
+
* Pure and exported so the truthfulness invariants can be asserted directly, without an HTTP round
|
|
2217
|
+
* trip: no row may name an artifact that the inputs do not contain, and no symbol may be dropped.
|
|
2218
|
+
*
|
|
2219
|
+
* @implements A-SPEC-219
|
|
2220
|
+
*/
|
|
2221
|
+
function buildPipelineRows(specs, files, allFindings, options) {
|
|
2222
|
+
const findingsScanned = options?.findingsScanned === true;
|
|
2223
|
+
const safeSpecs = Array.isArray(specs) ? specs : [];
|
|
2224
|
+
const safeFiles = Array.isArray(files) ? files : [];
|
|
2225
|
+
const safeFindings = Array.isArray(allFindings) ? allFindings : [];
|
|
2226
|
+
const rows = [];
|
|
2227
|
+
for (const reqItem of safeSpecs.filter((s) => typeof s?.id === 'string' && s.id.startsWith('REQ-'))) {
|
|
2228
|
+
const numPart = reqItem.id.replace('REQ-', '').split('.')[0];
|
|
2229
|
+
const pick = (kind) => safeSpecs.find((s) => specBelongsToReq(String(s?.id ?? ''), kind, numPart))?.id ?? null;
|
|
2230
|
+
const hspecId = pick('H-SPEC');
|
|
2231
|
+
const aspecId = pick('A-SPEC');
|
|
2232
|
+
const tspecId = pick('T-SPEC');
|
|
2233
|
+
const matchingFiles = safeFiles.filter((f) => fileBelongsToReq(f?.implementsSpecs ?? [], reqItem.id, numPart));
|
|
2234
|
+
const specFindings = safeFindings.filter((f) => f?.status === 'open' && findingBelongsToReq(f, reqItem.id, numPart));
|
|
2235
|
+
const common = {
|
|
2236
|
+
reqId: reqItem.id,
|
|
2237
|
+
reqTitle: reqItem.title,
|
|
2238
|
+
hspecId,
|
|
2239
|
+
aspecId,
|
|
2240
|
+
tspecId,
|
|
2241
|
+
findingsScanned,
|
|
2242
|
+
};
|
|
2243
|
+
if (matchingFiles.length === 0) {
|
|
2244
|
+
// No implementing file: stages 5 and 6 stay empty. Nothing is substituted for them.
|
|
2245
|
+
rows.push(makeRow({
|
|
2246
|
+
...common,
|
|
2247
|
+
findingsCount: specFindings.length,
|
|
2248
|
+
criticalCount: specFindings.filter((f) => f.severity === 'critical').length,
|
|
2249
|
+
}));
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
for (const f of matchingFiles) {
|
|
2253
|
+
const fileId = f.sourcePath || f.path;
|
|
2254
|
+
const syms = f.symbols || [];
|
|
2255
|
+
const fileMutants = (0, ast_mutation_1.generateAstMutants)([f]);
|
|
2256
|
+
const fileFindings = safeFindings.filter((fd) => fd?.status === 'open' && (fd?.file === fileId || findingBelongsToReq(fd, reqItem.id, numPart)));
|
|
2257
|
+
const findingsCount = fileFindings.length;
|
|
2258
|
+
const criticalCount = fileFindings.filter((fd) => fd.severity === 'critical').length;
|
|
2259
|
+
if (syms.length === 0) {
|
|
2260
|
+
rows.push(makeRow({ ...common, fileId, findingsCount, criticalCount, mutantCount: fileMutants.length }));
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
// Every symbol gets a row — no truncation, so the matrix count and the drill-down agree.
|
|
2264
|
+
for (const sym of syms) {
|
|
2265
|
+
rows.push(makeRow({
|
|
2266
|
+
...common,
|
|
2267
|
+
fileId,
|
|
2268
|
+
symbolId: sym.name,
|
|
2269
|
+
symbolLine: sym.startLine ?? null,
|
|
2270
|
+
findingsCount,
|
|
2271
|
+
criticalCount,
|
|
2272
|
+
mutantCount: fileMutants.filter((m) => m.symbolName === sym.name).length,
|
|
2273
|
+
}));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return rows;
|
|
2278
|
+
}
|