@fgv/ts-agent-memory 5.1.0-46 → 5.1.0-48

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 (336) hide show
  1. package/README.md +55 -0
  2. package/dist/packlets/ingest/orchestrator.js +115 -20
  3. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  4. package/dist/packlets/observe/memoryObservationStore.js +6 -0
  5. package/dist/packlets/observe/memoryObservationStore.js.map +1 -1
  6. package/dist/packlets/observe/observer.js.map +1 -1
  7. package/dist/packlets/store/fileTreeMemoryStore.js +63 -135
  8. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  9. package/dist/packlets/store/vectorMaintenance.js +251 -0
  10. package/dist/packlets/store/vectorMaintenance.js.map +1 -0
  11. package/dist/packlets/vector/inMemoryCosineIndex.js +81 -18
  12. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  13. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +7 -0
  14. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  15. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  16. package/dist/ts-agent-memory.d.ts +389 -71
  17. package/lib/packlets/ingest/orchestrator.d.ts +62 -5
  18. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  19. package/lib/packlets/ingest/orchestrator.js +115 -20
  20. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  21. package/lib/packlets/observe/memoryObservationStore.d.ts.map +1 -1
  22. package/lib/packlets/observe/memoryObservationStore.js +6 -0
  23. package/lib/packlets/observe/memoryObservationStore.js.map +1 -1
  24. package/lib/packlets/observe/observer.d.ts +61 -0
  25. package/lib/packlets/observe/observer.d.ts.map +1 -1
  26. package/lib/packlets/observe/observer.js.map +1 -1
  27. package/lib/packlets/store/fileTreeMemoryStore.d.ts +97 -60
  28. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  29. package/lib/packlets/store/fileTreeMemoryStore.js +63 -135
  30. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  31. package/lib/packlets/store/vectorMaintenance.d.ts +149 -0
  32. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -0
  33. package/lib/packlets/store/vectorMaintenance.js +255 -0
  34. package/lib/packlets/store/vectorMaintenance.js.map +1 -0
  35. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +30 -7
  36. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  37. package/lib/packlets/vector/inMemoryCosineIndex.js +80 -17
  38. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  39. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +7 -0
  40. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  41. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +7 -0
  42. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  43. package/lib/packlets/vector/vectorIndex.d.ts +129 -1
  44. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  45. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  46. package/package.json +17 -7
  47. package/.rush/temp/6e1299d1cebb080772163a532c221cd181b54f9f.tar.log +0 -462
  48. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +0 -9
  49. package/.rush/temp/operation/build/all.log +0 -9
  50. package/.rush/temp/operation/build/log-chunks.jsonl +0 -9
  51. package/.rush/temp/operation/build/state.json +0 -3
  52. package/.rush/temp/shrinkwrap-deps.json +0 -689
  53. package/config/api-extractor.json +0 -343
  54. package/config/jest.config.json +0 -14
  55. package/config/rig.json +0 -4
  56. package/dist/test/unit/converters/antagonistRoundTrip.test.js +0 -95
  57. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +0 -1
  58. package/dist/test/unit/converters/bodyConverterRegistry.test.js +0 -72
  59. package/dist/test/unit/converters/bodyConverterRegistry.test.js.map +0 -1
  60. package/dist/test/unit/converters/envelopeConverter.test.js +0 -313
  61. package/dist/test/unit/converters/envelopeConverter.test.js.map +0 -1
  62. package/dist/test/unit/index/memoryIndex.test.js +0 -214
  63. package/dist/test/unit/index/memoryIndex.test.js.map +0 -1
  64. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +0 -277
  65. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +0 -1
  66. package/dist/test/unit/ingest/cycleGuard.test.js +0 -81
  67. package/dist/test/unit/ingest/cycleGuard.test.js.map +0 -1
  68. package/dist/test/unit/ingest/orchestrator.test.js +0 -1055
  69. package/dist/test/unit/ingest/orchestrator.test.js.map +0 -1
  70. package/dist/test/unit/observe/memoryObservationStore.test.js +0 -118
  71. package/dist/test/unit/observe/memoryObservationStore.test.js.map +0 -1
  72. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js +0 -116
  73. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +0 -1
  74. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +0 -257
  75. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +0 -1
  76. package/dist/test/unit/retrieve/retrievers.test.js +0 -768
  77. package/dist/test/unit/retrieve/retrievers.test.js.map +0 -1
  78. package/dist/test/unit/retrieve/temporalRetrievers.test.js +0 -182
  79. package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +0 -1
  80. package/dist/test/unit/store/antagonistTemporalBoundary.test.js +0 -120
  81. package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +0 -1
  82. package/dist/test/unit/store/embedOnWrite.test.js +0 -317
  83. package/dist/test/unit/store/embedOnWrite.test.js.map +0 -1
  84. package/dist/test/unit/store/fileTreeMemoryStore.test.js +0 -778
  85. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +0 -1
  86. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js +0 -255
  87. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js.map +0 -1
  88. package/dist/test/unit/store/indexInjection.test.js +0 -432
  89. package/dist/test/unit/store/indexInjection.test.js.map +0 -1
  90. package/dist/test/unit/store/lenientOpen.test.js +0 -248
  91. package/dist/test/unit/store/lenientOpen.test.js.map +0 -1
  92. package/dist/test/unit/store/listScoped.test.js +0 -109
  93. package/dist/test/unit/store/listScoped.test.js.map +0 -1
  94. package/dist/test/unit/store/observations.test.js +0 -239
  95. package/dist/test/unit/store/observations.test.js.map +0 -1
  96. package/dist/test/unit/store/rankAxis.test.js +0 -254
  97. package/dist/test/unit/store/rankAxis.test.js.map +0 -1
  98. package/dist/test/unit/store/scopeEncoding.test.js +0 -24
  99. package/dist/test/unit/store/scopeEncoding.test.js.map +0 -1
  100. package/dist/test/unit/store/temporalStore.test.js +0 -398
  101. package/dist/test/unit/store/temporalStore.test.js.map +0 -1
  102. package/dist/test/unit/tools/memoryTools.test.js +0 -841
  103. package/dist/test/unit/tools/memoryTools.test.js.map +0 -1
  104. package/dist/test/unit/types/identityCodec.test.js +0 -187
  105. package/dist/test/unit/types/identityCodec.test.js.map +0 -1
  106. package/dist/test/unit/types/ids.test.js +0 -84
  107. package/dist/test/unit/types/ids.test.js.map +0 -1
  108. package/dist/test/unit/types/temporalCodec.test.js +0 -203
  109. package/dist/test/unit/types/temporalCodec.test.js.map +0 -1
  110. package/dist/test/unit/types/temporalPolicy.test.js +0 -83
  111. package/dist/test/unit/types/temporalPolicy.test.js.map +0 -1
  112. package/dist/test/unit/types/writePolicy.test.js +0 -331
  113. package/dist/test/unit/types/writePolicy.test.js.map +0 -1
  114. package/dist/test/unit/vector/fragmentConverters.test.js +0 -58
  115. package/dist/test/unit/vector/fragmentConverters.test.js.map +0 -1
  116. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +0 -252
  117. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +0 -1
  118. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +0 -362
  119. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +0 -1
  120. package/dist/test/unit/vector/vectorIndex.test.js +0 -51
  121. package/dist/test/unit/vector/vectorIndex.test.js.map +0 -1
  122. package/eslint.config.js +0 -15
  123. package/etc/ts-agent-memory.api.md +0 -938
  124. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +0 -10
  125. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +0 -1
  126. package/lib/test/unit/converters/antagonistRoundTrip.test.js +0 -97
  127. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +0 -1
  128. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts +0 -2
  129. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts.map +0 -1
  130. package/lib/test/unit/converters/bodyConverterRegistry.test.js +0 -74
  131. package/lib/test/unit/converters/bodyConverterRegistry.test.js.map +0 -1
  132. package/lib/test/unit/converters/envelopeConverter.test.d.ts +0 -2
  133. package/lib/test/unit/converters/envelopeConverter.test.d.ts.map +0 -1
  134. package/lib/test/unit/converters/envelopeConverter.test.js +0 -315
  135. package/lib/test/unit/converters/envelopeConverter.test.js.map +0 -1
  136. package/lib/test/unit/index/memoryIndex.test.d.ts +0 -2
  137. package/lib/test/unit/index/memoryIndex.test.d.ts.map +0 -1
  138. package/lib/test/unit/index/memoryIndex.test.js +0 -216
  139. package/lib/test/unit/index/memoryIndex.test.js.map +0 -1
  140. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +0 -9
  141. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +0 -1
  142. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +0 -279
  143. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +0 -1
  144. package/lib/test/unit/ingest/cycleGuard.test.d.ts +0 -2
  145. package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +0 -1
  146. package/lib/test/unit/ingest/cycleGuard.test.js +0 -83
  147. package/lib/test/unit/ingest/cycleGuard.test.js.map +0 -1
  148. package/lib/test/unit/ingest/orchestrator.test.d.ts +0 -2
  149. package/lib/test/unit/ingest/orchestrator.test.d.ts.map +0 -1
  150. package/lib/test/unit/ingest/orchestrator.test.js +0 -1057
  151. package/lib/test/unit/ingest/orchestrator.test.js.map +0 -1
  152. package/lib/test/unit/observe/memoryObservationStore.test.d.ts +0 -2
  153. package/lib/test/unit/observe/memoryObservationStore.test.d.ts.map +0 -1
  154. package/lib/test/unit/observe/memoryObservationStore.test.js +0 -120
  155. package/lib/test/unit/observe/memoryObservationStore.test.js.map +0 -1
  156. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts +0 -2
  157. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts.map +0 -1
  158. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js +0 -118
  159. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +0 -1
  160. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts +0 -2
  161. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts.map +0 -1
  162. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +0 -259
  163. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +0 -1
  164. package/lib/test/unit/retrieve/retrievers.test.d.ts +0 -2
  165. package/lib/test/unit/retrieve/retrievers.test.d.ts.map +0 -1
  166. package/lib/test/unit/retrieve/retrievers.test.js +0 -770
  167. package/lib/test/unit/retrieve/retrievers.test.js.map +0 -1
  168. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +0 -2
  169. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +0 -1
  170. package/lib/test/unit/retrieve/temporalRetrievers.test.js +0 -184
  171. package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +0 -1
  172. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +0 -9
  173. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +0 -1
  174. package/lib/test/unit/store/antagonistTemporalBoundary.test.js +0 -122
  175. package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +0 -1
  176. package/lib/test/unit/store/embedOnWrite.test.d.ts +0 -2
  177. package/lib/test/unit/store/embedOnWrite.test.d.ts.map +0 -1
  178. package/lib/test/unit/store/embedOnWrite.test.js +0 -319
  179. package/lib/test/unit/store/embedOnWrite.test.js.map +0 -1
  180. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts +0 -2
  181. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map +0 -1
  182. package/lib/test/unit/store/fileTreeMemoryStore.test.js +0 -780
  183. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +0 -1
  184. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts +0 -2
  185. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map +0 -1
  186. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js +0 -257
  187. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js.map +0 -1
  188. package/lib/test/unit/store/indexInjection.test.d.ts +0 -2
  189. package/lib/test/unit/store/indexInjection.test.d.ts.map +0 -1
  190. package/lib/test/unit/store/indexInjection.test.js +0 -434
  191. package/lib/test/unit/store/indexInjection.test.js.map +0 -1
  192. package/lib/test/unit/store/lenientOpen.test.d.ts +0 -2
  193. package/lib/test/unit/store/lenientOpen.test.d.ts.map +0 -1
  194. package/lib/test/unit/store/lenientOpen.test.js +0 -250
  195. package/lib/test/unit/store/lenientOpen.test.js.map +0 -1
  196. package/lib/test/unit/store/listScoped.test.d.ts +0 -2
  197. package/lib/test/unit/store/listScoped.test.d.ts.map +0 -1
  198. package/lib/test/unit/store/listScoped.test.js +0 -111
  199. package/lib/test/unit/store/listScoped.test.js.map +0 -1
  200. package/lib/test/unit/store/observations.test.d.ts +0 -2
  201. package/lib/test/unit/store/observations.test.d.ts.map +0 -1
  202. package/lib/test/unit/store/observations.test.js +0 -241
  203. package/lib/test/unit/store/observations.test.js.map +0 -1
  204. package/lib/test/unit/store/rankAxis.test.d.ts +0 -2
  205. package/lib/test/unit/store/rankAxis.test.d.ts.map +0 -1
  206. package/lib/test/unit/store/rankAxis.test.js +0 -256
  207. package/lib/test/unit/store/rankAxis.test.js.map +0 -1
  208. package/lib/test/unit/store/scopeEncoding.test.d.ts +0 -2
  209. package/lib/test/unit/store/scopeEncoding.test.d.ts.map +0 -1
  210. package/lib/test/unit/store/scopeEncoding.test.js +0 -26
  211. package/lib/test/unit/store/scopeEncoding.test.js.map +0 -1
  212. package/lib/test/unit/store/temporalStore.test.d.ts +0 -2
  213. package/lib/test/unit/store/temporalStore.test.d.ts.map +0 -1
  214. package/lib/test/unit/store/temporalStore.test.js +0 -400
  215. package/lib/test/unit/store/temporalStore.test.js.map +0 -1
  216. package/lib/test/unit/tools/memoryTools.test.d.ts +0 -2
  217. package/lib/test/unit/tools/memoryTools.test.d.ts.map +0 -1
  218. package/lib/test/unit/tools/memoryTools.test.js +0 -843
  219. package/lib/test/unit/tools/memoryTools.test.js.map +0 -1
  220. package/lib/test/unit/types/identityCodec.test.d.ts +0 -2
  221. package/lib/test/unit/types/identityCodec.test.d.ts.map +0 -1
  222. package/lib/test/unit/types/identityCodec.test.js +0 -189
  223. package/lib/test/unit/types/identityCodec.test.js.map +0 -1
  224. package/lib/test/unit/types/ids.test.d.ts +0 -2
  225. package/lib/test/unit/types/ids.test.d.ts.map +0 -1
  226. package/lib/test/unit/types/ids.test.js +0 -86
  227. package/lib/test/unit/types/ids.test.js.map +0 -1
  228. package/lib/test/unit/types/temporalCodec.test.d.ts +0 -2
  229. package/lib/test/unit/types/temporalCodec.test.d.ts.map +0 -1
  230. package/lib/test/unit/types/temporalCodec.test.js +0 -205
  231. package/lib/test/unit/types/temporalCodec.test.js.map +0 -1
  232. package/lib/test/unit/types/temporalPolicy.test.d.ts +0 -2
  233. package/lib/test/unit/types/temporalPolicy.test.d.ts.map +0 -1
  234. package/lib/test/unit/types/temporalPolicy.test.js +0 -85
  235. package/lib/test/unit/types/temporalPolicy.test.js.map +0 -1
  236. package/lib/test/unit/types/writePolicy.test.d.ts +0 -2
  237. package/lib/test/unit/types/writePolicy.test.d.ts.map +0 -1
  238. package/lib/test/unit/types/writePolicy.test.js +0 -333
  239. package/lib/test/unit/types/writePolicy.test.js.map +0 -1
  240. package/lib/test/unit/vector/fragmentConverters.test.d.ts +0 -2
  241. package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +0 -1
  242. package/lib/test/unit/vector/fragmentConverters.test.js +0 -60
  243. package/lib/test/unit/vector/fragmentConverters.test.js.map +0 -1
  244. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts +0 -2
  245. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map +0 -1
  246. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +0 -254
  247. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +0 -1
  248. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts +0 -2
  249. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts.map +0 -1
  250. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +0 -364
  251. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +0 -1
  252. package/lib/test/unit/vector/vectorIndex.test.d.ts +0 -2
  253. package/lib/test/unit/vector/vectorIndex.test.d.ts.map +0 -1
  254. package/lib/test/unit/vector/vectorIndex.test.js +0 -53
  255. package/lib/test/unit/vector/vectorIndex.test.js.map +0 -1
  256. package/rush-logs/ts-agent-memory.build.cache.log +0 -3
  257. package/rush-logs/ts-agent-memory.build.log +0 -9
  258. package/src/index.ts +0 -14
  259. package/src/packlets/converters/bodyConverterRegistry.ts +0 -105
  260. package/src/packlets/converters/envelopeConverter.ts +0 -233
  261. package/src/packlets/converters/index.ts +0 -7
  262. package/src/packlets/index/index.ts +0 -6
  263. package/src/packlets/index/memoryIndex.ts +0 -332
  264. package/src/packlets/ingest/cycleGuard.ts +0 -153
  265. package/src/packlets/ingest/hostStages.ts +0 -111
  266. package/src/packlets/ingest/index.ts +0 -9
  267. package/src/packlets/ingest/model.ts +0 -195
  268. package/src/packlets/ingest/orchestrator.ts +0 -873
  269. package/src/packlets/observe/index.ts +0 -7
  270. package/src/packlets/observe/memoryObservationStore.ts +0 -153
  271. package/src/packlets/observe/observer.ts +0 -119
  272. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +0 -138
  273. package/src/packlets/retrieve/hybridRetriever.ts +0 -194
  274. package/src/packlets/retrieve/index.ts +0 -14
  275. package/src/packlets/retrieve/linkTraversalRetriever.ts +0 -158
  276. package/src/packlets/retrieve/recencyRetriever.ts +0 -54
  277. package/src/packlets/retrieve/retriever.ts +0 -291
  278. package/src/packlets/retrieve/semanticRetriever.ts +0 -152
  279. package/src/packlets/retrieve/structuredFilterRetriever.ts +0 -58
  280. package/src/packlets/retrieve/tagRetriever.ts +0 -58
  281. package/src/packlets/retrieve/temporalRetrievers.ts +0 -210
  282. package/src/packlets/store/fileTreeMemoryStore.ts +0 -1834
  283. package/src/packlets/store/index.ts +0 -7
  284. package/src/packlets/store/scopeEncoding.ts +0 -36
  285. package/src/packlets/tools/index.ts +0 -6
  286. package/src/packlets/tools/memoryTools.ts +0 -706
  287. package/src/packlets/types/envelope.ts +0 -196
  288. package/src/packlets/types/filenameSafety.ts +0 -57
  289. package/src/packlets/types/identityCodec.ts +0 -447
  290. package/src/packlets/types/ids.ts +0 -124
  291. package/src/packlets/types/index.ts +0 -11
  292. package/src/packlets/types/temporal.ts +0 -96
  293. package/src/packlets/types/writePolicy.ts +0 -612
  294. package/src/packlets/vector/fragmentConverters.ts +0 -55
  295. package/src/packlets/vector/inMemoryCosineIndex.ts +0 -196
  296. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +0 -289
  297. package/src/packlets/vector/index.ts +0 -9
  298. package/src/packlets/vector/vectorIndex.ts +0 -273
  299. package/src/test/unit/converters/antagonistRoundTrip.test.ts +0 -110
  300. package/src/test/unit/converters/bodyConverterRegistry.test.ts +0 -89
  301. package/src/test/unit/converters/envelopeConverter.test.ts +0 -418
  302. package/src/test/unit/index/memoryIndex.test.ts +0 -272
  303. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +0 -367
  304. package/src/test/unit/ingest/cycleGuard.test.ts +0 -110
  305. package/src/test/unit/ingest/orchestrator.test.ts +0 -1351
  306. package/src/test/unit/observe/memoryObservationStore.test.ts +0 -158
  307. package/src/test/unit/retrieve/fragmentSemanticRetriever.test.ts +0 -163
  308. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +0 -329
  309. package/src/test/unit/retrieve/retrievers.test.ts +0 -1018
  310. package/src/test/unit/retrieve/temporalRetrievers.test.ts +0 -226
  311. package/src/test/unit/store/antagonistTemporalBoundary.test.ts +0 -158
  312. package/src/test/unit/store/embedOnWrite.test.ts +0 -418
  313. package/src/test/unit/store/fileTreeMemoryStore.test.ts +0 -1041
  314. package/src/test/unit/store/fragmentEmbedOnWrite.test.ts +0 -349
  315. package/src/test/unit/store/indexInjection.test.ts +0 -550
  316. package/src/test/unit/store/lenientOpen.test.ts +0 -292
  317. package/src/test/unit/store/listScoped.test.ts +0 -138
  318. package/src/test/unit/store/observations.test.ts +0 -290
  319. package/src/test/unit/store/rankAxis.test.ts +0 -349
  320. package/src/test/unit/store/scopeEncoding.test.ts +0 -37
  321. package/src/test/unit/store/temporalStore.test.ts +0 -469
  322. package/src/test/unit/tools/memoryTools.test.ts +0 -1120
  323. package/src/test/unit/types/identityCodec.test.ts +0 -266
  324. package/src/test/unit/types/ids.test.ts +0 -94
  325. package/src/test/unit/types/temporalCodec.test.ts +0 -259
  326. package/src/test/unit/types/temporalPolicy.test.ts +0 -124
  327. package/src/test/unit/types/writePolicy.test.ts +0 -446
  328. package/src/test/unit/vector/fragmentConverters.test.ts +0 -80
  329. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +0 -318
  330. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +0 -477
  331. package/src/test/unit/vector/vectorIndex.test.ts +0 -64
  332. package/temp/build/lint/_eslint-5eVG3S6w.json +0 -302
  333. package/temp/build/typescript/ts_8nwakTlr.json +0 -1
  334. package/temp/ts-agent-memory.api.json +0 -20933
  335. package/temp/ts-agent-memory.api.md +0 -938
  336. package/tsconfig.json +0 -8
@@ -1,1351 +0,0 @@
1
- /*
2
- * Copyright (c) 2026 Erik Fortune
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import '@fgv/ts-utils-jest';
7
- import { Converters, Logging, Result, fail, succeed } from '@fgv/ts-utils';
8
- import { FileTree } from '@fgv/ts-json-base';
9
- import {
10
- BodyConverterRegistry,
11
- CONTRADICTS_LINK_TYPE,
12
- DEFAULT_SIMILARITY_THRESHOLD,
13
- DEFAULT_SIMILARITY_TOP_K,
14
- EntityId,
15
- FileTreeMemoryStore,
16
- HOST_INGEST_PROVENANCE_SOURCE,
17
- IBodyConverterRegistry,
18
- ICandidateEdge,
19
- ICandidateRecord,
20
- IEdgeTarget,
21
- IEntityResolutionCandidate,
22
- IEntityResolver,
23
- IFactExtractor,
24
- IIdentityCodec,
25
- IIngestItem,
26
- IIngestItemResult,
27
- InMemoryCosineIndex,
28
- IMemoryClassification,
29
- IMemoryClassifier,
30
- IMemoryEnvelope,
31
- IMemoryRecord,
32
- IMemoryRecordSource,
33
- IMemoryStore,
34
- IProvenance,
35
- IScopedMemoryRecord,
36
- IRelationContext,
37
- IRelationExtractor,
38
- IVectorIndex,
39
- IVectorQueryHit,
40
- IWritePolicy,
41
- Kind,
42
- KnowledgeIdentityCodec,
43
- LinkType,
44
- LtmIdentityCodec,
45
- MemoryEmbedder,
46
- MtmIdentityCodec,
47
- MemoryId,
48
- MemoryIngestOrchestrator,
49
- MemoryScopeKey,
50
- ResolutionVerdict,
51
- Tag,
52
- TemporalIdentityCodec,
53
- TemporalVersionedPolicy,
54
- serializeMemoryFile
55
- } from '../../../index';
56
-
57
- // --- kinds --------------------------------------------------------------------
58
- const noteKind: Kind = 'note' as Kind;
59
- const factKind: Kind = 'fact' as Kind;
60
- const numKind: Kind = 'num' as Kind;
61
- const orphanKind: Kind = 'orphan' as Kind;
62
- const ltmKind: Kind = 'ltm' as Kind;
63
-
64
- // --- shared fixtures ----------------------------------------------------------
65
- let clockValue: number;
66
- const clock = (): number => clockValue;
67
-
68
- function mutableRoot(
69
- files: ReadonlyArray<{ path: string; contents: string }> = []
70
- ): FileTree.IMutableFileTreeDirectoryItem {
71
- const tree = FileTree.inMemory([...files], { mutable: true }).orThrow();
72
- const root = tree.getDirectory('/').orThrow();
73
- if (!FileTree.isMutableDirectoryItem(root)) {
74
- throw new Error('expected a mutable root directory');
75
- }
76
- return root;
77
- }
78
-
79
- function registry(): IBodyConverterRegistry {
80
- const reg = BodyConverterRegistry.create().orThrow();
81
- reg.register(noteKind, Converters.string);
82
- reg.register(factKind, Converters.string);
83
- reg.register(numKind, Converters.number);
84
- reg.register(orphanKind, Converters.string);
85
- reg.register(ltmKind, Converters.string);
86
- return reg;
87
- }
88
-
89
- const codecs: ReadonlyMap<Kind, IIdentityCodec> = new Map<Kind, IIdentityCodec>([
90
- [noteKind, new KnowledgeIdentityCodec()],
91
- [numKind, new KnowledgeIdentityCodec()],
92
- [ltmKind, new LtmIdentityCodec()],
93
- [factKind, TemporalIdentityCodec.create('facts').orThrow()]
94
- ]);
95
-
96
- const policies: ReadonlyMap<Kind, IWritePolicy> = new Map<Kind, IWritePolicy>([
97
- [factKind, TemporalVersionedPolicy.create().orThrow()]
98
- ]);
99
-
100
- // A deterministic embedder keyed by body text (three-dimensional vectors so
101
- // cosine relationships are controllable). Unknown bodies map to a distinct axis.
102
- const VECTORS: ReadonlyMap<string, ReadonlyArray<number>> = new Map<string, ReadonlyArray<number>>([
103
- ['apple pie', [1, 0, 0]],
104
- ['apple tart', [0.99, 0.14, 0]],
105
- ['banana bread', [0, 1, 0]]
106
- ]);
107
- const embed: MemoryEmbedder = (record) => {
108
- if (record.body === 'boom') {
109
- return Promise.resolve(fail('embed boom'));
110
- }
111
- const vec: ReadonlyArray<number> = VECTORS.get(record.body as string) ?? [0, 0, 1];
112
- return Promise.resolve(succeed(Float32Array.from(vec)));
113
- };
114
-
115
- // --- mock host stages ---------------------------------------------------------
116
- function classifier(fn: (item: IIngestItem) => Result<IMemoryClassification>): IMemoryClassifier {
117
- return { classify: (item) => Promise.resolve(fn(item)) };
118
- }
119
- function throwingClassifier(): IMemoryClassifier {
120
- return {
121
- classify: () => {
122
- throw new Error('classifier boom');
123
- }
124
- };
125
- }
126
- function extractor(
127
- fn: (item: IIngestItem, c: IMemoryClassification) => Result<ReadonlyArray<ICandidateRecord>>
128
- ): IFactExtractor {
129
- return { extract: (item, c) => Promise.resolve(fn(item, c)) };
130
- }
131
- function resolver(
132
- fn: (
133
- candidate: ICandidateRecord,
134
- similar: ReadonlyArray<IEntityResolutionCandidate>
135
- ) => Result<ResolutionVerdict>
136
- ): IEntityResolver {
137
- return { resolve: (candidate, similar) => Promise.resolve(fn(candidate, similar)) };
138
- }
139
- function relater(fn: (ctx: IRelationContext) => Result<ReadonlyArray<ICandidateEdge>>): IRelationExtractor {
140
- return { relate: (ctx) => Promise.resolve(fn(ctx)) };
141
- }
142
-
143
- const noEdges: IRelationExtractor = relater(() => succeed([]));
144
- const classifyNote: IMemoryClassifier = classifier(() => succeed({ kind: noteKind, confidence: 0.9 }));
145
-
146
- /** Extractor that emits one note candidate per item, body = item.content, entityId = item.id. */
147
- function oneNoteExtractor(): IFactExtractor {
148
- return extractor((item) => succeed([candidate(noteKind, item.id, item.content as string)]));
149
- }
150
-
151
- function candidate(
152
- kind: Kind,
153
- entityId: string,
154
- body: unknown,
155
- opts: {
156
- readonly tags?: ReadonlyArray<string>;
157
- readonly links?: ICandidateRecord['envelope']['links'];
158
- readonly provenance?: IProvenance;
159
- readonly temporal?: IMemoryEnvelope['temporal'];
160
- } = {}
161
- ): ICandidateRecord {
162
- return {
163
- envelope: {
164
- entityId: entityId as EntityId,
165
- kind,
166
- tags: (opts.tags ?? []) as ReadonlyArray<Tag>,
167
- links: opts.links ?? [],
168
- provenance: opts.provenance ?? { source: 'agent' },
169
- ...(opts.temporal !== undefined ? { temporal: opts.temporal } : {})
170
- },
171
- body
172
- };
173
- }
174
-
175
- /**
176
- * A scope-qualified target, authored as `scope#id` or a bare id defaulting to the
177
- * knowledge scope (where the note/num fixtures live). `kt` is the assertion-side
178
- * spelling of the same helper.
179
- */
180
- function scopedTarget(spec: string): IEdgeTarget {
181
- const hash: number = spec.indexOf('#');
182
- const scope: string = hash >= 0 ? spec.slice(0, hash) : 'knowledge';
183
- const id: string = hash >= 0 ? spec.slice(hash + 1) : spec;
184
- return { scope: scope as MemoryScopeKey, id: id as MemoryId };
185
- }
186
- function kt(id: string, scope: string = 'knowledge'): IEdgeTarget {
187
- return { scope: scope as MemoryScopeKey, id: id as MemoryId };
188
- }
189
-
190
- /** The scope-qualified reference of a candidate about to be written, by its entityId. */
191
- function srcOf(ctx: IRelationContext, entityId: string): IEdgeTarget {
192
- const found = ctx.candidates.find((rc) => rc.candidate.envelope.entityId === (entityId as EntityId));
193
- if (found === undefined) {
194
- throw new Error(`test relater: no candidate with entityId '${entityId}'`);
195
- }
196
- return found.id;
197
- }
198
-
199
- function candEdge(source: string, type: string, target: string, confidence?: number): ICandidateEdge {
200
- return {
201
- source: scopedTarget(source),
202
- edge: {
203
- type: type as LinkType,
204
- target: scopedTarget(target),
205
- ...(confidence !== undefined ? { confidence } : {})
206
- }
207
- };
208
- }
209
-
210
- interface IBuildParams {
211
- readonly files?: ReadonlyArray<{ path: string; contents: string }>;
212
- readonly registry?: IBodyConverterRegistry;
213
- readonly vectorIndex?: IVectorIndex;
214
- readonly embed?: MemoryEmbedder;
215
- }
216
-
217
- function buildStore(params: IBuildParams = {}): FileTreeMemoryStore {
218
- return FileTreeMemoryStore.create({
219
- root: mutableRoot(params.files),
220
- registry: params.registry ?? registry(),
221
- codecs,
222
- writePolicies: policies,
223
- clock,
224
- vectorIndex: params.vectorIndex,
225
- embed: params.embed
226
- }).orThrow();
227
- }
228
-
229
- interface IOrchestratorParams {
230
- readonly store: IMemoryStore;
231
- readonly registry?: IBodyConverterRegistry;
232
- readonly classifier?: IMemoryClassifier;
233
- readonly extractor?: IFactExtractor;
234
- readonly relationExtractor?: IRelationExtractor;
235
- readonly entityResolver?: IEntityResolver;
236
- readonly vectorIndex?: IVectorIndex;
237
- readonly embed?: MemoryEmbedder;
238
- readonly similarityThreshold?: number;
239
- readonly cycleGuard?: 'reject' | 'off';
240
- }
241
-
242
- function buildOrchestrator(params: IOrchestratorParams): MemoryIngestOrchestrator {
243
- return MemoryIngestOrchestrator.create({
244
- store: params.store,
245
- registry: params.registry ?? registry(),
246
- codecs,
247
- classifier: params.classifier ?? classifyNote,
248
- extractor: params.extractor ?? oneNoteExtractor(),
249
- relationExtractor: params.relationExtractor ?? noEdges,
250
- entityResolver: params.entityResolver,
251
- vectorIndex: params.vectorIndex,
252
- embed: params.embed,
253
- similarityThreshold: params.similarityThreshold,
254
- cycleGuard: params.cycleGuard
255
- }).orThrow();
256
- }
257
-
258
- /** A seeded note file with only `source` provenance (the additive-provenance baseline). */
259
- function seedNote(
260
- id: string,
261
- body: string,
262
- provenance: Record<string, unknown> = { source: 'agent' }
263
- ): {
264
- path: string;
265
- contents: string;
266
- } {
267
- const envelope: IMemoryEnvelope = {
268
- id: id as MemoryId,
269
- entityId: id as EntityId,
270
- kind: noteKind,
271
- tags: [],
272
- links: [],
273
- created: 1,
274
- updated: 1,
275
- seq: 4,
276
- contentHash: 'seed',
277
- provenance: provenance as unknown as IProvenance
278
- };
279
- return { path: `knowledge/${id}.md`, contents: serializeMemoryFile(envelope, body).orThrow() };
280
- }
281
-
282
- beforeEach(() => {
283
- clockValue = 1000;
284
- });
285
-
286
- describe('MemoryIngestOrchestrator', () => {
287
- describe('create', () => {
288
- test('constructs via the factory', () => {
289
- expect(
290
- MemoryIngestOrchestrator.create({
291
- store: buildStore(),
292
- registry: registry(),
293
- codecs,
294
- classifier: classifyNote,
295
- extractor: oneNoteExtractor(),
296
- relationExtractor: noEdges
297
- })
298
- ).toSucceed();
299
- });
300
-
301
- test('exposes the documented similarity defaults', () => {
302
- expect(DEFAULT_SIMILARITY_THRESHOLD).toBe(0.85);
303
- expect(DEFAULT_SIMILARITY_TOP_K).toBe(5);
304
- });
305
- });
306
-
307
- describe('single-item ingest (first-class path)', () => {
308
- test('classifies, extracts, and writes one record end-to-end with host-ingest provenance + derivedFrom', async () => {
309
- const store = buildStore();
310
- const orch = buildOrchestrator({ store });
311
- const sourceId: IEdgeTarget = {
312
- scope: 'conversations/c1' as MemoryScopeKey,
313
- id: 'turn-3' as MemoryId
314
- };
315
- const result = await orch.ingestItem({ id: 'doc-1', content: 'the sky is blue', sourceId });
316
- expect(result).toSucceedAndSatisfy((r: IIngestItemResult) => {
317
- expect(r.item.id).toBe('doc-1');
318
- expect(r.records).toHaveLength(1);
319
- const rec = r.records[0];
320
- expect(rec.disposition).toBe('written');
321
- expect(rec.resolution.verdict).toBe('new');
322
- expect(rec.id).toBe('doc-1');
323
- expect(rec.record?.envelope.provenance.source).toBe(HOST_INGEST_PROVENANCE_SOURCE);
324
- expect(rec.record?.envelope.provenance.derivedFrom).toEqual(sourceId);
325
- expect(rec.record?.body).toBe('the sky is blue');
326
- expect(rec.interlock).toBeUndefined();
327
- });
328
- expect(await store.get(noteKind, 'doc-1' as EntityId)).toSucceedAndSatisfy(
329
- (rec: IMemoryRecord<unknown> | undefined) => {
330
- expect(rec?.body).toBe('the sky is blue');
331
- }
332
- );
333
- });
334
-
335
- test('omits derivedFrom when the item carries no sourceId', async () => {
336
- const store = buildStore();
337
- const orch = buildOrchestrator({ store });
338
- const result = await orch.ingestItem({ id: 'doc-1', content: 'a fact' });
339
- expect(result).toSucceedAndSatisfy((r: IIngestItemResult) => {
340
- expect(r.records[0].record?.envelope.provenance.derivedFrom).toBeUndefined();
341
- expect(r.records[0].record?.envelope.provenance.source).toBe(HOST_INGEST_PROVENANCE_SOURCE);
342
- });
343
- });
344
-
345
- test('yields an empty record set when the extractor finds nothing memorable', async () => {
346
- const store = buildStore();
347
- const orch = buildOrchestrator({ store, extractor: extractor(() => succeed([])) });
348
- expect(await orch.ingestItem({ id: 'doc-1', content: 'noise' })).toSucceedAndSatisfy(
349
- (r: IIngestItemResult) => {
350
- expect(r.records).toEqual([]);
351
- }
352
- );
353
- });
354
- });
355
-
356
- describe('host-stage failures abort loudly', () => {
357
- test('classifier failure', async () => {
358
- const orch = buildOrchestrator({
359
- store: buildStore(),
360
- classifier: classifier(() => fail('cannot classify'))
361
- });
362
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(/cannot classify/);
363
- });
364
-
365
- test('classifier throw is captured as a failure', async () => {
366
- const orch = buildOrchestrator({ store: buildStore(), classifier: throwingClassifier() });
367
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(
368
- /classify threw.*classifier boom/
369
- );
370
- });
371
-
372
- test('extractor failure', async () => {
373
- const orch = buildOrchestrator({
374
- store: buildStore(),
375
- extractor: extractor(() => fail('extract error'))
376
- });
377
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(/extract error/);
378
- });
379
-
380
- test('relation-extractor failure', async () => {
381
- const orch = buildOrchestrator({
382
- store: buildStore(),
383
- relationExtractor: relater(() => fail('relate error'))
384
- });
385
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(/relate error/);
386
- });
387
- });
388
-
389
- describe('stage 3b — typed validation boundary', () => {
390
- test('rejects a candidate whose body fails the kind converter', async () => {
391
- const orch = buildOrchestrator({
392
- store: buildStore(),
393
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-1', 42)]))
394
- });
395
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(
396
- /candidate body for kind 'note' is invalid/
397
- );
398
- });
399
-
400
- test('rejects a non-string body even when the converter accepts it', async () => {
401
- const orch = buildOrchestrator({
402
- store: buildStore(),
403
- extractor: extractor(() => succeed([candidate(numKind, 'n-1', 42)]))
404
- });
405
- expect(await orch.ingestItem({ id: 'n-1', content: 'x' })).toFailWith(/must be a string/);
406
- });
407
-
408
- test('fails when no identity codec is registered for the candidate kind', async () => {
409
- const orch = buildOrchestrator({
410
- store: buildStore(),
411
- extractor: extractor(() => succeed([candidate(orphanKind, 'o-1', 'body')]))
412
- });
413
- expect(await orch.ingestItem({ id: 'o-1', content: 'x' })).toFailWith(/no identity codec/);
414
- });
415
-
416
- test('fails loudly when a pre-existing snapshot record cannot be scoped (no codec for its kind)', async () => {
417
- // The store can persist a note (it has the note codec), but the orchestrator
418
- // below is wired WITHOUT a note codec — so when it snapshots the store to run
419
- // the edge path, it cannot resolve the seeded note's scope and fails loudly
420
- // (never silently drops it from the cycle graph). This is the branch the
421
- // scope-qualified edge change added.
422
- const store = buildStore();
423
- await putNote(store, 'legacy', 'anchor');
424
- const orch = MemoryIngestOrchestrator.create({
425
- store,
426
- registry: registry(),
427
- // Deliberately omit the note codec (and any default) that the seeded record needs.
428
- codecs: new Map<Kind, IIdentityCodec>([[numKind, new KnowledgeIdentityCodec()]]),
429
- classifier: classifier(() => succeed({ kind: numKind })),
430
- extractor: extractor(() => succeed([candidate(numKind, 'n-1', 7)])),
431
- relationExtractor: noEdges
432
- }).orThrow();
433
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
434
- /cannot resolve scope for stored record 'legacy'.*no identity codec registered for kind 'note'/i
435
- );
436
- });
437
- });
438
-
439
- describe('stage 4 — exact dedup (layer 1)', () => {
440
- test('an exact {kind,body} match in scope is duplicate-of (no resolver)', async () => {
441
- const store = buildStore({ files: [seedNote('doc-a', 'hello')] });
442
- const orch = buildOrchestrator({
443
- store,
444
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'hello')]))
445
- });
446
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
447
- expect(r.records[0].disposition).toBe('deduped');
448
- expect(r.records[0].resolution).toEqual({ verdict: 'duplicate-of', target: kt('doc-a') });
449
- expect(r.records[0].id).toBe('doc-a');
450
- });
451
- // doc-b was never written.
452
- expect(await store.get(noteKind, 'doc-b' as EntityId)).toSucceedWith(undefined);
453
- });
454
-
455
- test('exact dedup takes precedence over the resolver (resolver never called)', async () => {
456
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
457
- const store = buildStore({ files: [seedNote('doc-a', 'hello')] });
458
- const orch = buildOrchestrator({
459
- store,
460
- entityResolver: resolver(() => fail('resolver must not be called')),
461
- vectorIndex,
462
- embed,
463
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'hello')]))
464
- });
465
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
466
- expect(r.records[0].disposition).toBe('deduped');
467
- });
468
- });
469
-
470
- test('a distinct body is a fresh write (no resolver → exact-only fall-back)', async () => {
471
- const store = buildStore({ files: [seedNote('doc-a', 'hello')] });
472
- const orch = buildOrchestrator({
473
- store,
474
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'goodbye')]))
475
- });
476
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
477
- expect(r.records[0].disposition).toBe('written');
478
- expect(r.records[0].resolution.verdict).toBe('new');
479
- });
480
- });
481
- });
482
- });
483
-
484
- // --- similarity + resolver fixtures ------------------------------------------
485
- const classifyFact: IMemoryClassifier = classifier(() => succeed({ kind: factKind }));
486
- function oneFactExtractor(): IFactExtractor {
487
- return extractor((item) => succeed([candidate(factKind, item.id, item.content as string)]));
488
- }
489
-
490
- async function putNote(store: FileTreeMemoryStore, id: string, body: string): Promise<void> {
491
- const rec: IMemoryRecord<unknown> = {
492
- envelope: {
493
- id: id as MemoryId,
494
- entityId: id as EntityId,
495
- kind: noteKind,
496
- tags: [],
497
- links: [],
498
- created: 0,
499
- updated: 0,
500
- seq: 0,
501
- contentHash: '',
502
- provenance: { source: 'agent' }
503
- },
504
- body
505
- };
506
- (await store.put(rec)).orThrow();
507
- }
508
-
509
- function mockStore(overrides: Partial<IMemoryStore>): IMemoryStore {
510
- return {
511
- get:
512
- overrides.get ??
513
- ((): Promise<Result<IMemoryRecord<unknown> | undefined>> => Promise.resolve(succeed(undefined))),
514
- getById:
515
- overrides.getById ??
516
- ((): Promise<Result<IMemoryRecord<unknown> | undefined>> => Promise.resolve(succeed(undefined))),
517
- list:
518
- overrides.list ??
519
- ((): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> => Promise.resolve(succeed([]))),
520
- listScoped:
521
- overrides.listScoped ??
522
- ((): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => Promise.resolve(succeed([]))),
523
- asRecordSource:
524
- overrides.asRecordSource ??
525
- ((): IMemoryRecordSource => ({
526
- list: (): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => Promise.resolve(succeed([]))
527
- })),
528
- put: overrides.put ?? ((r): Promise<Result<IMemoryRecord<unknown>>> => Promise.resolve(succeed(r))),
529
- delete: overrides.delete ?? ((): Promise<Result<MemoryId>> => Promise.resolve(fail('n/a')))
530
- };
531
- }
532
-
533
- describe('MemoryIngestOrchestrator — stage 4 similarity (layer 2)', () => {
534
- test('resolver verdict new writes the candidate despite a near-duplicate', async () => {
535
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
536
- const store = buildStore({ vectorIndex, embed });
537
- await putNote(store, 'doc-a', 'apple pie');
538
- const orch = buildOrchestrator({
539
- store,
540
- vectorIndex,
541
- embed,
542
- entityResolver: resolver(() => succeed({ verdict: 'new' })),
543
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
544
- });
545
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
546
- expect(r.records[0].disposition).toBe('written');
547
- expect(r.records[0].resolution.verdict).toBe('new');
548
- });
549
- });
550
-
551
- test('resolver verdict duplicate-of dedups against the surfaced neighbor', async () => {
552
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
553
- const store = buildStore({ vectorIndex, embed });
554
- await putNote(store, 'doc-a', 'apple pie');
555
- const orch = buildOrchestrator({
556
- store,
557
- vectorIndex,
558
- embed,
559
- entityResolver: resolver((__c, similar) =>
560
- succeed({ verdict: 'duplicate-of', target: similar[0].target })
561
- ),
562
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
563
- });
564
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
565
- expect(r.records[0].disposition).toBe('deduped');
566
- expect(r.records[0].id).toBe('doc-a');
567
- });
568
- expect(await store.get(noteKind, 'doc-b' as EntityId)).toSucceedWith(undefined);
569
- });
570
-
571
- test('resolver verdict supersede writes the candidate and records the superseded target', async () => {
572
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
573
- const store = buildStore({ vectorIndex, embed });
574
- await putNote(store, 'doc-a', 'apple pie');
575
- const orch = buildOrchestrator({
576
- store,
577
- vectorIndex,
578
- embed,
579
- entityResolver: resolver((__c, similar) =>
580
- succeed({ verdict: 'supersede', target: similar[0].target })
581
- ),
582
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
583
- });
584
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
585
- expect(r.records[0].disposition).toBe('written');
586
- expect(r.records[0].resolution).toEqual({ verdict: 'supersede', target: kt('doc-a') });
587
- expect(r.records[0].id).toBe('doc-b');
588
- });
589
- expect(await store.get(noteKind, 'doc-b' as EntityId)).toSucceedAndSatisfy(
590
- (rec: IMemoryRecord<unknown> | undefined) => expect(rec?.body).toBe('apple tart')
591
- );
592
- });
593
-
594
- test('resolver verdict merge-into re-addresses the write to the target entity', async () => {
595
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
596
- const store = buildStore({ vectorIndex, embed });
597
- await putNote(store, 'doc-a', 'apple pie');
598
- const orch = buildOrchestrator({
599
- store,
600
- vectorIndex,
601
- embed,
602
- entityResolver: resolver((__c, similar) =>
603
- succeed({ verdict: 'merge-into', target: similar[0].target })
604
- ),
605
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
606
- });
607
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
608
- expect(r.records[0].disposition).toBe('merged');
609
- expect(r.records[0].id).toBe('doc-a');
610
- });
611
- // The merge updated doc-a's body (LWW merge patch) and never minted doc-b.
612
- expect(await store.get(noteKind, 'doc-a' as EntityId)).toSucceedAndSatisfy(
613
- (rec: IMemoryRecord<unknown> | undefined) => expect(rec?.body).toBe('apple tart')
614
- );
615
- expect(await store.get(noteKind, 'doc-b' as EntityId)).toSucceedWith(undefined);
616
- });
617
-
618
- test('merge-into with a non-existent target fails loudly', async () => {
619
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
620
- const store = buildStore({ vectorIndex, embed });
621
- await putNote(store, 'doc-a', 'apple pie');
622
- const orch = buildOrchestrator({
623
- store,
624
- vectorIndex,
625
- embed,
626
- entityResolver: resolver(() => succeed({ verdict: 'merge-into', target: kt('ghost') })),
627
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
628
- });
629
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
630
- /merge-into target 'knowledge\/ghost' does not exist/
631
- );
632
- });
633
-
634
- test('a below-threshold neighbor is not surfaced (resolver not called)', async () => {
635
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
636
- const store = buildStore({ vectorIndex, embed });
637
- await putNote(store, 'doc-a', 'apple pie');
638
- const orch = buildOrchestrator({
639
- store,
640
- vectorIndex,
641
- embed,
642
- entityResolver: resolver(() => fail('resolver must not be called')),
643
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'banana bread')]))
644
- });
645
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
646
- expect(r.records[0].resolution.verdict).toBe('new');
647
- });
648
- });
649
-
650
- test('the candidate self-hit is excluded from surfaced neighbors', async () => {
651
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
652
- const store = buildStore({ vectorIndex, embed });
653
- await putNote(store, 'doc-a', 'apple pie');
654
- const orch = buildOrchestrator({
655
- store,
656
- vectorIndex,
657
- embed,
658
- entityResolver: resolver(() => fail('resolver must not be called')),
659
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-a', 'apple tart')]))
660
- });
661
- // Re-ingesting the SAME entity: its own prior vector is the only over-threshold
662
- // hit, and it is skipped as the self-hit, so no resolver dispatch occurs.
663
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
664
- expect(r.records[0].resolution.verdict).toBe('new');
665
- expect(r.records[0].disposition).toBe('written');
666
- });
667
- });
668
-
669
- test('a surfaced hit with no backing record is skipped', async () => {
670
- const ghostIndex: IVectorIndex = {
671
- add: (t) => Promise.resolve(succeed(t.id as string)),
672
- remove: (t) => Promise.resolve(succeed(t)),
673
- query: (): Promise<Result<ReadonlyArray<IVectorQueryHit>>> =>
674
- Promise.resolve(succeed([{ target: kt('ghost'), score: 0.99 }]))
675
- };
676
- const orch = buildOrchestrator({
677
- store: buildStore(),
678
- vectorIndex: ghostIndex,
679
- embed,
680
- entityResolver: resolver(() => fail('resolver must not be called')),
681
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
682
- });
683
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
684
- expect(r.records[0].resolution.verdict).toBe('new');
685
- });
686
- });
687
-
688
- test('an embedder failure surfaces loudly', async () => {
689
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
690
- const orch = buildOrchestrator({
691
- store: buildStore(),
692
- vectorIndex,
693
- embed,
694
- entityResolver: resolver(() => succeed({ verdict: 'new' })),
695
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'boom')]))
696
- });
697
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(/embed boom/);
698
- });
699
-
700
- test('a similarity-query failure surfaces loudly', async () => {
701
- const failingIndex: IVectorIndex = {
702
- add: (t) => Promise.resolve(succeed(t.id as string)),
703
- remove: (t) => Promise.resolve(succeed(t)),
704
- query: (): Promise<Result<ReadonlyArray<IVectorQueryHit>>> => Promise.resolve(fail('query kaput'))
705
- };
706
- const orch = buildOrchestrator({
707
- store: buildStore(),
708
- vectorIndex: failingIndex,
709
- embed,
710
- entityResolver: resolver(() => succeed({ verdict: 'new' })),
711
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
712
- });
713
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(/query kaput/);
714
- });
715
-
716
- test('a custom similarityThreshold gates the resolver dispatch', async () => {
717
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
718
- const store = buildStore({ vectorIndex, embed });
719
- await putNote(store, 'doc-a', 'apple pie');
720
- const orch = buildOrchestrator({
721
- store,
722
- vectorIndex,
723
- embed,
724
- similarityThreshold: 0.999, // 0.99 neighbor now falls below threshold
725
- entityResolver: resolver(() => fail('resolver must not be called')),
726
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
727
- });
728
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
729
- expect(r.records[0].resolution.verdict).toBe('new');
730
- });
731
- });
732
- });
733
-
734
- describe('MemoryIngestOrchestrator — layer-2 wiring gates (exact-only fall-back)', () => {
735
- const failResolver: IEntityResolver = resolver(() => fail('resolver must not be called'));
736
- const almostSimilar = extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]));
737
-
738
- test('no resolver → exact-only', async () => {
739
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
740
- const store = buildStore({ vectorIndex, embed });
741
- await putNote(store, 'doc-a', 'apple pie');
742
- const orch = buildOrchestrator({ store, vectorIndex, embed, extractor: almostSimilar });
743
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) =>
744
- expect(r.records[0].resolution.verdict).toBe('new')
745
- );
746
- });
747
-
748
- test('resolver + embed but no vector index → exact-only', async () => {
749
- const orch = buildOrchestrator({
750
- store: buildStore(),
751
- embed,
752
- entityResolver: failResolver,
753
- extractor: almostSimilar
754
- });
755
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) =>
756
- expect(r.records[0].resolution.verdict).toBe('new')
757
- );
758
- });
759
-
760
- test('resolver + vector index but no embedder → exact-only', async () => {
761
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
762
- const orch = buildOrchestrator({
763
- store: buildStore(),
764
- vectorIndex,
765
- entityResolver: failResolver,
766
- extractor: almostSimilar
767
- });
768
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) =>
769
- expect(r.records[0].resolution.verdict).toBe('new')
770
- );
771
- });
772
- });
773
-
774
- describe('MemoryIngestOrchestrator — stage 5 relate + cycle guard', () => {
775
- test('attaches a valid edge to an existing record', async () => {
776
- const store = buildStore();
777
- await putNote(store, 'doc-a', 'anchor');
778
- const orch = buildOrchestrator({
779
- store,
780
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'goodbye')])),
781
- relationExtractor: relater(() => succeed([candEdge('doc-b', 'mentions', 'doc-a', 0.7)]))
782
- });
783
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
784
- expect(r.records[0].edges).toHaveLength(1);
785
- expect(r.records[0].record?.envelope.links).toEqual([
786
- { type: 'mentions', target: kt('doc-a'), confidence: 0.7 }
787
- ]);
788
- });
789
- });
790
-
791
- test('attaches an edge between two sibling candidates', async () => {
792
- const orch = buildOrchestrator({
793
- store: buildStore(),
794
- extractor: extractor(() =>
795
- succeed([candidate(noteKind, 'doc-b', 'first'), candidate(noteKind, 'doc-c', 'second')])
796
- ),
797
- relationExtractor: relater(() => succeed([candEdge('doc-b', 'rel', 'doc-c')]))
798
- });
799
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
800
- expect(r.records).toHaveLength(2);
801
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-c') }]);
802
- expect(r.records[1].edges).toEqual([]);
803
- });
804
- });
805
-
806
- test('rejects an edge whose source is not a candidate being written', async () => {
807
- const orch = buildOrchestrator({
808
- store: buildStore(),
809
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'x')])),
810
- relationExtractor: relater(() => succeed([candEdge('ghost', 'rel', 'doc-b')]))
811
- });
812
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
813
- /edge source 'knowledge\/ghost' is not a candidate being written/
814
- );
815
- });
816
-
817
- test('rejects an edge whose target resolves to nothing', async () => {
818
- const orch = buildOrchestrator({
819
- store: buildStore(),
820
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'x')])),
821
- relationExtractor: relater(() => succeed([candEdge('doc-b', 'rel', 'ghost')]))
822
- });
823
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
824
- /edge target 'knowledge\/ghost' resolves to neither/
825
- );
826
- });
827
-
828
- test('rejects a cycle-inducing edge (default guard)', async () => {
829
- const orch = buildOrchestrator({
830
- store: buildStore(),
831
- extractor: extractor(() =>
832
- succeed([candidate(noteKind, 'doc-b', 'first'), candidate(noteKind, 'doc-c', 'second')])
833
- ),
834
- relationExtractor: relater(() =>
835
- succeed([candEdge('doc-b', 'rel', 'doc-c'), candEdge('doc-c', 'rel', 'doc-b')])
836
- )
837
- });
838
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(/would create a cycle/);
839
- });
840
-
841
- test('cycleGuard "off" admits an otherwise-cyclic edge set', async () => {
842
- const orch = buildOrchestrator({
843
- store: buildStore(),
844
- cycleGuard: 'off',
845
- extractor: extractor(() =>
846
- succeed([candidate(noteKind, 'doc-b', 'first'), candidate(noteKind, 'doc-c', 'second')])
847
- ),
848
- relationExtractor: relater(() =>
849
- succeed([candEdge('doc-b', 'rel', 'doc-c'), candEdge('doc-c', 'rel', 'doc-b')])
850
- )
851
- });
852
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
853
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-c') }]);
854
- expect(r.records[1].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-b') }]);
855
- });
856
- });
857
-
858
- test('a contradicts edge on a NON-temporal kind persists as a plain link (no interlock)', async () => {
859
- const store = buildStore();
860
- await putNote(store, 'doc-a', 'anchor');
861
- const orch = buildOrchestrator({
862
- store,
863
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'goodbye')])),
864
- relationExtractor: relater(() => succeed([candEdge('doc-b', 'contradicts', 'doc-a')]))
865
- });
866
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
867
- expect(r.records[0].interlock).toBeUndefined();
868
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'contradicts', target: kt('doc-a') }]);
869
- });
870
- });
871
-
872
- describe('_validateEdges cross-scope target disambiguation', () => {
873
- // Two EXISTING records share the idStem 'turn-3' across two conversations
874
- // (the MTM codec mints per-scope stems). A stage-5 edge target must resolve
875
- // to the CORRECT (scope, id) — with bare-id keying both would alias and a
876
- // wrong-scope target would silently validate. Guards the ingest-validation seam.
877
- const mtmKind: Kind = 'mtm' as Kind;
878
-
879
- function mtmRegistry(): IBodyConverterRegistry {
880
- const reg = registry();
881
- reg.register(mtmKind, Converters.string);
882
- return reg;
883
- }
884
-
885
- const mtmCodecs: ReadonlyMap<Kind, IIdentityCodec> = new Map<Kind, IIdentityCodec>([
886
- [mtmKind, new MtmIdentityCodec()]
887
- ]);
888
-
889
- async function seedTurn(store: FileTreeMemoryStore, entityId: string, body: string): Promise<void> {
890
- const addr = new MtmIdentityCodec().encode(entityId as EntityId).orThrow();
891
- const rec: IMemoryRecord<unknown> = {
892
- envelope: {
893
- id: addr.idStem as MemoryId,
894
- entityId: entityId as EntityId,
895
- kind: mtmKind,
896
- tags: [],
897
- links: [],
898
- created: 0,
899
- updated: 0,
900
- seq: 0,
901
- contentHash: '',
902
- provenance: { source: 'agent' }
903
- },
904
- body
905
- };
906
- (await store.put(rec)).orThrow();
907
- }
908
-
909
- function buildMtmOrchestrator(
910
- store: FileTreeMemoryStore,
911
- relationExtractor: IRelationExtractor
912
- ): MemoryIngestOrchestrator {
913
- return MemoryIngestOrchestrator.create({
914
- store,
915
- registry: mtmRegistry(),
916
- codecs: mtmCodecs,
917
- classifier: classifier(() => succeed({ kind: mtmKind })),
918
- // The candidate is a fresh turn-5 under conv-a (distinct stem from the seeds).
919
- extractor: extractor(() => succeed([candidate(mtmKind, 'conv-a:5', 'a new turn')])),
920
- relationExtractor
921
- }).orThrow();
922
- }
923
-
924
- async function seededStore(): Promise<FileTreeMemoryStore> {
925
- const store = FileTreeMemoryStore.create({
926
- root: mutableRoot(),
927
- registry: mtmRegistry(),
928
- codecs: mtmCodecs,
929
- clock
930
- }).orThrow();
931
- await seedTurn(store, 'conv-a:3', 'anchor in conversation a');
932
- await seedTurn(store, 'conv-b:3', 'anchor in conversation b');
933
- return store;
934
- }
935
-
936
- test('accepts a same-stem edge target that resolves to the correct scope', async () => {
937
- const store = await seededStore();
938
- const orch = buildMtmOrchestrator(
939
- store,
940
- relater((ctx) =>
941
- succeed([
942
- {
943
- source: srcOf(ctx, 'conv-a:5'),
944
- edge: { type: 'rel' as LinkType, target: kt('turn-3', 'conversations/conv-a') }
945
- }
946
- ])
947
- )
948
- );
949
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
950
- expect(r.records[0].record?.envelope.links).toEqual([
951
- { type: 'rel', target: kt('turn-3', 'conversations/conv-a') }
952
- ]);
953
- });
954
- });
955
-
956
- test('rejects a same-stem edge target whose scope has no matching record', async () => {
957
- const store = await seededStore();
958
- const orch = buildMtmOrchestrator(
959
- store,
960
- relater((ctx) =>
961
- succeed([
962
- {
963
- source: srcOf(ctx, 'conv-a:5'),
964
- // turn-3 exists under conv-a and conv-b, but NOT conv-z.
965
- edge: { type: 'rel' as LinkType, target: kt('turn-3', 'conversations/conv-z') }
966
- }
967
- ])
968
- )
969
- );
970
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
971
- /edge target 'conversations\/conv-z\/turn-3' resolves to neither a sibling candidate nor an existing record/
972
- );
973
- });
974
- });
975
- });
976
-
977
- describe('MemoryIngestOrchestrator — contradicts→temporal interlock', () => {
978
- test('a contradicts edge on a temporal kind invalidates the prior version and writes a new one', async () => {
979
- const store = buildStore();
980
- const firstOrch = buildOrchestrator({
981
- store,
982
- classifier: classifyFact,
983
- extractor: oneFactExtractor()
984
- });
985
- clockValue = 1000;
986
- const first = await firstOrch.ingestItem({ id: 'fact-1', content: 'the sky is blue' });
987
- const v1Id: MemoryId = first.orThrow().records[0].id;
988
- expect(v1Id).toBe('fact-1-v1');
989
-
990
- clockValue = 2000;
991
- const secondOrch = buildOrchestrator({
992
- store,
993
- classifier: classifyFact,
994
- extractor: extractor(() => succeed([candidate(factKind, 'fact-1', 'the sky is grey')])),
995
- relationExtractor: relater((ctx) =>
996
- succeed([
997
- {
998
- source: srcOf(ctx, 'fact-1'),
999
- edge: { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1000
- }
1001
- ])
1002
- )
1003
- });
1004
- expect(await secondOrch.ingestItem({ id: 'fact-1', content: 'the sky is grey' })).toSucceedAndSatisfy(
1005
- (r: IIngestItemResult) => {
1006
- expect(r.records[0].interlock).toBe('temporal-versioned');
1007
- expect(r.records[0].disposition).toBe('written');
1008
- expect(r.records[0].record?.envelope.links).toEqual([
1009
- { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1010
- ]);
1011
- }
1012
- );
1013
-
1014
- // The current version is the new (contradicting) one.
1015
- expect(await store.get(factKind, 'fact-1' as EntityId)).toSucceedAndSatisfy(
1016
- (rec: IMemoryRecord<unknown> | undefined) => expect(rec?.body).toBe('the sky is grey')
1017
- );
1018
- // The prior version is invalidated (invalidate-don't-delete).
1019
- expect(await store.list()).toSucceedAndSatisfy((all: ReadonlyArray<IMemoryRecord<unknown>>) => {
1020
- const v1 = all.find((rec) => rec.envelope.id === v1Id);
1021
- expect(v1?.envelope.temporal?.invalid_at).toBe(2000);
1022
- });
1023
- });
1024
-
1025
- test('an invalidated version does not exact-dedup a later candidate that repeats its body', async () => {
1026
- const store = buildStore();
1027
- clockValue = 1000;
1028
- const o1 = buildOrchestrator({ store, classifier: classifyFact, extractor: oneFactExtractor() });
1029
- const first = await o1.ingestItem({ id: 'fact-1', content: 'blue' });
1030
- const v1Id: MemoryId = first.orThrow().records[0].id;
1031
-
1032
- clockValue = 2000;
1033
- const o2 = buildOrchestrator({
1034
- store,
1035
- classifier: classifyFact,
1036
- extractor: extractor(() => succeed([candidate(factKind, 'fact-1', 'grey')])),
1037
- relationExtractor: relater((ctx) =>
1038
- succeed([
1039
- {
1040
- source: srcOf(ctx, 'fact-1'),
1041
- edge: { type: CONTRADICTS_LINK_TYPE, target: kt(v1Id, 'facts/entities/fact-1') }
1042
- }
1043
- ])
1044
- )
1045
- });
1046
- (await o2.ingestItem({ id: 'fact-1', content: 'grey' })).orThrow();
1047
-
1048
- // Re-introduce the ORIGINAL body: the invalidated v1 must be skipped by exact
1049
- // dedup, so this is a NEW version, not a duplicate-of.
1050
- clockValue = 3000;
1051
- const o3 = buildOrchestrator({
1052
- store,
1053
- classifier: classifyFact,
1054
- extractor: extractor(() => succeed([candidate(factKind, 'fact-1', 'blue')]))
1055
- });
1056
- expect(await o3.ingestItem({ id: 'fact-1', content: 'blue' })).toSucceedAndSatisfy(
1057
- (r: IIngestItemResult) => {
1058
- expect(r.records[0].resolution.verdict).toBe('new');
1059
- expect(r.records[0].disposition).toBe('written');
1060
- }
1061
- );
1062
- });
1063
- });
1064
-
1065
- describe('MemoryIngestOrchestrator — additive-provenance non-regression', () => {
1066
- test('a pre-existing record with only source provenance still loads and validates unchanged', async () => {
1067
- const store = buildStore({ files: [seedNote('legacy', 'old note', { source: 'human' })] });
1068
- // Loads at construction time.
1069
- expect(await store.getById('knowledge' as MemoryScopeKey, 'legacy' as MemoryId)).toSucceedAndSatisfy(
1070
- (rec: IMemoryRecord<unknown> | undefined) => {
1071
- expect(rec?.envelope.provenance).toEqual({ source: 'human' });
1072
- expect(rec?.envelope.provenance.by).toBeUndefined();
1073
- expect(rec?.envelope.provenance.derivedFrom).toBeUndefined();
1074
- }
1075
- );
1076
- // An unrelated ingest does not disturb it.
1077
- const orch = buildOrchestrator({ store });
1078
- (await orch.ingestItem({ id: 'fresh', content: 'new content' })).orThrow();
1079
- expect(await store.getById('knowledge' as MemoryScopeKey, 'legacy' as MemoryId)).toSucceedAndSatisfy(
1080
- (rec: IMemoryRecord<unknown> | undefined) => {
1081
- expect(rec?.envelope.provenance).toEqual({ source: 'human' });
1082
- }
1083
- );
1084
- });
1085
- });
1086
-
1087
- describe('MemoryIngestOrchestrator — ingestBatch', () => {
1088
- test('ingests a batch in order', async () => {
1089
- const store = buildStore();
1090
- const orch = buildOrchestrator({ store });
1091
- const result = await orch.ingestBatch([
1092
- { id: 'doc-1', content: 'one' },
1093
- { id: 'doc-2', content: 'two' }
1094
- ]);
1095
- expect(result).toSucceedAndSatisfy((results: ReadonlyArray<IIngestItemResult>) => {
1096
- expect(results).toHaveLength(2);
1097
- expect(results.map((r) => r.item.id)).toEqual(['doc-1', 'doc-2']);
1098
- expect(results.every((r) => r.records[0].disposition === 'written')).toBe(true);
1099
- });
1100
- });
1101
-
1102
- test('aborts the batch on the first item failure', async () => {
1103
- const orch = buildOrchestrator({
1104
- store: buildStore(),
1105
- classifier: classifier((item) =>
1106
- item.id === 'bad' ? fail('classify bad') : succeed({ kind: noteKind })
1107
- )
1108
- });
1109
- expect(
1110
- await orch.ingestBatch([
1111
- { id: 'bad', content: 'one' },
1112
- { id: 'doc-2', content: 'two' }
1113
- ])
1114
- ).toFailWith(/classify bad/);
1115
- });
1116
- });
1117
-
1118
- describe('MemoryIngestOrchestrator — store failure paths', () => {
1119
- test('a store snapshot (list) failure aborts the item', async () => {
1120
- const store = mockStore({ list: () => Promise.resolve(fail('list down')) });
1121
- const orch = buildOrchestrator({ store });
1122
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(
1123
- /failed to snapshot store.*list down/
1124
- );
1125
- });
1126
-
1127
- test('a store put failure surfaces as a write failure', async () => {
1128
- const store = mockStore({ put: () => Promise.resolve(fail('put kaput')) });
1129
- const orch = buildOrchestrator({ store });
1130
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(/write failed.*put kaput/);
1131
- });
1132
- });
1133
-
1134
- describe('MemoryIngestOrchestrator — optional constructor params', () => {
1135
- test('falls back to defaultCodec when no per-kind codec map is supplied', async () => {
1136
- const store = buildStore();
1137
- const orch = MemoryIngestOrchestrator.create({
1138
- store,
1139
- registry: registry(),
1140
- defaultCodec: new KnowledgeIdentityCodec(),
1141
- classifier: classifyNote,
1142
- extractor: oneNoteExtractor(),
1143
- relationExtractor: noEdges
1144
- }).orThrow();
1145
- expect(await orch.ingestItem({ id: 'doc-1', content: 'hello' })).toSucceedAndSatisfy(
1146
- (r: IIngestItemResult) => expect(r.records[0].disposition).toBe('written')
1147
- );
1148
- });
1149
-
1150
- test('honors an explicit similarityTopK and logs a captured host throw to the supplied logger', async () => {
1151
- const logger = new Logging.InMemoryLogger('detail');
1152
- const orch = MemoryIngestOrchestrator.create({
1153
- store: buildStore(),
1154
- registry: registry(),
1155
- codecs,
1156
- classifier: throwingClassifier(),
1157
- extractor: oneNoteExtractor(),
1158
- relationExtractor: noEdges,
1159
- similarityTopK: 3,
1160
- logger
1161
- }).orThrow();
1162
- expect(await orch.ingestItem({ id: 'doc-1', content: 'x' })).toFailWith(/classifier boom/);
1163
- expect(logger.logged.some((m) => /classifier boom/.test(m))).toBe(true);
1164
- });
1165
- });
1166
-
1167
- describe('MemoryIngestOrchestrator — review-punch-list fixes', () => {
1168
- test('a stored record with a non-string body fails the exact-dedup pass loudly', async () => {
1169
- const weird: IMemoryRecord<unknown> = {
1170
- envelope: {
1171
- id: 'weird' as MemoryId,
1172
- entityId: 'weird' as EntityId,
1173
- kind: noteKind,
1174
- tags: [],
1175
- links: [],
1176
- created: 0,
1177
- updated: 0,
1178
- seq: 0,
1179
- contentHash: '',
1180
- provenance: { source: 'agent' }
1181
- },
1182
- body: 42
1183
- };
1184
- const store = mockStore({ list: () => Promise.resolve(succeed([weird])) });
1185
- const orch = buildOrchestrator({ store });
1186
- expect(await orch.ingestItem({ id: 'doc-1', content: 'hello' })).toFailWith(
1187
- /stored record 'weird' has a non-string body/
1188
- );
1189
- });
1190
-
1191
- test('duplicate stage-5 edges are deduped on the written record', async () => {
1192
- const store = buildStore();
1193
- await putNote(store, 'doc-a', 'anchor');
1194
- const orch = buildOrchestrator({
1195
- store,
1196
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'goodbye')])),
1197
- relationExtractor: relater(() =>
1198
- succeed([candEdge('doc-b', 'rel', 'doc-a'), candEdge('doc-b', 'rel', 'doc-a')])
1199
- )
1200
- });
1201
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
1202
- expect(r.records[0].record?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-a') }]);
1203
- });
1204
- });
1205
-
1206
- test('two candidates merging into the same target do not duplicate a shared edge', async () => {
1207
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
1208
- const store = buildStore({ vectorIndex, embed });
1209
- await putNote(store, 'doc-a', 'apple pie');
1210
- await putNote(store, 'doc-x', 'target');
1211
- const orch = buildOrchestrator({
1212
- store,
1213
- vectorIndex,
1214
- embed,
1215
- entityResolver: resolver((__c, similar) =>
1216
- succeed({ verdict: 'merge-into', target: similar[0].target })
1217
- ),
1218
- extractor: extractor(() =>
1219
- succeed([candidate(noteKind, 'doc-b', 'apple tart'), candidate(noteKind, 'doc-c', 'apple tart')])
1220
- ),
1221
- relationExtractor: relater(() => succeed([candEdge('doc-a', 'rel', 'doc-x')]))
1222
- });
1223
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceed();
1224
- expect(await store.get(noteKind, 'doc-a' as EntityId)).toSucceedAndSatisfy(
1225
- (rec: IMemoryRecord<unknown> | undefined) => {
1226
- expect(rec?.envelope.links).toEqual([{ type: 'rel', target: kt('doc-x') }]);
1227
- }
1228
- );
1229
- });
1230
- });
1231
-
1232
- describe('MemoryIngestOrchestrator — merge-into safety (second review)', () => {
1233
- // Seed a record with pre-existing tags + links under a flat codec (id === entityId).
1234
- async function putFull(
1235
- store: FileTreeMemoryStore,
1236
- kind: Kind,
1237
- entityId: string,
1238
- body: string,
1239
- opts: { tags?: ReadonlyArray<string>; links?: IMemoryEnvelope['links'] } = {}
1240
- ): Promise<void> {
1241
- const rec: IMemoryRecord<unknown> = {
1242
- envelope: {
1243
- id: entityId as MemoryId,
1244
- entityId: entityId as EntityId,
1245
- kind,
1246
- tags: (opts.tags ?? []) as ReadonlyArray<Tag>,
1247
- links: opts.links ?? [],
1248
- created: 0,
1249
- updated: 0,
1250
- seq: 0,
1251
- contentHash: '',
1252
- provenance: { source: 'agent' }
1253
- },
1254
- body
1255
- };
1256
- (await store.put(rec)).orThrow();
1257
- }
1258
-
1259
- test('merge-into UNIONs the target’s existing tags + links (never overwrites them)', async () => {
1260
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
1261
- const store = buildStore({ vectorIndex, embed });
1262
- await putFull(store, noteKind, 'doc-a', 'apple pie', {
1263
- tags: ['old-tag'],
1264
- links: [{ type: 'existing' as LinkType, target: kt('doc-z') }]
1265
- });
1266
- await putFull(store, noteKind, 'doc-x', 'target of stage-5');
1267
- const orch = buildOrchestrator({
1268
- store,
1269
- vectorIndex,
1270
- embed,
1271
- entityResolver: resolver((__c, similar) =>
1272
- succeed({ verdict: 'merge-into', target: similar[0].target })
1273
- ),
1274
- extractor: extractor(() =>
1275
- succeed([
1276
- candidate(noteKind, 'doc-b', 'apple tart', {
1277
- tags: ['new-tag'],
1278
- links: [{ type: 'candlink' as LinkType, target: kt('doc-z') }]
1279
- })
1280
- ])
1281
- ),
1282
- relationExtractor: relater(() => succeed([candEdge('doc-a', 'stage5', 'doc-x')]))
1283
- });
1284
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toSucceedAndSatisfy((r: IIngestItemResult) => {
1285
- expect(r.records[0].disposition).toBe('merged');
1286
- });
1287
- expect(await store.get(noteKind, 'doc-a' as EntityId)).toSucceedAndSatisfy(
1288
- (rec: IMemoryRecord<unknown> | undefined) => {
1289
- // Original tag preserved AND the candidate's tag gained, each once.
1290
- expect([...(rec?.envelope.tags ?? [])].sort()).toEqual(['new-tag', 'old-tag']);
1291
- // Original link + candidate link + stage-5 edge, each exactly once.
1292
- expect(rec?.envelope.links).toEqual([
1293
- { type: 'existing', target: kt('doc-z') },
1294
- { type: 'candlink', target: kt('doc-z') },
1295
- { type: 'stage5', target: kt('doc-x') }
1296
- ]);
1297
- }
1298
- );
1299
- });
1300
-
1301
- test('merge-into a different-kind target fails loudly', async () => {
1302
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
1303
- const store = buildStore({ vectorIndex, embed });
1304
- // A DIFFERENT-kind (ltm) record embedded so it surfaces as a neighbor of a note candidate.
1305
- await putFull(store, ltmKind, 'conv-1', 'apple pie');
1306
- const orch = buildOrchestrator({
1307
- store,
1308
- vectorIndex,
1309
- embed,
1310
- entityResolver: resolver((__c, similar) =>
1311
- succeed({ verdict: 'merge-into', target: similar[0].target })
1312
- ),
1313
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1314
- });
1315
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1316
- /merge-into target 'conversations\/conv-1' is kind 'ltm' but the candidate is kind 'note'/
1317
- );
1318
- });
1319
-
1320
- test('supersede with a non-existent target fails loudly', async () => {
1321
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
1322
- const store = buildStore({ vectorIndex, embed });
1323
- await putFull(store, noteKind, 'doc-a', 'apple pie');
1324
- const orch = buildOrchestrator({
1325
- store,
1326
- vectorIndex,
1327
- embed,
1328
- entityResolver: resolver(() => succeed({ verdict: 'supersede', target: kt('ghost') })),
1329
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1330
- });
1331
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1332
- /supersede target 'knowledge\/ghost' does not exist/
1333
- );
1334
- });
1335
-
1336
- test('duplicate-of with a non-existent target fails loudly', async () => {
1337
- const vectorIndex = InMemoryCosineIndex.create().orThrow();
1338
- const store = buildStore({ vectorIndex, embed });
1339
- await putFull(store, noteKind, 'doc-a', 'apple pie');
1340
- const orch = buildOrchestrator({
1341
- store,
1342
- vectorIndex,
1343
- embed,
1344
- entityResolver: resolver(() => succeed({ verdict: 'duplicate-of', target: kt('ghost') })),
1345
- extractor: extractor(() => succeed([candidate(noteKind, 'doc-b', 'apple tart')]))
1346
- });
1347
- expect(await orch.ingestItem({ id: 'i', content: 'x' })).toFailWith(
1348
- /duplicate-of target 'knowledge\/ghost' does not exist/
1349
- );
1350
- });
1351
- });