@remnic/core 9.3.700 → 9.3.702

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 (323) hide show
  1. package/dist/access-boundary.d.ts +7 -7
  2. package/dist/access-boundary.js +17 -14
  3. package/dist/access-cli.js +40 -37
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +7 -7
  6. package/dist/access-http.js +20 -17
  7. package/dist/access-mcp.d.ts +7 -7
  8. package/dist/access-mcp.js +19 -16
  9. package/dist/access-operations.d.ts +7 -7
  10. package/dist/access-operations.js +18 -15
  11. package/dist/{access-service-Cte3ol0W.d.ts → access-service-COCzhgEL.d.ts} +397 -19
  12. package/dist/access-service.d.ts +6 -6
  13. package/dist/access-service.js +16 -13
  14. package/dist/access-surface-catalog.d.ts +7 -7
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-memory-bridge.js +2 -2
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +7 -6
  20. package/dist/active-recall.js.map +1 -1
  21. package/dist/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +5 -5
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +8 -7
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/capabilities.d.ts +1 -1
  30. package/dist/{catalog-DN1PzThs.d.ts → catalog-DxCzhjE6.d.ts} +28 -54
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +9 -8
  34. package/dist/causal-consolidation.js.map +1 -1
  35. package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
  36. package/dist/chunk-3FAMU5TX.js.map +1 -0
  37. package/dist/{chunk-PCZR32VL.js → chunk-3JJWNZTT.js} +2 -2
  38. package/dist/{chunk-YXLT4EMM.js → chunk-4463LUIE.js} +12 -2
  39. package/dist/chunk-4463LUIE.js.map +1 -0
  40. package/dist/{chunk-R5DB26G6.js → chunk-4SBW47WK.js} +9 -23
  41. package/dist/chunk-4SBW47WK.js.map +1 -0
  42. package/dist/{chunk-CCOXIDRM.js → chunk-5VXVXJH6.js} +149 -6
  43. package/dist/{chunk-CCOXIDRM.js.map → chunk-5VXVXJH6.js.map} +1 -1
  44. package/dist/{chunk-PQG4T5V3.js → chunk-64XXOQRW.js} +49 -45
  45. package/dist/chunk-64XXOQRW.js.map +1 -0
  46. package/dist/{chunk-YXIFA36P.js → chunk-6TAETM63.js} +2 -2
  47. package/dist/{chunk-6JDGADXK.js → chunk-6VVP6NK7.js} +2 -2
  48. package/dist/{chunk-FN2SM5SN.js → chunk-ABCGDW3A.js} +75 -12
  49. package/dist/chunk-ABCGDW3A.js.map +1 -0
  50. package/dist/{chunk-CHM274U6.js → chunk-AMNLZ6SF.js} +2 -2
  51. package/dist/{chunk-JX3YZVII.js → chunk-CZJ6QSKG.js} +3 -3
  52. package/dist/{chunk-GYVVQYA3.js → chunk-EOOVJK2U.js} +3 -3
  53. package/dist/{chunk-JKW5XSWC.js → chunk-EYJD6KIO.js} +2 -2
  54. package/dist/{chunk-XY4WJTEX.js → chunk-EZR35XHX.js} +2 -2
  55. package/dist/{chunk-UU6MVCJ6.js → chunk-FIOYURII.js} +32 -25
  56. package/dist/chunk-FIOYURII.js.map +1 -0
  57. package/dist/{chunk-2NWHLAXX.js → chunk-FVI5B7DE.js} +2 -2
  58. package/dist/{chunk-X74FJSW7.js → chunk-GFIARMA7.js} +16 -8
  59. package/dist/chunk-GFIARMA7.js.map +1 -0
  60. package/dist/{chunk-PONNZ54D.js → chunk-GY3SKOS4.js} +4 -4
  61. package/dist/{chunk-U33LWTQQ.js → chunk-HV57RHMD.js} +4 -4
  62. package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
  63. package/dist/{chunk-IKNQAGBV.js → chunk-IX3UQT4H.js} +1 -1
  64. package/dist/{chunk-IKNQAGBV.js.map → chunk-IX3UQT4H.js.map} +1 -1
  65. package/dist/{chunk-HDLC75NX.js → chunk-IX72AAMZ.js} +2 -2
  66. package/dist/{chunk-DR2JTSLZ.js → chunk-JMA4RYRN.js} +62 -163
  67. package/dist/chunk-JMA4RYRN.js.map +1 -0
  68. package/dist/{chunk-G5PKTQ5J.js → chunk-JTKFZMZ7.js} +2 -2
  69. package/dist/{chunk-YMTGXDN6.js → chunk-KC6TCAWV.js} +5 -5
  70. package/dist/{chunk-ROZJACKP.js → chunk-KKK7YTYN.js} +4 -1
  71. package/dist/chunk-KKK7YTYN.js.map +1 -0
  72. package/dist/{chunk-EC2AYKRX.js → chunk-L6W77GWW.js} +10 -24
  73. package/dist/chunk-L6W77GWW.js.map +1 -0
  74. package/dist/{chunk-ED35D32I.js → chunk-LQ4J7ELC.js} +2 -2
  75. package/dist/{chunk-YPR7DOPD.js → chunk-LTJAMRGI.js} +4 -4
  76. package/dist/{chunk-YPR7DOPD.js.map → chunk-LTJAMRGI.js.map} +1 -1
  77. package/dist/chunk-LUPVCGYK.js +63 -0
  78. package/dist/chunk-LUPVCGYK.js.map +1 -0
  79. package/dist/{chunk-RJ2THZ4H.js → chunk-MBUM2Y3L.js} +2 -2
  80. package/dist/{chunk-T5QAZIBO.js → chunk-MOXFPLD6.js} +3 -3
  81. package/dist/{chunk-O54DY26V.js → chunk-MXEWQKM7.js} +2 -2
  82. package/dist/{chunk-33L6XHU2.js → chunk-NUIJEGVD.js} +6 -6
  83. package/dist/{chunk-3E5WRQNQ.js → chunk-OXEAMU42.js} +529 -9
  84. package/dist/chunk-OXEAMU42.js.map +1 -0
  85. package/dist/{chunk-EOBJRBLC.js → chunk-QGJAGC2J.js} +2 -2
  86. package/dist/{chunk-SMIVW7XC.js → chunk-QIMFOCSH.js} +2 -2
  87. package/dist/{chunk-IJEZMWKA.js → chunk-SFOAQQDJ.js} +3 -3
  88. package/dist/{chunk-HRUULBBV.js → chunk-SHRRWOVY.js} +81 -4
  89. package/dist/chunk-SHRRWOVY.js.map +1 -0
  90. package/dist/{chunk-NINRTFSV.js → chunk-SINGJCUR.js} +5 -5
  91. package/dist/{chunk-ZPQVJEVQ.js → chunk-SK2CR6MW.js} +146 -2
  92. package/dist/chunk-SK2CR6MW.js.map +1 -0
  93. package/dist/{chunk-SEWF2O74.js → chunk-TGAHHCB6.js} +2 -2
  94. package/dist/{chunk-D75JXBV4.js → chunk-WN4GHSDH.js} +2 -2
  95. package/dist/{chunk-K4DWSPMW.js → chunk-WRGPE6AW.js} +2 -2
  96. package/dist/{chunk-GA5A6MJH.js → chunk-XGMCUY5P.js} +62 -116
  97. package/dist/chunk-XGMCUY5P.js.map +1 -0
  98. package/dist/{chunk-SDPDU2PM.js → chunk-YTMDF6S7.js} +2 -2
  99. package/dist/{chunk-ZDK2IW5F.js → chunk-Z7XEIAV4.js} +2 -2
  100. package/dist/{chunk-NHBEO3F3.js → chunk-ZCEI242W.js} +24 -24
  101. package/dist/{cli--yVN9yEV.d.ts → cli-BM4xQPp4.d.ts} +3 -3
  102. package/dist/cli.d.ts +7 -7
  103. package/dist/cli.js +35 -34
  104. package/dist/compounding/engine.d.ts +1 -1
  105. package/dist/compounding/engine.js +8 -7
  106. package/dist/compounding/preference-consolidator.d.ts +1 -1
  107. package/dist/compression-optimizer.d.ts +1 -1
  108. package/dist/config.d.ts +1 -1
  109. package/dist/config.js +4 -2
  110. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  111. package/dist/connectors/codex-materialize-runner.js +8 -7
  112. package/dist/connectors/codex-materialize.d.ts +1 -1
  113. package/dist/connectors/index.d.ts +1 -1
  114. package/dist/connectors/index.js +9 -8
  115. package/dist/consolidation-provenance-check.d.ts +1 -1
  116. package/dist/consolidation-undo.d.ts +1 -1
  117. package/dist/contradiction/index.d.ts +2 -2
  118. package/dist/conversation-index/backend.d.ts +1 -1
  119. package/dist/conversation-index/chunker.d.ts +1 -1
  120. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  121. package/dist/conversation-index/indexer.d.ts +1 -1
  122. package/dist/conversation-index/search.d.ts +1 -1
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/delinearize.d.ts +1 -1
  125. package/dist/direct-answer-wiring.d.ts +1 -1
  126. package/dist/direct-answer.d.ts +1 -1
  127. package/dist/embedding-fallback.d.ts +1 -1
  128. package/dist/enrichment/index.d.ts +1 -1
  129. package/dist/entity-retrieval.d.ts +1 -1
  130. package/dist/entity-retrieval.js +8 -7
  131. package/dist/entity-schema.d.ts +1 -1
  132. package/dist/event-order-recall.js +2 -1
  133. package/dist/explicit-capture.d.ts +5 -5
  134. package/dist/explicit-capture.js +2 -2
  135. package/dist/explicit-cue-recall.js +2 -1
  136. package/dist/extraction-faithfulness.d.ts +1 -1
  137. package/dist/extraction-judge-telemetry.d.ts +1 -1
  138. package/dist/extraction-judge-training.d.ts +1 -1
  139. package/dist/extraction-judge.d.ts +1 -1
  140. package/dist/extraction.d.ts +14 -1
  141. package/dist/extraction.js +5 -3
  142. package/dist/fallback-llm.d.ts +1 -1
  143. package/dist/{forget-BEXG5PQC.js → forget-6SOIPUMQ.js} +3 -3
  144. package/dist/identity-continuity.d.ts +1 -1
  145. package/dist/importance.d.ts +1 -1
  146. package/dist/index.d.ts +123 -123
  147. package/dist/index.js +57 -56
  148. package/dist/index.js.map +1 -1
  149. package/dist/intent.d.ts +1 -1
  150. package/dist/lcm/engine.d.ts +1 -1
  151. package/dist/lcm/index.d.ts +1 -1
  152. package/dist/lcm/tools.d.ts +1 -1
  153. package/dist/lifecycle.d.ts +1 -1
  154. package/dist/live-connectors-runner.d.ts +1 -1
  155. package/dist/local-llm.d.ts +1 -1
  156. package/dist/maintenance/memory-governance.d.ts +1 -1
  157. package/dist/maintenance/memory-governance.js +8 -7
  158. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -7
  159. package/dist/maintenance/rebuild-memory-projection.js +9 -8
  160. package/dist/mcp-memory-inspector-app.d.ts +7 -7
  161. package/dist/memory-action-policy.d.ts +1 -1
  162. package/dist/memory-cache.d.ts +1 -1
  163. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  164. package/dist/memory-projection-store.d.ts +1 -1
  165. package/dist/memory-provenance.d.ts +1 -1
  166. package/dist/memory-worth-outcomes.d.ts +1 -1
  167. package/dist/models-json.d.ts +1 -1
  168. package/dist/namespaces/migrate.d.ts +2 -2
  169. package/dist/namespaces/migrate.js +9 -8
  170. package/dist/namespaces/principal.d.ts +1 -1
  171. package/dist/namespaces/search.d.ts +1 -1
  172. package/dist/namespaces/storage.d.ts +14 -3
  173. package/dist/namespaces/storage.js +8 -7
  174. package/dist/native-knowledge.d.ts +1 -1
  175. package/dist/operator-toolkit.d.ts +1 -1
  176. package/dist/operator-toolkit.js +14 -13
  177. package/dist/orchestration/maintenance.d.ts +2 -2
  178. package/dist/orchestration/maintenance.js +10 -9
  179. package/dist/{orchestrator-CJI4xdqV.d.ts → orchestrator-C9CDWAm6.d.ts} +4 -4
  180. package/dist/orchestrator.d.ts +5 -5
  181. package/dist/orchestrator.js +33 -31
  182. package/dist/patterns-cli.d.ts +1 -1
  183. package/dist/policy-runtime.d.ts +1 -1
  184. package/dist/provenance.d.ts +36 -2
  185. package/dist/provenance.js +5 -1
  186. package/dist/qmd-recall-cache.d.ts +1 -1
  187. package/dist/qmd.d.ts +1 -1
  188. package/dist/recall-disclosure-escalation.d.ts +1 -1
  189. package/dist/recall-explain-renderer.d.ts +1 -1
  190. package/dist/recall-explain-renderer.js +3 -3
  191. package/dist/recall-pipeline-stages.d.ts +15 -0
  192. package/dist/recall-pipeline-stages.js +3 -56
  193. package/dist/recall-pipeline-stages.js.map +1 -1
  194. package/dist/recall-planner-llm.d.ts +1 -1
  195. package/dist/recall-state.d.ts +1 -1
  196. package/dist/recall-tag-filter.d.ts +1 -1
  197. package/dist/recall-xray-cli.d.ts +1 -1
  198. package/dist/recall-xray-cli.js +4 -4
  199. package/dist/recall-xray-renderer.d.ts +1 -1
  200. package/dist/recall-xray-renderer.js +3 -3
  201. package/dist/recall-xray.d.ts +1 -1
  202. package/dist/recall-xray.js +2 -2
  203. package/dist/resolve-auth-token.d.ts +1 -1
  204. package/dist/response-guidance-recall.js +2 -1
  205. package/dist/resume-bundles.js +7 -5
  206. package/dist/retrieval-agents.d.ts +1 -1
  207. package/dist/retrieval-tiers.d.ts +1 -1
  208. package/dist/routing/engine.d.ts +1 -1
  209. package/dist/routing/store.d.ts +1 -1
  210. package/dist/schemas.d.ts +19 -0
  211. package/dist/schemas.js +1 -1
  212. package/dist/search/embed-helper.d.ts +1 -1
  213. package/dist/search/factory.d.ts +1 -1
  214. package/dist/search/index.d.ts +1 -1
  215. package/dist/search/lancedb-backend.d.ts +1 -1
  216. package/dist/search/meilisearch-backend.d.ts +1 -1
  217. package/dist/search/noop-backend.d.ts +1 -1
  218. package/dist/search/orama-backend.d.ts +1 -1
  219. package/dist/search/port.d.ts +1 -1
  220. package/dist/search/remote-backend.d.ts +1 -1
  221. package/dist/{semantic-DJR8_DMQ.d.ts → semantic-SLAa_prH.d.ts} +1 -1
  222. package/dist/{semantic-consolidation-BtUfv-AL.d.ts → semantic-consolidation-DgFXyALl.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +9 -8
  225. package/dist/semantic-rule-promotion.js +8 -7
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +8 -7
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/storage.d.ts +13 -1
  233. package/dist/storage.js +7 -6
  234. package/dist/summarizer.d.ts +1 -1
  235. package/dist/summarizer.js +3 -2
  236. package/dist/summary-snapshot.d.ts +1 -1
  237. package/dist/summary-snapshot.js +2 -1
  238. package/dist/targeted-fact-recall.js +2 -1
  239. package/dist/temporal-supersession.d.ts +1 -1
  240. package/dist/temporal-validity.d.ts +1 -1
  241. package/dist/threading.d.ts +1 -1
  242. package/dist/tier-migration.d.ts +1 -1
  243. package/dist/tier-routing.d.ts +1 -1
  244. package/dist/topics.d.ts +1 -1
  245. package/dist/transcript.d.ts +1 -1
  246. package/dist/transcript.js +2 -2
  247. package/dist/{types-PuiPZ9iE.d.ts → types-MWKPZnM0.d.ts} +25 -1
  248. package/dist/types.d.ts +1 -1
  249. package/dist/types.js +1 -1
  250. package/dist/utility-runtime.d.ts +1 -1
  251. package/dist/utils/serialize-mutations.js +1 -1
  252. package/dist/verified-recall.js +8 -7
  253. package/package.json +2 -2
  254. package/src/access-http.ts +162 -0
  255. package/src/access-service.ts +215 -0
  256. package/src/admin/admin-surfaces.test.ts +458 -0
  257. package/src/admin/admin-surfaces.ts +819 -0
  258. package/src/event-order-recall.test.ts +53 -0
  259. package/src/event-order-recall.ts +61 -37
  260. package/src/explicit-cue-recall.ts +21 -1
  261. package/src/extraction.ts +91 -6
  262. package/src/namespaces/catalog.test.ts +222 -14
  263. package/src/namespaces/catalog.ts +54 -187
  264. package/src/namespaces/storage.ts +87 -80
  265. package/src/orchestrator.ts +46 -5
  266. package/src/provenance-extraction.test.ts +838 -0
  267. package/src/provenance.ts +413 -0
  268. package/src/recall-pipeline-parity.test.ts +288 -0
  269. package/src/recall-pipeline-stages.test.ts +43 -0
  270. package/src/recall-pipeline-stages.ts +22 -0
  271. package/src/response-guidance-recall.ts +32 -36
  272. package/src/schemas.ts +7 -0
  273. package/src/storage.ts +23 -0
  274. package/src/summary-snapshot.test.ts +63 -1
  275. package/src/summary-snapshot.ts +61 -80
  276. package/src/targeted-fact-recall.ts +25 -32
  277. package/src/types.ts +24 -0
  278. package/src/utils/serialize-mutations.ts +10 -6
  279. package/dist/chunk-3E5WRQNQ.js.map +0 -1
  280. package/dist/chunk-DR2JTSLZ.js.map +0 -1
  281. package/dist/chunk-EC2AYKRX.js.map +0 -1
  282. package/dist/chunk-FN2SM5SN.js.map +0 -1
  283. package/dist/chunk-GA5A6MJH.js.map +0 -1
  284. package/dist/chunk-HRUULBBV.js.map +0 -1
  285. package/dist/chunk-PQG4T5V3.js.map +0 -1
  286. package/dist/chunk-R5DB26G6.js.map +0 -1
  287. package/dist/chunk-ROZJACKP.js.map +0 -1
  288. package/dist/chunk-UU6MVCJ6.js.map +0 -1
  289. package/dist/chunk-X74FJSW7.js.map +0 -1
  290. package/dist/chunk-XJNBEDFE.js.map +0 -1
  291. package/dist/chunk-YXLT4EMM.js.map +0 -1
  292. package/dist/chunk-ZPQVJEVQ.js.map +0 -1
  293. /package/dist/{chunk-PCZR32VL.js.map → chunk-3JJWNZTT.js.map} +0 -0
  294. /package/dist/{chunk-YXIFA36P.js.map → chunk-6TAETM63.js.map} +0 -0
  295. /package/dist/{chunk-6JDGADXK.js.map → chunk-6VVP6NK7.js.map} +0 -0
  296. /package/dist/{chunk-CHM274U6.js.map → chunk-AMNLZ6SF.js.map} +0 -0
  297. /package/dist/{chunk-JX3YZVII.js.map → chunk-CZJ6QSKG.js.map} +0 -0
  298. /package/dist/{chunk-GYVVQYA3.js.map → chunk-EOOVJK2U.js.map} +0 -0
  299. /package/dist/{chunk-JKW5XSWC.js.map → chunk-EYJD6KIO.js.map} +0 -0
  300. /package/dist/{chunk-XY4WJTEX.js.map → chunk-EZR35XHX.js.map} +0 -0
  301. /package/dist/{chunk-2NWHLAXX.js.map → chunk-FVI5B7DE.js.map} +0 -0
  302. /package/dist/{chunk-PONNZ54D.js.map → chunk-GY3SKOS4.js.map} +0 -0
  303. /package/dist/{chunk-U33LWTQQ.js.map → chunk-HV57RHMD.js.map} +0 -0
  304. /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
  305. /package/dist/{chunk-HDLC75NX.js.map → chunk-IX72AAMZ.js.map} +0 -0
  306. /package/dist/{chunk-G5PKTQ5J.js.map → chunk-JTKFZMZ7.js.map} +0 -0
  307. /package/dist/{chunk-YMTGXDN6.js.map → chunk-KC6TCAWV.js.map} +0 -0
  308. /package/dist/{chunk-ED35D32I.js.map → chunk-LQ4J7ELC.js.map} +0 -0
  309. /package/dist/{chunk-RJ2THZ4H.js.map → chunk-MBUM2Y3L.js.map} +0 -0
  310. /package/dist/{chunk-T5QAZIBO.js.map → chunk-MOXFPLD6.js.map} +0 -0
  311. /package/dist/{chunk-O54DY26V.js.map → chunk-MXEWQKM7.js.map} +0 -0
  312. /package/dist/{chunk-33L6XHU2.js.map → chunk-NUIJEGVD.js.map} +0 -0
  313. /package/dist/{chunk-EOBJRBLC.js.map → chunk-QGJAGC2J.js.map} +0 -0
  314. /package/dist/{chunk-SMIVW7XC.js.map → chunk-QIMFOCSH.js.map} +0 -0
  315. /package/dist/{chunk-IJEZMWKA.js.map → chunk-SFOAQQDJ.js.map} +0 -0
  316. /package/dist/{chunk-NINRTFSV.js.map → chunk-SINGJCUR.js.map} +0 -0
  317. /package/dist/{chunk-SEWF2O74.js.map → chunk-TGAHHCB6.js.map} +0 -0
  318. /package/dist/{chunk-D75JXBV4.js.map → chunk-WN4GHSDH.js.map} +0 -0
  319. /package/dist/{chunk-K4DWSPMW.js.map → chunk-WRGPE6AW.js.map} +0 -0
  320. /package/dist/{chunk-SDPDU2PM.js.map → chunk-YTMDF6S7.js.map} +0 -0
  321. /package/dist/{chunk-ZDK2IW5F.js.map → chunk-Z7XEIAV4.js.map} +0 -0
  322. /package/dist/{chunk-NHBEO3F3.js.map → chunk-ZCEI242W.js.map} +0 -0
  323. /package/dist/{forget-BEXG5PQC.js.map → forget-6SOIPUMQ.js.map} +0 -0
@@ -0,0 +1,288 @@
1
+ // Issue #1539 PRs 3–6 — migration parity guards.
2
+ //
3
+ // Each recall pipeline (targeted-fact, response-guidance, explicit-cue,
4
+ // event-order) was migrated from an inline dedup/score/threshold/sort block to
5
+ // a thin config object handed to `unifiedDedupeAndRank` (the spine). The
6
+ // per-tier test files (targeted-fact-recall.test.ts etc.) are the end-to-end
7
+ // characterization — they exercise buildXxxRecallSection on full fixtures. This
8
+ // file is the SEAM guard: it pins, per pipeline, that the declared config shape
9
+ // routes through the spine with the correct divergence (sort direction,
10
+ // content-dedup on/off, rank threshold, transform-applied-to-output-but-not-
11
+ // score). If a future refactor flips one of these silently, the matching test
12
+ // here fails BEFORE the end-to-end suites have a chance to mask it.
13
+ //
14
+ // These tests do NOT re-implement the per-tier scorers — those are genuinely
15
+ // per-tier policy and stay in the per-tier test files. Here we use small
16
+ // representative scorers that exercise the divergence dimension each pipeline
17
+ // declares on the spine.
18
+
19
+ import assert from "node:assert/strict";
20
+ import test from "node:test";
21
+
22
+ import type { EvidencePackItem } from "./evidence-pack.js";
23
+ import { unifiedDedupeAndRank } from "./recall-pipeline-stages.js";
24
+
25
+ function item(
26
+ turnIndex: number,
27
+ content: string,
28
+ overrides: Partial<EvidencePackItem> = {},
29
+ ): EvidencePackItem {
30
+ return {
31
+ id: `s1:${turnIndex}`,
32
+ sessionId: "s1",
33
+ turnIndex,
34
+ role: "user",
35
+ content,
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // PR3 — targeted-fact shape: DESC, content-dedup, transform-applied-to-output,
42
+ // score-on-original. No rank threshold.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ test("parity targeted-fact: DESC ordering on score ties (latest turnIndex first)", () => {
46
+ const items = [
47
+ item(1, "early turn"),
48
+ item(5, "late turn"),
49
+ item(3, "middle turn"),
50
+ ];
51
+ // Constant scorer forces score ties so turnIndex DESC is the deciding key.
52
+ const ranked = unifiedDedupeAndRank(items, {
53
+ query: "",
54
+ intents: [],
55
+ scoreEvidence: () => 10,
56
+ });
57
+ assert.equal(ranked[0].turnIndex, 5);
58
+ assert.equal(ranked[1].turnIndex, 3);
59
+ assert.equal(ranked[2].turnIndex, 1);
60
+ });
61
+
62
+ test("parity targeted-fact: content transform applied to output, NOT to scorer input", () => {
63
+ const items = [item(2, "Revenue was $500")];
64
+ // A scorer that reads ORIGINAL content must NOT see the appended cue suffix.
65
+ let scorerObserved = "";
66
+ unifiedDedupeAndRank(items, {
67
+ query: "",
68
+ intents: [],
69
+ scoreEvidence: (original) => {
70
+ scorerObserved = original.content;
71
+ return 1;
72
+ },
73
+ transformContent: (content) => `${content} [CUE]`,
74
+ });
75
+ assert.equal(scorerObserved, "Revenue was $500");
76
+ // ...and the output content carries the transform:
77
+ const ranked = unifiedDedupeAndRank(items, {
78
+ query: "",
79
+ intents: [],
80
+ scoreEvidence: () => 1,
81
+ transformContent: (content) => `${content} [CUE]`,
82
+ });
83
+ assert.equal(ranked[0].content, "Revenue was $500 [CUE]");
84
+ });
85
+
86
+ test("parity targeted-fact: duplicate normalized content collapses (default dedupByContent)", () => {
87
+ const items = [
88
+ item(1, "Same content"),
89
+ item(2, "same content"), // whitespace/case differences normalize equal
90
+ item(3, "Distinct"),
91
+ ];
92
+ const ranked = unifiedDedupeAndRank(items, {
93
+ query: "",
94
+ intents: [],
95
+ scoreEvidence: () => 10,
96
+ });
97
+ assert.equal(ranked.length, 2);
98
+ // First-seen wins: turnIndex 1 is kept, turnIndex 2 (same normalized content)
99
+ // is dropped. Output is then sorted DESC by turnIndex on the score tie.
100
+ assert.deepEqual(
101
+ ranked.map((r) => r.turnIndex),
102
+ [3, 1],
103
+ );
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // PR4 — response-guidance shape: same as targeted-fact (DESC + content-dedup +
108
+ // transform + score-on-original) but with guidance-cue transform. Pin the shape
109
+ // matches; the guidance-specific transform is exercised end-to-end in
110
+ // response-guidance-recall.test.ts.
111
+ // ---------------------------------------------------------------------------
112
+
113
+ test("parity response-guidance: DESC ordering on score ties", () => {
114
+ const items = [
115
+ item(2, "guidance b"),
116
+ item(7, "guidance a"),
117
+ ];
118
+ const ranked = unifiedDedupeAndRank(items, {
119
+ query: "",
120
+ intents: [],
121
+ scoreEvidence: () => 5,
122
+ });
123
+ assert.equal(ranked[0].turnIndex, 7);
124
+ assert.equal(ranked[1].turnIndex, 2);
125
+ });
126
+
127
+ test("parity response-guidance: rank primary key always DESC regardless of turnIndex", () => {
128
+ const items = [
129
+ item(1, "low rank high turnIndex"),
130
+ item(9, "high rank low turnIndex"),
131
+ ];
132
+ const ranked = unifiedDedupeAndRank(items, {
133
+ query: "",
134
+ intents: [],
135
+ scoreEvidence: (original) => (original.content.includes("high rank") ? 20 : 1),
136
+ });
137
+ assert.equal(ranked[0].turnIndex, 9);
138
+ assert.equal(ranked[1].turnIndex, 1);
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // PR5 — explicit-cue shape: the pipeline that OPTS OUT of scoring AND sorting.
143
+ // Its deliberate value ordering (turn references -> content cues -> lexical
144
+ // cues, by evidence type then read-key priority) is the highest-value design
145
+ // choice — preserved verbatim via preserveInsertionOrder + constant scorer +
146
+ // id-only dedup (the spine call is behavior-idempotent; items are already
147
+ // deduped by seenTurns during collection).
148
+ // ---------------------------------------------------------------------------
149
+
150
+ test("parity explicit-cue: preserveInsertionOrder keeps collection order intact", () => {
151
+ // Insertion order is 2, 4, 1. A default sort would reorder to 4, 2, 1.
152
+ // explicit-cue must NOT reorder — the collection order IS the value order.
153
+ const items = [
154
+ item(2, "explicit cue b", { id: "s:2" }),
155
+ item(4, "explicit cue a", { id: "s:4" }),
156
+ item(1, "explicit cue c", { id: "s:1" }),
157
+ ];
158
+ const ranked = unifiedDedupeAndRank(items, {
159
+ query: "",
160
+ intents: [],
161
+ scoreEvidence: () => 0,
162
+ dedupByContent: false,
163
+ preserveInsertionOrder: true,
164
+ });
165
+ assert.deepEqual(
166
+ ranked.map((r) => r.turnIndex),
167
+ [2, 4, 1],
168
+ );
169
+ });
170
+
171
+ test("parity explicit-cue: id-only dedup (dedupByContent false) keeps distinct turns", () => {
172
+ const items = [
173
+ item(1, "same body", { id: "s:1" }),
174
+ item(2, "same body", { id: "s:2" }),
175
+ ];
176
+ const ranked = unifiedDedupeAndRank(items, {
177
+ query: "",
178
+ intents: [],
179
+ scoreEvidence: () => 0,
180
+ dedupByContent: false,
181
+ preserveInsertionOrder: true,
182
+ });
183
+ assert.equal(ranked.length, 2);
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // PR6 — event-order shape: ASC ordering, NO content-dedup (dedupByContent:false
188
+ // keeps distinct turns even when cue-appended bodies match), declared
189
+ // rankThreshold of 6, transform applied to output.
190
+ // ---------------------------------------------------------------------------
191
+
192
+ test("parity event-order: ASC ordering on rank ties (earliest turnIndex first)", () => {
193
+ const items = [
194
+ item(5, "turn five"),
195
+ item(1, "turn one"),
196
+ item(3, "turn three"),
197
+ ];
198
+ const ranked = unifiedDedupeAndRank(items, {
199
+ query: "",
200
+ intents: [],
201
+ scoreEvidence: () => 10,
202
+ turnIndexSortDirection: "asc",
203
+ });
204
+ assert.deepEqual(
205
+ ranked.map((r) => r.turnIndex),
206
+ [1, 3, 5],
207
+ );
208
+ });
209
+
210
+ test("parity event-order: dedupByContent false keeps distinct turns with identical bodies", () => {
211
+ // Two distinct turns (different ids/turnIndex) but identical cue-appended body.
212
+ // Event-order MUST keep both — they are legitimate repeated turns in a
213
+ // chronological transcript. This is the cursor-bugbot a4299851 regression guard.
214
+ const items = [
215
+ item(1, "the user asked again"),
216
+ item(4, "the user asked again"),
217
+ ];
218
+ const ranked = unifiedDedupeAndRank(items, {
219
+ query: "",
220
+ intents: [],
221
+ scoreEvidence: () => 10,
222
+ dedupByContent: false,
223
+ turnIndexSortDirection: "asc",
224
+ });
225
+ assert.equal(ranked.length, 2);
226
+ assert.deepEqual(
227
+ ranked.map((r) => r.turnIndex),
228
+ [1, 4],
229
+ );
230
+ });
231
+
232
+ test("parity event-order: declared rankThreshold (6) drops below-threshold items", () => {
233
+ const items = [
234
+ item(1, "strong", { id: "s:1" }),
235
+ item(2, "weak", { id: "s:2" }),
236
+ item(3, "borderline", { id: "s:3" }),
237
+ ];
238
+ const ranked = unifiedDedupeAndRank(items, {
239
+ query: "",
240
+ intents: [],
241
+ scoreEvidence: (original) => {
242
+ if (original.content === "strong") return 10;
243
+ if (original.content === "borderline") return 6; // exactly threshold -> kept (>=)
244
+ return 5; // weak -> dropped
245
+ },
246
+ rankThreshold: 6,
247
+ turnIndexSortDirection: "asc",
248
+ });
249
+ // rank is ALWAYS DESC primary: strong (10) outranks borderline (6).
250
+ // turnIndex ASC only breaks ties within the same rank.
251
+ assert.deepEqual(
252
+ ranked.map((r) => r.content),
253
+ ["strong", "borderline"],
254
+ );
255
+ });
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Cross-pipeline divergence guard: the SAME corpus produces DIFFERENT orderings
259
+ // under DESC vs ASC config. This is the core #1539 defect class — copy-pasting
260
+ // the comparator into a chronological pipeline silently inverts ordering.
261
+ // ---------------------------------------------------------------------------
262
+
263
+ test("parity divergence: DESC vs ASC configs on the same corpus invert turnIndex ordering", () => {
264
+ const corpus = [
265
+ item(1, "a", { id: "x:1" }),
266
+ item(2, "b", { id: "x:2" }),
267
+ item(3, "c", { id: "x:3" }),
268
+ ];
269
+ const desc = unifiedDedupeAndRank(corpus, {
270
+ query: "",
271
+ intents: [],
272
+ scoreEvidence: () => 10,
273
+ });
274
+ const asc = unifiedDedupeAndRank(corpus, {
275
+ query: "",
276
+ intents: [],
277
+ scoreEvidence: () => 10,
278
+ turnIndexSortDirection: "asc",
279
+ });
280
+ assert.deepEqual(
281
+ desc.map((r) => r.turnIndex),
282
+ [3, 2, 1],
283
+ );
284
+ assert.deepEqual(
285
+ asc.map((r) => r.turnIndex),
286
+ [1, 2, 3],
287
+ );
288
+ });
@@ -267,3 +267,46 @@ test("unifiedDedupeAndRank: dedupByContent true (default) still collapses identi
267
267
  });
268
268
  assert.equal(resultExplicit.length, 1, "explicit dedupByContent: true collapses identical content");
269
269
  });
270
+
271
+ test("unifiedDedupeAndRank: preserveInsertionOrder skips the sort stage (explicit-cue shape)", () => {
272
+ // Insertion order is turn-index 3, 1, 2. A DESC sort would reorder to 3, 2, 1.
273
+ // preserveInsertionOrder must keep the original (post-dedup) order: 3, 1, 2.
274
+ const items: EvidencePackItem[] = [
275
+ item(3, "turn ref", { id: "s:3" }),
276
+ item(1, "content cue", { id: "s:1" }),
277
+ item(2, "lexical hit", { id: "s:2" }),
278
+ ];
279
+ const result = unifiedDedupeAndRank(items, {
280
+ query: "q",
281
+ intents: NO_INTENTS,
282
+ scoreEvidence: () => 0, // explicit-cue does not score
283
+ dedupByContent: false, // explicit-cue dedupes by id only (seenTurns)
284
+ preserveInsertionOrder: true,
285
+ });
286
+ assert.deepEqual(
287
+ result.map((r) => r.id),
288
+ ["s:3", "s:1", "s:2"],
289
+ "insertion order is preserved — no sort applied",
290
+ );
291
+ });
292
+
293
+ test("unifiedDedupeAndRank: preserveInsertionOrder still applies threshold filter", () => {
294
+ const items: EvidencePackItem[] = [
295
+ item(1, "strong", { id: "s:1" }),
296
+ item(2, "weak", { id: "s:2" }),
297
+ item(3, "medium", { id: "s:3" }),
298
+ ];
299
+ const result = unifiedDedupeAndRank(items, {
300
+ query: "q",
301
+ intents: NO_INTENTS,
302
+ scoreEvidence: (original) => (original.content === "strong" ? 10 : original.content === "medium" ? 5 : 1),
303
+ rankThreshold: 5,
304
+ dedupByContent: false,
305
+ preserveInsertionOrder: true,
306
+ });
307
+ // threshold drops "weak" (rank 1); insertion order of survivors preserved.
308
+ assert.deepEqual(
309
+ result.map((r) => r.id),
310
+ ["s:1", "s:3"],
311
+ );
312
+ });
@@ -125,6 +125,21 @@ export interface UnifiedRankConfig<TIntent> {
125
125
  * The rank primary key is ALWAYS DESC regardless of this setting.
126
126
  */
127
127
  turnIndexSortDirection?: TurnIndexSortDirection;
128
+ /**
129
+ * When `true`, skip the sort stage entirely and return items in their
130
+ * post-dedup insertion order (first-seen first). Used by explicit-cue,
131
+ * whose deliberate value ordering — turn references → content cues →
132
+ * lexical cues, gathered by evidence TYPE then read-key priority — is the
133
+ * pipeline's highest-value design choice. A rank-based sort would invert
134
+ * it (turn references are deliberately UNSCORED while lexical hits carry
135
+ * numeric scores; sorting by score would demote turn references below
136
+ * weak lexical hits — cursor[bot] / codex P2 on #1505).
137
+ *
138
+ * Score and threshold-filter still apply when declared; only the sort is
139
+ * skipped. The constant scorer (`scoreEvidence: () => 0`) makes the
140
+ * no-scoring policy explicit alongside this flag.
141
+ */
142
+ preserveInsertionOrder?: boolean;
128
143
  }
129
144
 
130
145
  /**
@@ -232,6 +247,13 @@ export function unifiedDedupeAndRank<TIntent>(
232
247
  // turnIndex direction is configurable: DESC (relevance) or ASC (chronology).
233
248
  // The tertiary tiebreaker follows the direction:
234
249
  // DESC → score DESC; ASC → content.localeCompare.
250
+ //
251
+ // explicit-cue opts out of sorting entirely (preserveInsertionOrder) — its
252
+ // deliberate value ordering is the collection order (turn references →
253
+ // content cues → lexical cues), and a rank-based sort would invert it.
254
+ if (config.preserveInsertionOrder) {
255
+ return filtered;
256
+ }
235
257
  return filtered.sort(makeComparator(direction));
236
258
  }
237
259
 
@@ -5,6 +5,11 @@ import {
5
5
  resolveLcmReadSessionIds,
6
6
  } from "./lcm-fallback-read.js";
7
7
 
8
+ import {
9
+ unifiedDedupeAndRank,
10
+ type RankedEvidenceItem,
11
+ } from "./recall-pipeline-stages.js";
12
+
8
13
  export interface ResponseGuidanceRecallOptions {
9
14
  engine: ExplicitCueRecallEngine | null | undefined;
10
15
  sessionId?: string;
@@ -24,10 +29,6 @@ export interface ResponseGuidanceRecallOptions {
24
29
  forceGeneric?: boolean;
25
30
  }
26
31
 
27
- interface RankedGuidanceItem extends EvidencePackItem {
28
- rank: number;
29
- }
30
-
31
32
  type GuidanceIntent =
32
33
  | "dates"
33
34
  | "editing"
@@ -341,42 +342,37 @@ async function collectGuidanceScanItems(
341
342
  return items;
342
343
  }
343
344
 
345
+ /**
346
+ * Issue #1539 PR4: response-guidance now routes dedup/score/threshold/sort through
347
+ * the unified spine (`unifiedDedupeAndRank`). The divergence that lived inline
348
+ * here — append-guidance-cues content transform (two-arg: content + intents),
349
+ * relevance scoring on ORIGINAL content, default DESC turn-index ordering with
350
+ * -1 sentinel for missing turns — is now declared config on the spine. The score
351
+ * and appendGuidanceCues helpers stay in this module because they are genuinely
352
+ * per-tier policy; only the duplicated dedup/sort/threshold machinery moved out.
353
+ *
354
+ * Behavior is byte-for-byte identical to the inline implementation it replaces
355
+ * (the spine was extracted to match it exactly; see recall-pipeline-stages.ts).
356
+ * The response-guidance-recall.test.ts suite is the characterization.
357
+ *
358
+ * Note: response-guidance (like the pre-migration code) dedups on the TRANSFORMED
359
+ * (cue-appended) content, not the original — this matches the spine's default
360
+ * (transform-then-dedup) exactly. No divergence from prior behavior.
361
+ */
344
362
  function rankAndDedupeGuidanceItems(
345
363
  items: EvidencePackItem[],
346
364
  query: string,
347
365
  intents: readonly GuidanceIntent[],
348
- ): RankedGuidanceItem[] {
349
- const seenIds = new Set<string>();
350
- const seenContent = new Set<string>();
351
- const ranked: RankedGuidanceItem[] = [];
352
-
353
- for (const item of items) {
354
- const id = item.id ?? (
355
- item.sessionId && typeof item.turnIndex === "number"
356
- ? `${item.sessionId}:${item.turnIndex}`
357
- : undefined
358
- );
359
- if (id && seenIds.has(id)) continue;
360
-
361
- const enhancedContent = appendGuidanceCues(item.content, intents);
362
- const contentKey = enhancedContent.toLowerCase().replace(/\s+/g, " ").trim();
363
- if (seenContent.has(contentKey)) continue;
364
-
365
- if (id) seenIds.add(id);
366
- seenContent.add(contentKey);
367
- ranked.push({
368
- ...item,
369
- content: enhancedContent,
370
- rank: scoreGuidanceEvidence(item, query, intents),
371
- });
372
- }
373
-
374
- return ranked.sort((left, right) => {
375
- if (right.rank !== left.rank) return right.rank - left.rank;
376
- const leftTurn = typeof left.turnIndex === "number" ? left.turnIndex : -1;
377
- const rightTurn = typeof right.turnIndex === "number" ? right.turnIndex : -1;
378
- if (rightTurn !== leftTurn) return rightTurn - leftTurn;
379
- return (right.score ?? 0) - (left.score ?? 0);
366
+ ): RankedEvidenceItem[] {
367
+ // The spine's config.intents is typed TIntent[] (readability contract — it is
368
+ // never mutated). Cast the readonly array through; safe because the spine only
369
+ // forwards intents to the score/transform callbacks.
370
+ const mutableIntents = intents as GuidanceIntent[];
371
+ return unifiedDedupeAndRank(items, {
372
+ query,
373
+ intents: mutableIntents,
374
+ scoreEvidence: (item, q, intentArr) => scoreGuidanceEvidence(item, q, intentArr),
375
+ transformContent: (content, intentArr) => appendGuidanceCues(content, intentArr),
380
376
  });
381
377
  }
382
378
 
package/src/schemas.ts CHANGED
@@ -93,6 +93,13 @@ export const ExtractedFactSchema = z.object({
93
93
  .describe(
94
94
  'Scope classification: "global" for cross-project knowledge (framework bugs, library behavior, API patterns, user preferences, tool configs, general coding patterns); "project" for project-specific knowledge (file paths, env configs, deployment details, project workarounds). Defaults to "project" when a coding context is active.',
95
95
  ),
96
+ quote: z
97
+ .string()
98
+ .optional()
99
+ .nullable()
100
+ .describe(
101
+ "The EXACT verbatim words from the conversation that support this fact. Copy a contiguous span from a single speaker turn (not a paraphrase). Cap at ~300 characters. This is the grounding evidence for downstream faithfulness verification (issue #1575).",
102
+ ),
96
103
  structuredAttributes: z
97
104
  .record(z.string(), z.string())
98
105
  .optional()
package/src/storage.ts CHANGED
@@ -88,6 +88,7 @@ import type {
88
88
  ScoredEntity,
89
89
  TopicScore,
90
90
  FileHygieneConfig,
91
+ ProvenanceSource,
91
92
  } from "./types.js";
92
93
  import { confidenceTier, SPECULATIVE_TTL_DAYS } from "./types.js";
93
94
  import {
@@ -3608,6 +3609,14 @@ export class StorageManager {
3608
3609
  * can consume it. Absent = gate was off or fact predates #1576.
3609
3610
  */
3610
3611
  faithfulness?: import("./types.js").FaithfulnessFrontmatter;
3612
+ /**
3613
+ * Claim-level provenance spans (issue #1575 PR 2). When provided,
3614
+ * persisted to frontmatter so downstream readers (memory_get, x-ray,
3615
+ * faithfulness gate #1576) can consume them. Absent = fact predates
3616
+ * #1575 or provenance was disabled.
3617
+ */
3618
+ sources?: ProvenanceSource[];
3619
+ provenance?: "verified" | "unverified" | "none";
3611
3620
  } = {},
3612
3621
  ): Promise<string> {
3613
3622
  await this.ensureDirectories();
@@ -3681,6 +3690,14 @@ export class StorageManager {
3681
3690
  if (options.faithfulness !== undefined) {
3682
3691
  fm.faithfulness = options.faithfulness;
3683
3692
  }
3693
+ // Claim-level provenance (issue #1575 PR 2). Wire through to frontmatter
3694
+ // so verified spans survive extraction → storage → memory_get end-to-end.
3695
+ if (options.sources !== undefined) {
3696
+ fm.sources = options.sources;
3697
+ }
3698
+ if (options.provenance !== undefined) {
3699
+ fm.provenance = options.provenance;
3700
+ }
3684
3701
 
3685
3702
  // Append structured attributes as searchable suffix so QMD indexes them.
3686
3703
  // normalizeAttributePairs sorts and lowercases keys so the enriched content
@@ -7338,6 +7355,10 @@ export class StorageManager {
7338
7355
  status?: import("./types.js").MemoryStatus;
7339
7356
  /** Faithfulness gate verdict (issue #1576), propagated from the parent fact. */
7340
7357
  faithfulness?: import("./types.js").FaithfulnessFrontmatter;
7358
+ /** Claim-level provenance spans (issue #1575 PR 2), propagated from the parent fact so a chunk surfaced independently (memory_get/x-ray) preserves them (chatgpt-codex-connector thread Ocvmo). */
7359
+ sources?: ProvenanceSource[];
7360
+ /** Coarse provenance tag (issue #1575 PR 2), propagated from the parent. */
7361
+ provenance?: "verified" | "unverified" | "none";
7341
7362
  } = {},
7342
7363
  ): Promise<string> {
7343
7364
  await this.ensureDirectories();
@@ -7369,6 +7390,8 @@ export class StorageManager {
7369
7390
  valid_at: validAt,
7370
7391
  ...(options.status ? { status: options.status } : {}),
7371
7392
  ...(options.faithfulness ? { faithfulness: options.faithfulness } : {}),
7393
+ ...(options.sources ? { sources: options.sources } : {}),
7394
+ ...(options.provenance ? { provenance: options.provenance } : {}),
7372
7395
  };
7373
7396
 
7374
7397
  const sanitized = sanitizeMemoryContent(content);
@@ -2,7 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { mkdtemp, mkdir, open, readFile, unlink, writeFile } from "node:fs/promises";
5
+ import { mkdtemp, mkdir, open, readFile, rm, unlink, writeFile } from "node:fs/promises";
6
6
  import { HourlySummarizer } from "./summarizer.js";
7
7
  import {
8
8
  readSummarySnapshot,
@@ -756,3 +756,65 @@ test("hourly transcript lookup reads encoded transcript channel directories", as
756
756
  ["encoded transcript"],
757
757
  );
758
758
  });
759
+
760
+ // ── Issue #1524 adoption prove-fail: summary-snapshot upserts must run through
761
+ // the shared serializeMutations chain (rejection-recovering). The defect class
762
+ // is a naive bare-.then(fn) chain that silently drops subsequent tasks after a
763
+ // rejection: if upsert A rejects (e.g. transient I/O error) and the chain does
764
+ // not recover, upsert B queued behind it never runs — its hour goes missing
765
+ // from the snapshot. We seed the failure by rejecting the first upsert (a
766
+ // hostile lock directory that throws on mkdir), then upsert B must STILL land
767
+ // its hour. Pre-fix (a poison chain) B would be skipped.
768
+ test("upsertSummarySnapshot recovers after a prior upsert rejects (issue #1524 poison-chain prove-fail)", async () => {
769
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-summary-poison-chain-"));
770
+ const sessionKey = "session-poison";
771
+ try {
772
+ // The lock directory for sessionKey is <root>/state/summaries/<encoded>.lock.
773
+ // Pre-create a FILE at the summaries root so the upsert's mkdir(dirname)
774
+ // inside writeSummarySnapshot throws ENOTDIR — a deterministic rejection
775
+ // that does not depend on filesystem timing. Both upserts target the SAME
776
+ // sessionKey so they serialize through the same chain.
777
+ const summariesRoot = path.join(memoryDir, "state", "summaries");
778
+ await mkdir(path.dirname(summariesRoot), { recursive: true });
779
+ await writeFile(summariesRoot, "not-a-dir", "utf-8");
780
+
781
+ const summaryA = {
782
+ hour: "2026-03-26T08:00:00.000Z",
783
+ sessionKey,
784
+ bullets: ["A"],
785
+ turnCount: 1,
786
+ generatedAt: "2026-03-26T08:15:00.000Z",
787
+ };
788
+ const summaryB = {
789
+ hour: "2026-03-26T09:00:00.000Z",
790
+ sessionKey,
791
+ bullets: ["B"],
792
+ turnCount: 1,
793
+ generatedAt: "2026-03-26T09:15:00.000Z",
794
+ };
795
+
796
+ // Upsert A rejects (hostile summaries root). We MUST catch — the contract
797
+ // is that the chain recovers, not that the failing op silently succeeds.
798
+ await upsertSummarySnapshot(memoryDir, summaryA).then(
799
+ () => assert.fail("upsert A should have rejected on the hostile summaries root"),
800
+ () => undefined,
801
+ );
802
+
803
+ // Restore a usable summaries root so upsert B can actually write.
804
+ await unlink(summariesRoot);
805
+ await mkdir(summariesRoot, { recursive: true });
806
+
807
+ // Upsert B MUST still run despite A's rejection. With a poison chain B
808
+ // would be skipped (no snapshot, no B-hour bullet).
809
+ await upsertSummarySnapshot(memoryDir, summaryB);
810
+
811
+ const snapshot = await readSummarySnapshot(memoryDir, sessionKey);
812
+ assert.ok(snapshot, "upsert B landed after upsert A rejected (chain recovered)");
813
+ assert.ok(
814
+ snapshot.some((s) => s.bullets.includes("B")),
815
+ "upsert B's hour survived — the chain was not poisoned by A's rejection",
816
+ );
817
+ } finally {
818
+ await rm(memoryDir, { recursive: true, force: true });
819
+ }
820
+ });