bikky 0.3.13 → 0.4.1

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 (362) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CODE_OF_CONDUCT.md +80 -0
  3. package/CONTRIBUTING.md +206 -0
  4. package/README.md +94 -20
  5. package/SECURITY.md +58 -0
  6. package/SUPPORT.md +22 -0
  7. package/dist/config.d.ts +68 -1
  8. package/dist/config.js +197 -4
  9. package/dist/daemon/extraction.d.ts +12 -2
  10. package/dist/daemon/extraction.js +85 -133
  11. package/dist/daemon/loop.js +15 -1
  12. package/dist/daemon/qdrant.js +0 -1
  13. package/dist/daemon/transcript-sources.d.ts +26 -0
  14. package/dist/daemon/transcript-sources.js +193 -0
  15. package/dist/daemon/watcher.d.ts +3 -2
  16. package/dist/daemon/watcher.js +51 -2
  17. package/dist/install.d.ts +9 -1
  18. package/dist/install.js +62 -34
  19. package/dist/lib/qdrant-pool.d.ts +57 -0
  20. package/dist/lib/qdrant-pool.js +104 -0
  21. package/dist/mcp/api.d.ts +57 -19
  22. package/dist/mcp/api.js +134 -72
  23. package/dist/mcp/helpers.d.ts +0 -1
  24. package/dist/mcp/helpers.js +2 -15
  25. package/dist/mcp/index.js +29 -14
  26. package/dist/mcp/tools.d.ts +0 -7
  27. package/dist/mcp/tools.js +618 -276
  28. package/dist/mcp/types.d.ts +0 -3
  29. package/dist/routing.d.ts +53 -0
  30. package/dist/routing.js +129 -0
  31. package/dist/search-scope.d.ts +24 -0
  32. package/dist/search-scope.js +174 -0
  33. package/docs/config/fully-hosted.md +57 -0
  34. package/docs/config/hosted-models.md +50 -0
  35. package/docs/config/hosted-qdrant-local-models.md +39 -0
  36. package/docs/config/local.md +34 -0
  37. package/docs/configuration.md +403 -0
  38. package/docs/privacy-first.md +140 -0
  39. package/docs/screenshots/dashboard.png +0 -0
  40. package/docs/screenshots/graph.png +0 -0
  41. package/docs/screenshots/memory.png +0 -0
  42. package/package.json +28 -7
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js.map +0 -1
  45. package/dist/config.d.ts.map +0 -1
  46. package/dist/config.js.map +0 -1
  47. package/dist/config.test.d.ts +0 -9
  48. package/dist/config.test.d.ts.map +0 -1
  49. package/dist/config.test.js +0 -576
  50. package/dist/config.test.js.map +0 -1
  51. package/dist/daemon/capture-policy.d.ts.map +0 -1
  52. package/dist/daemon/capture-policy.js.map +0 -1
  53. package/dist/daemon/capture-policy.test.d.ts +0 -2
  54. package/dist/daemon/capture-policy.test.d.ts.map +0 -1
  55. package/dist/daemon/capture-policy.test.js +0 -48
  56. package/dist/daemon/capture-policy.test.js.map +0 -1
  57. package/dist/daemon/consolidation.d.ts.map +0 -1
  58. package/dist/daemon/consolidation.js.map +0 -1
  59. package/dist/daemon/entity-typing.d.ts.map +0 -1
  60. package/dist/daemon/entity-typing.js.map +0 -1
  61. package/dist/daemon/entity-typing.test.d.ts +0 -2
  62. package/dist/daemon/entity-typing.test.d.ts.map +0 -1
  63. package/dist/daemon/entity-typing.test.js +0 -50
  64. package/dist/daemon/entity-typing.test.js.map +0 -1
  65. package/dist/daemon/episode-summary.d.ts.map +0 -1
  66. package/dist/daemon/episode-summary.js.map +0 -1
  67. package/dist/daemon/episode-summary.test.d.ts +0 -2
  68. package/dist/daemon/episode-summary.test.d.ts.map +0 -1
  69. package/dist/daemon/episode-summary.test.js +0 -104
  70. package/dist/daemon/episode-summary.test.js.map +0 -1
  71. package/dist/daemon/extraction-quality.test.d.ts +0 -2
  72. package/dist/daemon/extraction-quality.test.d.ts.map +0 -1
  73. package/dist/daemon/extraction-quality.test.js +0 -283
  74. package/dist/daemon/extraction-quality.test.js.map +0 -1
  75. package/dist/daemon/extraction-rules.d.ts.map +0 -1
  76. package/dist/daemon/extraction-rules.js.map +0 -1
  77. package/dist/daemon/extraction-rules.test.d.ts +0 -2
  78. package/dist/daemon/extraction-rules.test.d.ts.map +0 -1
  79. package/dist/daemon/extraction-rules.test.js +0 -203
  80. package/dist/daemon/extraction-rules.test.js.map +0 -1
  81. package/dist/daemon/extraction.d.ts.map +0 -1
  82. package/dist/daemon/extraction.js.map +0 -1
  83. package/dist/daemon/extraction.test.d.ts +0 -2
  84. package/dist/daemon/extraction.test.d.ts.map +0 -1
  85. package/dist/daemon/extraction.test.js +0 -225
  86. package/dist/daemon/extraction.test.js.map +0 -1
  87. package/dist/daemon/index.d.ts.map +0 -1
  88. package/dist/daemon/index.js.map +0 -1
  89. package/dist/daemon/loop.d.ts.map +0 -1
  90. package/dist/daemon/loop.js.map +0 -1
  91. package/dist/daemon/loop.test.d.ts +0 -2
  92. package/dist/daemon/loop.test.d.ts.map +0 -1
  93. package/dist/daemon/loop.test.js +0 -85
  94. package/dist/daemon/loop.test.js.map +0 -1
  95. package/dist/daemon/maintenance-state.d.ts.map +0 -1
  96. package/dist/daemon/maintenance-state.js.map +0 -1
  97. package/dist/daemon/maintenance-state.test.d.ts +0 -2
  98. package/dist/daemon/maintenance-state.test.d.ts.map +0 -1
  99. package/dist/daemon/maintenance-state.test.js +0 -56
  100. package/dist/daemon/maintenance-state.test.js.map +0 -1
  101. package/dist/daemon/qdrant.d.ts.map +0 -1
  102. package/dist/daemon/qdrant.js.map +0 -1
  103. package/dist/daemon/qdrant.test.d.ts +0 -8
  104. package/dist/daemon/qdrant.test.d.ts.map +0 -1
  105. package/dist/daemon/qdrant.test.js +0 -265
  106. package/dist/daemon/qdrant.test.js.map +0 -1
  107. package/dist/daemon/relations-vocab.d.ts.map +0 -1
  108. package/dist/daemon/relations-vocab.js.map +0 -1
  109. package/dist/daemon/relations-vocab.test.d.ts +0 -2
  110. package/dist/daemon/relations-vocab.test.d.ts.map +0 -1
  111. package/dist/daemon/relations-vocab.test.js +0 -69
  112. package/dist/daemon/relations-vocab.test.js.map +0 -1
  113. package/dist/daemon/relations.d.ts.map +0 -1
  114. package/dist/daemon/relations.js.map +0 -1
  115. package/dist/daemon/relations.test.d.ts +0 -2
  116. package/dist/daemon/relations.test.d.ts.map +0 -1
  117. package/dist/daemon/relations.test.js +0 -36
  118. package/dist/daemon/relations.test.js.map +0 -1
  119. package/dist/daemon/session-index.d.ts.map +0 -1
  120. package/dist/daemon/session-index.js.map +0 -1
  121. package/dist/daemon/session-index.test.d.ts +0 -2
  122. package/dist/daemon/session-index.test.d.ts.map +0 -1
  123. package/dist/daemon/session-index.test.js +0 -60
  124. package/dist/daemon/session-index.test.js.map +0 -1
  125. package/dist/daemon/session-summary.d.ts.map +0 -1
  126. package/dist/daemon/session-summary.js.map +0 -1
  127. package/dist/daemon/session-summary.test.d.ts +0 -2
  128. package/dist/daemon/session-summary.test.d.ts.map +0 -1
  129. package/dist/daemon/session-summary.test.js +0 -162
  130. package/dist/daemon/session-summary.test.js.map +0 -1
  131. package/dist/daemon/staleness.d.ts.map +0 -1
  132. package/dist/daemon/staleness.js.map +0 -1
  133. package/dist/daemon/staleness.test.d.ts +0 -7
  134. package/dist/daemon/staleness.test.d.ts.map +0 -1
  135. package/dist/daemon/staleness.test.js +0 -128
  136. package/dist/daemon/staleness.test.js.map +0 -1
  137. package/dist/daemon/watcher-health.d.ts.map +0 -1
  138. package/dist/daemon/watcher-health.js.map +0 -1
  139. package/dist/daemon/watcher-health.test.d.ts +0 -5
  140. package/dist/daemon/watcher-health.test.d.ts.map +0 -1
  141. package/dist/daemon/watcher-health.test.js +0 -119
  142. package/dist/daemon/watcher-health.test.js.map +0 -1
  143. package/dist/daemon/watcher.d.ts.map +0 -1
  144. package/dist/daemon/watcher.js.map +0 -1
  145. package/dist/daemon/watcher.test.d.ts +0 -9
  146. package/dist/daemon/watcher.test.d.ts.map +0 -1
  147. package/dist/daemon/watcher.test.js +0 -204
  148. package/dist/daemon/watcher.test.js.map +0 -1
  149. package/dist/daemon/workstream-resolver.d.ts.map +0 -1
  150. package/dist/daemon/workstream-resolver.js.map +0 -1
  151. package/dist/daemon/workstream-resolver.test.d.ts +0 -2
  152. package/dist/daemon/workstream-resolver.test.d.ts.map +0 -1
  153. package/dist/daemon/workstream-resolver.test.js +0 -128
  154. package/dist/daemon/workstream-resolver.test.js.map +0 -1
  155. package/dist/daemon/workstream-summary.d.ts.map +0 -1
  156. package/dist/daemon/workstream-summary.js.map +0 -1
  157. package/dist/daemon/workstream-summary.test.d.ts +0 -2
  158. package/dist/daemon/workstream-summary.test.d.ts.map +0 -1
  159. package/dist/daemon/workstream-summary.test.js +0 -89
  160. package/dist/daemon/workstream-summary.test.js.map +0 -1
  161. package/dist/install.d.ts.map +0 -1
  162. package/dist/install.js.map +0 -1
  163. package/dist/install.test.d.ts +0 -9
  164. package/dist/install.test.d.ts.map +0 -1
  165. package/dist/install.test.js +0 -126
  166. package/dist/install.test.js.map +0 -1
  167. package/dist/lib/qdrant-client.d.ts.map +0 -1
  168. package/dist/lib/qdrant-client.js.map +0 -1
  169. package/dist/lib/qdrant-client.test.d.ts +0 -8
  170. package/dist/lib/qdrant-client.test.d.ts.map +0 -1
  171. package/dist/lib/qdrant-client.test.js +0 -274
  172. package/dist/lib/qdrant-client.test.js.map +0 -1
  173. package/dist/lifecycle.d.ts.map +0 -1
  174. package/dist/lifecycle.js.map +0 -1
  175. package/dist/lifecycle.test.d.ts +0 -8
  176. package/dist/lifecycle.test.d.ts.map +0 -1
  177. package/dist/lifecycle.test.js +0 -74
  178. package/dist/lifecycle.test.js.map +0 -1
  179. package/dist/llm/embedding/index.d.ts.map +0 -1
  180. package/dist/llm/embedding/index.js.map +0 -1
  181. package/dist/llm/embedding/index.test.d.ts +0 -8
  182. package/dist/llm/embedding/index.test.d.ts.map +0 -1
  183. package/dist/llm/embedding/index.test.js +0 -100
  184. package/dist/llm/embedding/index.test.js.map +0 -1
  185. package/dist/llm/embedding/providers/bedrock.d.ts.map +0 -1
  186. package/dist/llm/embedding/providers/bedrock.js.map +0 -1
  187. package/dist/llm/embedding/providers/bedrock.test.d.ts +0 -2
  188. package/dist/llm/embedding/providers/bedrock.test.d.ts.map +0 -1
  189. package/dist/llm/embedding/providers/bedrock.test.js +0 -24
  190. package/dist/llm/embedding/providers/bedrock.test.js.map +0 -1
  191. package/dist/llm/embedding/providers/index.d.ts.map +0 -1
  192. package/dist/llm/embedding/providers/index.js.map +0 -1
  193. package/dist/llm/embedding/providers/ollama.d.ts.map +0 -1
  194. package/dist/llm/embedding/providers/ollama.js.map +0 -1
  195. package/dist/llm/embedding/providers/ollama.test.d.ts +0 -2
  196. package/dist/llm/embedding/providers/ollama.test.d.ts.map +0 -1
  197. package/dist/llm/embedding/providers/ollama.test.js +0 -54
  198. package/dist/llm/embedding/providers/ollama.test.js.map +0 -1
  199. package/dist/llm/embedding/providers/openai.d.ts.map +0 -1
  200. package/dist/llm/embedding/providers/openai.js.map +0 -1
  201. package/dist/llm/embedding/providers/openai.test.d.ts +0 -2
  202. package/dist/llm/embedding/providers/openai.test.d.ts.map +0 -1
  203. package/dist/llm/embedding/providers/openai.test.js +0 -48
  204. package/dist/llm/embedding/providers/openai.test.js.map +0 -1
  205. package/dist/llm/embedding/providers/portkey.d.ts.map +0 -1
  206. package/dist/llm/embedding/providers/portkey.js.map +0 -1
  207. package/dist/llm/embedding/providers/portkey.test.d.ts +0 -2
  208. package/dist/llm/embedding/providers/portkey.test.d.ts.map +0 -1
  209. package/dist/llm/embedding/providers/portkey.test.js +0 -56
  210. package/dist/llm/embedding/providers/portkey.test.js.map +0 -1
  211. package/dist/llm/embedding/registry.d.ts.map +0 -1
  212. package/dist/llm/embedding/registry.js.map +0 -1
  213. package/dist/llm/embedding/registry.test.d.ts +0 -7
  214. package/dist/llm/embedding/registry.test.d.ts.map +0 -1
  215. package/dist/llm/embedding/registry.test.js +0 -68
  216. package/dist/llm/embedding/registry.test.js.map +0 -1
  217. package/dist/llm/embedding/types.d.ts.map +0 -1
  218. package/dist/llm/embedding/types.js.map +0 -1
  219. package/dist/llm/errors.d.ts.map +0 -1
  220. package/dist/llm/errors.js.map +0 -1
  221. package/dist/llm/errors.test.d.ts +0 -2
  222. package/dist/llm/errors.test.d.ts.map +0 -1
  223. package/dist/llm/errors.test.js +0 -103
  224. package/dist/llm/errors.test.js.map +0 -1
  225. package/dist/llm/fetch.d.ts.map +0 -1
  226. package/dist/llm/fetch.js.map +0 -1
  227. package/dist/llm/index.d.ts.map +0 -1
  228. package/dist/llm/index.js.map +0 -1
  229. package/dist/llm/inference/index.d.ts.map +0 -1
  230. package/dist/llm/inference/index.js.map +0 -1
  231. package/dist/llm/inference/index.test.d.ts +0 -6
  232. package/dist/llm/inference/index.test.d.ts.map +0 -1
  233. package/dist/llm/inference/index.test.js +0 -150
  234. package/dist/llm/inference/index.test.js.map +0 -1
  235. package/dist/llm/inference/providers/bedrock.d.ts.map +0 -1
  236. package/dist/llm/inference/providers/bedrock.js.map +0 -1
  237. package/dist/llm/inference/providers/bedrock.test.d.ts +0 -2
  238. package/dist/llm/inference/providers/bedrock.test.d.ts.map +0 -1
  239. package/dist/llm/inference/providers/bedrock.test.js +0 -68
  240. package/dist/llm/inference/providers/bedrock.test.js.map +0 -1
  241. package/dist/llm/inference/providers/index.d.ts.map +0 -1
  242. package/dist/llm/inference/providers/index.js.map +0 -1
  243. package/dist/llm/inference/providers/ollama.d.ts.map +0 -1
  244. package/dist/llm/inference/providers/ollama.js.map +0 -1
  245. package/dist/llm/inference/providers/ollama.test.d.ts +0 -2
  246. package/dist/llm/inference/providers/ollama.test.d.ts.map +0 -1
  247. package/dist/llm/inference/providers/ollama.test.js +0 -57
  248. package/dist/llm/inference/providers/ollama.test.js.map +0 -1
  249. package/dist/llm/inference/providers/openai.d.ts.map +0 -1
  250. package/dist/llm/inference/providers/openai.js.map +0 -1
  251. package/dist/llm/inference/providers/openai.test.d.ts +0 -2
  252. package/dist/llm/inference/providers/openai.test.d.ts.map +0 -1
  253. package/dist/llm/inference/providers/openai.test.js +0 -82
  254. package/dist/llm/inference/providers/openai.test.js.map +0 -1
  255. package/dist/llm/inference/providers/portkey.d.ts.map +0 -1
  256. package/dist/llm/inference/providers/portkey.js.map +0 -1
  257. package/dist/llm/inference/providers/portkey.test.d.ts +0 -2
  258. package/dist/llm/inference/providers/portkey.test.d.ts.map +0 -1
  259. package/dist/llm/inference/providers/portkey.test.js +0 -48
  260. package/dist/llm/inference/providers/portkey.test.js.map +0 -1
  261. package/dist/llm/inference/registry.d.ts.map +0 -1
  262. package/dist/llm/inference/registry.js.map +0 -1
  263. package/dist/llm/inference/registry.test.d.ts +0 -6
  264. package/dist/llm/inference/registry.test.d.ts.map +0 -1
  265. package/dist/llm/inference/registry.test.js +0 -63
  266. package/dist/llm/inference/registry.test.js.map +0 -1
  267. package/dist/llm/inference/types.d.ts.map +0 -1
  268. package/dist/llm/inference/types.js.map +0 -1
  269. package/dist/llm/telemetry.d.ts.map +0 -1
  270. package/dist/llm/telemetry.js.map +0 -1
  271. package/dist/llm/telemetry.test.d.ts +0 -5
  272. package/dist/llm/telemetry.test.d.ts.map +0 -1
  273. package/dist/llm/telemetry.test.js +0 -89
  274. package/dist/llm/telemetry.test.js.map +0 -1
  275. package/dist/llm/types.d.ts.map +0 -1
  276. package/dist/llm/types.js.map +0 -1
  277. package/dist/logger.d.ts.map +0 -1
  278. package/dist/logger.js.map +0 -1
  279. package/dist/logger.test.d.ts +0 -5
  280. package/dist/logger.test.d.ts.map +0 -1
  281. package/dist/logger.test.js +0 -103
  282. package/dist/logger.test.js.map +0 -1
  283. package/dist/mcp/api.d.ts.map +0 -1
  284. package/dist/mcp/api.js.map +0 -1
  285. package/dist/mcp/api.test.d.ts +0 -6
  286. package/dist/mcp/api.test.d.ts.map +0 -1
  287. package/dist/mcp/api.test.js +0 -130
  288. package/dist/mcp/api.test.js.map +0 -1
  289. package/dist/mcp/helpers.d.ts.map +0 -1
  290. package/dist/mcp/helpers.js.map +0 -1
  291. package/dist/mcp/helpers.test.d.ts +0 -5
  292. package/dist/mcp/helpers.test.d.ts.map +0 -1
  293. package/dist/mcp/helpers.test.js +0 -548
  294. package/dist/mcp/helpers.test.js.map +0 -1
  295. package/dist/mcp/index.d.ts.map +0 -1
  296. package/dist/mcp/index.js.map +0 -1
  297. package/dist/mcp/taxonomy.d.ts.map +0 -1
  298. package/dist/mcp/taxonomy.js.map +0 -1
  299. package/dist/mcp/taxonomy.test.d.ts +0 -5
  300. package/dist/mcp/taxonomy.test.d.ts.map +0 -1
  301. package/dist/mcp/taxonomy.test.js +0 -215
  302. package/dist/mcp/taxonomy.test.js.map +0 -1
  303. package/dist/mcp/tools.d.ts.map +0 -1
  304. package/dist/mcp/tools.integration.itest.d.ts +0 -23
  305. package/dist/mcp/tools.integration.itest.d.ts.map +0 -1
  306. package/dist/mcp/tools.integration.itest.js +0 -171
  307. package/dist/mcp/tools.integration.itest.js.map +0 -1
  308. package/dist/mcp/tools.js.map +0 -1
  309. package/dist/mcp/tools.test.d.ts +0 -16
  310. package/dist/mcp/tools.test.d.ts.map +0 -1
  311. package/dist/mcp/tools.test.js +0 -908
  312. package/dist/mcp/tools.test.js.map +0 -1
  313. package/dist/mcp/types.d.ts.map +0 -1
  314. package/dist/mcp/types.js.map +0 -1
  315. package/dist/postinstall.d.ts.map +0 -1
  316. package/dist/postinstall.js.map +0 -1
  317. package/dist/privacy/redaction.d.ts.map +0 -1
  318. package/dist/privacy/redaction.js.map +0 -1
  319. package/dist/privacy/redaction.test.d.ts +0 -2
  320. package/dist/privacy/redaction.test.d.ts.map +0 -1
  321. package/dist/privacy/redaction.test.js +0 -51
  322. package/dist/privacy/redaction.test.js.map +0 -1
  323. package/dist/prompts/brief.d.ts.map +0 -1
  324. package/dist/prompts/brief.js.map +0 -1
  325. package/dist/prompts/contradiction.d.ts.map +0 -1
  326. package/dist/prompts/contradiction.js.map +0 -1
  327. package/dist/prompts/distill.d.ts.map +0 -1
  328. package/dist/prompts/distill.js.map +0 -1
  329. package/dist/prompts/entity-typing.d.ts.map +0 -1
  330. package/dist/prompts/entity-typing.js.map +0 -1
  331. package/dist/prompts/episode-summary.d.ts.map +0 -1
  332. package/dist/prompts/episode-summary.js.map +0 -1
  333. package/dist/prompts/extraction.d.ts.map +0 -1
  334. package/dist/prompts/extraction.js.map +0 -1
  335. package/dist/prompts/index.d.ts.map +0 -1
  336. package/dist/prompts/index.js.map +0 -1
  337. package/dist/prompts/prompts.test.d.ts +0 -8
  338. package/dist/prompts/prompts.test.d.ts.map +0 -1
  339. package/dist/prompts/prompts.test.js +0 -140
  340. package/dist/prompts/prompts.test.js.map +0 -1
  341. package/dist/prompts/relations.d.ts.map +0 -1
  342. package/dist/prompts/relations.js.map +0 -1
  343. package/dist/prompts/workstream-summary.d.ts.map +0 -1
  344. package/dist/prompts/workstream-summary.js.map +0 -1
  345. package/dist/provenance/actor.d.ts.map +0 -1
  346. package/dist/provenance/actor.js.map +0 -1
  347. package/dist/provenance/actor.test.d.ts +0 -2
  348. package/dist/provenance/actor.test.d.ts.map +0 -1
  349. package/dist/provenance/actor.test.js +0 -49
  350. package/dist/provenance/actor.test.js.map +0 -1
  351. package/dist/render.d.ts.map +0 -1
  352. package/dist/render.js.map +0 -1
  353. package/dist/render.test.d.ts +0 -8
  354. package/dist/render.test.d.ts.map +0 -1
  355. package/dist/render.test.js +0 -244
  356. package/dist/render.test.js.map +0 -1
  357. package/dist/status.d.ts.map +0 -1
  358. package/dist/status.js.map +0 -1
  359. package/dist/status.test.d.ts +0 -5
  360. package/dist/status.test.d.ts.map +0 -1
  361. package/dist/status.test.js +0 -203
  362. package/dist/status.test.js.map +0 -1
@@ -1,908 +0,0 @@
1
- /**
2
- * Integration tests for the MCP tool handlers in tools.ts.
3
- *
4
- * tools.ts registers all tool handlers inside `registerTools(mcp)`. To exercise
5
- * them without a live MCP server we use a minimal fake McpServer that captures
6
- * each `tool(name, desc, schema, handler)` registration into a map. Tests then
7
- * invoke handlers directly with mocked fetch responses for Qdrant + embedding.
8
- *
9
- * Covers the data-integrity + recall surface:
10
- * - memory_store: hash dedup, vector dedup, supersedes, relation insertion
11
- * - memory_recall: filter passthrough, graph_depth=1 traversal, ranking
12
- * - memory_entity: facts + relations aggregation with dedup
13
- * - memory_forget: marks fact as superseded with reason
14
- */
15
- import { describe, it, before, beforeEach, after } from "node:test";
16
- import assert from "node:assert/strict";
17
- import { registerTools, resolveScope } from "./tools.js";
18
- import { setQdrantUrl, setQdrantApiKey, setReady, setCollection, initEmbedding, } from "./api.js";
19
- import { THRESHOLD_DUPLICATE, THRESHOLD_RELATED } from "./taxonomy.js";
20
- function makeFakeServer() {
21
- const handlers = new Map();
22
- const server = {
23
- tool(name, _desc, _schema, handler) {
24
- handlers.set(name, handler);
25
- },
26
- };
27
- return { server, handlers };
28
- }
29
- const realFetch = globalThis.fetch;
30
- let calls;
31
- let responders;
32
- function installFetchMock() {
33
- calls = [];
34
- responders = [];
35
- globalThis.fetch = (async (input, init) => {
36
- const url = typeof input === "string" ? input : input.toString();
37
- const body = init?.body ? JSON.parse(String(init.body)) : null;
38
- const call = { url, method: init?.method ?? "GET", body };
39
- calls.push(call);
40
- for (const r of responders) {
41
- const matches = typeof r.match === "string" ? url.includes(r.match) : r.match.test(url);
42
- if (matches) {
43
- const data = r.respond(call);
44
- return new Response(JSON.stringify(data), {
45
- status: 200,
46
- headers: { "Content-Type": "application/json" },
47
- });
48
- }
49
- }
50
- // Default: empty success.
51
- return new Response(JSON.stringify({ status: "ok", result: null }), { status: 200 });
52
- });
53
- }
54
- function on(match, respond) {
55
- responders.push({ match, respond });
56
- }
57
- function callsTo(match) {
58
- return calls.filter((c) => typeof match === "string" ? c.url.includes(match) : match.test(c.url));
59
- }
60
- // ---------------------------------------------------------------------------
61
- // Suite setup
62
- // ---------------------------------------------------------------------------
63
- let handlers;
64
- async function invoke(name, args) {
65
- const h = handlers.get(name);
66
- if (!h)
67
- throw new Error(`Handler '${name}' not registered`);
68
- return h(args);
69
- }
70
- function textOf(result) {
71
- return result.content[0]?.text ?? "";
72
- }
73
- describe("mcp/tools handlers", () => {
74
- before(() => {
75
- setQdrantUrl("https://qdrant.test:6333");
76
- setQdrantApiKey("test-key");
77
- setCollection("bikky-test");
78
- setReady(true);
79
- initEmbedding({
80
- provider: "ollama",
81
- baseUrl: "http://embed.test:11434",
82
- model: "test-model",
83
- dimensions: 4,
84
- apiKey: null,
85
- });
86
- const fake = makeFakeServer();
87
- // The real registerTools expects an McpServer; the only methods used are
88
- // `tool(...)` so the fake is structurally compatible at runtime.
89
- registerTools(fake.server);
90
- handlers = fake.handlers;
91
- });
92
- beforeEach(() => {
93
- installFetchMock();
94
- // Always provide an embedding for any embed() call.
95
- on("/v1/embeddings", () => ({ data: [{ embedding: [0.1, 0.2, 0.3, 0.4] }] }));
96
- });
97
- after(() => {
98
- globalThis.fetch = realFetch;
99
- setReady(false);
100
- setQdrantUrl(null);
101
- setQdrantApiKey(null);
102
- });
103
- // ─────────────────────────────────────────────────────────────────────────
104
- // Tool registration sanity
105
- // ─────────────────────────────────────────────────────────────────────────
106
- it("registers all expected memory tools", () => {
107
- for (const name of [
108
- "memory_store",
109
- "memory_recall",
110
- "memory_entity",
111
- "memory_relations",
112
- "memory_forget",
113
- "memory_verify",
114
- "memory_review",
115
- "memory_heartbeat",
116
- "memory_mark_useful",
117
- "memory_report_outcome",
118
- "memory_session_summary",
119
- "memory_distill",
120
- ]) {
121
- assert.ok(handlers.has(name), `expected handler '${name}' to be registered`);
122
- }
123
- });
124
- // ─────────────────────────────────────────────────────────────────────────
125
- // memory_store — dedup + insertion pipeline
126
- // ─────────────────────────────────────────────────────────────────────────
127
- describe("memory_store", () => {
128
- it("reinforces an exact content-hash match without re-embedding", async () => {
129
- on("/points/scroll", () => ({
130
- result: {
131
- points: [{
132
- id: "existing-1",
133
- payload: { content: "x", category: "engineering", reinforcement_count: 2 },
134
- }],
135
- },
136
- }));
137
- let setPayloadBody = null;
138
- on("/points/payload", (call) => {
139
- setPayloadBody = call.body;
140
- return { status: "ok" };
141
- });
142
- const result = await invoke("memory_store", {
143
- content: "qdrant runs on port 6333",
144
- category: "engineering",
145
- entities: ["qdrant"],
146
- });
147
- const parsed = JSON.parse(textOf(result));
148
- assert.equal(parsed.action, "reinforced");
149
- assert.equal(parsed.fact_id, "existing-1");
150
- assert.equal(parsed.reinforcement_count, 3);
151
- assert.deepEqual(setPayloadBody.points, ["existing-1"]);
152
- // Hash dedup hits before embedding.
153
- assert.equal(callsTo("/v1/embeddings").length, 0);
154
- // Hash dedup hits before semantic search.
155
- assert.equal(callsTo("/points/search").length, 0);
156
- });
157
- it("reinforces when semantic similarity exceeds THRESHOLD_DUPLICATE", async () => {
158
- on("/points/scroll", () => ({ result: { points: [] } })); // no hash hit
159
- on("/points/search", () => ({
160
- result: [{
161
- id: "near-dup",
162
- score: THRESHOLD_DUPLICATE + 0.01,
163
- payload: { content: "near", category: "engineering", reinforcement_count: 1 },
164
- }],
165
- }));
166
- on("/points/payload", () => ({ status: "ok" }));
167
- const result = await invoke("memory_store", {
168
- content: "qdrant listens on 6333",
169
- category: "engineering",
170
- entities: ["qdrant"],
171
- });
172
- const parsed = JSON.parse(textOf(result));
173
- assert.equal(parsed.action, "reinforced");
174
- assert.equal(parsed.fact_id, "near-dup");
175
- assert.equal(parsed.reinforcement_count, 2);
176
- assert.ok(parsed.similarity > THRESHOLD_DUPLICATE);
177
- // Should not insert a new point.
178
- assert.equal(callsTo(/\/points$/).length, 0);
179
- });
180
- it("inserts a new point when no duplicates are found", async () => {
181
- on("/points/scroll", () => ({ result: { points: [] } }));
182
- on("/points/search", () => ({ result: [] }));
183
- const upsertBodies = [];
184
- on(/\/points$/, (call) => {
185
- if (call.method === "PUT" && call.body)
186
- upsertBodies.push(call.body);
187
- return { status: "ok" };
188
- });
189
- const result = await invoke("memory_store", {
190
- content: "platform is on AWS",
191
- category: "engineering",
192
- entities: ["Platform"],
193
- importance: 0.7,
194
- });
195
- const parsed = JSON.parse(textOf(result));
196
- assert.equal(parsed.action, "inserted");
197
- assert.ok(parsed.fact_id, "should return a generated fact_id");
198
- assert.equal(upsertBodies.length, 1, "expected an upsert PUT to /points");
199
- const upsertBody = upsertBodies[0];
200
- const points = upsertBody.points;
201
- assert.ok(Array.isArray(points), "expected points array");
202
- const pt = points[0];
203
- assert.equal(pt.payload.content, "platform is on AWS");
204
- assert.deepEqual(pt.payload.entities, ["platform"]); // lowercased
205
- assert.equal(pt.payload.reinforcement_count, 1);
206
- assert.equal(pt.payload.importance, 0.7);
207
- assert.ok(pt.payload.content_hash, "content_hash should be set");
208
- assert.equal(pt.payload.superseded_by, null);
209
- });
210
- it("redacts secret values before embedding and storage", async () => {
211
- on("/points/scroll", () => ({ result: { points: [] } }));
212
- on("/points/search", () => ({ result: [] }));
213
- const upsertBodies = [];
214
- on(/\/points$/, (call) => {
215
- if (call.method === "PUT" && call.body)
216
- upsertBodies.push(call.body);
217
- return { status: "ok" };
218
- });
219
- const result = await invoke("memory_store", {
220
- content: "platform deploy uses password=supersecretvalue",
221
- category: "engineering",
222
- entities: ["platform"],
223
- });
224
- const parsed = JSON.parse(textOf(result));
225
- assert.equal(parsed.action, "inserted");
226
- const embedCall = callsTo("/v1/embeddings")[0];
227
- assert.equal(embedCall?.body?.input, "platform deploy uses password=[REDACTED:secret]");
228
- assert.equal(upsertBodies.length, 1, "expected an upsert PUT to /points");
229
- const points = upsertBodies[0].points;
230
- assert.ok(Array.isArray(points), "expected points array");
231
- const pt = points[0];
232
- assert.equal(pt.payload.content, "platform deploy uses password=[REDACTED:secret]");
233
- assert.deepEqual(pt.payload.redaction, {
234
- redacted: true,
235
- summary: "secret:1",
236
- matches: [{ type: "secret", count: 1 }],
237
- });
238
- });
239
- it("flags potential conflicts when similarity is in the related band with shared entities", async () => {
240
- on("/points/scroll", () => ({ result: { points: [] } }));
241
- const related = (THRESHOLD_RELATED + THRESHOLD_DUPLICATE) / 2;
242
- on("/points/search", () => ({
243
- result: [{
244
- id: "related-1",
245
- score: related,
246
- payload: {
247
- content: "old fact about platform",
248
- category: "engineering",
249
- entities: ["platform"],
250
- },
251
- }],
252
- }));
253
- on(/\/points$/, () => ({ status: "ok" }));
254
- const result = await invoke("memory_store", {
255
- content: "new fact about platform",
256
- category: "engineering",
257
- entities: ["platform"],
258
- });
259
- const parsed = JSON.parse(textOf(result));
260
- assert.equal(parsed.action, "inserted");
261
- assert.ok(Array.isArray(parsed.similar_facts) && parsed.similar_facts.length === 1);
262
- assert.ok(Array.isArray(parsed.potential_conflicts) && parsed.potential_conflicts.length === 1);
263
- assert.deepEqual(parsed.potential_conflicts[0].shared_entities, ["platform"]);
264
- assert.ok(parsed.conflict_hint, "conflict_hint should be present");
265
- });
266
- it("supersedes the prior fact when supersedes is provided", async () => {
267
- on("/points/scroll", () => ({ result: { points: [] } }));
268
- on("/points/search", () => ({ result: [] }));
269
- const payloadCalls = [];
270
- on("/points/payload", (call) => {
271
- payloadCalls.push(call.body);
272
- return { status: "ok" };
273
- });
274
- on(/\/points$/, (call) => {
275
- // POST without `points` key = qdrantGetPoints (lookup of supersedes target)
276
- const body = call.body;
277
- if (call.method === "POST" && !body.points) {
278
- return { result: [{ id: "old-fact-id", payload: { content: "old fact" } }] };
279
- }
280
- return { status: "ok" };
281
- });
282
- const result = await invoke("memory_store", {
283
- content: "qdrant now on 7000",
284
- category: "engineering",
285
- entities: ["qdrant"],
286
- supersedes: "old-fact-id",
287
- });
288
- const parsed = JSON.parse(textOf(result));
289
- assert.equal(parsed.action, "inserted");
290
- assert.equal(payloadCalls.length, 1);
291
- const supersedeCall = payloadCalls[0];
292
- assert.deepEqual(supersedeCall.points, ["old-fact-id"]);
293
- assert.equal(supersedeCall.payload.superseded_by, parsed.fact_id);
294
- assert.ok(supersedeCall.payload.superseded_at);
295
- });
296
- it("inserts a relation point alongside the fact when relation is provided", async () => {
297
- on("/points/scroll", () => ({ result: { points: [] } }));
298
- on("/points/search", () => ({ result: [] }));
299
- const upserts = [];
300
- on(/\/points$/, (call) => {
301
- if (call.method === "PUT")
302
- upserts.push(call.body);
303
- return { status: "ok" };
304
- });
305
- const result = await invoke("memory_store", {
306
- content: "saber owns platform",
307
- category: "human",
308
- entities: ["saber", "platform"],
309
- relation: { from: "Saber", type: "Owns", to: "Platform" },
310
- });
311
- const parsed = JSON.parse(textOf(result));
312
- assert.equal(parsed.action, "inserted");
313
- assert.ok(parsed.relation_id, "relation_id should be returned");
314
- // First upsert is the fact, second is the relation.
315
- assert.equal(upserts.length, 2);
316
- const relPt = upserts[1].points[0];
317
- assert.equal(relPt.payload.kind, "relation");
318
- assert.equal(relPt.payload.from_entity, "saber");
319
- assert.equal(relPt.payload.to_entity, "platform");
320
- assert.equal(relPt.payload.relation_type, "owns");
321
- assert.deepEqual(relPt.payload.entities, ["saber", "platform"]);
322
- });
323
- it("scopes relation redaction metadata to the relation payload", async () => {
324
- on("/points/scroll", () => ({ result: { points: [] } }));
325
- on("/points/search", () => ({ result: [] }));
326
- const upserts = [];
327
- on(/\/points$/, (call) => {
328
- if (call.method === "PUT")
329
- upserts.push(call.body);
330
- return { status: "ok" };
331
- });
332
- const result = await invoke("memory_store", {
333
- content: "saber owns platform",
334
- category: "human",
335
- entities: ["saber", "platform"],
336
- relation: { from: "api_key=relationsecret", type: "Owns", to: "Platform" },
337
- });
338
- const parsed = JSON.parse(textOf(result));
339
- assert.equal(parsed.action, "inserted");
340
- assert.deepEqual(parsed.redaction, {
341
- redacted: true,
342
- summary: "secret:1",
343
- matches: [{ type: "secret", count: 1 }],
344
- });
345
- assert.equal(upserts.length, 2);
346
- const factPt = upserts[0].points[0];
347
- const relPt = upserts[1].points[0];
348
- assert.equal(factPt.payload.redaction, undefined);
349
- assert.equal(relPt.payload.content, "api_key=[REDACTED:secret] Owns Platform");
350
- assert.deepEqual(relPt.payload.redaction, {
351
- redacted: true,
352
- summary: "secret:1",
353
- matches: [{ type: "secret", count: 1 }],
354
- });
355
- });
356
- });
357
- // ─────────────────────────────────────────────────────────────────────────
358
- // memory_recall — filter composition + graph_depth
359
- // ─────────────────────────────────────────────────────────────────────────
360
- describe("memory_recall", () => {
361
- it("returns 'No matching facts found' when search returns no results", async () => {
362
- on("/points/search", () => ({ result: [] }));
363
- const result = await invoke("memory_recall", { query: "nothing" });
364
- assert.match(textOf(result), /No matching facts found/);
365
- });
366
- it("returns parseable empty JSON when output_format=json and search has no results", async () => {
367
- on("/points/search", () => ({ result: [] }));
368
- const result = await invoke("memory_recall", { query: "nothing", output_format: "json" });
369
- const parsed = JSON.parse(textOf(result));
370
- assert.equal(parsed.query, "nothing");
371
- assert.equal(parsed.result_count, 0);
372
- assert.equal(parsed.related_count, 0);
373
- assert.deepEqual(parsed.results, []);
374
- assert.deepEqual(parsed.related, []);
375
- });
376
- it("passes category, domain, kind, entity, since, until, and metadata into the Qdrant filter", async () => {
377
- let searchBody = null;
378
- on("/points/search", (call) => {
379
- searchBody = call.body;
380
- return { result: [] };
381
- });
382
- await invoke("memory_recall", {
383
- query: "q",
384
- category: "engineering",
385
- domain: "software_engineering",
386
- kind: "fact",
387
- entity: "Qdrant",
388
- since: "2025-01-01T00:00:00Z",
389
- until: "2025-12-31T00:00:00Z",
390
- metadata_filter: { project: "bikky" },
391
- });
392
- assert.ok(searchBody);
393
- const filter = searchBody.filter;
394
- const must = filter.must;
395
- // excludeSuperseded → adds `is_null` condition into `must`.
396
- assert.ok(must.some((c) => c.is_null?.key === "superseded_by"), "expected an is_null:superseded_by condition in must");
397
- // Field filters present in `must`.
398
- const keys = must.map((c) => c.key ?? "").filter(Boolean);
399
- for (const k of ["category", "domain", "kind", "entities", "created_at", "metadata.project"]) {
400
- assert.ok(keys.includes(k), `expected filter.must to contain key=${k}; got ${keys.join(",")}`);
401
- }
402
- // Entity is lowercased.
403
- const entityCond = must.find((c) => c.key === "entities");
404
- assert.equal(entityCond.match.value, "qdrant");
405
- });
406
- it("ranks results by combined score and slices to limit", async () => {
407
- // Three points: low score should win after combined weighting only if
408
- // freshness/reinforcement compensate; we just verify deterministic order
409
- // with three scores and limit=2.
410
- on("/points/search", () => ({
411
- result: [
412
- { id: "a", score: 0.6, payload: { content: "A", category: "engineering", entities: [], reinforcement_count: 1, confidence: 0.9, importance: 0.5, created_at: new Date().toISOString() } },
413
- { id: "b", score: 0.9, payload: { content: "B", category: "engineering", entities: [], reinforcement_count: 1, confidence: 0.9, importance: 0.5, created_at: new Date().toISOString() } },
414
- { id: "c", score: 0.75, payload: { content: "C", category: "engineering", entities: [], reinforcement_count: 1, confidence: 0.9, importance: 0.5, created_at: new Date().toISOString() } },
415
- ],
416
- }));
417
- const result = await invoke("memory_recall", { query: "q", limit: 2 });
418
- const text = textOf(result);
419
- const lines = text.split("\n").filter(Boolean);
420
- assert.equal(lines.length, 2, "should slice to limit=2");
421
- // 'B' has highest vector score → highest combined score → first.
422
- assert.match(lines[0], /\bB\b/);
423
- assert.match(lines[1], /\bC\b/);
424
- });
425
- it("returns structured JSON with stable fact fields when output_format=json", async () => {
426
- const now = new Date().toISOString();
427
- on("/points/search", () => ({
428
- result: [{
429
- id: "fact-json",
430
- score: 0.91,
431
- payload: {
432
- content: "JSON recall should be easy to parse",
433
- category: "engineering",
434
- domain: "software_engineering",
435
- kind: "fact",
436
- source: "user",
437
- entities: ["mcp", "recall"],
438
- confidence: 0.88,
439
- importance: 0.7,
440
- reinforcement_count: 2,
441
- verification_count: 1,
442
- useful_count: 3,
443
- not_useful_count: 0,
444
- metadata: { project: "bikky" },
445
- created_at: now,
446
- updated_at: now,
447
- },
448
- }],
449
- }));
450
- const result = await invoke("memory_recall", { query: "json recall", output_format: "json" });
451
- const parsed = JSON.parse(textOf(result));
452
- assert.equal(parsed.query, "json recall");
453
- assert.equal(parsed.requested_limit, 10);
454
- assert.equal(parsed.effective_limit, 10);
455
- assert.equal(parsed.limit_clamped, false);
456
- assert.equal(parsed.result_count, 1);
457
- assert.equal(parsed.related_count, 0);
458
- assert.equal(parsed.results.length, 1);
459
- assert.equal(parsed.results[0].id, "fact-json");
460
- assert.equal(parsed.results[0].content, "JSON recall should be easy to parse");
461
- assert.equal(parsed.results[0].category, "engineering");
462
- assert.deepEqual(parsed.results[0].entities, ["mcp", "recall"]);
463
- assert.equal(parsed.results[0].confidence, 0.88);
464
- assert.equal(parsed.results[0].score, 0.91);
465
- assert.deepEqual(parsed.related, []);
466
- });
467
- it("clamps large recall limits and reports the effective limit in JSON", async () => {
468
- const now = new Date().toISOString();
469
- on("/points/search", () => {
470
- return {
471
- result: Array.from({ length: 60 }, (_, i) => ({
472
- id: `fact-${i}`,
473
- score: 1 - i / 100,
474
- payload: {
475
- content: `fact ${i}`,
476
- category: "engineering",
477
- entities: [],
478
- confidence: 0.9,
479
- reinforcement_count: 1,
480
- created_at: now,
481
- },
482
- })),
483
- };
484
- });
485
- const result = await invoke("memory_recall", { query: "many", limit: 999, output_format: "json" });
486
- const parsed = JSON.parse(textOf(result));
487
- const searchBody = callsTo("/points/search")[0]?.body;
488
- assert.equal(searchBody?.limit, 100);
489
- assert.equal(parsed.requested_limit, 999);
490
- assert.equal(parsed.effective_limit, 50);
491
- assert.equal(parsed.max_limit, 50);
492
- assert.equal(parsed.limit_clamped, true);
493
- assert.equal(parsed.results.length, 50);
494
- assert.equal(parsed.result_count, 50);
495
- });
496
- it("appends 1-hop related facts when graph_depth=1", async () => {
497
- // Primary search returns one fact mentioning entity 'a'.
498
- on("/points/search", () => ({
499
- result: [{
500
- id: "p1",
501
- score: 0.9,
502
- payload: { content: "primary", category: "engineering", entities: ["a"], reinforcement_count: 1 },
503
- }],
504
- }));
505
- // Scroll mock dispatches based on filter shape inside the body.
506
- on("/points/scroll", (call) => {
507
- const body = call.body;
508
- const conditions = body.filter.must;
509
- const fromCond = conditions.find((c) => c.key === "from_entity");
510
- const toCond = conditions.find((c) => c.key === "to_entity");
511
- const entitiesCond = conditions.find((c) => c.key === "entities");
512
- // graphTraversal queries: outgoing(from_entity=a), incoming(to_entity=a), then entities=b.
513
- if (fromCond?.match.value === "a") {
514
- return { result: { points: [{
515
- id: "rel1",
516
- payload: { content: "a -> b", category: "human", entities: ["a", "b"], from_entity: "a", to_entity: "b", relation_type: "uses", reinforcement_count: 1 },
517
- }] } };
518
- }
519
- if (toCond?.match.value === "a") {
520
- return { result: { points: [] } };
521
- }
522
- if (entitiesCond?.match.value === "b") {
523
- return { result: { points: [{
524
- id: "neighbor",
525
- payload: { content: "b is interesting", category: "engineering", entities: ["b"], reinforcement_count: 1 },
526
- }] } };
527
- }
528
- return { result: { points: [] } };
529
- });
530
- const result = await invoke("memory_recall", { query: "q", graph_depth: 1, limit: 5 });
531
- const text = textOf(result);
532
- assert.match(text, /primary/);
533
- assert.match(text, /Related \(1-hop\)/);
534
- assert.match(text, /b is interesting/);
535
- });
536
- it("separates primary and 1-hop related facts in JSON output", async () => {
537
- on("/points/search", () => ({
538
- result: [{
539
- id: "p1",
540
- score: 0.9,
541
- payload: {
542
- content: "primary",
543
- category: "engineering",
544
- entities: ["a"],
545
- confidence: 0.9,
546
- reinforcement_count: 1,
547
- created_at: new Date().toISOString(),
548
- },
549
- }],
550
- }));
551
- on("/points/scroll", (call) => {
552
- const body = call.body;
553
- const conditions = body.filter.must;
554
- const fromCond = conditions.find((c) => c.key === "from_entity");
555
- const toCond = conditions.find((c) => c.key === "to_entity");
556
- const entitiesCond = conditions.find((c) => c.key === "entities");
557
- if (fromCond?.match.value === "a") {
558
- return { result: { points: [{
559
- id: "rel1",
560
- payload: { content: "a -> b", category: "human", entities: ["a", "b"], from_entity: "a", to_entity: "b", relation_type: "uses", reinforcement_count: 1 },
561
- }] } };
562
- }
563
- if (toCond?.match.value === "a") {
564
- return { result: { points: [] } };
565
- }
566
- if (entitiesCond?.match.value === "b") {
567
- return { result: { points: [{
568
- id: "neighbor",
569
- score: 0.7,
570
- payload: {
571
- content: "b is interesting",
572
- category: "engineering",
573
- entities: ["b"],
574
- confidence: 0.8,
575
- reinforcement_count: 1,
576
- created_at: new Date().toISOString(),
577
- },
578
- }] } };
579
- }
580
- return { result: { points: [] } };
581
- });
582
- const result = await invoke("memory_recall", { query: "q", graph_depth: 1, limit: 5, output_format: "json" });
583
- const parsed = JSON.parse(textOf(result));
584
- assert.equal(parsed.graph_depth, 1);
585
- assert.equal(parsed.result_count, 1);
586
- assert.equal(parsed.related_count, 1);
587
- assert.equal(parsed.results[0].id, "p1");
588
- assert.equal(parsed.related[0].id, "neighbor");
589
- assert.equal(parsed.related[0].content, "b is interesting");
590
- });
591
- });
592
- // ─────────────────────────────────────────────────────────────────────────
593
- // memory_entity — facts + relations aggregation
594
- // ─────────────────────────────────────────────────────────────────────────
595
- describe("memory_entity", () => {
596
- it("aggregates facts mentioning the entity plus from/to relations and dedups", async () => {
597
- on("/points/scroll", (call) => {
598
- const body = call.body;
599
- // Filter may contain workspace_id conditions first; scan all conditions for the entity-shaped one.
600
- const conds = body.filter.must;
601
- const entityCond = conds.find((c) => c.key === "entities" && c.match?.value === "platform");
602
- const fromCond = conds.find((c) => c.key === "from_entity" && c.match?.value === "platform");
603
- const toCond = conds.find((c) => c.key === "to_entity" && c.match?.value === "platform");
604
- if (entityCond) {
605
- return { result: { points: [
606
- { id: "f1", payload: { content: "platform fact", category: "engineering", entities: ["platform"], reinforcement_count: 1 } },
607
- ] } };
608
- }
609
- if (fromCond) {
610
- return { result: { points: [
611
- { id: "r1", payload: { content: "platform uses qdrant", category: "human", from_entity: "platform", to_entity: "qdrant", relation_type: "uses" } },
612
- ] } };
613
- }
614
- if (toCond) {
615
- // Same relation appears as 'to' lookup too — must be deduped by id.
616
- return { result: { points: [
617
- { id: "r1", payload: { content: "platform uses qdrant", category: "human", from_entity: "platform", to_entity: "qdrant", relation_type: "uses" } },
618
- { id: "r2", payload: { content: "saber owns platform", category: "human", from_entity: "saber", to_entity: "platform", relation_type: "owns" } },
619
- ] } };
620
- }
621
- return { result: { points: [] } };
622
- });
623
- const result = await invoke("memory_entity", { name: "Platform" });
624
- const text = textOf(result);
625
- assert.match(text, /Facts about Platform \(1\)/);
626
- assert.match(text, /platform fact/);
627
- assert.match(text, /Relations \(2\)/); // r1 deduped, r1+r2 unique
628
- assert.match(text, /platform --\[uses\]--> qdrant/);
629
- assert.match(text, /saber --\[owns\]--> platform/);
630
- });
631
- it("returns a friendly message when nothing is found", async () => {
632
- on("/points/scroll", () => ({ result: { points: [] } }));
633
- const result = await invoke("memory_entity", { name: "ghost" });
634
- assert.match(textOf(result), /No facts or relations found for 'ghost'/);
635
- });
636
- });
637
- // ─────────────────────────────────────────────────────────────────────────
638
- // memory_forget — supersession marker
639
- // ─────────────────────────────────────────────────────────────────────────
640
- describe("memory_forget", () => {
641
- it("marks the fact as superseded with reason and timestamp", async () => {
642
- let body = null;
643
- on("/points/payload", (call) => {
644
- body = call.body;
645
- return { status: "ok" };
646
- });
647
- // memory_forget first calls qdrantGetPoints (POST /points with {ids,...}) for the workspace check.
648
- on(/\/points$/, (call) => {
649
- const callBody = call.body;
650
- if (call.method === "POST" && callBody.ids) {
651
- return { result: [{ id: "victim", payload: { content: "to be forgotten" } }] };
652
- }
653
- return { status: "ok" };
654
- });
655
- const result = await invoke("memory_forget", { fact_id: "victim", reason: "outdated" });
656
- const parsed = JSON.parse(textOf(result));
657
- assert.equal(parsed.status, "forgotten");
658
- assert.equal(parsed.fact_id, "victim");
659
- assert.equal(parsed.reason, "outdated");
660
- assert.ok(body);
661
- const b = body;
662
- assert.deepEqual(b.points, ["victim"]);
663
- assert.equal(b.payload.superseded_by, "forgotten:outdated");
664
- assert.ok(b.payload.superseded_at);
665
- assert.ok(b.payload.updated_at);
666
- });
667
- it("returns a graceful error string when Qdrant rejects the call", async () => {
668
- // No matcher → default 200 OK; replace with explicit failure.
669
- responders = [];
670
- globalThis.fetch = (async () => new Response("nope", { status: 500 }));
671
- const result = await invoke("memory_forget", { fact_id: "x", reason: "y" });
672
- const text = textOf(result);
673
- assert.match(text, /^Error: /);
674
- });
675
- });
676
- // ─────────────────────────────────────────────────────────────────────────
677
- // memory_mark_useful
678
- // ─────────────────────────────────────────────────────────────────────────
679
- describe("memory_mark_useful", () => {
680
- it("bumps useful_count and writes a feedback_event telemetry row", async () => {
681
- const setPayloadCalls = [];
682
- let upsertedPayload = null;
683
- on(/\/points$/, (call) => {
684
- const b = call.body;
685
- if (call.method === "POST" && b.ids) {
686
- return { result: [{ id: "fact-1", payload: { content: "x", useful_count: 2 } }] };
687
- }
688
- if (call.method === "PUT" && b.points) {
689
- upsertedPayload = b.points[0].payload;
690
- return { status: "ok" };
691
- }
692
- return { status: "ok" };
693
- });
694
- on("/points/payload", (call) => {
695
- setPayloadCalls.push(call.body);
696
- return { status: "ok" };
697
- });
698
- const result = await invoke("memory_mark_useful", { fact_id: "fact-1", note: "saved a re-debug" });
699
- const parsed = JSON.parse(textOf(result));
700
- assert.equal(parsed.status, "marked_useful");
701
- assert.equal(parsed.fact_id, "fact-1");
702
- assert.equal(parsed.useful_count, 3);
703
- assert.ok(parsed.event_id);
704
- // Counter bump on the original fact.
705
- const counterBump = setPayloadCalls.find((b) => b.payload.useful_count !== undefined);
706
- assert.ok(counterBump, "expected a useful_count payload update");
707
- const bumpPayload = counterBump.payload;
708
- assert.equal(bumpPayload.useful_count, 3);
709
- assert.ok(bumpPayload.last_useful_at);
710
- // Telemetry row written.
711
- assert.ok(upsertedPayload, "expected a telemetry feedback_event upsert");
712
- const tp = upsertedPayload;
713
- assert.equal(tp.kind, "telemetry");
714
- assert.equal(tp.memory_subtype, "feedback_event");
715
- assert.equal(tp.target_fact_id, "fact-1");
716
- assert.equal(tp.feedback_kind, "useful");
717
- assert.match(String(tp.content), /saved a re-debug/);
718
- });
719
- it("returns not_found when the fact does not exist", async () => {
720
- on(/\/points$/, (call) => {
721
- const b = call.body;
722
- if (call.method === "POST" && b.ids)
723
- return { result: [] };
724
- return { status: "ok" };
725
- });
726
- const result = await invoke("memory_mark_useful", { fact_id: "missing" });
727
- const parsed = JSON.parse(textOf(result));
728
- assert.equal(parsed.status, "not_found");
729
- assert.equal(parsed.fact_id, "missing");
730
- });
731
- });
732
- // ─────────────────────────────────────────────────────────────────────────
733
- // memory_report_outcome
734
- // ─────────────────────────────────────────────────────────────────────────
735
- describe("memory_report_outcome", () => {
736
- it("writes an outcome_event telemetry row carrying the outcome value", async () => {
737
- let upsertedPayload = null;
738
- on(/\/points$/, (call) => {
739
- const b = call.body;
740
- if (call.method === "POST" && b.ids) {
741
- return { result: [{ id: "fact-2", payload: { content: "y" } }] };
742
- }
743
- if (call.method === "PUT" && b.points) {
744
- upsertedPayload = b.points[0].payload;
745
- return { status: "ok" };
746
- }
747
- return { status: "ok" };
748
- });
749
- const result = await invoke("memory_report_outcome", {
750
- fact_id: "fact-2",
751
- outcome: "misleading",
752
- notes: "API moved in v2",
753
- });
754
- const parsed = JSON.parse(textOf(result));
755
- assert.equal(parsed.status, "outcome_recorded");
756
- assert.equal(parsed.outcome, "misleading");
757
- assert.ok(parsed.event_id);
758
- assert.ok(upsertedPayload);
759
- const tp = upsertedPayload;
760
- assert.equal(tp.kind, "telemetry");
761
- assert.equal(tp.memory_subtype, "outcome_event");
762
- assert.equal(tp.target_fact_id, "fact-2");
763
- assert.equal(tp.outcome, "misleading");
764
- // Negative outcomes are higher importance.
765
- assert.equal(tp.importance, 0.6);
766
- assert.match(String(tp.content), /API moved in v2/);
767
- });
768
- it("uses lower importance for non-negative outcomes", async () => {
769
- let upsertedPayload = null;
770
- on(/\/points$/, (call) => {
771
- const b = call.body;
772
- if (call.method === "POST" && b.ids) {
773
- return { result: [{ id: "fact-3", payload: { content: "z" } }] };
774
- }
775
- if (call.method === "PUT" && b.points) {
776
- upsertedPayload = b.points[0].payload;
777
- return { status: "ok" };
778
- }
779
- return { status: "ok" };
780
- });
781
- await invoke("memory_report_outcome", { fact_id: "fact-3", outcome: "useful" });
782
- const tp = upsertedPayload;
783
- assert.equal(tp.outcome, "useful");
784
- assert.equal(tp.importance, 0.3);
785
- });
786
- });
787
- // ─────────────────────────────────────────────────────────────────────────
788
- // memory_session_summary
789
- // ─────────────────────────────────────────────────────────────────────────
790
- describe("memory_session_summary", () => {
791
- it("inserts a summary point pinned to kind=summary, memory_subtype=session_index", async () => {
792
- let upsertedPayload = null;
793
- on(/\/points$/, (call) => {
794
- const b = call.body;
795
- if (call.method === "PUT" && b.points) {
796
- upsertedPayload = b.points[0].payload;
797
- return { status: "ok" };
798
- }
799
- return { status: "ok" };
800
- });
801
- const result = await invoke("memory_session_summary", {
802
- content: "Built taxonomy slim and added 4 feedback tools.",
803
- entities: ["Bikky", "Taxonomy"],
804
- episode_id: "ep-1",
805
- workstream_key: "ws-bikky",
806
- repo: "bikky-dev/bikky",
807
- });
808
- const parsed = JSON.parse(textOf(result));
809
- assert.equal(parsed.status, "summary_stored");
810
- assert.ok(parsed.summary_id);
811
- assert.ok(upsertedPayload);
812
- const p = upsertedPayload;
813
- assert.equal(p.kind, "summary");
814
- assert.equal(p.memory_subtype, "session_index");
815
- assert.equal(p.source, "agent");
816
- assert.equal(p.episode_id, "ep-1");
817
- assert.equal(p.workstream_key, "ws-bikky");
818
- assert.equal(p.repo, "bikky-dev/bikky");
819
- assert.deepEqual(p.entities, ["bikky", "taxonomy"]);
820
- });
821
- });
822
- // ─────────────────────────────────────────────────────────────────────────
823
- // memory_distill
824
- // ─────────────────────────────────────────────────────────────────────────
825
- describe("memory_distill", () => {
826
- it("inserts a distilled point pinned to kind=distilled, memory_subtype=convention", async () => {
827
- let upsertedPayload = null;
828
- on(/\/points$/, (call) => {
829
- const b = call.body;
830
- if (call.method === "PUT" && b.points) {
831
- upsertedPayload = b.points[0].payload;
832
- return { status: "ok" };
833
- }
834
- return { status: "ok" };
835
- });
836
- const result = await invoke("memory_distill", {
837
- content: "Always create the relevant Qdrant payload index before adding a new filter.",
838
- entities: ["Qdrant", "filter"],
839
- });
840
- const parsed = JSON.parse(textOf(result));
841
- assert.equal(parsed.status, "distilled_stored");
842
- assert.ok(parsed.distilled_id);
843
- assert.ok(upsertedPayload);
844
- const p = upsertedPayload;
845
- assert.equal(p.kind, "distilled");
846
- assert.equal(p.memory_subtype, "convention");
847
- assert.equal(p.source, "agent");
848
- assert.deepEqual(p.entities, ["qdrant", "filter"]);
849
- });
850
- it("supersedes a prior distilled fact when supersedes is provided", async () => {
851
- const setPayloadCalls = [];
852
- on(/\/points$/, (call) => {
853
- const b = call.body;
854
- if (call.method === "POST" && b.ids) {
855
- return { result: [{ id: "old-1", payload: { content: "old convention" } }] };
856
- }
857
- return { status: "ok" };
858
- });
859
- on("/points/payload", (call) => {
860
- setPayloadCalls.push(call.body);
861
- return { status: "ok" };
862
- });
863
- const result = await invoke("memory_distill", {
864
- content: "Refined convention.",
865
- entities: ["x"],
866
- supersedes: "old-1",
867
- });
868
- const parsed = JSON.parse(textOf(result));
869
- assert.equal(parsed.status, "distilled_stored");
870
- assert.equal(parsed.supersedes, "old-1");
871
- const supersedeCall = setPayloadCalls.find((b) => {
872
- const points = b.points;
873
- return Array.isArray(points) && points.includes("old-1");
874
- });
875
- assert.ok(supersedeCall, "expected a supersede payload update on old-1");
876
- const sp = supersedeCall.payload;
877
- assert.equal(sp.superseded_by, parsed.distilled_id);
878
- assert.ok(sp.superseded_at);
879
- });
880
- });
881
- });
882
- describe("resolveScope", () => {
883
- const savedEnv = process.env.BIKKY_WORKSPACE;
884
- before(() => {
885
- delete process.env.BIKKY_WORKSPACE;
886
- });
887
- after(() => {
888
- if (savedEnv === undefined)
889
- delete process.env.BIKKY_WORKSPACE;
890
- else
891
- process.env.BIKKY_WORKSPACE = savedEnv;
892
- });
893
- it("auto-enables includeLegacy when scope resolves to literal 'default'", () => {
894
- const scope = resolveScope("default");
895
- assert.equal(scope.workspaceId, "default");
896
- assert.equal(scope.includeLegacy, true);
897
- });
898
- it("keeps includeLegacy=false for any other named workspace", () => {
899
- const scope = resolveScope("agent00");
900
- assert.equal(scope.workspaceId, "agent00");
901
- assert.equal(scope.includeLegacy, false);
902
- });
903
- it("honours explicit includeLegacyWorkspace=true regardless of workspace name", () => {
904
- const scope = resolveScope("agent00", true);
905
- assert.equal(scope.includeLegacy, true);
906
- });
907
- });
908
- //# sourceMappingURL=tools.test.js.map