codebase-onboarder 0.1.0

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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. package/shared/search/query.js +518 -0
@@ -0,0 +1,592 @@
1
+ // High-performance, interactive force-directed graph canvas engine.
2
+ // Implements Barnes-Hut physics, neighbor subgraph highlighting,
3
+ // theme-aware crisp text rendering, and rich interactive controls.
4
+
5
+ export function initForceGraph(canvas, options) {
6
+ const opts = options || {};
7
+ const ctx = canvas.getContext('2d');
8
+
9
+ let animationFrameId = null;
10
+ let nodes = [];
11
+ let edges = [];
12
+ let communities = {};
13
+
14
+ let transform = { x: 0, y: 0, k: 1 };
15
+ let width = canvas.width;
16
+ let height = canvas.height;
17
+
18
+ let alpha = 1.0;
19
+ const alphaDecay = 0.985;
20
+ const alphaMin = 0.001;
21
+
22
+ let draggedNode = null;
23
+ let hoveredNode = null;
24
+ let selectedNode = null;
25
+ let highlightPath = null;
26
+ let searchQuery = '';
27
+
28
+ // Adjacency maps for fast interactive neighbor highlighting
29
+ let inNeighbors = new Map(); // node -> Set of source nodes
30
+ let outNeighbors = new Map(); // node -> Set of target nodes
31
+ let nodeEdges = new Map(); // node -> Set of edges
32
+
33
+ function resize() {
34
+ const parent = canvas.parentElement;
35
+ if (!parent) return;
36
+ const rect = parent.getBoundingClientRect();
37
+ if (rect.width === 0 || rect.height === 0) return;
38
+
39
+ const dpr = window.devicePixelRatio || 1;
40
+ canvas.width = rect.width * dpr;
41
+ canvas.height = rect.height * dpr;
42
+ canvas.style.width = rect.width + 'px';
43
+ canvas.style.height = rect.height + 'px';
44
+ ctx.scale(dpr, dpr);
45
+
46
+ width = rect.width;
47
+ height = rect.height;
48
+
49
+ if (nodes.length > 0 && transform.k === 1 && transform.x === 0 && transform.y === 0) {
50
+ fitToScreen();
51
+ } else {
52
+ render();
53
+ }
54
+ }
55
+
56
+ // Quadtree spatial index for Barnes-Hut O(N log N) repulsion
57
+ class QuadTree {
58
+ constructor(x, y, w, h) {
59
+ this.x = x;
60
+ this.y = y;
61
+ this.w = w;
62
+ this.h = h;
63
+ this.nodes = [];
64
+ this.mass = 0;
65
+ this.cx = 0;
66
+ this.cy = 0;
67
+ this.children = null;
68
+ }
69
+
70
+ insert(node) {
71
+ if (this.children) {
72
+ const i = (node.x > this.x + this.w / 2 ? 1 : 0) + (node.y > this.y + this.h / 2 ? 2 : 0);
73
+ this.children[i].insert(node);
74
+ } else {
75
+ this.nodes.push(node);
76
+ if (this.nodes.length > 1 && this.w > 4) {
77
+ this.subdivide();
78
+ }
79
+ }
80
+ this.mass += 1;
81
+ this.cx = (this.cx * (this.mass - 1) + node.x) / this.mass;
82
+ this.cy = (this.cy * (this.mass - 1) + node.y) / this.mass;
83
+ }
84
+
85
+ subdivide() {
86
+ const hw = this.w / 2;
87
+ const hh = this.h / 2;
88
+ this.children = [
89
+ new QuadTree(this.x, this.y, hw, hh),
90
+ new QuadTree(this.x + hw, this.y, hw, hh),
91
+ new QuadTree(this.x, this.y + hh, hw, hh),
92
+ new QuadTree(this.x + hw, this.y + hh, hw, hh)
93
+ ];
94
+ const oldNodes = this.nodes;
95
+ this.nodes = [];
96
+ for (const n of oldNodes) {
97
+ const i = (n.x > this.x + hw ? 1 : 0) + (n.y > this.y + hh ? 2 : 0);
98
+ this.children[i].insert(n);
99
+ }
100
+ }
101
+
102
+ applyForce(node, theta = 0.85) {
103
+ if (this.mass === 0) return;
104
+ const dx = this.cx - node.x;
105
+ const dy = this.cy - node.y;
106
+ const distSq = dx * dx + dy * dy;
107
+
108
+ if (this.w / Math.sqrt(distSq || 1) < theta || !this.children) {
109
+ if (distSq > 0.01) {
110
+ const dist = Math.sqrt(distSq);
111
+ // Scale repulsion force inversely proportional to distance
112
+ const f = -120 * this.mass / (distSq + 100);
113
+ node.vx += (dx / dist) * f;
114
+ node.vy += (dy / dist) * f;
115
+ }
116
+ } else {
117
+ for (const child of this.children) {
118
+ child.applyForce(node, theta);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ function buildAdjacency() {
125
+ inNeighbors = new Map();
126
+ outNeighbors = new Map();
127
+ nodeEdges = new Map();
128
+
129
+ for (const n of nodes) {
130
+ inNeighbors.set(n, new Set());
131
+ outNeighbors.set(n, new Set());
132
+ nodeEdges.set(n, new Set());
133
+ }
134
+
135
+ for (const e of edges) {
136
+ if (e.source && e.target) {
137
+ outNeighbors.get(e.source)?.add(e.target);
138
+ inNeighbors.get(e.target)?.add(e.source);
139
+ nodeEdges.get(e.source)?.add(e);
140
+ nodeEdges.get(e.target)?.add(e);
141
+ }
142
+ }
143
+ }
144
+
145
+ function update(newNodes, newEdges, newComms) {
146
+ communities = newComms || {};
147
+
148
+ const posMap = new Map(nodes.map(n => [n.path, { x: n.x, y: n.y, vx: n.vx, vy: n.vy }]));
149
+
150
+ nodes = newNodes.map((n, i) => {
151
+ const p = posMap.get(n.path);
152
+ const angle = (i / Math.max(1, newNodes.length)) * Math.PI * 2;
153
+ const dist = Math.min(width, height) * 0.35;
154
+ return {
155
+ ...n,
156
+ x: p ? p.x : Math.cos(angle) * dist + (Math.random() - 0.5) * 40,
157
+ y: p ? p.y : Math.sin(angle) * dist + (Math.random() - 0.5) * 40,
158
+ vx: p ? p.vx : 0,
159
+ vy: p ? p.vy : 0,
160
+ r: Math.max(5, Math.min(24, 4 + Math.sqrt(n.fanIn || 0) * 2.5)),
161
+ basename: n.path.split('/').pop() || n.path
162
+ };
163
+ });
164
+
165
+ const nodeByPath = new Map(nodes.map(n => [n.path, n]));
166
+ edges = (newEdges || []).map(e => ({
167
+ source: nodeByPath.get(e.from),
168
+ target: nodeByPath.get(e.to),
169
+ from: e.from,
170
+ to: e.to
171
+ })).filter(e => e.source && e.target);
172
+
173
+ buildAdjacency();
174
+
175
+ alpha = 1.0;
176
+ if (!animationFrameId) tick();
177
+ }
178
+
179
+ function tick() {
180
+ if (alpha > alphaMin) {
181
+ alpha *= alphaDecay;
182
+
183
+ // Repulsion via Quadtree
184
+ if (nodes.length > 0) {
185
+ let minX = nodes[0].x, maxX = nodes[0].x;
186
+ let minY = nodes[0].y, maxY = nodes[0].y;
187
+ for (const n of nodes) {
188
+ if (n.x < minX) minX = n.x; if (n.x > maxX) maxX = n.x;
189
+ if (n.y < minY) minY = n.y; if (n.y > maxY) maxY = n.y;
190
+ }
191
+ const span = Math.max(maxX - minX, maxY - minY, 100);
192
+ const qt = new QuadTree(minX - 20, minY - 20, span + 40, span + 40);
193
+ for (const n of nodes) qt.insert(n);
194
+ for (const n of nodes) qt.applyForce(n);
195
+ }
196
+
197
+ // Hooke's Law Edge Springs
198
+ for (const e of edges) {
199
+ const dx = e.target.x - e.source.x;
200
+ const dy = e.target.y - e.source.y;
201
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
202
+ const targetDist = 45 + (e.source.r + e.target.r) * 1.5;
203
+ const f = (dist - targetDist) * 0.04 * alpha;
204
+ const fx = (dx / dist) * f;
205
+ const fy = (dy / dist) * f;
206
+ e.source.vx += fx; e.source.vy += fy;
207
+ e.target.vx -= fx; e.target.vy -= fy;
208
+ }
209
+
210
+ // Center Gravity
211
+ for (const n of nodes) {
212
+ n.vx -= n.x * 0.015 * alpha;
213
+ n.vy -= n.y * 0.015 * alpha;
214
+ }
215
+
216
+ // Velocity Verlet update
217
+ for (const n of nodes) {
218
+ if (n !== draggedNode) {
219
+ n.vx *= 0.65;
220
+ n.vy *= 0.65;
221
+ n.x += n.vx;
222
+ n.y += n.vy;
223
+ }
224
+ }
225
+ }
226
+
227
+ render();
228
+
229
+ if (alpha > alphaMin || draggedNode) {
230
+ animationFrameId = requestAnimationFrame(tick);
231
+ } else {
232
+ animationFrameId = null;
233
+ }
234
+ }
235
+
236
+ function reheat() {
237
+ alpha = 0.5;
238
+ if (!animationFrameId) tick();
239
+ }
240
+
241
+ function isDarkTheme() {
242
+ return document.documentElement.dataset.theme === 'dark';
243
+ }
244
+
245
+ function render() {
246
+ ctx.save();
247
+ ctx.clearRect(0, 0, width, height);
248
+
249
+ ctx.translate(transform.x, transform.y);
250
+ ctx.scale(transform.k, transform.k);
251
+
252
+ const dark = isDarkTheme();
253
+ const activeNode = hoveredNode || selectedNode;
254
+ const hasFocus = Boolean(activeNode || highlightPath || searchQuery);
255
+
256
+ const inSet = activeNode ? inNeighbors.get(activeNode) : null;
257
+ const outSet = activeNode ? outNeighbors.get(activeNode) : null;
258
+
259
+ // Draw Edges
260
+ for (const e of edges) {
261
+ const isOut = activeNode && e.source === activeNode;
262
+ const isIn = activeNode && e.target === activeNode;
263
+ const isConnected = isOut || isIn;
264
+
265
+ ctx.beginPath();
266
+ ctx.moveTo(e.source.x, e.source.y);
267
+ ctx.lineTo(e.target.x, e.target.y);
268
+
269
+ if (isConnected) {
270
+ ctx.strokeStyle = isOut ? '#f43f5e' : '#38bdf8'; // Outbound (rose) vs Inbound (cyan)
271
+ ctx.lineWidth = 2.4 / transform.k;
272
+ ctx.globalAlpha = 0.95;
273
+ } else {
274
+ ctx.strokeStyle = dark ? 'rgba(255, 255, 255, 0.25)' : 'rgba(30, 41, 59, 0.22)';
275
+ ctx.lineWidth = 1 / transform.k;
276
+ ctx.globalAlpha = hasFocus ? 0.04 : 0.45;
277
+ }
278
+ ctx.stroke();
279
+ }
280
+
281
+ // Draw Nodes
282
+ for (const n of nodes) {
283
+ const isTarget = n === activeNode;
284
+ const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
285
+ const isPathMatch = highlightPath && n.path === highlightPath;
286
+ const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
287
+ const isHighlighted = isTarget || isNeighbor || isPathMatch || isSearchMatch;
288
+
289
+ const nodeAlpha = hasFocus ? (isHighlighted ? 1.0 : 0.18) : 0.92;
290
+ ctx.globalAlpha = nodeAlpha;
291
+
292
+ // Node circle fill
293
+ ctx.beginPath();
294
+ ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI);
295
+
296
+ const hue = n.community ? (n.community * 137.5) % 360 : ((n.fanIn * 37) % 360);
297
+ ctx.fillStyle = `hsl(${hue}, 75%, ${dark ? '55%' : '45%'})`;
298
+ ctx.fill();
299
+
300
+ // Outer border / glow ring
301
+ if (isHighlighted) {
302
+ ctx.lineWidth = (isTarget ? 3.5 : 2) / transform.k;
303
+ ctx.strokeStyle = isTarget ? (dark ? '#ffffff' : '#0f172a') : (inSet?.has(n) ? '#38bdf8' : '#f43f5e');
304
+ ctx.stroke();
305
+ } else {
306
+ ctx.lineWidth = 1 / transform.k;
307
+ ctx.strokeStyle = dark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
308
+ ctx.stroke();
309
+ }
310
+ }
311
+
312
+ // Draw Crisp Theme-Aware Text Labels (with dark/light halo outline)
313
+ ctx.textAlign = 'left';
314
+ ctx.textBaseline = 'middle';
315
+
316
+ for (const n of nodes) {
317
+ const isTarget = n === activeNode;
318
+ const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
319
+ const isPathMatch = highlightPath && n.path === highlightPath;
320
+ const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
321
+ const shouldShowLabel = isTarget || isNeighbor || isPathMatch || isSearchMatch || (n.fanIn >= 2 && transform.k >= 0.4) || transform.k >= 1.1;
322
+
323
+ if (!shouldShowLabel) continue;
324
+
325
+ const isProminent = isTarget || isNeighbor || isPathMatch || isSearchMatch;
326
+ ctx.globalAlpha = hasFocus ? (isProminent ? 1.0 : 0.15) : 0.95;
327
+
328
+ const fontSize = Math.max(10, Math.min(14, 11 / Math.sqrt(transform.k)));
329
+ ctx.font = `${isProminent ? '600' : '500'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace`;
330
+
331
+ const textX = n.x + n.r + 4 / transform.k;
332
+ const textY = n.y;
333
+ const label = n.basename;
334
+
335
+ // Stroke halo background for contrast against any background
336
+ ctx.lineWidth = 3.5 / transform.k;
337
+ ctx.lineJoin = 'round';
338
+ ctx.strokeStyle = dark ? 'rgba(10, 14, 23, 0.92)' : 'rgba(255, 255, 255, 0.95)';
339
+ ctx.strokeText(label, textX, textY);
340
+
341
+ // Bright / High-contrast foreground text
342
+ if (isTarget) {
343
+ ctx.fillStyle = dark ? '#38bdf8' : '#0284c7';
344
+ } else if (isProminent) {
345
+ ctx.fillStyle = dark ? '#f8fafc' : '#0f172a';
346
+ } else {
347
+ ctx.fillStyle = dark ? '#cbd5e1' : '#334155';
348
+ }
349
+ ctx.fillText(label, textX, textY);
350
+ }
351
+
352
+ ctx.restore();
353
+ }
354
+
355
+ function fitToScreen() {
356
+ if (nodes.length === 0) {
357
+ transform = { x: width / 2, y: height / 2, k: 1 };
358
+ render();
359
+ return;
360
+ }
361
+
362
+ let minX = nodes[0].x, maxX = nodes[0].x;
363
+ let minY = nodes[0].y, maxY = nodes[0].y;
364
+ for (const n of nodes) {
365
+ if (n.x - n.r < minX) minX = n.x - n.r;
366
+ if (n.x + n.r > maxX) maxX = n.x + n.r;
367
+ if (n.y - n.r < minY) minY = n.y - n.r;
368
+ if (n.y + n.r > maxY) maxY = n.y + n.r;
369
+ }
370
+
371
+ const spanW = Math.max(40, maxX - minX);
372
+ const spanH = Math.max(40, maxY - minY);
373
+ const padding = 60;
374
+
375
+ const k = Math.min(
376
+ (width - padding * 2) / spanW,
377
+ (height - padding * 2) / spanH,
378
+ 2.0
379
+ );
380
+
381
+ const cx = (minX + maxX) / 2;
382
+ const cy = (minY + maxY) / 2;
383
+
384
+ transform.k = Math.max(0.2, k);
385
+ transform.x = width / 2 - cx * transform.k;
386
+ transform.y = height / 2 - cy * transform.k;
387
+
388
+ render();
389
+ }
390
+
391
+ function resetLayout() {
392
+ const dist = Math.min(width, height) * 0.35;
393
+ nodes.forEach((n, i) => {
394
+ const angle = (i / Math.max(1, nodes.length)) * Math.PI * 2;
395
+ n.x = Math.cos(angle) * dist + (Math.random() - 0.5) * 30;
396
+ n.y = Math.sin(angle) * dist + (Math.random() - 0.5) * 30;
397
+ n.vx = 0;
398
+ n.vy = 0;
399
+ });
400
+ fitToScreen();
401
+ alpha = 1.0;
402
+ if (!animationFrameId) tick();
403
+ }
404
+
405
+ function flyToNode(node) {
406
+ if (!node) return;
407
+ const targetK = Math.max(1.2, transform.k);
408
+ transform.k = targetK;
409
+ transform.x = width / 2 - node.x * targetK;
410
+ transform.y = height / 2 - node.y * targetK;
411
+ selectedNode = node;
412
+ render();
413
+ }
414
+
415
+ function updateTooltip(node, clientX, clientY) {
416
+ const tooltip = document.getElementById('graphTooltip');
417
+ if (!tooltip) return;
418
+
419
+ if (!node) {
420
+ tooltip.hidden = true;
421
+ return;
422
+ }
423
+
424
+ const inCount = inNeighbors.get(node)?.size || node.fanIn || 0;
425
+ const outCount = outNeighbors.get(node)?.size || node.fanOut || 0;
426
+
427
+ tooltip.innerHTML = `
428
+ <div style="font-weight:600; font-size:13px; margin-bottom:4px; display:flex; align-items:center; gap:6px;">
429
+ <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:hsl(${(node.community * 137.5) % 360}, 75%, 50%);"></span>
430
+ ${node.basename}
431
+ </div>
432
+ <div style="font-size:11px; color:var(--faint); margin-bottom:8px; word-break:break-all;">${node.path}</div>
433
+ <div style="display:flex; gap:6px; flex-wrap:wrap; font-size:11px;">
434
+ <span class="role-chip" style="background:var(--accent-soft); color:var(--accent);"><b>${inCount}</b> dependents</span>
435
+ <span class="role-chip" style="background:var(--card);"><b>${outCount}</b> imports</span>
436
+ ${node.risk ? `<span class="role-chip is-cycle">risk <b>${node.risk}</b></span>` : ''}
437
+ </div>
438
+ <div style="font-size:10px; color:var(--faint); margin-top:6px; font-style:italic;">Click to open · Double-click to center</div>
439
+ `;
440
+
441
+ tooltip.style.left = Math.min(clientX + 14, window.innerWidth - 260) + 'px';
442
+ tooltip.style.top = Math.min(clientY + 14, window.innerHeight - 160) + 'px';
443
+ tooltip.hidden = false;
444
+ }
445
+
446
+ // --- Interaction Event Handlers ---
447
+ let isPanning = false;
448
+ let startX = 0, startY = 0;
449
+ let lastClickTime = 0;
450
+
451
+ canvas.addEventListener('mousedown', e => {
452
+ const rect = canvas.getBoundingClientRect();
453
+ const mx = (e.clientX - rect.left - transform.x) / transform.k;
454
+ const my = (e.clientY - rect.top - transform.y) / transform.k;
455
+
456
+ draggedNode = nodes.find(n => {
457
+ const dx = n.x - mx;
458
+ const dy = n.y - my;
459
+ return Math.sqrt(dx * dx + dy * dy) <= (n.r + 4);
460
+ });
461
+
462
+ if (draggedNode) {
463
+ draggedNode.vx = 0;
464
+ draggedNode.vy = 0;
465
+ reheat();
466
+ } else {
467
+ isPanning = true;
468
+ startX = e.clientX - transform.x;
469
+ startY = e.clientY - transform.y;
470
+ }
471
+ });
472
+
473
+ canvas.addEventListener('mousemove', e => {
474
+ const rect = canvas.getBoundingClientRect();
475
+
476
+ if (draggedNode) {
477
+ draggedNode.x = (e.clientX - rect.left - transform.x) / transform.k;
478
+ draggedNode.y = (e.clientY - rect.top - transform.y) / transform.k;
479
+ draggedNode.vx = 0;
480
+ draggedNode.vy = 0;
481
+ updateTooltip(draggedNode, e.clientX, e.clientY);
482
+ if (!animationFrameId) render();
483
+ } else if (isPanning) {
484
+ transform.x = e.clientX - startX;
485
+ transform.y = e.clientY - startY;
486
+ if (!animationFrameId) render();
487
+ } else {
488
+ const mx = (e.clientX - rect.left - transform.x) / transform.k;
489
+ const my = (e.clientY - rect.top - transform.y) / transform.k;
490
+
491
+ const newHover = nodes.find(n => {
492
+ const dx = n.x - mx;
493
+ const dy = n.y - my;
494
+ return Math.sqrt(dx * dx + dy * dy) <= (n.r + 4);
495
+ });
496
+
497
+ if (newHover !== hoveredNode) {
498
+ hoveredNode = newHover || null;
499
+ canvas.style.cursor = hoveredNode ? 'pointer' : 'grab';
500
+ updateTooltip(hoveredNode, e.clientX, e.clientY);
501
+ if (!animationFrameId) render();
502
+ } else if (hoveredNode) {
503
+ updateTooltip(hoveredNode, e.clientX, e.clientY);
504
+ }
505
+ }
506
+ });
507
+
508
+ window.addEventListener('mouseup', () => {
509
+ draggedNode = null;
510
+ isPanning = false;
511
+ });
512
+
513
+ canvas.addEventListener('wheel', e => {
514
+ e.preventDefault();
515
+ const rect = canvas.getBoundingClientRect();
516
+ const mx = e.clientX - rect.left;
517
+ const my = e.clientY - rect.top;
518
+
519
+ const zoomFactor = e.deltaY < 0 ? 1.15 : 0.87;
520
+ const newK = Math.max(0.1, Math.min(5.0, transform.k * zoomFactor));
521
+
522
+ transform.x = mx - (mx - transform.x) * (newK / transform.k);
523
+ transform.y = my - (my - transform.y) * (newK / transform.k);
524
+ transform.k = newK;
525
+
526
+ if (!animationFrameId) render();
527
+ }, { passive: false });
528
+
529
+ canvas.addEventListener('click', e => {
530
+ const now = Date.now();
531
+ const isDouble = now - lastClickTime < 300;
532
+ lastClickTime = now;
533
+
534
+ if (hoveredNode) {
535
+ if (isDouble) {
536
+ flyToNode(hoveredNode);
537
+ } else {
538
+ selectedNode = hoveredNode;
539
+ canvas.dispatchEvent(new CustomEvent('graph-node-click', {
540
+ detail: { path: hoveredNode.path },
541
+ bubbles: true
542
+ }));
543
+ }
544
+ } else {
545
+ selectedNode = null;
546
+ if (!animationFrameId) render();
547
+ }
548
+ });
549
+
550
+ canvas.addEventListener('mouseleave', () => {
551
+ hoveredNode = null;
552
+ updateTooltip(null);
553
+ if (!animationFrameId) render();
554
+ });
555
+
556
+ // Wire Controls HUD
557
+ const resetBtn = document.getElementById('graphReset');
558
+ const zoomInBtn = document.getElementById('graphZoomIn');
559
+ const zoomOutBtn = document.getElementById('graphZoomOut');
560
+
561
+ if (resetBtn) resetBtn.onclick = () => resetLayout();
562
+ if (zoomInBtn) zoomInBtn.onclick = () => {
563
+ const newK = Math.min(5.0, transform.k * 1.25);
564
+ transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
565
+ transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
566
+ transform.k = newK;
567
+ if (!animationFrameId) render();
568
+ };
569
+ if (zoomOutBtn) zoomOutBtn.onclick = () => {
570
+ const newK = Math.max(0.1, transform.k * 0.8);
571
+ transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
572
+ transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
573
+ transform.k = newK;
574
+ if (!animationFrameId) render();
575
+ };
576
+
577
+ window.addEventListener('resize', resize);
578
+ setTimeout(resize, 50);
579
+
580
+ return {
581
+ update,
582
+ resize,
583
+ fit: fitToScreen,
584
+ reset: resetLayout,
585
+ search: q => { searchQuery = q; if (!animationFrameId) render(); },
586
+ highlight: path => { highlightPath = path; if (!animationFrameId) render(); },
587
+ destroy: () => {
588
+ window.removeEventListener('resize', resize);
589
+ if (animationFrameId) cancelAnimationFrame(animationFrameId);
590
+ }
591
+ };
592
+ }