@aisdlc/core 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +172 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +172 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -750,6 +750,12 @@ function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
|
750
750
|
"node_modules",
|
|
751
751
|
".git",
|
|
752
752
|
"dist",
|
|
753
|
+
"bin",
|
|
754
|
+
"obj",
|
|
755
|
+
"build",
|
|
756
|
+
"out",
|
|
757
|
+
"target",
|
|
758
|
+
"coverage",
|
|
753
759
|
".changeset",
|
|
754
760
|
"scratch",
|
|
755
761
|
"test-scaffold",
|
|
@@ -761,7 +767,9 @@ function walkMdFiles(dir, fileList = [], excludeDirs = [
|
|
|
761
767
|
".claude",
|
|
762
768
|
".cursor",
|
|
763
769
|
".github",
|
|
764
|
-
".vscode"
|
|
770
|
+
".vscode",
|
|
771
|
+
".vs",
|
|
772
|
+
".idea"
|
|
765
773
|
]) {
|
|
766
774
|
try {
|
|
767
775
|
if (!fs6__namespace.existsSync(dir)) return fileList;
|
|
@@ -3312,10 +3320,18 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
|
3312
3320
|
"node_modules",
|
|
3313
3321
|
".git",
|
|
3314
3322
|
"dist",
|
|
3323
|
+
"bin",
|
|
3324
|
+
"obj",
|
|
3325
|
+
"build",
|
|
3326
|
+
"out",
|
|
3327
|
+
"target",
|
|
3328
|
+
"coverage",
|
|
3315
3329
|
"scratch",
|
|
3316
3330
|
"fixtures",
|
|
3317
3331
|
"templates",
|
|
3318
|
-
"examples"
|
|
3332
|
+
"examples",
|
|
3333
|
+
".vs",
|
|
3334
|
+
".idea"
|
|
3319
3335
|
]);
|
|
3320
3336
|
function shouldSkipDir(name) {
|
|
3321
3337
|
return SKIPPED_DIR_NAMES.has(name) || name.startsWith("temp-");
|
|
@@ -3418,7 +3434,12 @@ function verifyTraceability(options = {}) {
|
|
|
3418
3434
|
}
|
|
3419
3435
|
}
|
|
3420
3436
|
if (relFile.includes("architecture/") || relFile.includes("06_runtime_view") || relFile.includes("08_security_concept") || relFile.includes("09_decisions")) {
|
|
3421
|
-
architectureViews.push({
|
|
3437
|
+
architectureViews.push({
|
|
3438
|
+
file: relFile,
|
|
3439
|
+
id: frontmatter.id ? String(frontmatter.id).trim() : void 0,
|
|
3440
|
+
type: typeof frontmatter.type === "string" ? frontmatter.type.trim() : void 0,
|
|
3441
|
+
content
|
|
3442
|
+
});
|
|
3422
3443
|
}
|
|
3423
3444
|
} catch {
|
|
3424
3445
|
}
|
|
@@ -3521,8 +3542,9 @@ function verifyTraceability(options = {}) {
|
|
|
3521
3542
|
}
|
|
3522
3543
|
for (const view of architectureViews) {
|
|
3523
3544
|
if (view.content.includes(reqId)) {
|
|
3524
|
-
const
|
|
3525
|
-
|
|
3545
|
+
const isEntityWithId = view.type === "component" || view.type === "adr" || view.type === "architecture-decision-record" || view.type === "security-enclave" || view.id !== void 0 && (view.id.startsWith("CMP-") || view.id.startsWith("ADR-") || view.id.startsWith("SEC-ENC-"));
|
|
3546
|
+
const viewIdentifier = isEntityWithId && view.id ? view.id : path8__namespace.basename(view.file);
|
|
3547
|
+
if (!archTraces.includes(viewIdentifier)) archTraces.push(viewIdentifier);
|
|
3526
3548
|
}
|
|
3527
3549
|
}
|
|
3528
3550
|
const uniqueArchTraces = Array.from(new Set(archTraces));
|
|
@@ -3859,7 +3881,7 @@ function scanAllTestArtifacts(rootDir) {
|
|
|
3859
3881
|
for (const e of entries) {
|
|
3860
3882
|
const fullPath = path8__namespace.join(dir, e.name);
|
|
3861
3883
|
if (e.isDirectory()) {
|
|
3862
|
-
if (!["node_modules", ".git", "dist", ".changeset", "scratch", "fixtures", "templates", "examples"].includes(e.name)) {
|
|
3884
|
+
if (!["node_modules", ".git", "dist", "bin", "obj", "build", "out", "target", "coverage", ".changeset", "scratch", "fixtures", "templates", "examples", ".vs", ".idea"].includes(e.name)) {
|
|
3863
3885
|
walk(fullPath);
|
|
3864
3886
|
}
|
|
3865
3887
|
} else {
|
|
@@ -7164,7 +7186,31 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7164
7186
|
for (const n of pdac.drifts) {
|
|
7165
7187
|
driftTargets.add(n.targetId);
|
|
7166
7188
|
}
|
|
7189
|
+
const NON_GRAPH_TYPES = /* @__PURE__ */ new Set([
|
|
7190
|
+
"term",
|
|
7191
|
+
"handoff",
|
|
7192
|
+
"agent-handoff",
|
|
7193
|
+
"tasks",
|
|
7194
|
+
"task-plan",
|
|
7195
|
+
"spec-change-proposal",
|
|
7196
|
+
"proposal",
|
|
7197
|
+
"spec-design",
|
|
7198
|
+
"spec-change-design",
|
|
7199
|
+
"design",
|
|
7200
|
+
"delivery-spec",
|
|
7201
|
+
"spec",
|
|
7202
|
+
"pre-merge-audit-report",
|
|
7203
|
+
"user-design-feedback",
|
|
7204
|
+
"release-kpis",
|
|
7205
|
+
"session-telemetry",
|
|
7206
|
+
"license-manifest",
|
|
7207
|
+
"mcp-config",
|
|
7208
|
+
"user-manual",
|
|
7209
|
+
"production-manual",
|
|
7210
|
+
"architecture-glossary"
|
|
7211
|
+
]);
|
|
7167
7212
|
const scannedArtifactsMap = /* @__PURE__ */ new Map();
|
|
7213
|
+
const archViewByBasename = /* @__PURE__ */ new Map();
|
|
7168
7214
|
const artifactDirs = ["product", "architecture", "specs", "security"];
|
|
7169
7215
|
for (const dirName of artifactDirs) {
|
|
7170
7216
|
const fullDirPath = path8__namespace.join(rootDir, dirName);
|
|
@@ -7174,6 +7220,7 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7174
7220
|
try {
|
|
7175
7221
|
const fileContent = fs6__namespace.readFileSync(f, "utf-8");
|
|
7176
7222
|
const relPath = path8__namespace.relative(rootDir, f).replace(/\\/g, "/");
|
|
7223
|
+
if (relPath.includes("/changes/") || relPath.startsWith("changes/")) continue;
|
|
7177
7224
|
const match = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
7178
7225
|
if (!match) continue;
|
|
7179
7226
|
let fm = {};
|
|
@@ -7185,6 +7232,9 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7185
7232
|
const id = typeof fm.id === "string" ? fm.id.trim() : void 0;
|
|
7186
7233
|
if (!id) continue;
|
|
7187
7234
|
let type = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
7235
|
+
if (NON_GRAPH_TYPES.has(type) || id.startsWith("TERM-") || id.startsWith("HOF-") || id.startsWith("AHOF-") || id.startsWith("CHG-") || id.startsWith("DSG-") || id.startsWith("SPEC-") || id.startsWith("TSK-") || id.startsWith("UDF-") || id.startsWith("AUDIT-")) {
|
|
7236
|
+
continue;
|
|
7237
|
+
}
|
|
7188
7238
|
let layer = "product";
|
|
7189
7239
|
if (id.startsWith("JRN-") || type === "journey") {
|
|
7190
7240
|
type = "journey";
|
|
@@ -7219,16 +7269,19 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7219
7269
|
} else if (id.startsWith("SEC-REQ-") || type === "security-requirement") {
|
|
7220
7270
|
type = "security-requirement";
|
|
7221
7271
|
layer = "requirement";
|
|
7222
|
-
} else if (id.startsWith("FR-") || id.startsWith("QR-") || id.startsWith("CON-") || type === "requirement") {
|
|
7272
|
+
} else if (id.startsWith("FR-") || id.startsWith("QR-") || id.startsWith("CON-") || type === "requirement" || type === "functional-requirement" || type === "quality-requirement") {
|
|
7223
7273
|
type = "requirement";
|
|
7224
7274
|
layer = "requirement";
|
|
7275
|
+
} else if (id.startsWith("ARCH-") || relPath.includes("architecture/")) {
|
|
7276
|
+
type = "architecture";
|
|
7277
|
+
layer = "architecture";
|
|
7225
7278
|
} else {
|
|
7226
7279
|
type = type || "product";
|
|
7227
7280
|
layer = "product";
|
|
7228
7281
|
}
|
|
7229
7282
|
const title = typeof fm.title === "string" ? fm.title.trim() : id;
|
|
7230
7283
|
const content = fileContent.replace(/^---\r?\n[\s\S]*?\r?\n---/, "").trim();
|
|
7231
|
-
|
|
7284
|
+
const scannedArt = {
|
|
7232
7285
|
id,
|
|
7233
7286
|
type,
|
|
7234
7287
|
title,
|
|
@@ -7236,7 +7289,12 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7236
7289
|
filePath: relPath,
|
|
7237
7290
|
content,
|
|
7238
7291
|
frontmatter: fm
|
|
7239
|
-
}
|
|
7292
|
+
};
|
|
7293
|
+
if (layer === "architecture" && type === "architecture") {
|
|
7294
|
+
archViewByBasename.set(path8__namespace.basename(relPath), scannedArt);
|
|
7295
|
+
} else {
|
|
7296
|
+
scannedArtifactsMap.set(id, scannedArt);
|
|
7297
|
+
}
|
|
7240
7298
|
} catch {
|
|
7241
7299
|
}
|
|
7242
7300
|
}
|
|
@@ -7245,7 +7303,7 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7245
7303
|
for (const r of trace.rows) {
|
|
7246
7304
|
const isOk = (r.productStatus === "COMPLIANT" || r.productStatus === "CONFORME") && (r.archStatus === "COMPLIANT" || r.archStatus === "CONFORME") && (r.testStatus === "COMPLIANT" || r.testStatus === "CONFORME");
|
|
7247
7305
|
const status = isOk ? "COMPLIANT" : "ORPHAN";
|
|
7248
|
-
const reqType = r.id.startsWith("SEC-REQ-") ? "security-requirement" :
|
|
7306
|
+
const reqType = r.id.startsWith("SEC-REQ-") ? "security-requirement" : "requirement";
|
|
7249
7307
|
nodeMap.set(r.id, {
|
|
7250
7308
|
id: r.id,
|
|
7251
7309
|
label: r.id,
|
|
@@ -7305,13 +7363,16 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7305
7363
|
});
|
|
7306
7364
|
if (!nodeMap.has(a)) {
|
|
7307
7365
|
const aType = a.startsWith("CMP-") ? "component" : a.startsWith("ADR-") ? "adr" : a.startsWith("SEC-ENC-") ? "security-enclave" : "architecture";
|
|
7366
|
+
const viewArt = archViewByBasename.get(a);
|
|
7308
7367
|
nodeMap.set(a, {
|
|
7309
7368
|
id: a,
|
|
7310
7369
|
label: a,
|
|
7311
|
-
title: a,
|
|
7370
|
+
title: viewArt?.title || a,
|
|
7312
7371
|
type: aType,
|
|
7313
7372
|
layer: "architecture",
|
|
7314
|
-
status: "COMPLIANT"
|
|
7373
|
+
status: "COMPLIANT",
|
|
7374
|
+
filePath: viewArt?.filePath,
|
|
7375
|
+
content: viewArt?.content
|
|
7315
7376
|
});
|
|
7316
7377
|
}
|
|
7317
7378
|
}
|
|
@@ -7443,7 +7504,31 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7443
7504
|
status: "COMPLIANT"
|
|
7444
7505
|
});
|
|
7445
7506
|
}
|
|
7446
|
-
const
|
|
7507
|
+
const supportingActors = Array.isArray(fm["supporting-actors"]) ? fm["supporting-actors"] : Array.isArray(fm["secondary-actors"]) ? fm["secondary-actors"] : [];
|
|
7508
|
+
for (const actorId of supportingActors) {
|
|
7509
|
+
if (typeof actorId === "string" && actorId.trim()) {
|
|
7510
|
+
const a = actorId.trim();
|
|
7511
|
+
if (!nodeMap.has(a)) {
|
|
7512
|
+
nodeMap.set(a, {
|
|
7513
|
+
id: a,
|
|
7514
|
+
label: a,
|
|
7515
|
+
title: a,
|
|
7516
|
+
type: "actor",
|
|
7517
|
+
layer: "product",
|
|
7518
|
+
status: "COMPLIANT"
|
|
7519
|
+
});
|
|
7520
|
+
}
|
|
7521
|
+
addEdge({
|
|
7522
|
+
id: `edge-${a}-${art.id}`,
|
|
7523
|
+
source: a,
|
|
7524
|
+
target: art.id,
|
|
7525
|
+
label: "initiates",
|
|
7526
|
+
relation: "initiates",
|
|
7527
|
+
status: "COMPLIANT"
|
|
7528
|
+
});
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
const governedBy = Array.isArray(fm["governed-by"]) ? fm["governed-by"] : Array.isArray(fm["business-rules"]) ? fm["business-rules"] : [];
|
|
7447
7532
|
for (const ruleId of governedBy) {
|
|
7448
7533
|
if (typeof ruleId === "string" && ruleId.trim()) {
|
|
7449
7534
|
const r = ruleId.trim();
|
|
@@ -7504,7 +7589,31 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7504
7589
|
status: "COMPLIANT"
|
|
7505
7590
|
});
|
|
7506
7591
|
}
|
|
7507
|
-
const
|
|
7592
|
+
const secondaryThreatActors = Array.isArray(fm["secondary-actors"]) ? fm["secondary-actors"] : Array.isArray(fm["supporting-actors"]) ? fm["supporting-actors"] : [];
|
|
7593
|
+
for (const secActor of secondaryThreatActors) {
|
|
7594
|
+
if (typeof secActor === "string" && secActor.trim()) {
|
|
7595
|
+
const sa = secActor.trim();
|
|
7596
|
+
if (!nodeMap.has(sa)) {
|
|
7597
|
+
nodeMap.set(sa, {
|
|
7598
|
+
id: sa,
|
|
7599
|
+
label: sa,
|
|
7600
|
+
title: sa,
|
|
7601
|
+
type: sa.startsWith("ACT-THREAT-") ? "threat-actor" : "actor",
|
|
7602
|
+
layer: "product",
|
|
7603
|
+
status: "COMPLIANT"
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
addEdge({
|
|
7607
|
+
id: `edge-${sa}-${art.id}`,
|
|
7608
|
+
source: sa,
|
|
7609
|
+
target: art.id,
|
|
7610
|
+
label: "threatens",
|
|
7611
|
+
relation: "threatens",
|
|
7612
|
+
status: "COMPLIANT"
|
|
7613
|
+
});
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
7616
|
+
const rawTargets = Array.isArray(fm["targets-use-case"]) ? fm["targets-use-case"] : typeof fm["targets-use-case"] === "string" ? [fm["targets-use-case"]] : Array.isArray(fm["targets-use-cases"]) ? fm["targets-use-cases"] : Array.isArray(fm["targets"]) ? fm["targets"] : [];
|
|
7508
7617
|
for (const tgt of rawTargets) {
|
|
7509
7618
|
if (typeof tgt === "string" && tgt.trim()) {
|
|
7510
7619
|
const t = tgt.trim();
|
|
@@ -7569,6 +7678,40 @@ function buildGraphElements(rootDir = process.cwd()) {
|
|
|
7569
7678
|
});
|
|
7570
7679
|
}
|
|
7571
7680
|
}
|
|
7681
|
+
} else if (art.type === "adr") {
|
|
7682
|
+
const affectedComponents = Array.isArray(fm["affects-components"]) ? fm["affects-components"] : typeof fm["affects-components"] === "string" ? [fm["affects-components"]] : [];
|
|
7683
|
+
for (const cmpId of affectedComponents) {
|
|
7684
|
+
if (typeof cmpId === "string" && cmpId.trim()) {
|
|
7685
|
+
const c = cmpId.trim();
|
|
7686
|
+
if (nodeMap.has(c)) {
|
|
7687
|
+
addEdge({
|
|
7688
|
+
id: `edge-${art.id}-${c}`,
|
|
7689
|
+
source: art.id,
|
|
7690
|
+
target: c,
|
|
7691
|
+
label: "governs",
|
|
7692
|
+
relation: "governs",
|
|
7693
|
+
status: "COMPLIANT"
|
|
7694
|
+
});
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
} else if (art.type === "security-enclave") {
|
|
7699
|
+
const enforcedBy = Array.isArray(fm["enforced-by"]) ? fm["enforced-by"] : typeof fm["enforced-by"] === "string" ? [fm["enforced-by"]] : [];
|
|
7700
|
+
for (const secReq of enforcedBy) {
|
|
7701
|
+
if (typeof secReq === "string" && secReq.trim()) {
|
|
7702
|
+
const s = secReq.trim();
|
|
7703
|
+
if (nodeMap.has(s)) {
|
|
7704
|
+
addEdge({
|
|
7705
|
+
id: `edge-${s}-${art.id}`,
|
|
7706
|
+
source: s,
|
|
7707
|
+
target: art.id,
|
|
7708
|
+
label: "satisfies",
|
|
7709
|
+
relation: "satisfies",
|
|
7710
|
+
status: "COMPLIANT"
|
|
7711
|
+
});
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7572
7715
|
}
|
|
7573
7716
|
}
|
|
7574
7717
|
for (const [id, node] of nodeMap.entries()) {
|
|
@@ -7718,6 +7861,9 @@ function collectDashboardMetrics(rootDir) {
|
|
|
7718
7861
|
function escapeHtml(str) {
|
|
7719
7862
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7720
7863
|
}
|
|
7864
|
+
function safeJsonForScript(data) {
|
|
7865
|
+
return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
7866
|
+
}
|
|
7721
7867
|
function renderAutonomyModeSection(mode, statusIcon, count, roleDescription, allTasks = []) {
|
|
7722
7868
|
const modeTasks = (allTasks || []).filter(
|
|
7723
7869
|
(t) => (t.task?.autonomyMode || "").toUpperCase() === mode
|
|
@@ -7829,14 +7975,14 @@ function renderAutonomyModeSection(mode, statusIcon, count, roleDescription, all
|
|
|
7829
7975
|
}
|
|
7830
7976
|
function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard Interactivo de Trazabilidad y Calidad") {
|
|
7831
7977
|
const cytoscapeScript = getCytoscapeScript();
|
|
7832
|
-
const elementsJson =
|
|
7978
|
+
const elementsJson = safeJsonForScript(graphData.elements);
|
|
7833
7979
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7834
7980
|
return `<!DOCTYPE html>
|
|
7835
7981
|
<html lang="es">
|
|
7836
7982
|
<head>
|
|
7837
7983
|
<meta charset="UTF-8">
|
|
7838
7984
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7839
|
-
<title>${title}</title>
|
|
7985
|
+
<title>${escapeHtml(title)}</title>
|
|
7840
7986
|
<style>
|
|
7841
7987
|
:root {
|
|
7842
7988
|
--bg-dark: #0f172a;
|
|
@@ -8211,12 +8357,12 @@ function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard
|
|
|
8211
8357
|
${metricsData.trace.rows.map((r) => {
|
|
8212
8358
|
const isRowOk = (r.productStatus === "COMPLIANT" || r.productStatus === "CONFORME") && (r.archStatus === "COMPLIANT" || r.archStatus === "CONFORME") && (r.testStatus === "COMPLIANT" || r.testStatus === "CONFORME");
|
|
8213
8359
|
return `
|
|
8214
|
-
<tr onclick="focusNode('${r.id}')" onmouseenter="startRowTooltip(event, '${r.id}')" onmouseleave="cancelRowTooltip()" onmousemove="updateRowTooltipPos(event)" style="cursor: pointer;">
|
|
8215
|
-
<td><strong><code>${r.id}</code></strong></td>
|
|
8216
|
-
<td>${r.title}</td>
|
|
8217
|
-
<td><code>${r.productTraces}</code></td>
|
|
8218
|
-
<td><code>${r.archTraces}</code></td>
|
|
8219
|
-
<td><code>${r.testTraces}</code></td>
|
|
8360
|
+
<tr onclick="focusNode('${escapeHtml(String(r.id || ""))}')" onmouseenter="startRowTooltip(event, '${escapeHtml(String(r.id || ""))}')" onmouseleave="cancelRowTooltip()" onmousemove="updateRowTooltipPos(event)" style="cursor: pointer;">
|
|
8361
|
+
<td><strong><code>${escapeHtml(String(r.id || ""))}</code></strong></td>
|
|
8362
|
+
<td>${escapeHtml(String(r.title || ""))}</td>
|
|
8363
|
+
<td><code>${escapeHtml(String(r.productTraces || ""))}</code></td>
|
|
8364
|
+
<td><code>${escapeHtml(String(r.archTraces || ""))}</code></td>
|
|
8365
|
+
<td><code>${escapeHtml(String(r.testTraces || ""))}</code></td>
|
|
8220
8366
|
<td><span class="badge ${isRowOk ? "badge-green" : "badge-amber"}">${isRowOk ? "COMPLIANT" : "ORPHAN"}</span></td>
|
|
8221
8367
|
</tr>
|
|
8222
8368
|
`;
|
|
@@ -8429,7 +8575,7 @@ function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard
|
|
|
8429
8575
|
</script>
|
|
8430
8576
|
<script>
|
|
8431
8577
|
var elements = ${elementsJson};
|
|
8432
|
-
var govTasks = ${
|
|
8578
|
+
var govTasks = ${safeJsonForScript(metricsData.gov.tasks || [])};
|
|
8433
8579
|
var cy = null;
|
|
8434
8580
|
|
|
8435
8581
|
var reqHoverTimer = null;
|
|
@@ -9062,9 +9208,10 @@ function renderDashboardHtml(graphData, metricsData, title = "AI-SDLC: Dashboard
|
|
|
9062
9208
|
|
|
9063
9209
|
function switchTab(name) {
|
|
9064
9210
|
hideTooltip();
|
|
9065
|
-
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
|
9211
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
|
9212
|
+
b.classList.toggle('active', b.getAttribute('onclick') === "switchTab('" + name + "')");
|
|
9213
|
+
});
|
|
9066
9214
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
9067
|
-
event.target?.classList?.add('active');
|
|
9068
9215
|
var p = document.getElementById('tab-' + name);
|
|
9069
9216
|
if (p) p.classList.add('active');
|
|
9070
9217
|
if (name === 'graph' && cy) {
|