@joekytc/dsh-swarm 0.1.2 → 0.3.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 (603) hide show
  1. package/README.md +177 -140
  2. package/README.zh-CN.md +173 -126
  3. package/client/BoardCard.tsx +24 -2
  4. package/client/ConfigSection.tsx +162 -0
  5. package/client/ConfigSelect.tsx +71 -0
  6. package/client/KanbanBoard.tsx +3 -0
  7. package/client/KanbanTab.tsx +4 -3
  8. package/client/TaskDrawer.tsx +4 -26
  9. package/client/WorkflowRail.tsx +3 -1
  10. package/client/config-store.ts +92 -0
  11. package/client/config.css +110 -0
  12. package/client/index.ts +39 -4
  13. package/client/kanban.css +88 -2
  14. package/client/session-bridge.ts +39 -0
  15. package/client/timeline-model.ts +12 -1
  16. package/client/workflow-model.ts +3 -1
  17. package/lib/client.js +519 -56
  18. package/lib/config.d.ts +26 -2
  19. package/lib/config.js +20 -1
  20. package/lib/dispatcher/agent-runner.d.ts +37 -7
  21. package/lib/dispatcher/agent-runner.js +239 -86
  22. package/lib/dispatcher/chain-auditor.d.ts +4 -4
  23. package/lib/dispatcher/chain-auditor.js +14 -9
  24. package/lib/dispatcher/dispatcher.d.ts +69 -10
  25. package/lib/dispatcher/dispatcher.js +281 -42
  26. package/lib/dispatcher/event-waker.d.ts +1 -1
  27. package/lib/dispatcher/event-waker.js +2 -2
  28. package/lib/dispatcher/git-credentials.d.ts +2 -2
  29. package/lib/dispatcher/git-credentials.js +2 -2
  30. package/lib/dispatcher/model-candidates.d.ts +1 -1
  31. package/lib/dispatcher/model-candidates.js +1 -1
  32. package/lib/dispatcher/session-events.d.ts +12 -16
  33. package/lib/dispatcher/session-events.js +18 -6
  34. package/lib/dispatcher/target-repo.d.ts +2 -2
  35. package/lib/dispatcher/target-repo.js +3 -3
  36. package/lib/dispatcher/v-orchestrator.d.ts +44 -11
  37. package/lib/dispatcher/v-orchestrator.js +257 -49
  38. package/lib/dispatcher/watchdog.d.ts +1 -1
  39. package/lib/dispatcher/watchdog.js +2 -2
  40. package/lib/domain/config-override.d.ts +52 -0
  41. package/lib/domain/config-override.js +138 -0
  42. package/lib/domain/delivery-contract.d.ts +4 -3
  43. package/lib/domain/delivery-contract.js +30 -18
  44. package/lib/domain/delivery-evidence.d.ts +1 -1
  45. package/lib/domain/delivery-evidence.js +1 -1
  46. package/lib/domain/event-store.js +1 -1
  47. package/lib/domain/gate-policy.d.ts +26 -0
  48. package/lib/domain/gate-policy.js +44 -0
  49. package/lib/domain/im-message.d.ts +15 -0
  50. package/lib/domain/im-message.js +149 -0
  51. package/lib/domain/kanban-service.d.ts +28 -12
  52. package/lib/domain/kanban-service.js +73 -30
  53. package/lib/domain/memory.js +1 -1
  54. package/lib/domain/ocr-review.d.ts +47 -0
  55. package/lib/domain/ocr-review.js +105 -0
  56. package/lib/domain/permissions.js +6 -6
  57. package/lib/domain/planning-checklist.d.ts +5 -0
  58. package/lib/domain/planning-checklist.js +80 -6
  59. package/lib/domain/projection.js +8 -7
  60. package/lib/domain/review-evidence.js +1 -1
  61. package/lib/domain/state-machine.js +3 -2
  62. package/lib/domain/task-parents.js +1 -1
  63. package/lib/domain/types.d.ts +5 -5
  64. package/lib/index.js +28 -25
  65. package/lib/roles/clean-fs-tools.d.ts +22 -0
  66. package/lib/roles/clean-fs-tools.js +117 -0
  67. package/lib/roles/preset-installer.js +3 -3
  68. package/lib/roles/skill-installer.d.ts +6 -0
  69. package/lib/roles/skill-installer.js +32 -0
  70. package/lib/roles/toolsets.d.ts +71 -15
  71. package/lib/roles/toolsets.js +418 -68
  72. package/lib/roles/wiki-worker.d.ts +1 -0
  73. package/lib/roles/wiki-worker.js +12 -4
  74. package/lib/routes/kanban-http.d.ts +28 -2
  75. package/lib/routes/kanban-http.js +199 -6
  76. package/lib/routes/kanban-sse.d.ts +1 -1
  77. package/lib/routes/kanban-sse.js +1 -1
  78. package/lib/routes/planning-driver.d.ts +6 -2
  79. package/lib/routes/planning-driver.js +15 -7
  80. package/lib/routes/prefix-router.d.ts +19 -3
  81. package/lib/routes/prefix-router.js +40 -3
  82. package/lib/services/config-provider.d.ts +38 -0
  83. package/lib/services/config-provider.js +92 -0
  84. package/lib/services/gate-runner.d.ts +24 -0
  85. package/lib/services/gate-runner.js +87 -0
  86. package/lib/services/im-delivery.d.ts +94 -0
  87. package/lib/services/im-delivery.js +263 -0
  88. package/lib/services/kanban-provider.d.ts +6 -2
  89. package/lib/services/kanban-provider.js +41 -3
  90. package/lib/services/llm-catalog.d.ts +35 -0
  91. package/lib/services/llm-catalog.js +19 -0
  92. package/lib/services/ocr-cli.d.ts +35 -0
  93. package/lib/services/ocr-cli.js +128 -0
  94. package/lib/tools/kanban-tools.d.ts +2 -2
  95. package/lib/tools/kanban-tools.js +2 -1
  96. package/lib/tools/main-session-tools.d.ts +22 -3
  97. package/lib/tools/main-session-tools.js +135 -30
  98. package/lib/tools/ocr-review-tools.d.ts +11 -0
  99. package/lib/tools/ocr-review-tools.js +63 -0
  100. package/lib/tools/planning-tools.d.ts +8 -1
  101. package/lib/tools/planning-tools.js +44 -15
  102. package/lib/tools/prefetch-tools.js +1 -0
  103. package/lib/tools/spec-card-tools.js +1 -0
  104. package/lib/tools/wiki-tools.js +6 -5
  105. package/lib/wiki/kb-linkage.d.ts +9 -2
  106. package/lib/wiki/kb-linkage.js +3 -3
  107. package/lib/wiki/local-kb-client.d.ts +14 -0
  108. package/lib/wiki/local-kb-client.js +101 -0
  109. package/lib/wiki/local-kb.d.ts +6 -0
  110. package/lib/wiki/local-kb.js +25 -0
  111. package/lib/wiki/memory-recall.d.ts +14 -6
  112. package/lib/wiki/memory-recall.js +17 -13
  113. package/lib/wiki/page-path.d.ts +9 -3
  114. package/lib/wiki/page-path.js +33 -12
  115. package/lib/wiki/wiki-vault-client.d.ts +3 -3
  116. package/lib/wiki/wiki-vault-client.js +6 -6
  117. package/package.json +16 -11
  118. package/personas/kanban-d/agent.cordis.yml +5 -5
  119. package/personas/kanban-d/preset.yml +1 -1
  120. package/personas/kanban-dt/agent.cordis.yml +25 -7
  121. package/personas/kanban-dt/preset.yml +1 -1
  122. package/personas/kanban-p/agent.cordis.yml +19 -3
  123. package/personas/kanban-p/preset.yml +1 -1
  124. package/personas/kanban-pt/agent.cordis.yml +39 -9
  125. package/personas/kanban-pt/preset.yml +1 -1
  126. package/personas/kanban-v/agent.cordis.yml +3 -3
  127. package/personas/kanban-w/agent.cordis.yml +37 -18
  128. package/personas/kanban-w/preset.yml +1 -1
  129. package/personas/persona-d.md +1 -1
  130. package/personas/persona-dt.md +13 -5
  131. package/personas/persona-p.md +5 -2
  132. package/personas/persona-pt.md +13 -6
  133. package/personas/persona-v.md +2 -2
  134. package/personas/persona-w.md +11 -9
  135. package/personas/swarm/agent.cordis.yml +43 -0
  136. package/personas/swarm/preset.yml +5 -0
  137. package/skills/grill-me/SKILL.md +71 -0
  138. package/skills/llm-wiki-skill/.mise.toml +2 -0
  139. package/skills/llm-wiki-skill/.nvmrc +1 -0
  140. package/skills/llm-wiki-skill/AGENTS.md +173 -0
  141. package/skills/llm-wiki-skill/CHANGELOG.md +548 -0
  142. package/skills/llm-wiki-skill/CLAUDE.md +152 -0
  143. package/skills/llm-wiki-skill/HERMES.md +44 -0
  144. package/skills/llm-wiki-skill/README.en.md +261 -0
  145. package/skills/llm-wiki-skill/README.md +291 -0
  146. package/skills/llm-wiki-skill/SKILL.md +1133 -0
  147. package/skills/llm-wiki-skill/assets/graph-demo.gif +0 -0
  148. package/skills/llm-wiki-skill/assets/graph-preview.png +0 -0
  149. package/skills/llm-wiki-skill/deps/LICENSE-d3.txt +13 -0
  150. package/skills/llm-wiki-skill/deps/LICENSE-marked.txt +44 -0
  151. package/skills/llm-wiki-skill/deps/LICENSE-purify.txt +568 -0
  152. package/skills/llm-wiki-skill/deps/LICENSE-roughjs.txt +21 -0
  153. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/SKILL.md +261 -0
  154. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/references/config/first-time-setup.md +106 -0
  155. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/cdp.ts +179 -0
  156. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/constants.ts +13 -0
  157. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/defuddle-converter.ts +58 -0
  158. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/html-to-markdown.ts +135 -0
  159. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/legacy-converter.ts +629 -0
  160. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/main.ts +314 -0
  161. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/markdown-conversion-shared.ts +305 -0
  162. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/media-localizer.ts +317 -0
  163. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/package.json +14 -0
  164. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/paths.ts +29 -0
  165. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/package.json +9 -0
  166. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.test.ts +307 -0
  167. package/skills/llm-wiki-skill/deps/baoyu-url-to-markdown/scripts/vendor/baoyu-chrome-cdp/src/index.ts +523 -0
  168. package/skills/llm-wiki-skill/deps/d3.min.js +2 -0
  169. package/skills/llm-wiki-skill/deps/marked.min.js +6 -0
  170. package/skills/llm-wiki-skill/deps/purify.min.js +3 -0
  171. package/skills/llm-wiki-skill/deps/rough.min.js +7 -0
  172. package/skills/llm-wiki-skill/deps/youtube-transcript/SKILL.md +47 -0
  173. package/skills/llm-wiki-skill/deps/youtube-transcript/scripts/get_transcript.py +72 -0
  174. package/skills/llm-wiki-skill/designs/graph-edge-legibility/graph-edge-treatments.html +480 -0
  175. package/skills/llm-wiki-skill/docs/design/oriental-atlas/DESIGN.md +183 -0
  176. package/skills/llm-wiki-skill/docs/design/oriental-atlas/README.md +10 -0
  177. package/skills/llm-wiki-skill/docs/design/oriental-atlas/design-brief.md +170 -0
  178. package/skills/llm-wiki-skill/docs/design/oriental-atlas/oriental-editorial-atlas.html +1908 -0
  179. package/skills/llm-wiki-skill/docs/design/oriental-atlas/reviews/result-review.md +68 -0
  180. package/skills/llm-wiki-skill/docs/graph/2026-06-16-interaction-risk-audit.md +88 -0
  181. package/skills/llm-wiki-skill/docs/graph/2026-06-16-interaction-test-coverage-map.md +212 -0
  182. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-18-phase-0-2-baseline.md +86 -0
  183. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-18-phase-1-3-dom-svg-large-baseline.md +85 -0
  184. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-18-phase-1-4-disposable-validation-gate.md +97 -0
  185. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-18-phase-2-4-global-renderer-trial-matrix.md +168 -0
  186. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-19-phase-6-1-sigma-graphology-trial.md +71 -0
  187. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-19-phase-6-2-vis-network-trial.md +78 -0
  188. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-19-phase-6-3-aggregation-fallback-trial.md +72 -0
  189. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-19-phase-6-4-global-renderer-route-decision.md +94 -0
  190. package/skills/llm-wiki-skill/docs/graph/performance/2026-06-19-sigma-global-production-integration-result.md +102 -0
  191. package/skills/llm-wiki-skill/docs/graph/performance/artifacts/2026-06-18-phase-0-2/stage-4.5-offline-dense-wheel.json +9 -0
  192. package/skills/llm-wiki-skill/docs/graph/performance/artifacts/2026-06-18-phase-0-2/stage-4.5-offline-dense-wheel.png +0 -0
  193. package/skills/llm-wiki-skill/docs/graph/performance/artifacts/2026-06-18-phase-0-2/stage-4.5-offline-navigation.png +0 -0
  194. package/skills/llm-wiki-skill/docs/spark/2026-06-15-omp-tool-status-events-design.md +174 -0
  195. package/skills/llm-wiki-skill/docs/spark/2026-06-16-graph-interaction-geometry-design.md +832 -0
  196. package/skills/llm-wiki-skill/docs/spark/2026-06-16-graph-six-layer-architecture-design.md +577 -0
  197. package/skills/llm-wiki-skill/docs/spark/2026-06-17-static-renderer-coordination-split-design.md +172 -0
  198. package/skills/llm-wiki-skill/docs/spark/2026-06-18-large-graph-global-community-design.md +909 -0
  199. package/skills/llm-wiki-skill/docs/spark/2026-06-19-sigma-global-graph-renderer-design.md +360 -0
  200. package/skills/llm-wiki-skill/docs/spark/2026-06-20-paper-ui-port-design.md +162 -0
  201. package/skills/llm-wiki-skill/docs/spark/2026-06-20-paper-ui-v2-shell-alignment-design.md +384 -0
  202. package/skills/llm-wiki-skill/docs/spark/2026-06-21-graph-experience-completion-design.md +534 -0
  203. package/skills/llm-wiki-skill/docs/spark/2026-06-22-chat-auto-scroll-design.md +185 -0
  204. package/skills/llm-wiki-skill/docs/spark/2026-06-22-graph-community-node-visual-design.md +266 -0
  205. package/skills/llm-wiki-skill/docs/spark/2026-06-25-sigma-overlay-refactor-perf-design.md +170 -0
  206. package/skills/llm-wiki-skill/docs/spark/2026-06-26-global-sigma-community-spotlight-design.md +393 -0
  207. package/skills/llm-wiki-skill/docs/spark/mockups/2026-06-18-large-graph-global-community-flow.html +746 -0
  208. package/skills/llm-wiki-skill/install.ps1 +87 -0
  209. package/skills/llm-wiki-skill/install.sh +722 -0
  210. package/skills/llm-wiki-skill/package-lock.json +11171 -0
  211. package/skills/llm-wiki-skill/package.json +24 -0
  212. package/skills/llm-wiki-skill/packages/graph-engine/package.json +37 -0
  213. package/skills/llm-wiki-skill/packages/graph-engine/src/anim/.gitkeep +1 -0
  214. package/skills/llm-wiki-skill/packages/graph-engine/src/anim/index.ts +179 -0
  215. package/skills/llm-wiki-skill/packages/graph-engine/src/architecture.ts +79 -0
  216. package/skills/llm-wiki-skill/packages/graph-engine/src/diff.ts +121 -0
  217. package/skills/llm-wiki-skill/packages/graph-engine/src/facade.ts +1406 -0
  218. package/skills/llm-wiki-skill/packages/graph-engine/src/graph-node.ts +37 -0
  219. package/skills/llm-wiki-skill/packages/graph-engine/src/index.ts +25 -0
  220. package/skills/llm-wiki-skill/packages/graph-engine/src/layout/edge-geometry.ts +17 -0
  221. package/skills/llm-wiki-skill/packages/graph-engine/src/layout/index.ts +16 -0
  222. package/skills/llm-wiki-skill/packages/graph-engine/src/layout/spatial-index.ts +509 -0
  223. package/skills/llm-wiki-skill/packages/graph-engine/src/model/atlas.ts +18 -0
  224. package/skills/llm-wiki-skill/packages/graph-engine/src/model/index.ts +10 -0
  225. package/skills/llm-wiki-skill/packages/graph-engine/src/model/labels.ts +11 -0
  226. package/skills/llm-wiki-skill/packages/graph-engine/src/model/layout.ts +81 -0
  227. package/skills/llm-wiki-skill/packages/graph-engine/src/model/learning.ts +8 -0
  228. package/skills/llm-wiki-skill/packages/graph-engine/src/model/legacy-helpers.ts +1379 -0
  229. package/skills/llm-wiki-skill/packages/graph-engine/src/model/queue.ts +7 -0
  230. package/skills/llm-wiki-skill/packages/graph-engine/src/model/storage.ts +4 -0
  231. package/skills/llm-wiki-skill/packages/graph-engine/src/model/visibility.ts +9 -0
  232. package/skills/llm-wiki-skill/packages/graph-engine/src/render/.gitkeep +1 -0
  233. package/skills/llm-wiki-skill/packages/graph-engine/src/render/adapter.ts +443 -0
  234. package/skills/llm-wiki-skill/packages/graph-engine/src/render/aggregation-containers.ts +64 -0
  235. package/skills/llm-wiki-skill/packages/graph-engine/src/render/community-cloud-geometry.ts +178 -0
  236. package/skills/llm-wiki-skill/packages/graph-engine/src/render/community-wash.ts +238 -0
  237. package/skills/llm-wiki-skill/packages/graph-engine/src/render/community-washes.ts +19 -0
  238. package/skills/llm-wiki-skill/packages/graph-engine/src/render/compat-contract.ts +14 -0
  239. package/skills/llm-wiki-skill/packages/graph-engine/src/render/controller.ts +705 -0
  240. package/skills/llm-wiki-skill/packages/graph-engine/src/render/controls.ts +288 -0
  241. package/skills/llm-wiki-skill/packages/graph-engine/src/render/dom-svg-renderer.ts +180 -0
  242. package/skills/llm-wiki-skill/packages/graph-engine/src/render/edges.ts +51 -0
  243. package/skills/llm-wiki-skill/packages/graph-engine/src/render/geometry.ts +380 -0
  244. package/skills/llm-wiki-skill/packages/graph-engine/src/render/gestures.ts +673 -0
  245. package/skills/llm-wiki-skill/packages/graph-engine/src/render/graph-renderer-root.ts +387 -0
  246. package/skills/llm-wiki-skill/packages/graph-engine/src/render/hit-testing.ts +56 -0
  247. package/skills/llm-wiki-skill/packages/graph-engine/src/render/host-dom.ts +14 -0
  248. package/skills/llm-wiki-skill/packages/graph-engine/src/render/hover-card.ts +37 -0
  249. package/skills/llm-wiki-skill/packages/graph-engine/src/render/index.ts +213 -0
  250. package/skills/llm-wiki-skill/packages/graph-engine/src/render/keyboard.ts +48 -0
  251. package/skills/llm-wiki-skill/packages/graph-engine/src/render/legend.ts +25 -0
  252. package/skills/llm-wiki-skill/packages/graph-engine/src/render/minimap.ts +45 -0
  253. package/skills/llm-wiki-skill/packages/graph-engine/src/render/model.ts +1339 -0
  254. package/skills/llm-wiki-skill/packages/graph-engine/src/render/node-drag-lifecycle.ts +94 -0
  255. package/skills/llm-wiki-skill/packages/graph-engine/src/render/nodes.ts +103 -0
  256. package/skills/llm-wiki-skill/packages/graph-engine/src/render/offline-reader.ts +209 -0
  257. package/skills/llm-wiki-skill/packages/graph-engine/src/render/overlays-presenter.ts +192 -0
  258. package/skills/llm-wiki-skill/packages/graph-engine/src/render/overlays.ts +73 -0
  259. package/skills/llm-wiki-skill/packages/graph-engine/src/render/pin-position.ts +51 -0
  260. package/skills/llm-wiki-skill/packages/graph-engine/src/render/preview.ts +65 -0
  261. package/skills/llm-wiki-skill/packages/graph-engine/src/render/relation-focus.ts +91 -0
  262. package/skills/llm-wiki-skill/packages/graph-engine/src/render/render-context.ts +85 -0
  263. package/skills/llm-wiki-skill/packages/graph-engine/src/render/render-pipeline.ts +871 -0
  264. package/skills/llm-wiki-skill/packages/graph-engine/src/render/render-styles.ts +1405 -0
  265. package/skills/llm-wiki-skill/packages/graph-engine/src/render/renderer-surface.ts +75 -0
  266. package/skills/llm-wiki-skill/packages/graph-engine/src/render/search.ts +57 -0
  267. package/skills/llm-wiki-skill/packages/graph-engine/src/render/sigma-coordinates.ts +44 -0
  268. package/skills/llm-wiki-skill/packages/graph-engine/src/render/sigma-global-drag.ts +246 -0
  269. package/skills/llm-wiki-skill/packages/graph-engine/src/render/sigma-global-renderer.ts +1425 -0
  270. package/skills/llm-wiki-skill/packages/graph-engine/src/render/sigma-overlay-svg.ts +121 -0
  271. package/skills/llm-wiki-skill/packages/graph-engine/src/render/simulation-bridge.ts +59 -0
  272. package/skills/llm-wiki-skill/packages/graph-engine/src/render/state.ts +262 -0
  273. package/skills/llm-wiki-skill/packages/graph-engine/src/render/toolbar.ts +40 -0
  274. package/skills/llm-wiki-skill/packages/graph-engine/src/render/viewport.ts +336 -0
  275. package/skills/llm-wiki-skill/packages/graph-engine/src/select/.gitkeep +1 -0
  276. package/skills/llm-wiki-skill/packages/graph-engine/src/select/index.ts +189 -0
  277. package/skills/llm-wiki-skill/packages/graph-engine/src/sim/index.ts +372 -0
  278. package/skills/llm-wiki-skill/packages/graph-engine/src/sim/pins.ts +84 -0
  279. package/skills/llm-wiki-skill/packages/graph-engine/src/summary/index.ts +447 -0
  280. package/skills/llm-wiki-skill/packages/graph-engine/src/themes/.gitkeep +1 -0
  281. package/skills/llm-wiki-skill/packages/graph-engine/src/themes/index.ts +8 -0
  282. package/skills/llm-wiki-skill/packages/graph-engine/src/themes/tokens.ts +117 -0
  283. package/skills/llm-wiki-skill/packages/graph-engine/src/types/d3-quadtree.d.ts +22 -0
  284. package/skills/llm-wiki-skill/packages/graph-engine/src/types.ts +531 -0
  285. package/skills/llm-wiki-skill/packages/graph-engine/test/aggregation-fallback-trial-adapter.test.ts +115 -0
  286. package/skills/llm-wiki-skill/packages/graph-engine/test/aggregation-fallback-trial-adapter.ts +222 -0
  287. package/skills/llm-wiki-skill/packages/graph-engine/test/anim.test.ts +121 -0
  288. package/skills/llm-wiki-skill/packages/graph-engine/test/architecture.test.ts +63 -0
  289. package/skills/llm-wiki-skill/packages/graph-engine/test/community-cloud-geometry.test.ts +86 -0
  290. package/skills/llm-wiki-skill/packages/graph-engine/test/community-wash.test.ts +97 -0
  291. package/skills/llm-wiki-skill/packages/graph-engine/test/diff.test.ts +133 -0
  292. package/skills/llm-wiki-skill/packages/graph-engine/test/facade.test.ts +1369 -0
  293. package/skills/llm-wiki-skill/packages/graph-engine/test/geometry.test.ts +147 -0
  294. package/skills/llm-wiki-skill/packages/graph-engine/test/gestures.test.ts +768 -0
  295. package/skills/llm-wiki-skill/packages/graph-engine/test/graph-layout.test.ts +110 -0
  296. package/skills/llm-wiki-skill/packages/graph-engine/test/helpers.test.ts +287 -0
  297. package/skills/llm-wiki-skill/packages/graph-engine/test/interaction-contract.test.ts +106 -0
  298. package/skills/llm-wiki-skill/packages/graph-engine/test/keyboard.test.ts +59 -0
  299. package/skills/llm-wiki-skill/packages/graph-engine/test/large-fixtures.test.ts +47 -0
  300. package/skills/llm-wiki-skill/packages/graph-engine/test/large-graph-fixtures.ts +420 -0
  301. package/skills/llm-wiki-skill/packages/graph-engine/test/learning.test.ts +259 -0
  302. package/skills/llm-wiki-skill/packages/graph-engine/test/node-drag-lifecycle.test.ts +166 -0
  303. package/skills/llm-wiki-skill/packages/graph-engine/test/overlays.test.ts +115 -0
  304. package/skills/llm-wiki-skill/packages/graph-engine/test/preview.test.ts +56 -0
  305. package/skills/llm-wiki-skill/packages/graph-engine/test/queue.test.ts +114 -0
  306. package/skills/llm-wiki-skill/packages/graph-engine/test/relation-focus.test.ts +115 -0
  307. package/skills/llm-wiki-skill/packages/graph-engine/test/render-model.test.ts +1030 -0
  308. package/skills/llm-wiki-skill/packages/graph-engine/test/renderer-adapter-contract.test.ts +271 -0
  309. package/skills/llm-wiki-skill/packages/graph-engine/test/renderer-boundary.test.ts +618 -0
  310. package/skills/llm-wiki-skill/packages/graph-engine/test/renderer-lifecycle.test.ts +1711 -0
  311. package/skills/llm-wiki-skill/packages/graph-engine/test/route-continuity.test.ts +284 -0
  312. package/skills/llm-wiki-skill/packages/graph-engine/test/runtime-state.test.ts +281 -0
  313. package/skills/llm-wiki-skill/packages/graph-engine/test/search-and-legend.test.ts +124 -0
  314. package/skills/llm-wiki-skill/packages/graph-engine/test/select.test.ts +168 -0
  315. package/skills/llm-wiki-skill/packages/graph-engine/test/sigma-coordinates.test.ts +50 -0
  316. package/skills/llm-wiki-skill/packages/graph-engine/test/sigma-global-renderer.test.ts +2210 -0
  317. package/skills/llm-wiki-skill/packages/graph-engine/test/sigma-trial-adapter.test.ts +82 -0
  318. package/skills/llm-wiki-skill/packages/graph-engine/test/sigma-trial-adapter.ts +271 -0
  319. package/skills/llm-wiki-skill/packages/graph-engine/test/sim.test.ts +207 -0
  320. package/skills/llm-wiki-skill/packages/graph-engine/test/simulation-bridge.test.ts +96 -0
  321. package/skills/llm-wiki-skill/packages/graph-engine/test/spatial-index.test.ts +222 -0
  322. package/skills/llm-wiki-skill/packages/graph-engine/test/state.test.ts +189 -0
  323. package/skills/llm-wiki-skill/packages/graph-engine/test/summary-contract.test.ts +422 -0
  324. package/skills/llm-wiki-skill/packages/graph-engine/test/themes.test.ts +62 -0
  325. package/skills/llm-wiki-skill/packages/graph-engine/test/toolbar.test.ts +64 -0
  326. package/skills/llm-wiki-skill/packages/graph-engine/test/viewport.test.ts +213 -0
  327. package/skills/llm-wiki-skill/packages/graph-engine/test/vis-network-trial-adapter.test.ts +82 -0
  328. package/skills/llm-wiki-skill/packages/graph-engine/test/vis-network-trial-adapter.ts +186 -0
  329. package/skills/llm-wiki-skill/packages/graph-engine/tsconfig.json +17 -0
  330. package/skills/llm-wiki-skill/packages/graph-engine/vite.config.ts +15 -0
  331. package/skills/llm-wiki-skill/platforms/claude/CLAUDE.md +37 -0
  332. package/skills/llm-wiki-skill/platforms/claude/companions/llm-wiki-upgrade/SKILL.md +95 -0
  333. package/skills/llm-wiki-skill/platforms/codex/AGENTS.md +23 -0
  334. package/skills/llm-wiki-skill/platforms/hermes/README.md +31 -0
  335. package/skills/llm-wiki-skill/platforms/openclaw/README.md +31 -0
  336. package/skills/llm-wiki-skill/scripts/adapter-state.sh +424 -0
  337. package/skills/llm-wiki-skill/scripts/build-graph-data.sh +395 -0
  338. package/skills/llm-wiki-skill/scripts/build-graph-html.sh +459 -0
  339. package/skills/llm-wiki-skill/scripts/cache.sh +352 -0
  340. package/skills/llm-wiki-skill/scripts/create-source-page.sh +100 -0
  341. package/skills/llm-wiki-skill/scripts/delete-helper.sh +68 -0
  342. package/skills/llm-wiki-skill/scripts/graph-analysis.js +732 -0
  343. package/skills/llm-wiki-skill/scripts/hook-session-start.sh +46 -0
  344. package/skills/llm-wiki-skill/scripts/init-wiki.sh +95 -0
  345. package/skills/llm-wiki-skill/scripts/lib/source-signal-eligibility.js +158 -0
  346. package/skills/llm-wiki-skill/scripts/lint-fix.sh +114 -0
  347. package/skills/llm-wiki-skill/scripts/lint-runner.sh +217 -0
  348. package/skills/llm-wiki-skill/scripts/runtime-context.sh +77 -0
  349. package/skills/llm-wiki-skill/scripts/shared-config.sh +83 -0
  350. package/skills/llm-wiki-skill/scripts/source-record-contract.tsv +10 -0
  351. package/skills/llm-wiki-skill/scripts/source-registry.sh +349 -0
  352. package/skills/llm-wiki-skill/scripts/source-registry.tsv +10 -0
  353. package/skills/llm-wiki-skill/scripts/source-signal-coverage.js +83 -0
  354. package/skills/llm-wiki-skill/scripts/validate-step1.sh +144 -0
  355. package/skills/llm-wiki-skill/scripts/wiki-compat.sh +267 -0
  356. package/skills/llm-wiki-skill/setup.sh +8 -0
  357. package/skills/llm-wiki-skill/templates/entity-template.md +30 -0
  358. package/skills/llm-wiki-skill/templates/graph-styles/wash/footer.html +27 -0
  359. package/skills/llm-wiki-skill/templates/graph-styles/wash/graph-wash-helpers.js +1326 -0
  360. package/skills/llm-wiki-skill/templates/graph-styles/wash/graph-wash.js +1009 -0
  361. package/skills/llm-wiki-skill/templates/graph-styles/wash/header.html +2002 -0
  362. package/skills/llm-wiki-skill/templates/index-en-template.md +51 -0
  363. package/skills/llm-wiki-skill/templates/index-template.md +51 -0
  364. package/skills/llm-wiki-skill/templates/log-en-template.md +11 -0
  365. package/skills/llm-wiki-skill/templates/log-template.md +11 -0
  366. package/skills/llm-wiki-skill/templates/overview-en-template.md +41 -0
  367. package/skills/llm-wiki-skill/templates/overview-template.md +41 -0
  368. package/skills/llm-wiki-skill/templates/purpose-en-template.md +16 -0
  369. package/skills/llm-wiki-skill/templates/purpose-template.md +16 -0
  370. package/skills/llm-wiki-skill/templates/query-template.md +19 -0
  371. package/skills/llm-wiki-skill/templates/schema-template.md +185 -0
  372. package/skills/llm-wiki-skill/templates/source-template.md +51 -0
  373. package/skills/llm-wiki-skill/templates/synthesis-template.md +25 -0
  374. package/skills/llm-wiki-skill/templates/topic-template.md +35 -0
  375. package/skills/llm-wiki-skill/tests/adapter-state.sh +338 -0
  376. package/skills/llm-wiki-skill/tests/browser/graph-aggregation-fallback-trial.ts +737 -0
  377. package/skills/llm-wiki-skill/tests/browser/graph-community-node-map.mjs +255 -0
  378. package/skills/llm-wiki-skill/tests/browser/graph-community-wash-interactions.mjs +170 -0
  379. package/skills/llm-wiki-skill/tests/browser/graph-default-behavior.mjs +630 -0
  380. package/skills/llm-wiki-skill/tests/browser/graph-density-preview.mjs +53 -0
  381. package/skills/llm-wiki-skill/tests/browser/graph-html-insights.mjs +41 -0
  382. package/skills/llm-wiki-skill/tests/browser/graph-large-performance.ts +689 -0
  383. package/skills/llm-wiki-skill/tests/browser/graph-node-slim.mjs +75 -0
  384. package/skills/llm-wiki-skill/tests/browser/graph-offline-phase-6.mjs +660 -0
  385. package/skills/llm-wiki-skill/tests/browser/graph-renderer-trial-shared.ts +291 -0
  386. package/skills/llm-wiki-skill/tests/browser/graph-sigma-global-production.ts +1437 -0
  387. package/skills/llm-wiki-skill/tests/browser/graph-sigma-graphology-trial.ts +949 -0
  388. package/skills/llm-wiki-skill/tests/browser/graph-stage-4-5.mjs +815 -0
  389. package/skills/llm-wiki-skill/tests/browser/graph-vis-network-trial.ts +762 -0
  390. package/skills/llm-wiki-skill/tests/browser/graph-workbench-interactions.mjs +730 -0
  391. package/skills/llm-wiki-skill/tests/browser/validate-graph-trial-result.mjs +149 -0
  392. package/skills/llm-wiki-skill/tests/expected/graph-data-empty.json +60 -0
  393. package/skills/llm-wiki-skill/tests/expected/graph-data-sample.json +396 -0
  394. package/skills/llm-wiki-skill/tests/expected/graph-interactive-basic.html +1736 -0
  395. package/skills/llm-wiki-skill/tests/expected/lint-output.txt +44 -0
  396. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/index.md +1 -0
  397. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/entities/Alpha.md +10 -0
  398. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/entities/Beta.md +7 -0
  399. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/entities/Delta.md +8 -0
  400. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/entities/Gamma.md +8 -0
  401. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/queries/20260422-abc.md +10 -0
  402. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/synthesis/Crystal.md +8 -0
  403. package/skills/llm-wiki-skill/tests/fixtures/coverage-sample-wiki/wiki/topics/Overview.md +9 -0
  404. package/skills/llm-wiki-skill/tests/fixtures/graph-data-empty-wiki/wiki/entities/.gitkeep +0 -0
  405. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/purpose.md +3 -0
  406. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/entities/Attention.md +7 -0
  407. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/entities/Decoder.md +7 -0
  408. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/entities/Encoder.md +7 -0
  409. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/entities/GPT.md +7 -0
  410. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/entities/Transformer.md +13 -0
  411. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/sources/paper.md +9 -0
  412. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/topics/arch.md +12 -0
  413. package/skills/llm-wiki-skill/tests/fixtures/graph-data-sample-wiki/wiki/topics/finetune.md +9 -0
  414. package/skills/llm-wiki-skill/tests/fixtures/graph-interactive-basic/wiki/graph-data.json +40 -0
  415. package/skills/llm-wiki-skill/tests/fixtures/graph-interactive-dense/wiki/graph-data.json +3965 -0
  416. package/skills/llm-wiki-skill/tests/fixtures/graph-interactive-multicomm/wiki/graph-data.json +212 -0
  417. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/index.md +14 -0
  418. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/entities/C++.md +6 -0
  419. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/entities/Lonely.md +4 -0
  420. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/entities/Other.md +8 -0
  421. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/entities/Real.md +7 -0
  422. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/sources/2026-01-01-test-source.md +15 -0
  423. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/sources/2026-02-01-inline-img.md +12 -0
  424. package/skills/llm-wiki-skill/tests/fixtures/lint-sample-wiki/wiki/topics/AIbasic.md +3 -0
  425. package/skills/llm-wiki-skill/tests/graph-aggregation-fallback-trial.regression-1.sh +46 -0
  426. package/skills/llm-wiki-skill/tests/graph-analysis-helper.regression-1.sh +237 -0
  427. package/skills/llm-wiki-skill/tests/graph-browser-large-performance.regression-1.sh +39 -0
  428. package/skills/llm-wiki-skill/tests/graph-browser-stage-4-5.regression-1.sh +163 -0
  429. package/skills/llm-wiki-skill/tests/graph-build-failures.regression-1.sh +91 -0
  430. package/skills/llm-wiki-skill/tests/graph-community-node-map.regression-1.sh +107 -0
  431. package/skills/llm-wiki-skill/tests/graph-community-wash-interactions.regression-1.sh +35 -0
  432. package/skills/llm-wiki-skill/tests/graph-data-confidence-merge.regression-1.sh +309 -0
  433. package/skills/llm-wiki-skill/tests/graph-data-source-paths.regression-1.sh +49 -0
  434. package/skills/llm-wiki-skill/tests/graph-default-behavior.regression-1.sh +35 -0
  435. package/skills/llm-wiki-skill/tests/graph-html-a11y.regression-1.sh +50 -0
  436. package/skills/llm-wiki-skill/tests/graph-html-brand-link.regression-1.sh +47 -0
  437. package/skills/llm-wiki-skill/tests/graph-html-density.regression-1.sh +228 -0
  438. package/skills/llm-wiki-skill/tests/graph-html-drawer-neighbors.regression-1.sh +47 -0
  439. package/skills/llm-wiki-skill/tests/graph-html-insights.regression-1.sh +80 -0
  440. package/skills/llm-wiki-skill/tests/graph-html-long-label.regression-1.sh +92 -0
  441. package/skills/llm-wiki-skill/tests/graph-html-minimap.regression-1.sh +51 -0
  442. package/skills/llm-wiki-skill/tests/graph-html-mobile.regression-1.sh +48 -0
  443. package/skills/llm-wiki-skill/tests/graph-html-oriental-atlas-contract.regression-1.sh +94 -0
  444. package/skills/llm-wiki-skill/tests/graph-html-oriental-design-contract.regression-1.sh +82 -0
  445. package/skills/llm-wiki-skill/tests/graph-html-search.regression-1.sh +52 -0
  446. package/skills/llm-wiki-skill/tests/graph-html-styles.regression-1.sh +66 -0
  447. package/skills/llm-wiki-skill/tests/graph-html-toolbar.regression-1.sh +62 -0
  448. package/skills/llm-wiki-skill/tests/graph-offline-phase-6.regression-1.sh +51 -0
  449. package/skills/llm-wiki-skill/tests/graph-sigma-global-production.regression-1.sh +64 -0
  450. package/skills/llm-wiki-skill/tests/graph-sigma-graphology-trial.regression-1.sh +46 -0
  451. package/skills/llm-wiki-skill/tests/graph-vis-network-trial.regression-1.sh +46 -0
  452. package/skills/llm-wiki-skill/tests/graph-workbench-interactions.regression-1.sh +151 -0
  453. package/skills/llm-wiki-skill/tests/js/graph-wash-bootstrap.test.js +110 -0
  454. package/skills/llm-wiki-skill/tests/js/graph-wash-helpers.test.js +296 -0
  455. package/skills/llm-wiki-skill/tests/js/graph-wash-learning.test.js +260 -0
  456. package/skills/llm-wiki-skill/tests/js/graph-wash-queue.test.js +115 -0
  457. package/skills/llm-wiki-skill/tests/js/graph-wash-runtime-state.test.js +282 -0
  458. package/skills/llm-wiki-skill/tests/js/source-signal-coverage.test.js +48 -0
  459. package/skills/llm-wiki-skill/tests/js/source-signal-eligibility.test.js +202 -0
  460. package/skills/llm-wiki-skill/tests/lib/graph-html-engine-helpers.sh +110 -0
  461. package/skills/llm-wiki-skill/tests/lint-output.regression-1.sh +32 -0
  462. package/skills/llm-wiki-skill/tests/regression.sh +1754 -0
  463. package/skills/llm-wiki-skill/workbench/AGENTS.md +110 -0
  464. package/skills/llm-wiki-skill/workbench/CLAUDE.md +55 -0
  465. package/skills/llm-wiki-skill/workbench/LICENSE +21 -0
  466. package/skills/llm-wiki-skill/workbench/PRODUCT.md +1353 -0
  467. package/skills/llm-wiki-skill/workbench/README.md +30 -0
  468. package/skills/llm-wiki-skill/workbench/docs/current-kb-retrieval-design.md +587 -0
  469. package/skills/llm-wiki-skill/workbench/docs/graph-evolution-1-design.md +210 -0
  470. package/skills/llm-wiki-skill/workbench/docs/stage-2-design.md +1139 -0
  471. package/skills/llm-wiki-skill/workbench/docs/stage-3-design.md +1269 -0
  472. package/skills/llm-wiki-skill/workbench/docs/stage-3.5-design.md +1164 -0
  473. package/skills/llm-wiki-skill/workbench/docs/stage-4-design.md +504 -0
  474. package/skills/llm-wiki-skill/workbench/docs/stage-4.5-design.md +195 -0
  475. package/skills/llm-wiki-skill/workbench/docs/superpowers/specs/2026-05-28-resizable-preview-layout-design.md +165 -0
  476. package/skills/llm-wiki-skill/workbench/docs/superpowers/specs/2026-05-28-settings-panel-scroll-fix.md +50 -0
  477. package/skills/llm-wiki-skill/workbench/server/package.json +31 -0
  478. package/skills/llm-wiki-skill/workbench/server/src/agent.ts +457 -0
  479. package/skills/llm-wiki-skill/workbench/server/src/artifacts.ts +248 -0
  480. package/skills/llm-wiki-skill/workbench/server/src/auth.ts +164 -0
  481. package/skills/llm-wiki-skill/workbench/server/src/config.ts +130 -0
  482. package/skills/llm-wiki-skill/workbench/server/src/conversations.test.ts +159 -0
  483. package/skills/llm-wiki-skill/workbench/server/src/conversations.ts +270 -0
  484. package/skills/llm-wiki-skill/workbench/server/src/digest/batch.ts +189 -0
  485. package/skills/llm-wiki-skill/workbench/server/src/digest/concurrency.test.ts +36 -0
  486. package/skills/llm-wiki-skill/workbench/server/src/digest/concurrency.ts +31 -0
  487. package/skills/llm-wiki-skill/workbench/server/src/digest/subagent.ts +91 -0
  488. package/skills/llm-wiki-skill/workbench/server/src/extensions/artifacts.ts +81 -0
  489. package/skills/llm-wiki-skill/workbench/server/src/extensions/knowledge-base.ts +263 -0
  490. package/skills/llm-wiki-skill/workbench/server/src/extensions/new-wiki.ts +42 -0
  491. package/skills/llm-wiki-skill/workbench/server/src/extensions/synthesis.ts +172 -0
  492. package/skills/llm-wiki-skill/workbench/server/src/graph-watcher.test.ts +196 -0
  493. package/skills/llm-wiki-skill/workbench/server/src/graph.test.ts +113 -0
  494. package/skills/llm-wiki-skill/workbench/server/src/graph.ts +351 -0
  495. package/skills/llm-wiki-skill/workbench/server/src/index.ts +1148 -0
  496. package/skills/llm-wiki-skill/workbench/server/src/knowledge-bases.test.ts +30 -0
  497. package/skills/llm-wiki-skill/workbench/server/src/knowledge-bases.ts +321 -0
  498. package/skills/llm-wiki-skill/workbench/server/src/pages.test.ts +41 -0
  499. package/skills/llm-wiki-skill/workbench/server/src/pages.ts +160 -0
  500. package/skills/llm-wiki-skill/workbench/server/src/retrieval.test.ts +151 -0
  501. package/skills/llm-wiki-skill/workbench/server/src/retrieval.ts +427 -0
  502. package/skills/llm-wiki-skill/workbench/server/src/tool-status-events.test.ts +363 -0
  503. package/skills/llm-wiki-skill/workbench/server/src/tool-status-events.ts +616 -0
  504. package/skills/llm-wiki-skill/workbench/server/src/wiki-init.test.ts +19 -0
  505. package/skills/llm-wiki-skill/workbench/server/src/wiki-init.ts +188 -0
  506. package/skills/llm-wiki-skill/workbench/server/tsconfig.json +10 -0
  507. package/skills/llm-wiki-skill/workbench/tsconfig.json +20 -0
  508. package/skills/llm-wiki-skill/workbench/web/README.md +73 -0
  509. package/skills/llm-wiki-skill/workbench/web/components.json +21 -0
  510. package/skills/llm-wiki-skill/workbench/web/eslint.config.js +22 -0
  511. package/skills/llm-wiki-skill/workbench/web/index.html +16 -0
  512. package/skills/llm-wiki-skill/workbench/web/package.json +54 -0
  513. package/skills/llm-wiki-skill/workbench/web/src/App.tsx +1195 -0
  514. package/skills/llm-wiki-skill/workbench/web/src/components/AddExternalDialog.tsx +375 -0
  515. package/skills/llm-wiki-skill/workbench/web/src/components/AppearancePanel.tsx +158 -0
  516. package/skills/llm-wiki-skill/workbench/web/src/components/ArtifactView.tsx +15 -0
  517. package/skills/llm-wiki-skill/workbench/web/src/components/BatchDigestPanel.tsx +117 -0
  518. package/skills/llm-wiki-skill/workbench/web/src/components/ChatPanel.tsx +890 -0
  519. package/skills/llm-wiki-skill/workbench/web/src/components/CommandMenu.tsx +75 -0
  520. package/skills/llm-wiki-skill/workbench/web/src/components/ExportButtons.tsx +58 -0
  521. package/skills/llm-wiki-skill/workbench/web/src/components/GraphPanel.tsx +800 -0
  522. package/skills/llm-wiki-skill/workbench/web/src/components/GraphReader.tsx +52 -0
  523. package/skills/llm-wiki-skill/workbench/web/src/components/GraphSelection.tsx +100 -0
  524. package/skills/llm-wiki-skill/workbench/web/src/components/GraphSummaryDrawer.tsx +269 -0
  525. package/skills/llm-wiki-skill/workbench/web/src/components/MainViewTabs.tsx +69 -0
  526. package/skills/llm-wiki-skill/workbench/web/src/components/MarkdownView.tsx +106 -0
  527. package/skills/llm-wiki-skill/workbench/web/src/components/NewWikiDialog.tsx +103 -0
  528. package/skills/llm-wiki-skill/workbench/web/src/components/RefMenu.tsx +44 -0
  529. package/skills/llm-wiki-skill/workbench/web/src/components/RightDrawer.tsx +298 -0
  530. package/skills/llm-wiki-skill/workbench/web/src/components/SearchPanel.tsx +165 -0
  531. package/skills/llm-wiki-skill/workbench/web/src/components/SettingsPanel.tsx +320 -0
  532. package/skills/llm-wiki-skill/workbench/web/src/components/Sidebar.tsx +354 -0
  533. package/skills/llm-wiki-skill/workbench/web/src/components/ToolHistorySummary.tsx +111 -0
  534. package/skills/llm-wiki-skill/workbench/web/src/components/ToolStatusRunway.tsx +70 -0
  535. package/skills/llm-wiki-skill/workbench/web/src/components/TopBar.tsx +348 -0
  536. package/skills/llm-wiki-skill/workbench/web/src/components/renderers/DownloadOnlyRenderer.tsx +88 -0
  537. package/skills/llm-wiki-skill/workbench/web/src/components/renderers/HtmlRenderer.tsx +92 -0
  538. package/skills/llm-wiki-skill/workbench/web/src/components/ui/button.tsx +52 -0
  539. package/skills/llm-wiki-skill/workbench/web/src/components/ui/command.tsx +50 -0
  540. package/skills/llm-wiki-skill/workbench/web/src/components/ui/dialog.tsx +156 -0
  541. package/skills/llm-wiki-skill/workbench/web/src/components/ui/input.tsx +21 -0
  542. package/skills/llm-wiki-skill/workbench/web/src/components/ui/tooltip.tsx +57 -0
  543. package/skills/llm-wiki-skill/workbench/web/src/index.css +4204 -0
  544. package/skills/llm-wiki-skill/workbench/web/src/lib/api.ts +648 -0
  545. package/skills/llm-wiki-skill/workbench/web/src/lib/appearance.ts +114 -0
  546. package/skills/llm-wiki-skill/workbench/web/src/lib/drawer-layout.ts +41 -0
  547. package/skills/llm-wiki-skill/workbench/web/src/lib/drawer-state.ts +168 -0
  548. package/skills/llm-wiki-skill/workbench/web/src/lib/graph-reader.ts +25 -0
  549. package/skills/llm-wiki-skill/workbench/web/src/lib/graph-selection-drawer.ts +35 -0
  550. package/skills/llm-wiki-skill/workbench/web/src/lib/graph-selection.ts +96 -0
  551. package/skills/llm-wiki-skill/workbench/web/src/lib/graph-summary-actions.ts +184 -0
  552. package/skills/llm-wiki-skill/workbench/web/src/lib/legacy-tool-status.ts +113 -0
  553. package/skills/llm-wiki-skill/workbench/web/src/lib/model-roles.ts +20 -0
  554. package/skills/llm-wiki-skill/workbench/web/src/lib/search-filter.ts +104 -0
  555. package/skills/llm-wiki-skill/workbench/web/src/lib/sse.ts +72 -0
  556. package/skills/llm-wiki-skill/workbench/web/src/lib/tool-status-format.ts +170 -0
  557. package/skills/llm-wiki-skill/workbench/web/src/lib/tool-status-model.ts +392 -0
  558. package/skills/llm-wiki-skill/workbench/web/src/lib/utils.ts +10 -0
  559. package/skills/llm-wiki-skill/workbench/web/src/lib/view-status.ts +16 -0
  560. package/skills/llm-wiki-skill/workbench/web/src/lib/wiki-links.ts +32 -0
  561. package/skills/llm-wiki-skill/workbench/web/src/main.tsx +10 -0
  562. package/skills/llm-wiki-skill/workbench/web/test/api.test.ts +61 -0
  563. package/skills/llm-wiki-skill/workbench/web/test/app-shell-drawer-layout.test.tsx +48 -0
  564. package/skills/llm-wiki-skill/workbench/web/test/appearance-panel.test.tsx +121 -0
  565. package/skills/llm-wiki-skill/workbench/web/test/appearance.test.ts +262 -0
  566. package/skills/llm-wiki-skill/workbench/web/test/chat-host-source.test.tsx +22 -0
  567. package/skills/llm-wiki-skill/workbench/web/test/chat-panel-auto-scroll.test.tsx +311 -0
  568. package/skills/llm-wiki-skill/workbench/web/test/chat-panel-bubbles.test.tsx +48 -0
  569. package/skills/llm-wiki-skill/workbench/web/test/chat-panel-composer.test.tsx +86 -0
  570. package/skills/llm-wiki-skill/workbench/web/test/chat-panel-tool-status.test.tsx +26 -0
  571. package/skills/llm-wiki-skill/workbench/web/test/export-batch-paper.test.tsx +101 -0
  572. package/skills/llm-wiki-skill/workbench/web/test/graph-drawer-state.test.ts +257 -0
  573. package/skills/llm-wiki-skill/workbench/web/test/graph-panel-paper.test.tsx +200 -0
  574. package/skills/llm-wiki-skill/workbench/web/test/graph-reader-request.test.ts +32 -0
  575. package/skills/llm-wiki-skill/workbench/web/test/graph-reader.test.ts +62 -0
  576. package/skills/llm-wiki-skill/workbench/web/test/graph-selection-drawer.test.ts +56 -0
  577. package/skills/llm-wiki-skill/workbench/web/test/graph-selection.test.ts +83 -0
  578. package/skills/llm-wiki-skill/workbench/web/test/graph-summary-actions.test.ts +188 -0
  579. package/skills/llm-wiki-skill/workbench/web/test/main-view-tabs.test.tsx +34 -0
  580. package/skills/llm-wiki-skill/workbench/web/test/markdown-view.test.tsx +34 -0
  581. package/skills/llm-wiki-skill/workbench/web/test/popup-menus.test.tsx +85 -0
  582. package/skills/llm-wiki-skill/workbench/web/test/render.test.tsx +30 -0
  583. package/skills/llm-wiki-skill/workbench/web/test/render.tsx +38 -0
  584. package/skills/llm-wiki-skill/workbench/web/test/right-drawer-graph-summary.test.tsx +287 -0
  585. package/skills/llm-wiki-skill/workbench/web/test/right-drawer-interactions.test.tsx +182 -0
  586. package/skills/llm-wiki-skill/workbench/web/test/search-filter.test.ts +65 -0
  587. package/skills/llm-wiki-skill/workbench/web/test/search-panel.test.tsx +155 -0
  588. package/skills/llm-wiki-skill/workbench/web/test/setup-dom.ts +66 -0
  589. package/skills/llm-wiki-skill/workbench/web/test/sidebar-v2-source.test.ts +17 -0
  590. package/skills/llm-wiki-skill/workbench/web/test/sidebar.test.tsx +110 -0
  591. package/skills/llm-wiki-skill/workbench/web/test/tool-history-summary.test.tsx +150 -0
  592. package/skills/llm-wiki-skill/workbench/web/test/tool-status-format.test.ts +107 -0
  593. package/skills/llm-wiki-skill/workbench/web/test/tool-status-model.test.ts +324 -0
  594. package/skills/llm-wiki-skill/workbench/web/test/tool-status-runway.test.ts +159 -0
  595. package/skills/llm-wiki-skill/workbench/web/test/topbar.test.tsx +248 -0
  596. package/skills/llm-wiki-skill/workbench/web/test/visual/paper-ui.ts +898 -0
  597. package/skills/llm-wiki-skill/workbench/web/test/wiki-links.test.ts +23 -0
  598. package/skills/llm-wiki-skill/workbench/web/test-results/paper-ui/actual/.gitkeep +1 -0
  599. package/skills/llm-wiki-skill/workbench/web/test-results/paper-ui/baseline/.gitkeep +1 -0
  600. package/skills/llm-wiki-skill/workbench/web/tsconfig.app.json +30 -0
  601. package/skills/llm-wiki-skill/workbench/web/tsconfig.json +7 -0
  602. package/skills/llm-wiki-skill/workbench/web/tsconfig.node.json +24 -0
  603. package/skills/llm-wiki-skill/workbench/web/vite.config.ts +28 -0
@@ -0,0 +1,1711 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import type { GraphData, GraphDiff, SelectionInput } from "../src";
5
+ import { createGraphRenderer } from "../src/render";
6
+ import {
7
+ createGraphFacadeRouteManager,
8
+ type GraphFacadeRenderer,
9
+ type GraphFacadeRouteRendererFactoryInput
10
+ } from "../src/facade";
11
+
12
+ describe("graph renderer lifecycle", () => {
13
+ it("renders a static over-limit notice without aggregation containers or DOM full graph", () => {
14
+ const ownerDocument = new FakeDocument();
15
+ const container = ownerDocument.createElement("div");
16
+ let sigmaAttempts = 0;
17
+ const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
18
+ state: {
19
+ data: largeFallbackGraphData(),
20
+ pins: {},
21
+ theme: "shan-shui",
22
+ focus: null,
23
+ typeFilters: {},
24
+ aggregationMarkers: [],
25
+ selection: null,
26
+ searchResultIds: [],
27
+ temporaryObject: null
28
+ },
29
+ factories: {
30
+ createSigmaGlobal: () => {
31
+ sigmaAttempts += 1;
32
+ throw new Error("WebGL unavailable");
33
+ }
34
+ }
35
+ });
36
+ const overLimitView = findByClass(container, "graph-over-limit-notice-view")[0];
37
+ const notice = findByClass(container, "graph-over-limit-notice")[0];
38
+ const title = findByClass(container, "graph-over-limit-notice-title")[0];
39
+ const body = findByClass(container, "graph-over-limit-notice-body")[0];
40
+ const oldContainers = findByClass(container, "graph-aggregation-safety-container");
41
+ const oldActions = findByClass(container, "graph-aggregation-safety-actions");
42
+ const retry = findByDataset(container, "action", "retry-sigma");
43
+ const clear = findByDataset(container, "action", "clear-selection");
44
+
45
+ assert.equal(manager.routeId, "over-limit-notice");
46
+ assert.equal(overLimitView?.dataset.notice, "node-count-over-limit");
47
+ assert.equal(overLimitView?.dataset.nodeLimit, "2000");
48
+ assert.equal(overLimitView?.dataset.containerCount, "0");
49
+ assert.equal(notice?.dataset.role, "over-limit-notice");
50
+ assert.equal(title?.textContent, "图谱节点较多");
51
+ assert.equal(body?.textContent, "当前图谱超过 2000 个节点。请用搜索、筛选或进入社区缩小范围。");
52
+ assert.deepEqual(oldContainers, []);
53
+ assert.deepEqual(oldActions, []);
54
+ assert.equal(retry, undefined);
55
+ assert.equal(clear, undefined);
56
+ assert.deepEqual(visibleNodeIds({ root: container as unknown as HTMLElement }), []);
57
+ assert.equal(sigmaAttempts, 0);
58
+ });
59
+
60
+ it("routes a community click to lightweight selection instead of focusing the community", () => {
61
+ const ownerDocument = new FakeDocument();
62
+ const container = ownerDocument.createElement("div");
63
+ const selections: SelectionInput[] = [];
64
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
65
+ data: graphDataWithCommunities([
66
+ ["a", "community-a"],
67
+ ["b", "community-b"]
68
+ ]),
69
+ theme: "shan-shui",
70
+ live: false,
71
+ onSelectionInput: (selection) => selections.push(selection)
72
+ });
73
+
74
+ findByDataset(renderer.root as unknown as FakeElement, "communityId", "community-a")?.dispatch("click");
75
+
76
+ assert.deepEqual(selections, [{ kind: "community", id: "community-a" }]);
77
+ assert.ok(nodeElement(renderer, "a"));
78
+ assert.ok(nodeElement(renderer, "b"));
79
+
80
+ renderer.destroy();
81
+ });
82
+
83
+ it("does not render aggregation marker containers in normal DOM/SVG output", () => {
84
+ const ownerDocument = new FakeDocument();
85
+ const container = ownerDocument.createElement("div");
86
+ const selections: SelectionInput[] = [];
87
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
88
+ data: graphDataWithCommunities([
89
+ ["a", "community-a"],
90
+ ["b", "community-a"],
91
+ ["c", "community-b"]
92
+ ]),
93
+ theme: "shan-shui",
94
+ live: false,
95
+ aggregationMarkers: [
96
+ {
97
+ id: "agg-community-a",
98
+ label: "Community A overflow",
99
+ communityId: "community-a",
100
+ nodeIds: ["a", "b"],
101
+ selectedNodeIds: ["a"],
102
+ searchResultIds: ["b"],
103
+ totalCount: 6
104
+ }
105
+ ],
106
+ onSelectionInput: (selection) => selections.push(selection)
107
+ });
108
+ const aggregation = findByDataset(renderer.root as unknown as FakeElement, "aggregationId", "agg-community-a");
109
+
110
+ assert.equal(aggregation, undefined);
111
+ assert.deepEqual(selections, []);
112
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b", "c"]);
113
+ assert.equal(renderer.root.dataset.llmWikiGraphFocus, undefined);
114
+
115
+ renderer.destroy();
116
+ });
117
+
118
+ it("renders poor community quality as a light warning with only core connectivity fallback", () => {
119
+ const ownerDocument = new FakeDocument();
120
+ const container = ownerDocument.createElement("div");
121
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
122
+ data: poorCommunityQualityGraphData(),
123
+ theme: "shan-shui",
124
+ live: false
125
+ });
126
+ const notice = findByClass(renderer.root as unknown as FakeElement, "graph-quality-notice")[0];
127
+ const action = findByClass(renderer.root as unknown as FakeElement, "graph-quality-notice-action")[0];
128
+ const wash = findByClass(renderer.root as unknown as FakeElement, "community-wash")[0];
129
+
130
+ assert.equal(renderer.root.dataset.communityQuality, "poor");
131
+ assert.equal(renderer.root.dataset.communityBoundaryCertainty, "low");
132
+ assert.equal(renderer.root.dataset.communityAuxiliaryViews, "core-structure-connectivity");
133
+ assert.ok(notice);
134
+ assert.equal(notice.dataset.qualityLevel, "poor");
135
+ assert.equal(action?.dataset.auxiliaryViewId, "core-structure-connectivity");
136
+ assert.equal(/type|source|time/i.test(renderer.root.dataset.communityAuxiliaryViews || ""), false);
137
+ assert.equal(wash?.dataset.boundaryCertainty, "low");
138
+
139
+ renderer.destroy();
140
+ });
141
+
142
+ it("routes a global node click to lightweight selection instead of opening the page", () => {
143
+ const ownerDocument = new FakeDocument();
144
+ const container = ownerDocument.createElement("div");
145
+ const opened: string[] = [];
146
+ const selections: SelectionInput[] = [];
147
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
148
+ data: graphData(["a"]),
149
+ theme: "shan-shui",
150
+ live: false,
151
+ onNodeOpen: (id) => opened.push(id),
152
+ onSelectionInput: (selection) => selections.push(selection)
153
+ });
154
+
155
+ nodeElement(renderer, "a")?.dispatch("click", { detail: 0 });
156
+
157
+ assert.deepEqual(opened, []);
158
+ assert.deepEqual(selections, [{ kind: "node", id: "a" }]);
159
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
160
+
161
+ renderer.destroy();
162
+ });
163
+
164
+ it("preserves DOM community selection when refreshed data still contains the selected object", () => {
165
+ const ownerDocument = new FakeDocument();
166
+ const container = ownerDocument.createElement("div");
167
+ const clearRequests: number[] = [];
168
+ const selections: SelectionInput[] = [];
169
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
170
+ data: graphDataWithCommunities([
171
+ ["a", "community-a"],
172
+ ["b", "community-a"],
173
+ ["c", "community-b"]
174
+ ]),
175
+ theme: "shan-shui",
176
+ live: false,
177
+ focus: { kind: "community", id: "community-a" },
178
+ onSelectionInput: (selection) => selections.push(selection),
179
+ onSelectionClearRequested: () => clearRequests.push(1)
180
+ });
181
+
182
+ renderer.select({ kind: "node", id: "a" });
183
+ assert.deepEqual(selections, []);
184
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
185
+
186
+ renderer.setData(graphDataWithCommunities([
187
+ ["a", "community-a"],
188
+ ["b", "community-a"],
189
+ ["d", "community-b"]
190
+ ]));
191
+
192
+ assert.deepEqual(clearRequests, []);
193
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
194
+ assert.ok(nodeElement(renderer, "b"));
195
+ assert.equal(nodeElement(renderer, "c"), undefined);
196
+
197
+ renderer.destroy();
198
+ });
199
+
200
+ it("clears selection on a blank click without leaving community focus", () => {
201
+ const ownerDocument = new FakeDocument();
202
+ const container = ownerDocument.createElement("div");
203
+ const clearRequests: number[] = [];
204
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
205
+ data: graphDataWithCommunities([
206
+ ["a", "community-a"],
207
+ ["b", "community-a"],
208
+ ["c", "community-b"]
209
+ ]),
210
+ theme: "shan-shui",
211
+ live: false,
212
+ focus: { kind: "community", id: "community-a" },
213
+ onSelectionClearRequested: () => clearRequests.push(1)
214
+ });
215
+
216
+ renderer.select({ kind: "node", id: "a" });
217
+ dispatchPointerSequence(renderer.root as unknown as FakeElement, 20, 20);
218
+
219
+ assert.equal(clearRequests.length, 1);
220
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "false");
221
+ assert.ok(nodeElement(renderer, "a"));
222
+ assert.ok(nodeElement(renderer, "b"));
223
+ assert.equal(nodeElement(renderer, "c"), undefined);
224
+
225
+ renderer.destroy();
226
+ });
227
+
228
+ it("applies community relation focus immediately on node hover and clears it on leave", async () => {
229
+ const ownerDocument = new FakeDocument();
230
+ const container = ownerDocument.createElement("div");
231
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
232
+ data: relationFocusGraphData(),
233
+ theme: "shan-shui",
234
+ live: false,
235
+ focus: { kind: "community", id: "community-a" }
236
+ });
237
+
238
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
239
+
240
+ assert.equal(renderer.root.dataset.relationFocus, "active");
241
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
242
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "focus");
243
+ assert.equal(nodeElement(renderer, "b")?.dataset.relationFocusDepth, "first");
244
+ assert.equal(nodeElement(renderer, "c")?.dataset.relationFocusDepth, "first");
245
+ assert.equal(nodeElement(renderer, "d")?.dataset.relationFocusDepth, "second");
246
+ assert.equal(nodeElement(renderer, "e")?.dataset.relationFocusDepth, "unrelated");
247
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.relationFocusDepth, "first");
248
+ assert.equal(edgeElement(renderer, "b-d")?.dataset.relationFocusDepth, "second");
249
+ assert.equal(edgeElement(renderer, "d-e")?.dataset.relationFocusDepth, "unrelated");
250
+
251
+ nodeElement(renderer, "a")?.dispatch("pointerleave");
252
+ await delay(100);
253
+
254
+ assert.equal(renderer.root.dataset.relationFocus, "idle");
255
+ assert.equal(renderer.root.dataset.relationFocusNode, "");
256
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "none");
257
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.relationFocusDepth, "none");
258
+
259
+ renderer.destroy();
260
+ });
261
+
262
+ it("keeps clicked community node as fixed relation focus and lets hover temporarily override it", async () => {
263
+ const ownerDocument = new FakeDocument();
264
+ const container = ownerDocument.createElement("div");
265
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
266
+ data: relationFocusGraphData(),
267
+ theme: "shan-shui",
268
+ live: false,
269
+ focus: { kind: "community", id: "community-a" }
270
+ });
271
+
272
+ nodeElement(renderer, "a")?.dispatch("click", { detail: 0 });
273
+
274
+ assert.equal(renderer.root.dataset.relationFocus, "active");
275
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
276
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
277
+ assert.equal(nodeElement(renderer, "b")?.dataset.relationFocusDepth, "first");
278
+
279
+ nodeElement(renderer, "d")?.dispatch("pointerenter");
280
+
281
+ assert.equal(renderer.root.dataset.relationFocusNode, "d");
282
+ assert.equal(nodeElement(renderer, "d")?.dataset.relationFocusDepth, "focus");
283
+ assert.equal(nodeElement(renderer, "b")?.dataset.relationFocusDepth, "first");
284
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "second");
285
+
286
+ nodeElement(renderer, "d")?.dispatch("pointerleave");
287
+ await delay(100);
288
+
289
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
290
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "focus");
291
+ assert.equal(nodeElement(renderer, "d")?.dataset.relationFocusDepth, "second");
292
+
293
+ dispatchPointerSequence(renderer.root as unknown as FakeElement, 20, 20);
294
+
295
+ assert.equal(renderer.root.dataset.relationFocus, "idle");
296
+ assert.equal(renderer.root.dataset.relationFocusNode, "");
297
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "false");
298
+
299
+ renderer.destroy();
300
+ });
301
+
302
+ it("does not open node hover preview cards inside focused community view", async () => {
303
+ const ownerDocument = new FakeDocument();
304
+ const container = ownerDocument.createElement("div");
305
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
306
+ data: relationFocusGraphData(),
307
+ theme: "shan-shui",
308
+ live: false,
309
+ focus: { kind: "community", id: "community-a" }
310
+ });
311
+
312
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
313
+ await delay(360);
314
+
315
+ const preview = findByClass(renderer.root as unknown as FakeElement, "graph-hover-preview")[0];
316
+ assert.notEqual(preview?.dataset.state, "open");
317
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
318
+
319
+ renderer.destroy();
320
+ });
321
+
322
+ it("keeps relation focus continuous when hovering between community nodes", async () => {
323
+ const ownerDocument = new FakeDocument();
324
+ const container = ownerDocument.createElement("div");
325
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
326
+ data: relationFocusGraphData(),
327
+ theme: "shan-shui",
328
+ live: false,
329
+ focus: { kind: "community", id: "community-a" }
330
+ });
331
+
332
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
333
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
334
+
335
+ nodeElement(renderer, "a")?.dispatch("pointerleave");
336
+ nodeElement(renderer, "b")?.dispatch("pointerenter");
337
+
338
+ assert.equal(renderer.root.dataset.relationFocusNode, "b");
339
+ assert.notEqual(renderer.root.dataset.relationFocus, "idle");
340
+
341
+ renderer.destroy();
342
+ });
343
+
344
+ it("does not apply relation focus outside focused community view", () => {
345
+ const ownerDocument = new FakeDocument();
346
+ const container = ownerDocument.createElement("div");
347
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
348
+ data: relationFocusGraphData(),
349
+ theme: "shan-shui",
350
+ live: false
351
+ });
352
+
353
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
354
+
355
+ assert.equal(renderer.root.dataset.relationFocus, "idle");
356
+ assert.equal(renderer.root.dataset.relationFocusNode, "");
357
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "none");
358
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.relationFocusDepth, "none");
359
+
360
+ renderer.destroy();
361
+ });
362
+
363
+ it("keeps type-filter-hidden nodes and edges hidden during community relation focus", () => {
364
+ const ownerDocument = new FakeDocument();
365
+ const container = ownerDocument.createElement("div");
366
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
367
+ data: relationFocusGraphData(),
368
+ theme: "shan-shui",
369
+ live: false,
370
+ focus: { kind: "community", id: "community-a" }
371
+ });
372
+
373
+ renderer.setTypeFilters({ entity: false });
374
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
375
+
376
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "hidden");
377
+ assert.equal(nodeElement(renderer, "d")?.dataset.filterState, "hidden");
378
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.filterState, "hidden");
379
+ assert.equal(edgeElement(renderer, "b-d")?.dataset.filterState, "hidden");
380
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.relationFocusDepth, "first");
381
+ assert.equal(edgeElement(renderer, "b-d")?.dataset.relationFocusDepth, "second");
382
+
383
+ renderer.destroy();
384
+ });
385
+
386
+ it("marks focused community DOM output as the scoped lightweight graph view", () => {
387
+ const ownerDocument = new FakeDocument();
388
+ const container = ownerDocument.createElement("div");
389
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
390
+ data: relationFocusGraphData(),
391
+ theme: "shan-shui",
392
+ live: false,
393
+ focus: { kind: "community", id: "community-a" }
394
+ });
395
+
396
+ assert.equal(renderer.root.dataset.communityMapState, "lightweight");
397
+ assert.equal(renderer.root.dataset.relationFocus, "idle");
398
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "none");
399
+
400
+ renderer.destroy();
401
+ });
402
+
403
+ it("clears community relation focus when the view returns to global", () => {
404
+ const ownerDocument = new FakeDocument();
405
+ const container = ownerDocument.createElement("div");
406
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
407
+ data: relationFocusGraphData(),
408
+ theme: "shan-shui",
409
+ live: false,
410
+ focus: { kind: "community", id: "community-a" }
411
+ });
412
+
413
+ nodeElement(renderer, "a")?.dispatch("pointerenter");
414
+ assert.equal(renderer.root.dataset.relationFocus, "active");
415
+ assert.equal(renderer.root.dataset.relationFocusNode, "a");
416
+
417
+ renderer.render({ focus: null });
418
+
419
+ assert.equal(renderer.root.dataset.relationFocus, "idle");
420
+ assert.equal(renderer.root.dataset.relationFocusNode, "");
421
+ assert.equal(nodeElement(renderer, "a")?.dataset.relationFocusDepth, "none");
422
+
423
+ renderer.destroy();
424
+ });
425
+
426
+ it("keeps node double click from silently unpinning or changing focus", () => {
427
+ const ownerDocument = new FakeDocument();
428
+ const container = ownerDocument.createElement("div");
429
+ const pinsChanged: unknown[] = [];
430
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
431
+ data: graphDataWithCommunities([
432
+ ["a", "community-a"],
433
+ ["b", "community-a"],
434
+ ["c", "community-b"]
435
+ ]),
436
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
437
+ theme: "shan-shui",
438
+ live: false,
439
+ focus: { kind: "community", id: "community-a" },
440
+ onPinsChanged: (pins) => pinsChanged.push(pins)
441
+ });
442
+
443
+ nodeElement(renderer, "a")?.dispatch("dblclick");
444
+
445
+ assert.deepEqual(pinsChanged, []);
446
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
447
+ assert.ok(nodeElement(renderer, "a"));
448
+ assert.ok(nodeElement(renderer, "b"));
449
+ assert.equal(nodeElement(renderer, "c"), undefined);
450
+
451
+ renderer.destroy();
452
+ });
453
+
454
+ it("fixes and unfixes node position only through the explicit renderer action", () => {
455
+ const ownerDocument = new FakeDocument();
456
+ const container = ownerDocument.createElement("div");
457
+ const pinsChanged: unknown[] = [];
458
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
459
+ data: graphData(["a"]),
460
+ theme: "shan-shui",
461
+ live: false,
462
+ onPinsChanged: (pins) => pinsChanged.push(pins)
463
+ });
464
+
465
+ assert.equal(renderer.setNodeFixed("a", "fix"), true);
466
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
467
+ assert.deepEqual(Object.keys(pinsChanged.at(-1) as Record<string, unknown>), ["wiki/a.md"]);
468
+
469
+ assert.equal(renderer.setNodeFixed("a", "unfix"), true);
470
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "false");
471
+ assert.deepEqual(pinsChanged.at(-1), {});
472
+
473
+ renderer.destroy();
474
+ });
475
+
476
+ it("returns global while preserving selection, search, filters, and fixed positions", async () => {
477
+ const ownerDocument = new FakeDocument();
478
+ const container = ownerDocument.createElement("div");
479
+ const clearRequests: number[] = [];
480
+ const viewResets: number[] = [];
481
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
482
+ data: graphDataForReturnGlobal(),
483
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
484
+ theme: "shan-shui",
485
+ live: false,
486
+ focus: { kind: "community", id: "community-a" },
487
+ typeFilters: { entity: true, source: true },
488
+ onSelectionClearRequested: () => clearRequests.push(1),
489
+ onViewReset: () => viewResets.push(1)
490
+ });
491
+
492
+ renderer.setTypeFilters({ entity: true, source: false });
493
+ renderer.select({ kind: "node", id: "a" });
494
+ const searchInput = findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0];
495
+ searchInput.value = "Node a";
496
+ searchInput.dispatch("input");
497
+
498
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b"]);
499
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "hidden");
500
+
501
+ renderer.resetView();
502
+ await waitForInteractionSettle();
503
+
504
+ assert.deepEqual(viewResets, [1]);
505
+ assert.deepEqual(clearRequests, []);
506
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b", "c"]);
507
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
508
+ assert.equal(nodeElement(renderer, "c")?.getAttribute("aria-pressed"), "false");
509
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
510
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "hidden");
511
+ assert.equal(findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0]?.value, "Node a");
512
+ assert.equal(nodeElement(renderer, "a")?.dataset.searchState, "match");
513
+ assert.equal(nodeElement(renderer, "c")?.dataset.searchState, "faded");
514
+
515
+ renderer.destroy();
516
+ });
517
+
518
+ it("routes the community toolbar return through Sigma global without rendering DOM full graph", () => {
519
+ const ownerDocument = new FakeDocument();
520
+ const container = ownerDocument.createElement("div");
521
+ const viewResets: number[] = [];
522
+ const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
523
+ const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
524
+ state: {
525
+ data: graphDataForReturnGlobal(),
526
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
527
+ theme: "shan-shui",
528
+ focus: null,
529
+ typeFilters: { entity: true, source: true },
530
+ aggregationMarkers: [],
531
+ selection: null,
532
+ searchResultIds: [],
533
+ temporaryObject: null
534
+ },
535
+ callbacks: {
536
+ onViewReset: () => viewResets.push(1)
537
+ },
538
+ factories: {
539
+ createSigmaGlobal: (input) => {
540
+ sigmaInputs.push(input);
541
+ return createSigmaShellRenderer(input);
542
+ }
543
+ }
544
+ });
545
+
546
+ manager.focusCommunity("community-a");
547
+ manager.setTypeFilters({ entity: true, source: false });
548
+ manager.select({ kind: "node", id: "a" });
549
+ manager.setPins({ "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } });
550
+ const searchInput = findByClass(container, "graph-search-input")[0];
551
+ searchInput.value = "Node a";
552
+ searchInput.dispatch("input");
553
+
554
+ assert.equal(manager.routeId, "dom-svg-community");
555
+ assert.deepEqual(visibleNodeIds({ root: container as unknown as HTMLElement }), ["a", "b"]);
556
+ assert.equal(nodeElement({ root: container as unknown as HTMLElement }, "b")?.dataset.filterState, "hidden");
557
+
558
+ findByText(container, "回全图")?.dispatch("click");
559
+
560
+ assert.equal(manager.routeId, "sigma-global");
561
+ assert.equal(findByClass(container, "sigma-global-route").length, 1);
562
+ assert.deepEqual(visibleNodeIds({ root: container as unknown as HTMLElement }), []);
563
+ assert.equal(sigmaInputs.at(-1)?.options.focus, null);
564
+ const activeSigmaShell = findByClass(container, "sigma-global-route")[0];
565
+ assert.equal(activeSigmaShell?.dataset.selectedKind, "node");
566
+ assert.equal(activeSigmaShell?.dataset.selectedId, "a");
567
+ assert.equal(activeSigmaShell?.dataset.searchResultIds, "a");
568
+ assert.equal(activeSigmaShell?.dataset.typeFilters, "entity:true,source:false");
569
+ assert.equal(activeSigmaShell?.dataset.pinnedPaths, "wiki/a.md");
570
+ assert.deepEqual(sigmaInputs.at(-1)?.options.selection, { kind: "node", id: "a" });
571
+ assert.deepEqual(sigmaInputs.at(-1)?.options.searchResultIds, ["a"]);
572
+ assert.deepEqual(sigmaInputs.at(-1)?.options.typeFilters, { entity: true, source: false });
573
+ assert.deepEqual(Object.keys(sigmaInputs.at(-1)?.options.pins || {}), ["wiki/a.md"]);
574
+ assert.deepEqual(viewResets, [1]);
575
+
576
+ manager.destroy();
577
+ });
578
+
579
+ it("clears saved pins when resetting the Sigma global layout", () => {
580
+ const ownerDocument = new FakeDocument();
581
+ const container = ownerDocument.createElement("div");
582
+ const pinsChanged: unknown[] = [];
583
+ const state = {
584
+ data: graphDataForReturnGlobal(),
585
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
586
+ theme: "shan-shui" as const,
587
+ focus: null,
588
+ typeFilters: { entity: true, source: true },
589
+ aggregationMarkers: [],
590
+ selection: null,
591
+ searchResultIds: [],
592
+ temporaryObject: null
593
+ };
594
+ const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
595
+ state,
596
+ callbacks: {
597
+ onPinsChanged: (pins) => pinsChanged.push(pins)
598
+ },
599
+ factories: {
600
+ createSigmaGlobal: (input) => createSigmaShellRenderer(input)
601
+ }
602
+ });
603
+ const sigmaShell = findByClass(container, "sigma-global-route")[0];
604
+
605
+ assert.equal(manager.routeId, "sigma-global");
606
+ assert.equal(sigmaShell?.dataset.pinnedPaths, "wiki/a.md");
607
+
608
+ manager.resetLayout();
609
+
610
+ assert.deepEqual(pinsChanged, [{}]);
611
+ assert.deepEqual(state.pins, {});
612
+ assert.equal(sigmaShell?.dataset.pinnedPaths, "");
613
+
614
+ manager.select({ kind: "community", id: "community-a" });
615
+ assert.deepEqual(state.pins, {});
616
+ assert.equal(sigmaShell?.dataset.selectedKind, "community");
617
+ assert.equal(sigmaShell?.dataset.selectedId, "community-a");
618
+ assert.equal(sigmaShell?.dataset.pinnedPaths, "");
619
+
620
+ manager.destroy();
621
+ });
622
+
623
+ it("clears saved pins when resetting the over-limit notice layout", () => {
624
+ const ownerDocument = new FakeDocument();
625
+ const container = ownerDocument.createElement("div");
626
+ const pinsChanged: unknown[] = [];
627
+ const state = {
628
+ data: largeFallbackGraphData(),
629
+ pins: { "wiki/large/0.md": { x: 120, y: 140, coordinateSpace: "world" } },
630
+ theme: "shan-shui" as const,
631
+ focus: null,
632
+ typeFilters: {},
633
+ aggregationMarkers: [],
634
+ selection: null,
635
+ searchResultIds: [],
636
+ temporaryObject: null
637
+ };
638
+ const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
639
+ state,
640
+ callbacks: {
641
+ onPinsChanged: (pins) => pinsChanged.push(pins)
642
+ }
643
+ });
644
+ const overLimitView = findByClass(container, "graph-over-limit-notice-view")[0];
645
+
646
+ assert.equal(manager.routeId, "over-limit-notice");
647
+ assert.equal(overLimitView?.dataset.pinnedCount, "1");
648
+
649
+ manager.resetLayout();
650
+
651
+ assert.deepEqual(pinsChanged, [{}]);
652
+ assert.deepEqual(state.pins, {});
653
+ assert.equal(overLimitView?.dataset.pinnedCount, "0");
654
+
655
+ manager.destroy();
656
+ });
657
+
658
+ it("keeps DOM/SVG small fallback on the same lightweight global interaction rules", () => {
659
+ const ownerDocument = new FakeDocument();
660
+ const container = ownerDocument.createElement("div");
661
+ const selections: SelectionInput[] = [];
662
+ const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
663
+ state: {
664
+ data: graphDataForReturnGlobal(),
665
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
666
+ theme: "shan-shui",
667
+ focus: null,
668
+ typeFilters: { entity: true, source: true },
669
+ aggregationMarkers: [],
670
+ selection: null,
671
+ searchQuery: "Node a",
672
+ searchResultIds: ["a"],
673
+ temporaryObject: null
674
+ },
675
+ callbacks: {
676
+ onSelectionInput: (selection) => selections.push(selection)
677
+ },
678
+ factories: {
679
+ createSigmaGlobal: () => {
680
+ throw new Error("WebGL unavailable");
681
+ }
682
+ }
683
+ });
684
+
685
+ assert.equal(manager.routeId, "dom-svg-small-fallback");
686
+ assert.equal(findByClass(container, "graph-aggregation-safety-container").length, 0);
687
+ assert.equal(findByClass(container, "graph-aggregation-safety-actions").length, 0);
688
+ assert.equal(findByClass(container, "community-button").length, 0);
689
+ assert.equal(findByClass(container, "community-wash").length > 0, true);
690
+ assert.deepEqual(visibleNodeIds({ root: container as unknown as HTMLElement }), ["a", "b", "c"]);
691
+ assert.equal(nodeElement({ root: container as unknown as HTMLElement }, "a")?.dataset.searchState, "match");
692
+ assert.equal(nodeElement({ root: container as unknown as HTMLElement }, "a")?.dataset.pinned, "true");
693
+
694
+ nodeElement({ root: container as unknown as HTMLElement }, "a")?.dispatch("click", { detail: 0 });
695
+ assert.deepEqual(selections.at(-1), { kind: "node", id: "a" });
696
+ assert.equal(manager.routeId, "dom-svg-small-fallback");
697
+
698
+ findByDataset(container, "communityId", "community-a")?.dispatch("click");
699
+ assert.deepEqual(selections.at(-1), { kind: "community", id: "community-a" });
700
+ assert.equal(manager.routeId, "dom-svg-small-fallback");
701
+
702
+ manager.focusCommunity("community-a");
703
+ assert.equal(manager.routeId, "dom-svg-community");
704
+ findByText(container, "回全图")?.dispatch("click");
705
+ assert.equal(manager.routeId, "dom-svg-small-fallback");
706
+
707
+ manager.setData(largeFallbackGraphData());
708
+ assert.equal(manager.routeId, "over-limit-notice");
709
+ assert.equal(findByClass(container, "graph-over-limit-notice").length, 1);
710
+ assert.deepEqual(visibleNodeIds({ root: container as unknown as HTMLElement }), []);
711
+
712
+ manager.destroy();
713
+ });
714
+
715
+ it("keeps reset layout separate from return global", async () => {
716
+ const ownerDocument = new FakeDocument();
717
+ const container = ownerDocument.createElement("div");
718
+ const viewResets: number[] = [];
719
+ const pinsChanged: unknown[] = [];
720
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
721
+ data: graphDataForReturnGlobal(),
722
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
723
+ theme: "shan-shui",
724
+ live: false,
725
+ focus: { kind: "community", id: "community-a" },
726
+ typeFilters: { entity: true, source: true },
727
+ onViewReset: () => viewResets.push(1),
728
+ onPinsChanged: (pins) => pinsChanged.push(pins)
729
+ });
730
+
731
+ renderer.select({ kind: "node", id: "a" });
732
+ const searchInput = findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0];
733
+ searchInput.value = "Node a";
734
+ searchInput.dispatch("input");
735
+
736
+ renderer.resetLayout();
737
+ await waitForViewportCommit();
738
+
739
+ assert.deepEqual(viewResets, []);
740
+ assert.deepEqual(pinsChanged.at(-1), {});
741
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b"]);
742
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
743
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "false");
744
+ assert.equal(findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0]?.value, "Node a");
745
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "visible");
746
+ assert.equal(nodeElement(renderer, "c"), undefined);
747
+
748
+ renderer.destroy();
749
+ });
750
+
751
+ it("returns global with a selected community still selected", async () => {
752
+ const ownerDocument = new FakeDocument();
753
+ const container = ownerDocument.createElement("div");
754
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
755
+ data: graphDataForReturnGlobal(),
756
+ theme: "shan-shui",
757
+ live: false,
758
+ focus: { kind: "community", id: "community-a" }
759
+ });
760
+
761
+ renderer.select({ kind: "community", id: "community-a" });
762
+ renderer.resetView();
763
+ await waitForInteractionSettle();
764
+
765
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b", "c"]);
766
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "true");
767
+ assert.equal(nodeElement(renderer, "b")?.getAttribute("aria-pressed"), "true");
768
+ assert.equal(nodeElement(renderer, "c")?.getAttribute("aria-pressed"), "false");
769
+
770
+ renderer.destroy();
771
+ });
772
+
773
+ it("updates toolbar panel state without repainting the graph", () => {
774
+ const ownerDocument = new FakeDocument();
775
+ const container = ownerDocument.createElement("div");
776
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
777
+ data: graphData(["a"]),
778
+ theme: "shan-shui",
779
+ live: false
780
+ });
781
+
782
+ const toolbar = findByClass(renderer.root as unknown as FakeElement, "graph-toolbar")[0];
783
+ const filtersButton = findByText(toolbar, "筛选");
784
+ const legendButton = findByText(toolbar, "图例");
785
+ const panel = findByClass(toolbar, "graph-toolbar-panel")[0];
786
+ const node = nodeElement(renderer, "a");
787
+
788
+ filtersButton?.dispatch("click");
789
+
790
+ assert.equal(renderer.root.dataset.toolbarPanel, "filters");
791
+ assert.equal(toolbar.dataset.panel, "filters");
792
+ assert.equal(panel.dataset.state, "filters");
793
+ assert.equal(filtersButton?.dataset.active, "true");
794
+ assert.equal(legendButton?.dataset.active, "false");
795
+ assert.equal(findByClass(renderer.root as unknown as FakeElement, "graph-toolbar")[0], toolbar);
796
+ assert.equal(nodeElement(renderer, "a"), node);
797
+
798
+ renderer.destroy();
799
+ });
800
+
801
+ it("updates search highlights without rebuilding graph elements or moving layout", () => {
802
+ const ownerDocument = new FakeDocument();
803
+ const container = ownerDocument.createElement("div");
804
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
805
+ data: graphDataForReturnGlobal(),
806
+ theme: "shan-shui",
807
+ live: false
808
+ });
809
+
810
+ const node = nodeElement(renderer, "a");
811
+ const otherNode = nodeElement(renderer, "b");
812
+ const edge = edgeElement(renderer, "a-b");
813
+ const contentLayer = findByClass(renderer.root as unknown as FakeElement, "graph-content-layer")[0];
814
+ assert.ok(node);
815
+ assert.ok(otherNode);
816
+ assert.ok(edge);
817
+ const nodeLeft = node.style.left;
818
+ const nodeTop = node.style.top;
819
+ const transform = contentLayer?.style.transform;
820
+
821
+ const searchInput = findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0];
822
+ searchInput.value = "Node a";
823
+ searchInput.dispatch("input");
824
+
825
+ assert.equal(nodeElement(renderer, "a"), node);
826
+ assert.equal(nodeElement(renderer, "b"), otherNode);
827
+ assert.equal(edgeElement(renderer, "a-b"), edge);
828
+ assert.equal(node.style.left, nodeLeft);
829
+ assert.equal(node.style.top, nodeTop);
830
+ assert.equal(contentLayer?.style.transform, transform);
831
+ assert.equal(node.dataset.searchState, "match");
832
+ assert.equal(otherNode.dataset.searchState, "faded");
833
+
834
+ renderer.destroy();
835
+ });
836
+
837
+ it("supports keyboard search result navigation and lightweight activation", () => {
838
+ const ownerDocument = new FakeDocument();
839
+ const container = ownerDocument.createElement("div");
840
+ const selections: SelectionInput[] = [];
841
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
842
+ data: graphDataForReturnGlobal(),
843
+ theme: "shan-shui",
844
+ live: false,
845
+ onSelectionInput: (selection) => selections.push(selection)
846
+ });
847
+
848
+ const searchInput = findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0];
849
+ searchInput.value = "Node";
850
+ searchInput.dispatch("focus");
851
+ searchInput.dispatch("input");
852
+
853
+ searchInput.dispatch("keydown", { key: "ArrowDown" });
854
+ assert.equal(nodeElement(renderer, "a")?.dataset.searchFocus, "true");
855
+ assert.equal(findByClass(renderer.root as unknown as FakeElement, "graph-search-status")[0]?.textContent, "1/3");
856
+
857
+ searchInput.dispatch("keydown", { key: "ArrowUp" });
858
+ assert.equal(nodeElement(renderer, "c")?.dataset.searchFocus, "true");
859
+ assert.equal(findByClass(renderer.root as unknown as FakeElement, "graph-search-status")[0]?.textContent, "3/3");
860
+
861
+ searchInput.dispatch("keydown", { key: "Enter" });
862
+ assert.deepEqual(selections.at(-1), { kind: "node", id: "c" });
863
+ assert.equal(nodeElement(renderer, "c")?.getAttribute("aria-pressed"), "true");
864
+
865
+ renderer.destroy();
866
+ });
867
+
868
+ it("applies Escape priority without clearing pins or resetting layout", async () => {
869
+ const ownerDocument = new FakeDocument();
870
+ const container = ownerDocument.createElement("div");
871
+ const clearRequests: number[] = [];
872
+ const viewResets: number[] = [];
873
+ const pinsChanged: unknown[] = [];
874
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
875
+ data: graphDataForReturnGlobal(),
876
+ pins: { "wiki/a.md": { x: 120, y: 140, coordinateSpace: "world" } },
877
+ theme: "shan-shui",
878
+ live: false,
879
+ focus: { kind: "community", id: "community-a" },
880
+ onSelectionClearRequested: () => clearRequests.push(1),
881
+ onViewReset: () => viewResets.push(1),
882
+ onPinsChanged: (pins) => pinsChanged.push(pins)
883
+ });
884
+
885
+ renderer.root.focus();
886
+ assert.equal(renderer.root.tabIndex, 0);
887
+ assert.equal(ownerDocument.activeElement, renderer.root);
888
+ ownerDocument.dispatch("keydown", { key: "f", metaKey: true });
889
+ assert.equal(renderer.root.dataset.searchOpen, "true");
890
+ const searchInput = findByClass(renderer.root as unknown as FakeElement, "graph-search-input")[0];
891
+ assert.equal(ownerDocument.activeElement, searchInput);
892
+
893
+ ownerDocument.dispatch("keydown", { key: "Escape" });
894
+ assert.equal(renderer.root.dataset.searchOpen, "false");
895
+ assert.equal(ownerDocument.activeElement, renderer.root);
896
+ assert.deepEqual(viewResets, []);
897
+ assert.deepEqual(pinsChanged, []);
898
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
899
+
900
+ renderer.select({ kind: "node", id: "a" });
901
+ renderer.root.focus();
902
+ ownerDocument.dispatch("keydown", { key: "Escape" });
903
+ assert.deepEqual(clearRequests, [1]);
904
+ assert.deepEqual(viewResets, []);
905
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b"]);
906
+ assert.equal(nodeElement(renderer, "a")?.getAttribute("aria-pressed"), "false");
907
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
908
+
909
+ ownerDocument.dispatch("keydown", { key: "Escape" });
910
+ await waitForViewportCommit();
911
+
912
+ assert.deepEqual(viewResets, [1]);
913
+ assert.deepEqual(pinsChanged, []);
914
+ assert.deepEqual(visibleNodeIds(renderer), ["a", "b", "c"]);
915
+ assert.equal(nodeElement(renderer, "a")?.dataset.pinned, "true");
916
+
917
+ renderer.destroy();
918
+ });
919
+
920
+ it("updates type filters without rebuilding graph elements or moving layout", () => {
921
+ const ownerDocument = new FakeDocument();
922
+ const container = ownerDocument.createElement("div");
923
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
924
+ data: graphDataForReturnGlobal(),
925
+ theme: "shan-shui",
926
+ live: false,
927
+ typeFilters: { entity: true, source: true }
928
+ });
929
+
930
+ const entityNode = nodeElement(renderer, "a");
931
+ const sourceNode = nodeElement(renderer, "b");
932
+ const edge = edgeElement(renderer, "a-b");
933
+ assert.ok(entityNode);
934
+ assert.ok(sourceNode);
935
+ assert.ok(edge);
936
+ const entityLeft = entityNode.style.left;
937
+ const entityTop = entityNode.style.top;
938
+ const sourceLeft = sourceNode.style.left;
939
+ const sourceTop = sourceNode.style.top;
940
+ const edgePath = edge.getAttribute("d");
941
+
942
+ renderer.setTypeFilters({ entity: true, source: false });
943
+
944
+ assert.equal(nodeElement(renderer, "a"), entityNode);
945
+ assert.equal(nodeElement(renderer, "b"), sourceNode);
946
+ assert.equal(edgeElement(renderer, "a-b"), edge);
947
+ assert.equal(entityNode.style.left, entityLeft);
948
+ assert.equal(entityNode.style.top, entityTop);
949
+ assert.equal(sourceNode.style.left, sourceLeft);
950
+ assert.equal(sourceNode.style.top, sourceTop);
951
+ assert.equal(edge.getAttribute("d"), edgePath);
952
+ assert.equal(entityNode.dataset.filterState, "visible");
953
+ assert.equal(sourceNode.dataset.filterState, "hidden");
954
+ assert.equal(edge.dataset.filterState, "hidden");
955
+
956
+ renderer.setTypeFilters({ entity: true, source: true });
957
+
958
+ assert.equal(nodeElement(renderer, "a"), entityNode);
959
+ assert.equal(nodeElement(renderer, "b"), sourceNode);
960
+ assert.equal(edgeElement(renderer, "a-b"), edge);
961
+ assert.equal(entityNode.dataset.filterState, "visible");
962
+ assert.equal(sourceNode.dataset.filterState, "visible");
963
+ assert.equal(edge.dataset.filterState, "visible");
964
+
965
+ renderer.destroy();
966
+ });
967
+
968
+ it("temporarily reveals a filtered selected node with one-hop context without clearing filters", () => {
969
+ const ownerDocument = new FakeDocument();
970
+ const container = ownerDocument.createElement("div");
971
+ const visibilityStates: unknown[] = [];
972
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
973
+ data: graphDataForReturnGlobal(),
974
+ theme: "shan-shui",
975
+ live: false,
976
+ typeFilters: { entity: true, source: true },
977
+ onVisibilityStateChange: (state) => visibilityStates.push(state)
978
+ });
979
+
980
+ renderer.setTypeFilters({ entity: true, source: false });
981
+
982
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "hidden");
983
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.filterState, "hidden");
984
+
985
+ renderer.showTemporaryObject({ kind: "node", nodeId: "b" });
986
+
987
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "visible");
988
+ assert.equal(nodeElement(renderer, "a")?.dataset.filterState, "visible");
989
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.filterState, "visible");
990
+ assert.equal(renderer.root.dataset.typeFiltersActive, "true");
991
+ const temporaryState = visibilityStates.find((state) =>
992
+ JSON.stringify((state as { temporaryObject: unknown }).temporaryObject) === JSON.stringify({ kind: "node", nodeId: "b" })
993
+ ) as { temporaryObject: unknown; typeFilters: Record<string, boolean> } | undefined;
994
+ assert.ok(temporaryState);
995
+ assert.equal(temporaryState.typeFilters.source, false);
996
+
997
+ renderer.clearTemporaryObjectDisplay();
998
+
999
+ assert.equal(nodeElement(renderer, "b")?.dataset.filterState, "hidden");
1000
+ assert.equal(edgeElement(renderer, "a-b")?.dataset.filterState, "hidden");
1001
+ assert.equal(renderer.root.dataset.typeFiltersActive, "true");
1002
+ assert.equal((visibilityStates.at(-1) as { temporaryObject: unknown } | undefined)?.temporaryObject, null);
1003
+
1004
+ renderer.destroy();
1005
+ });
1006
+
1007
+ it("degrades interaction detail during lightweight viewport changes and restores after settle", async () => {
1008
+ const ownerDocument = new FakeDocument();
1009
+ const container = ownerDocument.createElement("div");
1010
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
1011
+ data: connectedGraphData(["a", "b", "c"]),
1012
+ theme: "shan-shui",
1013
+ live: false
1014
+ });
1015
+
1016
+ const nodeBefore = nodeElement(renderer, "a");
1017
+ const edgeBefore = edgeElement(renderer, "a-b");
1018
+ assert.equal(renderer.root.dataset.interactionMode, "idle");
1019
+
1020
+ const root = renderer.root as unknown as FakeElement;
1021
+ root.dispatch("pointerdown", { pointerId: 1, clientX: 60, clientY: 80 });
1022
+ root.dispatch("pointermove", { pointerId: 1, clientX: 120, clientY: 120 });
1023
+
1024
+ assert.equal(renderer.root.dataset.interactionMode, "active");
1025
+ assert.equal(nodeElement(renderer, "a"), nodeBefore);
1026
+ assert.equal(edgeElement(renderer, "a-b"), edgeBefore);
1027
+ assert.ok(Number(renderer.root.dataset.interactionUpdatedObjects || "0") <= Number(renderer.root.dataset.interactionMaxUpdates || "0"));
1028
+ assert.equal(nodeBefore?.dataset.coreAnchor, "true");
1029
+ assert.equal(nodeBefore?.dataset.traceable, "true");
1030
+
1031
+ await waitForInteractionSettle();
1032
+ assert.equal(renderer.root.dataset.interactionMode, "idle");
1033
+ assert.equal(nodeElement(renderer, "a"), nodeBefore);
1034
+ assert.equal(edgeElement(renderer, "a-b"), edgeBefore);
1035
+
1036
+ renderer.destroy();
1037
+ });
1038
+
1039
+ it("does not let stale diff settlement mutate a refreshed graph", async () => {
1040
+ const ownerDocument = new FakeDocument();
1041
+ const container = ownerDocument.createElement("div");
1042
+ const renderer = createGraphRenderer(container as unknown as HTMLElement, {
1043
+ data: graphData(["a"]),
1044
+ theme: "shan-shui",
1045
+ live: false
1046
+ });
1047
+
1048
+ const staleDiff = renderer.applyDiff(diff({ addedNodes: ["a"], nodeCount: 1 }), { durationMs: 420 });
1049
+ assert.equal(renderer.root.dataset.diffState, "playing");
1050
+ assert.equal(nodeElement(renderer, "a")?.classList.contains("is-diff-added"), true);
1051
+
1052
+ renderer.setData(graphData(["b"]));
1053
+ assert.equal(renderer.root.dataset.diffState, undefined);
1054
+ assert.equal(nodeElement(renderer, "a"), undefined);
1055
+
1056
+ const currentDiff = renderer.applyDiff(diff({ addedNodes: ["b"], nodeCount: 1 }), { durationMs: 420 });
1057
+ assert.equal(renderer.root.dataset.diffState, "playing");
1058
+ assert.equal(nodeElement(renderer, "b")?.classList.contains("is-diff-added"), true);
1059
+
1060
+ await staleDiff;
1061
+ assert.equal(renderer.root.dataset.diffState, "playing");
1062
+ assert.equal(nodeElement(renderer, "b")?.classList.contains("is-diff-added"), true);
1063
+
1064
+ await currentDiff;
1065
+ assert.equal(renderer.root.dataset.diffState, "settled");
1066
+ assert.equal(nodeElement(renderer, "b")?.classList.contains("is-diff-added"), false);
1067
+
1068
+ renderer.destroy();
1069
+ });
1070
+ });
1071
+
1072
+ function graphData(ids: string[]): GraphData {
1073
+ return graphDataWithCommunities(ids.map((id) => [id, "community-a"]));
1074
+ }
1075
+
1076
+ function connectedGraphData(ids: string[]): GraphData {
1077
+ const data = graphData(ids);
1078
+ data.meta.total_edges = Math.max(0, ids.length - 1);
1079
+ data.edges = ids.slice(1).map((id, index) => ({
1080
+ id: `${ids[index]}-${id}`,
1081
+ from: ids[index],
1082
+ to: id,
1083
+ type: "EXTRACTED"
1084
+ }));
1085
+ return data;
1086
+ }
1087
+
1088
+ function graphDataWithCommunities(entries: Array<[string, string]>): GraphData {
1089
+ return {
1090
+ meta: {
1091
+ build_date: "2026-06-17",
1092
+ wiki_title: "Lifecycle graph",
1093
+ total_nodes: entries.length,
1094
+ total_edges: 0
1095
+ },
1096
+ nodes: entries.map(([id, community]) => ({
1097
+ id,
1098
+ label: `Node ${id}`,
1099
+ type: "topic",
1100
+ community,
1101
+ source_path: `wiki/${id}.md`,
1102
+ content: `Node ${id}`
1103
+ })),
1104
+ edges: []
1105
+ };
1106
+ }
1107
+
1108
+ function poorCommunityQualityGraphData(): GraphData {
1109
+ const nodes = Array.from({ length: 90 }, (_, index) => ({
1110
+ id: `poor-${index}`,
1111
+ label: `Poor node ${index}`,
1112
+ type: "entity",
1113
+ community: "community",
1114
+ source_path: `wiki/poor/${index}.md`,
1115
+ content: `Poor node ${index}`
1116
+ }));
1117
+ return {
1118
+ meta: {
1119
+ build_date: "2026-06-17",
1120
+ wiki_title: "Poor community quality",
1121
+ total_nodes: nodes.length,
1122
+ total_edges: 0
1123
+ },
1124
+ nodes,
1125
+ edges: [],
1126
+ learning: {
1127
+ version: 1,
1128
+ entry: { recommended_start_node_id: "poor-0", recommended_start_reason: "fixture", default_mode: "global" },
1129
+ views: {
1130
+ path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
1131
+ community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: true, degraded: true },
1132
+ global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
1133
+ },
1134
+ communities: [
1135
+ { id: "community", label: "community", node_count: nodes.length, is_weak: true }
1136
+ ]
1137
+ }
1138
+ };
1139
+ }
1140
+
1141
+ function graphDataForReturnGlobal(): GraphData {
1142
+ return {
1143
+ meta: {
1144
+ build_date: "2026-06-17",
1145
+ wiki_title: "Return global graph",
1146
+ total_nodes: 3,
1147
+ total_edges: 1
1148
+ },
1149
+ nodes: [
1150
+ { id: "a", label: "Node a", type: "entity", community: "community-a", source_path: "wiki/a.md", content: "Node a detail" },
1151
+ { id: "b", label: "Node b", type: "source", community: "community-a", source_path: "wiki/b.md", content: "Node b detail" },
1152
+ { id: "c", label: "Node c", type: "entity", community: "community-b", source_path: "wiki/c.md", content: "Node c detail" }
1153
+ ],
1154
+ edges: [
1155
+ { id: "a-b", from: "a", to: "b", type: "EXTRACTED" }
1156
+ ]
1157
+ };
1158
+ }
1159
+
1160
+ function relationFocusGraphData(): GraphData {
1161
+ return {
1162
+ meta: {
1163
+ build_date: "2026-06-22",
1164
+ wiki_title: "Relation focus graph",
1165
+ total_nodes: 5,
1166
+ total_edges: 4
1167
+ },
1168
+ nodes: [
1169
+ { id: "a", label: "Node a", type: "topic", community: "community-a", source_path: "wiki/a.md", content: "Node a" },
1170
+ { id: "b", label: "Node b", type: "entity", community: "community-a", source_path: "wiki/b.md", content: "Node b" },
1171
+ { id: "c", label: "Node c", type: "source", community: "community-a", source_path: "wiki/c.md", content: "Node c" },
1172
+ { id: "d", label: "Node d", type: "entity", community: "community-a", source_path: "wiki/d.md", content: "Node d" },
1173
+ { id: "e", label: "Node e", type: "entity", community: "community-a", source_path: "wiki/e.md", content: "Node e" }
1174
+ ],
1175
+ edges: [
1176
+ { id: "a-b", from: "a", to: "b", type: "EXTRACTED", confidence: "EXTRACTED", relation_type: "实现", weight: 1 },
1177
+ { id: "a-c", from: "a", to: "c", type: "INFERRED", confidence: "INFERRED", relation_type: "对比", weight: 0.7 },
1178
+ { id: "b-d", from: "b", to: "d", type: "EXTRACTED", confidence: "EXTRACTED", relation_type: "依赖", weight: 0.6 },
1179
+ { id: "d-e", from: "d", to: "e", type: "EXTRACTED", confidence: "EXTRACTED", relation_type: "衍生", weight: 0.5 }
1180
+ ]
1181
+ };
1182
+ }
1183
+
1184
+ function largeFallbackGraphData(): GraphData {
1185
+ const nodes = Array.from({ length: 2101 }, (_, index) => ({
1186
+ id: `large-${index}`,
1187
+ label: `Large ${index}`,
1188
+ type: "topic",
1189
+ community: index < 600 ? "large-community" : `community-${index}`,
1190
+ source_path: `wiki/large/${index}.md`,
1191
+ content: `Large ${index}`
1192
+ }));
1193
+ const edges = Array.from({ length: 4101 }, (_, index) => ({
1194
+ id: `large-edge-${index}`,
1195
+ from: nodes[index % nodes.length].id,
1196
+ to: nodes[(index + 1) % nodes.length].id,
1197
+ type: "EXTRACTED"
1198
+ }));
1199
+ return {
1200
+ meta: {
1201
+ build_date: "2026-06-19",
1202
+ wiki_title: "Large fallback graph",
1203
+ total_nodes: nodes.length,
1204
+ total_edges: edges.length
1205
+ },
1206
+ nodes,
1207
+ edges
1208
+ };
1209
+ }
1210
+
1211
+ function createSigmaShellRenderer(input: GraphFacadeRouteRendererFactoryInput): GraphFacadeRenderer {
1212
+ const shell = input.container.ownerDocument.createElement("div");
1213
+ shell.className = "sigma-global-route";
1214
+ shell.dataset.route = "sigma-global";
1215
+ input.container.append(shell);
1216
+ let options = input.options;
1217
+ renderSigmaShellState();
1218
+
1219
+ return {
1220
+ applyDiff() {
1221
+ return Promise.resolve();
1222
+ },
1223
+ isDragging() {
1224
+ return false;
1225
+ },
1226
+ setData(data, pins) {
1227
+ options = { ...options, data, pins: pins || options.pins };
1228
+ renderSigmaShellState();
1229
+ },
1230
+ setAggregationMarkers(markers) {
1231
+ options = { ...options, aggregationMarkers: markers };
1232
+ renderSigmaShellState();
1233
+ },
1234
+ focusNode(path) {
1235
+ const node = options.data.nodes.find((item) => item.id === path || item.source_path === path);
1236
+ options = { ...options, selection: node ? { kind: "node", id: node.id } : options.selection };
1237
+ renderSigmaShellState();
1238
+ },
1239
+ focusCommunity() {},
1240
+ setTypeFilters(filters) {
1241
+ options = { ...options, typeFilters: filters };
1242
+ renderSigmaShellState();
1243
+ },
1244
+ showTemporaryObject(object) {
1245
+ options = { ...options, temporaryObject: object };
1246
+ renderSigmaShellState();
1247
+ },
1248
+ clearTemporaryObjectDisplay() {
1249
+ options = { ...options, temporaryObject: null };
1250
+ renderSigmaShellState();
1251
+ },
1252
+ resetView() {
1253
+ options = { ...options, focus: null, selection: null };
1254
+ renderSigmaShellState();
1255
+ },
1256
+ select(selection) {
1257
+ options = { ...options, selection };
1258
+ renderSigmaShellState();
1259
+ },
1260
+ previewNode() {},
1261
+ clearSelection() {
1262
+ options = { ...options, selection: null };
1263
+ renderSigmaShellState();
1264
+ },
1265
+ clearInteraction() {
1266
+ options = { ...options, focus: null, selection: null, temporaryObject: null };
1267
+ renderSigmaShellState();
1268
+ },
1269
+ setNodeFixed() {
1270
+ return false;
1271
+ },
1272
+ setTheme(theme) {
1273
+ options = { ...options, theme };
1274
+ renderSigmaShellState();
1275
+ },
1276
+ setPins(pins) {
1277
+ options = { ...options, pins };
1278
+ renderSigmaShellState();
1279
+ },
1280
+ resetLayout() {
1281
+ const nextPins = {};
1282
+ options = { ...options, pins: nextPins };
1283
+ renderSigmaShellState();
1284
+ input.options.callbacks.onPinsChanged?.(nextPins);
1285
+ },
1286
+ destroy() {
1287
+ shell.remove();
1288
+ }
1289
+ };
1290
+
1291
+ function renderSigmaShellState(): void {
1292
+ const selection = options.selection;
1293
+ shell.dataset.focus = options.focus ? JSON.stringify(options.focus) : "";
1294
+ shell.dataset.selectedKind = selection?.kind || "";
1295
+ shell.dataset.selectedId = selection && "id" in selection ? selection.id : "";
1296
+ shell.dataset.searchResultIds = options.searchResultIds.join(",");
1297
+ shell.dataset.typeFilters = Object.entries(options.typeFilters)
1298
+ .sort(([left], [right]) => left.localeCompare(right))
1299
+ .map(([type, enabled]) => `${type}:${enabled}`)
1300
+ .join(",");
1301
+ shell.dataset.pinnedPaths = Object.keys(options.pins).sort().join(",");
1302
+ }
1303
+ }
1304
+
1305
+ function visibleNodeIds(renderer: { root: HTMLElement }): string[] {
1306
+ return collectNodes(renderer.root as unknown as FakeElement).map((node) => node.dataset.id || "").sort();
1307
+ }
1308
+
1309
+ function collectNodes(root: FakeElement): FakeElement[] {
1310
+ const matches: FakeElement[] = [];
1311
+ if (root.classList.contains("node")) matches.push(root);
1312
+ for (const child of root.children) matches.push(...collectNodes(child));
1313
+ return matches;
1314
+ }
1315
+
1316
+ async function waitForViewportCommit(): Promise<void> {
1317
+ await new Promise((resolve) => setTimeout(resolve, 24));
1318
+ }
1319
+
1320
+ async function waitForInteractionSettle(): Promise<void> {
1321
+ await new Promise((resolve) => setTimeout(resolve, 240));
1322
+ }
1323
+
1324
+ function delay(ms: number): Promise<void> {
1325
+ return new Promise((resolve) => setTimeout(resolve, ms));
1326
+ }
1327
+
1328
+ function diff(overrides: Partial<GraphDiff> & { nodeCount: number }): GraphDiff {
1329
+ return {
1330
+ addedNodes: overrides.addedNodes || [],
1331
+ removedNodes: overrides.removedNodes || [],
1332
+ recoloredNodes: overrides.recoloredNodes || [],
1333
+ addedEdges: overrides.addedEdges || [],
1334
+ removedEdges: overrides.removedEdges || [],
1335
+ newCommunities: overrides.newCommunities || [],
1336
+ stats: {
1337
+ nodeCount: overrides.nodeCount,
1338
+ edgeCount: 0,
1339
+ communityCount: 1
1340
+ }
1341
+ };
1342
+ }
1343
+
1344
+ function nodeElement(renderer: { root: HTMLElement }, id: string): FakeElement | undefined {
1345
+ return findByDataset(renderer.root as unknown as FakeElement, "id", id);
1346
+ }
1347
+
1348
+ function edgeElement(renderer: { root: HTMLElement }, id: string): FakeElement | undefined {
1349
+ return findByDataset(renderer.root as unknown as FakeElement, "edgeId", id);
1350
+ }
1351
+
1352
+ function findByDataset(root: FakeElement, key: string, value: string): FakeElement | undefined {
1353
+ if (root.dataset[key] === value) return root;
1354
+ for (const child of root.children) {
1355
+ const match = findByDataset(child, key, value);
1356
+ if (match) return match;
1357
+ }
1358
+ return undefined;
1359
+ }
1360
+
1361
+ class FakeDocument {
1362
+ readonly head = new FakeElement("head", this);
1363
+ activeElement: FakeElement | null = null;
1364
+ private readonly listeners = new Map<string, Array<(event: FakeEvent) => void>>();
1365
+ readonly defaultView = {
1366
+ localStorage: null,
1367
+ matchMedia: () => ({ matches: false }),
1368
+ requestAnimationFrame: (callback: () => void) => setTimeout(callback, 0) as unknown as number
1369
+ };
1370
+
1371
+ createElement(tagName: string): FakeElement {
1372
+ return new FakeElement(tagName, this);
1373
+ }
1374
+
1375
+ createElementNS(_namespace: string, tagName: string): FakeElement {
1376
+ return new FakeElement(tagName, this);
1377
+ }
1378
+
1379
+ getElementById(id: string): FakeElement | null {
1380
+ return findById(this.head, id) || null;
1381
+ }
1382
+
1383
+ addEventListener(type: string, listener: unknown): void {
1384
+ const listeners = this.listeners.get(type) || [];
1385
+ listeners.push(listener as (event: FakeEvent) => void);
1386
+ this.listeners.set(type, listeners);
1387
+ }
1388
+
1389
+ removeEventListener(type: string, listener: unknown): void {
1390
+ const listeners = this.listeners.get(type) || [];
1391
+ this.listeners.set(type, listeners.filter((candidate) => candidate !== listener));
1392
+ }
1393
+
1394
+ dispatch(type: string, init: Partial<FakeEvent> = {}): FakeEvent {
1395
+ const event = new FakeEvent(type, { ...init, target: init.target || this.activeElement });
1396
+ for (const listener of this.listeners.get(type) || []) listener(event);
1397
+ return event;
1398
+ }
1399
+ }
1400
+
1401
+ class FakeElement {
1402
+ readonly children: FakeElement[] = [];
1403
+ private readonly listeners = new Map<string, Array<(event: FakeEvent) => void>>();
1404
+ readonly dataset: Record<string, string | undefined> = {};
1405
+ readonly style = new FakeStyle();
1406
+ readonly classList = new FakeClassList(this);
1407
+ ownerDocument: FakeDocument;
1408
+ parentElement: FakeElement | null = null;
1409
+ className = "";
1410
+ textContent = "";
1411
+ type = "";
1412
+ title = "";
1413
+ href = "";
1414
+ innerHTML = "";
1415
+ checked = false;
1416
+ value = "";
1417
+ tabIndex = -1;
1418
+ scrollLeft = 0;
1419
+ scrollTop = 0;
1420
+ id = "";
1421
+ private capturedPointerId: number | null = null;
1422
+
1423
+ constructor(readonly tagName: string, ownerDocument: FakeDocument) {
1424
+ this.ownerDocument = ownerDocument;
1425
+ }
1426
+
1427
+ append(...children: Array<FakeElement | string>): void {
1428
+ for (const child of children) {
1429
+ if (typeof child === "string") {
1430
+ this.textContent += child;
1431
+ } else {
1432
+ this.appendChild(child);
1433
+ }
1434
+ }
1435
+ }
1436
+
1437
+ appendChild(child: FakeElement): FakeElement {
1438
+ child.parentElement = this;
1439
+ this.children.push(child);
1440
+ return child;
1441
+ }
1442
+
1443
+ prepend(child: FakeElement): void {
1444
+ child.parentElement = this;
1445
+ this.children.unshift(child);
1446
+ }
1447
+
1448
+ replaceChildren(...children: FakeElement[]): void {
1449
+ for (const child of this.children) child.parentElement = null;
1450
+ this.children.splice(0);
1451
+ for (const child of children) this.appendChild(child);
1452
+ }
1453
+
1454
+ remove(): void {
1455
+ if (!this.parentElement) return;
1456
+ const siblings = this.parentElement.children;
1457
+ const index = siblings.indexOf(this);
1458
+ if (index >= 0) siblings.splice(index, 1);
1459
+ this.parentElement = null;
1460
+ }
1461
+
1462
+ contains(candidate: FakeElement): boolean {
1463
+ if (candidate === this) return true;
1464
+ return this.children.some((child) => child.contains(candidate));
1465
+ }
1466
+
1467
+ setAttribute(name: string, value: string): void {
1468
+ if (name === "class") this.className = value;
1469
+ else if (name === "href") this.href = value;
1470
+ else if (name === "id") this.id = value;
1471
+ else if (name.startsWith("data-")) this.dataset[dataKey(name)] = value;
1472
+ else (this as unknown as Record<string, string>)[name] = value;
1473
+ }
1474
+
1475
+ getAttribute(name: string): string | null {
1476
+ if (name === "class") return this.className;
1477
+ if (name === "href") return this.href || null;
1478
+ if (name === "id") return this.id || null;
1479
+ if (name.startsWith("data-")) return this.dataset[dataKey(name)] || null;
1480
+ const value = (this as unknown as Record<string, string>)[name];
1481
+ return value || null;
1482
+ }
1483
+
1484
+ addEventListener(type: string, listener: unknown): void {
1485
+ const listeners = this.listeners.get(type) || [];
1486
+ listeners.push(listener as (event: FakeEvent) => void);
1487
+ this.listeners.set(type, listeners);
1488
+ }
1489
+
1490
+ removeEventListener(type: string, listener: unknown): void {
1491
+ const listeners = this.listeners.get(type) || [];
1492
+ this.listeners.set(type, listeners.filter((candidate) => candidate !== listener));
1493
+ }
1494
+
1495
+ dispatch(type: string, init: Partial<FakeEvent> = {}): void {
1496
+ const event = new FakeEvent(type, { ...init, target: init.target || this });
1497
+ for (const listener of this.listeners.get(type) || []) listener(event);
1498
+ }
1499
+
1500
+ focus(_options?: unknown): void {
1501
+ this.ownerDocument.activeElement = this;
1502
+ }
1503
+
1504
+ select(): void {}
1505
+
1506
+ setPointerCapture(pointerId: number): void {
1507
+ this.capturedPointerId = pointerId;
1508
+ }
1509
+
1510
+ releasePointerCapture(pointerId: number): void {
1511
+ if (this.capturedPointerId === pointerId) this.capturedPointerId = null;
1512
+ }
1513
+
1514
+ hasPointerCapture(pointerId: number): boolean {
1515
+ return this.capturedPointerId === pointerId;
1516
+ }
1517
+
1518
+ getBoundingClientRect(): { left: number; top: number; width: number; height: number } {
1519
+ return { left: 0, top: 0, width: 960, height: 640 };
1520
+ }
1521
+
1522
+ querySelectorAll(selector: string): FakeElement[] {
1523
+ if (selector !== ".graph-type-filter input[data-type]") return [];
1524
+ return collectElements(this).filter((element) =>
1525
+ element.tagName === "input" &&
1526
+ element.dataset.type !== undefined &&
1527
+ hasAncestorClass(element, "graph-type-filter")
1528
+ );
1529
+ }
1530
+
1531
+ querySelector(selector: string): FakeElement | null {
1532
+ if (!selector.startsWith(".")) return null;
1533
+ const className = selector.slice(1);
1534
+ return findByClass(this, className)[0] || null;
1535
+ }
1536
+ }
1537
+
1538
+ class FakeEvent {
1539
+ propagationStopped = false;
1540
+ defaultPrevented = false;
1541
+ detail = 1;
1542
+ shiftKey = false;
1543
+ button = 0;
1544
+ pointerId = 1;
1545
+ clientX = 0;
1546
+ clientY = 0;
1547
+ deltaY = 0;
1548
+ deltaMode = 0;
1549
+ ctrlKey = false;
1550
+ metaKey = false;
1551
+ target: FakeElement | null = null;
1552
+
1553
+ constructor(readonly type: string, init: Partial<FakeEvent> = {}) {
1554
+ Object.assign(this, init);
1555
+ }
1556
+
1557
+ stopPropagation(): void {
1558
+ this.propagationStopped = true;
1559
+ }
1560
+
1561
+ preventDefault(): void {
1562
+ this.defaultPrevented = true;
1563
+ }
1564
+ }
1565
+
1566
+ class FakeStyle {
1567
+ private readonly values = new Map<string, string>();
1568
+
1569
+ setProperty(name: string, value: string): void {
1570
+ this.values.set(name, value);
1571
+ }
1572
+
1573
+ removeProperty(name: string): string {
1574
+ const value = this.values.get(name) || "";
1575
+ this.values.delete(name);
1576
+ return value;
1577
+ }
1578
+
1579
+ set colorScheme(value: string) {
1580
+ this.setProperty("color-scheme", value);
1581
+ }
1582
+
1583
+ set left(value: string) {
1584
+ this.setProperty("left", value);
1585
+ }
1586
+
1587
+ set top(value: string) {
1588
+ this.setProperty("top", value);
1589
+ }
1590
+
1591
+ set translate(value: string) {
1592
+ this.setProperty("translate", value);
1593
+ }
1594
+
1595
+ set strokeWidth(value: string) {
1596
+ this.setProperty("stroke-width", value);
1597
+ }
1598
+
1599
+ set opacity(value: string) {
1600
+ this.setProperty("opacity", value);
1601
+ }
1602
+
1603
+ set cursor(value: string) {
1604
+ this.setProperty("cursor", value);
1605
+ }
1606
+
1607
+ set background(value: string) {
1608
+ this.setProperty("background", value);
1609
+ }
1610
+
1611
+ get left(): string {
1612
+ return this.values.get("left") || "";
1613
+ }
1614
+
1615
+ get top(): string {
1616
+ return this.values.get("top") || "";
1617
+ }
1618
+
1619
+ get transform(): string {
1620
+ return this.values.get("transform") || "";
1621
+ }
1622
+
1623
+ set transform(value: string) {
1624
+ this.setProperty("transform", value);
1625
+ }
1626
+
1627
+ set transformOrigin(value: string) {
1628
+ this.setProperty("transform-origin", value);
1629
+ }
1630
+ }
1631
+
1632
+ class FakeClassList {
1633
+ constructor(private readonly element: FakeElement) {}
1634
+
1635
+ add(...classNames: string[]): void {
1636
+ this.write([...this.read(), ...classNames]);
1637
+ }
1638
+
1639
+ remove(...classNames: string[]): void {
1640
+ const remove = new Set(classNames);
1641
+ this.write(this.read().filter((className) => !remove.has(className)));
1642
+ }
1643
+
1644
+ toggle(className: string, force?: boolean): void {
1645
+ const classNames = new Set(this.read());
1646
+ const shouldAdd = force ?? !classNames.has(className);
1647
+ if (shouldAdd) classNames.add(className);
1648
+ else classNames.delete(className);
1649
+ this.write([...classNames]);
1650
+ }
1651
+
1652
+ contains(className: string): boolean {
1653
+ return this.read().includes(className);
1654
+ }
1655
+
1656
+ private read(): string[] {
1657
+ return this.element.className.split(/\s+/).filter(Boolean);
1658
+ }
1659
+
1660
+ private write(classNames: string[]): void {
1661
+ this.element.className = [...new Set(classNames)].join(" ");
1662
+ }
1663
+ }
1664
+
1665
+ function findById(root: FakeElement, id: string): FakeElement | undefined {
1666
+ if (root.id === id) return root;
1667
+ for (const child of root.children) {
1668
+ const match = findById(child, id);
1669
+ if (match) return match;
1670
+ }
1671
+ return undefined;
1672
+ }
1673
+
1674
+ function findByClass(root: FakeElement, className: string): FakeElement[] {
1675
+ const matches: FakeElement[] = [];
1676
+ const classes = new Set(root.className.split(/\s+/).filter(Boolean));
1677
+ if (classes.has(className)) matches.push(root);
1678
+ for (const child of root.children) matches.push(...findByClass(child, className));
1679
+ return matches;
1680
+ }
1681
+
1682
+ function findByText(root: FakeElement, text: string): FakeElement | undefined {
1683
+ if (root.textContent === text) return root;
1684
+ for (const child of root.children) {
1685
+ const match = findByText(child, text);
1686
+ if (match) return match;
1687
+ }
1688
+ return undefined;
1689
+ }
1690
+
1691
+ function collectElements(root: FakeElement): FakeElement[] {
1692
+ return [root, ...root.children.flatMap((child) => collectElements(child))];
1693
+ }
1694
+
1695
+ function hasAncestorClass(element: FakeElement, className: string): boolean {
1696
+ let current = element.parentElement;
1697
+ while (current) {
1698
+ if (current.classList.contains(className)) return true;
1699
+ current = current.parentElement;
1700
+ }
1701
+ return false;
1702
+ }
1703
+
1704
+ function dispatchPointerSequence(root: FakeElement, x: number, y: number): void {
1705
+ root.dispatch("pointerdown", { pointerId: 1, clientX: x, clientY: y });
1706
+ root.dispatch("pointerup", { pointerId: 1, clientX: x, clientY: y });
1707
+ }
1708
+
1709
+ function dataKey(attribute: string): string {
1710
+ return attribute.slice("data-".length).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
1711
+ }