@plumpslabs/kuma 2.3.20 → 2.3.22
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/README.md +18 -0
- package/dist/index.js +10221 -332
- package/package.json +2 -2
- package/packages/ide/studio/dist/index.js +37 -5
- package/packages/ide/studio/public/index.html +239 -68
- package/dist/agentDetector-YOWQVJFR.js +0 -186
- package/dist/chunk-3BRBJZ7P.js +0 -1055
- package/dist/chunk-3OHYYXYN.js +0 -71
- package/dist/chunk-ABKE45T4.js +0 -1264
- package/dist/chunk-E2KFPEBT.js +0 -183
- package/dist/chunk-FKRSI5U5.js +0 -282
- package/dist/chunk-GFLSAXAH.js +0 -155
- package/dist/chunk-L7F67KUP.js +0 -172
- package/dist/chunk-LVKOGXLC.js +0 -658
- package/dist/chunk-PRUTTZBS.js +0 -1113
- package/dist/contextDigest-QB5XHPXE.js +0 -177
- package/dist/domainRules-QLPAQASB.js +0 -20
- package/dist/init-PL4XL662.js +0 -15
- package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
- package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
- package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
- package/dist/kumaContractEngine-KX27T4N7.js +0 -305
- package/dist/kumaDb-4XZ5S2LH.js +0 -65
- package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
- package/dist/kumaGotchas-XRGFFBTA.js +0 -151
- package/dist/kumaGraph-UMXZNGYF.js +0 -44
- package/dist/kumaMemory-FBJMV77G.js +0 -16
- package/dist/kumaMiner-XJETL7TL.js +0 -176
- package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
- package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
- package/dist/kumaSearch-PV4QTKE7.js +0 -321
- package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
- package/dist/kumaVerifier-6YEGC77M.js +0 -265
- package/dist/kumaVisualize-264OEBGJ.js +0 -264
- package/dist/pathValidator-V4DC6U6Z.js +0 -22
- package/dist/safetyAudit-O45SPNTS.js +0 -12
- package/dist/safetyScore-TMMRD2MV.js +0 -333
- package/dist/sessionMemory-YPKVIOMV.js +0 -6
- package/dist/skillGenerator-PWEJKZNX.js +0 -304
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.22",
|
|
4
4
|
"description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"homepage": "https://github.com/plumpslabs/kuma#readme",
|
|
64
64
|
"license": "MIT",
|
|
65
65
|
"scripts": {
|
|
66
|
-
"build": "tsup src/index.ts --format esm --clean --out-dir dist",
|
|
66
|
+
"build": "tsup src/index.ts --format esm --no-splitting --clean --out-dir dist",
|
|
67
67
|
"build:studio": "cd packages/ide/studio && tsup src/index.ts --format esm --clean --out-dir dist",
|
|
68
68
|
"build:all": "pnpm run build && pnpm run build:studio",
|
|
69
69
|
"dev": "tsup src/index.ts --format esm --watch --out-dir dist",
|
|
@@ -38,7 +38,17 @@ function query(sql) {
|
|
|
38
38
|
function queryJson(sql) {
|
|
39
39
|
const raw = query(sql);
|
|
40
40
|
if (!raw) return [];
|
|
41
|
-
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
try {
|
|
45
|
+
const sanitized = raw.replace(/[\r\n]+/g, " ");
|
|
46
|
+
return JSON.parse(sanitized);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn("[Studio DB] queryJson parse failed:", e);
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
42
52
|
}
|
|
43
53
|
function getDashboardData() {
|
|
44
54
|
return {
|
|
@@ -53,31 +63,38 @@ function getDashboardData() {
|
|
|
53
63
|
)
|
|
54
64
|
`),
|
|
55
65
|
nodes: queryJson(
|
|
56
|
-
`SELECT json_group_array(json_object('id',id,'name',name,'type',type,'file_path',file_path)) FROM nodes ORDER BY
|
|
66
|
+
`SELECT json_group_array(json_object('id',id,'name',name,'type',type,'file_path',file_path)) FROM nodes ORDER BY updated_at DESC`
|
|
57
67
|
),
|
|
58
68
|
edges: queryJson(
|
|
59
69
|
`SELECT json_group_array(json_object('source',source_id,'target',target_id,'relation',type)) FROM edges`
|
|
60
70
|
),
|
|
61
71
|
gotchas: queryJson(
|
|
62
|
-
`SELECT json_group_array(json_object('id',id,'file_path',file_path,'description',description,'severity',severity,'workaround',workaround)) FROM known_gotchas ORDER BY severity DESC`
|
|
72
|
+
`SELECT json_group_array(json_object('id',id,'file_path',file_path,'description',REPLACE(REPLACE(description,char(10),' '),char(13),''),'severity',severity,'workaround',REPLACE(REPLACE(COALESCE(workaround,''),char(10),' '),char(13),''))) FROM known_gotchas ORDER BY severity DESC`
|
|
63
73
|
),
|
|
64
74
|
trajectories: queryJson(
|
|
65
|
-
`SELECT json_group_array(json_object('id',id,'goal',goal,'total_duration_ms',total_duration_ms,'success_rate',success_rate,'created_at',created_at)) FROM trajectories ORDER BY created_at DESC LIMIT 20`
|
|
75
|
+
`SELECT json_group_array(json_object('id',id,'goal',REPLACE(REPLACE(goal,char(10),' '),char(13),''),'total_duration_ms',total_duration_ms,'success_rate',success_rate,'created_at',created_at)) FROM trajectories ORDER BY created_at DESC LIMIT 20`
|
|
66
76
|
),
|
|
67
77
|
health: queryJson(
|
|
68
|
-
`SELECT json_group_array(json_object('score',score,'summary',summary,'risk_level',risk_level,'created_at',created_at)) FROM health_snapshots ORDER BY created_at DESC LIMIT 10`
|
|
78
|
+
`SELECT json_group_array(json_object('score',score,'summary',REPLACE(REPLACE(COALESCE(summary,''),char(10),' '),char(13),''),'risk_level',risk_level,'created_at',created_at)) FROM health_snapshots ORDER BY created_at DESC LIMIT 10`
|
|
69
79
|
)
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// src/api.ts
|
|
84
|
+
import fs from "fs";
|
|
74
85
|
var api = new Hono();
|
|
75
86
|
api.use("/*", cors());
|
|
76
87
|
api.get("/status", (c) => {
|
|
77
88
|
const dbPath = findKumaDb();
|
|
89
|
+
let dbSize = "0 KB";
|
|
90
|
+
if (dbPath && fs.existsSync(dbPath)) {
|
|
91
|
+
const bytes = fs.statSync(dbPath).size;
|
|
92
|
+
dbSize = bytes > 1024 * 1024 ? (bytes / (1024 * 1024)).toFixed(2) + " MB" : (bytes / 1024).toFixed(1) + " KB";
|
|
93
|
+
}
|
|
78
94
|
return c.json({
|
|
79
95
|
ok: !!dbPath,
|
|
80
96
|
dbPath,
|
|
97
|
+
dbSize,
|
|
81
98
|
project: dbPath ? dbPath.replace("/.kuma/kuma.db", "").split("/").pop() : null
|
|
82
99
|
});
|
|
83
100
|
});
|
|
@@ -89,6 +106,21 @@ api.get("/dashboard", (c) => {
|
|
|
89
106
|
return c.json({ error: e.message }, 500);
|
|
90
107
|
}
|
|
91
108
|
});
|
|
109
|
+
api.post("/reset-db", (c) => {
|
|
110
|
+
try {
|
|
111
|
+
const dbPath = findKumaDb();
|
|
112
|
+
if (dbPath && fs.existsSync(dbPath)) {
|
|
113
|
+
fs.unlinkSync(dbPath);
|
|
114
|
+
}
|
|
115
|
+
return c.json({ ok: true, message: "Kuma DB reset successfully" });
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return c.json({ error: e.message }, 500);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
api.post("/stop-server", (c) => {
|
|
121
|
+
setTimeout(() => process.exit(0), 500);
|
|
122
|
+
return c.json({ ok: true, message: "Kuma Studio server shutting down" });
|
|
123
|
+
});
|
|
92
124
|
var api_default = api;
|
|
93
125
|
|
|
94
126
|
// src/index.ts
|
|
@@ -71,19 +71,20 @@ header .status .dot.off { background: var(--red); }
|
|
|
71
71
|
.health-item .meta { font-size: 11px; color: var(--muted); }
|
|
72
72
|
.empty { text-align: center; color: var(--muted); padding: 40px 20px; font-size: 14px; }
|
|
73
73
|
#legend {
|
|
74
|
-
position: absolute; bottom: 16px; left: 16px; background: rgba(22,33,62,0.
|
|
74
|
+
position: absolute; bottom: 16px; left: 16px; background: rgba(22,33,62,0.95);
|
|
75
75
|
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
76
76
|
font-size: 11px; z-index: 10; max-height: 200px; overflow-y: auto;
|
|
77
|
-
backdrop-filter: blur(8px);
|
|
77
|
+
backdrop-filter: blur(8px); color: #f8fafc;
|
|
78
78
|
}
|
|
79
|
-
#legend .title { font-weight: 600; margin-bottom: 6px; color:
|
|
80
|
-
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
|
|
79
|
+
#legend .title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; }
|
|
80
|
+
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; color: #f1f5f9; }
|
|
81
81
|
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
82
82
|
#graph-tooltip {
|
|
83
|
-
position: absolute; display: none; background: rgba(
|
|
83
|
+
position: absolute; display: none; background: rgba(15, 23, 42, 0.96);
|
|
84
84
|
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
85
|
-
font-size: 12px; z-index: 20; max-width:
|
|
86
|
-
|
|
85
|
+
font-size: 12px; z-index: 20; max-width: 420px; word-break: break-all;
|
|
86
|
+
overflow-wrap: anywhere; white-space: normal; color: #f8fafc;
|
|
87
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6); backdrop-filter: blur(12px);
|
|
87
88
|
pointer-events: none;
|
|
88
89
|
}
|
|
89
90
|
#search-box {
|
|
@@ -108,6 +109,12 @@ header .status .dot.off { background: var(--red); }
|
|
|
108
109
|
transition: background 0.2s;
|
|
109
110
|
}
|
|
110
111
|
#graph-controls button:hover { background: var(--accent); border-color: var(--accent); }
|
|
112
|
+
#graph-controls button.active {
|
|
113
|
+
background: var(--accent) !important;
|
|
114
|
+
border-color: var(--accent) !important;
|
|
115
|
+
color: #ffffff !important;
|
|
116
|
+
box-shadow: 0 0 10px rgba(124, 58, 237, 0.4);
|
|
117
|
+
}
|
|
111
118
|
#loading {
|
|
112
119
|
position: fixed; inset: 0; background: var(--bg); display: flex;
|
|
113
120
|
align-items: center; justify-content: center; z-index: 100;
|
|
@@ -125,7 +132,11 @@ header .status .dot.off { background: var(--red); }
|
|
|
125
132
|
<header>
|
|
126
133
|
<h1>♢ Kuma Studio</h1>
|
|
127
134
|
<div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
|
|
128
|
-
<
|
|
135
|
+
<div style="margin-left:auto;display:flex;gap:8px;-webkit-app-region:no-drag">
|
|
136
|
+
<button onclick="refetch()" id="refetch-btn" style="background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s">↻ Refetch</button>
|
|
137
|
+
<button onclick="resetKumaDb()" style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);border-radius:6px;color:var(--red);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s">🗑 Reset DB</button>
|
|
138
|
+
<button onclick="stopKumaStudio()" style="background:rgba(148,163,184,0.15);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#94a3b8;padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s">■ Stop Server</button>
|
|
139
|
+
</div>
|
|
129
140
|
</header>
|
|
130
141
|
<div class="stats-bar" id="stats-bar"></div>
|
|
131
142
|
<div id="main">
|
|
@@ -133,11 +144,11 @@ header .status .dot.off { background: var(--red); }
|
|
|
133
144
|
<div id="graph-container"></div>
|
|
134
145
|
<div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
|
|
135
146
|
<div id="graph-controls">
|
|
136
|
-
<button onclick="resetGraphFocus()">Reset Focus</button>
|
|
137
|
-
<button onclick="if(network)network.fit()">Fit</button>
|
|
138
|
-
<button onclick="exportGraphPNG()">Export PNG</button>
|
|
139
|
-
<button onclick="
|
|
140
|
-
<button onclick="
|
|
147
|
+
<button onclick="resetGraphFocus()" title="Reset node opacity and search">Reset Focus</button>
|
|
148
|
+
<button onclick="if(network)network.fit()" title="Fit graph to viewport">Fit</button>
|
|
149
|
+
<button onclick="exportGraphPNG()" title="Export graph as PNG image">Export PNG</button>
|
|
150
|
+
<button id="btn-unfreeze-physics" class="active" onclick="restartPhysics()" title="Enable physics simulation (nodes will move freely)">Unfreeze Physics</button>
|
|
151
|
+
<button id="btn-freeze-physics" onclick="stopPhysics()" title="Freeze physics simulation (lock nodes in place)">Freeze Physics</button>
|
|
141
152
|
</div>
|
|
142
153
|
<div id="graph-tooltip"></div>
|
|
143
154
|
<div id="legend"></div>
|
|
@@ -154,12 +165,43 @@ header .status .dot.off { background: var(--red); }
|
|
|
154
165
|
|
|
155
166
|
<script>
|
|
156
167
|
const API = "/api";
|
|
157
|
-
let network = null, nodesData = null, allNodes = null, allEdges = null;
|
|
168
|
+
let network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
|
|
169
|
+
|
|
170
|
+
async function resetKumaDb() {
|
|
171
|
+
var confirmation = prompt("⚠️ CRITICAL ACTION: TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all recorded knowledge nodes, edges, gotchas, and trajectories in .kuma/kuma.db:");
|
|
172
|
+
if (confirmation !== "DELETE") {
|
|
173
|
+
if (confirmation !== null) alert("Reset cancelled. Confirmation text did not match 'DELETE'.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
var res = await fetch(API + "/reset-db", { method: "POST" }).then(function(r){return r.json()});
|
|
178
|
+
if (res.ok) {
|
|
179
|
+
alert("Kuma DB reset successfully.");
|
|
180
|
+
refetch();
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
alert("Reset failed: " + e.message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function stopKumaStudio() {
|
|
188
|
+
if (!confirm("Stop Kuma Studio background server? You can restart it anytime with 'agy kuma studio'.")) return;
|
|
189
|
+
try {
|
|
190
|
+
await fetch(API + "/stop-server", { method: "POST" });
|
|
191
|
+
document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;color:#94a3b8;font-size:16px;">Kuma Studio Server Stopped. You can close this tab.</div>';
|
|
192
|
+
} catch (e) {
|
|
193
|
+
alert("Stop failed: " + e.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
158
196
|
|
|
159
197
|
function resetGraphFocus() {
|
|
160
198
|
if (!network) return;
|
|
161
199
|
network.body.data.nodes.forEach(function(n) {
|
|
162
|
-
network.body.data.nodes.update({
|
|
200
|
+
network.body.data.nodes.update({
|
|
201
|
+
id: n.id,
|
|
202
|
+
opacity: 1.0,
|
|
203
|
+
font: { color: '#ffffff', bold: true }
|
|
204
|
+
});
|
|
163
205
|
});
|
|
164
206
|
document.getElementById("graph-tooltip").style.display = 'none';
|
|
165
207
|
var searchInput = document.getElementById("node-search");
|
|
@@ -176,6 +218,22 @@ function exportGraphPNG() {
|
|
|
176
218
|
link.click();
|
|
177
219
|
}
|
|
178
220
|
|
|
221
|
+
function restartPhysics() {
|
|
222
|
+
if (!network) return;
|
|
223
|
+
network.setOptions({ physics: { enabled: true } });
|
|
224
|
+
network.startSimulation();
|
|
225
|
+
document.getElementById('btn-unfreeze-physics').classList.add('active');
|
|
226
|
+
document.getElementById('btn-freeze-physics').classList.remove('active');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stopPhysics() {
|
|
230
|
+
if (!network) return;
|
|
231
|
+
network.stopSimulation();
|
|
232
|
+
network.setOptions({ physics: { enabled: false } });
|
|
233
|
+
document.getElementById('btn-freeze-physics').classList.add('active');
|
|
234
|
+
document.getElementById('btn-unfreeze-physics').classList.remove('active');
|
|
235
|
+
}
|
|
236
|
+
|
|
179
237
|
async function init() {
|
|
180
238
|
var status = await fetch(API+"/status").then(function(r){return r.json()});
|
|
181
239
|
var dot = document.getElementById("status-dot");
|
|
@@ -183,6 +241,7 @@ async function init() {
|
|
|
183
241
|
if (status.ok) {
|
|
184
242
|
dot.className = "dot on";
|
|
185
243
|
txt.textContent = status.project || "connected";
|
|
244
|
+
currentDbSize = status.dbSize || "0 KB";
|
|
186
245
|
} else {
|
|
187
246
|
dot.className = "dot off";
|
|
188
247
|
txt.textContent = "no kuma.db found";
|
|
@@ -213,6 +272,7 @@ function renderStats(data) {
|
|
|
213
272
|
'<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
|
|
214
273
|
'<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
|
|
215
274
|
'<div class="stat"><span class="num">'+(s.trajectory_count||0)+'</span><span class="lbl">trajectories</span></div>'+
|
|
275
|
+
'<div class="stat"><span class="num" style="color:var(--accent)">'+currentDbSize+'</span><span class="lbl">kuma.db</span></div>'+
|
|
216
276
|
'<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
|
|
217
277
|
}
|
|
218
278
|
|
|
@@ -225,19 +285,21 @@ function truncateLabel(name, maxLen) {
|
|
|
225
285
|
|
|
226
286
|
/** Calculate node size based on label length and type */
|
|
227
287
|
function calcNodeSize(name, type) {
|
|
228
|
-
var
|
|
229
|
-
if (
|
|
230
|
-
if (type === "
|
|
231
|
-
|
|
288
|
+
var isGotcha = name && name.toLowerCase().indexOf("gotcha:") === 0;
|
|
289
|
+
if (isGotcha) return 14;
|
|
290
|
+
if (type === "variable") return 14;
|
|
291
|
+
var base = 18;
|
|
292
|
+
if (type === "feature_domain") base = 30;
|
|
293
|
+
if (type === "workflow") base = 24;
|
|
294
|
+
if (type === "cross_service_link") base = 20;
|
|
232
295
|
if (type === "function") base = 16;
|
|
233
|
-
if (type === "class") base =
|
|
234
|
-
if (type === "module") base =
|
|
235
|
-
if (type === "component") base =
|
|
236
|
-
if (type === "route") base =
|
|
237
|
-
if (type === "test") base =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return Math.min(32, Math.max(base, base + len * 0.4));
|
|
296
|
+
if (type === "class") base = 20;
|
|
297
|
+
if (type === "module") base = 22;
|
|
298
|
+
if (type === "component") base = 20;
|
|
299
|
+
if (type === "route") base = 16;
|
|
300
|
+
if (type === "test") base = 18;
|
|
301
|
+
var len = name ? Math.min(name.length, 25) : 4;
|
|
302
|
+
return Math.min(34, Math.max(base, base + len * 0.4));
|
|
241
303
|
}
|
|
242
304
|
|
|
243
305
|
|
|
@@ -250,12 +312,32 @@ function renderGraph(data) {
|
|
|
250
312
|
}
|
|
251
313
|
|
|
252
314
|
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
253
|
-
var colors = {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
function:'
|
|
258
|
-
|
|
315
|
+
var colors = {
|
|
316
|
+
feature_domain: '#dc2626',
|
|
317
|
+
workflow: '#2563eb',
|
|
318
|
+
cross_service_link: '#059669',
|
|
319
|
+
function: '#7c3aed',
|
|
320
|
+
class: '#0891b2',
|
|
321
|
+
module: '#16a34a',
|
|
322
|
+
component: '#ea580c',
|
|
323
|
+
file: '#d97706',
|
|
324
|
+
route: '#db2777',
|
|
325
|
+
test: '#0d9488',
|
|
326
|
+
variable: '#6366f1'
|
|
327
|
+
};
|
|
328
|
+
var shapes = {
|
|
329
|
+
feature_domain: 'star',
|
|
330
|
+
workflow: 'ellipse',
|
|
331
|
+
cross_service_link: 'box',
|
|
332
|
+
function: 'box',
|
|
333
|
+
class: 'hexagon',
|
|
334
|
+
module: 'ellipse',
|
|
335
|
+
component: 'diamond',
|
|
336
|
+
file: 'box',
|
|
337
|
+
route: 'triangleDown',
|
|
338
|
+
test: 'star',
|
|
339
|
+
variable: 'box'
|
|
340
|
+
};
|
|
259
341
|
|
|
260
342
|
// Limit nodes for performance if > 200
|
|
261
343
|
var maxNodes = 300;
|
|
@@ -266,16 +348,27 @@ function renderGraph(data) {
|
|
|
266
348
|
}
|
|
267
349
|
|
|
268
350
|
var visNodes = new vis.DataSet(displayNodes.map(function(n){
|
|
269
|
-
var
|
|
270
|
-
var
|
|
271
|
-
var
|
|
351
|
+
var isGotcha = n.name && n.name.toLowerCase().indexOf("gotcha:") === 0;
|
|
352
|
+
var isVariable = n.type === 'variable' && !isGotcha;
|
|
353
|
+
var shape = isGotcha ? 'triangle' : (shapes[n.type] || 'dot');
|
|
354
|
+
var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : (isVariable ? 16 : 32));
|
|
355
|
+
var label = truncateLabel(n.name, maxLen);
|
|
356
|
+
var size = isGotcha ? 12 : (isVariable ? 10 : calcNodeSize(n.name, n.type));
|
|
357
|
+
var color = isGotcha ? '#f97316' : (colors[n.type] || '#64748b');
|
|
272
358
|
return {
|
|
273
359
|
id: n.id,
|
|
274
360
|
label: label,
|
|
275
|
-
|
|
276
|
-
color: {
|
|
277
|
-
|
|
278
|
-
|
|
361
|
+
|
|
362
|
+
color: {
|
|
363
|
+
background: color,
|
|
364
|
+
border: color,
|
|
365
|
+
highlight: { background: color, border: '#38bdf8' },
|
|
366
|
+
hover: { background: color, border: '#a855f7' }
|
|
367
|
+
},
|
|
368
|
+
shape: shape,
|
|
369
|
+
font: isVariable
|
|
370
|
+
? { color: '#ffffff', size: 10, face: 'sans-serif', multi: false, bold: true }
|
|
371
|
+
: { color: '#ffffff', size: Math.min(13, Math.max(10, size * 0.55)), face: 'sans-serif', multi: false, bold: true },
|
|
279
372
|
borderWidth: 0,
|
|
280
373
|
size: size,
|
|
281
374
|
mass: 1 + (size / 40),
|
|
@@ -283,20 +376,53 @@ function renderGraph(data) {
|
|
|
283
376
|
}));
|
|
284
377
|
|
|
285
378
|
var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
|
|
286
|
-
var
|
|
379
|
+
var edgeList = allEdges.slice();
|
|
380
|
+
|
|
381
|
+
// Auto-connect floating file nodes to matching cross_service_link flow nodes
|
|
382
|
+
var nameToLinkIds = {};
|
|
383
|
+
displayNodes.forEach(function(n) {
|
|
384
|
+
if (n.type === 'cross_service_link') {
|
|
385
|
+
var cleanName = n.name.toLowerCase().trim();
|
|
386
|
+
if (!nameToLinkIds[cleanName]) nameToLinkIds[cleanName] = [];
|
|
387
|
+
nameToLinkIds[cleanName].push(n.id);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
displayNodes.forEach(function(n) {
|
|
392
|
+
if (n.type === 'file' || n.type === 'component' || n.type === 'function') {
|
|
393
|
+
var fn = n.name.toLowerCase().trim();
|
|
394
|
+
Object.keys(nameToLinkIds).forEach(function(linkName) {
|
|
395
|
+
if (fn.includes(linkName) || linkName.includes(fn)) {
|
|
396
|
+
var targetLinkIds = nameToLinkIds[linkName];
|
|
397
|
+
targetLinkIds.forEach(function(linkId) {
|
|
398
|
+
edgeList.push({
|
|
399
|
+
source: n.id,
|
|
400
|
+
target: linkId,
|
|
401
|
+
relation: 'contains'
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
var displayEdges = edgeList.filter(function(e){
|
|
287
410
|
return nodeIds.has(e.source) && nodeIds.has(e.target);
|
|
288
411
|
}).slice(0, 500);
|
|
289
412
|
|
|
290
|
-
var
|
|
413
|
+
var edgeDefaults = {};
|
|
414
|
+
var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
|
|
291
415
|
var edgeColor = '#64748b';
|
|
292
416
|
var width = 1.5;
|
|
293
417
|
var dashes = false;
|
|
294
418
|
if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
|
|
295
419
|
if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
|
|
296
420
|
if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
|
|
297
|
-
if (e.relation === 'contains') { edgeColor = '#
|
|
421
|
+
if (e.relation === 'contains') { edgeColor = '#64748b'; width = 1.5; dashes = true; }
|
|
298
422
|
|
|
423
|
+
edgeDefaults['e'+idx] = { color: edgeColor, width: width };
|
|
299
424
|
return {
|
|
425
|
+
id: 'e'+idx,
|
|
300
426
|
from: e.source, to: e.target,
|
|
301
427
|
label: e.relation || '',
|
|
302
428
|
arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
|
|
@@ -339,9 +465,10 @@ function renderGraph(data) {
|
|
|
339
465
|
},
|
|
340
466
|
nodes: {
|
|
341
467
|
borderWidth: 0,
|
|
342
|
-
font: { color: '#
|
|
343
|
-
margin: { top:
|
|
344
|
-
|
|
468
|
+
font: { color: '#ffffff', size: 13, face: 'sans-serif', bold: 'true' },
|
|
469
|
+
margin: { top: 10, bottom: 10, left: 16, right: 16 },
|
|
470
|
+
shapeProperties: { borderRadius: 8 },
|
|
471
|
+
shadow: { enabled: true, color: 'rgba(0,0,0,0.4)', size: 8, x: 0, y: 3 },
|
|
345
472
|
},
|
|
346
473
|
edges: {
|
|
347
474
|
smooth: { type: 'continuous', roundness: 0.3 },
|
|
@@ -351,46 +478,72 @@ function renderGraph(data) {
|
|
|
351
478
|
configure: { enabled: false },
|
|
352
479
|
});
|
|
353
480
|
|
|
354
|
-
// Click node →
|
|
481
|
+
// Click node → highlight connected neighborhood (tooltip via hover only)
|
|
355
482
|
network.on('click', function(params) {
|
|
356
|
-
var tip = document.getElementById("graph-tooltip");
|
|
357
483
|
if (params.nodes.length) {
|
|
358
484
|
var selectedId = params.nodes[0];
|
|
359
485
|
var connectedNodeIds = new Set(network.getConnectedNodes(selectedId));
|
|
360
486
|
connectedNodeIds.add(selectedId);
|
|
361
487
|
|
|
362
488
|
network.body.data.nodes.forEach(function(n) {
|
|
489
|
+
var isConnected = connectedNodeIds.has(n.id);
|
|
363
490
|
network.body.data.nodes.update({
|
|
364
491
|
id: n.id,
|
|
365
|
-
opacity:
|
|
492
|
+
opacity: isConnected ? 1.0 : 0.12,
|
|
493
|
+
font: {
|
|
494
|
+
color: isConnected ? '#ffffff' : 'rgba(255,255,255,0.07)',
|
|
495
|
+
bold: isConnected
|
|
496
|
+
}
|
|
366
497
|
});
|
|
367
498
|
});
|
|
368
499
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
500
|
+
network.body.data.edges.forEach(function(e) {
|
|
501
|
+
var def = edgeDefaults[e.id];
|
|
502
|
+
var isConnected = connectedNodeIds.has(e.from) && connectedNodeIds.has(e.to);
|
|
503
|
+
network.body.data.edges.update({
|
|
504
|
+
id: e.id,
|
|
505
|
+
color: {
|
|
506
|
+
color: isConnected ? (def ? def.color : '#64748b') : 'rgba(100,116,139,0.05)',
|
|
507
|
+
highlight: '#a855f7',
|
|
508
|
+
hover: '#38bdf8',
|
|
509
|
+
opacity: isConnected ? 0.85 : 0.05
|
|
510
|
+
},
|
|
511
|
+
width: isConnected ? (def ? def.width : 1.5) : 0.3,
|
|
512
|
+
font: {
|
|
513
|
+
color: isConnected ? '#94a3b8' : 'rgba(148,163,184,0.1)',
|
|
514
|
+
size: 10,
|
|
515
|
+
strokeWidth: 0,
|
|
516
|
+
background: 'transparent'
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
385
520
|
} else {
|
|
386
|
-
|
|
521
|
+
document.getElementById("graph-tooltip").style.display = 'none';
|
|
387
522
|
network.body.data.nodes.forEach(function(n) {
|
|
388
|
-
network.body.data.nodes.update({
|
|
523
|
+
network.body.data.nodes.update({
|
|
524
|
+
id: n.id,
|
|
525
|
+
opacity: 1.0,
|
|
526
|
+
font: { color: '#ffffff', bold: true }
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
network.body.data.edges.forEach(function(e) {
|
|
530
|
+
var def = edgeDefaults[e.id];
|
|
531
|
+
network.body.data.edges.update({
|
|
532
|
+
id: e.id,
|
|
533
|
+
color: {
|
|
534
|
+
color: def ? def.color : '#64748b',
|
|
535
|
+
highlight: '#a855f7',
|
|
536
|
+
hover: '#38bdf8',
|
|
537
|
+
opacity: 0.85
|
|
538
|
+
},
|
|
539
|
+
width: def ? def.width : 1.5,
|
|
540
|
+
font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' }
|
|
541
|
+
});
|
|
389
542
|
});
|
|
390
543
|
}
|
|
391
544
|
});
|
|
392
545
|
|
|
393
|
-
// Hover highlight
|
|
546
|
+
// Hover node → show tooltip + border highlight
|
|
394
547
|
network.on('hoverNode', function(params) {
|
|
395
548
|
var nodeId = params.node;
|
|
396
549
|
network.body.data.nodes.updateOnly({
|
|
@@ -398,6 +551,23 @@ function renderGraph(data) {
|
|
|
398
551
|
borderWidth: 2,
|
|
399
552
|
borderWidthSelected: 2,
|
|
400
553
|
});
|
|
554
|
+
|
|
555
|
+
var n = allNodes.find(function(x){return x.id===nodeId});
|
|
556
|
+
if (n) {
|
|
557
|
+
var tip = document.getElementById("graph-tooltip");
|
|
558
|
+
tip.style.display = 'block';
|
|
559
|
+
tip.innerHTML = '<div style="word-break:break-all;overflow-wrap:anywhere;max-width:390px;white-space:normal;line-height:1.4"><b style="color:#f8fafc">' + n.name + '</b> <span style="color:#94a3b8">' + n.type + '</span>' +
|
|
560
|
+
(n.file_path ? '<br><span style="color:#38bdf8;font-size:11px;word-break:break-all;overflow-wrap:anywhere">' + n.file_path + '</span>' : '') + '</div>';
|
|
561
|
+
var pos = network.getPositions([n.id]);
|
|
562
|
+
if (pos && pos[n.id]) {
|
|
563
|
+
var canvasPos = network.canvasToDOM(pos[n.id]);
|
|
564
|
+
var panelRect = container.getBoundingClientRect();
|
|
565
|
+
var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
|
|
566
|
+
var tipY = Math.max(canvasPos.y - 10, 0);
|
|
567
|
+
tip.style.left = Math.max(tipX, 8) + 'px';
|
|
568
|
+
tip.style.top = tipY + 'px';
|
|
569
|
+
}
|
|
570
|
+
}
|
|
401
571
|
});
|
|
402
572
|
network.on('blurNode', function(params) {
|
|
403
573
|
network.body.data.nodes.updateOnly({
|
|
@@ -405,6 +575,7 @@ function renderGraph(data) {
|
|
|
405
575
|
borderWidth: 0,
|
|
406
576
|
borderWidthSelected: 0,
|
|
407
577
|
});
|
|
578
|
+
document.getElementById("graph-tooltip").style.display = 'none';
|
|
408
579
|
});
|
|
409
580
|
|
|
410
581
|
renderLegend(types, colors);
|