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,235 @@
1
+ // Design Ref: §4 — FederationNode (Hyperswarm P2P)
2
+ // Plan SC: SC1 (2노드 연결), SC5 (오프라인 정상)
3
+
4
+ import { createHash } from 'node:crypto';
5
+ import { EventEmitter } from 'node:events';
6
+ import { getOrCreateIdentity, type NodeIdentity } from './identity.js';
7
+ import type { PeerInfo, FederationMessage } from './types.js';
8
+
9
+ const FEDERATION_TOPIC = createHash('sha256').update('stellavault-federation-v1').digest();
10
+
11
+ export class FederationNode extends EventEmitter {
12
+ private swarm: any = null;
13
+ private identity: NodeIdentity;
14
+ private peers = new Map<string, { info: PeerInfo; conn: any }>();
15
+ private running = false;
16
+ private documentCount = 0;
17
+ private topTopics: string[] = [];
18
+
19
+ constructor(displayName?: string) {
20
+ super();
21
+ this.identity = getOrCreateIdentity(displayName);
22
+ }
23
+
24
+ get peerId(): string { return this.identity.peerId; }
25
+ get displayName(): string { return this.identity.displayName; }
26
+ get peerCount(): number { return this.peers.size; }
27
+ get isRunning(): boolean { return this.running; }
28
+
29
+ setLocalStats(documentCount: number, topTopics: string[]) {
30
+ this.documentCount = documentCount;
31
+ this.topTopics = topTopics.slice(0, 5);
32
+ }
33
+
34
+ // Design Ref: §4 — join()
35
+ async join(): Promise<void> {
36
+ if (this.running) return;
37
+
38
+ const Hyperswarm = (await import('hyperswarm')).default;
39
+ this.swarm = new Hyperswarm({ maxPeers: 50 });
40
+
41
+ this.swarm.on('connection', (conn: any, _info: any) => {
42
+ this.handleConnection(conn);
43
+ });
44
+
45
+ const discovery = this.swarm.join(FEDERATION_TOPIC, { server: true, client: true });
46
+ await discovery.flushed();
47
+
48
+ this.running = true;
49
+ this.emit('joined', { peerId: this.peerId, topic: FEDERATION_TOPIC.toString('hex').slice(0, 16) });
50
+ }
51
+
52
+ // Design Ref: §4 — joinDirect() 수동 IP 폴백
53
+ async joinDirect(host: string, port: number): Promise<void> {
54
+ const net = await import('node:net');
55
+ const conn = net.connect(port, host);
56
+
57
+ await new Promise<void>((resolve, reject) => {
58
+ conn.on('connect', () => {
59
+ this.handleConnection(conn);
60
+ resolve();
61
+ });
62
+ conn.on('error', reject);
63
+ setTimeout(() => reject(new Error('Connection timeout')), 15000);
64
+ });
65
+
66
+ if (!this.running) this.running = true;
67
+ }
68
+
69
+ async leave(): Promise<void> {
70
+ if (!this.running) return;
71
+
72
+ for (const [, peer] of this.peers) {
73
+ try {
74
+ this.sendMessage(peer.conn, { type: 'leave', peerId: this.peerId });
75
+ peer.conn.end();
76
+ } catch { /* ignore */ }
77
+ }
78
+
79
+ this.peers.clear();
80
+ await this.swarm?.destroy();
81
+ this.swarm = null;
82
+ this.running = false;
83
+ this.emit('left');
84
+ }
85
+
86
+ getPeers(): PeerInfo[] {
87
+ return [...this.peers.values()].map(p => p.info);
88
+ }
89
+
90
+ // 피어에게 검색 쿼리 전송 (FederatedSearch에서 사용)
91
+ sendSearchQuery(peerId: string, queryId: string, embedding: number[], limit: number): void {
92
+ const peer = this.peers.get(peerId);
93
+ if (!peer) return;
94
+ this.sendMessage(peer.conn, { type: 'search_query', queryId, embedding, limit });
95
+ }
96
+
97
+ // 피어에게 검색 결과 응답 (FederatedSearch에서 사용)
98
+ sendSearchResult(peerId: string, queryId: string, results: Array<{ title: string; similarity: number; snippet: string }>): void {
99
+ const peer = this.peers.get(peerId);
100
+ if (!peer) return;
101
+ this.sendMessage(peer.conn, { type: 'search_result', queryId, results });
102
+ }
103
+
104
+ // --- Private ---
105
+
106
+ private handleConnection(conn: any) {
107
+ // 핸드셰이크 전송
108
+ this.sendMessage(conn, {
109
+ type: 'handshake',
110
+ peerId: this.peerId,
111
+ displayName: this.identity.displayName,
112
+ version: '0.1.0',
113
+ documentCount: this.documentCount,
114
+ topTopics: this.topTopics,
115
+ });
116
+
117
+ let buffer = '';
118
+ const MAX_BUFFER = 1024 * 1024; // HIGH-04: 1MB 버퍼 제한 (OOM 방지)
119
+ const MAX_MESSAGE = 64 * 1024; // 개별 메시지 64KB 제한
120
+
121
+ conn.on('data', (data: Buffer) => {
122
+ buffer += data.toString();
123
+
124
+ // 버퍼 크기 초과 시 연결 종료
125
+ if (buffer.length > MAX_BUFFER) {
126
+ console.error('Federation: buffer overflow from peer, disconnecting');
127
+ buffer = '';
128
+ conn.end();
129
+ return;
130
+ }
131
+
132
+ const lines = buffer.split('\n');
133
+ buffer = lines.pop() ?? '';
134
+
135
+ for (const line of lines) {
136
+ if (!line.trim()) continue;
137
+ if (line.length > MAX_MESSAGE) continue; // 초대형 메시지 무시
138
+ try {
139
+ const msg: FederationMessage = JSON.parse(line);
140
+ this.handleMessage(conn, msg);
141
+ } catch { /* malformed — ignore */ }
142
+ }
143
+ });
144
+
145
+ conn.on('close', () => {
146
+ for (const [peerId, peer] of this.peers) {
147
+ if (peer.conn === conn) {
148
+ this.peers.delete(peerId);
149
+ this.emit('peer_left', { peerId });
150
+ break;
151
+ }
152
+ }
153
+ });
154
+
155
+ conn.on('error', () => { /* swallow */ });
156
+ }
157
+
158
+ // MED: 메시지 스키마 기본 검증
159
+ private validateMessage(msg: any): boolean {
160
+ if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return false;
161
+ if (msg.type === 'handshake' && (typeof msg.peerId !== 'string' || typeof msg.displayName !== 'string')) return false;
162
+ if (msg.type === 'search_query' && (!Array.isArray(msg.embedding) || msg.embedding.length !== 384)) return false;
163
+ if (msg.type === 'search_result' && !Array.isArray(msg.results)) return false;
164
+ return true;
165
+ }
166
+
167
+ private handleMessage(conn: any, msg: FederationMessage) {
168
+ if (!this.validateMessage(msg)) return; // 스키마 불일치 메시지 무시
169
+
170
+ switch (msg.type) {
171
+ case 'handshake': {
172
+ // MED: displayName 길이 제한
173
+ const safeName = (msg.displayName ?? '').slice(0, 50);
174
+ const peerInfo: PeerInfo = {
175
+ peerId: msg.peerId,
176
+ displayName: safeName,
177
+ documentCount: Math.min(msg.documentCount ?? 0, 1000000), // 합리적 상한
178
+ topTopics: (msg.topTopics ?? []).slice(0, 10),
179
+ joinedAt: new Date().toISOString(),
180
+ lastSeen: new Date().toISOString(),
181
+ };
182
+ this.peers.set(msg.peerId, { info: peerInfo, conn });
183
+ this.emit('peer_joined', peerInfo);
184
+ break;
185
+ }
186
+
187
+ case 'search_query': {
188
+ // Design Ref: §5 — 검색 요청 수신
189
+ this.emit('search_request', {
190
+ peerId: msg.queryId, // queryId를 추적용으로 사용
191
+ queryId: msg.queryId,
192
+ embedding: msg.embedding,
193
+ limit: msg.limit,
194
+ // respond 함수: 호출 측에서 사용
195
+ respondTo: (() => {
196
+ // 어느 피어가 보냈는지 찾기
197
+ for (const [pid, peer] of this.peers) {
198
+ if (peer.conn === conn) return pid;
199
+ }
200
+ return null;
201
+ })(),
202
+ });
203
+ break;
204
+ }
205
+
206
+ case 'search_result': {
207
+ // FederatedSearch가 이벤트로 수신
208
+ this.emit('search_response', {
209
+ queryId: msg.queryId,
210
+ results: msg.results,
211
+ peerId: (() => {
212
+ for (const [pid, peer] of this.peers) {
213
+ if (peer.conn === conn) return pid;
214
+ }
215
+ return 'unknown';
216
+ })(),
217
+ });
218
+ break;
219
+ }
220
+
221
+ case 'leave': {
222
+ this.peers.delete(msg.peerId);
223
+ this.emit('peer_left', { peerId: msg.peerId });
224
+ break;
225
+ }
226
+ }
227
+ }
228
+
229
+ // Design Ref: §7 — JSON + newline delimiter
230
+ private sendMessage(conn: any, msg: FederationMessage) {
231
+ try {
232
+ conn.write(JSON.stringify(msg) + '\n');
233
+ } catch { /* connection may be closed */ }
234
+ }
235
+ }
@@ -0,0 +1,52 @@
1
+ // Federation Phase 2: Differential Privacy
2
+ // 임베딩 벡터에 노이즈를 추가하여 원문 복원 방지
3
+
4
+ import { randomBytes } from 'node:crypto';
5
+
6
+ export interface DPConfig {
7
+ epsilon: number; // 프라이버시 예산 (낮을수록 더 안전, 기본 1.0)
8
+ enabled: boolean;
9
+ }
10
+
11
+ const DEFAULT_DP: DPConfig = { epsilon: 1.0, enabled: true };
12
+
13
+ // 가우시안 노이즈 생성 (Box-Muller 변환)
14
+ function gaussianNoise(): number {
15
+ const u1 = Math.max(1e-10, Math.random());
16
+ const u2 = Math.random();
17
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
18
+ }
19
+
20
+ // 임베딩 벡터에 Differential Privacy 노이즈 추가
21
+ export function addDPNoise(embedding: number[], config: DPConfig = DEFAULT_DP): number[] {
22
+ if (!config.enabled) return embedding;
23
+
24
+ const sensitivity = 1.0; // L2 sensitivity (normalized embeddings)
25
+ const sigma = sensitivity / config.epsilon;
26
+
27
+ return embedding.map(v => v + gaussianNoise() * sigma);
28
+ }
29
+
30
+ // 노이즈 추가 후 L2 정규화 (코사인 유사도 유지)
31
+ export function addDPNoiseNormalized(embedding: number[], config: DPConfig = DEFAULT_DP): number[] {
32
+ const noisy = addDPNoise(embedding, config);
33
+
34
+ // L2 정규화
35
+ let norm = 0;
36
+ for (const v of noisy) norm += v * v;
37
+ norm = Math.sqrt(norm);
38
+ if (norm === 0) return noisy;
39
+
40
+ return noisy.map(v => v / norm);
41
+ }
42
+
43
+ // 스니펫에 DP 적용 (단어 단위 마스킹)
44
+ export function maskSnippet(snippet: string, maskRate = 0.3): string {
45
+ const words = snippet.split(/\s+/);
46
+ return words.map(w => {
47
+ if (Math.random() < maskRate && w.length > 2) {
48
+ return w[0] + '***';
49
+ }
50
+ return w;
51
+ }).join(' ');
52
+ }
@@ -0,0 +1,202 @@
1
+ // Federation: Node Reputation System
2
+ // 자동 평판 점수 — trust + consistency + freshness + consensus + history
3
+
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { getTrustLevel } from './trust.js';
8
+
9
+ export interface ReputationRecord {
10
+ peerId: string;
11
+ displayName: string;
12
+
13
+ // 수동 (Web of Trust)
14
+ trustBase: number; // vouch=30, neutral=0, block=-100
15
+
16
+ // 자동: 결과 일관성
17
+ consistencyScore: number; // 0-25, 다른 노드들과 결과 유사도
18
+ consistencySamples: number; // 측정 횟수
19
+
20
+ // 자동: 활동 빈도
21
+ freshnessScore: number; // 0-15, 최근 활동
22
+ lastSeen: string;
23
+ totalInteractions: number;
24
+
25
+ // 자동: 합의
26
+ consensusScore: number; // 0-20, 같은 쿼리에 다른 노드와 동일 결과
27
+ consensusSamples: number;
28
+
29
+ // 자동: 유용성 피드백
30
+ historyScore: number; // 0-10, 사용자 피드백 (결과 클릭율 등)
31
+ helpfulCount: number;
32
+ unhelpfulCount: number;
33
+
34
+ updatedAt: string;
35
+ }
36
+
37
+ const REP_FILE = join(homedir(), '.stellavault', 'federation', 'reputation.json');
38
+
39
+ function loadRepDb(): Map<string, ReputationRecord> {
40
+ if (!existsSync(REP_FILE)) return new Map();
41
+ const records = JSON.parse(readFileSync(REP_FILE, 'utf-8')) as ReputationRecord[];
42
+ return new Map(records.map(r => [r.peerId, r]));
43
+ }
44
+
45
+ function saveRepDb(db: Map<string, ReputationRecord>): void {
46
+ mkdirSync(join(homedir(), '.stellavault', 'federation'), { recursive: true });
47
+ writeFileSync(REP_FILE, JSON.stringify([...db.values()], null, 2), 'utf-8');
48
+ }
49
+
50
+ function getOrCreateRecord(peerId: string, displayName = ''): ReputationRecord {
51
+ const db = loadRepDb();
52
+ if (db.has(peerId)) return db.get(peerId)!;
53
+ return {
54
+ peerId, displayName,
55
+ trustBase: 0,
56
+ consistencyScore: 0, consistencySamples: 0,
57
+ freshnessScore: 0, lastSeen: new Date().toISOString(), totalInteractions: 0,
58
+ consensusScore: 0, consensusSamples: 0,
59
+ historyScore: 0, helpfulCount: 0, unhelpfulCount: 0,
60
+ updatedAt: new Date().toISOString(),
61
+ };
62
+ }
63
+
64
+ // 종합 평판 점수 계산 (0-100)
65
+ export function computeReputation(peerId: string): number {
66
+ const rec = getOrCreateRecord(peerId);
67
+
68
+ // 수동 신뢰 기반
69
+ const trustLevel = getTrustLevel(peerId);
70
+ const trustBase = trustLevel === 'vouched' ? 30 : trustLevel === 'blocked' ? -100 : 0;
71
+
72
+ // blocked이면 즉시 0
73
+ if (trustBase <= -100) return 0;
74
+
75
+ const score = Math.max(0, Math.min(100,
76
+ trustBase +
77
+ rec.consistencyScore +
78
+ rec.freshnessScore +
79
+ rec.consensusScore +
80
+ rec.historyScore +
81
+ // 새 노드 보너스 (상호작용 적으면 중립 시작)
82
+ (rec.totalInteractions < 5 ? 40 : 0)
83
+ ));
84
+
85
+ return Math.round(score);
86
+ }
87
+
88
+ // 검색 결과에 합의 검증
89
+ export function verifyConsensus(
90
+ queryId: string,
91
+ allResults: Array<{ peerId: string; title: string; similarity: number }>,
92
+ ): Map<string, number> {
93
+ // 같은 제목을 반환한 노드가 많을수록 → 합의 높음
94
+ const titleCounts = new Map<string, number>();
95
+ for (const r of allResults) {
96
+ titleCounts.set(r.title, (titleCounts.get(r.title) ?? 0) + 1);
97
+ }
98
+
99
+ const totalPeers = new Set(allResults.map(r => r.peerId)).size;
100
+ const consensusBoost = new Map<string, number>();
101
+
102
+ for (const r of allResults) {
103
+ const count = titleCounts.get(r.title) ?? 1;
104
+ // 여러 노드가 같은 결과 → 신뢰도 부스트
105
+ const boost = count > 1 ? Math.min((count / totalPeers) * 20, 20) : 0;
106
+ const current = consensusBoost.get(r.peerId) ?? 0;
107
+ consensusBoost.set(r.peerId, Math.max(current, boost));
108
+ }
109
+
110
+ return consensusBoost;
111
+ }
112
+
113
+ // 노드 상호작용 기록 (검색 응답 받을 때마다)
114
+ export function recordInteraction(peerId: string, displayName: string): void {
115
+ const db = loadRepDb();
116
+ const rec = getOrCreateRecord(peerId, displayName);
117
+
118
+ rec.totalInteractions++;
119
+ rec.lastSeen = new Date().toISOString();
120
+ rec.displayName = displayName || rec.displayName;
121
+
122
+ // Freshness: 최근 7일 이내 활동이면 만점
123
+ const daysSinceLastSeen = (Date.now() - new Date(rec.lastSeen).getTime()) / 86400000;
124
+ rec.freshnessScore = daysSinceLastSeen < 1 ? 15 : daysSinceLastSeen < 7 ? 10 : daysSinceLastSeen < 30 ? 5 : 0;
125
+
126
+ rec.updatedAt = new Date().toISOString();
127
+ db.set(peerId, rec);
128
+ saveRepDb(db);
129
+ }
130
+
131
+ // 결과 일관성 업데이트 (다른 노드와 비교)
132
+ export function recordConsistency(peerId: string, matchRate: number): void {
133
+ const db = loadRepDb();
134
+ const rec = getOrCreateRecord(peerId);
135
+
136
+ rec.consistencySamples++;
137
+ // 이동 평균
138
+ rec.consistencyScore = Math.round(
139
+ (rec.consistencyScore * (rec.consistencySamples - 1) + matchRate * 25) / rec.consistencySamples
140
+ );
141
+
142
+ rec.updatedAt = new Date().toISOString();
143
+ db.set(peerId, rec);
144
+ saveRepDb(db);
145
+ }
146
+
147
+ // 사용자 피드백 (결과가 유용했는지)
148
+ export function recordFeedback(peerId: string, helpful: boolean): void {
149
+ const db = loadRepDb();
150
+ const rec = getOrCreateRecord(peerId);
151
+
152
+ if (helpful) rec.helpfulCount++;
153
+ else rec.unhelpfulCount++;
154
+
155
+ const total = rec.helpfulCount + rec.unhelpfulCount;
156
+ rec.historyScore = total > 0 ? Math.round((rec.helpfulCount / total) * 10) : 0;
157
+
158
+ rec.updatedAt = new Date().toISOString();
159
+ db.set(peerId, rec);
160
+ saveRepDb(db);
161
+ }
162
+
163
+ // 합의 점수 업데이트
164
+ export function recordConsensus(peerId: string, consensusBoost: number): void {
165
+ const db = loadRepDb();
166
+ const rec = getOrCreateRecord(peerId);
167
+
168
+ rec.consensusSamples++;
169
+ rec.consensusScore = Math.round(
170
+ (rec.consensusScore * (rec.consensusSamples - 1) + consensusBoost) / rec.consensusSamples
171
+ );
172
+
173
+ rec.updatedAt = new Date().toISOString();
174
+ db.set(peerId, rec);
175
+ saveRepDb(db);
176
+ }
177
+
178
+ // 평판 목록 조회
179
+ export function getReputationBoard(): Array<ReputationRecord & { reputation: number }> {
180
+ const db = loadRepDb();
181
+ return [...db.values()]
182
+ .map(r => ({ ...r, reputation: computeReputation(r.peerId) }))
183
+ .sort((a, b) => b.reputation - a.reputation);
184
+ }
185
+
186
+ // 검색 결과 필터 + 정렬 (평판 가중)
187
+ export function filterByReputation<T extends { peerId: string; similarity: number }>(
188
+ results: T[],
189
+ minReputation = 10,
190
+ ): (T & { adjustedScore: number; reputation: number })[] {
191
+ return results
192
+ .map(r => {
193
+ const rep = computeReputation(r.peerId);
194
+ // blocked 노드 필터링
195
+ if (rep === 0) return null;
196
+ // 평판 가중 점수: similarity * 0.7 + reputation/100 * 0.3
197
+ const adjustedScore = r.similarity * 0.7 + (rep / 100) * 0.3;
198
+ return { ...r, adjustedScore, reputation: rep };
199
+ })
200
+ .filter((r): r is NonNullable<typeof r> => r !== null && r.reputation >= minReputation)
201
+ .sort((a, b) => b.adjustedScore - a.adjustedScore);
202
+ }
@@ -0,0 +1,129 @@
1
+ // Design Ref: §5 — FederatedSearch (피어 검색 + 결과 병합)
2
+ // Plan SC: SC2 (연합 검색), SC3 (원문 비노출), SC4 (3초 이내)
3
+
4
+ import { randomUUID } from 'node:crypto';
5
+ import type { FederationNode } from './node.js';
6
+ import type { VectorStore } from '../store/types.js';
7
+ import type { Embedder } from '../indexer/embedder.js';
8
+ import type { FederatedSearchResult } from './types.js';
9
+ import { maskSnippet } from './privacy.js';
10
+ import { isDocumentShareable, sanitizeSnippet } from './sharing.js';
11
+
12
+ export interface FederatedSearchOptions {
13
+ limit?: number;
14
+ timeout?: number; // ms, default 5000
15
+ }
16
+
17
+ export class FederatedSearch {
18
+ private additionalStores: VectorStore[] = [];
19
+
20
+ constructor(
21
+ private node: FederationNode,
22
+ private store: VectorStore,
23
+ private embedder: Embedder,
24
+ ) {}
25
+
26
+ // Multi-vault: 추가 store 등록 (Federation 검색 응답 시 전체 vault 검색)
27
+ addStore(store: VectorStore): void {
28
+ this.additionalStores.push(store);
29
+ }
30
+
31
+ // Design Ref: §5 — search() 요청 측
32
+ async search(query: string, options: FederatedSearchOptions = {}): Promise<FederatedSearchResult[]> {
33
+ const { limit = 5, timeout = 5000 } = options;
34
+ const peers = this.node.getPeers();
35
+
36
+ if (peers.length === 0) {
37
+ return [];
38
+ }
39
+
40
+ // 1. 쿼리 임베딩 생성
41
+ const embedding = await this.embedder.embed(query);
42
+ const queryId = randomUUID().slice(0, 8);
43
+
44
+ // 2. 모든 피어에 병렬 전송 + 응답 대기
45
+ const results: FederatedSearchResult[] = [];
46
+ const peerMap = new Map(peers.map(p => [p.peerId, p.displayName]));
47
+
48
+ const responsePromises = peers.map((peer) => {
49
+ return new Promise<void>((resolve) => {
50
+ const timer = setTimeout(resolve, timeout);
51
+
52
+ const handler = (data: { queryId: string; results: any[]; peerId: string }) => {
53
+ if (data.queryId !== queryId) return;
54
+
55
+ for (const r of data.results) {
56
+ results.push({
57
+ title: r.title,
58
+ similarity: r.similarity,
59
+ snippet: r.snippet,
60
+ peerId: peer.peerId,
61
+ peerName: peer.displayName,
62
+ });
63
+ }
64
+
65
+ clearTimeout(timer);
66
+ this.node.removeListener('search_response', handler);
67
+ resolve();
68
+ };
69
+
70
+ this.node.on('search_response', handler);
71
+
72
+ // 쿼리 전송
73
+ this.node.sendSearchQuery(peer.peerId, queryId, Array.from(embedding), limit);
74
+ });
75
+ });
76
+
77
+ await Promise.allSettled(responsePromises);
78
+
79
+ // 3. 결과 병합 — similarity 내림차순
80
+ return results
81
+ .sort((a, b) => b.similarity - a.similarity)
82
+ .slice(0, limit);
83
+ }
84
+
85
+ // Design Ref: §5 — startResponder() 피어 요청 수신 측
86
+ startResponder(): void {
87
+ this.node.on('search_request', async (req: {
88
+ queryId: string;
89
+ embedding: number[];
90
+ limit: number;
91
+ respondTo: string | null;
92
+ }) => {
93
+ if (!req.respondTo) return;
94
+
95
+ try {
96
+ // 받은 임베딩으로 모든 로컬 vault DB 검색 (multi-vault)
97
+ const allStores = [this.store, ...this.additionalStores];
98
+ const allScored = await Promise.all(
99
+ allStores.map(s => s.searchSemantic(req.embedding, req.limit).catch(() => []))
100
+ );
101
+ const scored = allScored.flat().sort((a, b) => b.score - a.score).slice(0, req.limit);
102
+
103
+ // Plan SC: SC3 — 원문 비노출. 제목+유사도+50자만.
104
+ const safe: Array<{ title: string; similarity: number; snippet: string }> = [];
105
+ for (const s of scored) {
106
+ const chunk = await this.store.getChunk(s.chunkId);
107
+ if (!chunk) continue;
108
+ // 청크의 documentId에서 문서 제목 가져오기
109
+ const doc = await this.store.getDocument(chunk.documentId);
110
+ if (!doc) continue;
111
+
112
+ // Sharing filter: 비공개 문서는 검색 결과에서 제외
113
+ if (!isDocumentShareable({ tags: doc.tags, filePath: doc.filePath, id: doc.id, content: doc.content })) continue;
114
+
115
+ safe.push({
116
+ title: doc.title ?? chunk.heading ?? 'Untitled',
117
+ similarity: Math.round(s.score * 1000) / 1000,
118
+ snippet: sanitizeSnippet(maskSnippet(chunk.content.slice(0, 50), 0.2)),
119
+ });
120
+ }
121
+
122
+ this.node.sendSearchResult(req.respondTo, req.queryId, safe);
123
+ } catch (err) {
124
+ // 검색 실패 시 빈 결과 반환
125
+ this.node.sendSearchResult(req.respondTo, req.queryId, []);
126
+ }
127
+ });
128
+ }
129
+ }