stellavault 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/.env.example +12 -0
  2. package/CLAUDE.md +39 -0
  3. package/CONTRIBUTING.md +65 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/memory/MEMORY.md +25 -0
  7. package/package.json +33 -0
  8. package/packages/cli/bin/ekh.js +2 -0
  9. package/packages/cli/bin/stellavault.js +2 -0
  10. package/packages/cli/dist/commands/brief-cmd.d.ts +2 -0
  11. package/packages/cli/dist/commands/brief-cmd.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/brief-cmd.js +82 -0
  13. package/packages/cli/dist/commands/brief-cmd.js.map +1 -0
  14. package/packages/cli/dist/commands/capture-cmd.d.ts +7 -0
  15. package/packages/cli/dist/commands/capture-cmd.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/capture-cmd.js +31 -0
  17. package/packages/cli/dist/commands/capture-cmd.js.map +1 -0
  18. package/packages/cli/dist/commands/card-cmd.d.ts +4 -0
  19. package/packages/cli/dist/commands/card-cmd.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/card-cmd.js +26 -0
  21. package/packages/cli/dist/commands/card-cmd.js.map +1 -0
  22. package/packages/cli/dist/commands/clip-cmd.d.ts +4 -0
  23. package/packages/cli/dist/commands/clip-cmd.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/clip-cmd.js +151 -0
  25. package/packages/cli/dist/commands/clip-cmd.js.map +1 -0
  26. package/packages/cli/dist/commands/cloud-cmd.d.ts +4 -0
  27. package/packages/cli/dist/commands/cloud-cmd.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/cloud-cmd.js +64 -0
  29. package/packages/cli/dist/commands/cloud-cmd.js.map +1 -0
  30. package/packages/cli/dist/commands/contradictions-cmd.d.ts +2 -0
  31. package/packages/cli/dist/commands/contradictions-cmd.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/contradictions-cmd.js +34 -0
  33. package/packages/cli/dist/commands/contradictions-cmd.js.map +1 -0
  34. package/packages/cli/dist/commands/decay-cmd.d.ts +2 -0
  35. package/packages/cli/dist/commands/decay-cmd.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/decay-cmd.js +48 -0
  37. package/packages/cli/dist/commands/decay-cmd.js.map +1 -0
  38. package/packages/cli/dist/commands/digest-cmd.d.ts +4 -0
  39. package/packages/cli/dist/commands/digest-cmd.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/digest-cmd.js +79 -0
  41. package/packages/cli/dist/commands/digest-cmd.js.map +1 -0
  42. package/packages/cli/dist/commands/duplicates-cmd.d.ts +4 -0
  43. package/packages/cli/dist/commands/duplicates-cmd.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/duplicates-cmd.js +30 -0
  45. package/packages/cli/dist/commands/duplicates-cmd.js.map +1 -0
  46. package/packages/cli/dist/commands/federate-cmd.d.ts +5 -0
  47. package/packages/cli/dist/commands/federate-cmd.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/federate-cmd.js +217 -0
  49. package/packages/cli/dist/commands/federate-cmd.js.map +1 -0
  50. package/packages/cli/dist/commands/gaps-cmd.d.ts +2 -0
  51. package/packages/cli/dist/commands/gaps-cmd.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/gaps-cmd.js +33 -0
  53. package/packages/cli/dist/commands/gaps-cmd.js.map +1 -0
  54. package/packages/cli/dist/commands/graph-cmd.d.ts +2 -0
  55. package/packages/cli/dist/commands/graph-cmd.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/graph-cmd.js +77 -0
  57. package/packages/cli/dist/commands/graph-cmd.js.map +1 -0
  58. package/packages/cli/dist/commands/index-cmd.d.ts +2 -0
  59. package/packages/cli/dist/commands/index-cmd.d.ts.map +1 -0
  60. package/packages/cli/dist/commands/index-cmd.js +57 -0
  61. package/packages/cli/dist/commands/index-cmd.js.map +1 -0
  62. package/packages/cli/dist/commands/init-cmd.d.ts +2 -0
  63. package/packages/cli/dist/commands/init-cmd.d.ts.map +1 -0
  64. package/packages/cli/dist/commands/init-cmd.js +123 -0
  65. package/packages/cli/dist/commands/init-cmd.js.map +1 -0
  66. package/packages/cli/dist/commands/learn-cmd.d.ts +2 -0
  67. package/packages/cli/dist/commands/learn-cmd.d.ts.map +1 -0
  68. package/packages/cli/dist/commands/learn-cmd.js +48 -0
  69. package/packages/cli/dist/commands/learn-cmd.js.map +1 -0
  70. package/packages/cli/dist/commands/pack-cmd.d.ts +15 -0
  71. package/packages/cli/dist/commands/pack-cmd.d.ts.map +1 -0
  72. package/packages/cli/dist/commands/pack-cmd.js +93 -0
  73. package/packages/cli/dist/commands/pack-cmd.js.map +1 -0
  74. package/packages/cli/dist/commands/review-cmd.d.ts +4 -0
  75. package/packages/cli/dist/commands/review-cmd.d.ts.map +1 -0
  76. package/packages/cli/dist/commands/review-cmd.js +107 -0
  77. package/packages/cli/dist/commands/review-cmd.js.map +1 -0
  78. package/packages/cli/dist/commands/search-cmd.d.ts +4 -0
  79. package/packages/cli/dist/commands/search-cmd.d.ts.map +1 -0
  80. package/packages/cli/dist/commands/search-cmd.js +38 -0
  81. package/packages/cli/dist/commands/search-cmd.js.map +1 -0
  82. package/packages/cli/dist/commands/serve-cmd.d.ts +2 -0
  83. package/packages/cli/dist/commands/serve-cmd.d.ts.map +1 -0
  84. package/packages/cli/dist/commands/serve-cmd.js +14 -0
  85. package/packages/cli/dist/commands/serve-cmd.js.map +1 -0
  86. package/packages/cli/dist/commands/status-cmd.d.ts +2 -0
  87. package/packages/cli/dist/commands/status-cmd.d.ts.map +1 -0
  88. package/packages/cli/dist/commands/status-cmd.js +33 -0
  89. package/packages/cli/dist/commands/status-cmd.js.map +1 -0
  90. package/packages/cli/dist/commands/sync-cmd.d.ts +5 -0
  91. package/packages/cli/dist/commands/sync-cmd.d.ts.map +1 -0
  92. package/packages/cli/dist/commands/sync-cmd.js +62 -0
  93. package/packages/cli/dist/commands/sync-cmd.js.map +1 -0
  94. package/packages/cli/dist/commands/vault-cmd.d.ts +10 -0
  95. package/packages/cli/dist/commands/vault-cmd.d.ts.map +1 -0
  96. package/packages/cli/dist/commands/vault-cmd.js +54 -0
  97. package/packages/cli/dist/commands/vault-cmd.js.map +1 -0
  98. package/packages/cli/dist/index.d.ts +2 -0
  99. package/packages/cli/dist/index.d.ts.map +1 -0
  100. package/packages/cli/dist/index.js +156 -0
  101. package/packages/cli/dist/index.js.map +1 -0
  102. package/packages/cli/package.json +24 -0
  103. package/packages/cli/src/commands/brief-cmd.ts +87 -0
  104. package/packages/cli/src/commands/capture-cmd.ts +34 -0
  105. package/packages/cli/src/commands/card-cmd.ts +29 -0
  106. package/packages/cli/src/commands/clip-cmd.ts +172 -0
  107. package/packages/cli/src/commands/cloud-cmd.ts +75 -0
  108. package/packages/cli/src/commands/contradictions-cmd.ts +41 -0
  109. package/packages/cli/src/commands/decay-cmd.ts +57 -0
  110. package/packages/cli/src/commands/digest-cmd.ts +89 -0
  111. package/packages/cli/src/commands/duplicates-cmd.ts +38 -0
  112. package/packages/cli/src/commands/federate-cmd.ts +236 -0
  113. package/packages/cli/src/commands/gaps-cmd.ts +40 -0
  114. package/packages/cli/src/commands/graph-cmd.ts +88 -0
  115. package/packages/cli/src/commands/index-cmd.ts +65 -0
  116. package/packages/cli/src/commands/init-cmd.ts +145 -0
  117. package/packages/cli/src/commands/learn-cmd.ts +56 -0
  118. package/packages/cli/src/commands/pack-cmd.ts +121 -0
  119. package/packages/cli/src/commands/review-cmd.ts +125 -0
  120. package/packages/cli/src/commands/search-cmd.ts +45 -0
  121. package/packages/cli/src/commands/serve-cmd.ts +17 -0
  122. package/packages/cli/src/commands/status-cmd.ts +37 -0
  123. package/packages/cli/src/commands/sync-cmd.ts +68 -0
  124. package/packages/cli/src/commands/vault-cmd.ts +64 -0
  125. package/packages/cli/src/index.ts +187 -0
  126. package/packages/core/package.json +40 -0
  127. package/packages/core/src/api/dashboard.ts +138 -0
  128. package/packages/core/src/api/graph-data.ts +286 -0
  129. package/packages/core/src/api/pwa.ts +82 -0
  130. package/packages/core/src/api/server.ts +660 -0
  131. package/packages/core/src/capture/voice.ts +168 -0
  132. package/packages/core/src/cloud/index.ts +2 -0
  133. package/packages/core/src/cloud/sync.ts +167 -0
  134. package/packages/core/src/config.ts +82 -0
  135. package/packages/core/src/federation/credits.ts +80 -0
  136. package/packages/core/src/federation/hyperswarm.d.ts +19 -0
  137. package/packages/core/src/federation/identity.ts +90 -0
  138. package/packages/core/src/federation/index.ts +8 -0
  139. package/packages/core/src/federation/node.ts +235 -0
  140. package/packages/core/src/federation/privacy.ts +52 -0
  141. package/packages/core/src/federation/reputation.ts +202 -0
  142. package/packages/core/src/federation/search.ts +129 -0
  143. package/packages/core/src/federation/sharing.ts +165 -0
  144. package/packages/core/src/federation/trust.ts +76 -0
  145. package/packages/core/src/federation/types.ts +25 -0
  146. package/packages/core/src/i18n/index.ts +85 -0
  147. package/packages/core/src/index.ts +133 -0
  148. package/packages/core/src/indexer/chunker.ts +180 -0
  149. package/packages/core/src/indexer/embedder.ts +9 -0
  150. package/packages/core/src/indexer/index.ts +113 -0
  151. package/packages/core/src/indexer/local-embedder.ts +35 -0
  152. package/packages/core/src/indexer/scanner.ts +142 -0
  153. package/packages/core/src/indexer/watcher.ts +62 -0
  154. package/packages/core/src/intelligence/contradiction-detector.ts +134 -0
  155. package/packages/core/src/intelligence/decay-engine.ts +229 -0
  156. package/packages/core/src/intelligence/duplicate-detector.ts +71 -0
  157. package/packages/core/src/intelligence/fsrs.ts +79 -0
  158. package/packages/core/src/intelligence/gap-detector.ts +109 -0
  159. package/packages/core/src/intelligence/learning-path.ts +86 -0
  160. package/packages/core/src/intelligence/notifications.ts +106 -0
  161. package/packages/core/src/intelligence/predictive-gaps.ts +94 -0
  162. package/packages/core/src/intelligence/semantic-versioning.ts +97 -0
  163. package/packages/core/src/intelligence/types.ts +28 -0
  164. package/packages/core/src/mcp/custom-tools.ts +97 -0
  165. package/packages/core/src/mcp/index.ts +1 -0
  166. package/packages/core/src/mcp/server.ts +142 -0
  167. package/packages/core/src/mcp/tools/agentic-graph.ts +96 -0
  168. package/packages/core/src/mcp/tools/brief.ts +49 -0
  169. package/packages/core/src/mcp/tools/decay.ts +40 -0
  170. package/packages/core/src/mcp/tools/decision-journal.ts +95 -0
  171. package/packages/core/src/mcp/tools/export.ts +72 -0
  172. package/packages/core/src/mcp/tools/federated-search.ts +43 -0
  173. package/packages/core/src/mcp/tools/generate-claude-md.ts +130 -0
  174. package/packages/core/src/mcp/tools/get-document.ts +26 -0
  175. package/packages/core/src/mcp/tools/get-related.ts +41 -0
  176. package/packages/core/src/mcp/tools/learning-path.ts +52 -0
  177. package/packages/core/src/mcp/tools/list-topics.ts +20 -0
  178. package/packages/core/src/mcp/tools/search.ts +35 -0
  179. package/packages/core/src/mcp/tools/snapshot.ts +98 -0
  180. package/packages/core/src/multi-vault/index.ts +118 -0
  181. package/packages/core/src/pack/creator.ts +127 -0
  182. package/packages/core/src/pack/exporter.ts +21 -0
  183. package/packages/core/src/pack/importer.ts +82 -0
  184. package/packages/core/src/pack/index.ts +5 -0
  185. package/packages/core/src/pack/marketplace.ts +103 -0
  186. package/packages/core/src/pack/pii-masker.ts +38 -0
  187. package/packages/core/src/pack/types.ts +39 -0
  188. package/packages/core/src/plugins/index.ts +100 -0
  189. package/packages/core/src/plugins/webhooks.ts +110 -0
  190. package/packages/core/src/search/bm25.ts +16 -0
  191. package/packages/core/src/search/index.ts +83 -0
  192. package/packages/core/src/search/rrf.ts +31 -0
  193. package/packages/core/src/search/semantic.ts +15 -0
  194. package/packages/core/src/store/index.ts +2 -0
  195. package/packages/core/src/store/sqlite-vec.ts +290 -0
  196. package/packages/core/src/store/types.ts +22 -0
  197. package/packages/core/src/team/index.ts +126 -0
  198. package/packages/core/src/types/chunk.ts +25 -0
  199. package/packages/core/src/types/document.ts +24 -0
  200. package/packages/core/src/types/graph.ts +44 -0
  201. package/packages/core/src/types/index.ts +15 -0
  202. package/packages/core/src/types/search.ts +38 -0
  203. package/packages/core/src/utils/retry.ts +85 -0
  204. package/packages/core/tests/api-card.test.ts +60 -0
  205. package/packages/core/tests/api-routes.test.ts +98 -0
  206. package/packages/core/tests/bm25.test.ts +87 -0
  207. package/packages/core/tests/chunker.test.ts +48 -0
  208. package/packages/core/tests/cluster.test.ts +75 -0
  209. package/packages/core/tests/constellation.test.ts +77 -0
  210. package/packages/core/tests/export-utils.test.ts +97 -0
  211. package/packages/core/tests/fsrs.test.ts +96 -0
  212. package/packages/core/tests/gesture-detector.test.ts +45 -0
  213. package/packages/core/tests/graph-data.test.ts +87 -0
  214. package/packages/core/tests/layout.test.ts +83 -0
  215. package/packages/core/tests/mcp.test.ts +148 -0
  216. package/packages/core/tests/pack.test.ts +127 -0
  217. package/packages/core/tests/pii-masker.test.ts +42 -0
  218. package/packages/core/tests/profile-card.test.ts +62 -0
  219. package/packages/core/tests/rrf.test.ts +29 -0
  220. package/packages/core/tests/search-integration.test.ts +139 -0
  221. package/packages/core/tests/store.test.ts +80 -0
  222. package/packages/graph/click-result.png +0 -0
  223. package/packages/graph/index.html +17 -0
  224. package/packages/graph/package.json +32 -0
  225. package/packages/graph/src/App.tsx +7 -0
  226. package/packages/graph/src/api/client.ts +39 -0
  227. package/packages/graph/src/components/ClusterFilter.tsx +73 -0
  228. package/packages/graph/src/components/ConstellationView.tsx +232 -0
  229. package/packages/graph/src/components/ExportPanel.tsx +177 -0
  230. package/packages/graph/src/components/Graph3D.tsx +230 -0
  231. package/packages/graph/src/components/GraphEdges.tsx +100 -0
  232. package/packages/graph/src/components/GraphNodes.tsx +386 -0
  233. package/packages/graph/src/components/HealthDashboard.tsx +173 -0
  234. package/packages/graph/src/components/Layout.tsx +214 -0
  235. package/packages/graph/src/components/MotionOverlay.tsx +81 -0
  236. package/packages/graph/src/components/MotionToggle.tsx +33 -0
  237. package/packages/graph/src/components/MultiverseView.tsx +286 -0
  238. package/packages/graph/src/components/NodeDetail.tsx +232 -0
  239. package/packages/graph/src/components/PulseParticle.tsx +232 -0
  240. package/packages/graph/src/components/SearchBar.tsx +107 -0
  241. package/packages/graph/src/components/StarField.tsx +197 -0
  242. package/packages/graph/src/components/StatusBar.tsx +53 -0
  243. package/packages/graph/src/components/Timeline.tsx +148 -0
  244. package/packages/graph/src/components/ToolsPanel.tsx +512 -0
  245. package/packages/graph/src/components/Tooltip.tsx +100 -0
  246. package/packages/graph/src/components/TypeFilter.tsx +131 -0
  247. package/packages/graph/src/embed/EmbedGraph.tsx +144 -0
  248. package/packages/graph/src/hooks/useConstellationLOD.ts +76 -0
  249. package/packages/graph/src/hooks/useDecay.ts +37 -0
  250. package/packages/graph/src/hooks/useExport.ts +165 -0
  251. package/packages/graph/src/hooks/useGraph.ts +69 -0
  252. package/packages/graph/src/hooks/useKeyboardNav.ts +122 -0
  253. package/packages/graph/src/hooks/useLayout.ts +45 -0
  254. package/packages/graph/src/hooks/useMotion.ts +120 -0
  255. package/packages/graph/src/hooks/usePulse.ts +58 -0
  256. package/packages/graph/src/hooks/useSearch.ts +71 -0
  257. package/packages/graph/src/lib/constellation.ts +107 -0
  258. package/packages/graph/src/lib/export-utils.ts +48 -0
  259. package/packages/graph/src/lib/gesture-detector.ts +123 -0
  260. package/packages/graph/src/lib/layout.worker.ts +153 -0
  261. package/packages/graph/src/lib/motion-controller.ts +83 -0
  262. package/packages/graph/src/lib/profile-card.ts +122 -0
  263. package/packages/graph/src/main.tsx +4 -0
  264. package/packages/graph/src/stores/graph-store.ts +155 -0
  265. package/packages/graph/success.png +0 -0
  266. package/packages/graph/test-click.mjs +49 -0
  267. package/packages/graph/test-explore.mjs +102 -0
  268. package/packages/graph/test-final.mjs +61 -0
  269. package/packages/graph/test-graph.mjs +139 -0
  270. package/packages/graph/test-hover.mjs +48 -0
  271. package/packages/graph/test-pulse.mjs +68 -0
  272. package/packages/graph/test-screenshot.mjs +56 -0
  273. package/packages/graph/test-v2.mjs +97 -0
  274. package/packages/graph/vite.config.ts +15 -0
  275. package/packages/sync/.env.example +11 -0
  276. package/packages/sync/.sync-state.json +317 -0
  277. package/packages/sync/.upload-state.json +1009 -0
  278. package/packages/sync/create-stella-network-notion.mjs +151 -0
  279. package/packages/sync/create-stellavault-project-notion.mjs +322 -0
  280. package/packages/sync/logs/sync-2026-03-28.log +6 -0
  281. package/packages/sync/logs/sync-2026-03-29.log +12 -0
  282. package/packages/sync/logs/sync-2026-03-30.log +6 -0
  283. package/packages/sync/logs/sync-2026-03-31.log +6 -0
  284. package/packages/sync/logs/sync-2026-04-01.log +6 -0
  285. package/packages/sync/logs/sync-2026-04-02.log +6 -0
  286. package/packages/sync/package-lock.json +373 -0
  287. package/packages/sync/package.json +16 -0
  288. package/packages/sync/run-sync.bat +18 -0
  289. package/packages/sync/run-sync.mjs +46 -0
  290. package/packages/sync/setup-scheduler.mjs +119 -0
  291. package/packages/sync/structured-sync.mjs +187 -0
  292. package/packages/sync/sync-to-obsidian.mjs +264 -0
  293. package/packages/sync/upload-pdca-to-notion.mjs +495 -0
  294. package/tsconfig.base.json +18 -0
@@ -0,0 +1,153 @@
1
+ // Design Ref: §6.1 — Force-Directed Layout (Web Worker)
2
+ // 뇌 형태 ellipsoid 초기 배치 + Barnes-Hut 근사 force simulation
3
+
4
+ interface LayoutMessage {
5
+ type: 'init';
6
+ nodes: Array<{ id: string; clusterId: number; size: number }>;
7
+ edges: Array<{ source: string; target: string; weight: number }>;
8
+ options?: Partial<LayoutOptions>;
9
+ }
10
+
11
+ interface LayoutOptions {
12
+ iterations: number;
13
+ repulsion: number;
14
+ attraction: number;
15
+ damping: number;
16
+ brainScale: [number, number, number]; // x, y, z 반지름 (뇌 비율)
17
+ }
18
+
19
+ const DEFAULTS: LayoutOptions = {
20
+ iterations: 200,
21
+ repulsion: 800,
22
+ attraction: 0.005,
23
+ damping: 0.92,
24
+ brainScale: [250, 180, 200], // 좌우 넓고, 위아래 납작, 앞뒤 중간 = 뇌 비율
25
+ };
26
+
27
+ self.onmessage = (e: MessageEvent<LayoutMessage>) => {
28
+ if (e.data.type !== 'init') return;
29
+
30
+ const { nodes, edges, options } = e.data;
31
+ const opts = { ...DEFAULTS, ...options };
32
+ const n = nodes.length;
33
+
34
+ if (n === 0) {
35
+ self.postMessage({ type: 'done', positions: [] });
36
+ return;
37
+ }
38
+
39
+ // 노드 인덱스 맵
40
+ const idxMap = new Map<string, number>();
41
+ nodes.forEach((node, i) => idxMap.set(node.id, i));
42
+
43
+ // 엣지를 인덱스 기반으로 변환
44
+ const edgeIdx = edges
45
+ .map(e => ({ s: idxMap.get(e.source) ?? -1, t: idxMap.get(e.target) ?? -1, w: e.weight }))
46
+ .filter(e => e.s >= 0 && e.t >= 0);
47
+
48
+ // 초기 배치: 뇌 형태 ellipsoid 표면 + 내부
49
+ const pos = new Float64Array(n * 3);
50
+ const vel = new Float64Array(n * 3);
51
+ const [rx, ry, rz] = opts.brainScale;
52
+
53
+ for (let i = 0; i < n; i++) {
54
+ // 구면 좌표 → 타원체 매핑
55
+ const theta = Math.acos(2 * Math.random() - 1); // 0~π
56
+ const phi = Math.random() * 2 * Math.PI; // 0~2π
57
+ const r = 0.5 + 0.5 * Math.random(); // 반지름 비율 (내부에도 분포)
58
+
59
+ // 뇌 좌반구/우반구 비대칭 (약간의 주름 효과)
60
+ const wobble = 1 + 0.1 * Math.sin(5 * theta) * Math.cos(3 * phi);
61
+
62
+ pos[i * 3] = rx * r * Math.sin(theta) * Math.cos(phi) * wobble;
63
+ pos[i * 3 + 1] = ry * r * Math.cos(theta) * wobble;
64
+ pos[i * 3 + 2] = rz * r * Math.sin(theta) * Math.sin(phi) * wobble;
65
+
66
+ // 같은 클러스터끼리 가깝게 초기 배치
67
+ const cluster = nodes[i].clusterId;
68
+ const clusterAngle = (cluster / 10) * 2 * Math.PI;
69
+ pos[i * 3] += 30 * Math.cos(clusterAngle);
70
+ pos[i * 3 + 2] += 30 * Math.sin(clusterAngle);
71
+ }
72
+
73
+ // Force simulation
74
+ for (let iter = 0; iter < opts.iterations; iter++) {
75
+ const alpha = 1 - iter / opts.iterations; // cooling
76
+ const repForce = opts.repulsion * alpha;
77
+
78
+ // 반발력 (모든 노드 쌍 — n < 2000이면 O(n²) 허용)
79
+ for (let i = 0; i < n; i++) {
80
+ for (let j = i + 1; j < n; j++) {
81
+ const dx = pos[j * 3] - pos[i * 3];
82
+ const dy = pos[j * 3 + 1] - pos[i * 3 + 1];
83
+ const dz = pos[j * 3 + 2] - pos[i * 3 + 2];
84
+ const distSq = dx * dx + dy * dy + dz * dz + 0.01;
85
+ const force = repForce / distSq;
86
+ const fx = dx * force / Math.sqrt(distSq);
87
+ const fy = dy * force / Math.sqrt(distSq);
88
+ const fz = dz * force / Math.sqrt(distSq);
89
+
90
+ vel[i * 3] -= fx;
91
+ vel[i * 3 + 1] -= fy;
92
+ vel[i * 3 + 2] -= fz;
93
+ vel[j * 3] += fx;
94
+ vel[j * 3 + 1] += fy;
95
+ vel[j * 3 + 2] += fz;
96
+ }
97
+ }
98
+
99
+ // 인력 (연결된 노드)
100
+ for (const edge of edgeIdx) {
101
+ const dx = pos[edge.t * 3] - pos[edge.s * 3];
102
+ const dy = pos[edge.t * 3 + 1] - pos[edge.s * 3 + 1];
103
+ const dz = pos[edge.t * 3 + 2] - pos[edge.s * 3 + 2];
104
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) + 0.01;
105
+ const force = opts.attraction * dist * edge.w;
106
+ const fx = dx / dist * force;
107
+ const fy = dy / dist * force;
108
+ const fz = dz / dist * force;
109
+
110
+ vel[edge.s * 3] += fx;
111
+ vel[edge.s * 3 + 1] += fy;
112
+ vel[edge.s * 3 + 2] += fz;
113
+ vel[edge.t * 3] -= fx;
114
+ vel[edge.t * 3 + 1] -= fy;
115
+ vel[edge.t * 3 + 2] -= fz;
116
+ }
117
+
118
+ // 뇌 형태 유지 — ellipsoid 경계 소프트 제약
119
+ for (let i = 0; i < n; i++) {
120
+ const x = pos[i * 3], y = pos[i * 3 + 1], z = pos[i * 3 + 2];
121
+ // ellipsoid 밖이면 안쪽으로 끌어당김
122
+ const ellipDist = (x / rx) ** 2 + (y / ry) ** 2 + (z / rz) ** 2;
123
+ if (ellipDist > 1) {
124
+ const pullback = 0.3 * (ellipDist - 1);
125
+ vel[i * 3] -= x * pullback * 0.01;
126
+ vel[i * 3 + 1] -= y * pullback * 0.01;
127
+ vel[i * 3 + 2] -= z * pullback * 0.01;
128
+ }
129
+
130
+ // 속도 적용 + 감쇠
131
+ pos[i * 3] += vel[i * 3];
132
+ pos[i * 3 + 1] += vel[i * 3 + 1];
133
+ pos[i * 3 + 2] += vel[i * 3 + 2];
134
+ vel[i * 3] *= opts.damping;
135
+ vel[i * 3 + 1] *= opts.damping;
136
+ vel[i * 3 + 2] *= opts.damping;
137
+ }
138
+
139
+ // 매 20 iteration마다 중간 결과 전송 (애니메이션)
140
+ if (iter % 20 === 0 || iter === opts.iterations - 1) {
141
+ const positions: Array<[number, number, number]> = [];
142
+ for (let i = 0; i < n; i++) {
143
+ positions.push([pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]]);
144
+ }
145
+ self.postMessage({
146
+ type: iter === opts.iterations - 1 ? 'done' : 'progress',
147
+ positions,
148
+ iteration: iter,
149
+ total: opts.iterations,
150
+ });
151
+ }
152
+ }
153
+ };
@@ -0,0 +1,83 @@
1
+ // Design Ref: §2 — MediaPipe Hands 초기화 + 웹캠 관리
2
+
3
+ import { detectGesture, resetGestureState, type GestureResult } from './gesture-detector.js';
4
+
5
+ export interface MotionController {
6
+ start(): Promise<void>;
7
+ stop(): void;
8
+ isRunning(): boolean;
9
+ getVideoElement(): HTMLVideoElement | null;
10
+ onGesture(cb: (gesture: GestureResult) => void): void;
11
+ }
12
+
13
+ export function createMotionController(): MotionController {
14
+ let video: HTMLVideoElement | null = null;
15
+ let hands: any = null;
16
+ let camera: any = null;
17
+ let running = false;
18
+ let gestureCallback: ((g: GestureResult) => void) | null = null;
19
+
20
+ return {
21
+ async start() {
22
+ if (running) return;
23
+
24
+ // 동적 import (lazy load — ~5MB WASM)
25
+ const [handsModule, cameraModule] = await Promise.all([
26
+ import('@mediapipe/hands'),
27
+ import('@mediapipe/camera_utils'),
28
+ ]);
29
+
30
+ // 비디오 요소 생성
31
+ video = document.createElement('video');
32
+ video.setAttribute('playsinline', '');
33
+ video.style.display = 'none';
34
+ document.body.appendChild(video);
35
+
36
+ // MediaPipe Hands 초기화
37
+ hands = new handsModule.Hands({
38
+ locateFile: (file: string) =>
39
+ `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`,
40
+ });
41
+ hands.setOptions({
42
+ maxNumHands: 1,
43
+ modelComplexity: 0, // 0=lite (가장 빠름)
44
+ minDetectionConfidence: 0.6,
45
+ minTrackingConfidence: 0.5,
46
+ });
47
+
48
+ hands.onResults((results: any) => {
49
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
50
+ const gesture = detectGesture(results.multiHandLandmarks[0]);
51
+ if (gesture.type !== 'none' && gestureCallback) {
52
+ gestureCallback(gesture);
53
+ }
54
+ }
55
+ });
56
+
57
+ // 카메라 시작
58
+ camera = new cameraModule.Camera(video, {
59
+ onFrame: async () => {
60
+ if (hands && video) await hands.send({ image: video });
61
+ },
62
+ width: 320,
63
+ height: 240,
64
+ });
65
+
66
+ await camera.start();
67
+ running = true;
68
+ resetGestureState();
69
+ },
70
+
71
+ stop() {
72
+ if (camera) { camera.stop(); camera = null; }
73
+ if (hands) { hands.close(); hands = null; }
74
+ if (video) { video.remove(); video = null; }
75
+ running = false;
76
+ resetGestureState();
77
+ },
78
+
79
+ isRunning() { return running; },
80
+ getVideoElement() { return video; },
81
+ onGesture(cb) { gestureCallback = cb; },
82
+ };
83
+ }
@@ -0,0 +1,122 @@
1
+ // Design Ref: §4 — SVG 프로필 카드 생성
2
+ // 레이더 차트 (상위 6 클러스터) + 워드클라우드 (상위 20 태그) + 통계
3
+
4
+ interface ClusterInfo {
5
+ label: string;
6
+ color: string;
7
+ nodeCount: number;
8
+ }
9
+
10
+ interface CardData {
11
+ title: string;
12
+ documentCount: number;
13
+ clusterCount: number;
14
+ edgeCount: number;
15
+ clusters: ClusterInfo[];
16
+ topTags: Array<{ tag: string; count: number }>;
17
+ }
18
+
19
+ export function generateProfileCardSVG(data: CardData): string {
20
+ const W = 800, H = 420;
21
+ const top6 = data.clusters.slice(0, 6);
22
+ const maxCount = Math.max(1, ...top6.map(c => c.nodeCount));
23
+
24
+ // 레이더 차트 계산
25
+ const radarCx = 200, radarCy = 220, radarR = 100;
26
+ const radarPoints = top6.map((c, i) => {
27
+ const angle = (Math.PI * 2 * i) / top6.length - Math.PI / 2;
28
+ const r = radarR * (c.nodeCount / maxCount);
29
+ return {
30
+ x: radarCx + r * Math.cos(angle),
31
+ y: radarCy + r * Math.sin(angle),
32
+ labelX: radarCx + (radarR + 20) * Math.cos(angle),
33
+ labelY: radarCy + (radarR + 20) * Math.sin(angle),
34
+ label: c.label.split(',')[0].trim().slice(0, 12),
35
+ color: c.color,
36
+ };
37
+ });
38
+
39
+ const radarPath = radarPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + 'Z';
40
+
41
+ // 레이더 그리드 (3단계)
42
+ const gridPaths = [0.33, 0.66, 1].map(scale => {
43
+ return top6.map((_, i) => {
44
+ const angle = (Math.PI * 2 * i) / top6.length - Math.PI / 2;
45
+ const x = radarCx + radarR * scale * Math.cos(angle);
46
+ const y = radarCy + radarR * scale * Math.sin(angle);
47
+ return `${i === 0 ? 'M' : 'L'}${x},${y}`;
48
+ }).join(' ') + 'Z';
49
+ });
50
+
51
+ // 워드클라우드
52
+ const tags20 = data.topTags.slice(0, 20);
53
+ const maxTagCount = Math.max(1, ...tags20.map(t => t.count));
54
+
55
+ const tagElements = tags20.map((t, i) => {
56
+ const size = 10 + 14 * (t.count / maxTagCount);
57
+ const col = i % 2 === 0 ? Math.floor(i / 2) : Math.floor(i / 2);
58
+ const row = i % 2;
59
+ const x = 480 + (col % 5) * 60;
60
+ const y = 140 + row * 30 + Math.floor(i / 10) * 70;
61
+ const opacity = 0.5 + 0.5 * (t.count / maxTagCount);
62
+ return `<text x="${x}" y="${y}" font-size="${size}" fill="#88aaff" opacity="${opacity}" font-family="monospace">#${escapeXml(t.tag)}</text>`;
63
+ }).join('\n ');
64
+
65
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
66
+ <defs>
67
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
68
+ <stop offset="0%" stop-color="#0d1028"/>
69
+ <stop offset="100%" stop-color="#050510"/>
70
+ </linearGradient>
71
+ <linearGradient id="radar-fill" x1="0%" y1="0%" x2="100%" y2="100%">
72
+ <stop offset="0%" stop-color="#6366f1" stop-opacity="0.3"/>
73
+ <stop offset="100%" stop-color="#06b6d4" stop-opacity="0.15"/>
74
+ </linearGradient>
75
+ </defs>
76
+
77
+ <!-- Background -->
78
+ <rect width="${W}" height="${H}" rx="16" fill="url(#bg)"/>
79
+ <rect width="${W}" height="${H}" rx="16" fill="none" stroke="#6366f140" stroke-width="1"/>
80
+
81
+ <!-- Header -->
82
+ <text x="30" y="40" font-size="20" font-weight="700" fill="#c0c0f0" font-family="system-ui, sans-serif">
83
+ 🧠 ${escapeXml(data.title)}
84
+ </text>
85
+ <text x="30" y="65" font-size="13" fill="#556" font-family="monospace">
86
+ ${data.documentCount} documents · ${data.clusterCount} clusters · ${data.edgeCount} connections
87
+ </text>
88
+ <line x1="30" y1="80" x2="${W - 30}" y2="80" stroke="#6366f120" stroke-width="1"/>
89
+
90
+ <!-- Radar Chart -->
91
+ <text x="${radarCx}" y="105" font-size="11" fill="#667" text-anchor="middle" font-family="system-ui">KNOWLEDGE DISTRIBUTION</text>
92
+
93
+ <!-- Grid -->
94
+ ${gridPaths.map(p => `<path d="${p}" fill="none" stroke="#6366f115" stroke-width="0.5"/>`).join('\n ')}
95
+
96
+ <!-- Radar axes -->
97
+ ${radarPoints.map(p => `<line x1="${radarCx}" y1="${radarCy}" x2="${p.labelX}" y2="${p.labelY}" stroke="#6366f110" stroke-width="0.5"/>`).join('\n ')}
98
+
99
+ <!-- Radar shape -->
100
+ <path d="${radarPath}" fill="url(#radar-fill)" stroke="#818cf8" stroke-width="1.5"/>
101
+
102
+ <!-- Radar dots + labels -->
103
+ ${radarPoints.map(p => `
104
+ <circle cx="${p.x}" cy="${p.y}" r="3" fill="${p.color}"/>
105
+ <text x="${p.labelX}" y="${p.labelY + 4}" font-size="9" fill="#889" text-anchor="middle" font-family="monospace">${escapeXml(p.label)}</text>
106
+ `).join('')}
107
+
108
+ <!-- Word Cloud -->
109
+ <text x="580" y="105" font-size="11" fill="#667" text-anchor="middle" font-family="system-ui">TOP TOPICS</text>
110
+ <rect x="440" y="115" width="320" height="240" rx="8" fill="#6366f108"/>
111
+ ${tagElements}
112
+
113
+ <!-- Footer -->
114
+ <text x="${W / 2}" y="${H - 15}" font-size="10" fill="#334" text-anchor="middle" font-family="monospace">
115
+ Generated by Stellavault
116
+ </text>
117
+ </svg>`;
118
+ }
119
+
120
+ function escapeXml(s: string): string {
121
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
122
+ }
@@ -0,0 +1,4 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { App } from './App.js';
3
+
4
+ createRoot(document.getElementById('root')!).render(<App />);
@@ -0,0 +1,155 @@
1
+ // Design Ref: §10.1 — zustand (R3F 렌더 루프 충돌 방지)
2
+
3
+ import { create } from 'zustand';
4
+
5
+ interface GraphNode {
6
+ id: string;
7
+ label: string;
8
+ filePath: string;
9
+ tags: string[];
10
+ clusterId: number;
11
+ position?: [number, number, number];
12
+ size: number;
13
+ source?: string;
14
+ type?: string;
15
+ lastModified?: string;
16
+ }
17
+
18
+ interface GraphEdge {
19
+ source: string;
20
+ target: string;
21
+ weight: number;
22
+ }
23
+
24
+ interface Cluster {
25
+ id: number;
26
+ label: string;
27
+ color: string;
28
+ nodeCount: number;
29
+ }
30
+
31
+ type GraphMode = 'semantic' | 'folder';
32
+
33
+ interface GraphState {
34
+ nodes: GraphNode[];
35
+ edges: GraphEdge[];
36
+ clusters: Cluster[];
37
+ selectedNodeId: string | null;
38
+ hoveredNodeId: string | null;
39
+ highlightedNodeIds: Set<string>;
40
+ searchQuery: string;
41
+ loading: boolean;
42
+ error: string | null;
43
+ mode: GraphMode;
44
+ pulseParticlePos: [number, number, number] | null;
45
+ hiddenClusters: Set<number>;
46
+ theme: 'dark' | 'light';
47
+ isExporting: boolean;
48
+ isRecording: boolean;
49
+ lodLevel: 'universe' | 'constellation' | 'note';
50
+ showDecayOverlay: boolean;
51
+ decayData: Record<string, number>;
52
+ showConstellation: boolean;
53
+ hiddenTypes: Set<string>;
54
+ showTimeline: boolean;
55
+ timelineRange: [number, number] | null; // [startMs, endMs] or null = show all
56
+ searchHistory: string[];
57
+ maxVisibleNodes: number; // performance cap for large vaults
58
+ viewMode: 'universe' | 'multiverse';
59
+ federationPeers: Array<{ peerId: string; displayName: string; documentCount: number; topTopics: string[] }>;
60
+ setViewMode: (mode: 'universe' | 'multiverse') => void;
61
+ setFederationPeers: (peers: Array<{ peerId: string; displayName: string; documentCount: number; topTopics: string[] }>) => void;
62
+
63
+ setGraphData: (nodes: GraphNode[], edges: GraphEdge[], clusters: Cluster[]) => void;
64
+ selectNode: (id: string | null) => void;
65
+ hoverNode: (id: string | null) => void;
66
+ setHighlightedNodes: (ids: string[]) => void;
67
+ setSearchQuery: (query: string) => void;
68
+ setLoading: (loading: boolean) => void;
69
+ setError: (error: string | null) => void;
70
+ setMode: (mode: GraphMode) => void;
71
+ setPulseParticlePos: (pos: [number, number, number] | null) => void;
72
+ toggleHiddenCluster: (id: number) => void;
73
+ toggleTheme: () => void;
74
+ setExporting: (v: boolean) => void;
75
+ setRecording: (v: boolean) => void;
76
+ setLodLevel: (level: 'universe' | 'constellation' | 'note') => void;
77
+ toggleDecayOverlay: () => void;
78
+ setDecayData: (data: Record<string, number>) => void;
79
+ toggleConstellation: () => void;
80
+ toggleHiddenType: (type: string) => void;
81
+ toggleTimeline: () => void;
82
+ setTimelineRange: (range: [number, number] | null) => void;
83
+ addSearchHistory: (query: string) => void;
84
+ clearSearchHistory: () => void;
85
+ }
86
+
87
+ export const useGraphStore = create<GraphState>((set) => ({
88
+ nodes: [],
89
+ edges: [],
90
+ clusters: [],
91
+ selectedNodeId: null,
92
+ hoveredNodeId: null,
93
+ highlightedNodeIds: new Set(),
94
+ searchQuery: '',
95
+ loading: false,
96
+ error: null,
97
+ mode: 'semantic',
98
+ pulseParticlePos: null,
99
+ hiddenClusters: new Set(),
100
+ theme: 'dark',
101
+ isExporting: false,
102
+ isRecording: false,
103
+ lodLevel: 'constellation' as const,
104
+ showDecayOverlay: false,
105
+ decayData: {},
106
+ showConstellation: true,
107
+ hiddenTypes: new Set(),
108
+ showTimeline: false,
109
+ timelineRange: null,
110
+ searchHistory: JSON.parse(localStorage.getItem('sv_search_history') ?? '[]') as string[],
111
+ maxVisibleNodes: 5000,
112
+ viewMode: 'universe' as const,
113
+ federationPeers: [],
114
+ setViewMode: (mode) => set({ viewMode: mode }),
115
+ setFederationPeers: (peers) => set({ federationPeers: peers }),
116
+
117
+ setGraphData: (nodes, edges, clusters) => set({ nodes, edges, clusters, hiddenClusters: new Set() }),
118
+ selectNode: (id) => set({ selectedNodeId: id }),
119
+ hoverNode: (id) => set({ hoveredNodeId: id }),
120
+ setHighlightedNodes: (ids) => set({ highlightedNodeIds: new Set(ids) }),
121
+ setSearchQuery: (query) => set({ searchQuery: query }),
122
+ setLoading: (loading) => set({ loading }),
123
+ setError: (error) => set({ error }),
124
+ setMode: (mode) => set({ mode }),
125
+ setPulseParticlePos: (pos) => set({ pulseParticlePos: pos }),
126
+ toggleTheme: () => set((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' as const })),
127
+ setExporting: (v) => set({ isExporting: v }),
128
+ setRecording: (v) => set({ isRecording: v }),
129
+ setLodLevel: (level) => set({ lodLevel: level }),
130
+ toggleDecayOverlay: () => set((s) => ({ showDecayOverlay: !s.showDecayOverlay })),
131
+ setDecayData: (data) => set({ decayData: data }),
132
+ toggleConstellation: () => set((s) => ({ showConstellation: !s.showConstellation })),
133
+ toggleHiddenCluster: (id) => set((s) => {
134
+ const next = new Set(s.hiddenClusters);
135
+ if (next.has(id)) next.delete(id); else next.add(id);
136
+ return { hiddenClusters: next };
137
+ }),
138
+ addSearchHistory: (query) => set((s) => {
139
+ const filtered = s.searchHistory.filter((q) => q !== query);
140
+ const next = [query, ...filtered].slice(0, 20);
141
+ localStorage.setItem('sv_search_history', JSON.stringify(next));
142
+ return { searchHistory: next };
143
+ }),
144
+ clearSearchHistory: () => set(() => {
145
+ localStorage.removeItem('sv_search_history');
146
+ return { searchHistory: [] };
147
+ }),
148
+ toggleTimeline: () => set((s) => ({ showTimeline: !s.showTimeline, timelineRange: s.showTimeline ? null : s.timelineRange })),
149
+ setTimelineRange: (range) => set({ timelineRange: range }),
150
+ toggleHiddenType: (type) => set((s) => {
151
+ const next = new Set(s.hiddenTypes);
152
+ if (next.has(type)) next.delete(type); else next.add(type);
153
+ return { hiddenTypes: next };
154
+ }),
155
+ }));
Binary file
@@ -0,0 +1,49 @@
1
+ import { chromium } from 'playwright';
2
+
3
+ const browser = await chromium.launch({ headless: false });
4
+ const page = await browser.newPage();
5
+ await page.goto('http://localhost:5173');
6
+ await page.waitForTimeout(4000);
7
+
8
+ const canvas = await page.locator('canvas').boundingBox();
9
+ const cx = canvas.x + canvas.width / 2;
10
+ const cy = canvas.y + canvas.height / 2;
11
+
12
+ // 노드 찾기: 넓은 범위 스캔
13
+ let found = false;
14
+ for (let dx = -200; dx <= 200; dx += 30) {
15
+ for (let dy = -150; dy <= 150; dy += 30) {
16
+ await page.mouse.move(cx + dx, cy + dy);
17
+ await page.waitForTimeout(50);
18
+ const cursor = await page.evaluate(() => document.body.style.cursor);
19
+ if (cursor === 'pointer') {
20
+ console.log(`NODE FOUND at [${dx}, ${dy}]`);
21
+
22
+ // 호버 확인
23
+ await page.waitForTimeout(200);
24
+
25
+ // 클릭 (pointerdown 방식)
26
+ await page.mouse.click(cx + dx, cy + dy);
27
+ await page.waitForTimeout(1500);
28
+
29
+ // 사이드패널 확인
30
+ const result = await page.evaluate(() => {
31
+ const text = document.body.innerText;
32
+ return {
33
+ hasPanel: text.includes('DOCUMENT PREVIEW') || text.includes('Document Preview'),
34
+ hasExplore: text.includes('Explore connections'),
35
+ snippet: text.slice(0, 400),
36
+ };
37
+ });
38
+ console.log('PANEL:', JSON.stringify(result, null, 2));
39
+ found = true;
40
+ break;
41
+ }
42
+ }
43
+ if (found) break;
44
+ }
45
+
46
+ if (!found) console.log('No node found in scan range');
47
+
48
+ await page.waitForTimeout(2000);
49
+ await browser.close();
@@ -0,0 +1,102 @@
1
+ import { chromium } from 'playwright';
2
+
3
+ const browser = await chromium.launch({ headless: false });
4
+ const page = await browser.newPage({ viewport: { width: 1400, height: 800 } });
5
+ page.on('console', m => console.log(`[${m.type()}]`, m.text()));
6
+ await page.goto('http://localhost:5173');
7
+ await page.waitForTimeout(5000);
8
+
9
+ const canvas = await page.locator('canvas').boundingBox();
10
+ const cx = canvas.x + canvas.width / 2;
11
+ const cy = canvas.y + canvas.height / 2;
12
+
13
+ // 1. 노드 찾아서 클릭
14
+ let nodeX = 0, nodeY = 0;
15
+ for (let dx = -250; dx <= 250; dx += 15) {
16
+ for (let dy = -200; dy <= 200; dy += 15) {
17
+ await page.mouse.move(cx + dx, cy + dy);
18
+ await page.waitForTimeout(20);
19
+ if (await page.evaluate(() => document.body.style.cursor) === 'pointer') {
20
+ nodeX = cx + dx; nodeY = cy + dy;
21
+ break;
22
+ }
23
+ }
24
+ if (nodeX) break;
25
+ }
26
+ console.log('Node at', nodeX - cx, nodeY - cy);
27
+
28
+ // 클릭 → 패널 열기
29
+ await page.mouse.move(nodeX, nodeY);
30
+ await page.waitForTimeout(200);
31
+ await page.mouse.down(); await page.waitForTimeout(50); await page.mouse.up();
32
+ await page.waitForTimeout(1500);
33
+
34
+ const panelOpen = await page.evaluate(() => document.body.innerText.includes('Explore connections'));
35
+ console.log('Panel open:', panelOpen);
36
+
37
+ if (panelOpen) {
38
+ // 2. Explore 클릭
39
+ console.log('Clicking Explore...');
40
+ const btn = page.locator('button', { hasText: 'Explore connections' });
41
+ await btn.click();
42
+
43
+ // 3. 3초 동안 상태 모니터링
44
+ for (let i = 0; i < 15; i++) {
45
+ await page.waitForTimeout(200);
46
+ const state = await page.evaluate(() => {
47
+ const cv = document.querySelector('canvas');
48
+ return {
49
+ canvasVisible: cv ? cv.offsetWidth > 0 : false,
50
+ bodyText: document.body.innerText.slice(0, 100),
51
+ };
52
+ });
53
+ if (i % 3 === 0) console.log(` tick ${i}: canvas=${state.canvasVisible}`);
54
+ }
55
+
56
+ // 4. 다른 노드 클릭 시도
57
+ console.log('Trying to click another node...');
58
+ let found2 = false;
59
+ for (let dx = -200; dx <= 200; dx += 20) {
60
+ for (let dy = -150; dy <= 150; dy += 20) {
61
+ await page.mouse.move(cx + dx, cy + dy);
62
+ await page.waitForTimeout(20);
63
+ if (await page.evaluate(() => document.body.style.cursor) === 'pointer') {
64
+ console.log('Found second node at', dx, dy);
65
+ await page.mouse.down(); await page.waitForTimeout(50); await page.mouse.up();
66
+ await page.waitForTimeout(1000);
67
+
68
+ const afterSecond = await page.evaluate(() => {
69
+ const cv = document.querySelector('canvas');
70
+ return {
71
+ canvasVisible: cv ? cv.offsetWidth > 0 : false,
72
+ canvasW: cv?.offsetWidth ?? 0,
73
+ bodyText: document.body.innerText.slice(0, 150),
74
+ };
75
+ });
76
+ console.log('After second click:', JSON.stringify(afterSecond));
77
+ found2 = true;
78
+ break;
79
+ }
80
+ }
81
+ if (found2) break;
82
+ }
83
+
84
+ // 5. 빈 곳 클릭
85
+ console.log('Clicking empty space...');
86
+ await page.mouse.move(cx + 300, cy + 250);
87
+ await page.waitForTimeout(200);
88
+ await page.mouse.down(); await page.waitForTimeout(50); await page.mouse.up();
89
+ await page.waitForTimeout(1000);
90
+
91
+ const afterEmpty = await page.evaluate(() => {
92
+ const cv = document.querySelector('canvas');
93
+ return {
94
+ canvasVisible: cv ? cv.offsetWidth > 0 : false,
95
+ canvasW: cv?.offsetWidth ?? 0,
96
+ };
97
+ });
98
+ console.log('After empty click:', JSON.stringify(afterEmpty));
99
+ }
100
+
101
+ await page.waitForTimeout(2000);
102
+ await browser.close();