claude-memory-layer 1.0.26 → 1.0.28

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 (328) hide show
  1. package/.env.example +7 -0
  2. package/AGENTS.md +11 -0
  3. package/README.md +184 -41
  4. package/benchmarks/replay/anonymized-real-sessions.json +48 -0
  5. package/dist/cli/index.js +10097 -6003
  6. package/dist/cli/index.js.map +4 -4
  7. package/dist/core/index.js +9745 -5587
  8. package/dist/core/index.js.map +4 -4
  9. package/dist/hooks/post-tool-use.js +6545 -5270
  10. package/dist/hooks/post-tool-use.js.map +4 -4
  11. package/dist/hooks/semantic-daemon.js +6646 -5354
  12. package/dist/hooks/semantic-daemon.js.map +4 -4
  13. package/dist/hooks/session-end.js +6618 -5347
  14. package/dist/hooks/session-end.js.map +4 -4
  15. package/dist/hooks/session-start.js +6619 -5354
  16. package/dist/hooks/session-start.js.map +4 -4
  17. package/dist/hooks/stop.js +6614 -5325
  18. package/dist/hooks/stop.js.map +4 -4
  19. package/dist/hooks/user-prompt-submit.js +6702 -5356
  20. package/dist/hooks/user-prompt-submit.js.map +4 -4
  21. package/dist/index.js +13537 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/mcp/index.js +20770 -0
  24. package/dist/mcp/index.js.map +7 -0
  25. package/dist/server/api/index.js +6632 -5319
  26. package/dist/server/api/index.js.map +4 -4
  27. package/dist/server/index.js +6667 -5340
  28. package/dist/server/index.js.map +4 -4
  29. package/dist/services/memory-service.js +6568 -5350
  30. package/dist/services/memory-service.js.map +4 -4
  31. package/dist/ui/assets/js/bootstrap.js +244 -0
  32. package/dist/ui/assets/js/chat.js +373 -0
  33. package/dist/ui/assets/js/disclosure.js +232 -0
  34. package/dist/ui/assets/js/modals.js +298 -0
  35. package/dist/ui/assets/js/overview.js +655 -0
  36. package/dist/ui/assets/js/state.js +72 -0
  37. package/dist/ui/assets/js/views.js +468 -0
  38. package/dist/ui/index.html +43 -1
  39. package/dist/ui/index.ts +3 -0
  40. package/dist/ui/style.css +222 -0
  41. package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
  42. package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
  43. package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
  44. package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
  45. package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
  46. package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
  47. package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
  48. package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
  49. package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
  50. package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
  51. package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
  52. package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
  53. package/docs/architecture/comparison-index.md +47 -0
  54. package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
  55. package/package.json +9 -5
  56. package/scripts/build.ts +25 -8
  57. package/scripts/generate-session-qrels.ts +126 -0
  58. package/scripts/replay-retrieval-benchmark.ts +69 -0
  59. package/specs/thin-core-refactor/context.md +275 -0
  60. package/specs/thin-core-refactor/plan.md +536 -0
  61. package/specs/thin-core-refactor/spec.md +465 -0
  62. package/src/adapters/claude/capture/index.ts +3 -0
  63. package/src/adapters/claude/context/index.ts +3 -0
  64. package/src/adapters/claude/hooks/index.ts +21 -0
  65. package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
  66. package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
  67. package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
  68. package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
  69. package/src/adapters/claude/hooks/session-end.ts +59 -0
  70. package/src/adapters/claude/hooks/session-start.ts +73 -0
  71. package/src/adapters/claude/hooks/stop.ts +128 -0
  72. package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
  73. package/src/adapters/claude/index.ts +4 -0
  74. package/src/adapters/claude/transcript/index.ts +4 -0
  75. package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
  76. package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
  77. package/src/apps/cli/claude-settings-hooks.ts +138 -0
  78. package/src/apps/cli/codex-import-runner.ts +125 -0
  79. package/src/apps/cli/codex-validation-output.ts +95 -0
  80. package/src/apps/cli/hermes-import-runner.ts +130 -0
  81. package/src/apps/cli/hermes-validation-output.ts +91 -0
  82. package/src/apps/cli/index.ts +1731 -0
  83. package/src/apps/cli/mcp-install.ts +106 -0
  84. package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
  85. package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
  86. package/src/apps/dashboard/assets/js/chat.js +373 -0
  87. package/src/apps/dashboard/assets/js/disclosure.js +232 -0
  88. package/src/apps/dashboard/assets/js/modals.js +298 -0
  89. package/src/apps/dashboard/assets/js/overview.js +655 -0
  90. package/src/apps/dashboard/assets/js/state.js +72 -0
  91. package/src/apps/dashboard/assets/js/views.js +468 -0
  92. package/src/{ui → apps/dashboard}/index.html +43 -1
  93. package/src/apps/dashboard/index.ts +3 -0
  94. package/src/{ui → apps/dashboard}/style.css +222 -0
  95. package/src/apps/index.ts +5 -0
  96. package/src/apps/server/api/chat.ts +244 -0
  97. package/src/apps/server/api/citations.ts +105 -0
  98. package/src/apps/server/api/events.ts +137 -0
  99. package/src/apps/server/api/health.ts +53 -0
  100. package/src/apps/server/api/index.ts +26 -0
  101. package/src/apps/server/api/projects.ts +74 -0
  102. package/src/apps/server/api/search.ts +184 -0
  103. package/src/apps/server/api/sessions.ts +115 -0
  104. package/src/apps/server/api/stats.ts +723 -0
  105. package/src/apps/server/api/turns.ts +143 -0
  106. package/src/apps/server/api/utils.ts +65 -0
  107. package/src/apps/server/index.ts +111 -0
  108. package/src/cli/index.ts +2 -1311
  109. package/src/cli/retrieval-disclosure-output.ts +2 -0
  110. package/src/compat/index.ts +5 -0
  111. package/src/core/derive/fact-deriver.ts +170 -0
  112. package/src/core/derive/index.ts +2 -0
  113. package/src/core/derive/summary-deriver.ts +76 -0
  114. package/src/core/embedder.ts +4 -152
  115. package/src/core/engine/embedding-maintenance-service.ts +187 -0
  116. package/src/core/engine/endless-memory-services.ts +4 -0
  117. package/src/core/engine/index.ts +19 -0
  118. package/src/core/engine/memory-engine-services.ts +170 -0
  119. package/src/core/engine/memory-ingest-service.ts +317 -0
  120. package/src/core/engine/memory-query-service.ts +173 -0
  121. package/src/core/engine/memory-runtime-service.ts +162 -0
  122. package/src/core/engine/memory-service-composition.ts +231 -0
  123. package/src/core/engine/retrieval-analytics-service.ts +181 -0
  124. package/src/core/engine/retrieval-disclosure-service.ts +420 -0
  125. package/src/core/engine/retrieval-orchestrator.ts +377 -0
  126. package/src/core/engine/retrieval-services.ts +176 -0
  127. package/src/core/engine/shared-memory-services.ts +4 -0
  128. package/src/core/entity-repo.ts +1 -3
  129. package/src/core/event-store.ts +3 -3
  130. package/src/core/evidence-aligner.ts +2 -2
  131. package/src/core/external-market-context.ts +582 -0
  132. package/src/core/graduation.ts +2 -3
  133. package/src/core/index.ts +21 -0
  134. package/src/core/matcher.ts +2 -4
  135. package/src/core/model/memory-fact.ts +30 -0
  136. package/src/core/model/memory-rule.ts +14 -0
  137. package/src/core/model/memory-summary.ts +21 -0
  138. package/src/core/model/raw-event.ts +28 -0
  139. package/src/core/model/retrieval-result.ts +35 -0
  140. package/src/core/privacy/filter.ts +21 -10
  141. package/src/core/product-validation-matrix.ts +314 -0
  142. package/src/core/progressive-retriever.ts +1 -2
  143. package/src/core/registry/project-path.ts +54 -0
  144. package/src/core/registry/session-registry.ts +69 -0
  145. package/src/core/replay-evaluator.ts +625 -0
  146. package/src/core/retrieval-benchmark.ts +117 -0
  147. package/src/core/retrieval-quality.ts +109 -0
  148. package/src/core/retriever.ts +53 -15
  149. package/src/core/session-qrels.ts +360 -0
  150. package/src/core/shared-event-store.ts +1 -1
  151. package/src/core/sqlite-event-store.ts +35 -11
  152. package/src/core/task/blocker-resolver.ts +2 -2
  153. package/src/core/task/task-resolver.ts +0 -1
  154. package/src/core/vector-outbox.ts +1 -10
  155. package/src/core/vector-worker.ts +1 -1
  156. package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
  157. package/src/extensions/endless-memory/index.ts +1 -0
  158. package/src/extensions/index.ts +5 -0
  159. package/src/extensions/mcp/handlers.ts +960 -0
  160. package/src/extensions/mcp/index.ts +48 -0
  161. package/src/extensions/mcp/tools.ts +252 -0
  162. package/src/extensions/shared-memory/index.ts +1 -0
  163. package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
  164. package/src/extensions/vector/embedder.ts +197 -0
  165. package/src/extensions/vector/index.ts +1 -0
  166. package/src/hooks/post-tool-use.ts +3 -236
  167. package/src/hooks/semantic-daemon-client.ts +1 -208
  168. package/src/hooks/semantic-daemon.ts +6 -271
  169. package/src/hooks/session-end.ts +4 -79
  170. package/src/hooks/session-start.ts +4 -73
  171. package/src/hooks/stop.ts +3 -173
  172. package/src/hooks/user-prompt-submit.ts +3 -338
  173. package/src/index.ts +13 -0
  174. package/src/mcp/handlers.ts +2 -212
  175. package/src/mcp/index.ts +3 -46
  176. package/src/mcp/tools.ts +2 -78
  177. package/src/server/api/chat.ts +2 -244
  178. package/src/server/api/citations.ts +2 -105
  179. package/src/server/api/events.ts +2 -137
  180. package/src/server/api/health.ts +2 -53
  181. package/src/server/api/index.ts +2 -26
  182. package/src/server/api/projects.ts +2 -74
  183. package/src/server/api/search.ts +2 -102
  184. package/src/server/api/sessions.ts +2 -115
  185. package/src/server/api/stats.ts +2 -724
  186. package/src/server/api/turns.ts +2 -143
  187. package/src/server/api/utils.ts +2 -46
  188. package/src/server/index.ts +2 -100
  189. package/src/services/bootstrap-organizer.ts +46 -26
  190. package/src/services/codex-session-history-importer.ts +521 -29
  191. package/src/services/hermes-session-history-importer.ts +733 -0
  192. package/src/services/memory-service-config.ts +36 -0
  193. package/src/services/memory-service-registry.ts +150 -0
  194. package/src/services/memory-service.ts +211 -1325
  195. package/src/services/session-history-importer.ts +58 -14
  196. package/tests/README.md +23 -0
  197. package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
  198. package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
  199. package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
  200. package/tests/apps/app-layer-boundary.test.ts +48 -0
  201. package/tests/apps/claude-settings-hooks.test.ts +107 -0
  202. package/tests/apps/cli-disclosure-output.test.ts +212 -0
  203. package/tests/apps/codex-import-runner.test.ts +99 -0
  204. package/tests/apps/codex-validation-output.test.ts +100 -0
  205. package/tests/apps/hermes-import-runner.test.ts +99 -0
  206. package/tests/apps/mcp-install-command.test.ts +59 -0
  207. package/tests/apps/package-build-entrypoints.test.ts +30 -0
  208. package/tests/apps/search-api-disclosure.test.ts +162 -0
  209. package/tests/apps/stats-api-lightweight.test.ts +67 -0
  210. package/tests/apps/ui-disclosure-output.test.ts +140 -0
  211. package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
  212. package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
  213. package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
  214. package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
  215. package/tests/core/embedding-maintenance-service.test.ts +282 -0
  216. package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
  217. package/tests/core/external-market-context.test.ts +209 -0
  218. package/tests/core/fact-deriver.test.ts +79 -0
  219. package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
  220. package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
  221. package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
  222. package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
  223. package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
  224. package/tests/core/memory-engine-services.test.ts +240 -0
  225. package/tests/core/memory-ingest-service.test.ts +296 -0
  226. package/tests/core/memory-query-service.test.ts +129 -0
  227. package/tests/core/memory-runtime-service.test.ts +201 -0
  228. package/tests/core/memory-service-composition.test.ts +192 -0
  229. package/tests/core/memory-service-config.test.ts +41 -0
  230. package/tests/core/memory-service-facade.test.ts +30 -0
  231. package/tests/core/memory-service-registry.test.ts +206 -0
  232. package/tests/core/product-validation-matrix.test.ts +61 -0
  233. package/tests/core/project-registry.test.ts +78 -0
  234. package/tests/core/replay-evaluator.test.ts +181 -0
  235. package/tests/core/retrieval-analytics-service.test.ts +210 -0
  236. package/tests/core/retrieval-benchmark.test.ts +93 -0
  237. package/tests/core/retrieval-disclosure-service.test.ts +264 -0
  238. package/tests/core/retrieval-orchestrator.test.ts +403 -0
  239. package/tests/core/retrieval-quality.test.ts +31 -0
  240. package/tests/core/retrieval-services.test.ts +185 -0
  241. package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
  242. package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
  243. package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
  244. package/tests/core/session-history-importer-filter.test.ts +78 -0
  245. package/tests/core/session-qrels.test.ts +250 -0
  246. package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
  247. package/tests/core/summary-deriver.test.ts +66 -0
  248. package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
  249. package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
  250. package/tests/extensions/endless-memory-services.test.ts +325 -0
  251. package/tests/extensions/mcp-context-tools.test.ts +905 -0
  252. package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
  253. package/tests/extensions/mcp-package-build.test.ts +22 -0
  254. package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
  255. package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
  256. package/tests/extensions/shared-memory-services.test.ts +309 -0
  257. package/tests/extensions/vector-extension-boundary.test.ts +21 -0
  258. package/.claude/settings.local.json +0 -25
  259. package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
  260. package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
  261. package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
  262. package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
  263. package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
  264. package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
  265. package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
  266. package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
  267. package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
  268. package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
  269. package/.npm-cache/_update-notifier-last-checked +0 -0
  270. package/bootstrap-kb/decisions/decisions.md +0 -244
  271. package/bootstrap-kb/glossary/glossary.md +0 -46
  272. package/bootstrap-kb/modules/.claude-plugin.md +0 -22
  273. package/bootstrap-kb/modules/agents.md.md +0 -15
  274. package/bootstrap-kb/modules/claude.md.md +0 -15
  275. package/bootstrap-kb/modules/context.md.md +0 -15
  276. package/bootstrap-kb/modules/docs.md +0 -18
  277. package/bootstrap-kb/modules/handoff.md.md +0 -15
  278. package/bootstrap-kb/modules/package-lock.json.md +0 -15
  279. package/bootstrap-kb/modules/package.json.md +0 -15
  280. package/bootstrap-kb/modules/plan.md.md +0 -15
  281. package/bootstrap-kb/modules/readme.md.md +0 -15
  282. package/bootstrap-kb/modules/scripts.md +0 -26
  283. package/bootstrap-kb/modules/spec.md.md +0 -15
  284. package/bootstrap-kb/modules/specs.md +0 -20
  285. package/bootstrap-kb/modules/src.md +0 -51
  286. package/bootstrap-kb/modules/tests.md +0 -42
  287. package/bootstrap-kb/modules/tsconfig.json.md +0 -15
  288. package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
  289. package/bootstrap-kb/overview/overview.md +0 -40
  290. package/bootstrap-kb/sources/manifest.json +0 -950
  291. package/bootstrap-kb/sources/manifest.md +0 -227
  292. package/bootstrap-kb/timeline/timeline.md +0 -57
  293. package/claude-memory-layer-1.0.14.tgz +0 -0
  294. package/d.sh +0 -3
  295. package/deploy.sh +0 -3
  296. package/dist/ui/app.js +0 -2101
  297. package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
  298. package/memory/_index.md +0 -418
  299. package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
  300. package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
  301. package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
  302. package/memory/agent_response/uncategorized/2026-03-05.md +0 -48
  303. package/memory/default/uncategorized/2026-02-25.md +0 -4839
  304. package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
  305. package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
  306. package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
  307. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
  308. package/memory/specs/citations-system/2026-02-25.md +0 -1121
  309. package/memory/specs/endless-mode/2026-02-25.md +0 -1392
  310. package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
  311. package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
  312. package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
  313. package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
  314. package/memory/specs/private-tags/2026-02-25.md +0 -1057
  315. package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
  316. package/memory/specs/task-entity-system/2026-02-25.md +0 -924
  317. package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
  318. package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
  319. package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
  320. package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
  321. package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
  322. package/memory/tool_observation/uncategorized/2026-03-05.md +0 -29
  323. package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
  324. package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
  325. package/specs/optional-duckdb/context.md +0 -77
  326. package/specs/optional-duckdb/plan.md +0 -142
  327. package/specs/optional-duckdb/spec.md +0 -35
  328. package/src/ui/app.js +0 -2101
@@ -0,0 +1,282 @@
1
+ import * as path from 'path';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import {
5
+ createEmbeddingMaintenanceService,
6
+ type EmbeddingMaintenanceFileSystem
7
+ } from '../../src/core/engine/embedding-maintenance-service.js';
8
+
9
+ function event(id: string, content = `content for ${id}`) {
10
+ return { id, content };
11
+ }
12
+
13
+ function createHarness(options?: {
14
+ currentModel?: string;
15
+ vectorCount?: number;
16
+ files?: Record<string, string>;
17
+ workerRunning?: boolean;
18
+ nullWorker?: boolean;
19
+ events?: Array<{ id: string; content: string }>;
20
+ }) {
21
+ const storagePath = '/memory-root/project-a';
22
+ const metaPath = path.join(storagePath, 'embedding-meta.json');
23
+ const files = new Map<string, string>(Object.entries(options?.files ?? {}));
24
+ const calls: string[] = [];
25
+
26
+ const fileSystem: EmbeddingMaintenanceFileSystem = {
27
+ existsSync: vi.fn((targetPath: string) => files.has(targetPath)),
28
+ readFileSync: vi.fn((targetPath: string) => files.get(targetPath) ?? ''),
29
+ writeFileSync: vi.fn((targetPath: string, content: string) => {
30
+ files.set(targetPath, content);
31
+ })
32
+ };
33
+
34
+ const initialize = vi.fn(async () => {
35
+ calls.push('initialize');
36
+ });
37
+ const vectorStore = {
38
+ count: vi.fn(async () => options?.vectorCount ?? 0),
39
+ clearAll: vi.fn(async () => {
40
+ calls.push('clear-vectors');
41
+ })
42
+ };
43
+ const events = options?.events ?? [];
44
+ const eventStore = {
45
+ clearEmbeddingOutbox: vi.fn(async () => {
46
+ calls.push('clear-outbox');
47
+ }),
48
+ getEventsPage: vi.fn(async (limit: number, offset: number) => events.slice(offset, offset + limit)),
49
+ enqueueForEmbedding: vi.fn(async (eventId: string, _content: string) => {
50
+ calls.push(`enqueue:${eventId}`);
51
+ })
52
+ };
53
+ const worker = {
54
+ isRunning: vi.fn(() => options?.workerRunning ?? false),
55
+ stop: vi.fn(() => {
56
+ calls.push('stop-worker');
57
+ }),
58
+ start: vi.fn(() => {
59
+ calls.push('start-worker');
60
+ })
61
+ };
62
+
63
+ const service = createEmbeddingMaintenanceService({
64
+ storagePath,
65
+ initialize,
66
+ getEmbeddingModelName: vi.fn(() => options?.currentModel ?? 'embedding-model-a'),
67
+ vectorStore,
68
+ eventStore,
69
+ getVectorWorker: () => (options?.nullWorker ? null : worker),
70
+ fileSystem
71
+ });
72
+
73
+ return {
74
+ service,
75
+ storagePath,
76
+ metaPath,
77
+ files,
78
+ calls,
79
+ fileSystem,
80
+ initialize,
81
+ vectorStore,
82
+ eventStore,
83
+ worker
84
+ };
85
+ }
86
+
87
+ describe('EmbeddingMaintenanceService', () => {
88
+ it('initializes embedding metadata when no prior vectors exist', async () => {
89
+ const h = createHarness({ vectorCount: 0 });
90
+
91
+ const result = await h.service.ensureEmbeddingModelForImport();
92
+
93
+ expect(result).toEqual({
94
+ changed: false,
95
+ previousModel: null,
96
+ currentModel: 'embedding-model-a',
97
+ enqueued: 0,
98
+ reason: 'initialized-meta'
99
+ });
100
+ expect(h.initialize).toHaveBeenCalledTimes(1);
101
+ expect(h.vectorStore.count).toHaveBeenCalledTimes(1);
102
+ expect(h.vectorStore.clearAll).not.toHaveBeenCalled();
103
+ expect(h.eventStore.clearEmbeddingOutbox).not.toHaveBeenCalled();
104
+ expect(JSON.parse(h.files.get(h.metaPath)!)).toMatchObject({
105
+ model: 'embedding-model-a',
106
+ updatedAt: expect.any(String)
107
+ });
108
+ });
109
+
110
+ it('returns unchanged when stored metadata already matches the current model', async () => {
111
+ const metaPath = path.join('/memory-root/project-a', 'embedding-meta.json');
112
+ const h = createHarness({
113
+ vectorCount: 5,
114
+ files: {
115
+ [metaPath]: JSON.stringify({ model: 'embedding-model-a', updatedAt: '2026-05-02T00:00:00.000Z' })
116
+ }
117
+ });
118
+
119
+ const result = await h.service.ensureEmbeddingModelForImport();
120
+
121
+ expect(result).toEqual({
122
+ changed: false,
123
+ previousModel: 'embedding-model-a',
124
+ currentModel: 'embedding-model-a',
125
+ enqueued: 0
126
+ });
127
+ expect(h.fileSystem.writeFileSync).not.toHaveBeenCalled();
128
+ expect(h.vectorStore.clearAll).not.toHaveBeenCalled();
129
+ expect(h.eventStore.clearEmbeddingOutbox).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('reports a dry-run model mismatch without clearing vectors or outbox state', async () => {
133
+ const metaPath = path.join('/memory-root/project-a', 'embedding-meta.json');
134
+ const h = createHarness({
135
+ currentModel: 'embedding-model-b',
136
+ vectorCount: 2,
137
+ files: {
138
+ [metaPath]: JSON.stringify({ model: 'embedding-model-a' })
139
+ }
140
+ });
141
+
142
+ const result = await h.service.ensureEmbeddingModelForImport({ autoMigrate: false });
143
+
144
+ expect(result).toEqual({
145
+ changed: true,
146
+ previousModel: 'embedding-model-a',
147
+ currentModel: 'embedding-model-b',
148
+ enqueued: 0,
149
+ reason: 'model-mismatch'
150
+ });
151
+ expect(h.vectorStore.clearAll).not.toHaveBeenCalled();
152
+ expect(h.eventStore.clearEmbeddingOutbox).not.toHaveBeenCalled();
153
+ expect(h.eventStore.enqueueForEmbedding).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it('reports legacy vectors without metadata as a dry-run migration requirement', async () => {
157
+ const h = createHarness({ vectorCount: 3 });
158
+
159
+ const result = await h.service.ensureEmbeddingModelForImport({ autoMigrate: false });
160
+
161
+ expect(result).toEqual({
162
+ changed: true,
163
+ previousModel: null,
164
+ currentModel: 'embedding-model-a',
165
+ enqueued: 0,
166
+ reason: 'legacy-vectors-without-meta'
167
+ });
168
+ expect(h.fileSystem.writeFileSync).not.toHaveBeenCalled();
169
+ expect(h.vectorStore.clearAll).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('migrates vectors by clearing indexes, re-enqueueing events, and restarting a running worker', async () => {
173
+ const metaPath = path.join('/memory-root/project-a', 'embedding-meta.json');
174
+ const h = createHarness({
175
+ currentModel: 'embedding-model-b',
176
+ vectorCount: 10,
177
+ workerRunning: true,
178
+ files: {
179
+ [metaPath]: JSON.stringify({ model: 'embedding-model-a' })
180
+ },
181
+ events: [event('e1', 'first memory'), event('e2', 'second memory')]
182
+ });
183
+
184
+ const result = await h.service.ensureEmbeddingModelForImport();
185
+
186
+ expect(result).toEqual({
187
+ changed: true,
188
+ previousModel: 'embedding-model-a',
189
+ currentModel: 'embedding-model-b',
190
+ enqueued: 2,
191
+ reason: 'model-mismatch'
192
+ });
193
+ expect(h.calls).toEqual([
194
+ 'initialize',
195
+ 'stop-worker',
196
+ 'clear-vectors',
197
+ 'clear-outbox',
198
+ 'enqueue:e1',
199
+ 'enqueue:e2',
200
+ 'start-worker'
201
+ ]);
202
+ expect(h.eventStore.getEventsPage).toHaveBeenCalledWith(1000, 0);
203
+ expect(h.eventStore.enqueueForEmbedding).toHaveBeenNthCalledWith(1, 'e1', 'first memory');
204
+ expect(h.eventStore.enqueueForEmbedding).toHaveBeenNthCalledWith(2, 'e2', 'second memory');
205
+ expect(JSON.parse(h.files.get(h.metaPath)!)).toMatchObject({
206
+ model: 'embedding-model-b',
207
+ previousModel: 'embedding-model-a',
208
+ migratedAt: expect.any(String),
209
+ enqueued: 2
210
+ });
211
+ });
212
+
213
+ it('auto-migrates legacy vectors without metadata and does not restart an idle worker', async () => {
214
+ const h = createHarness({
215
+ vectorCount: 3,
216
+ workerRunning: false,
217
+ events: [event('legacy-1', 'legacy memory')]
218
+ });
219
+
220
+ const result = await h.service.ensureEmbeddingModelForImport();
221
+
222
+ expect(result).toEqual({
223
+ changed: true,
224
+ previousModel: null,
225
+ currentModel: 'embedding-model-a',
226
+ enqueued: 1,
227
+ reason: 'legacy-vectors-without-meta'
228
+ });
229
+ expect(h.worker.stop).not.toHaveBeenCalled();
230
+ expect(h.worker.start).not.toHaveBeenCalled();
231
+ expect(h.calls).toEqual(['initialize', 'clear-vectors', 'clear-outbox', 'enqueue:legacy-1']);
232
+ expect(JSON.parse(h.files.get(h.metaPath)!)).toMatchObject({
233
+ model: 'embedding-model-a',
234
+ previousModel: null,
235
+ migratedAt: expect.any(String),
236
+ enqueued: 1
237
+ });
238
+ });
239
+
240
+ it('re-enqueues events across page boundaries during migration', async () => {
241
+ const metaPath = path.join('/memory-root/project-a', 'embedding-meta.json');
242
+ const events = Array.from({ length: 1001 }, (_, index) => event(`event-${index + 1}`));
243
+ const h = createHarness({
244
+ currentModel: 'embedding-model-b',
245
+ vectorCount: 1001,
246
+ files: {
247
+ [metaPath]: JSON.stringify({ model: 'embedding-model-a' })
248
+ },
249
+ events
250
+ });
251
+
252
+ const result = await h.service.ensureEmbeddingModelForImport();
253
+
254
+ expect(result.enqueued).toBe(1001);
255
+ expect(h.eventStore.getEventsPage).toHaveBeenNthCalledWith(1, 1000, 0);
256
+ expect(h.eventStore.getEventsPage).toHaveBeenNthCalledWith(2, 1000, 1000);
257
+ expect(h.eventStore.getEventsPage).toHaveBeenCalledTimes(2);
258
+ expect(h.eventStore.enqueueForEmbedding).toHaveBeenCalledTimes(1001);
259
+ expect(h.eventStore.enqueueForEmbedding).toHaveBeenNthCalledWith(1001, 'event-1001', 'content for event-1001');
260
+ });
261
+
262
+ it('migrates without worker lifecycle calls when no vector worker is available', async () => {
263
+ const metaPath = path.join('/memory-root/project-a', 'embedding-meta.json');
264
+ const h = createHarness({
265
+ currentModel: 'embedding-model-b',
266
+ vectorCount: 1,
267
+ nullWorker: true,
268
+ files: {
269
+ [metaPath]: JSON.stringify({ model: 'embedding-model-a' })
270
+ },
271
+ events: [event('e1')]
272
+ });
273
+
274
+ const result = await h.service.ensureEmbeddingModelForImport();
275
+
276
+ expect(result.enqueued).toBe(1);
277
+ expect(h.calls).toEqual(['initialize', 'clear-vectors', 'clear-outbox', 'enqueue:e1']);
278
+ expect(h.worker.isRunning).not.toHaveBeenCalled();
279
+ expect(h.worker.stop).not.toHaveBeenCalled();
280
+ expect(h.worker.start).not.toHaveBeenCalled();
281
+ });
282
+ });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, it, expect } from 'vitest';
6
- import { EvidenceAligner } from '../src/core/evidence-aligner.js';
6
+ import { EvidenceAligner } from '../../src/core/evidence-aligner.js';
7
7
 
8
8
  describe('EvidenceAligner', () => {
9
9
  const aligner = new EvidenceAligner();
@@ -0,0 +1,209 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ fetchExternalMarketContext,
5
+ renderExternalMarketContextReport
6
+ } from '../../src/core/external-market-context.js';
7
+
8
+ const originalEnv = process.env;
9
+
10
+ function dartList(filings: Array<Record<string, string>>) {
11
+ return { status: '000', message: '정상', list: filings, page_count: filings.length, total_count: filings.length };
12
+ }
13
+
14
+ function fredObservations(value: string) {
15
+ return { observations: [{ date: '2026-05-01', value }] };
16
+ }
17
+
18
+ describe('external market context', () => {
19
+ beforeEach(() => {
20
+ vi.restoreAllMocks();
21
+ process.env = {
22
+ ...originalEnv,
23
+ DART_API_KEY: 'dk',
24
+ FRED_API_KEY: 'fk',
25
+ FINNHUB_API_KEY: 'hk'
26
+ };
27
+ });
28
+
29
+ afterEach(() => {
30
+ process.env = originalEnv;
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ it('fails closed for invalid explicit core providers before network fetch', async () => {
35
+ globalThis.fetch = vi.fn(async () => {
36
+ throw new Error('fetch should not be called for invalid providers');
37
+ }) as never;
38
+
39
+ await expect(fetchExternalMarketContext({ providers: ['bogus'] as never })).rejects.toThrow('Invalid providers');
40
+ expect(globalThis.fetch).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('skips missing provider keys without external fetches', async () => {
44
+ process.env = {
45
+ ...originalEnv,
46
+ DART_API_KEY: '',
47
+ FRED_API_KEY: '',
48
+ FINNHUB_API_KEY: ''
49
+ };
50
+ globalThis.fetch = vi.fn(async () => {
51
+ throw new Error('fetch should not be called when all provider keys are missing');
52
+ }) as never;
53
+
54
+ const report = await fetchExternalMarketContext({
55
+ company: '삼성전자',
56
+ dartCorpCode: '00126380',
57
+ symbol: '005930.KS',
58
+ providers: ['dart', 'fred', 'finnhub']
59
+ });
60
+
61
+ expect(report.dart).toMatchObject({ status: 'skipped', warnings: ['DART_API_KEY is not set'] });
62
+ expect(report.fred).toMatchObject({ status: 'skipped', warnings: ['FRED_API_KEY is not set'] });
63
+ expect(report.finnhub).toMatchObject({ status: 'skipped', warnings: ['FINNHUB_API_KEY is not set'] });
64
+ expect(globalThis.fetch).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('treats DART status 013 as an empty successful filing result', async () => {
68
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({ status: '013', message: '조회된 데이타가 없습니다.' }), { status: 200 })) as never;
69
+
70
+ const report = await fetchExternalMarketContext({
71
+ company: '삼성전자',
72
+ dartCorpCode: '00126380',
73
+ providers: ['dart'],
74
+ now: new Date('2026-05-06T00:00:00Z')
75
+ });
76
+
77
+ expect(report.dart).toMatchObject({ status: 'ok', filings: [], displayedFilings: [] });
78
+ expect(report.analysis?.marketSnapshot?.coverage.dart).toMatchObject({
79
+ status: 'ok',
80
+ filingsAnalyzed: 0,
81
+ renderedFilings: 0,
82
+ confidence: 'exact-corp-code'
83
+ });
84
+ });
85
+
86
+ it('adds abort signals to provider requests and caps large FRED series lists', async () => {
87
+ const fetchSignals: Array<AbortSignal | null | undefined> = [];
88
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
89
+ fetchSignals.push(init?.signal);
90
+ const url = String(input);
91
+ if (url.includes('opendart.fss.or.kr')) return new Response(JSON.stringify(dartList([])), { status: 200 });
92
+ if (url.includes('stlouisfed.org')) return new Response(JSON.stringify(fredObservations('5.25')), { status: 200 });
93
+ if (url.includes('finnhub.io')) return new Response(JSON.stringify({ name: 'Samsung Electronics', ticker: '005930.KS' }), { status: 200 });
94
+ throw new Error(`unexpected URL ${url}`);
95
+ }) as never;
96
+ const fredSeries = Array.from({ length: 15 }, (_, index) => `SERIES${index}`);
97
+
98
+ const report = await fetchExternalMarketContext({
99
+ company: '삼성전자',
100
+ dartCorpCode: '00126380',
101
+ symbol: '005930.KS',
102
+ providers: ['dart', 'fred', 'finnhub'],
103
+ fredSeries
104
+ });
105
+
106
+ expect(report.query.fredSeries).toHaveLength(10);
107
+ expect(report.fred?.series).toHaveLength(10);
108
+ expect(report.fred?.warnings?.some((warning) => warning.includes('truncated to 10'))).toBe(true);
109
+ expect(globalThis.fetch).toHaveBeenCalledTimes(12);
110
+ expect(fetchSignals.every((signal) => signal instanceof AbortSignal)).toBe(true);
111
+ });
112
+
113
+ it('treats an empty Finnhub profile as skipped instead of emitting profile evidence', async () => {
114
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({}), { status: 200 })) as never;
115
+
116
+ const report = await fetchExternalMarketContext({ symbol: 'NOPE', providers: ['finnhub'] });
117
+ const snapshot = report.analysis?.marketSnapshot;
118
+
119
+ expect(report.finnhub).toMatchObject({ status: 'skipped', warnings: ['Finnhub returned no profile data'] });
120
+ expect(report.finnhub?.profile).toBeUndefined();
121
+ expect(snapshot?.coverage.finnhub).toMatchObject({ status: 'skipped', hasProfile: false });
122
+ expect(snapshot?.catalysts.some((item) => item.evidence.some((evidence) => evidence.provider === 'finnhub'))).toBe(false);
123
+ });
124
+
125
+ it('builds a structured multi-provider MarketContextSnapshot with bull, bear, risk, and catalyst evidence', async () => {
126
+ const filings = [
127
+ { corp_name: '삼성전자', rcept_no: '20260501000001', report_nm: '단일판매ㆍ공급계약체결', flr_nm: '삼성전자', rcept_dt: '20260501', rm: '' },
128
+ { corp_name: '삼성전자', rcept_no: '20260502000002', report_nm: '영업(잠정)실적(공정공시)', flr_nm: '삼성전자', rcept_dt: '20260502', rm: '' },
129
+ { corp_name: '삼성전자', rcept_no: '20260503000003', report_nm: '소송등의제기ㆍ신청(경영권분쟁소송)', flr_nm: '삼성전자', rcept_dt: '20260503', rm: '' },
130
+ { corp_name: '삼성전자', rcept_no: '20260504000004', report_nm: '유상증자결정', flr_nm: '삼성전자', rcept_dt: '20260504', rm: '' }
131
+ ];
132
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
133
+ const url = String(input);
134
+ if (url.includes('opendart.fss.or.kr')) return new Response(JSON.stringify(dartList(filings)), { status: 200 });
135
+ if (url.includes('stlouisfed.org')) return new Response(JSON.stringify(fredObservations('5.25')), { status: 200 });
136
+ if (url.includes('finnhub.io')) return new Response(JSON.stringify({ name: 'Samsung Electronics', ticker: '005930.KS', exchange: 'KRX', marketCapitalization: 450000, finnhubIndustry: 'Technology' }), { status: 200 });
137
+ throw new Error(`unexpected URL ${url}`);
138
+ }) as never;
139
+
140
+ const report = await fetchExternalMarketContext({
141
+ company: '삼성전자',
142
+ dartCorpCode: '00126380',
143
+ symbol: '005930.KS',
144
+ providers: ['dart', 'fred', 'finnhub'],
145
+ fredSeries: ['FEDFUNDS'],
146
+ now: new Date('2026-05-06T00:00:00Z')
147
+ });
148
+
149
+ const snapshot = report.analysis?.marketSnapshot;
150
+ expect(snapshot).toMatchObject({
151
+ schemaVersion: 'market-context-snapshot.v1',
152
+ subject: { company: '삼성전자', dartCorpCode: '00126380', symbol: '005930.KS' },
153
+ coverage: {
154
+ dart: { status: 'ok', filingsAnalyzed: 4, renderedFilings: 4, confidence: 'exact-corp-code' },
155
+ fred: { status: 'ok', seriesAnalyzed: 1 },
156
+ finnhub: { status: 'ok', hasProfile: true }
157
+ }
158
+ });
159
+ expect(snapshot?.bullCases.some((item) => item.evidence.some((evidence) => evidence.provider === 'dart' && evidence.receiptNo === '20260502000002'))).toBe(true);
160
+ expect(snapshot?.bearCases.some((item) => item.evidence.some((evidence) => evidence.provider === 'dart' && evidence.receiptNo === '20260504000004'))).toBe(true);
161
+ expect(snapshot?.risks.some((item) => item.evidence.some((evidence) => evidence.provider === 'dart' && evidence.receiptNo === '20260503000003'))).toBe(true);
162
+ expect(snapshot?.catalysts.some((item) => item.evidence.some((evidence) => evidence.provider === 'dart' && evidence.receiptNo === '20260501000001'))).toBe(true);
163
+ expect(snapshot?.risks.some((item) => item.evidence.some((evidence) => evidence.provider === 'fred' && evidence.seriesId === 'FEDFUNDS'))).toBe(true);
164
+ expect(snapshot?.bullCases.some((item) => item.evidence.some((evidence) => evidence.provider === 'finnhub' && evidence.symbol === '005930.KS'))).toBe(true);
165
+ });
166
+
167
+ it('renders the structured MarketContextSnapshot analysis report with provider evidence', async () => {
168
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
169
+ const url = String(input);
170
+ if (url.includes('opendart.fss.or.kr')) return new Response(JSON.stringify(dartList([{ corp_name: '삼성전자', rcept_no: '20260501000001', report_nm: '단일판매ㆍ공급계약체결', flr_nm: '삼성전자', rcept_dt: '20260501', rm: '' }])), { status: 200 });
171
+ if (url.includes('stlouisfed.org')) return new Response(JSON.stringify(fredObservations('5.25')), { status: 200 });
172
+ return new Response(JSON.stringify({ name: 'Samsung Electronics', ticker: '005930.KS', marketCapitalization: 450000 }), { status: 200 });
173
+ }) as never;
174
+
175
+ const report = await fetchExternalMarketContext({ company: '삼성전자', dartCorpCode: '00126380', symbol: '005930.KS', providers: ['dart', 'fred', 'finnhub'], fredSeries: ['FEDFUNDS'] });
176
+ const rendered = renderExternalMarketContextReport(report);
177
+
178
+ expect(rendered).toContain('### MarketContextSnapshot');
179
+ expect(rendered).toContain('**Bull case**');
180
+ expect(rendered).toContain('**Bear case**');
181
+ expect(rendered).toContain('**Risks**');
182
+ expect(rendered).toContain('**Catalysts**');
183
+ expect(rendered).toContain('DART: 단일판매ㆍ공급계약체결');
184
+ expect(rendered).toContain('FRED: FEDFUNDS');
185
+ expect(rendered).toContain('Finnhub: 005930.KS');
186
+ });
187
+
188
+ it('does not include analysis when includeSnapshot is false', async () => {
189
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify(dartList([])), { status: 200 })) as never;
190
+
191
+ const report = await fetchExternalMarketContext({ company: '삼성전자', dartCorpCode: '00126380', providers: ['dart'], includeSnapshot: false });
192
+
193
+ expect(report.analysis).toBeUndefined();
194
+ expect(renderExternalMarketContextReport(report)).not.toContain('MarketContextSnapshot');
195
+ });
196
+
197
+ it('redacts credential-bearing provider errors in JSON and Markdown', async () => {
198
+ globalThis.fetch = vi.fn(async () => {
199
+ throw new Error('failed URL https://opendart.fss.or.kr/api/list.json?crtfc_key=dk&corp_code=00126380');
200
+ }) as never;
201
+
202
+ const report = await fetchExternalMarketContext({ company: '삼성전자', dartCorpCode: '00126380', providers: ['dart'] });
203
+ const rendered = renderExternalMarketContextReport(report);
204
+
205
+ expect(rendered).toContain('[REDACTED]');
206
+ expect(rendered).not.toContain('dk');
207
+ expect(JSON.stringify(report)).not.toContain('dk');
208
+ });
209
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { FactDeriver, makeEventDerivedFactId } from '../../src/core/derive/fact-deriver.js';
4
+ import type { MemoryEvent } from '../../src/core/types.js';
5
+
6
+ function makeEvent(overrides: Partial<MemoryEvent>): MemoryEvent {
7
+ return {
8
+ id: '11111111-1111-4111-8111-111111111111',
9
+ eventType: 'user_prompt',
10
+ sessionId: 'session-1',
11
+ timestamp: new Date('2026-04-30T00:00:00.000Z'),
12
+ content: 'default content',
13
+ canonicalKey: 'default-content',
14
+ dedupeKey: 'session-1:default-content',
15
+ ...overrides
16
+ };
17
+ }
18
+
19
+ describe('FactDeriver', () => {
20
+ it('derives a deterministic fact from a user prompt', () => {
21
+ const deriver = new FactDeriver();
22
+ const event = makeEvent({
23
+ content: 'We decided to keep SQLite as the canonical store.',
24
+ metadata: {
25
+ scope: { project: { hash: 'proj123' } },
26
+ tags: ['proj:proj123', 'architecture']
27
+ }
28
+ });
29
+
30
+ const facts = deriver.deriveFromEvent(event, { now: new Date('2026-04-30T01:02:03.000Z') });
31
+
32
+ expect(facts).toHaveLength(1);
33
+ expect(facts[0]).toMatchObject({
34
+ factId: makeEventDerivedFactId(event.id, 'decision'),
35
+ projectHash: 'proj123',
36
+ factType: 'decision',
37
+ text: 'User asked: We decided to keep SQLite as the canonical store.',
38
+ derivedFromEventIds: [event.id],
39
+ sourceKind: 'prompt',
40
+ confidence: 0.65,
41
+ importance: 0.5,
42
+ tags: ['proj:proj123', 'architecture'],
43
+ createdAt: '2026-04-30T01:02:03.000Z',
44
+ updatedAt: '2026-04-30T01:02:03.000Z'
45
+ });
46
+ });
47
+
48
+ it('derives tool observation facts with tool metadata', () => {
49
+ const deriver = new FactDeriver();
50
+ const event = makeEvent({
51
+ eventType: 'tool_observation',
52
+ content: '{"toolName":"terminal","success":false}',
53
+ metadata: {
54
+ toolName: 'terminal',
55
+ success: false,
56
+ fileRefs: ['src/services/memory-service.ts']
57
+ }
58
+ });
59
+
60
+ const [fact] = deriver.deriveFromEvent(event, {
61
+ projectHash: 'fallback-project',
62
+ now: new Date('2026-04-30T01:02:03.000Z')
63
+ });
64
+
65
+ expect(fact).toMatchObject({
66
+ factType: 'tool_observation',
67
+ projectHash: 'fallback-project',
68
+ sourceKind: 'tool',
69
+ text: 'Tool terminal failed: {"toolName":"terminal","success":false}',
70
+ fileRefs: ['src/services/memory-service.ts']
71
+ });
72
+ });
73
+
74
+ it('skips empty event content', () => {
75
+ const deriver = new FactDeriver();
76
+ const facts = deriver.deriveFromEvent(makeEvent({ content: ' ' }));
77
+ expect(facts).toEqual([]);
78
+ });
79
+ });