clawvault 3.1.0 → 3.2.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 (289) hide show
  1. package/README.md +422 -141
  2. package/bin/clawvault.js +10 -2
  3. package/bin/command-registration.test.js +3 -1
  4. package/bin/command-runtime.js +9 -1
  5. package/bin/register-core-commands.js +23 -28
  6. package/bin/register-maintenance-commands.js +39 -3
  7. package/bin/register-query-commands.js +58 -29
  8. package/bin/register-tailscale-commands.js +106 -0
  9. package/bin/register-task-commands.js +18 -1
  10. package/bin/register-task-commands.test.js +16 -0
  11. package/bin/register-vault-operations-commands.js +29 -1
  12. package/bin/register-workgraph-commands.js +1368 -0
  13. package/dashboard/lib/graph-diff.js +104 -0
  14. package/dashboard/lib/graph-diff.test.js +75 -0
  15. package/dashboard/lib/vault-parser.js +556 -0
  16. package/dashboard/lib/vault-parser.test.js +254 -0
  17. package/dashboard/public/app.js +796 -0
  18. package/dashboard/public/index.html +52 -0
  19. package/dashboard/public/styles.css +221 -0
  20. package/dashboard/server.js +374 -0
  21. package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
  22. package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
  23. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  24. package/dist/chunk-2ZDO52B4.js +52 -0
  25. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  26. package/dist/chunk-33VSQP4J.js +37 -0
  27. package/dist/chunk-4BQTQMJP.js +93 -0
  28. package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
  29. package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
  30. package/dist/chunk-6FH3IULF.js +352 -0
  31. package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
  32. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  33. package/dist/chunk-BSJ6RIT7.js +447 -0
  34. package/dist/chunk-BUEW6IIK.js +364 -0
  35. package/dist/{chunk-LI4O6NVK.js → chunk-CLJTREDS.js} +74 -14
  36. package/dist/chunk-EK6S23ZB.js +469 -0
  37. package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
  38. package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
  39. package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
  40. package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
  41. package/dist/{chunk-H62BP7RI.js → chunk-GAOWA7GR.js} +212 -46
  42. package/dist/chunk-GGA32J2R.js +784 -0
  43. package/dist/chunk-GNJL4YGR.js +79 -0
  44. package/dist/chunk-IVRIKYFE.js +520 -0
  45. package/dist/chunk-MDIH26GC.js +183 -0
  46. package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
  47. package/dist/chunk-MM6QGW3P.js +207 -0
  48. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  49. package/dist/chunk-NCKFNBHJ.js +257 -0
  50. package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
  51. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  52. package/dist/chunk-PBACDKKP.js +66 -0
  53. package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
  54. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  55. package/dist/chunk-QVEERJSP.js +152 -0
  56. package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
  57. package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
  58. package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
  59. package/dist/chunk-SS4B7P7V.js +99 -0
  60. package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
  61. package/dist/chunk-TIGW564L.js +628 -0
  62. package/dist/chunk-U4O6C46S.js +154 -0
  63. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  64. package/dist/chunk-VSL7KY3M.js +189 -0
  65. package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
  66. package/dist/chunk-WMGIIABP.js +15 -0
  67. package/dist/{chunk-33UGEQRT.js → chunk-X3SPPUFG.js} +151 -64
  68. package/dist/chunk-Y6VJKXGL.js +373 -0
  69. package/dist/{chunk-3WRJEKN4.js → chunk-ZN54U2OZ.js} +123 -10
  70. package/dist/cli/index.js +34 -24
  71. package/dist/commands/archive.js +3 -3
  72. package/dist/commands/backlog.js +3 -3
  73. package/dist/commands/blocked.js +3 -3
  74. package/dist/commands/canvas.d.ts +15 -0
  75. package/dist/commands/canvas.js +200 -0
  76. package/dist/commands/checkpoint.js +2 -2
  77. package/dist/commands/compat.js +2 -2
  78. package/dist/commands/context.js +8 -6
  79. package/dist/commands/doctor.d.ts +11 -7
  80. package/dist/commands/doctor.js +18 -16
  81. package/dist/commands/embed.js +5 -6
  82. package/dist/commands/entities.js +2 -2
  83. package/dist/commands/graph.js +4 -4
  84. package/dist/commands/inject.d.ts +1 -1
  85. package/dist/commands/inject.js +5 -6
  86. package/dist/commands/kanban.js +4 -4
  87. package/dist/commands/link.js +5 -5
  88. package/dist/commands/migrate-observations.js +4 -4
  89. package/dist/commands/observe.d.ts +0 -1
  90. package/dist/commands/observe.js +14 -13
  91. package/dist/commands/project.js +5 -5
  92. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  93. package/dist/commands/rebuild-embeddings.js +91 -0
  94. package/dist/commands/rebuild.js +12 -11
  95. package/dist/commands/recover.js +3 -3
  96. package/dist/commands/reflect.js +6 -7
  97. package/dist/commands/repair-session.js +1 -1
  98. package/dist/commands/replay.js +14 -14
  99. package/dist/commands/session-recap.js +1 -1
  100. package/dist/commands/setup.d.ts +2 -89
  101. package/dist/commands/setup.js +3 -21
  102. package/dist/commands/shell-init.js +1 -1
  103. package/dist/commands/sleep.d.ts +1 -1
  104. package/dist/commands/sleep.js +20 -19
  105. package/dist/commands/status.d.ts +2 -0
  106. package/dist/commands/status.js +57 -35
  107. package/dist/commands/sync-bd.d.ts +10 -0
  108. package/dist/commands/sync-bd.js +10 -0
  109. package/dist/commands/tailscale.d.ts +52 -0
  110. package/dist/commands/tailscale.js +26 -0
  111. package/dist/commands/task.js +4 -4
  112. package/dist/commands/template.js +2 -2
  113. package/dist/commands/wake.d.ts +1 -1
  114. package/dist/commands/wake.js +11 -10
  115. package/dist/commands/workgraph.d.ts +124 -0
  116. package/dist/commands/workgraph.js +38 -0
  117. package/dist/index.d.ts +341 -191
  118. package/dist/index.js +446 -116
  119. package/dist/{inject-Bzi5E-By.d.ts → inject-DYUrDqQO.d.ts} +3 -3
  120. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  121. package/dist/lib/auto-linker.js +2 -2
  122. package/dist/lib/canvas-layout.d.ts +115 -0
  123. package/dist/lib/canvas-layout.js +35 -0
  124. package/dist/lib/config.d.ts +27 -3
  125. package/dist/lib/config.js +4 -2
  126. package/dist/lib/entity-index.js +1 -1
  127. package/dist/lib/project-utils.js +4 -4
  128. package/dist/lib/session-repair.js +1 -1
  129. package/dist/lib/session-utils.js +1 -1
  130. package/dist/lib/tailscale.d.ts +225 -0
  131. package/dist/lib/tailscale.js +50 -0
  132. package/dist/lib/task-utils.js +3 -3
  133. package/dist/lib/template-engine.js +1 -1
  134. package/dist/lib/webdav.d.ts +109 -0
  135. package/dist/lib/webdav.js +35 -0
  136. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  137. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  138. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  139. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  140. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  141. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  142. package/dist/openclaw-plugin.d.ts +8 -0
  143. package/dist/openclaw-plugin.js +14 -0
  144. package/dist/registry-BR4326o0.d.ts +30 -0
  145. package/dist/store-CA-6sKCJ.d.ts +34 -0
  146. package/dist/thread-B9LhXNU0.d.ts +41 -0
  147. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  148. package/dist/{types-Y2_Um2Ls.d.ts → types-BbWJoC1c.d.ts} +1 -44
  149. package/dist/workgraph/index.d.ts +5 -0
  150. package/dist/workgraph/index.js +23 -0
  151. package/dist/workgraph/ledger.d.ts +2 -0
  152. package/dist/workgraph/ledger.js +25 -0
  153. package/dist/workgraph/registry.d.ts +2 -0
  154. package/dist/workgraph/registry.js +19 -0
  155. package/dist/workgraph/store.d.ts +2 -0
  156. package/dist/workgraph/store.js +25 -0
  157. package/dist/workgraph/thread.d.ts +2 -0
  158. package/dist/workgraph/thread.js +25 -0
  159. package/dist/workgraph/types.d.ts +54 -0
  160. package/dist/workgraph/types.js +7 -0
  161. package/hooks/clawvault/HOOK.md +113 -0
  162. package/hooks/clawvault/handler.js +1561 -0
  163. package/hooks/clawvault/handler.test.js +510 -0
  164. package/hooks/clawvault/openclaw.plugin.json +72 -0
  165. package/openclaw.plugin.json +65 -38
  166. package/package.json +25 -22
  167. package/dist/chunk-3RG5ZIWI.js +0 -10
  168. package/dist/chunk-3ZIH425O.js +0 -871
  169. package/dist/chunk-6U6MK36V.js +0 -205
  170. package/dist/chunk-CMB7UL7C.js +0 -327
  171. package/dist/chunk-D2H45LON.js +0 -1074
  172. package/dist/chunk-E7MFQB6D.js +0 -163
  173. package/dist/chunk-GQSLDZTS.js +0 -560
  174. package/dist/chunk-MFM6K7PU.js +0 -374
  175. package/dist/chunk-MXSSG3QU.js +0 -42
  176. package/dist/chunk-OCGVIN3L.js +0 -88
  177. package/dist/chunk-PAH27GSN.js +0 -108
  178. package/dist/chunk-YCUNCH2I.js +0 -78
  179. package/dist/cli/index.cjs +0 -8584
  180. package/dist/cli/index.d.cts +0 -5
  181. package/dist/commands/archive.cjs +0 -287
  182. package/dist/commands/archive.d.cts +0 -11
  183. package/dist/commands/backlog.cjs +0 -721
  184. package/dist/commands/backlog.d.cts +0 -53
  185. package/dist/commands/blocked.cjs +0 -204
  186. package/dist/commands/blocked.d.cts +0 -26
  187. package/dist/commands/checkpoint.cjs +0 -244
  188. package/dist/commands/checkpoint.d.cts +0 -41
  189. package/dist/commands/compat.cjs +0 -294
  190. package/dist/commands/compat.d.cts +0 -28
  191. package/dist/commands/context.cjs +0 -2990
  192. package/dist/commands/context.d.cts +0 -2
  193. package/dist/commands/doctor.cjs +0 -2986
  194. package/dist/commands/doctor.d.cts +0 -21
  195. package/dist/commands/embed.cjs +0 -232
  196. package/dist/commands/embed.d.cts +0 -17
  197. package/dist/commands/entities.cjs +0 -141
  198. package/dist/commands/entities.d.cts +0 -7
  199. package/dist/commands/graph.cjs +0 -501
  200. package/dist/commands/graph.d.cts +0 -21
  201. package/dist/commands/inject.cjs +0 -1636
  202. package/dist/commands/inject.d.cts +0 -2
  203. package/dist/commands/kanban.cjs +0 -884
  204. package/dist/commands/kanban.d.cts +0 -63
  205. package/dist/commands/link.cjs +0 -965
  206. package/dist/commands/link.d.cts +0 -11
  207. package/dist/commands/migrate-observations.cjs +0 -362
  208. package/dist/commands/migrate-observations.d.cts +0 -19
  209. package/dist/commands/observe.cjs +0 -4099
  210. package/dist/commands/observe.d.cts +0 -23
  211. package/dist/commands/project.cjs +0 -1341
  212. package/dist/commands/project.d.cts +0 -85
  213. package/dist/commands/rebuild.cjs +0 -3136
  214. package/dist/commands/rebuild.d.cts +0 -11
  215. package/dist/commands/recover.cjs +0 -361
  216. package/dist/commands/recover.d.cts +0 -38
  217. package/dist/commands/reflect.cjs +0 -1008
  218. package/dist/commands/reflect.d.cts +0 -11
  219. package/dist/commands/repair-session.cjs +0 -457
  220. package/dist/commands/repair-session.d.cts +0 -38
  221. package/dist/commands/replay.cjs +0 -4103
  222. package/dist/commands/replay.d.cts +0 -16
  223. package/dist/commands/session-recap.cjs +0 -353
  224. package/dist/commands/session-recap.d.cts +0 -27
  225. package/dist/commands/setup.cjs +0 -1278
  226. package/dist/commands/setup.d.cts +0 -99
  227. package/dist/commands/shell-init.cjs +0 -75
  228. package/dist/commands/shell-init.d.cts +0 -7
  229. package/dist/commands/sleep.cjs +0 -6029
  230. package/dist/commands/sleep.d.cts +0 -36
  231. package/dist/commands/status.cjs +0 -2737
  232. package/dist/commands/status.d.cts +0 -52
  233. package/dist/commands/task.cjs +0 -1236
  234. package/dist/commands/task.d.cts +0 -97
  235. package/dist/commands/template.cjs +0 -457
  236. package/dist/commands/template.d.cts +0 -36
  237. package/dist/commands/wake.cjs +0 -2627
  238. package/dist/commands/wake.d.cts +0 -22
  239. package/dist/context-BUGaWpyL.d.cts +0 -46
  240. package/dist/index.cjs +0 -12373
  241. package/dist/index.d.cts +0 -854
  242. package/dist/inject-Bzi5E-By.d.cts +0 -137
  243. package/dist/lib/auto-linker.cjs +0 -176
  244. package/dist/lib/auto-linker.d.cts +0 -26
  245. package/dist/lib/config.cjs +0 -78
  246. package/dist/lib/config.d.cts +0 -11
  247. package/dist/lib/entity-index.cjs +0 -84
  248. package/dist/lib/entity-index.d.cts +0 -26
  249. package/dist/lib/project-utils.cjs +0 -864
  250. package/dist/lib/project-utils.d.cts +0 -97
  251. package/dist/lib/session-repair.cjs +0 -239
  252. package/dist/lib/session-repair.d.cts +0 -110
  253. package/dist/lib/session-utils.cjs +0 -209
  254. package/dist/lib/session-utils.d.cts +0 -63
  255. package/dist/lib/task-utils.cjs +0 -1137
  256. package/dist/lib/task-utils.d.cts +0 -208
  257. package/dist/lib/template-engine.cjs +0 -47
  258. package/dist/lib/template-engine.d.cts +0 -11
  259. package/dist/plugin/index.cjs +0 -1907
  260. package/dist/plugin/index.d.cts +0 -36
  261. package/dist/plugin/index.d.ts +0 -36
  262. package/dist/plugin/index.js +0 -572
  263. package/dist/plugin/inject.cjs +0 -356
  264. package/dist/plugin/inject.d.cts +0 -54
  265. package/dist/plugin/inject.d.ts +0 -54
  266. package/dist/plugin/inject.js +0 -17
  267. package/dist/plugin/observe.cjs +0 -631
  268. package/dist/plugin/observe.d.cts +0 -39
  269. package/dist/plugin/observe.d.ts +0 -39
  270. package/dist/plugin/observe.js +0 -18
  271. package/dist/plugin/templates.cjs +0 -593
  272. package/dist/plugin/templates.d.cts +0 -52
  273. package/dist/plugin/templates.d.ts +0 -52
  274. package/dist/plugin/templates.js +0 -25
  275. package/dist/plugin/types.cjs +0 -18
  276. package/dist/plugin/types.d.cts +0 -209
  277. package/dist/plugin/types.d.ts +0 -209
  278. package/dist/plugin/types.js +0 -0
  279. package/dist/plugin/vault.cjs +0 -927
  280. package/dist/plugin/vault.d.cts +0 -68
  281. package/dist/plugin/vault.d.ts +0 -68
  282. package/dist/plugin/vault.js +0 -22
  283. package/dist/types-Y2_Um2Ls.d.cts +0 -205
  284. package/templates/memory-event.md +0 -67
  285. package/templates/party.md +0 -63
  286. package/templates/primitive-registry.yaml +0 -551
  287. package/templates/run.md +0 -68
  288. package/templates/trigger.md +0 -68
  289. package/templates/workspace.md +0 -50
@@ -0,0 +1,796 @@
1
+ const CATEGORY_COLORS = {
2
+ decisions: '#ff8b6a',
3
+ lessons: '#7af5e9',
4
+ people: '#ff8ea9',
5
+ projects: '#9ec6ff',
6
+ commitments: '#ffe18d',
7
+ research: '#9bf7bd',
8
+ unresolved: '#ffb363',
9
+ root: '#b4bcd1',
10
+ default: '#9dadc5'
11
+ };
12
+
13
+ const DIMMED_NODE_COLOR = 'rgba(67, 85, 108, 0.45)';
14
+ const DIMMED_LINK_COLOR = 'rgba(117, 138, 166, 0.16)';
15
+ const NORMAL_LINK_COLOR = 'rgba(167, 189, 214, 0.34)';
16
+ const HIGHLIGHT_LINK_COLOR = 'rgba(239, 247, 255, 0.92)';
17
+
18
+ const FILTER_DEBOUNCE_MS = 120;
19
+ const GRAPH_UPDATE_THROTTLE_MS = 1_000;
20
+
21
+ const state = {
22
+ allNodeById: new Map(),
23
+ allEdgeByKey: new Map(),
24
+ neighborsByNodeId: new Map(),
25
+ linksByNodeId: new Map(),
26
+ searchTerm: '',
27
+ category: 'all',
28
+ tag: 'all',
29
+ nodeType: 'all',
30
+ visibleNodeIds: new Set(),
31
+ hoveredNodeId: null,
32
+ selectedNodeId: null,
33
+ highlightedNodeIds: new Set(),
34
+ highlightedEdgeKeys: new Set(),
35
+ stats: null,
36
+ wsVersion: 0,
37
+ wsConnected: false,
38
+ tvMode: false,
39
+ lastInteractionAt: performance.now(),
40
+ filterDebounce: null,
41
+ graphDataThrottleTimer: null,
42
+ pendingGraphData: null,
43
+ lastGraphDataApplyAt: 0
44
+ };
45
+
46
+ const graphElement = document.querySelector('#graph');
47
+ const detailsElement = document.querySelector('#node-details');
48
+ const statsElement = document.querySelector('#stats');
49
+ const searchElement = document.querySelector('#search');
50
+ const categoryFilterElement = document.querySelector('#category-filter');
51
+ const tagFilterElement = document.querySelector('#tag-filter');
52
+ const nodeTypeFilterElement = document.querySelector('#node-type-filter');
53
+ const refreshButtonElement = document.querySelector('#refresh');
54
+ const tvModeButtonElement = document.querySelector('#tv-mode');
55
+ const realtimeStatusElement = document.querySelector('#realtime-status');
56
+
57
+ if (typeof window.ForceGraph !== 'function') {
58
+ statsElement.textContent = 'ForceGraph failed to load.';
59
+ throw new Error('force-graph library unavailable');
60
+ }
61
+
62
+ const graph = window
63
+ .ForceGraph()(graphElement)
64
+ .backgroundColor('#070a10')
65
+ .nodeId('id')
66
+ .linkSource('source')
67
+ .linkTarget('target')
68
+ .nodeRelSize(4)
69
+ .d3AlphaDecay(0.018)
70
+ .d3VelocityDecay(0.24)
71
+ .cooldownTicks(90)
72
+ .linkColor((link) => getLinkColor(link))
73
+ .linkWidth((link) => getLinkWidth(link))
74
+ .linkDirectionalParticles(0)
75
+ .nodeCanvasObject((node, ctx, globalScale) => renderNode(node, ctx, globalScale))
76
+ .nodePointerAreaPaint((node, color, ctx) => {
77
+ ctx.fillStyle = color;
78
+ ctx.beginPath();
79
+ ctx.arc(node.x, node.y, getNodeRadius(node) + 3, 0, Math.PI * 2, false);
80
+ ctx.fill();
81
+ })
82
+ .onNodeHover((node) => {
83
+ const nextHoveredNodeId = node?.id ?? null;
84
+ if (nextHoveredNodeId === state.hoveredNodeId) {
85
+ return;
86
+ }
87
+ state.hoveredNodeId = nextHoveredNodeId;
88
+ markInteraction();
89
+ syncHighlights();
90
+ })
91
+ .onNodeClick((node) => {
92
+ markInteraction();
93
+ selectNode(node);
94
+ })
95
+ .onBackgroundClick(() => {
96
+ markInteraction();
97
+ state.selectedNodeId = null;
98
+ syncHighlights();
99
+ renderEmptyDetails();
100
+ });
101
+
102
+ resizeGraphToContainer();
103
+ window.addEventListener('resize', resizeGraphToContainer);
104
+
105
+ searchElement.addEventListener('input', (event) => {
106
+ state.searchTerm = String(event.target.value ?? '').trim().toLowerCase();
107
+ markInteraction();
108
+ scheduleFilterApply();
109
+ });
110
+
111
+ categoryFilterElement.addEventListener('change', (event) => {
112
+ state.category = String(event.target.value ?? 'all');
113
+ markInteraction();
114
+ applyFiltersAndRender({ shouldLazyLoad: false });
115
+ });
116
+
117
+ tagFilterElement.addEventListener('change', (event) => {
118
+ state.tag = String(event.target.value ?? 'all');
119
+ markInteraction();
120
+ applyFiltersAndRender({ shouldLazyLoad: false });
121
+ });
122
+
123
+ nodeTypeFilterElement.addEventListener('change', (event) => {
124
+ state.nodeType = String(event.target.value ?? 'all');
125
+ markInteraction();
126
+ applyFiltersAndRender({ shouldLazyLoad: false });
127
+ });
128
+
129
+ refreshButtonElement.addEventListener('click', async () => {
130
+ markInteraction();
131
+ await fetchSnapshot({ refresh: true });
132
+ });
133
+
134
+ tvModeButtonElement.addEventListener('click', () => {
135
+ markInteraction();
136
+ setTvMode(!state.tvMode, { updateQuery: true, useFullscreen: true });
137
+ });
138
+
139
+ detailsElement.addEventListener('click', (event) => {
140
+ const target = event.target;
141
+ if (!(target instanceof HTMLElement)) {
142
+ return;
143
+ }
144
+ const linkedNodeId = target.dataset.nodeId;
145
+ if (!linkedNodeId) {
146
+ return;
147
+ }
148
+ event.preventDefault();
149
+ const node = state.allNodeById.get(linkedNodeId);
150
+ if (node) {
151
+ markInteraction();
152
+ selectNode(node);
153
+ }
154
+ });
155
+
156
+ window.addEventListener('keydown', (event) => {
157
+ if (event.key.toLowerCase() === 't') {
158
+ setTvMode(!state.tvMode, { updateQuery: true, useFullscreen: true });
159
+ }
160
+ });
161
+
162
+ for (const eventName of ['pointermove', 'wheel', 'keydown']) {
163
+ window.addEventListener(eventName, () => {
164
+ markInteraction();
165
+ });
166
+ }
167
+
168
+ const initialParams = new URLSearchParams(window.location.search);
169
+ if (initialParams.get('tv') === '1') {
170
+ setTvMode(true, { updateQuery: false, useFullscreen: false });
171
+ }
172
+ if (initialParams.get('webgl') === '1') {
173
+ enableWebglRendererIfSupported();
174
+ }
175
+ if (initialParams.get('realtime') === '1') {
176
+ startRealtime();
177
+ } else {
178
+ setRealtimeStatus({ text: 'Realtime: disabled (stability mode)', level: 'warn' });
179
+ }
180
+
181
+ void fetchSnapshot();
182
+
183
+ function resizeGraphToContainer() {
184
+ graph.width(graphElement.clientWidth);
185
+ graph.height(graphElement.clientHeight);
186
+ }
187
+
188
+ async function fetchSnapshot({ refresh = false } = {}) {
189
+ refreshButtonElement.disabled = true;
190
+ statsElement.textContent = 'Loading graph...';
191
+ try {
192
+ const response = await fetch(refresh ? '/api/graph?refresh=1' : '/api/graph');
193
+ if (!response.ok) {
194
+ throw new Error(`HTTP ${response.status}`);
195
+ }
196
+ const payload = await response.json();
197
+ applySnapshot(payload, { shouldLazyLoad: true, shouldFit: true });
198
+ } catch (error) {
199
+ statsElement.textContent = `Failed to load graph: ${error instanceof Error ? error.message : String(error)}`;
200
+ } finally {
201
+ refreshButtonElement.disabled = false;
202
+ }
203
+ }
204
+
205
+ function applySnapshot(payload, { shouldLazyLoad, shouldFit }) {
206
+ const nextNodes = Array.isArray(payload?.nodes) ? payload.nodes : [];
207
+ const nextEdges = Array.isArray(payload?.edges) ? payload.edges : [];
208
+
209
+ state.allNodeById = new Map();
210
+ for (const node of nextNodes) {
211
+ state.allNodeById.set(node.id, { ...node });
212
+ }
213
+
214
+ state.allEdgeByKey = new Map();
215
+ for (const edge of nextEdges) {
216
+ const sourceId = String(edge.source);
217
+ const targetId = String(edge.target);
218
+ const edgeType = String(edge.type ?? '');
219
+ const edgeLabel = String(edge.label ?? '');
220
+ const edgeKey = toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
221
+ state.allEdgeByKey.set(edgeKey, {
222
+ key: edgeKey,
223
+ source: sourceId,
224
+ target: targetId,
225
+ type: edgeType,
226
+ label: edgeLabel
227
+ });
228
+ }
229
+
230
+ state.stats = payload?.stats ?? null;
231
+ rebuildConnectionIndexes();
232
+ populateFilters();
233
+ applyFiltersAndRender({ shouldLazyLoad });
234
+ syncHighlights();
235
+ if (shouldFit) {
236
+ graph.zoomToFit(700, 100);
237
+ }
238
+ if (state.selectedNodeId) {
239
+ const selectedNode = state.allNodeById.get(state.selectedNodeId);
240
+ if (selectedNode) {
241
+ renderDetails(selectedNode);
242
+ } else {
243
+ state.selectedNodeId = null;
244
+ renderEmptyDetails();
245
+ }
246
+ } else {
247
+ renderEmptyDetails();
248
+ }
249
+ updateStats();
250
+ }
251
+
252
+ function applyPatch(payload) {
253
+ const addedNodes = Array.isArray(payload?.addedNodes) ? payload.addedNodes : [];
254
+ const updatedNodes = Array.isArray(payload?.updatedNodes) ? payload.updatedNodes : [];
255
+ const removedNodeIds = Array.isArray(payload?.removedNodeIds) ? payload.removedNodeIds : [];
256
+ const addedEdges = Array.isArray(payload?.addedEdges) ? payload.addedEdges : [];
257
+ const removedEdges = Array.isArray(payload?.removedEdges) ? payload.removedEdges : [];
258
+
259
+ for (const nodeId of removedNodeIds) {
260
+ state.allNodeById.delete(nodeId);
261
+ }
262
+
263
+ for (const node of addedNodes) {
264
+ state.allNodeById.set(node.id, { ...node });
265
+ }
266
+
267
+ for (const patchNode of updatedNodes) {
268
+ const existingNode = state.allNodeById.get(patchNode.id);
269
+ if (existingNode) {
270
+ Object.assign(existingNode, patchNode);
271
+ } else {
272
+ state.allNodeById.set(patchNode.id, { ...patchNode });
273
+ }
274
+ }
275
+
276
+ for (const edge of removedEdges) {
277
+ const sourceId = String(edge.source);
278
+ const targetId = String(edge.target);
279
+ const edgeType = String(edge.type ?? '');
280
+ const edgeLabel = String(edge.label ?? '');
281
+ state.allEdgeByKey.delete(toEdgeKey(sourceId, targetId, edgeType, edgeLabel));
282
+ }
283
+
284
+ for (const edge of addedEdges) {
285
+ const sourceId = String(edge.source);
286
+ const targetId = String(edge.target);
287
+ const edgeType = String(edge.type ?? '');
288
+ const edgeLabel = String(edge.label ?? '');
289
+ const key = toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
290
+ state.allEdgeByKey.set(key, { key, source: sourceId, target: targetId, type: edgeType, label: edgeLabel });
291
+ }
292
+
293
+ if (payload?.stats) {
294
+ state.stats = payload.stats;
295
+ }
296
+
297
+ if (removedNodeIds.includes(state.selectedNodeId)) {
298
+ state.selectedNodeId = null;
299
+ renderEmptyDetails();
300
+ }
301
+
302
+ rebuildConnectionIndexes();
303
+ populateFilters();
304
+ applyFiltersAndRender({ shouldLazyLoad: false });
305
+ syncHighlights();
306
+ const selectedNode = state.selectedNodeId ? state.allNodeById.get(state.selectedNodeId) : null;
307
+ if (selectedNode) {
308
+ renderDetails(selectedNode);
309
+ }
310
+ updateStats();
311
+ }
312
+
313
+ function rebuildConnectionIndexes() {
314
+ state.neighborsByNodeId = new Map();
315
+ state.linksByNodeId = new Map();
316
+
317
+ for (const node of state.allNodeById.values()) {
318
+ state.neighborsByNodeId.set(node.id, new Set());
319
+ state.linksByNodeId.set(node.id, new Set());
320
+ }
321
+
322
+ for (const edge of state.allEdgeByKey.values()) {
323
+ state.neighborsByNodeId.get(edge.source)?.add(edge.target);
324
+ state.neighborsByNodeId.get(edge.target)?.add(edge.source);
325
+ state.linksByNodeId.get(edge.source)?.add(edge.key);
326
+ state.linksByNodeId.get(edge.target)?.add(edge.key);
327
+ }
328
+ }
329
+
330
+ function applyFiltersAndRender({ shouldLazyLoad }) {
331
+ const filteredNodes = [];
332
+ for (const node of state.allNodeById.values()) {
333
+ if (isNodeVisible(node)) {
334
+ filteredNodes.push(node);
335
+ }
336
+ }
337
+
338
+ const visibleNodeIds = new Set(filteredNodes.map((node) => node.id));
339
+ const filteredLinks = [];
340
+ for (const edge of state.allEdgeByKey.values()) {
341
+ if (visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target)) {
342
+ filteredLinks.push({
343
+ source: edge.source,
344
+ target: edge.target,
345
+ type: edge.type,
346
+ label: edge.label,
347
+ _key: edge.key
348
+ });
349
+ }
350
+ }
351
+
352
+ state.visibleNodeIds = visibleNodeIds;
353
+ renderGraph(filteredNodes, filteredLinks, { shouldLazyLoad });
354
+ updateStats();
355
+ }
356
+
357
+ function renderGraph(nodes, links, { shouldLazyLoad }) {
358
+ void shouldLazyLoad;
359
+ queueGraphDataUpdate(nodes, links);
360
+ }
361
+
362
+ function scheduleFilterApply() {
363
+ if (state.filterDebounce) {
364
+ clearTimeout(state.filterDebounce);
365
+ }
366
+ state.filterDebounce = setTimeout(() => {
367
+ state.filterDebounce = null;
368
+ applyFiltersAndRender({ shouldLazyLoad: false });
369
+ }, FILTER_DEBOUNCE_MS);
370
+ }
371
+
372
+ function syncHighlights() {
373
+ const nextHighlightedNodeIds = new Set();
374
+ const nextHighlightedEdgeKeys = new Set();
375
+ const focusNodeId = state.selectedNodeId ?? state.hoveredNodeId;
376
+ if (!focusNodeId || !state.visibleNodeIds.has(focusNodeId)) {
377
+ if (state.highlightedNodeIds.size === 0 && state.highlightedEdgeKeys.size === 0) {
378
+ return;
379
+ }
380
+ state.highlightedNodeIds = nextHighlightedNodeIds;
381
+ state.highlightedEdgeKeys = nextHighlightedEdgeKeys;
382
+ graph.refresh();
383
+ return;
384
+ }
385
+
386
+ nextHighlightedNodeIds.add(focusNodeId);
387
+ for (const neighborNodeId of state.neighborsByNodeId.get(focusNodeId) ?? []) {
388
+ if (state.visibleNodeIds.has(neighborNodeId)) {
389
+ nextHighlightedNodeIds.add(neighborNodeId);
390
+ }
391
+ }
392
+ for (const edgeKey of state.linksByNodeId.get(focusNodeId) ?? []) {
393
+ nextHighlightedEdgeKeys.add(edgeKey);
394
+ }
395
+ if (
396
+ areSetsEqual(state.highlightedNodeIds, nextHighlightedNodeIds) &&
397
+ areSetsEqual(state.highlightedEdgeKeys, nextHighlightedEdgeKeys)
398
+ ) {
399
+ return;
400
+ }
401
+ state.highlightedNodeIds = nextHighlightedNodeIds;
402
+ state.highlightedEdgeKeys = nextHighlightedEdgeKeys;
403
+ graph.refresh();
404
+ }
405
+
406
+ function selectNode(node) {
407
+ state.selectedNodeId = node.id;
408
+ syncHighlights();
409
+ renderDetails(node);
410
+ focusNode(node, { zoom: state.tvMode ? 2.4 : 3.9, durationMs: 520 });
411
+ }
412
+
413
+ function focusNode(node, { zoom, durationMs }) {
414
+ graph.centerAt(node.x ?? 0, node.y ?? 0, durationMs);
415
+ graph.zoom(zoom, durationMs);
416
+ }
417
+
418
+ function renderEmptyDetails() {
419
+ detailsElement.innerHTML = '<p>Select a node to inspect details and connections.</p>';
420
+ }
421
+
422
+ function renderDetails(node) {
423
+ const neighbors = Array.from(state.neighborsByNodeId.get(node.id) ?? [])
424
+ .map((neighborId) => state.allNodeById.get(neighborId))
425
+ .filter(Boolean)
426
+ .sort((a, b) => a.title.localeCompare(b.title));
427
+
428
+ const tags = Array.isArray(node.tags) && node.tags.length > 0 ? node.tags.join(', ') : 'none';
429
+ const category = node.category || 'default';
430
+ const degree = Number(node.degree ?? neighbors.length);
431
+ const pathValue = node.path ?? '(unresolved link target)';
432
+ const nodeType = node.missing ? 'missing' : 'resolved';
433
+
434
+ const connectionItems = neighbors.length
435
+ ? neighbors
436
+ .map((neighbor) => {
437
+ const color = colorForCategory(neighbor.category);
438
+ return `<li><a href="#" class="connection-link" data-node-id="${escapeHtml(neighbor.id)}" style="color:${color}">${escapeHtml(neighbor.title)}</a></li>`;
439
+ })
440
+ .join('')
441
+ : '<li>No direct connections</li>';
442
+
443
+ detailsElement.innerHTML = `
444
+ <div class="meta-label">Title</div>
445
+ <p class="meta-value">${escapeHtml(node.title)}</p>
446
+ <div class="meta-label">ID</div>
447
+ <p class="meta-value">${escapeHtml(node.id)}</p>
448
+ <div class="meta-label">Category</div>
449
+ <p class="meta-value">${escapeHtml(category)}</p>
450
+ <div class="meta-label">Type</div>
451
+ <p class="meta-value">${nodeType}</p>
452
+ <div class="meta-label">Tags</div>
453
+ <p class="meta-value">${escapeHtml(tags)}</p>
454
+ <div class="meta-label">Degree</div>
455
+ <p class="meta-value">${degree}</p>
456
+ <div class="meta-label">Path</div>
457
+ <p class="meta-value">${escapeHtml(pathValue)}</p>
458
+ <div class="meta-label">Connections (${neighbors.length})</div>
459
+ <ul class="connection-list">${connectionItems}</ul>
460
+ `;
461
+ }
462
+
463
+ function updateStats() {
464
+ const totalNodes = state.stats?.nodeCount ?? state.allNodeById.size;
465
+ const totalLinks = state.stats?.edgeCount ?? state.allEdgeByKey.size;
466
+ const totalFiles = state.stats?.fileCount ?? totalNodes;
467
+ const visibleNodes = state.visibleNodeIds.size;
468
+ const label = `${visibleNodes}/${totalNodes} nodes • ${totalLinks} links • ${totalFiles} files`;
469
+ statsElement.textContent = state.wsVersion > 0 ? `${label} • live v${state.wsVersion}` : label;
470
+ }
471
+
472
+ function populateFilters() {
473
+ const currentCategory = state.category;
474
+ const currentTag = state.tag;
475
+
476
+ const categories = new Set(['all']);
477
+ const tags = new Set(['all']);
478
+ for (const node of state.allNodeById.values()) {
479
+ categories.add(node.category || 'default');
480
+ for (const tag of Array.isArray(node.tags) ? node.tags : []) {
481
+ if (tag) {
482
+ tags.add(tag);
483
+ }
484
+ }
485
+ }
486
+
487
+ setSelectOptions(categoryFilterElement, Array.from(categories).sort(sortFilterOption), currentCategory, 'All categories');
488
+ setSelectOptions(tagFilterElement, Array.from(tags).sort(sortFilterOption), currentTag, 'All tags');
489
+ }
490
+
491
+ function sortFilterOption(a, b) {
492
+ if (a === 'all') return -1;
493
+ if (b === 'all') return 1;
494
+ return a.localeCompare(b);
495
+ }
496
+
497
+ function setSelectOptions(selectElement, values, currentValue, allLabel) {
498
+ selectElement.innerHTML = values
499
+ .map((value) => `<option value="${escapeHtml(value)}">${escapeHtml(value === 'all' ? allLabel : value)}</option>`)
500
+ .join('');
501
+
502
+ if (values.includes(currentValue)) {
503
+ selectElement.value = currentValue;
504
+ } else {
505
+ selectElement.value = 'all';
506
+ if (selectElement === categoryFilterElement) {
507
+ state.category = 'all';
508
+ } else if (selectElement === tagFilterElement) {
509
+ state.tag = 'all';
510
+ }
511
+ }
512
+ }
513
+
514
+ function isNodeVisible(node) {
515
+ const matchesCategory = state.category === 'all' || (node.category || 'default') === state.category;
516
+ if (!matchesCategory) {
517
+ return false;
518
+ }
519
+
520
+ const matchesTag = state.tag === 'all' || (Array.isArray(node.tags) && node.tags.includes(state.tag));
521
+ if (!matchesTag) {
522
+ return false;
523
+ }
524
+
525
+ const isMissing = Boolean(node.missing);
526
+ if (state.nodeType === 'missing' && !isMissing) {
527
+ return false;
528
+ }
529
+ if (state.nodeType === 'resolved' && isMissing) {
530
+ return false;
531
+ }
532
+
533
+ if (!state.searchTerm) {
534
+ return true;
535
+ }
536
+
537
+ const haystack = [node.id, node.title, node.category, Array.isArray(node.tags) ? node.tags.join(' ') : '']
538
+ .join(' ')
539
+ .toLowerCase();
540
+ return haystack.includes(state.searchTerm);
541
+ }
542
+
543
+ function renderNode(node, ctx, globalScale) {
544
+ void globalScale;
545
+ const radius = getNodeRadius(node);
546
+ const nodeId = node.id;
547
+ const isFocused = state.selectedNodeId === nodeId || state.hoveredNodeId === nodeId;
548
+ const isHighlighted = state.highlightedNodeIds.has(nodeId);
549
+ const hasFocusContext = Boolean(state.selectedNodeId || state.hoveredNodeId);
550
+ const baseColor = getNodeColor(node);
551
+ const finalColor = hasFocusContext && !isHighlighted ? DIMMED_NODE_COLOR : baseColor;
552
+
553
+ ctx.beginPath();
554
+ ctx.arc(node.x, node.y, radius + (isFocused ? 1.2 : 0), 0, Math.PI * 2, false);
555
+ ctx.fillStyle = finalColor;
556
+ ctx.fill();
557
+ }
558
+
559
+ function getNodeRadius(node) {
560
+ const degree = Number(node.degree ?? 0);
561
+ return 2.7 + Math.min(7.6, Math.sqrt(degree + 1) * 1.08);
562
+ }
563
+
564
+ function getNodeColor(node) {
565
+ if (node.missing) {
566
+ return '#ffc58b';
567
+ }
568
+ if (state.highlightedNodeIds.has(node.id)) {
569
+ return '#f3faff';
570
+ }
571
+ return colorForCategory(node.category);
572
+ }
573
+
574
+ function getLinkColor(link) {
575
+ const key = linkKey(link);
576
+ if (state.highlightedEdgeKeys.has(key)) {
577
+ return HIGHLIGHT_LINK_COLOR;
578
+ }
579
+ if (state.selectedNodeId || state.hoveredNodeId) {
580
+ return DIMMED_LINK_COLOR;
581
+ }
582
+ return NORMAL_LINK_COLOR;
583
+ }
584
+
585
+ function getLinkWidth(link) {
586
+ const key = linkKey(link);
587
+ if (state.highlightedEdgeKeys.has(key)) {
588
+ return 2.1;
589
+ }
590
+ return 1;
591
+ }
592
+
593
+ function linkKey(link) {
594
+ if (link?._key) {
595
+ return link._key;
596
+ }
597
+ const sourceId = typeof link.source === 'object' ? link.source.id : String(link.source);
598
+ const targetId = typeof link.target === 'object' ? link.target.id : String(link.target);
599
+ const edgeType = String(link.type ?? '');
600
+ const edgeLabel = String(link.label ?? '');
601
+ return toEdgeKey(sourceId, targetId, edgeType, edgeLabel);
602
+ }
603
+
604
+ function colorForCategory(category) {
605
+ return CATEGORY_COLORS[category] ?? CATEGORY_COLORS.default;
606
+ }
607
+
608
+ function startRealtime() {
609
+ let reconnectDelayMs = 800;
610
+ let socket = null;
611
+ let reconnectTimer = null;
612
+
613
+ const scheduleReconnect = () => {
614
+ if (reconnectTimer) {
615
+ return;
616
+ }
617
+ reconnectTimer = setTimeout(() => {
618
+ reconnectTimer = null;
619
+ connect();
620
+ }, reconnectDelayMs);
621
+ reconnectDelayMs = Math.min(10_000, Math.round(reconnectDelayMs * 1.8));
622
+ };
623
+
624
+ const connect = () => {
625
+ if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
626
+ return;
627
+ }
628
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
629
+ const activeSocket = new WebSocket(`${protocol}://${window.location.host}/ws`);
630
+ socket = activeSocket;
631
+ setRealtimeStatus({ text: 'Realtime: connecting...', level: 'warn' });
632
+
633
+ activeSocket.addEventListener('open', () => {
634
+ if (socket !== activeSocket) {
635
+ return;
636
+ }
637
+ reconnectDelayMs = 800;
638
+ state.wsConnected = true;
639
+ setRealtimeStatus({ text: 'Realtime: connected', level: 'ok' });
640
+ });
641
+
642
+ activeSocket.addEventListener('message', (event) => {
643
+ if (socket !== activeSocket) {
644
+ return;
645
+ }
646
+ const message = parseMessage(event.data);
647
+ if (!message) {
648
+ return;
649
+ }
650
+ if (message.type === 'graph:init') {
651
+ const version = Number(message.payload?.version ?? 0);
652
+ if (version >= state.wsVersion) {
653
+ state.wsVersion = version;
654
+ applySnapshot(message.payload?.graph ?? {}, { shouldLazyLoad: true, shouldFit: false });
655
+ updateStats();
656
+ }
657
+ return;
658
+ }
659
+ if (message.type === 'graph:patch') {
660
+ const version = Number(message.payload?.version ?? 0);
661
+ if (version <= state.wsVersion) {
662
+ return;
663
+ }
664
+ state.wsVersion = version;
665
+ applyPatch(message.payload);
666
+ setRealtimeStatus({ text: `Realtime: updated (${message.payload?.reason ?? 'patch'})`, level: 'ok' });
667
+ }
668
+ });
669
+
670
+ activeSocket.addEventListener('error', () => {});
671
+
672
+ activeSocket.addEventListener('close', () => {
673
+ if (socket !== activeSocket) {
674
+ return;
675
+ }
676
+ state.wsConnected = false;
677
+ setRealtimeStatus({ text: 'Realtime: reconnecting...', level: 'warn' });
678
+ socket = null;
679
+ scheduleReconnect();
680
+ });
681
+ };
682
+
683
+ connect();
684
+ }
685
+
686
+ function enableWebglRendererIfSupported() {
687
+ const renderers = window.ForceGraph?.renderers;
688
+ const webglRenderer = renderers?.webgl;
689
+ if (typeof graph.graphRenderer === 'function' && webglRenderer) {
690
+ graph.graphRenderer(webglRenderer);
691
+ }
692
+ }
693
+
694
+ function parseMessage(value) {
695
+ if (typeof value !== 'string') {
696
+ return null;
697
+ }
698
+ try {
699
+ return JSON.parse(value);
700
+ } catch (_error) {
701
+ return null;
702
+ }
703
+ }
704
+
705
+ function setRealtimeStatus({ text, level }) {
706
+ realtimeStatusElement.textContent = text;
707
+ realtimeStatusElement.classList.remove('ok', 'warn');
708
+ if (level) {
709
+ realtimeStatusElement.classList.add(level);
710
+ }
711
+ }
712
+
713
+ function setTvMode(enabled, { updateQuery, useFullscreen }) {
714
+ state.tvMode = enabled;
715
+ document.body.classList.toggle('tv-mode', enabled);
716
+ tvModeButtonElement.setAttribute('aria-pressed', enabled ? 'true' : 'false');
717
+ tvModeButtonElement.textContent = enabled ? 'Exit TV Mode' : 'TV Mode';
718
+ resizeGraphToContainer();
719
+ graph.refresh();
720
+
721
+ if (updateQuery) {
722
+ const url = new URL(window.location.href);
723
+ if (enabled) {
724
+ url.searchParams.set('tv', '1');
725
+ } else {
726
+ url.searchParams.delete('tv');
727
+ }
728
+ window.history.replaceState({}, '', url);
729
+ }
730
+
731
+ if (useFullscreen && enabled && document.fullscreenElement == null) {
732
+ void document.documentElement.requestFullscreen().catch(() => {});
733
+ }
734
+ if (useFullscreen && !enabled && document.fullscreenElement != null) {
735
+ void document.exitFullscreen().catch(() => {});
736
+ }
737
+ }
738
+
739
+ function markInteraction() {
740
+ state.lastInteractionAt = performance.now();
741
+ }
742
+
743
+ function queueGraphDataUpdate(nodes, links) {
744
+ state.pendingGraphData = { nodes, links };
745
+ const now = performance.now();
746
+ const elapsedMs = now - state.lastGraphDataApplyAt;
747
+ if (elapsedMs >= GRAPH_UPDATE_THROTTLE_MS && state.graphDataThrottleTimer == null) {
748
+ applyPendingGraphData();
749
+ return;
750
+ }
751
+ if (state.graphDataThrottleTimer != null) {
752
+ return;
753
+ }
754
+ const delayMs = Math.max(0, GRAPH_UPDATE_THROTTLE_MS - elapsedMs);
755
+ state.graphDataThrottleTimer = setTimeout(() => {
756
+ state.graphDataThrottleTimer = null;
757
+ applyPendingGraphData();
758
+ }, delayMs);
759
+ }
760
+
761
+ function applyPendingGraphData() {
762
+ if (!state.pendingGraphData) {
763
+ return;
764
+ }
765
+ const { nodes, links } = state.pendingGraphData;
766
+ state.pendingGraphData = null;
767
+ state.lastGraphDataApplyAt = performance.now();
768
+ graph.graphData({ nodes, links });
769
+ graph.d3ReheatSimulation();
770
+ syncHighlights();
771
+ }
772
+
773
+ function areSetsEqual(left, right) {
774
+ if (left.size !== right.size) {
775
+ return false;
776
+ }
777
+ for (const value of left) {
778
+ if (!right.has(value)) {
779
+ return false;
780
+ }
781
+ }
782
+ return true;
783
+ }
784
+
785
+ function toEdgeKey(sourceId, targetId, edgeType = '', edgeLabel = '') {
786
+ return `${sourceId}=>${targetId}:${edgeType}:${edgeLabel}`;
787
+ }
788
+
789
+ function escapeHtml(value) {
790
+ return String(value)
791
+ .replaceAll('&', '&amp;')
792
+ .replaceAll('<', '&lt;')
793
+ .replaceAll('>', '&gt;')
794
+ .replaceAll('"', '&quot;')
795
+ .replaceAll("'", '&#039;');
796
+ }