clawvault 3.1.0 → 3.2.1

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 (273) 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 +451 -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-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
  22. package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
  23. package/dist/{chunk-F2JEUD4J.js → chunk-4ITRXIVT.js} +5 -7
  24. package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
  25. package/dist/chunk-5PJ4STIC.js +465 -0
  26. package/dist/{chunk-62YTUT6J.js → chunk-AZYOKJYC.js} +2 -2
  27. package/dist/chunk-BSJ6RIT7.js +447 -0
  28. package/dist/chunk-ECRZL5XR.js +50 -0
  29. package/dist/chunk-ERNE2FZ5.js +189 -0
  30. package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
  31. package/dist/{chunk-VGLOTGAS.js → chunk-FAKNOB7Y.js} +2 -2
  32. package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
  33. package/dist/chunk-GNJL4YGR.js +79 -0
  34. package/dist/chunk-HR4KN6S2.js +152 -0
  35. package/dist/{chunk-OZ7RIXTO.js → chunk-IIOU45CK.js} +1 -1
  36. package/dist/chunk-IJBFGPCS.js +33 -0
  37. package/dist/chunk-IVRIKYFE.js +520 -0
  38. package/dist/chunk-K7PNYS45.js +93 -0
  39. package/dist/chunk-MDIH26GC.js +183 -0
  40. package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
  41. package/dist/{chunk-H34S76MB.js → chunk-MNPUYCHQ.js} +6 -6
  42. package/dist/chunk-NTOPJI7W.js +207 -0
  43. package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
  44. package/dist/chunk-PG56HX5T.js +154 -0
  45. package/dist/{chunk-LNJA2UGL.js → chunk-PI4WMLMG.js} +7 -84
  46. package/dist/chunk-QMHPQYUV.js +363 -0
  47. package/dist/{chunk-H62BP7RI.js → chunk-QPDDIHXE.js} +209 -43
  48. package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
  49. package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
  50. package/dist/{chunk-SJSFRIYS.js → chunk-S5OJEGFG.js} +2 -2
  51. package/dist/chunk-SS4B7P7V.js +99 -0
  52. package/dist/chunk-TIGW564L.js +628 -0
  53. package/dist/chunk-U67V476Y.js +35 -0
  54. package/dist/{chunk-JY6FYXIT.js → chunk-UCQAOZHW.js} +6 -11
  55. package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
  56. package/dist/chunk-WIOLLGAD.js +190 -0
  57. package/dist/{chunk-3WRJEKN4.js → chunk-WJVWINEM.js} +72 -8
  58. package/dist/chunk-WMGIIABP.js +15 -0
  59. package/dist/{chunk-33UGEQRT.js → chunk-X3SPPUFG.js} +151 -64
  60. package/dist/{chunk-3NSBOUT3.js → chunk-Y3TIJEBP.js} +314 -79
  61. package/dist/chunk-Y6VJKXGL.js +373 -0
  62. package/dist/{chunk-LI4O6NVK.js → chunk-YDWHS4LJ.js} +49 -9
  63. package/dist/{chunk-U55BGUAU.js → chunk-YNIPYN4F.js} +5 -5
  64. package/dist/chunk-YXQCA6B7.js +226 -0
  65. package/dist/cli/index.js +26 -22
  66. package/dist/commands/archive.js +3 -3
  67. package/dist/commands/backlog.js +3 -3
  68. package/dist/commands/blocked.js +3 -3
  69. package/dist/commands/canvas.d.ts +15 -0
  70. package/dist/commands/canvas.js +200 -0
  71. package/dist/commands/checkpoint.js +2 -2
  72. package/dist/commands/compat.js +2 -2
  73. package/dist/commands/context.js +7 -5
  74. package/dist/commands/doctor.d.ts +11 -7
  75. package/dist/commands/doctor.js +16 -14
  76. package/dist/commands/embed.js +5 -6
  77. package/dist/commands/entities.js +2 -2
  78. package/dist/commands/graph.js +3 -3
  79. package/dist/commands/inject.d.ts +1 -1
  80. package/dist/commands/inject.js +4 -5
  81. package/dist/commands/kanban.js +4 -4
  82. package/dist/commands/link.js +2 -2
  83. package/dist/commands/migrate-observations.js +4 -4
  84. package/dist/commands/observe.d.ts +0 -1
  85. package/dist/commands/observe.js +13 -12
  86. package/dist/commands/project.js +5 -5
  87. package/dist/commands/rebuild-embeddings.d.ts +21 -0
  88. package/dist/commands/rebuild-embeddings.js +91 -0
  89. package/dist/commands/rebuild.js +12 -11
  90. package/dist/commands/recover.js +3 -3
  91. package/dist/commands/reflect.js +6 -7
  92. package/dist/commands/repair-session.js +1 -1
  93. package/dist/commands/replay.js +14 -14
  94. package/dist/commands/session-recap.js +1 -1
  95. package/dist/commands/setup.d.ts +2 -89
  96. package/dist/commands/setup.js +3 -21
  97. package/dist/commands/shell-init.js +1 -1
  98. package/dist/commands/sleep.d.ts +1 -1
  99. package/dist/commands/sleep.js +18 -17
  100. package/dist/commands/status.d.ts +2 -0
  101. package/dist/commands/status.js +40 -30
  102. package/dist/commands/sync-bd.d.ts +10 -0
  103. package/dist/commands/sync-bd.js +10 -0
  104. package/dist/commands/tailscale.d.ts +52 -0
  105. package/dist/commands/tailscale.js +26 -0
  106. package/dist/commands/task.js +4 -4
  107. package/dist/commands/template.js +2 -2
  108. package/dist/commands/wake.d.ts +1 -1
  109. package/dist/commands/wake.js +11 -10
  110. package/dist/index.d.ts +334 -191
  111. package/dist/index.js +432 -108
  112. package/dist/{inject-Bzi5E-By.d.ts → inject-DYUrDqQO.d.ts} +3 -3
  113. package/dist/ledger-B7g7jhqG.d.ts +44 -0
  114. package/dist/lib/auto-linker.js +1 -1
  115. package/dist/lib/canvas-layout.d.ts +115 -0
  116. package/dist/lib/canvas-layout.js +35 -0
  117. package/dist/lib/config.d.ts +27 -3
  118. package/dist/lib/config.js +4 -2
  119. package/dist/lib/entity-index.js +1 -1
  120. package/dist/lib/project-utils.js +4 -4
  121. package/dist/lib/session-repair.js +1 -1
  122. package/dist/lib/session-utils.js +1 -1
  123. package/dist/lib/tailscale.d.ts +225 -0
  124. package/dist/lib/tailscale.js +50 -0
  125. package/dist/lib/task-utils.js +3 -3
  126. package/dist/lib/template-engine.js +1 -1
  127. package/dist/lib/webdav.d.ts +109 -0
  128. package/dist/lib/webdav.js +35 -0
  129. package/dist/plugin/index.d.ts +344 -28
  130. package/dist/plugin/index.js +3919 -227
  131. package/dist/registry-BR4326o0.d.ts +30 -0
  132. package/dist/store-CA-6sKCJ.d.ts +34 -0
  133. package/dist/thread-B9LhXNU0.d.ts +41 -0
  134. package/dist/{types-Y2_Um2Ls.d.ts → types-BbWJoC1c.d.ts} +1 -44
  135. package/dist/workgraph/index.d.ts +5 -0
  136. package/dist/workgraph/index.js +23 -0
  137. package/dist/workgraph/ledger.d.ts +2 -0
  138. package/dist/workgraph/ledger.js +25 -0
  139. package/dist/workgraph/registry.d.ts +2 -0
  140. package/dist/workgraph/registry.js +19 -0
  141. package/dist/workgraph/store.d.ts +2 -0
  142. package/dist/workgraph/store.js +25 -0
  143. package/dist/workgraph/thread.d.ts +2 -0
  144. package/dist/workgraph/thread.js +25 -0
  145. package/dist/workgraph/types.d.ts +54 -0
  146. package/dist/workgraph/types.js +7 -0
  147. package/hooks/clawvault/HOOK.md +113 -0
  148. package/hooks/clawvault/handler.js +1559 -0
  149. package/hooks/clawvault/handler.test.js +510 -0
  150. package/hooks/clawvault/openclaw.plugin.json +72 -0
  151. package/openclaw.plugin.json +235 -30
  152. package/package.json +20 -20
  153. package/dist/chunk-3RG5ZIWI.js +0 -10
  154. package/dist/chunk-3ZIH425O.js +0 -871
  155. package/dist/chunk-6U6MK36V.js +0 -205
  156. package/dist/chunk-CMB7UL7C.js +0 -327
  157. package/dist/chunk-D2H45LON.js +0 -1074
  158. package/dist/chunk-E7MFQB6D.js +0 -163
  159. package/dist/chunk-GQSLDZTS.js +0 -560
  160. package/dist/chunk-MFM6K7PU.js +0 -374
  161. package/dist/chunk-MXSSG3QU.js +0 -42
  162. package/dist/chunk-OCGVIN3L.js +0 -88
  163. package/dist/chunk-PAH27GSN.js +0 -108
  164. package/dist/chunk-YCUNCH2I.js +0 -78
  165. package/dist/cli/index.cjs +0 -8584
  166. package/dist/cli/index.d.cts +0 -5
  167. package/dist/commands/archive.cjs +0 -287
  168. package/dist/commands/archive.d.cts +0 -11
  169. package/dist/commands/backlog.cjs +0 -721
  170. package/dist/commands/backlog.d.cts +0 -53
  171. package/dist/commands/blocked.cjs +0 -204
  172. package/dist/commands/blocked.d.cts +0 -26
  173. package/dist/commands/checkpoint.cjs +0 -244
  174. package/dist/commands/checkpoint.d.cts +0 -41
  175. package/dist/commands/compat.cjs +0 -294
  176. package/dist/commands/compat.d.cts +0 -28
  177. package/dist/commands/context.cjs +0 -2990
  178. package/dist/commands/context.d.cts +0 -2
  179. package/dist/commands/doctor.cjs +0 -2986
  180. package/dist/commands/doctor.d.cts +0 -21
  181. package/dist/commands/embed.cjs +0 -232
  182. package/dist/commands/embed.d.cts +0 -17
  183. package/dist/commands/entities.cjs +0 -141
  184. package/dist/commands/entities.d.cts +0 -7
  185. package/dist/commands/graph.cjs +0 -501
  186. package/dist/commands/graph.d.cts +0 -21
  187. package/dist/commands/inject.cjs +0 -1636
  188. package/dist/commands/inject.d.cts +0 -2
  189. package/dist/commands/kanban.cjs +0 -884
  190. package/dist/commands/kanban.d.cts +0 -63
  191. package/dist/commands/link.cjs +0 -965
  192. package/dist/commands/link.d.cts +0 -11
  193. package/dist/commands/migrate-observations.cjs +0 -362
  194. package/dist/commands/migrate-observations.d.cts +0 -19
  195. package/dist/commands/observe.cjs +0 -4099
  196. package/dist/commands/observe.d.cts +0 -23
  197. package/dist/commands/project.cjs +0 -1341
  198. package/dist/commands/project.d.cts +0 -85
  199. package/dist/commands/rebuild.cjs +0 -3136
  200. package/dist/commands/rebuild.d.cts +0 -11
  201. package/dist/commands/recover.cjs +0 -361
  202. package/dist/commands/recover.d.cts +0 -38
  203. package/dist/commands/reflect.cjs +0 -1008
  204. package/dist/commands/reflect.d.cts +0 -11
  205. package/dist/commands/repair-session.cjs +0 -457
  206. package/dist/commands/repair-session.d.cts +0 -38
  207. package/dist/commands/replay.cjs +0 -4103
  208. package/dist/commands/replay.d.cts +0 -16
  209. package/dist/commands/session-recap.cjs +0 -353
  210. package/dist/commands/session-recap.d.cts +0 -27
  211. package/dist/commands/setup.cjs +0 -1278
  212. package/dist/commands/setup.d.cts +0 -99
  213. package/dist/commands/shell-init.cjs +0 -75
  214. package/dist/commands/shell-init.d.cts +0 -7
  215. package/dist/commands/sleep.cjs +0 -6029
  216. package/dist/commands/sleep.d.cts +0 -36
  217. package/dist/commands/status.cjs +0 -2737
  218. package/dist/commands/status.d.cts +0 -52
  219. package/dist/commands/task.cjs +0 -1236
  220. package/dist/commands/task.d.cts +0 -97
  221. package/dist/commands/template.cjs +0 -457
  222. package/dist/commands/template.d.cts +0 -36
  223. package/dist/commands/wake.cjs +0 -2627
  224. package/dist/commands/wake.d.cts +0 -22
  225. package/dist/context-BUGaWpyL.d.cts +0 -46
  226. package/dist/index.cjs +0 -12373
  227. package/dist/index.d.cts +0 -854
  228. package/dist/inject-Bzi5E-By.d.cts +0 -137
  229. package/dist/lib/auto-linker.cjs +0 -176
  230. package/dist/lib/auto-linker.d.cts +0 -26
  231. package/dist/lib/config.cjs +0 -78
  232. package/dist/lib/config.d.cts +0 -11
  233. package/dist/lib/entity-index.cjs +0 -84
  234. package/dist/lib/entity-index.d.cts +0 -26
  235. package/dist/lib/project-utils.cjs +0 -864
  236. package/dist/lib/project-utils.d.cts +0 -97
  237. package/dist/lib/session-repair.cjs +0 -239
  238. package/dist/lib/session-repair.d.cts +0 -110
  239. package/dist/lib/session-utils.cjs +0 -209
  240. package/dist/lib/session-utils.d.cts +0 -63
  241. package/dist/lib/task-utils.cjs +0 -1137
  242. package/dist/lib/task-utils.d.cts +0 -208
  243. package/dist/lib/template-engine.cjs +0 -47
  244. package/dist/lib/template-engine.d.cts +0 -11
  245. package/dist/plugin/index.cjs +0 -1907
  246. package/dist/plugin/index.d.cts +0 -36
  247. package/dist/plugin/inject.cjs +0 -356
  248. package/dist/plugin/inject.d.cts +0 -54
  249. package/dist/plugin/inject.d.ts +0 -54
  250. package/dist/plugin/inject.js +0 -17
  251. package/dist/plugin/observe.cjs +0 -631
  252. package/dist/plugin/observe.d.cts +0 -39
  253. package/dist/plugin/observe.d.ts +0 -39
  254. package/dist/plugin/observe.js +0 -18
  255. package/dist/plugin/templates.cjs +0 -593
  256. package/dist/plugin/templates.d.cts +0 -52
  257. package/dist/plugin/templates.d.ts +0 -52
  258. package/dist/plugin/templates.js +0 -25
  259. package/dist/plugin/types.cjs +0 -18
  260. package/dist/plugin/types.d.cts +0 -209
  261. package/dist/plugin/types.d.ts +0 -209
  262. package/dist/plugin/types.js +0 -0
  263. package/dist/plugin/vault.cjs +0 -927
  264. package/dist/plugin/vault.d.cts +0 -68
  265. package/dist/plugin/vault.d.ts +0 -68
  266. package/dist/plugin/vault.js +0 -22
  267. package/dist/types-Y2_Um2Ls.d.cts +0 -205
  268. package/templates/memory-event.md +0 -67
  269. package/templates/party.md +0 -63
  270. package/templates/primitive-registry.yaml +0 -551
  271. package/templates/run.md +0 -68
  272. package/templates/trigger.md +0 -68
  273. 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
+ }