@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.
Files changed (39) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +10221 -332
  3. package/package.json +2 -2
  4. package/packages/ide/studio/dist/index.js +37 -5
  5. package/packages/ide/studio/public/index.html +239 -68
  6. package/dist/agentDetector-YOWQVJFR.js +0 -186
  7. package/dist/chunk-3BRBJZ7P.js +0 -1055
  8. package/dist/chunk-3OHYYXYN.js +0 -71
  9. package/dist/chunk-ABKE45T4.js +0 -1264
  10. package/dist/chunk-E2KFPEBT.js +0 -183
  11. package/dist/chunk-FKRSI5U5.js +0 -282
  12. package/dist/chunk-GFLSAXAH.js +0 -155
  13. package/dist/chunk-L7F67KUP.js +0 -172
  14. package/dist/chunk-LVKOGXLC.js +0 -658
  15. package/dist/chunk-PRUTTZBS.js +0 -1113
  16. package/dist/contextDigest-QB5XHPXE.js +0 -177
  17. package/dist/domainRules-QLPAQASB.js +0 -20
  18. package/dist/init-PL4XL662.js +0 -15
  19. package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
  20. package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
  21. package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
  22. package/dist/kumaContractEngine-KX27T4N7.js +0 -305
  23. package/dist/kumaDb-4XZ5S2LH.js +0 -65
  24. package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
  25. package/dist/kumaGotchas-XRGFFBTA.js +0 -151
  26. package/dist/kumaGraph-UMXZNGYF.js +0 -44
  27. package/dist/kumaMemory-FBJMV77G.js +0 -16
  28. package/dist/kumaMiner-XJETL7TL.js +0 -176
  29. package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
  30. package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
  31. package/dist/kumaSearch-PV4QTKE7.js +0 -321
  32. package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
  33. package/dist/kumaVerifier-6YEGC77M.js +0 -265
  34. package/dist/kumaVisualize-264OEBGJ.js +0 -264
  35. package/dist/pathValidator-V4DC6U6Z.js +0 -22
  36. package/dist/safetyAudit-O45SPNTS.js +0 -12
  37. package/dist/safetyScore-TMMRD2MV.js +0 -333
  38. package/dist/sessionMemory-YPKVIOMV.js +0 -6
  39. 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.20",
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
- return JSON.parse(raw);
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 created_at DESC`
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.92);
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: var(--muted); }
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(22,33,62,0.95);
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: 320px;
86
- box-shadow: 0 8px 32px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
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>&#9826; Kuma Studio</h1>
127
134
  <div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
128
- <button onclick="refetch()" id="refetch-btn" style="margin-left:auto;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;-webkit-app-region:no-drag" onmouseover="this.style.background='rgba(124,58,237,0.3)'" onmouseout="this.style.background='rgba(124,58,237,0.15)'">&#8635; Refetch</button>
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">&#8635; 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">&#128465; 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">&#9632; 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="if(network)network.startSimulation()">Restart</button>
140
- <button onclick="if(network)network.stopSimulation()">Stop</button>
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({ id: n.id, opacity: 1.0 });
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 base = 14;
229
- if (type === "feature_domain") base = 24;
230
- if (type === "workflow") base = 18;
231
- if (type === "cross_service_link") base = 16;
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 = 18;
234
- if (type === "module") base = 12;
235
- if (type === "component") base = 16;
236
- if (type === "route") base = 14;
237
- if (type === "test") base = 13;
238
- // Gentle scaling for long labels
239
- var len = name ? Math.min(name.length, 20) : 4;
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 = {feature_domain:'#ef4444',workflow:'#3b82f6',cross_service_link:'#10b981',
254
- function:'#7c3aed',class:'#06b6d4',module:'#22c55e',
255
- component:'#f97316',file:'#eab308',route:'#ec4899',test:'#14b8a6'};
256
- var shapes = {feature_domain:'star',workflow:'ellipse',cross_service_link:'box',
257
- function:'box',class:'hexagon',module:'ellipse',
258
- component:'diamond',file:'square',route:'triangle',test:'star'};
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 label = truncateLabel(n.name, 28);
270
- var size = calcNodeSize(n.name, n.type);
271
- var color = colors[n.type] || '#888';
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
- title: '<b>' + n.name + '</b> <span style="color:#888">' + n.type + '</span>' + (n.file_path ? '<br><span style="color:#06b6d4;font-size:11px">' + n.file_path + '</span>' : ''),
276
- color: { background: color, border: color, highlight: { background: color, border: '#fff' } },
277
- shape: shapes[n.type] || 'dot',
278
- font: { color: '#e0e0e0', size: Math.min(14, Math.max(10, size * 0.5)), face: 'monospace' },
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 displayEdges = allEdges.filter(function(e){
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 visEdges = new vis.DataSet(displayEdges.map(function(e){
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 = '#475569'; width = 1; }
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: '#e0e0e0', size: 12, face: 'monospace' },
343
- margin: { top: 4, bottom: 4, left: 6, right: 6 },
344
- shadow: { enabled: true, color: 'rgba(124,58,237,0.15)', size: 6, x: 0, y: 2 },
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 → show tooltip with file path + highlight connected neighborhood
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: connectedNodeIds.has(n.id) ? 1.0 : 0.15
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
- var n = allNodes.find(function(x){return x.id===selectedId});
370
- if (n) {
371
- tip.style.display = 'block';
372
- tip.innerHTML = '<b>' + n.name + '</b> <span style="color:var(--muted)">' + n.type + '</span>' +
373
- (n.file_path ? '<br><span style="color:var(--accent2);font-size:11px;word-break:break-all">' + n.file_path + '</span>' : '');
374
- // Position tooltip near the clicked node
375
- var pos = network.getPositions([n.id]);
376
- if (pos && pos[n.id]) {
377
- var canvasPos = network.canvasToDOM(pos[n.id]);
378
- var panelRect = container.getBoundingClientRect();
379
- var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
380
- var tipY = Math.max(canvasPos.y - 10, 0);
381
- tip.style.left = Math.max(tipX, 8) + 'px';
382
- tip.style.top = tipY + 'px';
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
- tip.style.display = 'none';
521
+ document.getElementById("graph-tooltip").style.display = 'none';
387
522
  network.body.data.nodes.forEach(function(n) {
388
- network.body.data.nodes.update({ id: n.id, opacity: 1.0 });
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);