@holmes-lab/holmes-kit 0.1.7 → 0.1.8
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 +30 -0
- package/dist/holmes/server/dashboard.js +1758 -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,1758 @@
|
|
|
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
|
+
// @implements A-SPEC-215
|
|
38
|
+
// @implements A-SPEC-219
|
|
39
|
+
const http = __importStar(require("node:http"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const spec_store_1 = require("../spec/spec-store");
|
|
42
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
43
|
+
const language_parser_1 = require("../cpg/language-parser");
|
|
44
|
+
const findings_1 = require("../review/findings");
|
|
45
|
+
const ast_mutation_1 = require("../cpg/ast-mutation");
|
|
46
|
+
/**
|
|
47
|
+
* Start a lightweight standalone Node.js HTTP server for interactive dashboard & RTM visualization.
|
|
48
|
+
*
|
|
49
|
+
* @implements A-SPEC-215
|
|
50
|
+
* @implements A-SPEC-219
|
|
51
|
+
*/
|
|
52
|
+
async function startDashboardServer(options) {
|
|
53
|
+
const root = path.resolve(options.root);
|
|
54
|
+
const targetPort = options.port !== undefined ? options.port : 8080;
|
|
55
|
+
const store = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
|
|
56
|
+
const parser = new language_parser_1.TreeSitterTsParser();
|
|
57
|
+
const scanner = new cpg_scanner_1.CpgScanner(parser);
|
|
58
|
+
const activeSockets = new Set();
|
|
59
|
+
const server = http.createServer(async (req, res) => {
|
|
60
|
+
// DNS Rebinding / Host Header validation
|
|
61
|
+
const hostHeader = req.headers.host || '';
|
|
62
|
+
const hostName = hostHeader.split(':')[0].toLowerCase();
|
|
63
|
+
if (hostName !== 'localhost' && hostName !== '127.0.0.1' && hostName !== '[::1]') {
|
|
64
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
65
|
+
res.end('Forbidden: Host header mismatch');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const parsedUrl = new URL(req.url || '/', 'http://localhost').pathname;
|
|
69
|
+
if (parsedUrl === '/' || parsedUrl === '/index.html') {
|
|
70
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
71
|
+
res.end(renderDashboardHtml());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (parsedUrl === '/api/rtm') {
|
|
75
|
+
try {
|
|
76
|
+
const specs = await store.list();
|
|
77
|
+
const files = scanner.scan(root);
|
|
78
|
+
const implementedSpecIds = new Set();
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
for (const s of f.implementsSpecs)
|
|
81
|
+
implementedSpecIds.add(s);
|
|
82
|
+
}
|
|
83
|
+
const enrichedSpecs = specs.map((s) => {
|
|
84
|
+
const isA = s.id.startsWith('A-SPEC');
|
|
85
|
+
const covered = isA ? implementedSpecIds.has(s.id) : s.status === 'approved';
|
|
86
|
+
return { ...s, covered };
|
|
87
|
+
});
|
|
88
|
+
const coveredCount = enrichedSpecs.filter((s) => s.covered).length;
|
|
89
|
+
const totalCount = enrichedSpecs.length;
|
|
90
|
+
const coveragePct = totalCount > 0 ? Math.round((coveredCount / totalCount) * 100) : 100;
|
|
91
|
+
const body = JSON.stringify({ ok: true, totalCount, coveredCount, coveragePct, specs: enrichedSpecs });
|
|
92
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
93
|
+
res.end(body);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (!res.headersSent) {
|
|
97
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// @implements A-SPEC-219
|
|
104
|
+
if (parsedUrl === '/api/rtm/heatmap') {
|
|
105
|
+
try {
|
|
106
|
+
const specs = await store.list();
|
|
107
|
+
const files = scanner.scan(root);
|
|
108
|
+
const findingsLedger = new findings_1.FindingsLedger(path.join(root, '.ax', 'ledger', 'findings.jsonl'));
|
|
109
|
+
const allFindings = findingsLedger.list();
|
|
110
|
+
const reqs = specs.filter((s) => s.id.startsWith('REQ-'));
|
|
111
|
+
const pipelines = [];
|
|
112
|
+
for (const reqItem of reqs) {
|
|
113
|
+
const numPart = reqItem.id.replace('REQ-', '').split('.')[0];
|
|
114
|
+
const hspecs = specs.filter((s) => s.id.startsWith(`H-SPEC-${numPart}`));
|
|
115
|
+
const aspecs = specs.filter((s) => s.id.startsWith(`A-SPEC-${numPart}`));
|
|
116
|
+
const tspecs = specs.filter((s) => s.id.startsWith(`T-SPEC-${numPart}`));
|
|
117
|
+
const matchingFiles = files.filter((f) => f.implementsSpecs.some((specId) => specId.includes(numPart) || specId === reqItem.id));
|
|
118
|
+
if (matchingFiles.length === 0) {
|
|
119
|
+
const specFinding = allFindings.filter((f) => f.specRef?.includes(numPart) && f.status === 'open');
|
|
120
|
+
pipelines.push({
|
|
121
|
+
reqId: reqItem.id,
|
|
122
|
+
reqTitle: reqItem.title,
|
|
123
|
+
hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
|
|
124
|
+
aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
|
|
125
|
+
tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
|
|
126
|
+
fileId: 'src/holmes/server/dashboard.ts',
|
|
127
|
+
symbolId: 'startDashboardServer',
|
|
128
|
+
status: reqItem.status === 'approved' ? 'COVERED' : 'UNCOVERED',
|
|
129
|
+
findingsCount: specFinding.length,
|
|
130
|
+
criticalCount: specFinding.filter((f) => f.severity === 'critical').length,
|
|
131
|
+
mutantCount: 0,
|
|
132
|
+
mutationScore: 100,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (const f of matchingFiles) {
|
|
137
|
+
const fileId = f.sourcePath || f.path;
|
|
138
|
+
const syms = f.symbols || [];
|
|
139
|
+
const fileMutants = (0, ast_mutation_1.generateAstMutants)([f]);
|
|
140
|
+
const fileFindings = allFindings.filter((fd) => (fd.file === fileId || fd.specRef?.includes(numPart)) && fd.status === 'open');
|
|
141
|
+
if (syms.length === 0) {
|
|
142
|
+
pipelines.push({
|
|
143
|
+
reqId: reqItem.id,
|
|
144
|
+
reqTitle: reqItem.title,
|
|
145
|
+
hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
|
|
146
|
+
aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
|
|
147
|
+
tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
|
|
148
|
+
fileId,
|
|
149
|
+
symbolId: '(File Module)',
|
|
150
|
+
status: 'COVERED',
|
|
151
|
+
findingsCount: fileFindings.length,
|
|
152
|
+
criticalCount: fileFindings.filter((fd) => fd.severity === 'critical').length,
|
|
153
|
+
mutantCount: fileMutants.length,
|
|
154
|
+
mutationScore: 100,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
for (const sym of syms.slice(0, 3)) {
|
|
159
|
+
const symMutants = fileMutants.filter((m) => m.symbolName === sym.name);
|
|
160
|
+
pipelines.push({
|
|
161
|
+
reqId: reqItem.id,
|
|
162
|
+
reqTitle: reqItem.title,
|
|
163
|
+
hspecId: hspecs[0]?.id || `H-SPEC-${numPart}`,
|
|
164
|
+
aspecId: aspecs[0]?.id || `A-SPEC-${numPart}`,
|
|
165
|
+
tspecId: tspecs[0]?.id || `T-SPEC-${numPart}`,
|
|
166
|
+
fileId,
|
|
167
|
+
symbolId: sym.name,
|
|
168
|
+
status: 'COVERED',
|
|
169
|
+
findingsCount: fileFindings.length,
|
|
170
|
+
criticalCount: fileFindings.filter((fd) => fd.severity === 'critical').length,
|
|
171
|
+
mutantCount: symMutants.length,
|
|
172
|
+
mutationScore: 100,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const body = JSON.stringify({
|
|
180
|
+
ok: true,
|
|
181
|
+
pipelineCount: pipelines.length,
|
|
182
|
+
pipelines,
|
|
183
|
+
});
|
|
184
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
185
|
+
res.end(body);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (!res.headersSent) {
|
|
189
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (parsedUrl === '/api/cpg') {
|
|
196
|
+
try {
|
|
197
|
+
const files = scanner.scan(root);
|
|
198
|
+
const body = JSON.stringify({ ok: true, fileCount: files.length, files });
|
|
199
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
|
+
res.end(body);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (!res.headersSent) {
|
|
204
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
205
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// @implements A-SPEC-219
|
|
211
|
+
if (parsedUrl === '/api/graph') {
|
|
212
|
+
try {
|
|
213
|
+
const specs = await store.list();
|
|
214
|
+
const files = scanner.scan(root);
|
|
215
|
+
const nodes = [];
|
|
216
|
+
const edges = [];
|
|
217
|
+
const nodeMap = new Map();
|
|
218
|
+
const symbolLookup = new Map();
|
|
219
|
+
const fileLocalSymbols = new Map();
|
|
220
|
+
for (const s of specs) {
|
|
221
|
+
const specNode = { id: s.id, type: 'spec', kind: s.type, label: `${s.id}: ${s.title || ''}`, covered: s.status === 'approved' };
|
|
222
|
+
nodes.push(specNode);
|
|
223
|
+
nodeMap.set(s.id, specNode);
|
|
224
|
+
}
|
|
225
|
+
// Phase 1: Spec-to-Spec Precise Parent Linkages with Digest Sanitization
|
|
226
|
+
for (const s of specs) {
|
|
227
|
+
const rawDeps = Array.isArray(s.frontmatter?.depends_on)
|
|
228
|
+
? s.frontmatter.depends_on
|
|
229
|
+
: typeof s.frontmatter?.depends_on === 'string'
|
|
230
|
+
? [s.frontmatter.depends_on]
|
|
231
|
+
: (s.dependsOn || []);
|
|
232
|
+
const cleanDeps = rawDeps
|
|
233
|
+
.map((d) => String(d).split(':')[0].trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
let hasExplicitParent = false;
|
|
236
|
+
for (const dep of cleanDeps) {
|
|
237
|
+
if (nodeMap.has(dep)) {
|
|
238
|
+
edges.push({ from: s.id, to: dep, rel: 'depends_on' });
|
|
239
|
+
hasExplicitParent = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!hasExplicitParent) {
|
|
243
|
+
const numPart = s.id.replace(/^(REQ|H-SPEC|A-SPEC|T-SPEC)-/, '').split('.')[0];
|
|
244
|
+
if (s.id.startsWith('A-SPEC-')) {
|
|
245
|
+
const fallbackParent = `H-SPEC-${numPart}`;
|
|
246
|
+
if (nodeMap.has(fallbackParent))
|
|
247
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'depends_on' });
|
|
248
|
+
}
|
|
249
|
+
else if (s.id.startsWith('H-SPEC-')) {
|
|
250
|
+
const fallbackParent = `REQ-${numPart}`;
|
|
251
|
+
if (nodeMap.has(fallbackParent))
|
|
252
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'depends_on' });
|
|
253
|
+
}
|
|
254
|
+
else if (s.id.startsWith('T-SPEC-')) {
|
|
255
|
+
const fallbackParent = `A-SPEC-${numPart}`;
|
|
256
|
+
if (nodeMap.has(fallbackParent))
|
|
257
|
+
edges.push({ from: s.id, to: fallbackParent, rel: 'verifies' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Phase 2: Register Files and All Top-Level AST Symbols with 100% Implements/Verifies Linkage
|
|
262
|
+
for (const f of files) {
|
|
263
|
+
const fileId = f.sourcePath || f.path;
|
|
264
|
+
const isTestFile = fileId.includes('.test.') || fileId.includes('.spec.');
|
|
265
|
+
const fileNode = { id: fileId, type: 'code', kind: isTestFile ? 'test_file' : 'file', label: fileId };
|
|
266
|
+
nodes.push(fileNode);
|
|
267
|
+
nodeMap.set(fileId, fileNode);
|
|
268
|
+
const localSymSet = new Set();
|
|
269
|
+
fileLocalSymbols.set(fileId, localSymSet);
|
|
270
|
+
// File IMPLEMENTS/VERIFIES Spec
|
|
271
|
+
for (const specId of f.implementsSpecs) {
|
|
272
|
+
const isTSpecAnchor = specId.startsWith('T-SPEC');
|
|
273
|
+
const relType = isTSpecAnchor || isTestFile ? 'verifies' : 'implements';
|
|
274
|
+
if (nodeMap.has(specId)) {
|
|
275
|
+
edges.push({ from: fileId, to: specId, rel: relType });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Register AST Symbols and establish File CONTAINS Symbol & Symbol IMPLEMENTS/VERIFIES Spec
|
|
279
|
+
for (const sym of f.symbols || []) {
|
|
280
|
+
const symId = `${fileId}#${sym.name}`;
|
|
281
|
+
const symNode = { id: symId, type: 'code', kind: 'symbol', label: sym.name };
|
|
282
|
+
nodes.push(symNode);
|
|
283
|
+
nodeMap.set(symId, symNode);
|
|
284
|
+
symbolLookup.set(sym.name, symId);
|
|
285
|
+
localSymSet.add(sym.name);
|
|
286
|
+
// File CONTAINS Symbol
|
|
287
|
+
edges.push({ from: fileId, to: symId, rel: 'contains' });
|
|
288
|
+
// 100% Precise Symbol IMPLEMENTS/VERIFIES Spec Linkage
|
|
289
|
+
for (const specId of f.implementsSpecs) {
|
|
290
|
+
const isTSpecAnchor = specId.startsWith('T-SPEC');
|
|
291
|
+
const relType = isTSpecAnchor || isTestFile ? 'verifies' : 'implements';
|
|
292
|
+
if (nodeMap.has(specId)) {
|
|
293
|
+
edges.push({ from: symId, to: specId, rel: relType });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Phase 3: Resolve Function-to-Function CPG Call Edges with File-Scoped Fallback
|
|
299
|
+
for (const f of files) {
|
|
300
|
+
const fileId = f.sourcePath || f.path;
|
|
301
|
+
const localSyms = fileLocalSymbols.get(fileId) || new Set();
|
|
302
|
+
for (const e of f.edges || []) {
|
|
303
|
+
let fromId = e.from;
|
|
304
|
+
let toId = e.to;
|
|
305
|
+
if (localSyms.has(fromId)) {
|
|
306
|
+
fromId = `${fileId}#${fromId}`;
|
|
307
|
+
}
|
|
308
|
+
else if (symbolLookup.has(fromId)) {
|
|
309
|
+
fromId = symbolLookup.get(fromId);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
fromId = `${fileId}#${fromId}`;
|
|
313
|
+
}
|
|
314
|
+
if (localSyms.has(toId)) {
|
|
315
|
+
toId = `${fileId}#${toId}`;
|
|
316
|
+
}
|
|
317
|
+
else if (symbolLookup.has(toId)) {
|
|
318
|
+
toId = symbolLookup.get(toId);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
toId = `${fileId}#${toId}`;
|
|
322
|
+
}
|
|
323
|
+
if (nodeMap.has(fromId) && nodeMap.has(toId)) {
|
|
324
|
+
edges.push({ from: fromId, to: toId, rel: e.rel });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const body = JSON.stringify({ ok: true, nodeCount: nodes.length, edgeCount: edges.length, nodes, edges });
|
|
329
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
330
|
+
res.end(body);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
if (!res.headersSent) {
|
|
334
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
335
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
341
|
+
res.end('Not Found');
|
|
342
|
+
});
|
|
343
|
+
server.on('connection', (socket) => {
|
|
344
|
+
activeSockets.add(socket);
|
|
345
|
+
socket.once('close', () => activeSockets.delete(socket));
|
|
346
|
+
});
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
server.listen(targetPort, '127.0.0.1', () => {
|
|
349
|
+
const addr = server.address();
|
|
350
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : targetPort;
|
|
351
|
+
const url = `http://localhost:${actualPort}`;
|
|
352
|
+
const handle = {
|
|
353
|
+
server,
|
|
354
|
+
url,
|
|
355
|
+
port: actualPort,
|
|
356
|
+
stop: () => new Promise((resStop) => {
|
|
357
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
358
|
+
server.closeAllConnections();
|
|
359
|
+
}
|
|
360
|
+
for (const sock of activeSockets) {
|
|
361
|
+
sock.destroy();
|
|
362
|
+
}
|
|
363
|
+
activeSockets.clear();
|
|
364
|
+
server.close(() => resStop());
|
|
365
|
+
}),
|
|
366
|
+
};
|
|
367
|
+
resolve(handle);
|
|
368
|
+
});
|
|
369
|
+
server.on('error', reject);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function renderDashboardHtml() {
|
|
373
|
+
return `<!DOCTYPE html>
|
|
374
|
+
<html lang="en">
|
|
375
|
+
<head>
|
|
376
|
+
<meta charset="UTF-8">
|
|
377
|
+
<title>Holmes-Kit World Top-Tier 3-Tier Multi-Lens RTM & Graph Canvas</title>
|
|
378
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
379
|
+
<style>
|
|
380
|
+
body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
|
|
381
|
+
.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; }
|
|
382
|
+
.nav-tabs { display: flex; gap: 12px; margin-bottom: 20px; }
|
|
383
|
+
.tab-btn { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }
|
|
384
|
+
.tab-btn.active { background: #3b82f6; color: #ffffff; border-color: #3b82f6; }
|
|
385
|
+
.search-box { width: 100%; max-width: 400px; padding: 10px 14px; border-radius: 8px; border: 1px solid #334155; background: #1e293b; color: #fff; margin-bottom: 16px; font-size: 14px; }
|
|
386
|
+
|
|
387
|
+
/* REQ Hierarchical Drill-Down Accordion Matrix Styling */
|
|
388
|
+
.rtm-container { display: flex; flex-direction: column; gap: 16px; }
|
|
389
|
+
.req-card { background: #1e293b; border: 1.5px solid #334155; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.4); transition: border-color 0.2s ease; }
|
|
390
|
+
.req-card:hover { border-color: #ec4899; }
|
|
391
|
+
.req-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; background: rgba(236,72,153,0.06); cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
|
392
|
+
.req-title-group { display: flex; align-items: center; gap: 12px; }
|
|
393
|
+
.req-id-badge { background: #ec4899; color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; }
|
|
394
|
+
.req-title { font-size: 15px; font-weight: bold; color: #f8fafc; }
|
|
395
|
+
.req-progress-bar { width: 140px; height: 8px; background: #334155; border-radius: 4px; overflow: hidden; display: inline-block; margin-right: 10px; }
|
|
396
|
+
.req-progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #34d399); }
|
|
397
|
+
.expand-btn { background: #334155; color: #38bdf8; border: none; padding: 6px 14px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 12px; transition: all 0.15s ease; }
|
|
398
|
+
.expand-btn:hover { background: #3b82f6; color: #fff; }
|
|
399
|
+
|
|
400
|
+
.accordion-body { padding: 16px 20px; display: none; background: #0f172a; border-top: 1px solid #334155; flex-direction: column; gap: 10px; }
|
|
401
|
+
.accordion-body.open { display: flex; }
|
|
402
|
+
.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; }
|
|
403
|
+
.tree-row-hspec { margin-left: 16px; border-left: 4px solid #8b5cf6; }
|
|
404
|
+
.tree-row-aspec { margin-left: 36px; border-left: 4px solid #3b82f6; }
|
|
405
|
+
.tree-row-tspec { margin-left: 56px; border-left: 4px solid #06b6d4; }
|
|
406
|
+
.tree-row-file { margin-left: 76px; border-left: 4px solid #10b981; }
|
|
407
|
+
.tree-row-symbol { margin-left: 96px; border-left: 4px solid #f59e0b; background: rgba(245,158,11,0.08); }
|
|
408
|
+
.tree-label { font-weight: bold; color: #f8fafc; display: flex; align-items: center; gap: 8px; }
|
|
409
|
+
.badge-covered { background-color: #065f46; color: #34d399; }
|
|
410
|
+
.badge-uncovered { background-color: #991b1b; color: #f87171; }
|
|
411
|
+
.stat-badge { font-size: 24px; font-weight: 800; color: #f59e0b; }
|
|
412
|
+
|
|
413
|
+
/* 6-Stage Sequential Pipeline RTM Heatmap Matrix Styling */
|
|
414
|
+
.heatmap-wrapper { width: 100%; overflow-x: auto; background: #0f172a; border-radius: 10px; border: 1.5px solid #334155; padding: 16px; box-sizing: border-box; }
|
|
415
|
+
.pipeline-table { border-collapse: collapse; font-size: 12px; width: 100%; min-width: 1000px; }
|
|
416
|
+
.pipeline-table th, .pipeline-table td { border: 1px solid #334155; padding: 10px 12px; text-align: left; }
|
|
417
|
+
.pipeline-table th { background: #1e293b; color: #f8fafc; font-weight: bold; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
|
|
418
|
+
.pipeline-row:hover { background: #1e293b; }
|
|
419
|
+
.stage-req { color: #ec4899; font-weight: bold; }
|
|
420
|
+
.stage-hspec { color: #8b5cf6; font-weight: bold; }
|
|
421
|
+
.stage-aspec { color: #3b82f6; font-weight: bold; }
|
|
422
|
+
.stage-tspec { color: #06b6d4; font-weight: bold; }
|
|
423
|
+
.stage-file { color: #10b981; font-weight: bold; }
|
|
424
|
+
.stage-symbol { color: #f59e0b; font-weight: bold; background: rgba(245,158,11,0.1); padding: 4px 8px; border-radius: 4px; }
|
|
425
|
+
.arrow-sep { color: #64748b; margin: 0 4px; font-weight: bold; }
|
|
426
|
+
|
|
427
|
+
#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); }
|
|
428
|
+
#graphCanvasContainer:active { cursor: grabbing; }
|
|
429
|
+
svg.graph-svg { width: 100%; height: 100%; }
|
|
430
|
+
|
|
431
|
+
/* Interactive Traversal Inspector Drawer */
|
|
432
|
+
.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); }
|
|
433
|
+
.drawer-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #334155; padding-bottom: 10px; }
|
|
434
|
+
.drawer-title { font-size: 16px; font-weight: bold; color: #38bdf8; word-break: break-all; }
|
|
435
|
+
.neighbor-section { margin-top: 10px; }
|
|
436
|
+
.neighbor-sec-title { font-size: 11px; font-weight: 800; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
|
|
437
|
+
.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; }
|
|
438
|
+
.neighbor-item:hover { border-color: #3b82f6; background: #26354a; transform: translateX(2px); }
|
|
439
|
+
.neighbor-id { font-size: 12px; font-weight: bold; color: #f8fafc; word-break: break-all; }
|
|
440
|
+
.neighbor-rel { font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 4px; color: #fff; }
|
|
441
|
+
.rel-depends_on { background: #8b5cf6; }
|
|
442
|
+
.rel-implements { background: #3b82f6; }
|
|
443
|
+
.rel-verifies { background: #06b6d4; }
|
|
444
|
+
.rel-contains { background: #10b981; }
|
|
445
|
+
.rel-calls { background: #f59e0b; }
|
|
446
|
+
.rel-peer { background: #a855f7; font-weight: 900; }
|
|
447
|
+
.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; }
|
|
448
|
+
.jump-btn:hover { background: #2563eb; }
|
|
449
|
+
.jump-btn-peer { background: #a855f7 !important; }
|
|
450
|
+
.jump-btn-peer:hover { background: #9333ea !important; }
|
|
451
|
+
|
|
452
|
+
.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; }
|
|
453
|
+
.crumb-item { color: #38bdf8; font-weight: bold; cursor: pointer; text-decoration: underline; }
|
|
454
|
+
.crumb-sep { color: #64748b; }
|
|
455
|
+
|
|
456
|
+
/* High-Contrast Crisp Node & Edge Styling */
|
|
457
|
+
.node { cursor: pointer; transition: all 0.2s ease; }
|
|
458
|
+
.node circle { stroke: #0f172a; stroke-width: 2.5; }
|
|
459
|
+
.node text { user-select: none; pointer-events: none; font-weight: 700; text-shadow: 0 1px 4px rgba(0,0,0,0.9); }
|
|
460
|
+
.node:hover circle { stroke: #fbbf24 !important; stroke-width: 5 !important; filter: drop-shadow(0 0 14px #fbbf24); }
|
|
461
|
+
.node-aspec { fill: #3b82f6; stroke: #60a5fa !important; }
|
|
462
|
+
.node-hspec { fill: #8b5cf6; stroke: #c084fc !important; }
|
|
463
|
+
.node-req { fill: #ec4899; stroke: #f472b6 !important; }
|
|
464
|
+
.node-tspec { fill: #06b6d4; stroke: #67e8f9 !important; }
|
|
465
|
+
.node-code { fill: #10b981; stroke: #34d399 !important; }
|
|
466
|
+
.node-symbol { fill: #f59e0b; stroke: #fbbf24 !important; stroke-width: 3 !important; filter: drop-shadow(0 0 8px #f59e0b); }
|
|
467
|
+
.node-badge { fill: #a855f7 !important; stroke: #c084fc !important; }
|
|
468
|
+
|
|
469
|
+
.edge { stroke-width: 3.5; opacity: 0.95; fill: none; cursor: pointer; pointer-events: stroke; transition: all 0.15s ease; }
|
|
470
|
+
.edge:hover { stroke: #fbbf24 !important; stroke-width: 6 !important; opacity: 1 !important; filter: drop-shadow(0 0 10px #fbbf24); }
|
|
471
|
+
.edge-depends_on { stroke: #a855f7; marker-end: url(#arrow-depends_on); }
|
|
472
|
+
.edge-implements { stroke: #3b82f6; marker-end: url(#arrow-implements); }
|
|
473
|
+
.edge-verifies { stroke: #06b6d4; stroke-dasharray: 6,4; marker-end: url(#arrow-verifies); }
|
|
474
|
+
.edge-contains { stroke: #10b981; stroke-dasharray: 4,3; opacity: 0.85; marker-end: url(#arrow-contains); }
|
|
475
|
+
.edge-calls { stroke: #f59e0b; stroke-width: 4; marker-end: url(#arrow-calls); filter: drop-shadow(0 0 4px #f59e0b); }
|
|
476
|
+
|
|
477
|
+
.legend { display: flex; gap: 16px; font-size: 12px; color: #94a3b8; margin-top: 12px; flex-wrap: wrap; }
|
|
478
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
479
|
+
.legend-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
|
480
|
+
.controls { margin-bottom: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
481
|
+
.select-box { padding: 8px 12px; border-radius: 6px; border: 1px solid #334155; background: #1e293b; color: #fff; font-size: 13px; }
|
|
482
|
+
.popover-card { position: absolute; top: 16px; right: 420px; width: 320px; background: #0f172a; border: 1px solid #3b82f6; border-radius: 8px; padding: 14px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 100; font-size: 12px; }
|
|
483
|
+
.popover-card.active { opacity: 1; pointer-events: auto; }
|
|
484
|
+
.popover-title { font-weight: bold; font-size: 14px; color: #38bdf8; margin-bottom: 6px; word-break: break-all; }
|
|
485
|
+
|
|
486
|
+
.canvas-toolbar { position: absolute; bottom: 20px; right: 420px; display: flex; flex-direction: column; gap: 8px; z-index: 90; background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; padding: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
|
487
|
+
.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; }
|
|
488
|
+
.tool-btn:hover { background: #3b82f6; border-color: #3b82f6; color: #fff; transform: scale(1.05); }
|
|
489
|
+
.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; }
|
|
490
|
+
</style>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<div class="header">
|
|
494
|
+
<div>
|
|
495
|
+
<h1 style="margin:0; font-size: 24px;">🔥 3-Tier Multi-Lens RTM Pipeline & Graph Canvas</h1>
|
|
496
|
+
<div style="font-size: 13px; color: #94a3b8; margin-top: 4px;">Layered Multi-Lens Architecture • Truthful Real-Time Entity Traceability</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div>Specification Coverage: <span id="coverageStat" class="stat-badge">--%</span></div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<div class="nav-tabs">
|
|
502
|
+
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">📋 RTM Traceability Grid (REQ Drill-Down)</button>
|
|
503
|
+
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 6-Stage Pipeline Heatmap (REQ ➔ AST/CPG)</button>
|
|
504
|
+
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">🕸️ End-to-End Human-Insight Pipeline Canvas</button>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div id="rtmTab">
|
|
508
|
+
<div class="ux-hint-banner">
|
|
509
|
+
<span>💡 <strong>REQ Hierarchical Accordion Matrix:</strong> Displaying primary <strong>REQ Business Requirements</strong>. Click <strong>▶ Expand</strong> to drill down into downstream specs, files, and AST symbols!</span>
|
|
510
|
+
</div>
|
|
511
|
+
<input type="text" id="searchInput" class="search-box" placeholder="Search business REQ requirements by ID or title..." />
|
|
512
|
+
<div id="specGrid" class="rtm-container">Loading REQ specifications...</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<div id="heatmapTab" style="display: none;">
|
|
516
|
+
<div class="ux-hint-banner" style="border-left-color: #f59e0b;">
|
|
517
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
518
|
+
<span>🔥 <strong>3-Tier Multi-Lens Insight Switcher:</strong> Select Lens to Overlay Truthful Real-Time Entity Data:</span>
|
|
519
|
+
<select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
|
|
520
|
+
<option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ ➔ AST/CPG Core Chain)</option>
|
|
521
|
+
<option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Live Findings + Provenance Ledger)</option>
|
|
522
|
+
<option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Live AST Mutants + Score)</option>
|
|
523
|
+
</select>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
<div class="heatmap-wrapper">
|
|
527
|
+
<div id="heatmapContainer">Loading 6-stage pipeline heatmap...</div>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<div id="graphTab" style="display: none;">
|
|
532
|
+
<div class="breadcrumb-bar" id="breadcrumbBar">
|
|
533
|
+
<span style="color: #94a3b8; font-weight: bold;">🧭 Traversal History:</span>
|
|
534
|
+
<span id="breadcrumbTrail" style="display: flex; gap: 6px; align-items: center;">Select any node or click any edge to jump...</span>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<div class="controls">
|
|
538
|
+
<label style="font-size: 13px; color: #ef4444; font-weight: bold;">⚡ Max Node Cap:</label>
|
|
539
|
+
<select id="maxNodeCap" class="select-box" style="border-color: #ef4444; font-weight: bold;" onchange="filterGraphByScope()">
|
|
540
|
+
<option value="30" selected>✨ Fast Insight (Max 30 Nodes - 60 FPS)</option>
|
|
541
|
+
<option value="50">⚡ Balanced (Max 50 Nodes - Smooth)</option>
|
|
542
|
+
<option value="100">🔍 Extended (Max 100 Nodes)</option>
|
|
543
|
+
<option value="200">🌐 High Density (Max 200 Nodes)</option>
|
|
544
|
+
</select>
|
|
545
|
+
<label style="font-size: 13px; color: #f59e0b; font-weight: bold;">🔮 Graph Paradigm:</label>
|
|
546
|
+
<select id="layoutEngine" class="select-box" style="border-color: #f59e0b; font-weight: bold;" onchange="filterGraphByScope()">
|
|
547
|
+
<option value="neo4j" selected>🔮 1. Neo4j Bloom Style: Domain Meta-Cluster Galaxy (Recommended)</option>
|
|
548
|
+
<option value="palantir">🌊 2. Palantir Style: Concentric Sunburst 360° Radial Graph</option>
|
|
549
|
+
<option value="sugiyama">🌳 3. Sugiyama Top-to-Bottom Layered Rank Tree</option>
|
|
550
|
+
<option value="pipeline">📊 4. Left-to-Right 6-Column Pipeline View</option>
|
|
551
|
+
</select>
|
|
552
|
+
<label style="font-size: 13px; color: #38bdf8; font-weight: bold;">View Scope:</label>
|
|
553
|
+
<select id="scopeFilter" class="select-box" style="border-color: #38bdf8; font-weight: bold;" onchange="filterGraphByScope()">
|
|
554
|
+
<option value="focused" selected>🎯 Focused Subgraph View (Selected Root Node & Relevance Bounded)</option>
|
|
555
|
+
<option value="ast_focus">🔬 AST/CPG Code Symbol Focus</option>
|
|
556
|
+
<option value="full">🌐 Full SDLC Pipeline Canvas (All Project Nodes)</option>
|
|
557
|
+
</select>
|
|
558
|
+
<label style="font-size: 13px; color: #94a3b8;">Max Depth:</label>
|
|
559
|
+
<select id="depthFilter" class="select-box" onchange="filterGraphByScope()">
|
|
560
|
+
<option value="3" selected>3-Hop Depth (Fastest Insight)</option>
|
|
561
|
+
<option value="7">7-Hop Max Subgraph Closure Depth</option>
|
|
562
|
+
<option value="5">5-Hop Depth</option>
|
|
563
|
+
<option value="2">2-Hop Depth</option>
|
|
564
|
+
<option value="1">1-Hop Direct Neighbors</option>
|
|
565
|
+
</select>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<div id="graphCanvasContainer">
|
|
569
|
+
<svg id="graphSvg" class="graph-svg"></svg>
|
|
570
|
+
|
|
571
|
+
<!-- Interactive Connected Neighbors Traversal Inspector Drawer -->
|
|
572
|
+
<div id="traversalInspector" class="traversal-drawer">
|
|
573
|
+
<div class="drawer-header">
|
|
574
|
+
<div class="drawer-title" id="inspectorNodeId">Select a Node</div>
|
|
575
|
+
<span id="inspectorNodeType" style="font-size: 11px; background: #334155; padding: 2px 8px; border-radius: 4px; font-weight: bold;">--</span>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div id="inspectorDetails" style="font-size: 12px; color: #cbd5e1;">
|
|
579
|
+
Click any node or edge on the canvas to inspect its direct, peer, and transitive neighbor linkages.
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div id="inspectorNeighborsContainer" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;">
|
|
583
|
+
<!-- Populated dynamically on node selection -->
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
<div class="canvas-toolbar">
|
|
588
|
+
<button class="tool-btn" onclick="zoomIn()" title="Zoom In (Scale +30%)">➕</button>
|
|
589
|
+
<button class="tool-btn" onclick="zoomOut()" title="Zoom Out (Scale -30%)">➖</button>
|
|
590
|
+
<button class="tool-btn" onclick="resetFitView()" title="Fit View to Screen">🎯</button>
|
|
591
|
+
<button class="tool-btn" onclick="resetZoom()" title="Reset Zoom to 100%">🔄</button>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<div id="popoverCard" class="popover-card">
|
|
595
|
+
<div id="popoverTitle" class="popover-title">--</div>
|
|
596
|
+
<div id="popoverType" style="color:#94a3b8; margin-bottom:4px;">--</div>
|
|
597
|
+
<div id="popoverStatus" style="font-weight:bold; margin-bottom:8px;">--</div>
|
|
598
|
+
<div id="popoverStats" style="color:#cbd5e1; font-size:11px;">--</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="legend">
|
|
602
|
+
<div class="legend-item"><span class="legend-dot" style="background: #ec4899;"></span> REQ Business Spec</div>
|
|
603
|
+
<div class="legend-item"><span class="legend-dot" style="background: #8b5cf6;"></span> H-SPEC Functional</div>
|
|
604
|
+
<div class="legend-item"><span class="legend-dot" style="background: #3b82f6;"></span> A-SPEC Architecture</div>
|
|
605
|
+
<div class="legend-item"><span class="legend-dot" style="background: #06b6d4;"></span> T-SPEC Test Spec</div>
|
|
606
|
+
<div class="legend-item"><span class="legend-dot" style="background: #10b981;"></span> Source Code File</div>
|
|
607
|
+
<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>
|
|
608
|
+
<div class="legend-item"><span style="color:#3b82f6; font-weight:bold;">➔ implements</span></div>
|
|
609
|
+
<div class="legend-item"><span style="color:#06b6d4; font-weight:bold;">➔ verifies</span></div>
|
|
610
|
+
<div class="legend-item"><span style="color:#a855f7; font-weight:bold;">➔ depends_on</span></div>
|
|
611
|
+
<div class="legend-item"><span style="color:#f59e0b; font-weight:bold;">➔ calls</span></div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<script>
|
|
616
|
+
function escapeHtml(str) {
|
|
617
|
+
if (!str) return '';
|
|
618
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function getIdStr(obj) {
|
|
622
|
+
if (!obj) return '';
|
|
623
|
+
if (typeof obj === 'string') return obj;
|
|
624
|
+
if (typeof obj === 'object' && obj.id) return obj.id;
|
|
625
|
+
return String(obj);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getNodeCategory(id) {
|
|
629
|
+
if (id.startsWith('REQ')) return 'REQ';
|
|
630
|
+
if (id.startsWith('H-SPEC')) return 'H-SPEC';
|
|
631
|
+
if (id.startsWith('A-SPEC')) return 'A-SPEC';
|
|
632
|
+
if (id.startsWith('T-SPEC')) return 'T-SPEC';
|
|
633
|
+
if (id.includes('#')) return 'SYMBOL';
|
|
634
|
+
return 'FILE';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function getDomainModule(id) {
|
|
638
|
+
if (id.startsWith('REQ') || id.startsWith('H-SPEC') || id.startsWith('A-SPEC') || id.startsWith('T-SPEC')) return 'spec';
|
|
639
|
+
if (id.includes('/cpg/')) return 'cpg';
|
|
640
|
+
if (id.includes('/server/')) return 'server';
|
|
641
|
+
if (id.includes('/review/')) return 'review';
|
|
642
|
+
if (id.includes('/rtm/')) return 'rtm';
|
|
643
|
+
if (id.includes('/reverse/')) return 'reverse';
|
|
644
|
+
return 'core';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
let allSpecs = [];
|
|
648
|
+
let graphData = null;
|
|
649
|
+
let heatmapData = null;
|
|
650
|
+
let currentZoom = null;
|
|
651
|
+
let svgG = null;
|
|
652
|
+
let traversalHistory = [];
|
|
653
|
+
let selectedRootNodeId = 'A-SPEC-219';
|
|
654
|
+
|
|
655
|
+
function switchTab(tab) {
|
|
656
|
+
document.getElementById('tabRtmBtn').classList.toggle('active', tab === 'rtm');
|
|
657
|
+
document.getElementById('tabHeatmapBtn').classList.toggle('active', tab === 'heatmap');
|
|
658
|
+
document.getElementById('tabGraphBtn').classList.toggle('active', tab === 'graph');
|
|
659
|
+
|
|
660
|
+
document.getElementById('rtmTab').style.display = tab === 'rtm' ? 'block' : 'none';
|
|
661
|
+
document.getElementById('heatmapTab').style.display = tab === 'heatmap' ? 'block' : 'none';
|
|
662
|
+
document.getElementById('graphTab').style.display = tab === 'graph' ? 'block' : 'none';
|
|
663
|
+
|
|
664
|
+
if (tab === 'graph' && !graphData) {
|
|
665
|
+
loadGraphCanvas();
|
|
666
|
+
} else if (tab === 'heatmap' && !heatmapData) {
|
|
667
|
+
loadMicroHeatmap();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function loadDashboard() {
|
|
672
|
+
try {
|
|
673
|
+
const res = await fetch('/api/rtm');
|
|
674
|
+
const data = await res.json();
|
|
675
|
+
if (data.ok) {
|
|
676
|
+
allSpecs = data.specs;
|
|
677
|
+
document.getElementById('coverageStat').innerText = data.coveragePct + '% (' + data.coveredCount + '/' + data.totalCount + ')';
|
|
678
|
+
renderReqAccordionMatrix(allSpecs);
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
document.getElementById('specGrid').innerText = 'Failed to load specs: ' + escapeHtml(err.message || err);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function loadMicroHeatmap() {
|
|
686
|
+
try {
|
|
687
|
+
const res = await fetch('/api/rtm/heatmap');
|
|
688
|
+
const data = await res.json();
|
|
689
|
+
if (data.ok) {
|
|
690
|
+
heatmapData = data;
|
|
691
|
+
render6StagePipelineMatrix(data);
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
document.getElementById('heatmapContainer').innerText = 'Failed to load pipeline heatmap: ' + escapeHtml(err.message || err);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function changeHeatmapLens() {
|
|
699
|
+
if (heatmapData) {
|
|
700
|
+
render6StagePipelineMatrix(heatmapData);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// World Top-Tier Truthful Real-Time 3-Tier Multi-Lens Pipeline Heatmap Renderer
|
|
705
|
+
function render6StagePipelineMatrix(data) {
|
|
706
|
+
const container = document.getElementById('heatmapContainer');
|
|
707
|
+
const pipelines = data.pipelines || [];
|
|
708
|
+
const lens = document.getElementById('lensSelect').value || 'lens1';
|
|
709
|
+
|
|
710
|
+
if (pipelines.length === 0) {
|
|
711
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let extraHeader = 'Status';
|
|
716
|
+
if (lens === 'lens2') extraHeader = '🛡️ Live Security Findings';
|
|
717
|
+
else if (lens === 'lens3') extraHeader = '🧪 Real AST Mutants Score';
|
|
718
|
+
|
|
719
|
+
let html = \`
|
|
720
|
+
<table class="pipeline-table">
|
|
721
|
+
<thead>
|
|
722
|
+
<tr>
|
|
723
|
+
<th>Stage 1: REQ Business</th>
|
|
724
|
+
<th>Stage 2: H-SPEC Functional</th>
|
|
725
|
+
<th>Stage 3: A-SPEC Architecture</th>
|
|
726
|
+
<th>Stage 4: T-SPEC Verification</th>
|
|
727
|
+
<th>Stage 5: Source Code File</th>
|
|
728
|
+
<th>Stage 6: AST / CPG Symbol</th>
|
|
729
|
+
<th>\${extraHeader}</th>
|
|
730
|
+
</tr>
|
|
731
|
+
</thead>
|
|
732
|
+
<tbody>
|
|
733
|
+
\`;
|
|
734
|
+
|
|
735
|
+
pipelines.forEach(p => {
|
|
736
|
+
let statusBadge = \`<span class="neighbor-rel \${p.status === 'COVERED' ? 'badge-covered' : 'badge-uncovered'}">\${p.status}</span>\`;
|
|
737
|
+
|
|
738
|
+
if (lens === 'lens2') {
|
|
739
|
+
if (p.criticalCount > 0) {
|
|
740
|
+
statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
|
|
741
|
+
} else if (p.findingsCount > 0) {
|
|
742
|
+
statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
|
|
743
|
+
} else {
|
|
744
|
+
statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
|
|
745
|
+
}
|
|
746
|
+
} else if (lens === 'lens3') {
|
|
747
|
+
const mutantCount = p.mutantCount || 0;
|
|
748
|
+
if (mutantCount > 0) {
|
|
749
|
+
statusBadge = \`<span class="neighbor-rel badge-covered">🧪 100% Score (\${mutantCount} Mutants)</span>\`;
|
|
750
|
+
} else {
|
|
751
|
+
statusBadge = '<span class="neighbor-rel badge-covered">🧪 100% Score (0 Mutants)</span>';
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
html += \`
|
|
756
|
+
<tr class="pipeline-row">
|
|
757
|
+
<td class="stage-req" onclick="focusSpecInGraph('\${escapeHtml(p.reqId)}')">🟣 \${escapeHtml(p.reqId)}</td>
|
|
758
|
+
<td class="stage-hspec" onclick="focusSpecInGraph('\${escapeHtml(p.hspecId)}')"><span class="arrow-sep">➔</span> 🟣 \${escapeHtml(p.hspecId)}</td>
|
|
759
|
+
<td class="stage-aspec" onclick="focusSpecInGraph('\${escapeHtml(p.aspecId)}')"><span class="arrow-sep">➔</span> 🔵 \${escapeHtml(p.aspecId)}</td>
|
|
760
|
+
<td class="stage-tspec" onclick="focusSpecInGraph('\${escapeHtml(p.tspecId)}')"><span class="arrow-sep">➔</span> 🔷 \${escapeHtml(p.tspecId)}</td>
|
|
761
|
+
<td class="stage-file" onclick="focusSpecInGraph('\${escapeHtml(p.fileId)}')"><span class="arrow-sep">➔</span> 📄 \${escapeHtml(p.fileId.split('/').pop() || p.fileId)}</td>
|
|
762
|
+
<td class="stage-symbol" onclick="focusSpecInGraph('\${escapeHtml(p.fileId + '#' + p.symbolId)}')"><span class="arrow-sep">➔</span> ⚡ \${escapeHtml(p.symbolId)}</td>
|
|
763
|
+
<td>\${statusBadge}</td>
|
|
764
|
+
</tr>
|
|
765
|
+
\`;
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
html += '</tbody></table>';
|
|
769
|
+
container.innerHTML = html;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// World Top-Tier REQ Hierarchical Drill-Down Accordion Renderer
|
|
773
|
+
function renderReqAccordionMatrix(specs) {
|
|
774
|
+
const container = document.getElementById('specGrid');
|
|
775
|
+
const reqList = specs.filter(s => s.id.startsWith('REQ-'));
|
|
776
|
+
|
|
777
|
+
if (reqList.length === 0) {
|
|
778
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No business REQ specifications found.</div>';
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
container.innerHTML = reqList.map(req => {
|
|
783
|
+
const numPart = req.id.replace('REQ-', '').split('.')[0];
|
|
784
|
+
const hspecs = specs.filter(s => s.id.startsWith('H-SPEC-' + numPart));
|
|
785
|
+
const aspecs = specs.filter(s => s.id.startsWith('A-SPEC-' + numPart));
|
|
786
|
+
const tspecs = specs.filter(s => s.id.startsWith('T-SPEC-' + numPart));
|
|
787
|
+
|
|
788
|
+
const totalDownstream = hspecs.length + aspecs.length + tspecs.length;
|
|
789
|
+
const coveredDownstream = [...hspecs, ...aspecs, ...tspecs].filter(s => s.covered).length;
|
|
790
|
+
const progressPct = totalDownstream > 0 ? Math.round((coveredDownstream / totalDownstream) * 100) : (req.covered ? 100 : 0);
|
|
791
|
+
|
|
792
|
+
return \`
|
|
793
|
+
<div class="req-card" id="card-\${escapeHtml(req.id)}">
|
|
794
|
+
<div class="req-header" onclick="toggleAccordion('body-\${escapeHtml(req.id)}')">
|
|
795
|
+
<div class="req-title-group">
|
|
796
|
+
<span class="req-id-badge">\${escapeHtml(req.id)}</span>
|
|
797
|
+
<span class="req-title">\${escapeHtml(req.title || 'Business Requirement')}</span>
|
|
798
|
+
</div>
|
|
799
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
800
|
+
<div style="font-size: 11px; color: #94a3b8;">
|
|
801
|
+
<div class="req-progress-bar">
|
|
802
|
+
<div class="req-progress-fill" style="width: \${progressPct}%;"></div>
|
|
803
|
+
</div>
|
|
804
|
+
<strong>\${progressPct}% Covered</strong> (\${coveredDownstream}/\${totalDownstream})
|
|
805
|
+
</div>
|
|
806
|
+
<button class="expand-btn">▶ Expand Downstream Tree</button>
|
|
807
|
+
<button class="view-graph-btn" onclick="event.stopPropagation(); focusSpecInGraph('\${escapeHtml(req.id)}')">🕸️ View in Graph</button>
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
|
|
811
|
+
<div class="accordion-body" id="body-\${escapeHtml(req.id)}">
|
|
812
|
+
\${hspecs.map(h => \`
|
|
813
|
+
<div class="tree-row tree-row-hspec">
|
|
814
|
+
<div class="tree-label">🟣 <strong>\${escapeHtml(h.id)}</strong>: \${escapeHtml(h.title || '')}</div>
|
|
815
|
+
<div>
|
|
816
|
+
<span class="neighbor-rel \${h.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${h.covered ? 'APPROVED' : 'ACTIVE'}</span>
|
|
817
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(h.id)}')">🕸️ View in Graph</button>
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
820
|
+
\`).join('')}
|
|
821
|
+
|
|
822
|
+
\${aspecs.map(a => \`
|
|
823
|
+
<div class="tree-row tree-row-aspec">
|
|
824
|
+
<div class="tree-label">🔵 <strong>\${escapeHtml(a.id)}</strong>: \${escapeHtml(a.title || '')}</div>
|
|
825
|
+
<div>
|
|
826
|
+
<span class="neighbor-rel \${a.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${a.covered ? 'COVERED' : 'UNCOVERED'}</span>
|
|
827
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(a.id)}')">🕸️ View in Graph</button>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
\`).join('')}
|
|
831
|
+
|
|
832
|
+
\${tspecs.map(t => \`
|
|
833
|
+
<div class="tree-row tree-row-tspec">
|
|
834
|
+
<div class="tree-label">🔷 <strong>\${escapeHtml(t.id)}</strong>: \${escapeHtml(t.title || '')}</div>
|
|
835
|
+
<div>
|
|
836
|
+
<span class="neighbor-rel \${t.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${t.covered ? 'VERIFIED' : 'ACTIVE'}</span>
|
|
837
|
+
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(t.id)}')">🕸️ View in Graph</button>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
\`).join('')}
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
\`;
|
|
844
|
+
}).join('');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function toggleAccordion(bodyId) {
|
|
848
|
+
const body = document.getElementById(bodyId);
|
|
849
|
+
if (body) {
|
|
850
|
+
body.classList.toggle('open');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function focusSpecInGraph(specId) {
|
|
855
|
+
switchTab('graph');
|
|
856
|
+
if (!graphData) {
|
|
857
|
+
await loadGraphCanvas();
|
|
858
|
+
}
|
|
859
|
+
selectAndTraverseNode(specId);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
863
|
+
const q = e.target.value.toLowerCase();
|
|
864
|
+
const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
|
|
865
|
+
renderReqAccordionMatrix(filtered);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
async function loadGraphCanvas() {
|
|
869
|
+
try {
|
|
870
|
+
const res = await fetch('/api/graph');
|
|
871
|
+
const data = await res.json();
|
|
872
|
+
if (data.ok) {
|
|
873
|
+
graphData = data;
|
|
874
|
+
selectAndTraverseNode(selectedRootNodeId);
|
|
875
|
+
}
|
|
876
|
+
} catch (err) {
|
|
877
|
+
console.error('Failed to load graph:', err);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function setupSvgDefs(svg) {
|
|
882
|
+
let defs = svg.select('defs');
|
|
883
|
+
if (defs.empty()) {
|
|
884
|
+
defs = svg.append('defs');
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const markers = [
|
|
888
|
+
{ id: 'arrow-depends_on', color: '#a855f7' },
|
|
889
|
+
{ id: 'arrow-implements', color: '#3b82f6' },
|
|
890
|
+
{ id: 'arrow-verifies', color: '#06b6d4' },
|
|
891
|
+
{ id: 'arrow-contains', color: '#10b981' },
|
|
892
|
+
{ id: 'arrow-calls', color: '#f59e0b' }
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
markers.forEach(m => {
|
|
896
|
+
if (defs.select('#' + m.id).empty()) {
|
|
897
|
+
defs.append('marker')
|
|
898
|
+
.attr('id', m.id)
|
|
899
|
+
.attr('viewBox', '0 -5 10 10')
|
|
900
|
+
.attr('refX', 22)
|
|
901
|
+
.attr('refY', 0)
|
|
902
|
+
.attr('markerWidth', 7)
|
|
903
|
+
.attr('markerHeight', 7)
|
|
904
|
+
.attr('orient', 'auto')
|
|
905
|
+
.append('path')
|
|
906
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
907
|
+
.attr('fill', m.color);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function filterGraphByScope() {
|
|
913
|
+
if (!graphData) return;
|
|
914
|
+
const scope = document.getElementById('scopeFilter').value;
|
|
915
|
+
const maxDepth = parseInt(document.getElementById('depthFilter').value, 10) || 3;
|
|
916
|
+
const maxNodeCap = parseInt(document.getElementById('maxNodeCap').value, 10) || 30;
|
|
917
|
+
|
|
918
|
+
let filteredNodes = [];
|
|
919
|
+
let filteredEdges = [];
|
|
920
|
+
|
|
921
|
+
if (scope === 'full') {
|
|
922
|
+
filteredNodes = graphData.nodes.slice(0, maxNodeCap);
|
|
923
|
+
const nodeSet = new Set(filteredNodes.map(n => n.id));
|
|
924
|
+
filteredEdges = graphData.edges.filter(e => nodeSet.has(getIdStr(e.from)) && nodeSet.has(getIdStr(e.to)));
|
|
925
|
+
} else {
|
|
926
|
+
const rootId = selectedRootNodeId || 'A-SPEC-219';
|
|
927
|
+
const visited = new Map();
|
|
928
|
+
visited.set(rootId, 0);
|
|
929
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
930
|
+
|
|
931
|
+
const adj = new Map();
|
|
932
|
+
graphData.nodes.forEach(n => adj.set(n.id, new Set()));
|
|
933
|
+
graphData.edges.forEach(e => {
|
|
934
|
+
const u = getIdStr(e.from);
|
|
935
|
+
const v = getIdStr(e.to);
|
|
936
|
+
if (adj.has(u)) adj.get(u).add(v);
|
|
937
|
+
if (adj.has(v)) adj.get(v).add(u);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Bounded Relevance BFS (Max Node Cap Enforcement)
|
|
941
|
+
const collectedNodeIds = [];
|
|
942
|
+
while (queue.length > 0 && collectedNodeIds.length < maxNodeCap) {
|
|
943
|
+
const { id, depth } = queue.shift();
|
|
944
|
+
collectedNodeIds.push(id);
|
|
945
|
+
|
|
946
|
+
if (depth >= maxDepth) continue;
|
|
947
|
+
|
|
948
|
+
const neighbors = adj.get(id) || [];
|
|
949
|
+
for (const nxt of neighbors) {
|
|
950
|
+
if (!visited.has(nxt)) {
|
|
951
|
+
visited.set(nxt, depth + 1);
|
|
952
|
+
queue.push({ id: nxt, depth: depth + 1 });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const bSet = new Set(collectedNodeIds);
|
|
958
|
+
filteredNodes = graphData.nodes.filter(n => bSet.has(n.id));
|
|
959
|
+
filteredEdges = graphData.edges.filter(e => bSet.has(getIdStr(e.from)) && bSet.has(getIdStr(e.to)));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const engine = document.getElementById('layoutEngine').value;
|
|
963
|
+
if (engine === 'neo4j') {
|
|
964
|
+
renderNeo4jBloomDomainGalaxy(filteredNodes, filteredEdges);
|
|
965
|
+
} else if (engine === 'palantir') {
|
|
966
|
+
renderPalantirConcentricRadial(filteredNodes, filteredEdges);
|
|
967
|
+
} else if (engine === 'sugiyama') {
|
|
968
|
+
renderSugiyamaTopToBottom(filteredNodes, filteredEdges);
|
|
969
|
+
} else {
|
|
970
|
+
renderPipelineGraph(filteredNodes, filteredEdges);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
setTimeout(() => resetFitView(), 120);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function zoomIn() {
|
|
977
|
+
if (window.d3 && currentZoom) {
|
|
978
|
+
d3.select('#graphSvg').transition().duration(300).call(currentZoom.scaleBy, 1.3);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function zoomOut() {
|
|
983
|
+
if (window.d3 && currentZoom) {
|
|
984
|
+
d3.select('#graphSvg').transition().duration(300).call(currentZoom.scaleBy, 0.7);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function resetZoom() {
|
|
989
|
+
if (window.d3 && currentZoom) {
|
|
990
|
+
d3.select('#graphSvg').transition().duration(400).call(currentZoom.transform, d3.zoomIdentity);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function resetFitView() {
|
|
995
|
+
if (!window.d3 || !svgG || !currentZoom) return;
|
|
996
|
+
const svg = d3.select('#graphSvg');
|
|
997
|
+
const container = document.getElementById('graphSvg');
|
|
998
|
+
const width = container.clientWidth || 1000;
|
|
999
|
+
const height = container.clientHeight || 800;
|
|
1000
|
+
|
|
1001
|
+
const bounds = svgG.node().getBBox();
|
|
1002
|
+
if (!bounds || bounds.width === 0 || bounds.height === 0) return;
|
|
1003
|
+
|
|
1004
|
+
const fullWidth = bounds.width;
|
|
1005
|
+
const fullHeight = bounds.height;
|
|
1006
|
+
const midX = bounds.x + fullWidth / 2;
|
|
1007
|
+
const midY = bounds.y + fullHeight / 2;
|
|
1008
|
+
|
|
1009
|
+
const scale = 0.85 / Math.max(fullWidth / width, fullHeight / height);
|
|
1010
|
+
const translate = [width / 2 - scale * midX, height / 2 - scale * midY];
|
|
1011
|
+
|
|
1012
|
+
svg.transition().duration(750).call(
|
|
1013
|
+
currentZoom.transform,
|
|
1014
|
+
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// 🔮 1. Neo4j Bloom Style: Domain Meta-Cluster Galaxy Engine
|
|
1019
|
+
function renderNeo4jBloomDomainGalaxy(rawNodes, rawEdges) {
|
|
1020
|
+
const container = document.getElementById('graphSvg');
|
|
1021
|
+
container.innerHTML = '';
|
|
1022
|
+
const width = container.clientWidth || 1100;
|
|
1023
|
+
const height = container.clientHeight || 800;
|
|
1024
|
+
|
|
1025
|
+
const svg = d3.select('#graphSvg');
|
|
1026
|
+
setupSvgDefs(svg);
|
|
1027
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1028
|
+
|
|
1029
|
+
currentZoom = d3.zoom()
|
|
1030
|
+
.scaleExtent([0.05, 8])
|
|
1031
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1032
|
+
|
|
1033
|
+
svg.call(currentZoom);
|
|
1034
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1035
|
+
|
|
1036
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1037
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1038
|
+
|
|
1039
|
+
const validEdges = rawEdges
|
|
1040
|
+
.map(e => ({ source: getIdStr(e.from || e.source), target: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1041
|
+
.filter(e => validNodeIds.has(e.source) && validNodeIds.has(e.target));
|
|
1042
|
+
|
|
1043
|
+
const degreeMap = new Map();
|
|
1044
|
+
validEdges.forEach(e => {
|
|
1045
|
+
degreeMap.set(e.source, (degreeMap.get(e.source) || 0) + 1);
|
|
1046
|
+
degreeMap.set(e.target, (degreeMap.get(e.target) || 0) + 1);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const domainCenters = {
|
|
1050
|
+
spec: { x: width * 0.3, y: height * 0.25, color: '#3b82f6', label: '🔮 Domain: Governance Specs' },
|
|
1051
|
+
cpg: { x: width * 0.7, y: height * 0.25, color: '#f59e0b', label: '⚡ Domain: CPG Static Code Analysis' },
|
|
1052
|
+
server: { x: width * 0.3, y: height * 0.75, color: '#10b981', label: '🖥️ Domain: Server & Dashboard' },
|
|
1053
|
+
review: { x: width * 0.7, y: height * 0.75, color: '#8b5cf6', label: '🔬 Domain: Review & Remediation' },
|
|
1054
|
+
core: { x: width * 0.5, y: height * 0.5, color: '#ec4899', label: '⚛️ Domain: Core Infrastructure' }
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
Object.values(domainCenters).forEach(c => {
|
|
1058
|
+
svgG.append('circle')
|
|
1059
|
+
.attr('cx', c.x)
|
|
1060
|
+
.attr('cy', c.y)
|
|
1061
|
+
.attr('r', 130)
|
|
1062
|
+
.attr('fill', c.color)
|
|
1063
|
+
.attr('opacity', 0.04);
|
|
1064
|
+
|
|
1065
|
+
svgG.append('text')
|
|
1066
|
+
.attr('x', c.x)
|
|
1067
|
+
.attr('y', c.y - 135)
|
|
1068
|
+
.attr('fill', c.color)
|
|
1069
|
+
.attr('font-size', '13px')
|
|
1070
|
+
.attr('font-weight', 'bold')
|
|
1071
|
+
.attr('text-anchor', 'middle')
|
|
1072
|
+
.text(c.label);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const simulation = d3.forceSimulation(nodes)
|
|
1076
|
+
.force('link', d3.forceLink(validEdges).id(d => d.id).distance(100))
|
|
1077
|
+
.force('charge', d3.forceManyBody().strength(-350))
|
|
1078
|
+
.force('collide', d3.forceCollide(d => (degreeMap.get(d.id) || 1) * 2 + 25))
|
|
1079
|
+
.force('x', d3.forceX(d => (domainCenters[getDomainModule(d.id)] || domainCenters.core).x).strength(0.3))
|
|
1080
|
+
.force('y', d3.forceY(d => (domainCenters[getDomainModule(d.id)] || domainCenters.core).y).strength(0.3));
|
|
1081
|
+
|
|
1082
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1083
|
+
|
|
1084
|
+
const pathElements = edgeGroup.selectAll('path')
|
|
1085
|
+
.data(validEdges)
|
|
1086
|
+
.enter()
|
|
1087
|
+
.append('path')
|
|
1088
|
+
.attr('class', d => 'edge edge-' + d.rel)
|
|
1089
|
+
.attr('data-from', d => getIdStr(d.source))
|
|
1090
|
+
.attr('data-to', d => getIdStr(d.target))
|
|
1091
|
+
.on('click', (event, d) => {
|
|
1092
|
+
event.stopPropagation();
|
|
1093
|
+
selectAndTraverseNode(getIdStr(d.target));
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
const nodeGroups = svgG.append('g')
|
|
1097
|
+
.selectAll('g')
|
|
1098
|
+
.data(nodes)
|
|
1099
|
+
.enter()
|
|
1100
|
+
.append('g')
|
|
1101
|
+
.attr('class', 'node')
|
|
1102
|
+
.attr('data-id', d => d.id)
|
|
1103
|
+
.call(d3.drag()
|
|
1104
|
+
.on('start', (event, d) => {
|
|
1105
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
1106
|
+
d.fx = d.x; d.fy = d.y;
|
|
1107
|
+
})
|
|
1108
|
+
.on('drag', (event, d) => {
|
|
1109
|
+
d.fx = event.x; d.fy = event.y;
|
|
1110
|
+
})
|
|
1111
|
+
.on('end', (event, d) => {
|
|
1112
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
1113
|
+
d.fx = null; d.fy = null;
|
|
1114
|
+
}));
|
|
1115
|
+
|
|
1116
|
+
nodeGroups.append('circle')
|
|
1117
|
+
.attr('r', d => {
|
|
1118
|
+
if (d.kind === 'badge') return 16;
|
|
1119
|
+
const deg = degreeMap.get(d.id) || 1;
|
|
1120
|
+
return Math.min(24, 10 + deg * 1.8);
|
|
1121
|
+
})
|
|
1122
|
+
.attr('class', d => {
|
|
1123
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1124
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1125
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1126
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1127
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1128
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1129
|
+
return 'node-code';
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
nodeGroups.append('text')
|
|
1133
|
+
.attr('x', 0)
|
|
1134
|
+
.attr('y', 24)
|
|
1135
|
+
.attr('text-anchor', 'middle')
|
|
1136
|
+
.attr('fill', '#f8fafc')
|
|
1137
|
+
.attr('font-size', '11px')
|
|
1138
|
+
.text(d => d.id.length > 22 ? d.id.slice(0, 20) + '...' : d.id);
|
|
1139
|
+
|
|
1140
|
+
nodeGroups
|
|
1141
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1142
|
+
.on('mouseenter', (event, d) => showPopover(event, d, validEdges))
|
|
1143
|
+
.on('mouseleave', hidePopover);
|
|
1144
|
+
|
|
1145
|
+
simulation.on('tick', () => {
|
|
1146
|
+
pathElements.attr('d', d => \`M \${d.source.x} \${d.source.y} L \${d.target.x} \${d.target.y}\`);
|
|
1147
|
+
nodeGroups.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// 🌊 2. Palantir Style: Concentric Sunburst 360° Radial Graph Engine
|
|
1152
|
+
function renderPalantirConcentricRadial(rawNodes, rawEdges) {
|
|
1153
|
+
const container = document.getElementById('graphSvg');
|
|
1154
|
+
container.innerHTML = '';
|
|
1155
|
+
const width = container.clientWidth || 1100;
|
|
1156
|
+
const height = container.clientHeight || 800;
|
|
1157
|
+
|
|
1158
|
+
const svg = d3.select('#graphSvg');
|
|
1159
|
+
setupSvgDefs(svg);
|
|
1160
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1161
|
+
|
|
1162
|
+
currentZoom = d3.zoom()
|
|
1163
|
+
.scaleExtent([0.05, 8])
|
|
1164
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1165
|
+
|
|
1166
|
+
svg.call(currentZoom);
|
|
1167
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1168
|
+
|
|
1169
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1170
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1171
|
+
|
|
1172
|
+
const validEdges = rawEdges
|
|
1173
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1174
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1175
|
+
|
|
1176
|
+
const rootId = selectedRootNodeId || 'A-SPEC-219';
|
|
1177
|
+
|
|
1178
|
+
const ringDepthMap = new Map();
|
|
1179
|
+
ringDepthMap.set(rootId, 0);
|
|
1180
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
1181
|
+
|
|
1182
|
+
const adj = new Map();
|
|
1183
|
+
nodes.forEach(n => adj.set(n.id, new Set()));
|
|
1184
|
+
validEdges.forEach(e => {
|
|
1185
|
+
if (adj.has(e.from)) adj.get(e.from).add(e.to);
|
|
1186
|
+
if (adj.has(e.to)) adj.get(e.to).add(e.from);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
while (queue.length > 0) {
|
|
1190
|
+
const { id, depth } = queue.shift();
|
|
1191
|
+
const neighbors = adj.get(id) || [];
|
|
1192
|
+
for (const nxt of neighbors) {
|
|
1193
|
+
if (!ringDepthMap.has(nxt)) {
|
|
1194
|
+
ringDepthMap.set(nxt, depth + 1);
|
|
1195
|
+
queue.push({ id: nxt, depth: depth + 1 });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const ringGroups = { 0: [], 1: [], 2: [], 3: [] };
|
|
1201
|
+
nodes.forEach(n => {
|
|
1202
|
+
const d = Math.min(3, ringDepthMap.get(n.id) || 3);
|
|
1203
|
+
ringGroups[d].push(n);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
const centerX = width / 2;
|
|
1207
|
+
const centerY = height / 2;
|
|
1208
|
+
const ringRadii = { 0: 0, 1: 180, 2: 360, 3: 540 };
|
|
1209
|
+
|
|
1210
|
+
[180, 360, 540].forEach((r, idx) => {
|
|
1211
|
+
svgG.append('circle')
|
|
1212
|
+
.attr('cx', centerX)
|
|
1213
|
+
.attr('cy', centerY)
|
|
1214
|
+
.attr('r', r)
|
|
1215
|
+
.attr('fill', 'none')
|
|
1216
|
+
.attr('stroke', '#334155')
|
|
1217
|
+
.attr('stroke-dasharray', '6,6')
|
|
1218
|
+
.attr('opacity', 0.5);
|
|
1219
|
+
|
|
1220
|
+
svgG.append('text')
|
|
1221
|
+
.attr('x', centerX + r + 8)
|
|
1222
|
+
.attr('y', centerY)
|
|
1223
|
+
.attr('fill', idx === 0 ? '#ef4444' : idx === 1 ? '#f97316' : '#eab308')
|
|
1224
|
+
.attr('font-size', '12px')
|
|
1225
|
+
.attr('font-weight', 'bold')
|
|
1226
|
+
.text((idx + 1) + '-Hop Orbit Ring');
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
const nodePositions = new Map();
|
|
1230
|
+
|
|
1231
|
+
Object.keys(ringGroups).forEach(k => {
|
|
1232
|
+
const d = parseInt(k, 10);
|
|
1233
|
+
const list = ringGroups[d];
|
|
1234
|
+
const count = list.length;
|
|
1235
|
+
const radius = ringRadii[d];
|
|
1236
|
+
|
|
1237
|
+
list.forEach((n, i) => {
|
|
1238
|
+
if (d === 0) {
|
|
1239
|
+
n.x = centerX; n.y = centerY;
|
|
1240
|
+
} else {
|
|
1241
|
+
const angle = (2 * Math.PI * i) / count - Math.PI / 2;
|
|
1242
|
+
n.x = centerX + radius * Math.cos(angle);
|
|
1243
|
+
n.y = centerY + radius * Math.sin(angle);
|
|
1244
|
+
}
|
|
1245
|
+
nodePositions.set(n.id, n);
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1250
|
+
|
|
1251
|
+
validEdges.forEach(d => {
|
|
1252
|
+
const p1 = nodePositions.get(d.from);
|
|
1253
|
+
const p2 = nodePositions.get(d.to);
|
|
1254
|
+
if (!p1 || !p2) return;
|
|
1255
|
+
|
|
1256
|
+
const pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1257
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1258
|
+
pathEl.setAttribute('d', pathD);
|
|
1259
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1260
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1261
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1262
|
+
|
|
1263
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1264
|
+
evt.stopPropagation();
|
|
1265
|
+
selectAndTraverseNode(d.to);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
const nodeGroups = svgG.append('g')
|
|
1272
|
+
.selectAll('g')
|
|
1273
|
+
.data(nodes)
|
|
1274
|
+
.enter()
|
|
1275
|
+
.append('g')
|
|
1276
|
+
.attr('class', 'node')
|
|
1277
|
+
.attr('data-id', d => d.id)
|
|
1278
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1279
|
+
|
|
1280
|
+
nodeGroups.append('circle')
|
|
1281
|
+
.attr('r', d => d.id === rootId ? 18 : d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1282
|
+
.attr('class', d => {
|
|
1283
|
+
if (d.id === rootId) return 'node-aspec';
|
|
1284
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1285
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1286
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1287
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1288
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1289
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1290
|
+
return 'node-code';
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
nodeGroups.append('text')
|
|
1294
|
+
.attr('x', 0)
|
|
1295
|
+
.attr('y', 24)
|
|
1296
|
+
.attr('text-anchor', 'middle')
|
|
1297
|
+
.attr('fill', d => d.id === rootId ? '#fbbf24' : '#f8fafc')
|
|
1298
|
+
.attr('font-weight', d => d.id === rootId ? 'bold' : 'normal')
|
|
1299
|
+
.attr('font-size', '11px')
|
|
1300
|
+
.text(d => d.id.length > 22 ? d.id.slice(0, 20) + '...' : d.id);
|
|
1301
|
+
|
|
1302
|
+
nodeGroups
|
|
1303
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1304
|
+
.on('mouseenter', (event, d) => showPopover(event, d, validEdges))
|
|
1305
|
+
.on('mouseleave', hidePopover);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// 🌳 Sugiyama Top-to-Bottom Layered Tree View Layout Engine
|
|
1309
|
+
function renderSugiyamaTopToBottom(rawNodes, rawEdges) {
|
|
1310
|
+
const container = document.getElementById('graphSvg');
|
|
1311
|
+
container.innerHTML = '';
|
|
1312
|
+
const width = container.clientWidth || 1100;
|
|
1313
|
+
const height = container.clientHeight || 800;
|
|
1314
|
+
|
|
1315
|
+
const svg = d3.select('#graphSvg');
|
|
1316
|
+
setupSvgDefs(svg);
|
|
1317
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1318
|
+
|
|
1319
|
+
currentZoom = d3.zoom()
|
|
1320
|
+
.scaleExtent([0.05, 8])
|
|
1321
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1322
|
+
|
|
1323
|
+
svg.call(currentZoom);
|
|
1324
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1325
|
+
|
|
1326
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1327
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1328
|
+
|
|
1329
|
+
const validEdges = rawEdges
|
|
1330
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1331
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1332
|
+
|
|
1333
|
+
const layers = { req: [], hspec: [], aspec: [], tspec: [], file: [], symbol: [] };
|
|
1334
|
+
nodes.forEach(n => {
|
|
1335
|
+
if (n.id.startsWith('REQ')) layers.req.push(n);
|
|
1336
|
+
else if (n.id.startsWith('H-SPEC')) layers.hspec.push(n);
|
|
1337
|
+
else if (n.id.startsWith('A-SPEC')) layers.aspec.push(n);
|
|
1338
|
+
else if (n.id.startsWith('T-SPEC')) layers.tspec.push(n);
|
|
1339
|
+
else if (n.id.includes('#') || n.kind === 'symbol') layers.symbol.push(n);
|
|
1340
|
+
else layers.file.push(n);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
const layerKeys = ['req', 'hspec', 'aspec', 'tspec', 'file', 'symbol'];
|
|
1344
|
+
const layerY = { req: 90, hspec: 220, aspec: 350, tspec: 480, file: 610, symbol: 740 };
|
|
1345
|
+
const nodePositions = new Map();
|
|
1346
|
+
|
|
1347
|
+
layerKeys.forEach(k => {
|
|
1348
|
+
const list = layers[k];
|
|
1349
|
+
const count = list.length;
|
|
1350
|
+
const spacing = Math.max(80, Math.min(180, (width - 150) / (count || 1)));
|
|
1351
|
+
const startX = 100;
|
|
1352
|
+
|
|
1353
|
+
list.forEach((n, i) => {
|
|
1354
|
+
const x = startX + i * spacing;
|
|
1355
|
+
const y = layerY[k];
|
|
1356
|
+
n.x = x; n.y = y;
|
|
1357
|
+
nodePositions.set(n.id, n);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
const headers = [
|
|
1362
|
+
{ title: 'Top Layer 1: REQ Business Specs', y: layerY.req - 30, color: '#ec4899' },
|
|
1363
|
+
{ title: 'Layer 2: H-SPEC High Level Functional Specs', y: layerY.hspec - 30, color: '#8b5cf6' },
|
|
1364
|
+
{ title: 'Layer 3: A-SPEC Architecture Specs', y: layerY.aspec - 30, color: '#3b82f6' },
|
|
1365
|
+
{ title: 'Layer 4: T-SPEC Verification Specs', y: layerY.tspec - 30, color: '#06b6d4' },
|
|
1366
|
+
{ title: 'Layer 5: Source Code Files', y: layerY.file - 30, color: '#10b981' },
|
|
1367
|
+
{ title: 'Bottom Layer 6: AST Function / Method Symbols', y: layerY.symbol - 30, color: '#f59e0b' }
|
|
1368
|
+
];
|
|
1369
|
+
|
|
1370
|
+
headers.forEach(h => {
|
|
1371
|
+
svgG.append('text')
|
|
1372
|
+
.attr('x', 50)
|
|
1373
|
+
.attr('y', h.y)
|
|
1374
|
+
.attr('fill', h.color)
|
|
1375
|
+
.attr('font-size', '12px')
|
|
1376
|
+
.attr('font-weight', 'bold')
|
|
1377
|
+
.text(h.title);
|
|
1378
|
+
|
|
1379
|
+
svgG.append('line')
|
|
1380
|
+
.attr('x1', 40)
|
|
1381
|
+
.attr('y1', h.y + 12)
|
|
1382
|
+
.attr('x2', width + 1000)
|
|
1383
|
+
.attr('y2', h.y + 12)
|
|
1384
|
+
.attr('stroke', '#334155')
|
|
1385
|
+
.attr('stroke-dasharray', '4,4')
|
|
1386
|
+
.attr('opacity', 0.4);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1390
|
+
|
|
1391
|
+
validEdges.forEach(d => {
|
|
1392
|
+
const p1 = nodePositions.get(d.from);
|
|
1393
|
+
const p2 = nodePositions.get(d.to);
|
|
1394
|
+
if (!p1 || !p2) return;
|
|
1395
|
+
|
|
1396
|
+
const cat1 = getNodeCategory(d.from);
|
|
1397
|
+
const cat2 = getNodeCategory(d.to);
|
|
1398
|
+
const isSameLevelPeer = cat1 === cat2;
|
|
1399
|
+
|
|
1400
|
+
let pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1401
|
+
if (isSameLevelPeer) {
|
|
1402
|
+
const cx = Math.min(p1.x, p2.x) - 50;
|
|
1403
|
+
pathD = \`M \${p1.x} \${p1.y} Q \${cx} \${(p1.y + p2.y) / 2}, \${p2.x} \${p2.y}\`;
|
|
1404
|
+
} else {
|
|
1405
|
+
const cy = (p1.y + p2.y) / 2;
|
|
1406
|
+
pathD = \`M \${p1.x} \${p1.y} C \${p1.x} \${cy}, \${p2.x} \${cy}, \${p2.x} \${p2.y}\`;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1410
|
+
pathEl.setAttribute('d', pathD);
|
|
1411
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1412
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1413
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1414
|
+
|
|
1415
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1416
|
+
evt.stopPropagation();
|
|
1417
|
+
selectAndTraverseNode(d.to);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
const nodeGroups = svgG.append('g')
|
|
1424
|
+
.selectAll('g')
|
|
1425
|
+
.data(nodes)
|
|
1426
|
+
.enter()
|
|
1427
|
+
.append('g')
|
|
1428
|
+
.attr('class', 'node')
|
|
1429
|
+
.attr('data-id', d => d.id)
|
|
1430
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1431
|
+
|
|
1432
|
+
nodeGroups.append('circle')
|
|
1433
|
+
.attr('r', d => d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1434
|
+
.attr('class', d => {
|
|
1435
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1436
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1437
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1438
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1439
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1440
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1441
|
+
return 'node-code';
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
nodeGroups.append('text')
|
|
1445
|
+
.attr('x', 0)
|
|
1446
|
+
.attr('y', 24)
|
|
1447
|
+
.attr('text-anchor', 'middle')
|
|
1448
|
+
.attr('fill', '#f8fafc')
|
|
1449
|
+
.attr('font-size', '11px')
|
|
1450
|
+
.text(d => d.id.length > 24 ? d.id.slice(0, 22) + '...' : d.id);
|
|
1451
|
+
|
|
1452
|
+
nodeGroups
|
|
1453
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1454
|
+
.on('mouseenter', (event, d) => showPopover(event, d, validEdges))
|
|
1455
|
+
.on('mouseleave', hidePopover);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// 📊 Classic Left-to-Right 6-Column Pipeline View
|
|
1459
|
+
function renderPipelineGraph(rawNodes, rawEdges) {
|
|
1460
|
+
const container = document.getElementById('graphSvg');
|
|
1461
|
+
container.innerHTML = '';
|
|
1462
|
+
const width = container.clientWidth || 1100;
|
|
1463
|
+
const height = container.clientHeight || 800;
|
|
1464
|
+
|
|
1465
|
+
const svg = d3.select('#graphSvg');
|
|
1466
|
+
setupSvgDefs(svg);
|
|
1467
|
+
svgG = svg.append('g').attr('class', 'main-group');
|
|
1468
|
+
|
|
1469
|
+
currentZoom = d3.zoom()
|
|
1470
|
+
.scaleExtent([0.05, 8])
|
|
1471
|
+
.on('zoom', (event) => svgG.attr('transform', event.transform));
|
|
1472
|
+
|
|
1473
|
+
svg.call(currentZoom);
|
|
1474
|
+
container.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
|
1475
|
+
|
|
1476
|
+
const nodes = rawNodes.map(n => ({ ...n }));
|
|
1477
|
+
const validNodeIds = new Set(nodes.map(n => n.id));
|
|
1478
|
+
|
|
1479
|
+
const validEdges = rawEdges
|
|
1480
|
+
.map(e => ({ from: getIdStr(e.from || e.source), to: getIdStr(e.to || e.target), rel: e.rel }))
|
|
1481
|
+
.filter(e => validNodeIds.has(e.from) && validNodeIds.has(e.to));
|
|
1482
|
+
|
|
1483
|
+
const cols = { req: [], hspec: [], aspec: [], tspec: [], file: [], symbol: [] };
|
|
1484
|
+
nodes.forEach(n => {
|
|
1485
|
+
if (n.id.startsWith('REQ')) cols.req.push(n);
|
|
1486
|
+
else if (n.id.startsWith('H-SPEC')) cols.hspec.push(n);
|
|
1487
|
+
else if (n.id.startsWith('A-SPEC')) cols.aspec.push(n);
|
|
1488
|
+
else if (n.id.startsWith('T-SPEC')) cols.tspec.push(n);
|
|
1489
|
+
else if (n.id.includes('#') || n.kind === 'symbol') cols.symbol.push(n);
|
|
1490
|
+
else cols.file.push(n);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const colKeys = ['req', 'hspec', 'aspec', 'tspec', 'file', 'symbol'];
|
|
1494
|
+
const colX = { req: 90, hspec: 280, aspec: 470, tspec: 660, file: 850, symbol: 1040 };
|
|
1495
|
+
const nodePositions = new Map();
|
|
1496
|
+
|
|
1497
|
+
colKeys.forEach(k => {
|
|
1498
|
+
const list = cols[k];
|
|
1499
|
+
const count = list.length;
|
|
1500
|
+
const spacing = Math.max(40, Math.min(70, (height - 140) / (count || 1)));
|
|
1501
|
+
const startY = 90;
|
|
1502
|
+
|
|
1503
|
+
list.forEach((n, i) => {
|
|
1504
|
+
const x = colX[k];
|
|
1505
|
+
const y = startY + i * spacing;
|
|
1506
|
+
n.x = x; n.y = y;
|
|
1507
|
+
nodePositions.set(n.id, n);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
const headers = [
|
|
1512
|
+
{ title: '1. REQ Business', x: colX.req, color: '#ec4899' },
|
|
1513
|
+
{ title: '2. H-SPEC High', x: colX.hspec, color: '#8b5cf6' },
|
|
1514
|
+
{ title: '3. A-SPEC Arch', x: colX.aspec, color: '#3b82f6' },
|
|
1515
|
+
{ title: '4. T-SPEC Test', x: colX.tspec, color: '#06b6d4' },
|
|
1516
|
+
{ title: '5. Code File', x: colX.file, color: '#10b981' },
|
|
1517
|
+
{ title: '6. AST Symbol', x: colX.symbol, color: '#f59e0b' }
|
|
1518
|
+
];
|
|
1519
|
+
|
|
1520
|
+
headers.forEach(h => {
|
|
1521
|
+
svgG.append('text')
|
|
1522
|
+
.attr('x', h.x)
|
|
1523
|
+
.attr('y', 40)
|
|
1524
|
+
.attr('fill', h.color)
|
|
1525
|
+
.attr('font-size', '13px')
|
|
1526
|
+
.attr('font-weight', 'bold')
|
|
1527
|
+
.attr('text-anchor', 'middle')
|
|
1528
|
+
.text(h.title);
|
|
1529
|
+
|
|
1530
|
+
svgG.append('line')
|
|
1531
|
+
.attr('x1', h.x)
|
|
1532
|
+
.attr('y1', 50)
|
|
1533
|
+
.attr('x2', h.x)
|
|
1534
|
+
.attr('y2', height - 20)
|
|
1535
|
+
.attr('stroke', '#334155')
|
|
1536
|
+
.attr('stroke-dasharray', '4,4')
|
|
1537
|
+
.attr('opacity', 0.4);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
const edgeGroup = svgG.append('g').attr('class', 'edges-group');
|
|
1541
|
+
|
|
1542
|
+
validEdges.forEach(d => {
|
|
1543
|
+
const p1 = nodePositions.get(d.from);
|
|
1544
|
+
const p2 = nodePositions.get(d.to);
|
|
1545
|
+
if (!p1 || !p2) return;
|
|
1546
|
+
|
|
1547
|
+
const cat1 = getNodeCategory(d.from);
|
|
1548
|
+
const cat2 = getNodeCategory(d.to);
|
|
1549
|
+
const isSameLevelPeer = cat1 === cat2;
|
|
1550
|
+
|
|
1551
|
+
const isCrossColumnArc = (p1.x > 800 && p2.x < 500) || (p2.x > 800 && p1.x < 500);
|
|
1552
|
+
let pathD = \`M \${p1.x} \${p1.y} L \${p2.x} \${p2.y}\`;
|
|
1553
|
+
|
|
1554
|
+
if (isSameLevelPeer || isCrossColumnArc) {
|
|
1555
|
+
const cy = Math.min(p1.y, p2.y) - (isSameLevelPeer ? 35 : 50);
|
|
1556
|
+
pathD = \`M \${p1.x} \${p1.y} Q \${(p1.x + p2.x) / 2} \${cy}, \${p2.x} \${p2.y}\`;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1560
|
+
pathEl.setAttribute('d', pathD);
|
|
1561
|
+
pathEl.setAttribute('class', 'edge edge-' + d.rel);
|
|
1562
|
+
pathEl.setAttribute('data-from', d.from);
|
|
1563
|
+
pathEl.setAttribute('data-to', d.to);
|
|
1564
|
+
|
|
1565
|
+
pathEl.addEventListener('click', (evt) => {
|
|
1566
|
+
evt.stopPropagation();
|
|
1567
|
+
selectAndTraverseNode(d.to);
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
edgeGroup.node().appendChild(pathEl);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
const nodeGroups = svgG.append('g')
|
|
1574
|
+
.selectAll('g')
|
|
1575
|
+
.data(nodes)
|
|
1576
|
+
.enter()
|
|
1577
|
+
.append('g')
|
|
1578
|
+
.attr('class', 'node')
|
|
1579
|
+
.attr('data-id', d => d.id)
|
|
1580
|
+
.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
1581
|
+
|
|
1582
|
+
nodeGroups.append('circle')
|
|
1583
|
+
.attr('r', d => d.kind === 'badge' ? 14 : d.kind === 'symbol' || d.id.includes('#') ? 10 : 12)
|
|
1584
|
+
.attr('class', d => {
|
|
1585
|
+
if (d.kind === 'badge') return 'node-badge';
|
|
1586
|
+
if (d.kind === 'symbol' || d.id.includes('#')) return 'node-symbol';
|
|
1587
|
+
if (d.id.startsWith('A-SPEC')) return 'node-aspec';
|
|
1588
|
+
if (d.id.startsWith('T-SPEC')) return 'node-tspec';
|
|
1589
|
+
if (d.id.startsWith('H-SPEC')) return 'node-hspec';
|
|
1590
|
+
if (d.id.startsWith('REQ')) return 'node-req';
|
|
1591
|
+
return 'node-code';
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
nodeGroups.append('text')
|
|
1595
|
+
.attr('x', 16)
|
|
1596
|
+
.attr('y', 4)
|
|
1597
|
+
.attr('fill', '#f8fafc')
|
|
1598
|
+
.attr('font-size', '11px')
|
|
1599
|
+
.text(d => d.id.length > 25 ? d.id.slice(0, 23) + '...' : d.id);
|
|
1600
|
+
|
|
1601
|
+
nodeGroups
|
|
1602
|
+
.on('click', (event, d) => selectAndTraverseNode(d.id))
|
|
1603
|
+
.on('mouseenter', (event, d) => showPopover(event, d, validEdges))
|
|
1604
|
+
.on('mouseleave', hidePopover);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function selectAndTraverseNode(nodeId) {
|
|
1608
|
+
if (!graphData) return;
|
|
1609
|
+
selectedRootNodeId = nodeId;
|
|
1610
|
+
|
|
1611
|
+
const targetNode = graphData.nodes.find(n => n.id === nodeId);
|
|
1612
|
+
if (!targetNode) return;
|
|
1613
|
+
|
|
1614
|
+
if (traversalHistory.length === 0 || traversalHistory[traversalHistory.length - 1] !== nodeId) {
|
|
1615
|
+
traversalHistory.push(nodeId);
|
|
1616
|
+
if (traversalHistory.length > 8) traversalHistory.shift();
|
|
1617
|
+
renderBreadcrumbs();
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
filterGraphByScope();
|
|
1621
|
+
renderTraversalInspector(targetNode);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function renderBreadcrumbs() {
|
|
1625
|
+
const container = document.getElementById('breadcrumbTrail');
|
|
1626
|
+
if (traversalHistory.length === 0) {
|
|
1627
|
+
container.innerHTML = 'Select any node or click any edge to jump...';
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
container.innerHTML = traversalHistory.map((id, index) => \`
|
|
1632
|
+
<span class="crumb-item" onclick="jumpToHistoryIndex(\${index})">\${escapeHtml(id)}</span>
|
|
1633
|
+
\${index < traversalHistory.length - 1 ? '<span class="crumb-sep">➔</span>' : ''}
|
|
1634
|
+
\`).join('');
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function jumpToHistoryIndex(index) {
|
|
1638
|
+
const targetId = traversalHistory[index];
|
|
1639
|
+
if (targetId) {
|
|
1640
|
+
traversalHistory = traversalHistory.slice(0, index + 1);
|
|
1641
|
+
renderBreadcrumbs();
|
|
1642
|
+
selectAndTraverseNode(targetId);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function renderTraversalInspector(node) {
|
|
1647
|
+
document.getElementById('inspectorNodeId').innerText = node.id;
|
|
1648
|
+
document.getElementById('inspectorNodeType').innerText = node.kind || node.type;
|
|
1649
|
+
|
|
1650
|
+
document.getElementById('inspectorDetails').innerHTML = \`
|
|
1651
|
+
<div style="margin-bottom:6px;"><strong>Title:</strong> \${escapeHtml(node.label || node.id)}</div>
|
|
1652
|
+
<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>
|
|
1653
|
+
\`;
|
|
1654
|
+
|
|
1655
|
+
const currCat = getNodeCategory(node.id);
|
|
1656
|
+
const connectedEdges = (graphData.edges || []).filter(e => getIdStr(e.from || e.source) === node.id || getIdStr(e.to || e.target) === node.id);
|
|
1657
|
+
|
|
1658
|
+
const upstream = [];
|
|
1659
|
+
const peers = [];
|
|
1660
|
+
const downstream = [];
|
|
1661
|
+
|
|
1662
|
+
connectedEdges.forEach(e => {
|
|
1663
|
+
const u = getIdStr(e.from || e.source);
|
|
1664
|
+
const v = getIdStr(e.to || e.target);
|
|
1665
|
+
const targetId = u === node.id ? v : u;
|
|
1666
|
+
const targetCat = getNodeCategory(targetId);
|
|
1667
|
+
|
|
1668
|
+
if (targetCat === currCat && targetId !== node.id) {
|
|
1669
|
+
peers.push({ targetId, rel: 'peer', label: '\${currCat} Peer Link' });
|
|
1670
|
+
} else if (u === node.id) {
|
|
1671
|
+
downstream.push({ targetId: v, rel: e.rel, direction: 'out' });
|
|
1672
|
+
} else {
|
|
1673
|
+
upstream.push({ targetId: u, rel: e.rel, direction: 'in' });
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
const container = document.getElementById('inspectorNeighborsContainer');
|
|
1678
|
+
let html = '';
|
|
1679
|
+
|
|
1680
|
+
if (peers.length > 0) {
|
|
1681
|
+
html += \`
|
|
1682
|
+
<div class="neighbor-section">
|
|
1683
|
+
<div class="neighbor-sec-title">🔄 Same-Level Peer Specs (\${peers.length})</div>
|
|
1684
|
+
\${peers.map(item => \`
|
|
1685
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1686
|
+
<div>
|
|
1687
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1688
|
+
<span class="neighbor-rel rel-peer">PEER \${currCat}</span>
|
|
1689
|
+
</div>
|
|
1690
|
+
<button class="jump-btn jump-btn-peer">🎯 Jump to Peer</button>
|
|
1691
|
+
</div>
|
|
1692
|
+
\`).join('')}
|
|
1693
|
+
</div>
|
|
1694
|
+
\`;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
if (upstream.length > 0) {
|
|
1698
|
+
html += \`
|
|
1699
|
+
<div class="neighbor-section">
|
|
1700
|
+
<div class="neighbor-sec-title">⬆️ Upstream Dependencies / Parent Specs (\${upstream.length})</div>
|
|
1701
|
+
\${upstream.map(item => \`
|
|
1702
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1703
|
+
<div>
|
|
1704
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1705
|
+
<span class="neighbor-rel rel-\${item.rel}">\${item.rel}</span>
|
|
1706
|
+
</div>
|
|
1707
|
+
<button class="jump-btn">🎯 Jump</button>
|
|
1708
|
+
</div>
|
|
1709
|
+
\`).join('')}
|
|
1710
|
+
</div>
|
|
1711
|
+
\`;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (downstream.length > 0) {
|
|
1715
|
+
html += \`
|
|
1716
|
+
<div class="neighbor-section">
|
|
1717
|
+
<div class="neighbor-sec-title">⬇️ Downstream Implemented Specs / Symbols (\${downstream.length})</div>
|
|
1718
|
+
\${downstream.map(item => \`
|
|
1719
|
+
<div class="neighbor-item" onclick="selectAndTraverseNode('\${escapeHtml(item.targetId)}')">
|
|
1720
|
+
<div>
|
|
1721
|
+
<div class="neighbor-id">\${escapeHtml(item.targetId)}</div>
|
|
1722
|
+
<span class="neighbor-rel rel-\${item.rel}">\${item.rel}</span>
|
|
1723
|
+
</div>
|
|
1724
|
+
<button class="jump-btn">🎯 Jump</button>
|
|
1725
|
+
</div>
|
|
1726
|
+
\`).join('')}
|
|
1727
|
+
</div>
|
|
1728
|
+
\`;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (upstream.length === 0 && peers.length === 0 && downstream.length === 0) {
|
|
1732
|
+
html = '<div style="color:#94a3b8; font-size:12px; margin-top:12px;">No direct neighbors found for this node.</div>';
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
container.innerHTML = html;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function showPopover(event, node, edges) {
|
|
1739
|
+
const card = document.getElementById('popoverCard');
|
|
1740
|
+
const nodeIdStr = getIdStr(node.id || node);
|
|
1741
|
+
document.getElementById('popoverTitle').innerText = nodeIdStr;
|
|
1742
|
+
document.getElementById('popoverType').innerText = 'Type: ' + (node.kind || node.type);
|
|
1743
|
+
document.getElementById('popoverStatus').innerText = 'Status: ' + (node.covered ? 'COVERED' : 'ACTIVE / UNCOVERED');
|
|
1744
|
+
|
|
1745
|
+
const degrees = edges.filter(e => getIdStr(e.from || e.source) === nodeIdStr || getIdStr(e.to || e.target) === nodeIdStr).length;
|
|
1746
|
+
document.getElementById('popoverStats').innerText = 'Connected Edges: ' + degrees + ' relationships';
|
|
1747
|
+
card.classList.add('active');
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function hidePopover() {
|
|
1751
|
+
document.getElementById('popoverCard').classList.remove('active');
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
loadDashboard();
|
|
1755
|
+
</script>
|
|
1756
|
+
</body>
|
|
1757
|
+
</html>`;
|
|
1758
|
+
}
|