clawvault 3.0.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 (291) hide show
  1. package/README.md +352 -20
  2. package/bin/clawvault.js +8 -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 -10
  6. package/bin/register-maintenance-commands.js +39 -3
  7. package/bin/register-query-commands.js +58 -29
  8. package/bin/register-task-commands.js +18 -1
  9. package/bin/register-task-commands.test.js +16 -0
  10. package/bin/register-vault-operations-commands.js +29 -1
  11. package/bin/register-workgraph-commands.js +1368 -0
  12. package/dashboard/lib/graph-diff.js +104 -0
  13. package/dashboard/lib/graph-diff.test.js +75 -0
  14. package/dashboard/lib/vault-parser.js +556 -0
  15. package/dashboard/lib/vault-parser.test.js +254 -0
  16. package/dashboard/public/app.js +796 -0
  17. package/dashboard/public/index.html +52 -0
  18. package/dashboard/public/styles.css +221 -0
  19. package/dashboard/server.js +374 -0
  20. package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
  21. package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
  22. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  23. package/dist/chunk-2ZDO52B4.js +52 -0
  24. package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
  25. package/dist/chunk-33VSQP4J.js +37 -0
  26. package/dist/chunk-4BQTQMJP.js +93 -0
  27. package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
  28. package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
  29. package/dist/chunk-6FH3IULF.js +352 -0
  30. package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
  31. package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
  32. package/dist/chunk-BSJ6RIT7.js +447 -0
  33. package/dist/chunk-BUEW6IIK.js +364 -0
  34. package/dist/{chunk-WGRQ6HDV.js → chunk-CLJTREDS.js} +74 -14
  35. package/dist/chunk-EK6S23ZB.js +469 -0
  36. package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
  37. package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
  38. package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
  39. package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
  40. package/dist/{chunk-YKTA5JOJ.js → chunk-GAOWA7GR.js} +212 -46
  41. package/dist/chunk-GGA32J2R.js +784 -0
  42. package/dist/chunk-GNJL4YGR.js +79 -0
  43. package/dist/chunk-MDIH26GC.js +183 -0
  44. package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
  45. package/dist/chunk-MM6QGW3P.js +207 -0
  46. package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
  47. package/dist/chunk-NCKFNBHJ.js +257 -0
  48. package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
  49. package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
  50. package/dist/chunk-PBACDKKP.js +66 -0
  51. package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
  52. package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
  53. package/dist/chunk-QVEERJSP.js +152 -0
  54. package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
  55. package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
  56. package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
  57. package/dist/chunk-SS4B7P7V.js +99 -0
  58. package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
  59. package/dist/chunk-U4O6C46S.js +154 -0
  60. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  61. package/dist/chunk-VSL7KY3M.js +189 -0
  62. package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
  63. package/dist/chunk-WMGIIABP.js +15 -0
  64. package/dist/{chunk-3D6BCTP6.js → chunk-X3SPPUFG.js} +51 -39
  65. package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
  66. package/dist/{chunk-ZVVFWOLW.js → chunk-ZN54U2OZ.js} +123 -10
  67. package/dist/cli/index.js +32 -25
  68. package/dist/commands/archive.js +3 -3
  69. package/dist/commands/backlog.js +3 -3
  70. package/dist/commands/blocked.js +3 -3
  71. package/dist/commands/canvas.d.ts +15 -0
  72. package/dist/commands/canvas.js +200 -0
  73. package/dist/commands/checkpoint.js +2 -2
  74. package/dist/commands/compat.js +2 -2
  75. package/dist/commands/context.js +8 -6
  76. package/dist/commands/doctor.d.ts +11 -7
  77. package/dist/commands/doctor.js +18 -16
  78. package/dist/commands/embed.js +5 -6
  79. package/dist/commands/entities.js +2 -2
  80. package/dist/commands/graph.js +4 -4
  81. package/dist/commands/inject.d.ts +1 -1
  82. package/dist/commands/inject.js +5 -6
  83. package/dist/commands/kanban.js +4 -4
  84. package/dist/commands/link.js +5 -5
  85. package/dist/commands/migrate-observations.js +4 -4
  86. package/dist/commands/observe.d.ts +0 -1
  87. package/dist/commands/observe.js +14 -13
  88. package/dist/commands/project.js +5 -5
  89. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  90. package/dist/commands/rebuild-embeddings.js +91 -0
  91. package/dist/commands/rebuild.js +12 -11
  92. package/dist/commands/recover.js +3 -3
  93. package/dist/commands/reflect.js +6 -7
  94. package/dist/commands/repair-session.js +1 -1
  95. package/dist/commands/replay.js +14 -14
  96. package/dist/commands/session-recap.js +1 -1
  97. package/dist/commands/setup.d.ts +2 -90
  98. package/dist/commands/setup.js +3 -21
  99. package/dist/commands/shell-init.js +1 -1
  100. package/dist/commands/sleep.d.ts +1 -1
  101. package/dist/commands/sleep.js +20 -19
  102. package/dist/commands/status.d.ts +2 -0
  103. package/dist/commands/status.js +57 -35
  104. package/dist/commands/sync-bd.d.ts +10 -0
  105. package/dist/commands/sync-bd.js +10 -0
  106. package/dist/commands/tailscale.js +3 -3
  107. package/dist/commands/task.js +4 -4
  108. package/dist/commands/template.js +2 -2
  109. package/dist/commands/wake.d.ts +1 -1
  110. package/dist/commands/wake.js +11 -10
  111. package/dist/commands/workgraph.d.ts +124 -0
  112. package/dist/commands/workgraph.js +38 -0
  113. package/dist/index.d.ts +337 -191
  114. package/dist/index.js +387 -118
  115. package/dist/{inject-Bzi5E-By.d.cts → inject-DYUrDqQO.d.ts} +3 -3
  116. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  117. package/dist/lib/auto-linker.js +2 -2
  118. package/dist/lib/canvas-layout.d.ts +100 -16
  119. package/dist/lib/canvas-layout.js +21 -78
  120. package/dist/lib/config.d.ts +27 -3
  121. package/dist/lib/config.js +4 -2
  122. package/dist/lib/entity-index.js +1 -1
  123. package/dist/lib/project-utils.js +4 -4
  124. package/dist/lib/session-repair.js +1 -1
  125. package/dist/lib/session-utils.js +1 -1
  126. package/dist/lib/tailscale.js +1 -1
  127. package/dist/lib/task-utils.js +3 -3
  128. package/dist/lib/template-engine.js +1 -1
  129. package/dist/lib/webdav.js +1 -1
  130. package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
  131. package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
  132. package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
  133. package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
  134. package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
  135. package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
  136. package/dist/openclaw-plugin.d.ts +8 -0
  137. package/dist/openclaw-plugin.js +14 -0
  138. package/dist/registry-BR4326o0.d.ts +30 -0
  139. package/dist/store-CA-6sKCJ.d.ts +34 -0
  140. package/dist/thread-B9LhXNU0.d.ts +41 -0
  141. package/dist/transformers.node-A2ZRORSQ.js +46775 -0
  142. package/dist/{types-Y2_Um2Ls.d.cts → types-BbWJoC1c.d.ts} +1 -44
  143. package/dist/workgraph/index.d.ts +5 -0
  144. package/dist/workgraph/index.js +23 -0
  145. package/dist/workgraph/ledger.d.ts +2 -0
  146. package/dist/workgraph/ledger.js +25 -0
  147. package/dist/workgraph/registry.d.ts +2 -0
  148. package/dist/workgraph/registry.js +19 -0
  149. package/dist/workgraph/store.d.ts +2 -0
  150. package/dist/workgraph/store.js +25 -0
  151. package/dist/workgraph/thread.d.ts +2 -0
  152. package/dist/workgraph/thread.js +25 -0
  153. package/dist/workgraph/types.d.ts +54 -0
  154. package/dist/workgraph/types.js +7 -0
  155. package/hooks/clawvault/HOOK.md +34 -4
  156. package/hooks/clawvault/handler.js +760 -78
  157. package/hooks/clawvault/handler.test.js +235 -79
  158. package/hooks/clawvault/openclaw.plugin.json +72 -0
  159. package/openclaw.plugin.json +65 -38
  160. package/package.json +15 -18
  161. package/dist/chunk-3RG5ZIWI.js +0 -10
  162. package/dist/chunk-6U6MK36V.js +0 -205
  163. package/dist/chunk-7R7O6STJ.js +0 -88
  164. package/dist/chunk-CMB7UL7C.js +0 -327
  165. package/dist/chunk-DEFFDRVP.js +0 -938
  166. package/dist/chunk-E7MFQB6D.js +0 -163
  167. package/dist/chunk-GAJV4IGR.js +0 -82
  168. package/dist/chunk-GQSLDZTS.js +0 -560
  169. package/dist/chunk-K234IDRJ.js +0 -1073
  170. package/dist/chunk-MFM6K7PU.js +0 -374
  171. package/dist/chunk-MXSSG3QU.js +0 -42
  172. package/dist/chunk-PAH27GSN.js +0 -108
  173. package/dist/cli/index.cjs +0 -10033
  174. package/dist/cli/index.d.cts +0 -5
  175. package/dist/commands/archive.cjs +0 -287
  176. package/dist/commands/archive.d.cts +0 -11
  177. package/dist/commands/backlog.cjs +0 -721
  178. package/dist/commands/backlog.d.cts +0 -53
  179. package/dist/commands/blocked.cjs +0 -204
  180. package/dist/commands/blocked.d.cts +0 -26
  181. package/dist/commands/checkpoint.cjs +0 -244
  182. package/dist/commands/checkpoint.d.cts +0 -41
  183. package/dist/commands/compat.cjs +0 -369
  184. package/dist/commands/compat.d.cts +0 -28
  185. package/dist/commands/context.cjs +0 -2989
  186. package/dist/commands/context.d.cts +0 -2
  187. package/dist/commands/doctor.cjs +0 -3062
  188. package/dist/commands/doctor.d.cts +0 -21
  189. package/dist/commands/embed.cjs +0 -232
  190. package/dist/commands/embed.d.cts +0 -17
  191. package/dist/commands/entities.cjs +0 -141
  192. package/dist/commands/entities.d.cts +0 -7
  193. package/dist/commands/graph.cjs +0 -501
  194. package/dist/commands/graph.d.cts +0 -21
  195. package/dist/commands/inject.cjs +0 -1636
  196. package/dist/commands/inject.d.cts +0 -2
  197. package/dist/commands/kanban.cjs +0 -884
  198. package/dist/commands/kanban.d.cts +0 -63
  199. package/dist/commands/link.cjs +0 -965
  200. package/dist/commands/link.d.cts +0 -11
  201. package/dist/commands/migrate-observations.cjs +0 -362
  202. package/dist/commands/migrate-observations.d.cts +0 -19
  203. package/dist/commands/observe.cjs +0 -4099
  204. package/dist/commands/observe.d.cts +0 -23
  205. package/dist/commands/project.cjs +0 -1341
  206. package/dist/commands/project.d.cts +0 -85
  207. package/dist/commands/rebuild.cjs +0 -3136
  208. package/dist/commands/rebuild.d.cts +0 -11
  209. package/dist/commands/recover.cjs +0 -361
  210. package/dist/commands/recover.d.cts +0 -38
  211. package/dist/commands/reflect.cjs +0 -1008
  212. package/dist/commands/reflect.d.cts +0 -11
  213. package/dist/commands/repair-session.cjs +0 -457
  214. package/dist/commands/repair-session.d.cts +0 -38
  215. package/dist/commands/replay.cjs +0 -4103
  216. package/dist/commands/replay.d.cts +0 -16
  217. package/dist/commands/session-recap.cjs +0 -353
  218. package/dist/commands/session-recap.d.cts +0 -27
  219. package/dist/commands/setup.cjs +0 -1345
  220. package/dist/commands/setup.d.cts +0 -100
  221. package/dist/commands/shell-init.cjs +0 -75
  222. package/dist/commands/shell-init.d.cts +0 -7
  223. package/dist/commands/sleep.cjs +0 -6028
  224. package/dist/commands/sleep.d.cts +0 -36
  225. package/dist/commands/status.cjs +0 -2736
  226. package/dist/commands/status.d.cts +0 -52
  227. package/dist/commands/tailscale.cjs +0 -1532
  228. package/dist/commands/tailscale.d.cts +0 -52
  229. package/dist/commands/task.cjs +0 -1236
  230. package/dist/commands/task.d.cts +0 -97
  231. package/dist/commands/template.cjs +0 -457
  232. package/dist/commands/template.d.cts +0 -36
  233. package/dist/commands/wake.cjs +0 -2626
  234. package/dist/commands/wake.d.cts +0 -22
  235. package/dist/context-BUGaWpyL.d.cts +0 -46
  236. package/dist/index.cjs +0 -14526
  237. package/dist/index.d.cts +0 -858
  238. package/dist/inject-Bzi5E-By.d.ts +0 -137
  239. package/dist/lib/auto-linker.cjs +0 -176
  240. package/dist/lib/auto-linker.d.cts +0 -26
  241. package/dist/lib/canvas-layout.cjs +0 -136
  242. package/dist/lib/canvas-layout.d.cts +0 -31
  243. package/dist/lib/config.cjs +0 -78
  244. package/dist/lib/config.d.cts +0 -11
  245. package/dist/lib/entity-index.cjs +0 -84
  246. package/dist/lib/entity-index.d.cts +0 -26
  247. package/dist/lib/project-utils.cjs +0 -864
  248. package/dist/lib/project-utils.d.cts +0 -97
  249. package/dist/lib/session-repair.cjs +0 -239
  250. package/dist/lib/session-repair.d.cts +0 -110
  251. package/dist/lib/session-utils.cjs +0 -209
  252. package/dist/lib/session-utils.d.cts +0 -63
  253. package/dist/lib/tailscale.cjs +0 -1183
  254. package/dist/lib/tailscale.d.cts +0 -225
  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/lib/webdav.cjs +0 -568
  260. package/dist/lib/webdav.d.cts +0 -109
  261. package/dist/plugin/index.cjs +0 -1907
  262. package/dist/plugin/index.d.cts +0 -36
  263. package/dist/plugin/index.d.ts +0 -36
  264. package/dist/plugin/index.js +0 -572
  265. package/dist/plugin/inject.cjs +0 -356
  266. package/dist/plugin/inject.d.cts +0 -54
  267. package/dist/plugin/inject.d.ts +0 -54
  268. package/dist/plugin/inject.js +0 -17
  269. package/dist/plugin/observe.cjs +0 -631
  270. package/dist/plugin/observe.d.cts +0 -39
  271. package/dist/plugin/observe.d.ts +0 -39
  272. package/dist/plugin/observe.js +0 -18
  273. package/dist/plugin/templates.cjs +0 -593
  274. package/dist/plugin/templates.d.cts +0 -52
  275. package/dist/plugin/templates.d.ts +0 -52
  276. package/dist/plugin/templates.js +0 -25
  277. package/dist/plugin/types.cjs +0 -18
  278. package/dist/plugin/types.d.cts +0 -209
  279. package/dist/plugin/types.d.ts +0 -209
  280. package/dist/plugin/types.js +0 -0
  281. package/dist/plugin/vault.cjs +0 -927
  282. package/dist/plugin/vault.d.cts +0 -68
  283. package/dist/plugin/vault.d.ts +0 -68
  284. package/dist/plugin/vault.js +0 -22
  285. package/dist/types-Y2_Um2Ls.d.ts +0 -205
  286. package/templates/memory-event.md +0 -67
  287. package/templates/party.md +0 -63
  288. package/templates/primitive-registry.yaml +0 -551
  289. package/templates/run.md +0 -68
  290. package/templates/trigger.md +0 -68
  291. 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
+ }