@holmes-lab/holmes-kit 0.1.7 → 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/CHANGELOG.md +8 -0
- package/README.md +11 -7
- package/bin/holmes-mcp.js +0 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +50 -0
- package/dist/holmes/cli/ci-gate.d.ts +18 -0
- package/dist/holmes/cli/doctor.d.ts +44 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +18 -0
- package/dist/holmes/cli/governed-precondition.d.ts +27 -0
- package/dist/holmes/cli/index.d.ts +14 -0
- package/dist/holmes/cli/index.js +23 -1
- package/dist/holmes/cli/init.d.ts +69 -0
- package/dist/holmes/cli/interactive-prompt.d.ts +9 -0
- package/dist/holmes/cli/playbook-skills.d.ts +129 -0
- package/dist/holmes/cli/roles-readme.d.ts +12 -0
- package/dist/holmes/cli/serve.d.ts +14 -0
- package/dist/holmes/cli/serve.js +70 -0
- package/dist/holmes/cli/settings-merge.d.ts +66 -0
- package/dist/holmes/config/config.d.ts +13 -0
- package/dist/holmes/context/bundler.d.ts +40 -0
- package/dist/holmes/context/render.d.ts +9 -0
- package/dist/holmes/context/tiers.d.ts +54 -0
- package/dist/holmes/context/tokens.d.ts +2 -0
- package/dist/holmes/cpg/ast-mutation.d.ts +31 -0
- package/dist/holmes/cpg/ast-mutation.js +126 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +69 -0
- package/dist/holmes/cpg/dynamic-cpg.d.ts +18 -0
- package/dist/holmes/cpg/dynamic-cpg.js +82 -0
- package/dist/holmes/cpg/hash-cache.d.ts +21 -0
- package/dist/holmes/cpg/language-parser-walk.d.ts +31 -0
- package/dist/holmes/cpg/language-parser-worker.d.ts +1 -0
- package/dist/holmes/cpg/language-parser.d.ts +116 -0
- package/dist/holmes/cpg/program-slicing.d.ts +21 -0
- package/dist/holmes/cpg/program-slicing.js +123 -0
- package/dist/holmes/cpg/scan-cache.d.ts +21 -0
- package/dist/holmes/cpg/source-path.d.ts +2 -0
- package/dist/holmes/cpg/test-files.d.ts +36 -0
- package/dist/holmes/governance/constitution-debt.d.ts +3 -0
- package/dist/holmes/governance/constitution-report.d.ts +23 -0
- package/dist/holmes/governance/constitution.d.ts +61 -0
- package/dist/holmes/governance/identity.d.ts +28 -0
- package/dist/holmes/governance/ledger-lock.d.ts +55 -0
- package/dist/holmes/governance/ledger-store.conformance.d.ts +11 -0
- package/dist/holmes/governance/ledger-store.d.ts +114 -0
- package/dist/holmes/governance/progress-ledger.d.ts +25 -0
- package/dist/holmes/governance/provenance-chain.d.ts +154 -0
- package/dist/holmes/governance/provenance-ledger.d.ts +102 -0
- package/dist/holmes/governance/provenance-schema.d.ts +21 -0
- package/dist/holmes/governance/replica-id.d.ts +20 -0
- package/dist/holmes/governance/role-policy.d.ts +52 -0
- package/dist/holmes/governance/trust-score.d.ts +32 -0
- package/dist/holmes/guardrail/anchors.d.ts +8 -0
- package/dist/holmes/guardrail/blind-spots.d.ts +32 -0
- package/dist/holmes/guardrail/decision-ledger.d.ts +29 -0
- package/dist/holmes/guardrail/executable-artifact.d.ts +1 -0
- package/dist/holmes/guardrail/governance-history.d.ts +15 -0
- package/dist/holmes/guardrail/phase.d.ts +27 -0
- package/dist/holmes/guardrail/risk-classifier.d.ts +52 -0
- package/dist/holmes/guardrail/risk-gate.d.ts +77 -0
- package/dist/holmes/guardrail/risk-types.d.ts +48 -0
- package/dist/holmes/guardrail/tspec-state.d.ts +71 -0
- package/dist/holmes/guardrail/write-target.d.ts +77 -0
- package/dist/holmes/hooks/adapters/antigravity.d.ts +42 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +144 -0
- package/dist/holmes/hooks/stop.d.ts +91 -0
- package/dist/holmes/mcp/basis.d.ts +100 -0
- package/dist/holmes/mcp/handlers.d.ts +482 -0
- package/dist/holmes/mcp/server.d.ts +2 -0
- package/dist/holmes/mcp/stdio-client.d.ts +51 -0
- package/dist/holmes/mcp/supervisor.d.ts +48 -0
- package/dist/holmes/mcp/tool-schemas.d.ts +12 -0
- package/dist/holmes/mcp/validate-args.d.ts +27 -0
- package/dist/holmes/messages/registry.d.ts +23 -0
- package/dist/holmes/project/baseline.d.ts +66 -0
- package/dist/holmes/project/change-source.d.ts +89 -0
- package/dist/holmes/project/ignore.d.ts +33 -0
- package/dist/holmes/project/root.d.ts +34 -0
- package/dist/holmes/reverse/anchor.d.ts +47 -0
- package/dist/holmes/reverse/cluster.d.ts +45 -0
- package/dist/holmes/reverse/draft.d.ts +33 -0
- package/dist/holmes/reverse/dynamic-wiring.d.ts +18 -0
- package/dist/holmes/reverse/scan.d.ts +103 -0
- package/dist/holmes/reverse/surface.d.ts +91 -0
- package/dist/holmes/reverse/test-map.d.ts +91 -0
- package/dist/holmes/review/coverage.d.ts +21 -0
- package/dist/holmes/review/findings.d.ts +49 -0
- package/dist/holmes/review/package.d.ts +26 -0
- package/dist/holmes/review/review-targets.d.ts +28 -0
- package/dist/holmes/review/scope.d.ts +13 -0
- package/dist/holmes/review/test-evidence.d.ts +31 -0
- package/dist/holmes/review/test-runner.d.ts +141 -0
- package/dist/holmes/rtm/dataflow-taint.d.ts +81 -0
- package/dist/holmes/rtm/gap-analyzer.d.ts +28 -0
- package/dist/holmes/rtm/git-changes.d.ts +33 -0
- package/dist/holmes/rtm/heatmap.d.ts +21 -0
- package/dist/holmes/rtm/incremental.d.ts +38 -0
- package/dist/holmes/rtm/localize.d.ts +36 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +101 -0
- package/dist/holmes/rtm/rtm-check.d.ts +13 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +88 -0
- package/dist/holmes/rtm/taint.d.ts +35 -0
- package/dist/holmes/rtm/test-scope.d.ts +85 -0
- package/dist/holmes/server/dashboard.d.ts +85 -0
- package/dist/holmes/server/dashboard.js +2188 -0
- package/dist/holmes/spec/approval-blockers.d.ts +62 -0
- package/dist/holmes/spec/breaking-change.d.ts +42 -0
- package/dist/holmes/spec/legacy-format.d.ts +93 -0
- package/dist/holmes/spec/spec-digest.d.ts +29 -0
- package/dist/holmes/spec/spec-parser.d.ts +12 -0
- package/dist/holmes/spec/spec-store.conformance.d.ts +10 -0
- package/dist/holmes/spec/spec-store.d.ts +130 -0
- package/dist/holmes/spec/spec-types.d.ts +150 -0
- package/dist/holmes/spec/validator.d.ts +65 -0
- package/package.json +2 -1
|
@@ -0,0 +1,2188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.startDashboardServer = startDashboardServer;
|
|
37
|
+
exports.isCanonicalStatus = isCanonicalStatus;
|
|
38
|
+
exports.specKindOf = specKindOf;
|
|
39
|
+
exports.buildPipelineRows = buildPipelineRows;
|
|
40
|
+
// @implements A-SPEC-215
|
|
41
|
+
// @implements A-SPEC-219
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const http = __importStar(require("node:http"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const spec_store_1 = require("../spec/spec-store");
|
|
46
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
47
|
+
const language_parser_1 = require("../cpg/language-parser");
|
|
48
|
+
const findings_1 = require("../review/findings");
|
|
49
|
+
const ast_mutation_1 = require("../cpg/ast-mutation");
|
|
50
|
+
/**
|
|
51
|
+
* Start a lightweight standalone Node.js HTTP server for interactive dashboard & RTM visualization.
|
|
52
|
+
*
|
|
53
|
+
* @implements A-SPEC-215
|
|
54
|
+
* @implements A-SPEC-219
|
|
55
|
+
*/
|
|
56
|
+
async function startDashboardServer(options) {
|
|
57
|
+
const root = path.resolve(options.root);
|
|
58
|
+
const targetPort = options.port !== undefined ? options.port : 8080;
|
|
59
|
+
const store = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
|
|
60
|
+
const parser = new language_parser_1.TreeSitterTsParser();
|
|
61
|
+
const scanner = new cpg_scanner_1.CpgScanner(parser);
|
|
62
|
+
const activeSockets = new Set();
|
|
63
|
+
const server = http.createServer(async (req, res) => {
|
|
64
|
+
// DNS Rebinding / Host Header validation
|
|
65
|
+
const hostHeader = req.headers.host || '';
|
|
66
|
+
const hostName = hostHeader.split(':')[0].toLowerCase();
|
|
67
|
+
if (hostName !== 'localhost' && hostName !== '127.0.0.1' && hostName !== '[::1]') {
|
|
68
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
69
|
+
res.end('Forbidden: Host header mismatch');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const parsedUrl = new URL(req.url || '/', 'http://localhost').pathname;
|
|
73
|
+
if (parsedUrl === '/' || parsedUrl === '/index.html') {
|
|
74
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
75
|
+
res.end(renderDashboardHtml());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (parsedUrl === '/api/rtm') {
|
|
79
|
+
try {
|
|
80
|
+
const specs = await store.list();
|
|
81
|
+
const files = scanner.scan(root);
|
|
82
|
+
const implementedSpecIds = new Set();
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
for (const s of f.implementsSpecs)
|
|
85
|
+
implementedSpecIds.add(s);
|
|
86
|
+
}
|
|
87
|
+
const enrichedSpecs = specs.map((s) => {
|
|
88
|
+
const isA = s.id.startsWith('A-SPEC');
|
|
89
|
+
const covered = isA ? implementedSpecIds.has(s.id) : s.status === 'approved';
|
|
90
|
+
return { ...s, covered, legacyStatus: !isCanonicalStatus(s.status) };
|
|
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 };
|
|
117
|
+
const coveredCount = enrichedSpecs.filter((s) => s.covered).length;
|
|
118
|
+
const totalCount = enrichedSpecs.length;
|
|
119
|
+
const body = JSON.stringify({
|
|
120
|
+
ok: true,
|
|
121
|
+
totalCount,
|
|
122
|
+
coveredCount,
|
|
123
|
+
approval,
|
|
124
|
+
implementation,
|
|
125
|
+
legacy,
|
|
126
|
+
specs: enrichedSpecs,
|
|
127
|
+
files,
|
|
128
|
+
});
|
|
129
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
130
|
+
res.end(body);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (!res.headersSent) {
|
|
134
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
135
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// @implements A-SPEC-219
|
|
141
|
+
if (parsedUrl === '/api/rtm/heatmap') {
|
|
142
|
+
try {
|
|
143
|
+
const specs = await store.list();
|
|
144
|
+
const files = scanner.scan(root);
|
|
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 });
|
|
154
|
+
const body = JSON.stringify({
|
|
155
|
+
ok: true,
|
|
156
|
+
pipelineCount: pipelines.length,
|
|
157
|
+
completeCount: pipelines.filter((p) => p.status === 'COVERED').length,
|
|
158
|
+
findingsScanned,
|
|
159
|
+
mutationScoreMeasured: false,
|
|
160
|
+
pipelines,
|
|
161
|
+
});
|
|
162
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
163
|
+
res.end(body);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (!res.headersSent) {
|
|
167
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
168
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (parsedUrl === '/api/cpg') {
|
|
174
|
+
try {
|
|
175
|
+
const files = scanner.scan(root);
|
|
176
|
+
const body = JSON.stringify({ ok: true, fileCount: files.length, files });
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
178
|
+
res.end(body);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (!res.headersSent) {
|
|
182
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
183
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// @implements A-SPEC-219
|
|
189
|
+
if (parsedUrl === '/api/graph') {
|
|
190
|
+
try {
|
|
191
|
+
const specs = await store.list();
|
|
192
|
+
const files = scanner.scan(root);
|
|
193
|
+
const nodes = [];
|
|
194
|
+
const edges = [];
|
|
195
|
+
const nodeMap = new Map();
|
|
196
|
+
const symbolLookup = new Map();
|
|
197
|
+
const fileLocalSymbols = new Map();
|
|
198
|
+
for (const s of specs) {
|
|
199
|
+
const specNode = { id: s.id, type: 'spec', kind: s.type, label: `${s.id}: ${s.title || ''}`, covered: s.status === 'approved' };
|
|
200
|
+
nodes.push(specNode);
|
|
201
|
+
nodeMap.set(s.id, specNode);
|
|
202
|
+
}
|
|
203
|
+
// Phase 1: Spec-to-Spec Precise Parent Linkages with Digest Sanitization
|
|
204
|
+
for (const s of specs) {
|
|
205
|
+
const rawDeps = Array.isArray(s.frontmatter?.depends_on)
|
|
206
|
+
? s.frontmatter.depends_on
|
|
207
|
+
: typeof s.frontmatter?.depends_on === 'string'
|
|
208
|
+
? [s.frontmatter.depends_on]
|
|
209
|
+
: (s.dependsOn || []);
|
|
210
|
+
const cleanDeps = rawDeps
|
|
211
|
+
.map((d) => String(d).split(':')[0].trim())
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
let hasExplicitParent = false;
|
|
214
|
+
for (const dep of cleanDeps) {
|
|
215
|
+
if (nodeMap.has(dep)) {
|
|
216
|
+
edges.push({ from: s.id, to: dep, rel: 'depends_on' });
|
|
217
|
+
hasExplicitParent = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!hasExplicitParent) {
|
|
221
|
+
const numPart = s.id.replace(/^(REQ|H-SPEC|A-SPEC|T-SPEC)-/, '').split('.')[0];
|
|
222
|
+
if (s.id.startsWith('A-SPEC-')) {
|
|
223
|
+
const fallbackParent = `H-SPEC-${numPart}`;
|
|
224
|
+
if (nodeMap.has(fallbackParent))
|
|
225
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'depends_on' });
|
|
226
|
+
}
|
|
227
|
+
else if (s.id.startsWith('H-SPEC-')) {
|
|
228
|
+
const fallbackParent = `REQ-${numPart}`;
|
|
229
|
+
if (nodeMap.has(fallbackParent))
|
|
230
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'depends_on' });
|
|
231
|
+
}
|
|
232
|
+
else if (s.id.startsWith('T-SPEC-')) {
|
|
233
|
+
const fallbackParent = `A-SPEC-${numPart}`;
|
|
234
|
+
if (nodeMap.has(fallbackParent))
|
|
235
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'verifies' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Phase 2: Register Files and All Top-Level AST Symbols with 100% Implements/Verifies Linkage
|
|
240
|
+
for (const f of files) {
|
|
241
|
+
const fileId = f.sourcePath || f.path;
|
|
242
|
+
const isTestFile = fileId.includes('.test.') || fileId.includes('.spec.');
|
|
243
|
+
const fileNode = { id: fileId, type: 'code', kind: isTestFile ? 'test_file' : 'file', label: fileId };
|
|
244
|
+
nodes.push(fileNode);
|
|
245
|
+
nodeMap.set(fileId, fileNode);
|
|
246
|
+
const localSymSet = new Set();
|
|
247
|
+
fileLocalSymbols.set(fileId, localSymSet);
|
|
248
|
+
// File IMPLEMENTS/VERIFIES Spec
|
|
249
|
+
for (const specId of f.implementsSpecs) {
|
|
250
|
+
const isTSpecAnchor = specId.startsWith('T-SPEC');
|
|
251
|
+
const relType = isTSpecAnchor || isTestFile ? 'verifies' : 'implements';
|
|
252
|
+
if (nodeMap.has(specId)) {
|
|
253
|
+
edges.push({ from: fileId, to: specId, rel: relType });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Register AST Symbols and establish File CONTAINS Symbol & Symbol IMPLEMENTS/VERIFIES Spec
|
|
257
|
+
for (const sym of f.symbols || []) {
|
|
258
|
+
const symId = `${fileId}#${sym.name}`;
|
|
259
|
+
const symNode = { id: symId, type: 'code', kind: 'symbol', label: sym.name };
|
|
260
|
+
nodes.push(symNode);
|
|
261
|
+
nodeMap.set(symId, symNode);
|
|
262
|
+
symbolLookup.set(sym.name, symId);
|
|
263
|
+
localSymSet.add(sym.name);
|
|
264
|
+
// File CONTAINS Symbol
|
|
265
|
+
edges.push({ from: fileId, to: symId, rel: 'contains' });
|
|
266
|
+
// 100% Precise Symbol IMPLEMENTS/VERIFIES Spec Linkage
|
|
267
|
+
for (const specId of f.implementsSpecs) {
|
|
268
|
+
const isTSpecAnchor = specId.startsWith('T-SPEC');
|
|
269
|
+
const relType = isTSpecAnchor || isTestFile ? 'verifies' : 'implements';
|
|
270
|
+
if (nodeMap.has(specId)) {
|
|
271
|
+
edges.push({ from: symId, to: specId, rel: relType });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Phase 3: Resolve Function-to-Function CPG Call Edges with File-Scoped Fallback
|
|
277
|
+
for (const f of files) {
|
|
278
|
+
const fileId = f.sourcePath || f.path;
|
|
279
|
+
const localSyms = fileLocalSymbols.get(fileId) || new Set();
|
|
280
|
+
for (const e of f.edges || []) {
|
|
281
|
+
let fromId = e.from;
|
|
282
|
+
let toId = e.to;
|
|
283
|
+
if (localSyms.has(fromId)) {
|
|
284
|
+
fromId = `${fileId}#${fromId}`;
|
|
285
|
+
}
|
|
286
|
+
else if (symbolLookup.has(fromId)) {
|
|
287
|
+
fromId = symbolLookup.get(fromId);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
fromId = `${fileId}#${fromId}`;
|
|
291
|
+
}
|
|
292
|
+
if (localSyms.has(toId)) {
|
|
293
|
+
toId = `${fileId}#${toId}`;
|
|
294
|
+
}
|
|
295
|
+
else if (symbolLookup.has(toId)) {
|
|
296
|
+
toId = symbolLookup.get(toId);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
toId = `${fileId}#${toId}`;
|
|
300
|
+
}
|
|
301
|
+
if (nodeMap.has(fromId) && nodeMap.has(toId)) {
|
|
302
|
+
edges.push({ from: fromId, to: toId, rel: e.rel });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const body = JSON.stringify({ ok: true, nodeCount: nodes.length, edgeCount: edges.length, nodes, edges });
|
|
307
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
308
|
+
res.end(body);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if (!res.headersSent) {
|
|
312
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
313
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
319
|
+
res.end('Not Found');
|
|
320
|
+
});
|
|
321
|
+
server.on('connection', (socket) => {
|
|
322
|
+
activeSockets.add(socket);
|
|
323
|
+
socket.once('close', () => activeSockets.delete(socket));
|
|
324
|
+
});
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
server.listen(targetPort, '127.0.0.1', () => {
|
|
327
|
+
const addr = server.address();
|
|
328
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : targetPort;
|
|
329
|
+
const url = `http://localhost:${actualPort}`;
|
|
330
|
+
const handle = {
|
|
331
|
+
server,
|
|
332
|
+
url,
|
|
333
|
+
port: actualPort,
|
|
334
|
+
stop: () => new Promise((resStop) => {
|
|
335
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
336
|
+
server.closeAllConnections();
|
|
337
|
+
}
|
|
338
|
+
for (const sock of activeSockets) {
|
|
339
|
+
sock.destroy();
|
|
340
|
+
}
|
|
341
|
+
activeSockets.clear();
|
|
342
|
+
server.close(() => resStop());
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
resolve(handle);
|
|
346
|
+
});
|
|
347
|
+
server.on('error', reject);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function renderDashboardHtml() {
|
|
351
|
+
return `<!DOCTYPE html>
|
|
352
|
+
<html lang="en">
|
|
353
|
+
<head>
|
|
354
|
+
<meta charset="UTF-8">
|
|
355
|
+
<title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
|
|
356
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
357
|
+
<style>
|
|
358
|
+
body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
|
|
359
|
+
.header { margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.12); padding-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
|
|
360
|
+
.nav-tabs { display: flex; gap: 12px; margin-bottom: 20px; }
|
|
361
|
+
.tab-btn { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }
|
|
362
|
+
.tab-btn.active { background: #3b82f6; color: #ffffff; border-color: #3b82f6; }
|
|
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; }
|
|
364
|
+
|
|
365
|
+
/* Quantitative REQ SDLC Health Matrix Grid Styling */
|
|
366
|
+
.rtm-container { display: flex; flex-direction: column; gap: 16px; }
|
|
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; }
|
|
368
|
+
.req-card:hover { border-color: #ec4899; }
|
|
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); }
|
|
370
|
+
.req-title-group { display: flex; align-items: center; gap: 12px; }
|
|
371
|
+
.req-id-badge { background: #ec4899; color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; }
|
|
372
|
+
.req-title { font-size: 15px; font-weight: bold; color: #f8fafc; }
|
|
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
|
+
|
|
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; }
|
|
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; }
|
|
387
|
+
|
|
388
|
+
.accordion-body { padding: 16px 20px; display: none; background: #0f172a; border-top: 1px solid #334155; flex-direction: column; gap: 10px; }
|
|
389
|
+
.accordion-body.open { display: flex; }
|
|
390
|
+
.tree-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-radius: 6px; background: #1e293b; border: 1px solid #334155; font-size: 12px; }
|
|
391
|
+
.tree-row-hspec { margin-left: 16px; border-left: 4px solid #8b5cf6; }
|
|
392
|
+
.tree-row-aspec { margin-left: 36px; border-left: 4px solid #3b82f6; }
|
|
393
|
+
.tree-row-tspec { margin-left: 56px; border-left: 4px solid #06b6d4; }
|
|
394
|
+
.tree-row-file { margin-left: 76px; border-left: 4px solid #10b981; }
|
|
395
|
+
.tree-row-symbol { margin-left: 96px; border-left: 4px solid #f59e0b; background: rgba(245,158,11,0.08); }
|
|
396
|
+
.tree-label { font-weight: bold; color: #f8fafc; display: flex; align-items: center; gap: 8px; }
|
|
397
|
+
.badge-covered { background-color: #065f46; color: #34d399; }
|
|
398
|
+
.badge-uncovered { background-color: #991b1b; color: #f87171; }
|
|
399
|
+
.stat-badge { font-size: 24px; font-weight: 800; color: #f59e0b; }
|
|
400
|
+
|
|
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%; }
|
|
404
|
+
.pipeline-table th, .pipeline-table td { border: 1px solid #334155; padding: 10px 12px; text-align: left; }
|
|
405
|
+
.pipeline-table th { background: #1e293b; color: #f8fafc; font-weight: bold; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
|
|
406
|
+
.pipeline-row:hover { background: #1e293b; }
|
|
407
|
+
.stage-req { color: #ec4899; font-weight: bold; }
|
|
408
|
+
.stage-hspec { color: #8b5cf6; font-weight: bold; }
|
|
409
|
+
.stage-aspec { color: #3b82f6; font-weight: bold; }
|
|
410
|
+
.stage-tspec { color: #06b6d4; font-weight: bold; }
|
|
411
|
+
.stage-file { color: #10b981; font-weight: bold; }
|
|
412
|
+
.stage-symbol { color: #f59e0b; font-weight: bold; background: rgba(245,158,11,0.1); padding: 4px 8px; border-radius: 4px; }
|
|
413
|
+
.arrow-sep { color: #64748b; margin: 0 4px; font-weight: bold; }
|
|
414
|
+
|
|
415
|
+
#graphCanvasContainer { width: 100%; height: 820px; background: #0f172a; border-radius: 12px; border: 1.5px solid #334155; position: relative; overflow: hidden; cursor: grab; display: flex; box-shadow: inset 0 0 40px rgba(0,0,0,0.8); }
|
|
416
|
+
#graphCanvasContainer:active { cursor: grabbing; }
|
|
417
|
+
svg.graph-svg { width: 100%; height: 100%; }
|
|
418
|
+
|
|
419
|
+
/* Interactive Traversal Inspector Drawer */
|
|
420
|
+
.traversal-drawer { width: 400px; height: 100%; background: #0b0f19; border-left: 1px solid #334155; padding: 18px; box-sizing: border-box; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; z-index: 80; box-shadow: -10px 0 35px rgba(0,0,0,0.7); }
|
|
421
|
+
.drawer-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #334155; padding-bottom: 10px; }
|
|
422
|
+
.drawer-title { font-size: 16px; font-weight: bold; color: #38bdf8; word-break: break-all; }
|
|
423
|
+
.neighbor-section { margin-top: 10px; }
|
|
424
|
+
.neighbor-sec-title { font-size: 11px; font-weight: 800; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
|
|
425
|
+
.neighbor-item { background: #1e293b; border: 1px solid #334155; padding: 10px 12px; border-radius: 6px; margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: all 0.15s ease; }
|
|
426
|
+
.neighbor-item:hover { border-color: #3b82f6; background: #26354a; transform: translateX(2px); }
|
|
427
|
+
.neighbor-id { font-size: 12px; font-weight: bold; color: #f8fafc; word-break: break-all; }
|
|
428
|
+
.neighbor-rel { font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 4px; color: #fff; }
|
|
429
|
+
.rel-depends_on { background: #8b5cf6; }
|
|
430
|
+
.rel-implements { background: #3b82f6; }
|
|
431
|
+
.rel-verifies { background: #06b6d4; }
|
|
432
|
+
.rel-contains { background: #10b981; }
|
|
433
|
+
.rel-calls { background: #f59e0b; }
|
|
434
|
+
.rel-peer { background: #a855f7; font-weight: 900; }
|
|
435
|
+
.jump-btn { background: #3b82f6; color: #fff; border: none; padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; font-weight: bold; transition: background 0.15s ease; }
|
|
436
|
+
.jump-btn:hover { background: #2563eb; }
|
|
437
|
+
.jump-btn-peer { background: #a855f7 !important; }
|
|
438
|
+
.jump-btn-peer:hover { background: #9333ea !important; }
|
|
439
|
+
|
|
440
|
+
.breadcrumb-bar { display: flex; gap: 8px; align-items: center; background: #1e293b; border: 1px solid #334155; padding: 8px 14px; border-radius: 8px; margin-bottom: 12px; font-size: 12px; overflow-x: auto; white-space: nowrap; }
|
|
441
|
+
.crumb-item { color: #38bdf8; font-weight: bold; cursor: pointer; text-decoration: underline; }
|
|
442
|
+
.crumb-sep { color: #64748b; }
|
|
443
|
+
|
|
444
|
+
/* High-Contrast Crisp Node & Edge Styling */
|
|
445
|
+
.node { cursor: pointer; transition: all 0.2s ease; }
|
|
446
|
+
.node circle { stroke: #0f172a; stroke-width: 2.5; }
|
|
447
|
+
.node text { user-select: none; pointer-events: none; font-weight: 700; text-shadow: 0 1px 4px rgba(0,0,0,0.9); }
|
|
448
|
+
.node:hover circle { stroke: #fbbf24 !important; stroke-width: 5 !important; filter: drop-shadow(0 0 14px #fbbf24); }
|
|
449
|
+
.node-aspec { fill: #3b82f6; stroke: #60a5fa !important; }
|
|
450
|
+
.node-hspec { fill: #8b5cf6; stroke: #c084fc !important; }
|
|
451
|
+
.node-req { fill: #ec4899; stroke: #f472b6 !important; }
|
|
452
|
+
.node-tspec { fill: #06b6d4; stroke: #67e8f9 !important; }
|
|
453
|
+
.node-code { fill: #10b981; stroke: #34d399 !important; }
|
|
454
|
+
.node-symbol { fill: #f59e0b; stroke: #fbbf24 !important; stroke-width: 3 !important; filter: drop-shadow(0 0 8px #f59e0b); }
|
|
455
|
+
.node-badge { fill: #a855f7 !important; stroke: #c084fc !important; }
|
|
456
|
+
|
|
457
|
+
.edge { stroke-width: 3.5; opacity: 0.95; fill: none; cursor: pointer; pointer-events: stroke; transition: all 0.15s ease; }
|
|
458
|
+
.edge:hover { stroke: #fbbf24 !important; stroke-width: 6 !important; opacity: 1 !important; filter: drop-shadow(0 0 10px #fbbf24); }
|
|
459
|
+
.edge-depends_on { stroke: #a855f7; marker-end: url(#arrow-depends_on); }
|
|
460
|
+
.edge-implements { stroke: #3b82f6; marker-end: url(#arrow-implements); }
|
|
461
|
+
.edge-verifies { stroke: #06b6d4; stroke-dasharray: 6,4; marker-end: url(#arrow-verifies); }
|
|
462
|
+
.edge-contains { stroke: #10b981; stroke-dasharray: 4,3; opacity: 0.85; marker-end: url(#arrow-contains); }
|
|
463
|
+
.edge-calls { stroke: #f59e0b; stroke-width: 4; marker-end: url(#arrow-calls); filter: drop-shadow(0 0 4px #f59e0b); }
|
|
464
|
+
|
|
465
|
+
.legend { display: flex; gap: 16px; font-size: 12px; color: #94a3b8; margin-top: 12px; flex-wrap: wrap; }
|
|
466
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
467
|
+
.legend-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
|
468
|
+
.controls { margin-bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
469
|
+
.select-box { padding: 8px 12px; border-radius: 6px; border: 1px solid #334155; background: #1e293b; color: #fff; font-size: 13px; }
|
|
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); }
|
|
481
|
+
.popover-card.active { opacity: 1; pointer-events: auto; }
|
|
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; }
|
|
487
|
+
|
|
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); }
|
|
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; }
|
|
490
|
+
.tool-btn:hover { background: #3b82f6; border-color: #3b82f6; color: #fff; transform: scale(1.05); }
|
|
491
|
+
.ux-hint-banner { background: #1e293b; border-left: 4px solid #ec4899; color: #cbd5e1; padding: 10px 14px; border-radius: 6px; font-size: 12px; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; }
|
|
492
|
+
</style>
|
|
493
|
+
</head>
|
|
494
|
+
<body>
|
|
495
|
+
<div class="header">
|
|
496
|
+
<div>
|
|
497
|
+
<h1 style="margin:0; font-size: 24px;">🔥 REQ-Grouped Pipeline Matrix & AST/CPG Canvas</h1>
|
|
498
|
+
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">REQ Master Section Blocks • Multi-Lens Quick Filter Bar • Full-collection CPG call graph (canvas shows a bounded subgraph)</div>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="axis-stats">
|
|
501
|
+
<span class="axis-stat" title="Specs signed off, over specs using the canonical status vocabulary">📋 Approval <span id="approvalStat" class="stat-badge">--%</span></span>
|
|
502
|
+
<span class="axis-stat" title="A-SPECs with code anchored to them, over canonical A-SPECs">⚙️ Implementation <span id="implStat" class="stat-badge">--%</span></span>
|
|
503
|
+
<span class="axis-stat legacy-chip" id="legacyChip" title="Specs whose status is outside draft|review|approved|outdated — excluded from both axes, not scored zero" onclick="switchTab('legacy')">🏷️ <span id="legacyStat">--</span></span>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div class="nav-tabs">
|
|
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>
|
|
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>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
<div id="rtmTab">
|
|
515
|
+
<div class="ux-hint-banner">
|
|
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>
|
|
517
|
+
</div>
|
|
518
|
+
<input type="text" id="searchInput" class="search-box" placeholder="Search business REQ requirements by ID or title..." />
|
|
519
|
+
<div id="specGrid" class="rtm-container">Loading REQ specifications...</div>
|
|
520
|
+
</div>
|
|
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
|
+
|
|
529
|
+
<div id="heatmapTab" style="display: none;">
|
|
530
|
+
<div class="ux-hint-banner" style="border-left-color: #f59e0b;">
|
|
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>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="heatmap-wrapper">
|
|
552
|
+
<div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div id="graphTab" style="display: none;">
|
|
557
|
+
<div class="breadcrumb-bar" id="breadcrumbBar">
|
|
558
|
+
<span style="color: #94a3b8; font-weight: bold;">🧭 Traversal History:</span>
|
|
559
|
+
<span id="breadcrumbTrail" style="display: flex; gap: 6px; align-items: center;">Select any node or click any edge to jump...</span>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="controls">
|
|
563
|
+
<label style="font-size: 13px; color: #ef4444; font-weight: bold;">⚡ Max Node Cap:</label>
|
|
564
|
+
<select id="maxNodeCap" class="select-box" style="border-color: #ef4444; font-weight: bold;" onchange="filterGraphByScope()">
|
|
565
|
+
<option value="30" selected>✨ Fast Insight (Max 30 Nodes - 60 FPS)</option>
|
|
566
|
+
<option value="50">⚡ Balanced (Max 50 Nodes - Smooth)</option>
|
|
567
|
+
<option value="100">🔍 Extended (Max 100 Nodes)</option>
|
|
568
|
+
<option value="200">🌐 High Density (Max 200 Nodes)</option>
|
|
569
|
+
</select>
|
|
570
|
+
<label style="font-size: 13px; color: #f59e0b; font-weight: bold;">🔮 Graph Paradigm:</label>
|
|
571
|
+
<select id="layoutEngine" class="select-box" style="border-color: #f59e0b; font-weight: bold;" onchange="filterGraphByScope()">
|
|
572
|
+
<option value="neo4j" selected>🔮 1. Neo4j Bloom Style: Domain Meta-Cluster Galaxy (Recommended)</option>
|
|
573
|
+
<option value="palantir">🌊 2. Palantir Style: Concentric Sunburst 360° Radial Graph</option>
|
|
574
|
+
<option value="sugiyama">🌳 3. Sugiyama Top-to-Bottom Layered Rank Tree</option>
|
|
575
|
+
<option value="pipeline">📊 4. Left-to-Right 6-Column Pipeline View</option>
|
|
576
|
+
</select>
|
|
577
|
+
<label style="font-size: 13px; color: #38bdf8; font-weight: bold;">View Scope:</label>
|
|
578
|
+
<select id="scopeFilter" class="select-box" style="border-color: #38bdf8; font-weight: bold;" onchange="filterGraphByScope()">
|
|
579
|
+
<option value="focused" selected>🎯 Focused Subgraph View (Selected Root Node & Relevance Bounded)</option>
|
|
580
|
+
<option value="ast_focus">🔬 AST/CPG Code Symbol Focus</option>
|
|
581
|
+
<option value="full">🌐 Full SDLC Pipeline Canvas (All Project Nodes)</option>
|
|
582
|
+
</select>
|
|
583
|
+
<label style="font-size: 13px; color: #94a3b8;">Max Depth:</label>
|
|
584
|
+
<select id="depthFilter" class="select-box" onchange="filterGraphByScope()">
|
|
585
|
+
<option value="3" selected>3-Hop Depth (Fastest Insight)</option>
|
|
586
|
+
<option value="7">7-Hop Max Subgraph Closure Depth</option>
|
|
587
|
+
<option value="5">5-Hop Depth</option>
|
|
588
|
+
<option value="2">2-Hop Depth</option>
|
|
589
|
+
<option value="1">1-Hop Direct Neighbors</option>
|
|
590
|
+
</select>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<div id="graphScopeNotice" class="scope-notice"></div>
|
|
594
|
+
<div id="graphCanvasContainer">
|
|
595
|
+
<svg id="graphSvg" class="graph-svg"></svg>
|
|
596
|
+
|
|
597
|
+
<!-- Interactive Connected Neighbors Traversal Inspector Drawer -->
|
|
598
|
+
<div id="traversalInspector" class="traversal-drawer">
|
|
599
|
+
<div class="drawer-header">
|
|
600
|
+
<div class="drawer-title" id="inspectorNodeId">Select a Node</div>
|
|
601
|
+
<span id="inspectorNodeType" style="font-size: 11px; background: #334155; padding: 2px 8px; border-radius: 4px; font-weight: bold;">--</span>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div id="inspectorDetails" style="font-size: 12px; color: #cbd5e1;">
|
|
605
|
+
Click any node or edge on the canvas to inspect its direct, peer, and transitive neighbor linkages.
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div id="inspectorNeighborsContainer" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;">
|
|
609
|
+
<!-- Populated dynamically on node selection -->
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<div class="canvas-toolbar">
|
|
614
|
+
<button class="tool-btn" onclick="zoomIn()" title="Zoom In (Scale +30%)">➕</button>
|
|
615
|
+
<button class="tool-btn" onclick="zoomOut()" title="Zoom Out (Scale -30%)">➖</button>
|
|
616
|
+
<button class="tool-btn" onclick="resetFitView()" title="Fit View to Screen">🎯</button>
|
|
617
|
+
<button class="tool-btn" onclick="resetZoom()" title="Reset Zoom to 100%">🔄</button>
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<!-- World Top-Tier Rich Hover Popover Card -->
|
|
621
|
+
<div id="popoverCard" class="popover-card">
|
|
622
|
+
<div id="popoverBadge" class="popover-badge">--</div>
|
|
623
|
+
<div id="popoverTitle" class="popover-title">--</div>
|
|
624
|
+
<div id="popoverSection1" class="popover-section"></div>
|
|
625
|
+
<div id="popoverSection2" class="popover-section"></div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
<div class="legend">
|
|
629
|
+
<div class="legend-item"><span class="legend-dot" style="background: #ec4899;"></span> REQ Business Spec</div>
|
|
630
|
+
<div class="legend-item"><span class="legend-dot" style="background: #8b5cf6;"></span> H-SPEC Functional</div>
|
|
631
|
+
<div class="legend-item"><span class="legend-dot" style="background: #3b82f6;"></span> A-SPEC Architecture</div>
|
|
632
|
+
<div class="legend-item"><span class="legend-dot" style="background: #06b6d4;"></span> T-SPEC Test Spec</div>
|
|
633
|
+
<div class="legend-item"><span class="legend-dot" style="background: #10b981;"></span> Source Code File</div>
|
|
634
|
+
<div class="legend-item"><span class="legend-dot" style="background: #f59e0b; border: 1.5px solid #fbbf24; box-shadow: 0 0 8px #f59e0b;"></span> ⚡ AST Function Symbol</div>
|
|
635
|
+
<div class="legend-item"><span style="color:#3b82f6; font-weight:bold;">➔ implements</span></div>
|
|
636
|
+
<div class="legend-item"><span style="color:#06b6d4; font-weight:bold;">➔ verifies</span></div>
|
|
637
|
+
<div class="legend-item"><span style="color:#a855f7; font-weight:bold;">➔ depends_on</span></div>
|
|
638
|
+
<div class="legend-item"><span style="color:#f59e0b; font-weight:bold;">➔ calls</span></div>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<script>
|
|
643
|
+
function escapeHtml(str) {
|
|
644
|
+
if (!str) return '';
|
|
645
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function getIdStr(obj) {
|
|
649
|
+
if (!obj) return '';
|
|
650
|
+
if (typeof obj === 'string') return obj;
|
|
651
|
+
if (typeof obj === 'object' && obj.id) return obj.id;
|
|
652
|
+
return String(obj);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function getNodeCategory(id) {
|
|
656
|
+
if (id.startsWith('REQ')) return 'REQ';
|
|
657
|
+
if (id.startsWith('H-SPEC')) return 'H-SPEC';
|
|
658
|
+
if (id.startsWith('A-SPEC')) return 'A-SPEC';
|
|
659
|
+
if (id.startsWith('T-SPEC')) return 'T-SPEC';
|
|
660
|
+
if (id.includes('#')) return 'SYMBOL';
|
|
661
|
+
return 'FILE';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getDomainModule(id) {
|
|
665
|
+
if (id.startsWith('REQ') || id.startsWith('H-SPEC') || id.startsWith('A-SPEC') || id.startsWith('T-SPEC')) return 'spec';
|
|
666
|
+
if (id.includes('/cpg/')) return 'cpg';
|
|
667
|
+
if (id.includes('/server/')) return 'server';
|
|
668
|
+
if (id.includes('/review/')) return 'review';
|
|
669
|
+
if (id.includes('/rtm/')) return 'rtm';
|
|
670
|
+
if (id.includes('/reverse/')) return 'reverse';
|
|
671
|
+
return 'core';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let allSpecs = [];
|
|
675
|
+
let allFiles = [];
|
|
676
|
+
let graphData = null;
|
|
677
|
+
let heatmapData = null;
|
|
678
|
+
let currentZoom = null;
|
|
679
|
+
let svgG = null;
|
|
680
|
+
let traversalHistory = [];
|
|
681
|
+
let selectedRootNodeId = 'A-SPEC-219';
|
|
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
|
+
|
|
720
|
+
function switchTab(tab) {
|
|
721
|
+
document.getElementById('tabRtmBtn').classList.toggle('active', tab === 'rtm');
|
|
722
|
+
document.getElementById('tabHeatmapBtn').classList.toggle('active', tab === 'heatmap');
|
|
723
|
+
document.getElementById('tabGraphBtn').classList.toggle('active', tab === 'graph');
|
|
724
|
+
document.getElementById('tabLegacyBtn').classList.toggle('active', tab === 'legacy');
|
|
725
|
+
|
|
726
|
+
document.getElementById('rtmTab').style.display = tab === 'rtm' ? 'block' : 'none';
|
|
727
|
+
document.getElementById('heatmapTab').style.display = tab === 'heatmap' ? 'block' : 'none';
|
|
728
|
+
document.getElementById('graphTab').style.display = tab === 'graph' ? 'block' : 'none';
|
|
729
|
+
document.getElementById('legacyTab').style.display = tab === 'legacy' ? 'block' : 'none';
|
|
730
|
+
|
|
731
|
+
if (tab === 'graph' && !graphData) {
|
|
732
|
+
loadGraphCanvas();
|
|
733
|
+
} else if (tab === 'heatmap' && !heatmapData) {
|
|
734
|
+
loadMicroHeatmap();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function loadDashboard() {
|
|
739
|
+
try {
|
|
740
|
+
const res = await fetch('/api/rtm');
|
|
741
|
+
const data = await res.json();
|
|
742
|
+
if (data.ok) {
|
|
743
|
+
allSpecs = data.specs;
|
|
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);
|
|
754
|
+
}
|
|
755
|
+
} catch (err) {
|
|
756
|
+
document.getElementById('specGrid').innerText = 'Failed to load specs: ' + escapeHtml(err.message || err);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function loadMicroHeatmap() {
|
|
761
|
+
try {
|
|
762
|
+
const res = await fetch('/api/rtm/heatmap');
|
|
763
|
+
const data = await res.json();
|
|
764
|
+
if (data.ok) {
|
|
765
|
+
heatmapData = data;
|
|
766
|
+
render6StagePipelineMatrix(data);
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
document.getElementById('heatmapContainer').innerText = 'Failed to load pipeline heatmap: ' + escapeHtml(err.message || err);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function changeHeatmapLens() {
|
|
774
|
+
if (heatmapData) {
|
|
775
|
+
render6StagePipelineMatrix(heatmapData);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
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
|
|
790
|
+
function render6StagePipelineMatrix(data) {
|
|
791
|
+
const container = document.getElementById('heatmapContainer');
|
|
792
|
+
const rawPipelines = data.pipelines || [];
|
|
793
|
+
const lens = document.getElementById('lensSelect').value || 'lens1';
|
|
794
|
+
const statusFilter = document.getElementById('pipelineFilterSelect').value || 'all';
|
|
795
|
+
|
|
796
|
+
if (rawPipelines.length === 0) {
|
|
797
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
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
|
+
}
|
|
808
|
+
|
|
809
|
+
// Group pipelines by reqId
|
|
810
|
+
const groupedMap = new Map();
|
|
811
|
+
pipelines.forEach(p => {
|
|
812
|
+
if (!groupedMap.has(p.reqId)) {
|
|
813
|
+
groupedMap.set(p.reqId, { reqId: p.reqId, reqTitle: p.reqTitle, items: [] });
|
|
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>';
|
|
841
|
+
|
|
842
|
+
html += \`
|
|
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>
|
|
913
|
+
\`;
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
container.innerHTML = html;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
|
|
920
|
+
function renderReqQuantitativeGrid(specs, files) {
|
|
921
|
+
const container = document.getElementById('specGrid');
|
|
922
|
+
const reqList = specs.filter(s => s.id.startsWith('REQ-'));
|
|
923
|
+
|
|
924
|
+
if (reqList.length === 0) {
|
|
925
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No business REQ specifications found.</div>';
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
container.innerHTML = reqList.map(req => {
|
|
930
|
+
const numPart = req.id.replace('REQ-', '').split('.')[0];
|
|
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
|
+
});
|
|
948
|
+
|
|
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>';
|
|
958
|
+
|
|
959
|
+
return \`
|
|
960
|
+
<div class="req-card" id="card-\${escapeHtml(req.id)}">
|
|
961
|
+
<div class="req-header" onclick="toggleAccordion('body-\${escapeHtml(req.id)}')">
|
|
962
|
+
<div class="req-title-group">
|
|
963
|
+
<span class="req-id-badge">\${escapeHtml(req.id)}</span>
|
|
964
|
+
<span class="req-title">\${escapeHtml(req.title || 'Business Requirement')}</span>
|
|
965
|
+
\${healthBadge}
|
|
966
|
+
</div>
|
|
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>
|
|
975
|
+
</div>
|
|
976
|
+
<button class="expand-btn">▶ Expand Matrix Tree</button>
|
|
977
|
+
<button class="view-graph-btn" onclick="event.stopPropagation(); focusSpecInGraph('\${escapeHtml(req.id)}')">🕸️ View in Graph</button>
|
|
978
|
+
</div>
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
<div class="accordion-body" id="body-\${escapeHtml(req.id)}">
|
|
982
|
+
\${hspecs.map(h => \`
|
|
983
|
+
<div class="tree-row tree-row-hspec">
|
|
984
|
+
<div class="tree-label">🟣 <strong>\${escapeHtml(h.id)}</strong>: \${escapeHtml(h.title || '')}</div>
|
|
985
|
+
<div>
|
|
986
|
+
<span class="neighbor-rel \${h.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${h.covered ? 'APPROVED' : 'ACTIVE'}</span>
|
|
987
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(h.id)}')">🕸️ View in Graph</button>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
\`).join('')}
|
|
991
|
+
|
|
992
|
+
\${aspecs.map(a => \`
|
|
993
|
+
<div class="tree-row tree-row-aspec">
|
|
994
|
+
<div class="tree-label">🔵 <strong>\${escapeHtml(a.id)}</strong>: \${escapeHtml(a.title || '')}</div>
|
|
995
|
+
<div>
|
|
996
|
+
<span class="neighbor-rel \${a.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${a.covered ? 'COVERED' : 'UNCOVERED'}</span>
|
|
997
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(a.id)}')">🕸️ View in Graph</button>
|
|
998
|
+
</div>
|
|
999
|
+
</div>
|
|
1000
|
+
\`).join('')}
|
|
1001
|
+
|
|
1002
|
+
\${tspecs.map(t => \`
|
|
1003
|
+
<div class="tree-row tree-row-tspec">
|
|
1004
|
+
<div class="tree-label">🔷 <strong>\${escapeHtml(t.id)}</strong>: \${escapeHtml(t.title || '')}</div>
|
|
1005
|
+
<div>
|
|
1006
|
+
<span class="neighbor-rel \${t.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${t.covered ? 'VERIFIED' : 'ACTIVE'}</span>
|
|
1007
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(t.id)}')">🕸️ View in Graph</button>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
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('')}
|
|
1034
|
+
</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
\`;
|
|
1037
|
+
}).join('');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function toggleAccordion(bodyId) {
|
|
1041
|
+
const body = document.getElementById(bodyId);
|
|
1042
|
+
if (body) {
|
|
1043
|
+
body.classList.toggle('open');
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function focusSpecInGraph(specId) {
|
|
1048
|
+
switchTab('graph');
|
|
1049
|
+
if (!graphData) {
|
|
1050
|
+
await loadGraphCanvas();
|
|
1051
|
+
}
|
|
1052
|
+
selectAndTraverseNode(specId);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
1056
|
+
const q = e.target.value.toLowerCase();
|
|
1057
|
+
const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
|
|
1058
|
+
renderReqQuantitativeGrid(filtered, allFiles);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
async function loadGraphCanvas() {
|
|
1062
|
+
try {
|
|
1063
|
+
const res = await fetch('/api/graph');
|
|
1064
|
+
const data = await res.json();
|
|
1065
|
+
if (data.ok) {
|
|
1066
|
+
graphData = data;
|
|
1067
|
+
selectAndTraverseNode(selectedRootNodeId);
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
console.error('Failed to load graph:', err);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function setupSvgDefs(svg) {
|
|
1075
|
+
let defs = svg.select('defs');
|
|
1076
|
+
if (defs.empty()) {
|
|
1077
|
+
defs = svg.append('defs');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const markers = [
|
|
1081
|
+
{ id: 'arrow-depends_on', color: '#a855f7' },
|
|
1082
|
+
{ id: 'arrow-implements', color: '#3b82f6' },
|
|
1083
|
+
{ id: 'arrow-verifies', color: '#06b6d4' },
|
|
1084
|
+
{ id: 'arrow-contains', color: '#10b981' },
|
|
1085
|
+
{ id: 'arrow-calls', color: '#f59e0b' }
|
|
1086
|
+
];
|
|
1087
|
+
|
|
1088
|
+
markers.forEach(m => {
|
|
1089
|
+
if (defs.select('#' + m.id).empty()) {
|
|
1090
|
+
defs.append('marker')
|
|
1091
|
+
.attr('id', m.id)
|
|
1092
|
+
.attr('viewBox', '0 -5 10 10')
|
|
1093
|
+
.attr('refX', 22)
|
|
1094
|
+
.attr('refY', 0)
|
|
1095
|
+
.attr('markerWidth', 7)
|
|
1096
|
+
.attr('markerHeight', 7)
|
|
1097
|
+
.attr('orient', 'auto')
|
|
1098
|
+
.append('path')
|
|
1099
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
1100
|
+
.attr('fill', m.color);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
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
|
+
|
|
1121
|
+
function filterGraphByScope() {
|
|
1122
|
+
if (!graphData) return;
|
|
1123
|
+
const scope = document.getElementById('scopeFilter').value;
|
|
1124
|
+
const maxDepth = parseInt(document.getElementById('depthFilter').value, 10) || 3;
|
|
1125
|
+
const maxNodeCap = parseInt(document.getElementById('maxNodeCap').value, 10) || 30;
|
|
1126
|
+
|
|
1127
|
+
let filteredNodes = [];
|
|
1128
|
+
let filteredEdges = [];
|
|
1129
|
+
|
|
1130
|
+
if (scope === 'full') {
|
|
1131
|
+
filteredNodes = graphData.nodes.slice(0, maxNodeCap);
|
|
1132
|
+
const nodeSet = new Set(filteredNodes.map(n => n.id));
|
|
1133
|
+
filteredEdges = graphData.edges.filter(e => nodeSet.has(getIdStr(e.from)) && nodeSet.has(getIdStr(e.to)));
|
|
1134
|
+
} else {
|
|
1135
|
+
const rootId = selectedRootNodeId || 'A-SPEC-219';
|
|
1136
|
+
const visited = new Map();
|
|
1137
|
+
visited.set(rootId, 0);
|
|
1138
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
1139
|
+
|
|
1140
|
+
const adj = new Map();
|
|
1141
|
+
graphData.nodes.forEach(n => adj.set(n.id, new Set()));
|
|
1142
|
+
graphData.edges.forEach(e => {
|
|
1143
|
+
const u = getIdStr(e.from);
|
|
1144
|
+
const v = getIdStr(e.to);
|
|
1145
|
+
if (adj.has(u)) adj.get(u).add(v);
|
|
1146
|
+
if (adj.has(v)) adj.get(v).add(u);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Bounded Relevance BFS (Max Node Cap Enforcement)
|
|
1150
|
+
const collectedNodeIds = [];
|
|
1151
|
+
while (queue.length > 0 && collectedNodeIds.length < maxNodeCap) {
|
|
1152
|
+
const { id, depth } = queue.shift();
|
|
1153
|
+
collectedNodeIds.push(id);
|
|
1154
|
+
|
|
1155
|
+
if (depth >= maxDepth) continue;
|
|
1156
|
+
|
|
1157
|
+
const neighbors = adj.get(id) || [];
|
|
1158
|
+
for (const nxt of neighbors) {
|
|
1159
|
+
if (!visited.has(nxt)) {
|
|
1160
|
+
visited.set(nxt, depth + 1);
|
|
1161
|
+
queue.push({ id: nxt, depth: depth + 1 });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const bSet = new Set(collectedNodeIds);
|
|
1167
|
+
filteredNodes = graphData.nodes.filter(n => bSet.has(n.id));
|
|
1168
|
+
filteredEdges = graphData.edges.filter(e => bSet.has(getIdStr(e.from)) && bSet.has(getIdStr(e.to)));
|
|
1169
|
+
}
|
|
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
|
+
|
|
1175
|
+
const engine = document.getElementById('layoutEngine').value;
|
|
1176
|
+
if (engine === 'neo4j') {
|
|
1177
|
+
renderNeo4jBloomDomainGalaxy(filteredNodes, filteredEdges);
|
|
1178
|
+
} else if (engine === 'palantir') {
|
|
1179
|
+
renderPalantirConcentricRadial(filteredNodes, filteredEdges);
|
|
1180
|
+
} else if (engine === 'sugiyama') {
|
|
1181
|
+
renderSugiyamaTopToBottom(filteredNodes, filteredEdges);
|
|
1182
|
+
} else {
|
|
1183
|
+
renderPipelineGraph(filteredNodes, filteredEdges);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
setTimeout(() => resetFitView(), 120);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function zoomIn() {
|
|
1190
|
+
if (window.d3 && currentZoom) {
|
|
1191
|
+
d3.select('#graphSvg').transition().duration(300).call(currentZoom.scaleBy, 1.3);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function zoomOut() {
|
|
1196
|
+
if (window.d3 && currentZoom) {
|
|
1197
|
+
d3.select('#graphSvg').transition().duration(300).call(currentZoom.scaleBy, 0.7);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function resetZoom() {
|
|
1202
|
+
if (window.d3 && currentZoom) {
|
|
1203
|
+
d3.select('#graphSvg').transition().duration(400).call(currentZoom.transform, d3.zoomIdentity);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function resetFitView() {
|
|
1208
|
+
if (!window.d3 || !svgG || !currentZoom) return;
|
|
1209
|
+
const svg = d3.select('#graphSvg');
|
|
1210
|
+
const container = document.getElementById('graphSvg');
|
|
1211
|
+
const width = container.clientWidth || 1000;
|
|
1212
|
+
const height = container.clientHeight || 800;
|
|
1213
|
+
|
|
1214
|
+
const bounds = svgG.node().getBBox();
|
|
1215
|
+
if (!bounds || bounds.width === 0 || bounds.height === 0) return;
|
|
1216
|
+
|
|
1217
|
+
const fullWidth = bounds.width;
|
|
1218
|
+
const fullHeight = bounds.height;
|
|
1219
|
+
const midX = bounds.x + fullWidth / 2;
|
|
1220
|
+
const midY = bounds.y + fullHeight / 2;
|
|
1221
|
+
|
|
1222
|
+
const scale = 0.85 / Math.max(fullWidth / width, fullHeight / height);
|
|
1223
|
+
const translate = [width / 2 - scale * midX, height / 2 - scale * midY];
|
|
1224
|
+
|
|
1225
|
+
svg.transition().duration(750).call(
|
|
1226
|
+
currentZoom.transform,
|
|
1227
|
+
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 🔮 1. Neo4j Bloom Style: Domain Meta-Cluster Galaxy Engine
|
|
1232
|
+
function renderNeo4jBloomDomainGalaxy(rawNodes, rawEdges) {
|
|
1233
|
+
const container = document.getElementById('graphSvg');
|
|
1234
|
+
container.innerHTML = '';
|
|
1235
|
+
const width = container.clientWidth || 1100;
|
|
1236
|
+
const height = container.clientHeight || 800;
|
|
1237
|
+
|
|
1238
|
+
const svg = d3.select('#graphSvg');
|
|
1239
|
+
setupSvgDefs(svg);
|
|
1240
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1241
|
+
|
|
1242
|
+
currentZoom = d3.zoom()
|
|
1243
|
+
.scaleExtent([0.05, 8])
|
|
1244
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1245
|
+
|
|
1246
|
+
svg.call(currentZoom);
|
|
1247
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1248
|
+
|
|
1249
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1250
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1251
|
+
|
|
1252
|
+
const validEdges = rawEdges
|
|
1253
|
+
.map(e => ({ source: getIdStr(e.from || e.source), target: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1254
|
+
.filter(e => validNodeIds.has(e.source) && validNodeIds.has(e.target));
|
|
1255
|
+
|
|
1256
|
+
const degreeMap = new Map();
|
|
1257
|
+
validEdges.forEach(e => {
|
|
1258
|
+
degreeMap.set(e.source, (degreeMap.get(e.source) || 0) + 1);
|
|
1259
|
+
degreeMap.set(e.target, (degreeMap.get(e.target) || 0) + 1);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const domainCenters = {
|
|
1263
|
+
spec: { x: width * 0.3, y: height * 0.25, color: '#3b82f6', label: '🔮 Domain: Governance Specs' },
|
|
1264
|
+
cpg: { x: width * 0.7, y: height * 0.25, color: '#f59e0b', label: '⚡ Domain: CPG Static Code Analysis' },
|
|
1265
|
+
server: { x: width * 0.3, y: height * 0.75, color: '#10b981', label: '🖥️ Domain: Server & Dashboard' },
|
|
1266
|
+
review: { x: width * 0.7, y: height * 0.75, color: '#8b5cf6', label: '🔬 Domain: Review & Remediation' },
|
|
1267
|
+
core: { x: width * 0.5, y: height * 0.5, color: '#ec4899', label: '⚛️ Domain: Core Infrastructure' }
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
Object.values(domainCenters).forEach(c => {
|
|
1271
|
+
svgG.append('circle')
|
|
1272
|
+
.attr('cx', c.x)
|
|
1273
|
+
.attr('cy', c.y)
|
|
1274
|
+
.attr('r', 130)
|
|
1275
|
+
.attr('fill', c.color)
|
|
1276
|
+
.attr('opacity', 0.04);
|
|
1277
|
+
|
|
1278
|
+
svgG.append('text')
|
|
1279
|
+
.attr('x', c.x)
|
|
1280
|
+
.attr('y', c.y - 135)
|
|
1281
|
+
.attr('fill', c.color)
|
|
1282
|
+
.attr('font-size', '13px')
|
|
1283
|
+
.attr('font-weight', 'bold')
|
|
1284
|
+
.attr('text-anchor', 'middle')
|
|
1285
|
+
.text(c.label);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
const simulation = d3.forceSimulation(nodes)
|
|
1289
|
+
.force('link', d3.forceLink(validEdges).id(d => d.id).distance(100))
|
|
1290
|
+
.force('charge', d3.forceManyBody().strength(-350))
|
|
1291
|
+
.force('collide', d3.forceCollide(d => (degreeMap.get(d.id) || 1) * 2 + 25))
|
|
1292
|
+
.force('x', d3.forceX(d => (domainCenters[getDomainModule(d.id)] || domainCenters.core).x).strength(0.3))
|
|
1293
|
+
.force('y', d3.forceY(d => (domainCenters[getDomainModule(d.id)] || domainCenters.core).y).strength(0.3));
|
|
1294
|
+
|
|
1295
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1296
|
+
|
|
1297
|
+
const pathElements = edgeGroup.selectAll('path')
|
|
1298
|
+
.data(validEdges)
|
|
1299
|
+
.enter()
|
|
1300
|
+
.append('path')
|
|
1301
|
+
.attr('class', d => 'edge edge-' + d.rel)
|
|
1302
|
+
.attr('data-from', d => getIdStr(d.source))
|
|
1303
|
+
.attr('data-to', d => getIdStr(d.target))
|
|
1304
|
+
.on('click', (event, d) => {
|
|
1305
|
+
event.stopPropagation();
|
|
1306
|
+
selectAndTraverseNode(getIdStr(d.target));
|
|
1307
|
+
})
|
|
1308
|
+
.on('mouseenter', (event, d) => showEdgePopover(event, d))
|
|
1309
|
+
.on('mouseleave', hidePopover);
|
|
1310
|
+
|
|
1311
|
+
const nodeGroups = svgG.append('g')
|
|
1312
|
+
.selectAll('g')
|
|
1313
|
+
.data(nodes)
|
|
1314
|
+
.enter()
|
|
1315
|
+
.append('g')
|
|
1316
|
+
.attr('class', 'node')
|
|
1317
|
+
.attr('data-id', d => d.id)
|
|
1318
|
+
.call(d3.drag()
|
|
1319
|
+
.on('start', (event, d) => {
|
|
1320
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1321
|
+
d.fx = d.x; d.fy = d.y;
|
|
1322
|
+
})
|
|
1323
|
+
.on('drag', (event, d) => {
|
|
1324
|
+
d.fx = event.x; d.fy = event.y;
|
|
1325
|
+
})
|
|
1326
|
+
.on('end', (event, d) => {
|
|
1327
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
1328
|
+
d.fx = null; d.fy = null;
|
|
1329
|
+
}));
|
|
1330
|
+
|
|
1331
|
+
nodeGroups.append('circle')
|
|
1332
|
+
.attr('r', d => {
|
|
1333
|
+
if (d.kind === 'badge') return 16;
|
|
1334
|
+
const deg = degreeMap.get(d.id) || 1;
|
|
1335
|
+
return Math.min(24, 10 + deg * 1.8);
|
|
1336
|
+
})
|
|
1337
|
+
.attr('class', d => {
|
|
1338
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1339
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1340
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1341
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1342
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1343
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1344
|
+
return 'node-code';
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
nodeGroups.append('text')
|
|
1348
|
+
.attr('x', 0)
|
|
1349
|
+
.attr('y', 24)
|
|
1350
|
+
.attr('text-anchor', 'middle')
|
|
1351
|
+
.attr('fill', '#f8fafc')
|
|
1352
|
+
.attr('font-size', '11px')
|
|
1353
|
+
.text(d => d.id.length > 22 ? d.id.slice(0, 20) + '...' : d.id);
|
|
1354
|
+
|
|
1355
|
+
nodeGroups
|
|
1356
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1357
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1358
|
+
.on('mouseleave', hidePopover);
|
|
1359
|
+
|
|
1360
|
+
simulation.on('tick', () => {
|
|
1361
|
+
pathElements.attr('d', d => \`M \${d.source.x} \${d.source.y} L \${d.target.x} \${d.target.y}\`);
|
|
1362
|
+
nodeGroups.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// 🌊 2. Palantir Style: Concentric Sunburst 360° Radial Graph Engine
|
|
1367
|
+
function renderPalantirConcentricRadial(rawNodes, rawEdges) {
|
|
1368
|
+
const container = document.getElementById('graphSvg');
|
|
1369
|
+
container.innerHTML = '';
|
|
1370
|
+
const width = container.clientWidth || 1100;
|
|
1371
|
+
const height = container.clientHeight || 800;
|
|
1372
|
+
|
|
1373
|
+
const svg = d3.select('#graphSvg');
|
|
1374
|
+
setupSvgDefs(svg);
|
|
1375
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1376
|
+
|
|
1377
|
+
currentZoom = d3.zoom()
|
|
1378
|
+
.scaleExtent([0.05, 8])
|
|
1379
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1380
|
+
|
|
1381
|
+
svg.call(currentZoom);
|
|
1382
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1383
|
+
|
|
1384
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1385
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1386
|
+
|
|
1387
|
+
const validEdges = rawEdges
|
|
1388
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1389
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1390
|
+
|
|
1391
|
+
const rootId = selectedRootNodeId || 'A-SPEC-219';
|
|
1392
|
+
|
|
1393
|
+
const ringDepthMap = new Map();
|
|
1394
|
+
ringDepthMap.set(rootId, 0);
|
|
1395
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
1396
|
+
|
|
1397
|
+
const adj = new Map();
|
|
1398
|
+
nodes.forEach(n => adj.set(n.id, new Set()));
|
|
1399
|
+
validEdges.forEach(e => {
|
|
1400
|
+
if (adj.has(e.from)) adj.get(e.from).add(e.to);
|
|
1401
|
+
if (adj.has(e.to)) adj.get(e.to).add(e.from);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
while (queue.length > 0) {
|
|
1405
|
+
const { id, depth } = queue.shift();
|
|
1406
|
+
const neighbors = adj.get(id) || [];
|
|
1407
|
+
for (const nxt of neighbors) {
|
|
1408
|
+
if (!ringDepthMap.has(nxt)) {
|
|
1409
|
+
ringDepthMap.set(nxt, depth + 1);
|
|
1410
|
+
queue.push({ id: nxt, depth: depth + 1 });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const ringGroups = { 0: [], 1: [], 2: [], 3: [] };
|
|
1416
|
+
nodes.forEach(n => {
|
|
1417
|
+
const d = Math.min(3, ringDepthMap.get(n.id) || 3);
|
|
1418
|
+
ringGroups[d].push(n);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
const centerX = width / 2;
|
|
1422
|
+
const centerY = height / 2;
|
|
1423
|
+
const ringRadii = { 0: 0, 1: 180, 2: 360, 3: 540 };
|
|
1424
|
+
|
|
1425
|
+
[180, 360, 540].forEach((r, idx) => {
|
|
1426
|
+
svgG.append('circle')
|
|
1427
|
+
.attr('cx', centerX)
|
|
1428
|
+
.attr('cy', centerY)
|
|
1429
|
+
.attr('r', r)
|
|
1430
|
+
.attr('fill', 'none')
|
|
1431
|
+
.attr('stroke', '#334155')
|
|
1432
|
+
.attr('stroke-dasharray', '6,6')
|
|
1433
|
+
.attr('opacity', 0.5);
|
|
1434
|
+
|
|
1435
|
+
svgG.append('text')
|
|
1436
|
+
.attr('x', centerX + r + 8)
|
|
1437
|
+
.attr('y', centerY)
|
|
1438
|
+
.attr('fill', idx === 0 ? '#ef4444' : idx === 1 ? '#f97316' : '#eab308')
|
|
1439
|
+
.attr('font-size', '12px')
|
|
1440
|
+
.attr('font-weight', 'bold')
|
|
1441
|
+
.text((idx + 1) + '-Hop Orbit Ring');
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
const nodePositions = new Map();
|
|
1445
|
+
|
|
1446
|
+
Object.keys(ringGroups).forEach(k => {
|
|
1447
|
+
const d = parseInt(k, 10);
|
|
1448
|
+
const list = ringGroups[d];
|
|
1449
|
+
const count = list.length;
|
|
1450
|
+
const radius = ringRadii[d];
|
|
1451
|
+
|
|
1452
|
+
list.forEach((n, i) => {
|
|
1453
|
+
if (d === 0) {
|
|
1454
|
+
n.x = centerX; n.y = centerY;
|
|
1455
|
+
} else {
|
|
1456
|
+
const angle = (2 * Math.PI * i) / count - Math.PI / 2;
|
|
1457
|
+
n.x = centerX + radius * Math.cos(angle);
|
|
1458
|
+
n.y = centerY + radius * Math.sin(angle);
|
|
1459
|
+
}
|
|
1460
|
+
nodePositions.set(n.id, n);
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1465
|
+
|
|
1466
|
+
validEdges.forEach(d => {
|
|
1467
|
+
const p1 = nodePositions.get(d.from);
|
|
1468
|
+
const p2 = nodePositions.get(d.to);
|
|
1469
|
+
if (!p1 || !p2) return;
|
|
1470
|
+
|
|
1471
|
+
const pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1472
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1473
|
+
pathEl.setAttribute('d', pathD);
|
|
1474
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1475
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1476
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1477
|
+
|
|
1478
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1479
|
+
evt.stopPropagation();
|
|
1480
|
+
selectAndTraverseNode(d.to);
|
|
1481
|
+
});
|
|
1482
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1483
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1484
|
+
|
|
1485
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
const nodeGroups = svgG.append('g')
|
|
1489
|
+
.selectAll('g')
|
|
1490
|
+
.data(nodes)
|
|
1491
|
+
.enter()
|
|
1492
|
+
.append('g')
|
|
1493
|
+
.attr('class', 'node')
|
|
1494
|
+
.attr('data-id', d => d.id)
|
|
1495
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1496
|
+
|
|
1497
|
+
nodeGroups.append('circle')
|
|
1498
|
+
.attr('r', d => d.id === rootId ? 18 : d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1499
|
+
.attr('class', d => {
|
|
1500
|
+
if (d.id === rootId) return 'node-aspec';
|
|
1501
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1502
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1503
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1504
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1505
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1506
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1507
|
+
return 'node-code';
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
nodeGroups.append('text')
|
|
1511
|
+
.attr('x', 0)
|
|
1512
|
+
.attr('y', 24)
|
|
1513
|
+
.attr('text-anchor', 'middle')
|
|
1514
|
+
.attr('fill', d => d.id === rootId ? '#fbbf24' : '#f8fafc')
|
|
1515
|
+
.attr('font-weight', d => d.id === rootId ? 'bold' : 'normal')
|
|
1516
|
+
.attr('font-size', '11px')
|
|
1517
|
+
.text(d => d.id.length > 22 ? d.id.slice(0, 20) + '...' : d.id);
|
|
1518
|
+
|
|
1519
|
+
nodeGroups
|
|
1520
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1521
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1522
|
+
.on('mouseleave', hidePopover);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// 🌳 Sugiyama Top-to-Bottom Layered Tree View Layout Engine
|
|
1526
|
+
function renderSugiyamaTopToBottom(rawNodes, rawEdges) {
|
|
1527
|
+
const container = document.getElementById('graphSvg');
|
|
1528
|
+
container.innerHTML = '';
|
|
1529
|
+
const width = container.clientWidth || 1100;
|
|
1530
|
+
const height = container.clientHeight || 800;
|
|
1531
|
+
|
|
1532
|
+
const svg = d3.select('#graphSvg');
|
|
1533
|
+
setupSvgDefs(svg);
|
|
1534
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1535
|
+
|
|
1536
|
+
currentZoom = d3.zoom()
|
|
1537
|
+
.scaleExtent([0.05, 8])
|
|
1538
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1539
|
+
|
|
1540
|
+
svg.call(currentZoom);
|
|
1541
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1542
|
+
|
|
1543
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1544
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1545
|
+
|
|
1546
|
+
const validEdges = rawEdges
|
|
1547
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1548
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1549
|
+
|
|
1550
|
+
const layers = { req: [], hspec: [], aspec: [], tspec: [], file: [], symbol: [] };
|
|
1551
|
+
nodes.forEach(n => {
|
|
1552
|
+
if (n.id.startsWith('REQ')) layers.req.push(n);
|
|
1553
|
+
else if (n.id.startsWith('H-SPEC')) layers.hspec.push(n);
|
|
1554
|
+
else if (n.id.startsWith('A-SPEC')) layers.aspec.push(n);
|
|
1555
|
+
else if (n.id.startsWith('T-SPEC')) layers.tspec.push(n);
|
|
1556
|
+
else if (n.id.includes('#') || n.kind === 'symbol') layers.symbol.push(n);
|
|
1557
|
+
else layers.file.push(n);
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
const layerKeys = ['req', 'hspec', 'aspec', 'tspec', 'file', 'symbol'];
|
|
1561
|
+
const layerY = { req: 90, hspec: 220, aspec: 350, tspec: 480, file: 610, symbol: 740 };
|
|
1562
|
+
const nodePositions = new Map();
|
|
1563
|
+
|
|
1564
|
+
layerKeys.forEach(k => {
|
|
1565
|
+
const list = layers[k];
|
|
1566
|
+
const count = list.length;
|
|
1567
|
+
const spacing = Math.max(80, Math.min(180, (width - 150) / (count || 1)));
|
|
1568
|
+
const startX = 100;
|
|
1569
|
+
|
|
1570
|
+
list.forEach((n, i) => {
|
|
1571
|
+
const x = startX + i * spacing;
|
|
1572
|
+
const y = layerY[k];
|
|
1573
|
+
n.x = x; n.y = y;
|
|
1574
|
+
nodePositions.set(n.id, n);
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
const headers = [
|
|
1579
|
+
{ title: 'Top Layer 1: REQ Business Specs', y: layerY.req - 30, color: '#ec4899' },
|
|
1580
|
+
{ title: 'Layer 2: H-SPEC High Level Functional Specs', y: layerY.hspec - 30, color: '#8b5cf6' },
|
|
1581
|
+
{ title: 'Layer 3: A-SPEC Architecture Specs', y: layerY.aspec - 30, color: '#3b82f6' },
|
|
1582
|
+
{ title: 'Layer 4: T-SPEC Verification Specs', y: layerY.tspec - 30, color: '#06b6d4' },
|
|
1583
|
+
{ title: 'Layer 5: Source Code Files', y: layerY.file - 30, color: '#10b981' },
|
|
1584
|
+
{ title: 'Bottom Layer 6: AST Function / Method Symbols', y: layerY.symbol - 30, color: '#f59e0b' }
|
|
1585
|
+
];
|
|
1586
|
+
|
|
1587
|
+
headers.forEach(h => {
|
|
1588
|
+
svgG.append('text')
|
|
1589
|
+
.attr('x', 50)
|
|
1590
|
+
.attr('y', h.y)
|
|
1591
|
+
.attr('fill', h.color)
|
|
1592
|
+
.attr('font-size', '12px')
|
|
1593
|
+
.attr('font-weight', 'bold')
|
|
1594
|
+
.text(h.title);
|
|
1595
|
+
|
|
1596
|
+
svgG.append('line')
|
|
1597
|
+
.attr('x1', 40)
|
|
1598
|
+
.attr('y1', h.y + 12)
|
|
1599
|
+
.attr('x2', width + 1000)
|
|
1600
|
+
.attr('y2', h.y + 12)
|
|
1601
|
+
.attr('stroke', '#334155')
|
|
1602
|
+
.attr('stroke-dasharray', '4,4')
|
|
1603
|
+
.attr('opacity', 0.4);
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1607
|
+
|
|
1608
|
+
validEdges.forEach(d => {
|
|
1609
|
+
const p1 = nodePositions.get(d.from);
|
|
1610
|
+
const p2 = nodePositions.get(d.to);
|
|
1611
|
+
if (!p1 || !p2) return;
|
|
1612
|
+
|
|
1613
|
+
const cat1 = getNodeCategory(d.from);
|
|
1614
|
+
const cat2 = getNodeCategory(d.to);
|
|
1615
|
+
const isSameLevelPeer = cat1 === cat2;
|
|
1616
|
+
|
|
1617
|
+
let pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1618
|
+
if (isSameLevelPeer) {
|
|
1619
|
+
const cx = Math.min(p1.x, p2.x) - 50;
|
|
1620
|
+
pathD = \`M \${p1.x} \${p1.y} Q \${cx} \${(p1.y + p2.y) / 2}, \${p2.x} \${p2.y}\`;
|
|
1621
|
+
} else {
|
|
1622
|
+
const cy = (p1.y + p2.y) / 2;
|
|
1623
|
+
pathD = \`M \${p1.x} \${p1.y} C \${p1.x} \${cy}, \${p2.x} \${cy}, \${p2.x} \${p2.y}\`;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1627
|
+
pathEl.setAttribute('d', pathD);
|
|
1628
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1629
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1630
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1631
|
+
|
|
1632
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1633
|
+
evt.stopPropagation();
|
|
1634
|
+
selectAndTraverseNode(d.to);
|
|
1635
|
+
});
|
|
1636
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1637
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1638
|
+
|
|
1639
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const nodeGroups = svgG.append('g')
|
|
1643
|
+
.selectAll('g')
|
|
1644
|
+
.data(nodes)
|
|
1645
|
+
.enter()
|
|
1646
|
+
.append('g')
|
|
1647
|
+
.attr('class', 'node')
|
|
1648
|
+
.attr('data-id', d => d.id)
|
|
1649
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1650
|
+
|
|
1651
|
+
nodeGroups.append('circle')
|
|
1652
|
+
.attr('r', d => d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1653
|
+
.attr('class', d => {
|
|
1654
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1655
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1656
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1657
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1658
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1659
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1660
|
+
return 'node-code';
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
nodeGroups.append('text')
|
|
1664
|
+
.attr('x', 0)
|
|
1665
|
+
.attr('y', 24)
|
|
1666
|
+
.attr('text-anchor', 'middle')
|
|
1667
|
+
.attr('fill', '#f8fafc')
|
|
1668
|
+
.attr('font-size', '11px')
|
|
1669
|
+
.text(d => d.id.length > 24 ? d.id.slice(0, 22) + '...' : d.id);
|
|
1670
|
+
|
|
1671
|
+
nodeGroups
|
|
1672
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1673
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1674
|
+
.on('mouseleave', hidePopover);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// 📊 Classic Left-to-Right 6-Column Pipeline View
|
|
1678
|
+
function renderPipelineGraph(rawNodes, rawEdges) {
|
|
1679
|
+
const container = document.getElementById('graphSvg');
|
|
1680
|
+
container.innerHTML = '';
|
|
1681
|
+
const width = container.clientWidth || 1100;
|
|
1682
|
+
const height = container.clientHeight || 800;
|
|
1683
|
+
|
|
1684
|
+
const svg = d3.select('#graphSvg');
|
|
1685
|
+
setupSvgDefs(svg);
|
|
1686
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1687
|
+
|
|
1688
|
+
currentZoom = d3.zoom()
|
|
1689
|
+
.scaleExtent([0.05, 8])
|
|
1690
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1691
|
+
|
|
1692
|
+
svg.call(currentZoom);
|
|
1693
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1694
|
+
|
|
1695
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1696
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1697
|
+
|
|
1698
|
+
const validEdges = rawEdges
|
|
1699
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1700
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1701
|
+
|
|
1702
|
+
const cols = { req: [], hspec: [], aspec: [], tspec: [], file: [], symbol: [] };
|
|
1703
|
+
nodes.forEach(n => {
|
|
1704
|
+
if (n.id.startsWith('REQ')) cols.req.push(n);
|
|
1705
|
+
else if (n.id.startsWith('H-SPEC')) cols.hspec.push(n);
|
|
1706
|
+
else if (n.id.startsWith('A-SPEC')) cols.aspec.push(n);
|
|
1707
|
+
else if (n.id.startsWith('T-SPEC')) cols.tspec.push(n);
|
|
1708
|
+
else if (n.id.includes('#') || n.kind === 'symbol') cols.symbol.push(n);
|
|
1709
|
+
else cols.file.push(n);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
const colKeys = ['req', 'hspec', 'aspec', 'tspec', 'file', 'symbol'];
|
|
1713
|
+
const colX = { req: 90, hspec: 280, aspec: 470, tspec: 660, file: 850, symbol: 1040 };
|
|
1714
|
+
const nodePositions = new Map();
|
|
1715
|
+
|
|
1716
|
+
colKeys.forEach(k => {
|
|
1717
|
+
const list = cols[k];
|
|
1718
|
+
const count = list.length;
|
|
1719
|
+
const spacing = Math.max(40, Math.min(70, (height - 140) / (count || 1)));
|
|
1720
|
+
const startY = 90;
|
|
1721
|
+
|
|
1722
|
+
list.forEach((n, i) => {
|
|
1723
|
+
const x = colX[k];
|
|
1724
|
+
const y = startY + i * spacing;
|
|
1725
|
+
n.x = x; n.y = y;
|
|
1726
|
+
nodePositions.set(n.id, n);
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
const headers = [
|
|
1731
|
+
{ title: '1. REQ Business', x: colX.req, color: '#ec4899' },
|
|
1732
|
+
{ title: '2. H-SPEC High', x: colX.hspec, color: '#8b5cf6' },
|
|
1733
|
+
{ title: '3. A-SPEC Arch', x: colX.aspec, color: '#3b82f6' },
|
|
1734
|
+
{ title: '4. T-SPEC Test', x: colX.tspec, color: '#06b6d4' },
|
|
1735
|
+
{ title: '5. Code File', x: colX.file, color: '#10b981' },
|
|
1736
|
+
{ title: '6. AST Symbol', x: colX.symbol, color: '#f59e0b' }
|
|
1737
|
+
];
|
|
1738
|
+
|
|
1739
|
+
headers.forEach(h => {
|
|
1740
|
+
svgG.append('text')
|
|
1741
|
+
.attr('x', h.x)
|
|
1742
|
+
.attr('y', 40)
|
|
1743
|
+
.attr('fill', h.color)
|
|
1744
|
+
.attr('font-size', '13px')
|
|
1745
|
+
.attr('font-weight', 'bold')
|
|
1746
|
+
.attr('text-anchor', 'middle')
|
|
1747
|
+
.text(h.title);
|
|
1748
|
+
|
|
1749
|
+
svgG.append('line')
|
|
1750
|
+
.attr('x1', h.x)
|
|
1751
|
+
.attr('y1', 50)
|
|
1752
|
+
.attr('x2', h.x)
|
|
1753
|
+
.attr('y2', height - 20)
|
|
1754
|
+
.attr('stroke', '#334155')
|
|
1755
|
+
.attr('stroke-dasharray', '4,4')
|
|
1756
|
+
.attr('opacity', 0.4);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1760
|
+
|
|
1761
|
+
validEdges.forEach(d => {
|
|
1762
|
+
const p1 = nodePositions.get(d.from);
|
|
1763
|
+
const p2 = nodePositions.get(d.to);
|
|
1764
|
+
if (!p1 || !p2) return;
|
|
1765
|
+
|
|
1766
|
+
const cat1 = getNodeCategory(d.from);
|
|
1767
|
+
const cat2 = getNodeCategory(d.to);
|
|
1768
|
+
const isSameLevelPeer = cat1 === cat2;
|
|
1769
|
+
|
|
1770
|
+
const isCrossColumnArc = (p1.x > 800 && p2.x < 500) || (p2.x > 800 && p1.x < 500);
|
|
1771
|
+
let pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1772
|
+
|
|
1773
|
+
if (isSameLevelPeer || isCrossColumnArc) {
|
|
1774
|
+
const cy = Math.min(p1.y, p2.y) - (isSameLevelPeer ? 35 : 50);
|
|
1775
|
+
pathD = \`M \${p1.x} \${p1.y} Q \${(p1.x + p2.x) / 2} \${cy}, \${p2.x} \${p2.y}\`;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1779
|
+
pathEl.setAttribute('d', pathD);
|
|
1780
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1781
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1782
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1783
|
+
|
|
1784
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1785
|
+
evt.stopPropagation();
|
|
1786
|
+
selectAndTraverseNode(d.to);
|
|
1787
|
+
});
|
|
1788
|
+
pathEl.addEventListener('mouseenter', (evt) => showEdgePopover(evt, d));
|
|
1789
|
+
pathEl.addEventListener('mouseleave', hidePopover);
|
|
1790
|
+
|
|
1791
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
const nodeGroups = svgG.append('g')
|
|
1795
|
+
.selectAll('g')
|
|
1796
|
+
.data(nodes)
|
|
1797
|
+
.enter()
|
|
1798
|
+
.append('g')
|
|
1799
|
+
.attr('class', 'node')
|
|
1800
|
+
.attr('data-id', d => d.id)
|
|
1801
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1802
|
+
|
|
1803
|
+
nodeGroups.append('circle')
|
|
1804
|
+
.attr('r', d => d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1805
|
+
.attr('class', d => {
|
|
1806
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1807
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1808
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1809
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1810
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1811
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1812
|
+
return 'node-code';
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
nodeGroups.append('text')
|
|
1816
|
+
.attr('x', 16)
|
|
1817
|
+
.attr('y', 4)
|
|
1818
|
+
.attr('fill', '#f8fafc')
|
|
1819
|
+
.attr('font-size', '11px')
|
|
1820
|
+
.text(d => d.id.length > 25 ? d.id.slice(0, 23) + '...' : d.id);
|
|
1821
|
+
|
|
1822
|
+
nodeGroups
|
|
1823
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1824
|
+
.on('mouseenter', (event, d) => showNodePopover(event, d, validEdges))
|
|
1825
|
+
.on('mouseleave', hidePopover);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function selectAndTraverseNode(nodeId) {
|
|
1829
|
+
if (!graphData) return;
|
|
1830
|
+
selectedRootNodeId = nodeId;
|
|
1831
|
+
|
|
1832
|
+
const targetNode = graphData.nodes.find(n => n.id === nodeId);
|
|
1833
|
+
if (!targetNode) return;
|
|
1834
|
+
|
|
1835
|
+
if (traversalHistory.length === 0 || traversalHistory[traversalHistory.length - 1] !== nodeId) {
|
|
1836
|
+
traversalHistory.push(nodeId);
|
|
1837
|
+
if (traversalHistory.length > 8) traversalHistory.shift();
|
|
1838
|
+
renderBreadcrumbs();
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
filterGraphByScope();
|
|
1842
|
+
renderTraversalInspector(targetNode);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function renderBreadcrumbs() {
|
|
1846
|
+
const container = document.getElementById('breadcrumbTrail');
|
|
1847
|
+
if (traversalHistory.length === 0) {
|
|
1848
|
+
container.innerHTML = 'Select any node or click any edge to jump...';
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
container.innerHTML = traversalHistory.map((id, index) => \`
|
|
1853
|
+
<span class="crumb-item" onclick="jumpToHistoryIndex(\${index})">\${escapeHtml(id)}</span>
|
|
1854
|
+
\${index < traversalHistory.length - 1 ? '<span class="crumb-sep">➔</span>' : ''}
|
|
1855
|
+
\`).join('');
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function jumpToHistoryIndex(index) {
|
|
1859
|
+
const targetId = traversalHistory[index];
|
|
1860
|
+
if (targetId) {
|
|
1861
|
+
traversalHistory = traversalHistory.slice(0, index + 1);
|
|
1862
|
+
renderBreadcrumbs();
|
|
1863
|
+
selectAndTraverseNode(targetId);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function renderTraversalInspector(node) {
|
|
1868
|
+
document.getElementById('inspectorNodeId').innerText = node.id;
|
|
1869
|
+
document.getElementById('inspectorNodeType').innerText = node.kind || node.type;
|
|
1870
|
+
|
|
1871
|
+
document.getElementById('inspectorDetails').innerHTML = \`
|
|
1872
|
+
<div style="margin-bottom:6px;"><strong>Title:</strong> \${escapeHtml(node.label || node.id)}</div>
|
|
1873
|
+
<div><strong>Status:</strong> \${node.covered ? '<span style="color:#34d399; font-weight:bold;">COVERED / APPROVED</span>' : '<span style="color:#f87171; font-weight:bold;">ACTIVE</span>'}</div>
|
|
1874
|
+
\`;
|
|
1875
|
+
|
|
1876
|
+
const currCat = getNodeCategory(node.id);
|
|
1877
|
+
const connectedEdges = (graphData.edges || []).filter(e => getIdStr(e.from || e.source) === node.id || getIdStr(e.to || e.target) === node.id);
|
|
1878
|
+
|
|
1879
|
+
const upstream = [];
|
|
1880
|
+
const peers = [];
|
|
1881
|
+
const downstream = [];
|
|
1882
|
+
|
|
1883
|
+
connectedEdges.forEach(e => {
|
|
1884
|
+
const u = getIdStr(e.from || e.source);
|
|
1885
|
+
const v = getIdStr(e.to || e.target);
|
|
1886
|
+
const targetId = u === node.id ? v : u;
|
|
1887
|
+
const targetCat = getNodeCategory(targetId);
|
|
1888
|
+
|
|
1889
|
+
if (targetCat === currCat && targetId !== node.id) {
|
|
1890
|
+
peers.push({ targetId, rel: 'peer', label: '\${currCat} Peer Link' });
|
|
1891
|
+
} else if (u === node.id) {
|
|
1892
|
+
downstream.push({ targetId: v, rel: e.rel, direction: 'out' });
|
|
1893
|
+
} else {
|
|
1894
|
+
upstream.push({ targetId: u, rel: e.rel, direction: 'in' });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
const container = document.getElementById('inspectorNeighborsContainer');
|
|
1899
|
+
let html = '';
|
|
1900
|
+
|
|
1901
|
+
if (peers.length > 0) {
|
|
1902
|
+
html += \`
|
|
1903
|
+
<div class="neighbor-section">
|
|
1904
|
+
<div class="neighbor-sec-title">🔄 Same-Level Peer Specs (\${peers.length})</div>
|
|
1905
|
+
\${peers.map(item => \`
|
|
1906
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1907
|
+
<div>
|
|
1908
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1909
|
+
<span class="neighbor-rel rel-peer">PEER \${currCat}</span>
|
|
1910
|
+
</div>
|
|
1911
|
+
<button class="jump-btn jump-btn-peer">🎯 Jump to Peer</button>
|
|
1912
|
+
</div>
|
|
1913
|
+
\`).join('')}
|
|
1914
|
+
</div>
|
|
1915
|
+
\`;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (upstream.length > 0) {
|
|
1919
|
+
html += \`
|
|
1920
|
+
<div class="neighbor-section">
|
|
1921
|
+
<div class="neighbor-sec-title">⬆️ Upstream Dependencies / Parent Specs (\${upstream.length})</div>
|
|
1922
|
+
\${upstream.map(item => \`
|
|
1923
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1924
|
+
<div>
|
|
1925
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1926
|
+
<span class="neighbor-rel rel-\${item.rel}">\${item.rel}</span>
|
|
1927
|
+
</div>
|
|
1928
|
+
<button class="jump-btn">🎯 Jump</button>
|
|
1929
|
+
</div>
|
|
1930
|
+
\`).join('')}
|
|
1931
|
+
</div>
|
|
1932
|
+
\`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (downstream.length > 0) {
|
|
1936
|
+
html += \`
|
|
1937
|
+
<div class="neighbor-section">
|
|
1938
|
+
<div class="neighbor-sec-title">⬇️ Downstream Implemented Specs / Symbols (\${downstream.length})</div>
|
|
1939
|
+
\${downstream.map(item => \`
|
|
1940
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1941
|
+
<div>
|
|
1942
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1943
|
+
<span class="neighbor-rel rel-\${item.rel}">\${item.rel}</span>
|
|
1944
|
+
</div>
|
|
1945
|
+
<button class="jump-btn">🎯 Jump</button>
|
|
1946
|
+
</div>
|
|
1947
|
+
\`).join('')}
|
|
1948
|
+
</div>
|
|
1949
|
+
\`;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
if (upstream.length === 0 && peers.length === 0 && downstream.length === 0) {
|
|
1953
|
+
html = '<div style="color:#94a3b8; font-size:12px; margin-top:12px;">No direct neighbors found for this node.</div>';
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
container.innerHTML = html;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// World Top-Tier High-Density Rich Node Tooltip Hover Popover Card
|
|
1960
|
+
function showNodePopover(event, node, edges) {
|
|
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
|
+
|
|
1967
|
+
const nodeIdStr = getIdStr(node.id || node);
|
|
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
|
+
|
|
2039
|
+
card.classList.add('active');
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function hidePopover() {
|
|
2043
|
+
document.getElementById('popoverCard').classList.remove('active');
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
loadDashboard();
|
|
2047
|
+
</script>
|
|
2048
|
+
</body>
|
|
2049
|
+
</html>`;
|
|
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
|
+
}
|