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,113 @@
1
+ // Design Ref: §6.3 — Incremental Indexing + Pipeline
2
+
3
+ import type { Embedder } from './embedder.js';
4
+ import type { VectorStore } from '../store/types.js';
5
+ import type { Document } from '../types/document.js';
6
+ import type { Chunk } from '../types/chunk.js';
7
+ import { scanVault } from './scanner.js';
8
+ import { chunkDocument, type ChunkOptions } from './chunker.js';
9
+ import { withRetry, errors } from '../utils/retry.js';
10
+
11
+ export { type Embedder } from './embedder.js';
12
+ export { createLocalEmbedder } from './local-embedder.js';
13
+ export { scanVault } from './scanner.js';
14
+ export { chunkDocument, estimateTokens } from './chunker.js';
15
+ export { createWatcher } from './watcher.js';
16
+
17
+ export interface IndexerOptions {
18
+ store: VectorStore;
19
+ embedder: Embedder;
20
+ chunkOptions?: Partial<ChunkOptions>;
21
+ onProgress?: (current: number, total: number, doc: Document) => void;
22
+ }
23
+
24
+ export interface IndexResult {
25
+ indexed: number;
26
+ skipped: number;
27
+ deleted: number;
28
+ failed: number;
29
+ totalChunks: number;
30
+ elapsedMs: number;
31
+ }
32
+
33
+ /**
34
+ * vault를 스캔하여 변경된 문서만 벡터화하는 증분 인덱서
35
+ */
36
+ export async function indexVault(
37
+ vaultPath: string,
38
+ options: IndexerOptions,
39
+ ): Promise<IndexResult> {
40
+ const start = Date.now();
41
+ const { store, embedder, chunkOptions, onProgress } = options;
42
+
43
+ // 1. 스캔
44
+ const { documents } = scanVault(vaultPath);
45
+
46
+ // 2. 기존 인덱스 상태 조회
47
+ const existingDocs = await store.getAllDocuments();
48
+ const existingMap = new Map(existingDocs.map(d => [d.id, d.contentHash]));
49
+
50
+ let indexed = 0;
51
+ let skipped = 0;
52
+ let failed = 0;
53
+ let totalChunks = 0;
54
+
55
+ // 3. 증분 처리 (에러 복구 포함)
56
+ const scannedIds = new Set<string>();
57
+ for (let i = 0; i < documents.length; i++) {
58
+ const doc = documents[i];
59
+ scannedIds.add(doc.id);
60
+ onProgress?.(i + 1, documents.length, doc);
61
+
62
+ // content_hash 비교 → 변경 없으면 SKIP
63
+ if (existingMap.get(doc.id) === doc.contentHash) {
64
+ skipped++;
65
+ continue;
66
+ }
67
+
68
+ try {
69
+ // 청킹
70
+ const chunks = chunkDocument(doc.id, doc.content, chunkOptions);
71
+
72
+ // 임베딩 (retry with backoff)
73
+ const texts = chunks.map(c => c.content);
74
+ const embeddings = await withRetry(
75
+ () => embedder.embedBatch(texts),
76
+ { maxRetries: 2, baseDelayMs: 1000 },
77
+ );
78
+ const chunksWithEmbeddings: Chunk[] = chunks.map((c, j) => ({
79
+ ...c,
80
+ embedding: embeddings[j],
81
+ }));
82
+
83
+ // 저장 (document → chunks)
84
+ await store.upsertDocument(doc);
85
+ await store.upsertChunks(chunksWithEmbeddings);
86
+
87
+ indexed++;
88
+ totalChunks += chunks.length;
89
+ } catch (err) {
90
+ // Graceful degradation: skip failed file, continue with rest
91
+ failed++;
92
+ console.error(errors.indexingFailed(doc.filePath, err).format());
93
+ }
94
+ }
95
+
96
+ // 4. 삭제된 파일 처리
97
+ let deleted = 0;
98
+ for (const [existingId] of existingMap) {
99
+ if (!scannedIds.has(existingId)) {
100
+ await store.deleteByDocumentId(existingId);
101
+ deleted++;
102
+ }
103
+ }
104
+
105
+ return {
106
+ indexed,
107
+ skipped,
108
+ deleted,
109
+ failed,
110
+ totalChunks,
111
+ elapsedMs: Date.now() - start,
112
+ };
113
+ }
@@ -0,0 +1,35 @@
1
+ // Design Ref: §3.2 — Embedder 로컬 구현 (nomic-embed-text via @xenova/transformers)
2
+
3
+ import type { Embedder } from './embedder.js';
4
+
5
+ export function createLocalEmbedder(modelName: string = 'nomic-embed-text-v1.5'): Embedder {
6
+ let pipeline: any;
7
+ // all-MiniLM-L6-v2: 384, nomic-embed-text: 768
8
+ let dims = modelName.includes('MiniLM') ? 384 : 768;
9
+
10
+ return {
11
+ async initialize() {
12
+ const { pipeline: createPipeline } = await import('@xenova/transformers');
13
+ pipeline = await createPipeline('feature-extraction', `Xenova/${modelName}`, {
14
+ quantized: true,
15
+ });
16
+ },
17
+
18
+ async embed(text: string): Promise<number[]> {
19
+ const output = await pipeline(text, { pooling: 'mean', normalize: true });
20
+ return Array.from(output.data as Float32Array).slice(0, dims);
21
+ },
22
+
23
+ async embedBatch(texts: string[]): Promise<number[][]> {
24
+ const results: number[][] = [];
25
+ // 순차 처리 (메모리 절약)
26
+ for (const text of texts) {
27
+ results.push(await this.embed(text));
28
+ }
29
+ return results;
30
+ },
31
+
32
+ get dimensions() { return dims; },
33
+ get modelName() { return modelName; },
34
+ };
35
+ }
@@ -0,0 +1,142 @@
1
+ // Design Ref: §6 — Indexer (scanner: glob + frontmatter 파싱)
2
+
3
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
4
+ import { join, relative, extname } from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import matter from 'gray-matter';
7
+ import type { Document } from '../types/document.js';
8
+
9
+ export interface ScanResult {
10
+ documents: Document[];
11
+ scannedFiles: number;
12
+ skippedFiles: number;
13
+ }
14
+
15
+ /**
16
+ * vault 디렉토리에서 모든 .md 파일을 스캔하여 Document 목록 반환
17
+ */
18
+ export function scanVault(vaultPath: string): ScanResult {
19
+ const documents: Document[] = [];
20
+ let skippedFiles = 0;
21
+
22
+ const mdFiles = findMdFiles(vaultPath);
23
+
24
+ for (const filePath of mdFiles) {
25
+ try {
26
+ const doc = parseDocument(vaultPath, filePath);
27
+ documents.push(doc);
28
+ } catch {
29
+ skippedFiles++;
30
+ }
31
+ }
32
+
33
+ return { documents, scannedFiles: mdFiles.length, skippedFiles };
34
+ }
35
+
36
+ function findMdFiles(dir: string, files: string[] = []): string[] {
37
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
38
+ const fullPath = join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'zh-CN') continue;
41
+ findMdFiles(fullPath, files);
42
+ } else if (extname(entry.name) === '.md') {
43
+ files.push(fullPath);
44
+ }
45
+ }
46
+ return files;
47
+ }
48
+
49
+ function parseDocument(vaultPath: string, filePath: string): Document {
50
+ const raw = readFileSync(filePath, 'utf-8');
51
+ const stat = statSync(filePath);
52
+ const { data: frontmatter, content } = matter(raw);
53
+
54
+ const relativePath = relative(vaultPath, filePath).replace(/\\/g, '/');
55
+ const id = createHash('sha256').update(relativePath).digest('hex').slice(0, 16);
56
+ const contentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
57
+
58
+ const title = (frontmatter.title as string)
59
+ ?? extractFirstHeading(content)
60
+ ?? relativePath.replace(/\.md$/, '');
61
+
62
+ const tags = extractTags(frontmatter, content);
63
+
64
+ // source/type 자동 추출 (원본 파일 수정 없이 DB에만 저장)
65
+ const source = inferSource(frontmatter, relativePath);
66
+ const type = inferType(frontmatter, relativePath);
67
+
68
+ return {
69
+ id,
70
+ filePath: relativePath,
71
+ title,
72
+ content,
73
+ frontmatter,
74
+ tags,
75
+ lastModified: stat.mtime.toISOString(),
76
+ contentHash,
77
+ source,
78
+ type,
79
+ };
80
+ }
81
+
82
+ function inferSource(frontmatter: Record<string, unknown>, filePath: string): string {
83
+ // frontmatter에 명시된 경우
84
+ if (frontmatter.source && typeof frontmatter.source === 'string') {
85
+ if (frontmatter.source.startsWith('http')) return 'clip';
86
+ return frontmatter.source;
87
+ }
88
+ // 경로 기반 추론
89
+ if (filePath.includes('clips/') || filePath.includes('clip/')) return 'clip';
90
+ if (filePath.includes('PDCA') || filePath.includes('pdca')) return 'local';
91
+ if (frontmatter['x-i18n']) return 'notion'; // Notion 번역 문서
92
+ if (frontmatter.clipped) return 'clip';
93
+ return 'local';
94
+ }
95
+
96
+ function inferType(frontmatter: Record<string, unknown>, filePath: string): string {
97
+ // frontmatter에 명시된 경우
98
+ if (frontmatter.type && typeof frontmatter.type === 'string') return frontmatter.type;
99
+ // tags 기반
100
+ const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
101
+ if (tags.includes('bridge') || tags.includes('auto-generated')) return 'bridge';
102
+ if (tags.includes('clip') || tags.includes('youtube')) return 'clip';
103
+ if (tags.includes('decision')) return 'decision';
104
+ // 경로 기반
105
+ if (filePath.includes('clips/')) return 'clip';
106
+ if (filePath.includes('Decisions/') || filePath.includes('decisions/')) return 'decision';
107
+ if (filePath.includes('Sessions/') || filePath.includes('sessions/')) return 'session';
108
+ if (filePath.includes('Research/')) return 'research';
109
+ if (filePath.includes('Lessons/')) return 'lesson';
110
+ if (filePath.includes('Templates/')) return 'template';
111
+ return 'note';
112
+ }
113
+
114
+ function extractFirstHeading(content: string): string | null {
115
+ const match = content.match(/^#\s+(.+)$/m);
116
+ return match ? match[1].trim() : null;
117
+ }
118
+
119
+ function extractTags(frontmatter: Record<string, unknown>, content: string): string[] {
120
+ const tags = new Set<string>();
121
+
122
+ // frontmatter tags
123
+ const fmTags = frontmatter.tags;
124
+ if (Array.isArray(fmTags)) {
125
+ fmTags.forEach(t => tags.add(String(t)));
126
+ } else if (typeof fmTags === 'string') {
127
+ fmTags.split(',').map(t => t.trim()).filter(Boolean).forEach(t => tags.add(t));
128
+ }
129
+
130
+ // inline #tags (CSS 컬러코드, 순수 숫자, heading # 제외)
131
+ const inlineTags = content.match(/(?:^|\s)#([a-zA-Z가-힣][a-zA-Z가-힣\w-]*)/g);
132
+ if (inlineTags) {
133
+ for (const raw of inlineTags) {
134
+ const tag = raw.trim().slice(1);
135
+ // CSS hex 컬러 (#fff, #6c5ce7 등) 제외
136
+ if (/^[0-9a-fA-F]{3,8}$/.test(tag)) continue;
137
+ tags.add(tag);
138
+ }
139
+ }
140
+
141
+ return [...tags];
142
+ }
@@ -0,0 +1,62 @@
1
+ // Design Ref: §6.3 — 파일 감시 + 증분 인덱싱 (debounce 5s)
2
+
3
+ import { watch, type FSWatcher } from 'chokidar';
4
+ import { extname } from 'node:path';
5
+ import type { Embedder } from './embedder.js';
6
+ import type { VectorStore } from '../store/types.js';
7
+ import { indexVault } from './index.js';
8
+ import type { ChunkOptions } from './chunker.js';
9
+
10
+ export interface WatcherOptions {
11
+ vaultPath: string;
12
+ store: VectorStore;
13
+ embedder: Embedder;
14
+ chunkOptions?: Partial<ChunkOptions>;
15
+ debounceMs?: number;
16
+ onReindex?: (result: { indexed: number; skipped: number }) => void;
17
+ }
18
+
19
+ export function createWatcher(options: WatcherOptions): { start(): void; stop(): void } {
20
+ const { vaultPath, store, embedder, chunkOptions, debounceMs = 5000, onReindex } = options;
21
+ let watcher: FSWatcher | null = null;
22
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
23
+ let reindexing = false;
24
+
25
+ async function triggerReindex() {
26
+ if (reindexing) return;
27
+ reindexing = true;
28
+ try {
29
+ const result = await indexVault(vaultPath, { store, embedder, chunkOptions });
30
+ onReindex?.({ indexed: result.indexed, skipped: result.skipped });
31
+ } finally {
32
+ reindexing = false;
33
+ }
34
+ }
35
+
36
+ function scheduleReindex() {
37
+ if (debounceTimer) clearTimeout(debounceTimer);
38
+ debounceTimer = setTimeout(() => triggerReindex(), debounceMs);
39
+ }
40
+
41
+ return {
42
+ start() {
43
+ watcher = watch(vaultPath, {
44
+ ignored: /(^|[\/\\])\.|node_modules/,
45
+ persistent: true,
46
+ ignoreInitial: true,
47
+ });
48
+
49
+ watcher.on('all', (event, path) => {
50
+ if (extname(path) !== '.md') return;
51
+ if (['add', 'change', 'unlink'].includes(event)) {
52
+ scheduleReindex();
53
+ }
54
+ });
55
+ },
56
+
57
+ stop() {
58
+ if (debounceTimer) clearTimeout(debounceTimer);
59
+ watcher?.close();
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,134 @@
1
+ // Contradiction Detector (F-A12)
2
+ // Finds potentially contradicting statements across notes
3
+ // Uses embedding similarity + negation pattern detection
4
+
5
+ import type { VectorStore } from '../store/types.js';
6
+
7
+ export interface ContradictionPair {
8
+ docA: { id: string; title: string; filePath: string; statement: string };
9
+ docB: { id: string; title: string; filePath: string; statement: string };
10
+ similarity: number;
11
+ confidence: number; // 0-1, how likely this is a real contradiction
12
+ type: 'negation' | 'value_conflict' | 'temporal' | 'semantic';
13
+ }
14
+
15
+ // Negation/opposition patterns
16
+ const NEGATION_PAIRS = [
17
+ ['should', 'should not'], ['must', 'must not'], ['always', 'never'],
18
+ ['best', 'worst'], ['good', 'bad'], ['correct', 'incorrect'],
19
+ ['true', 'false'], ['increase', 'decrease'], ['enable', 'disable'],
20
+ ['recommended', 'not recommended'], ['use', 'avoid'],
21
+ ['prefer', 'avoid'], ['do', "don't"], ['is', "isn't"],
22
+ ['can', "can't"], ['will', "won't"], ['important', 'unimportant'],
23
+ ['필요', '불필요'], ['해야', '하면 안'], ['좋', '나쁜'], ['맞', '틀'],
24
+ ];
25
+
26
+ function extractKeyStatements(content: string): string[] {
27
+ return content
28
+ .split(/[.\n!?]/)
29
+ .map(s => s.trim())
30
+ .filter(s => s.length > 20 && s.length < 200)
31
+ .filter(s => {
32
+ const lower = s.toLowerCase();
33
+ return /should|must|always|never|best|important|recommend|prefer|avoid|필요|해야|좋|나쁜/.test(lower);
34
+ })
35
+ .slice(0, 10); // max 10 statements per document
36
+ }
37
+
38
+ function detectNegationConflict(stmtA: string, stmtB: string): { isConflict: boolean; confidence: number; type: ContradictionPair['type'] } {
39
+ const a = stmtA.toLowerCase();
40
+ const b = stmtB.toLowerCase();
41
+
42
+ // Check negation pairs
43
+ for (const [pos, neg] of NEGATION_PAIRS) {
44
+ if ((a.includes(pos) && b.includes(neg)) || (a.includes(neg) && b.includes(pos))) {
45
+ // Check if they're talking about the same subject (share words)
46
+ const wordsA = new Set(a.split(/\s+/).filter(w => w.length > 3));
47
+ const wordsB = new Set(b.split(/\s+/).filter(w => w.length > 3));
48
+ const overlap = [...wordsA].filter(w => wordsB.has(w)).length;
49
+ const minSize = Math.min(wordsA.size, wordsB.size) || 1;
50
+ const subjectOverlap = overlap / minSize;
51
+
52
+ if (subjectOverlap > 0.2) {
53
+ return { isConflict: true, confidence: Math.min(0.5 + subjectOverlap * 0.5, 0.95), type: 'negation' };
54
+ }
55
+ }
56
+ }
57
+
58
+ // Check numeric value conflicts (e.g., "timeout should be 30s" vs "timeout should be 5s")
59
+ const numsA = a.match(/\d+/g);
60
+ const numsB = b.match(/\d+/g);
61
+ if (numsA && numsB) {
62
+ const wordsA = new Set(a.replace(/\d+/g, '').split(/\s+/).filter(w => w.length > 3));
63
+ const wordsB = new Set(b.replace(/\d+/g, '').split(/\s+/).filter(w => w.length > 3));
64
+ const overlap = [...wordsA].filter(w => wordsB.has(w)).length;
65
+ if (overlap >= 2 && numsA[0] !== numsB[0]) {
66
+ return { isConflict: true, confidence: 0.6, type: 'value_conflict' };
67
+ }
68
+ }
69
+
70
+ return { isConflict: false, confidence: 0, type: 'semantic' };
71
+ }
72
+
73
+ export async function detectContradictions(
74
+ store: VectorStore,
75
+ limit = 20,
76
+ ): Promise<ContradictionPair[]> {
77
+ const docs = await store.getAllDocuments();
78
+ const embeddings = await store.getDocumentEmbeddings();
79
+
80
+ if (docs.length < 2) return [];
81
+
82
+ // Build document vectors + key statements
83
+ const docData = new Map<string, { vec: number[]; title: string; filePath: string; statements: string[] }>();
84
+ for (const doc of docs) {
85
+ const vec = embeddings.get(doc.id);
86
+ if (!vec) continue;
87
+ const statements = extractKeyStatements(doc.content);
88
+ if (statements.length === 0) continue;
89
+ docData.set(doc.id, { vec: Array.from(vec), title: doc.title, filePath: doc.filePath, statements });
90
+ }
91
+
92
+ const ids = [...docData.keys()];
93
+ const results: ContradictionPair[] = [];
94
+
95
+ // Compare documents with moderate similarity (same topic, possibly conflicting)
96
+ for (let i = 0; i < ids.length && results.length < limit * 3; i++) {
97
+ for (let j = i + 1; j < ids.length && results.length < limit * 3; j++) {
98
+ const a = docData.get(ids[i])!;
99
+ const b = docData.get(ids[j])!;
100
+ const sim = cosineSim(a.vec, b.vec);
101
+
102
+ // Sweet spot: similar enough to be same topic, not identical
103
+ if (sim < 0.3 || sim > 0.9) continue;
104
+
105
+ // Compare statements
106
+ for (const stmtA of a.statements) {
107
+ for (const stmtB of b.statements) {
108
+ const { isConflict, confidence, type } = detectNegationConflict(stmtA, stmtB);
109
+ if (isConflict && confidence >= 0.5) {
110
+ results.push({
111
+ docA: { id: ids[i], title: a.title, filePath: a.filePath, statement: stmtA },
112
+ docB: { id: ids[j], title: b.title, filePath: b.filePath, statement: stmtB },
113
+ similarity: Math.round(sim * 1000) / 1000,
114
+ confidence: Math.round(confidence * 100) / 100,
115
+ type,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ return results
124
+ .sort((a, b) => b.confidence - a.confidence)
125
+ .slice(0, limit);
126
+ }
127
+
128
+ function cosineSim(a: number[], b: number[]): number {
129
+ if (a.length !== b.length) return 0;
130
+ let dot = 0, na = 0, nb = 0;
131
+ for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
132
+ const d = Math.sqrt(na) * Math.sqrt(nb);
133
+ return d === 0 ? 0 : dot / d;
134
+ }