@plumpslabs/kuma 2.3.19 → 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 +284 -63
  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.19",
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,9 +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="if(network)network.fit()">Fit</button>
137
- <button onclick="if(network)network.startSimulation()">Restart</button>
138
- <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>
139
152
  </div>
140
153
  <div id="graph-tooltip"></div>
141
154
  <div id="legend"></div>
@@ -152,7 +165,74 @@ header .status .dot.off { background: var(--red); }
152
165
 
153
166
  <script>
154
167
  const API = "/api";
155
- 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
+ }
196
+
197
+ function resetGraphFocus() {
198
+ if (!network) return;
199
+ network.body.data.nodes.forEach(function(n) {
200
+ network.body.data.nodes.update({
201
+ id: n.id,
202
+ opacity: 1.0,
203
+ font: { color: '#ffffff', bold: true }
204
+ });
205
+ });
206
+ document.getElementById("graph-tooltip").style.display = 'none';
207
+ var searchInput = document.getElementById("node-search");
208
+ if (searchInput) searchInput.value = "";
209
+ }
210
+
211
+ function exportGraphPNG() {
212
+ if (!network) return;
213
+ var canvas = document.querySelector("#graph-container canvas");
214
+ if (!canvas) return;
215
+ var link = document.createElement("a");
216
+ link.download = "kuma-knowledge-graph.png";
217
+ link.href = canvas.toDataURL("image/png");
218
+ link.click();
219
+ }
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
+ }
156
236
 
157
237
  async function init() {
158
238
  var status = await fetch(API+"/status").then(function(r){return r.json()});
@@ -161,6 +241,7 @@ async function init() {
161
241
  if (status.ok) {
162
242
  dot.className = "dot on";
163
243
  txt.textContent = status.project || "connected";
244
+ currentDbSize = status.dbSize || "0 KB";
164
245
  } else {
165
246
  dot.className = "dot off";
166
247
  txt.textContent = "no kuma.db found";
@@ -191,6 +272,7 @@ function renderStats(data) {
191
272
  '<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
192
273
  '<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
193
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>'+
194
276
  '<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
195
277
  }
196
278
 
@@ -203,16 +285,21 @@ function truncateLabel(name, maxLen) {
203
285
 
204
286
  /** Calculate node size based on label length and type */
205
287
  function calcNodeSize(name, type) {
206
- var base = 14;
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;
207
295
  if (type === "function") base = 16;
208
- if (type === "class") base = 18;
209
- if (type === "module") base = 12;
210
- if (type === "component") base = 16;
211
- if (type === "route") base = 14;
212
- if (type === "test") base = 13;
213
- // Gentle scaling for long labels
214
- var len = name ? Math.min(name.length, 20) : 4;
215
- return Math.min(28, 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));
216
303
  }
217
304
 
218
305
 
@@ -225,10 +312,32 @@ function renderGraph(data) {
225
312
  }
226
313
 
227
314
  var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
228
- var colors = {function:'#7c3aed',class:'#06b6d4',module:'#22c55e',
229
- component:'#f97316',file:'#eab308',route:'#ec4899',test:'#14b8a6'};
230
- var shapes = {function:'box',class:'hexagon',module:'ellipse',
231
- 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
+ };
232
341
 
233
342
  // Limit nodes for performance if > 200
234
343
  var maxNodes = 300;
@@ -239,16 +348,27 @@ function renderGraph(data) {
239
348
  }
240
349
 
241
350
  var visNodes = new vis.DataSet(displayNodes.map(function(n){
242
- var label = truncateLabel(n.name, 28);
243
- var size = calcNodeSize(n.name, n.type);
244
- 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');
245
358
  return {
246
359
  id: n.id,
247
360
  label: label,
248
- 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>' : ''),
249
- color: { background: color, border: color, highlight: { background: color, border: '#fff' } },
250
- shape: shapes[n.type] || 'dot',
251
- 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 },
252
372
  borderWidth: 0,
253
373
  size: size,
254
374
  mass: 1 + (size / 40),
@@ -256,21 +376,63 @@ function renderGraph(data) {
256
376
  }));
257
377
 
258
378
  var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
259
- 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){
260
410
  return nodeIds.has(e.source) && nodeIds.has(e.target);
261
411
  }).slice(0, 500);
262
412
 
263
- var visEdges = new vis.DataSet(displayEdges.map(function(e){
413
+ var edgeDefaults = {};
414
+ var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
415
+ var edgeColor = '#64748b';
416
+ var width = 1.5;
417
+ var dashes = false;
418
+ if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
419
+ if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
420
+ if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
421
+ if (e.relation === 'contains') { edgeColor = '#64748b'; width = 1.5; dashes = true; }
422
+
423
+ edgeDefaults['e'+idx] = { color: edgeColor, width: width };
264
424
  return {
425
+ id: 'e'+idx,
265
426
  from: e.source, to: e.target,
266
427
  label: e.relation || '',
267
- arrows: { to: { enabled: true, scaleFactor: 0.6, type: 'arrow' } },
268
- color: { color: '#4a4a6a', highlight: '#7c3aed', hover: '#7c3aed', opacity: 0.5 },
269
- font: { size: 10, color: '#666', strokeWidth: 0, background: 'transparent' },
428
+ arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
429
+ color: { color: edgeColor, highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 },
430
+ font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' },
431
+ dashes: dashes,
270
432
  smooth: { type: 'continuous', roundness: 0.3 },
271
- width: 1.5,
272
- hoverWidth: 2.5,
273
- selectionWidth: 2.5,
433
+ width: width,
434
+ hoverWidth: 3,
435
+ selectionWidth: 3,
274
436
  };
275
437
  }));
276
438
 
@@ -303,9 +465,10 @@ function renderGraph(data) {
303
465
  },
304
466
  nodes: {
305
467
  borderWidth: 0,
306
- font: { color: '#e0e0e0', size: 12, face: 'monospace' },
307
- margin: { top: 4, bottom: 4, left: 6, right: 6 },
308
- 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 },
309
472
  },
310
473
  edges: {
311
474
  smooth: { type: 'continuous', roundness: 0.3 },
@@ -315,32 +478,72 @@ function renderGraph(data) {
315
478
  configure: { enabled: false },
316
479
  });
317
480
 
318
- // Click node → show tooltip with file path
481
+ // Click node → highlight connected neighborhood (tooltip via hover only)
319
482
  network.on('click', function(params) {
320
- var tip = document.getElementById("graph-tooltip");
321
483
  if (params.nodes.length) {
322
- var n = allNodes.find(function(x){return x.id===params.nodes[0]});
323
- if (n) {
324
- tip.style.display = 'block';
325
- tip.innerHTML = '<b>' + n.name + '</b> <span style="color:var(--muted)">' + n.type + '</span>' +
326
- (n.file_path ? '<br><span style="color:var(--accent2);font-size:11px;word-break:break-all">' + n.file_path + '</span>' : '');
327
- // Position tooltip near the clicked node
328
- var pos = network.getPositions([n.id]);
329
- if (pos && pos[n.id]) {
330
- var canvasPos = network.canvasToDOM(pos[n.id]);
331
- var panelRect = container.getBoundingClientRect();
332
- var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
333
- var tipY = Math.max(canvasPos.y - 10, 0);
334
- tip.style.left = Math.max(tipX, 8) + 'px';
335
- tip.style.top = tipY + 'px';
336
- }
337
- }
484
+ var selectedId = params.nodes[0];
485
+ var connectedNodeIds = new Set(network.getConnectedNodes(selectedId));
486
+ connectedNodeIds.add(selectedId);
487
+
488
+ network.body.data.nodes.forEach(function(n) {
489
+ var isConnected = connectedNodeIds.has(n.id);
490
+ network.body.data.nodes.update({
491
+ id: n.id,
492
+ opacity: isConnected ? 1.0 : 0.12,
493
+ font: {
494
+ color: isConnected ? '#ffffff' : 'rgba(255,255,255,0.07)',
495
+ bold: isConnected
496
+ }
497
+ });
498
+ });
499
+
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
+ });
338
520
  } else {
339
- tip.style.display = 'none';
521
+ document.getElementById("graph-tooltip").style.display = 'none';
522
+ network.body.data.nodes.forEach(function(n) {
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
+ });
542
+ });
340
543
  }
341
544
  });
342
545
 
343
- // Hover highlight
546
+ // Hover node → show tooltip + border highlight
344
547
  network.on('hoverNode', function(params) {
345
548
  var nodeId = params.node;
346
549
  network.body.data.nodes.updateOnly({
@@ -348,6 +551,23 @@ function renderGraph(data) {
348
551
  borderWidth: 2,
349
552
  borderWidthSelected: 2,
350
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
+ }
351
571
  });
352
572
  network.on('blurNode', function(params) {
353
573
  network.body.data.nodes.updateOnly({
@@ -355,6 +575,7 @@ function renderGraph(data) {
355
575
  borderWidth: 0,
356
576
  borderWidthSelected: 0,
357
577
  });
578
+ document.getElementById("graph-tooltip").style.display = 'none';
358
579
  });
359
580
 
360
581
  renderLegend(types, colors);