@remnic/core 9.65.2 → 9.65.3

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 (449) hide show
  1. package/dist/access-admin-ops-surface.d.ts +44 -44
  2. package/dist/access-admin-ops-surface.js +14 -14
  3. package/dist/access-authorization-probe.d.ts +38 -38
  4. package/dist/access-authorization-probe.js +15 -15
  5. package/dist/access-boundary.d.ts +40 -40
  6. package/dist/access-boundary.js +14 -14
  7. package/dist/access-cli.js +52 -52
  8. package/dist/access-coding-context-resolution.d.ts +3 -1
  9. package/dist/access-extraction-force-flush.d.ts +25 -25
  10. package/dist/access-extraction-force-flush.js +14 -14
  11. package/dist/access-health-types.d.ts +3 -1
  12. package/dist/access-http-lcm-compaction.d.ts +39 -39
  13. package/dist/access-http-lifecycle-flush.d.ts +39 -39
  14. package/dist/access-http-offline-stream.d.ts +25 -25
  15. package/dist/access-http-query.d.ts +4 -2
  16. package/dist/access-http-query.js +16 -16
  17. package/dist/access-http.d.ts +51 -44
  18. package/dist/access-http.js +30 -30
  19. package/dist/access-identity-continuity-surface.d.ts +12 -12
  20. package/dist/access-identity-continuity-surface.js +14 -14
  21. package/dist/access-lcm-surface.d.ts +27 -27
  22. package/dist/access-lcm-surface.js +14 -14
  23. package/dist/access-mcp-admin-tools.d.ts +9 -1
  24. package/dist/access-mcp-admin-tools.js +3 -1
  25. package/dist/access-mcp-output-schemas.js +1 -1
  26. package/dist/access-mcp.d.ts +39 -39
  27. package/dist/access-mcp.js +24 -24
  28. package/dist/access-memory-search-fanout.d.ts +4 -2
  29. package/dist/{access-namespace-preflight-Dhvcl-T0.d.ts → access-namespace-preflight-B7Usyz-K.d.ts} +1 -1
  30. package/dist/access-namespace-preflight.d.ts +5 -3
  31. package/dist/access-namespace-preflight.js +14 -14
  32. package/dist/access-observe-write-surface.d.ts +39 -39
  33. package/dist/access-observe-write-surface.js +14 -14
  34. package/dist/access-offline-manifest.d.ts +12 -12
  35. package/dist/access-operations-batch.js +18 -18
  36. package/dist/access-operations.d.ts +40 -40
  37. package/dist/access-operations.js +21 -21
  38. package/dist/access-recall-concurrency.d.ts +40 -40
  39. package/dist/access-recall-concurrency.js +14 -14
  40. package/dist/access-recall-response.d.ts +24 -24
  41. package/dist/access-recall-response.js +14 -14
  42. package/dist/access-recall-surface.d.ts +24 -24
  43. package/dist/access-recall-surface.js +14 -14
  44. package/dist/access-schema.d.ts +45 -45
  45. package/dist/{access-service-Bke1alZO.d.ts → access-service-DkaIG3n8.d.ts} +219 -75
  46. package/dist/access-service-helpers.d.ts +40 -40
  47. package/dist/access-service.d.ts +47 -47
  48. package/dist/access-service.js +14 -14
  49. package/dist/access-surface-catalog.d.ts +40 -40
  50. package/dist/access-surface-catalog.js +20 -16
  51. package/dist/access-surface-catalog.js.map +1 -1
  52. package/dist/access-wearables-meetings-surface.d.ts +5 -3
  53. package/dist/action-confidence.d.ts +3 -1
  54. package/dist/active-memory-bridge.d.ts +3 -1
  55. package/dist/active-recall.d.ts +3 -1
  56. package/dist/active-recall.js +6 -1
  57. package/dist/active-recall.js.map +1 -1
  58. package/dist/ambient-provenance.d.ts +3 -1
  59. package/dist/artifact-search.d.ts +3 -1
  60. package/dist/behavior-learner.d.ts +3 -1
  61. package/dist/behavior-signals.d.ts +3 -1
  62. package/dist/bootstrap.d.ts +12 -12
  63. package/dist/briefing-window.d.ts +4 -2
  64. package/dist/briefing.d.ts +4 -2
  65. package/dist/briefing.js +3 -3
  66. package/dist/buffer-surprise-report.d.ts +3 -1
  67. package/dist/buffer-turn-helpers.d.ts +3 -1
  68. package/dist/buffer.d.ts +4 -2
  69. package/dist/bulk-import/index.d.ts +5 -3
  70. package/dist/calibration.d.ts +3 -1
  71. package/dist/capabilities.d.ts +3 -1
  72. package/dist/{catalog-DV6qnbr7.d.ts → catalog-CX2BZEJh.d.ts} +1 -1
  73. package/dist/causal-behavior.d.ts +1 -1
  74. package/dist/causal-consolidation.d.ts +3 -1
  75. package/dist/causal-consolidation.js +7 -7
  76. package/dist/causal-trajectory-graph.d.ts +1 -1
  77. package/dist/{chunk-R7JAM2XF.js → chunk-2VXWFYJE.js} +3 -3
  78. package/dist/{chunk-UXO5IMBK.js → chunk-4XDGYCZI.js} +2 -2
  79. package/dist/{chunk-OTHZWLUD.js → chunk-57HVIG5U.js} +2 -2
  80. package/dist/{chunk-EILZG5VC.js → chunk-6TAX66IA.js} +2 -2
  81. package/dist/{chunk-NKKD4ZAU.js → chunk-6VALIZKG.js} +2 -2
  82. package/dist/{chunk-BWLRUMOG.js → chunk-76PL4XLG.js} +435 -1
  83. package/dist/chunk-76PL4XLG.js.map +1 -0
  84. package/dist/{chunk-2E36UXVW.js → chunk-7KFIMGMP.js} +2 -2
  85. package/dist/{chunk-LGO6A7GJ.js → chunk-7QOO4LGC.js} +28 -28
  86. package/dist/{chunk-TBOJ76ER.js → chunk-AB3A7EFH.js} +18 -2
  87. package/dist/{chunk-TBOJ76ER.js.map → chunk-AB3A7EFH.js.map} +1 -1
  88. package/dist/{chunk-DZUJ46DO.js → chunk-AUO2LJ3K.js} +2 -2
  89. package/dist/{chunk-ZO6AXLEH.js → chunk-AYDDB3XI.js} +2 -2
  90. package/dist/{chunk-NTWN6HB2.js → chunk-AYYGRVRP.js} +2 -2
  91. package/dist/{chunk-7YQ4IYS2.js → chunk-AZAA5DNP.js} +2 -2
  92. package/dist/{chunk-4KYOLI6M.js → chunk-BKYPJUQL.js} +120 -14
  93. package/dist/chunk-BKYPJUQL.js.map +1 -0
  94. package/dist/{chunk-Z7PBQPJ4.js → chunk-BQUEAVNW.js} +2 -2
  95. package/dist/{chunk-35NN3ZFA.js → chunk-CTVWELNK.js} +21 -3
  96. package/dist/chunk-CTVWELNK.js.map +1 -0
  97. package/dist/{chunk-5YQ63T4B.js → chunk-D27J626P.js} +2 -2
  98. package/dist/{chunk-LZH5FR2W.js → chunk-DHTQP334.js} +2 -2
  99. package/dist/{chunk-Y7DK7HHJ.js → chunk-E4MVQQVX.js} +2 -2
  100. package/dist/{chunk-JYYBA7TF.js → chunk-EIVIB73I.js} +2 -2
  101. package/dist/{chunk-S2DPMJXH.js → chunk-EV3YFLCB.js} +2 -2
  102. package/dist/{chunk-TZNQUNKL.js → chunk-FC7TIRFS.js} +15 -1
  103. package/dist/chunk-FC7TIRFS.js.map +1 -0
  104. package/dist/{chunk-3I7ASZN3.js → chunk-G57LQRP6.js} +2 -2
  105. package/dist/{chunk-UIN2BWDD.js → chunk-GN55KU2P.js} +2 -2
  106. package/dist/{chunk-PE255F4T.js → chunk-GUXS322H.js} +2 -2
  107. package/dist/{chunk-V3MXUL3D.js → chunk-H5FW4S5Q.js} +319 -643
  108. package/dist/chunk-H5FW4S5Q.js.map +1 -0
  109. package/dist/chunk-I32NKXE2.js +879 -0
  110. package/dist/chunk-I32NKXE2.js.map +1 -0
  111. package/dist/{chunk-YB42WZCY.js → chunk-IQSC3V2E.js} +2 -2
  112. package/dist/{chunk-MN4KB2A4.js → chunk-MBDJJTAT.js} +3 -3
  113. package/dist/{chunk-SQI57UCJ.js → chunk-MRTUN665.js} +2 -2
  114. package/dist/{chunk-SBDCZ2AX.js → chunk-NXKJHSDN.js} +1 -1
  115. package/dist/chunk-NXKJHSDN.js.map +1 -0
  116. package/dist/{chunk-SPUJNJJC.js → chunk-NZWEEALH.js} +2 -2
  117. package/dist/{chunk-2XDWWPTN.js → chunk-OJD4WS2E.js} +2 -2
  118. package/dist/{chunk-ZWXJBFZU.js → chunk-OLWTCWQ2.js} +8 -8
  119. package/dist/{chunk-XRYZRW54.js → chunk-P4DHENRE.js} +2 -2
  120. package/dist/{chunk-J275IYMG.js → chunk-PAYGC3BD.js} +29 -2
  121. package/dist/chunk-PAYGC3BD.js.map +1 -0
  122. package/dist/{chunk-IID2CMUU.js → chunk-PDVQ5JU3.js} +2 -2
  123. package/dist/{chunk-7BY7V4YX.js → chunk-Q4WONDMJ.js} +4 -3
  124. package/dist/{chunk-7BY7V4YX.js.map → chunk-Q4WONDMJ.js.map} +1 -1
  125. package/dist/{chunk-5NM4OH4P.js → chunk-QFMO32TO.js} +3 -3
  126. package/dist/{chunk-SIHSCCUQ.js → chunk-QXIQPCCN.js} +1 -1
  127. package/dist/{chunk-2YI72KM3.js → chunk-R47WINCY.js} +2 -2
  128. package/dist/{chunk-ZQHI2BZB.js → chunk-RK4WXUI2.js} +2 -2
  129. package/dist/{chunk-QOS5LBZN.js → chunk-S7Q5CM7E.js} +2 -2
  130. package/dist/{chunk-GDFVPUJ5.js → chunk-TDKSCV62.js} +2 -2
  131. package/dist/{chunk-OCZIYAE5.js → chunk-TYUS5TLG.js} +3 -3
  132. package/dist/{chunk-GNCCUR6K.js → chunk-URHCUXKI.js} +2 -2
  133. package/dist/{chunk-TF77C7SV.js → chunk-VA5KMSAE.js} +7 -5
  134. package/dist/chunk-VA5KMSAE.js.map +1 -0
  135. package/dist/{chunk-SB7D4BT7.js → chunk-WN5FSGLE.js} +1545 -105
  136. package/dist/chunk-WN5FSGLE.js.map +1 -0
  137. package/dist/{chunk-YLDXWCZP.js → chunk-YF6UHWOV.js} +14 -9
  138. package/dist/chunk-YF6UHWOV.js.map +1 -0
  139. package/dist/{chunk-CT56C22C.js → chunk-Z4RPXHJH.js} +4 -4
  140. package/dist/{cli-cShQ13XG.d.ts → cli-ChKeYMZv.d.ts} +5 -5
  141. package/dist/cli.d.ts +25 -25
  142. package/dist/cli.js +44 -43
  143. package/dist/coding/pre-action-gate.d.ts +2 -60
  144. package/dist/compounding/engine.d.ts +4 -2
  145. package/dist/compounding/engine.js +3 -3
  146. package/dist/compounding/preference-consolidator.d.ts +3 -1
  147. package/dist/compression-optimizer.d.ts +3 -1
  148. package/dist/config.d.ts +3 -1
  149. package/dist/config.js +6 -1
  150. package/dist/connectors/codex-materialize-runner.d.ts +3 -1
  151. package/dist/connectors/codex-materialize-runner.js +3 -3
  152. package/dist/connectors/codex-materialize.d.ts +3 -1
  153. package/dist/connectors/index.d.ts +3 -1
  154. package/dist/connectors/index.js +3 -3
  155. package/dist/consolidation-provenance-check.d.ts +4 -2
  156. package/dist/consolidation-undo.d.ts +4 -2
  157. package/dist/contradiction/index.d.ts +10 -270
  158. package/dist/contradiction/index.js +11 -6
  159. package/dist/{contradiction-review-QKJH5DKD.js → contradiction-review-WRVLRWHS.js} +6 -2
  160. package/dist/{contradiction-scan-VOTQRRSX.js → contradiction-scan-AKUWPX5N.js} +5 -3
  161. package/dist/converge-config.d.ts +3 -1
  162. package/dist/convergence-refresh.d.ts +4 -2
  163. package/dist/convergence-refresh.js +5 -5
  164. package/dist/conversation-index/backend.d.ts +3 -1
  165. package/dist/conversation-index/chunker.d.ts +3 -1
  166. package/dist/conversation-index/faiss-adapter.d.ts +3 -1
  167. package/dist/conversation-index/indexer.d.ts +3 -1
  168. package/dist/conversation-index/search.d.ts +3 -1
  169. package/dist/corpus-watermark.d.ts +3 -1
  170. package/dist/corpus-watermark.js +5 -5
  171. package/dist/cross-lingual-recall.d.ts +3 -1
  172. package/dist/day-summary.d.ts +3 -1
  173. package/dist/delinearize.d.ts +3 -1
  174. package/dist/dependency-propagation-config.d.ts +3 -1
  175. package/dist/{dependency-propagation-delivery-CsGg-VwS.d.ts → dependency-propagation-delivery-B8ZMBBCY.d.ts} +1 -1
  176. package/dist/direct-answer-wiring.d.ts +3 -1
  177. package/dist/direct-answer.d.ts +3 -1
  178. package/dist/embedding-fallback.d.ts +3 -1
  179. package/dist/enrichment/index.d.ts +3 -1
  180. package/dist/entity-origin-fields.d.ts +3 -1
  181. package/dist/entity-retrieval.d.ts +4 -2
  182. package/dist/entity-retrieval.js +3 -3
  183. package/dist/entity-schema.d.ts +3 -1
  184. package/dist/episodic-context.d.ts +3 -1
  185. package/dist/explicit-capture.d.ts +12 -12
  186. package/dist/external-wiki-access.d.ts +40 -40
  187. package/dist/external-wiki-access.js +15 -15
  188. package/dist/external-wiki-collection-registration.d.ts +3 -1
  189. package/dist/external-wiki-collection.d.ts +3 -1
  190. package/dist/external-wiki-mcp-tools.d.ts +40 -40
  191. package/dist/extraction-error-classification.d.ts +3 -1
  192. package/dist/extraction-faithfulness.d.ts +3 -1
  193. package/dist/extraction-judge-telemetry.d.ts +3 -1
  194. package/dist/extraction-judge-training.d.ts +3 -1
  195. package/dist/extraction-judge.d.ts +3 -1
  196. package/dist/extraction-liveness.d.ts +3 -1
  197. package/dist/extraction-normalization.d.ts +3 -1
  198. package/dist/extraction-prompt.d.ts +3 -1
  199. package/dist/extraction-source-grounding-rules.d.ts +3 -1
  200. package/dist/extraction-source-grounding.d.ts +3 -1
  201. package/dist/extraction.d.ts +3 -1
  202. package/dist/extraction.js +3 -3
  203. package/dist/fallback-llm.d.ts +3 -1
  204. package/dist/graph-dashboard-diff.d.ts +3 -1
  205. package/dist/graph-dashboard-key.d.ts +3 -1
  206. package/dist/graph-dashboard-parser.d.ts +3 -1
  207. package/dist/graph-edge-reinforcement.d.ts +3 -1
  208. package/dist/graph-path-reconstruction.d.ts +3 -1
  209. package/dist/graph-path-scoring.d.ts +3 -1
  210. package/dist/graph-snapshot.d.ts +3 -1
  211. package/dist/graph.d.ts +3 -1
  212. package/dist/harmonic-retrieval.js +1 -1
  213. package/dist/importance.d.ts +3 -1
  214. package/dist/importers/index.d.ts +3 -1
  215. package/dist/in-flight-reads.d.ts +3 -1
  216. package/dist/index-CexDrJzC.d.ts +284 -0
  217. package/dist/index.d.ts +251 -29
  218. package/dist/index.js +129 -75
  219. package/dist/intent.d.ts +3 -1
  220. package/dist/lcm/engine.d.ts +3 -1
  221. package/dist/lcm/index.d.ts +3 -1
  222. package/dist/lcm/tools.d.ts +3 -1
  223. package/dist/lifecycle.d.ts +3 -1
  224. package/dist/live-connectors-runner.d.ts +3 -1
  225. package/dist/local-llm.d.ts +3 -1
  226. package/dist/local-model-endpoint.d.ts +3 -1
  227. package/dist/maintenance/memory-governance.d.ts +3 -1
  228. package/dist/maintenance/memory-governance.js +3 -3
  229. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +4 -2
  230. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  231. package/dist/maintenance/rebuild-memory-projection.d.ts +4 -2
  232. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  233. package/dist/{maintenance-jhMpeGne.d.ts → maintenance-nkdtSrVr.d.ts} +3 -3
  234. package/dist/mcp-memory-inspector-app.d.ts +37 -37
  235. package/dist/memory-action-policy.d.ts +3 -1
  236. package/dist/memory-cache.d.ts +3 -1
  237. package/dist/memory-lifecycle-ledger-utils.d.ts +3 -1
  238. package/dist/memory-projection-store.d.ts +3 -1
  239. package/dist/memory-provenance.d.ts +3 -1
  240. package/dist/memory-snapshot.d.ts +3 -1
  241. package/dist/memory-subject-cli.d.ts +40 -40
  242. package/dist/memory-subject.d.ts +42 -42
  243. package/dist/memory-worth-outcomes.d.ts +4 -2
  244. package/dist/models-json.d.ts +3 -1
  245. package/dist/namespaces/migrate.d.ts +5 -3
  246. package/dist/namespaces/migrate.js +4 -4
  247. package/dist/namespaces/principal.d.ts +3 -1
  248. package/dist/namespaces/search.d.ts +3 -1
  249. package/dist/namespaces/storage.d.ts +5 -3
  250. package/dist/namespaces/storage.js +3 -3
  251. package/dist/native-knowledge.d.ts +3 -1
  252. package/dist/offline-sync-impression-drain.d.ts +3 -1
  253. package/dist/operator-doctor-corpus.d.ts +3 -1
  254. package/dist/operator-doctor-corpus.js +6 -6
  255. package/dist/operator-doctor-replica.d.ts +3 -1
  256. package/dist/operator-toolkit.d.ts +5 -3
  257. package/dist/operator-toolkit.js +15 -12
  258. package/dist/orchestration/compression-guideline-coordinator.d.ts +4 -2
  259. package/dist/orchestration/maintenance.d.ts +6 -4
  260. package/dist/orchestration/maintenance.js +9 -9
  261. package/dist/{orchestrator-DyGUC7re.d.ts → orchestrator-Bvq1-RQs.d.ts} +23 -9
  262. package/dist/orchestrator.d.ts +12 -12
  263. package/dist/orchestrator.js +52 -52
  264. package/dist/patterns-cli.d.ts +3 -1
  265. package/dist/{pipeline-C25Oxtp1.d.ts → pipeline-C8vfIiTX.d.ts} +1 -1
  266. package/dist/policy-runtime.d.ts +3 -1
  267. package/dist/proactive-contention.d.ts +3 -1
  268. package/dist/provenance.d.ts +3 -1
  269. package/dist/{public-http-CMe7NQzj.d.ts → public-http-Cy0aYbys.d.ts} +1 -1
  270. package/dist/{qmd-DkDVCbgF.d.ts → qmd-B9p1SZTZ.d.ts} +1 -1
  271. package/dist/qmd-preflight.d.ts +4 -2
  272. package/dist/qmd-recall-cache.d.ts +3 -1
  273. package/dist/qmd.d.ts +4 -2
  274. package/dist/recall-concurrency-config.d.ts +3 -1
  275. package/dist/recall-disclosure-escalation.d.ts +3 -1
  276. package/dist/recall-explain-renderer.d.ts +3 -1
  277. package/dist/recall-explain-renderer.js +3 -3
  278. package/dist/recall-memory-map.d.ts +3 -1
  279. package/dist/recall-planner-llm.d.ts +3 -1
  280. package/dist/recall-state.d.ts +3 -1
  281. package/dist/recall-tag-filter.d.ts +3 -1
  282. package/dist/recall-timings.d.ts +3 -1
  283. package/dist/recall-xray-cli.d.ts +3 -1
  284. package/dist/recall-xray-cli.js +4 -4
  285. package/dist/recall-xray-renderer.d.ts +3 -1
  286. package/dist/recall-xray-renderer.js +3 -3
  287. package/dist/recall-xray.d.ts +3 -1
  288. package/dist/recall-xray.js +2 -2
  289. package/dist/reconcile/cursor.d.ts +3 -1
  290. package/dist/reconcile/manifest.d.ts +3 -1
  291. package/dist/reconcile/plan.d.ts +3 -1
  292. package/dist/replay/normalizers/chatgpt.d.ts +3 -1
  293. package/dist/replay/normalizers/claude.d.ts +3 -1
  294. package/dist/replay/normalizers/openclaw.d.ts +3 -1
  295. package/dist/replay/normalizers/shared.d.ts +3 -1
  296. package/dist/replay/runner.d.ts +3 -1
  297. package/dist/replay/types.d.ts +3 -1
  298. package/dist/replica-divergence.d.ts +3 -1
  299. package/dist/replica-peers-config.d.ts +3 -1
  300. package/dist/{resolution-MFY72STR.js → resolution-4552BNRV.js} +13 -3
  301. package/dist/resolve-auth-token.d.ts +3 -1
  302. package/dist/resume-bundles.js +4 -2
  303. package/dist/retrieval-agents.d.ts +4 -2
  304. package/dist/retrieval-tiers.d.ts +3 -1
  305. package/dist/routing/engine.d.ts +3 -1
  306. package/dist/routing/store.d.ts +3 -1
  307. package/dist/salvage-envelope.d.ts +3 -1
  308. package/dist/schemas.d.ts +44 -44
  309. package/dist/{scope-profiles-B9xMoFO-.d.ts → scope-profiles-DLrRxFWR.d.ts} +1 -1
  310. package/dist/search/embed-helper.d.ts +3 -1
  311. package/dist/search/factory.d.ts +3 -1
  312. package/dist/search/index.d.ts +3 -1
  313. package/dist/search/lancedb-backend.d.ts +3 -1
  314. package/dist/search/meilisearch-backend.d.ts +3 -1
  315. package/dist/search/noop-backend.d.ts +3 -1
  316. package/dist/search/orama-backend.d.ts +3 -1
  317. package/dist/search/port.d.ts +3 -1
  318. package/dist/search/remote-backend.d.ts +3 -1
  319. package/dist/{semantic-TddhF0y7.d.ts → semantic-BalR6EXU.d.ts} +1 -1
  320. package/dist/{semantic-consolidation-C4uykW84.d.ts → semantic-consolidation-DlN6xe20.d.ts} +1 -1
  321. package/dist/semantic-consolidation.d.ts +4 -2
  322. package/dist/semantic-consolidation.js +4 -4
  323. package/dist/semantic-rule-promotion.js +3 -3
  324. package/dist/semantic-rule-verifier.d.ts +3 -1
  325. package/dist/semantic-rule-verifier.js +3 -3
  326. package/dist/{service-DCZDZNcV.d.ts → service-GkYyVfm9.d.ts} +2 -2
  327. package/dist/session-observer-bands.d.ts +3 -1
  328. package/dist/session-observer-state.d.ts +3 -1
  329. package/dist/shared-context/manager.d.ts +3 -1
  330. package/dist/signal.d.ts +3 -1
  331. package/dist/source-agent-qualifier.d.ts +3 -1
  332. package/dist/source-agent-qualifier.js +3 -3
  333. package/dist/{storage-CcKmEjL8.d.ts → storage-BIDzuKIn.d.ts} +1 -1
  334. package/dist/storage.d.ts +4 -2
  335. package/dist/storage.js +2 -2
  336. package/dist/subject-config.d.ts +3 -1
  337. package/dist/summarizer.d.ts +3 -1
  338. package/dist/summary-snapshot.d.ts +3 -1
  339. package/dist/support-passport/index.d.ts +43 -43
  340. package/dist/support-passport/index.js +17 -17
  341. package/dist/temporal-supersession.d.ts +4 -2
  342. package/dist/temporal-timeline-recall.d.ts +3 -1
  343. package/dist/temporal-validity.d.ts +3 -1
  344. package/dist/threading.d.ts +3 -1
  345. package/dist/tier-migration.d.ts +4 -2
  346. package/dist/tier-routing.d.ts +3 -1
  347. package/dist/topics.d.ts +3 -1
  348. package/dist/transcript.d.ts +3 -1
  349. package/dist/transfer/types.d.ts +20 -20
  350. package/dist/trust-score-stage.d.ts +3 -1
  351. package/dist/trust-score.d.ts +3 -1
  352. package/dist/{types-Pi0lfaEt.d.ts → types-BxAcOl5Q.d.ts} +323 -4
  353. package/dist/types.d.ts +3 -1
  354. package/dist/types.js +1 -1
  355. package/dist/utility-runtime.d.ts +3 -1
  356. package/dist/verified-recall.js +3 -3
  357. package/dist/who-knows-cli.d.ts +40 -40
  358. package/dist/who-knows-cli.js +4 -4
  359. package/dist/who-knows.d.ts +42 -42
  360. package/dist/who-knows.js +3 -3
  361. package/dist/write-envelope.d.ts +3 -1
  362. package/package.json +2 -2
  363. package/src/access-boundary.ts +4 -0
  364. package/src/access-http.ts +6 -5
  365. package/src/access-mcp-admin-tools.ts +24 -0
  366. package/src/access-mcp-output-schemas.ts +14 -0
  367. package/src/access-mcp.ts +2 -2
  368. package/src/access-operations-batch.ts +5 -0
  369. package/src/access-service.ts +27 -0
  370. package/src/access-surface-catalog.test.ts +24 -0
  371. package/src/access-surface-catalog.ts +4 -0
  372. package/src/coding/action-gate.test.ts +341 -0
  373. package/src/coding/action-gate.ts +556 -0
  374. package/src/config.ts +5 -0
  375. package/src/contradiction/contradiction-review.ts +50 -4
  376. package/src/contradiction/contradiction-scan.ts +14 -3
  377. package/src/contradiction/resolution.ts +50 -2
  378. package/src/index.ts +4 -3
  379. package/src/orchestration/recall-rerank-coordinator.ts +123 -4
  380. package/src/orchestration/recall-result-formatter.ts +6 -2
  381. package/src/orchestration/recall-search-pipeline.ts +14 -1
  382. package/src/preferences/drift-config.ts +118 -0
  383. package/src/preferences/drift-recall.ts +133 -0
  384. package/src/preferences/drift-types.ts +87 -0
  385. package/src/preferences/index.ts +12 -0
  386. package/src/preferences/preference-drift.test.ts +690 -0
  387. package/src/preferences/preference-drift.ts +724 -0
  388. package/src/review/review-deck-access.test.ts +325 -0
  389. package/src/review/review-deck-http-base.ts +133 -0
  390. package/src/review/review-deck-mutation.test.ts +417 -0
  391. package/src/review/review-deck-mutation.ts +677 -0
  392. package/src/review/review-deck-snapshot.ts +196 -0
  393. package/src/review/review-deck-surface.ts +117 -0
  394. package/src/review/review-deck.test.ts +347 -0
  395. package/src/review/review-deck.ts +311 -0
  396. package/src/storage.ts +4 -6
  397. package/src/types.ts +7 -3
  398. package/dist/chunk-35NN3ZFA.js.map +0 -1
  399. package/dist/chunk-4KYOLI6M.js.map +0 -1
  400. package/dist/chunk-BWLRUMOG.js.map +0 -1
  401. package/dist/chunk-J275IYMG.js.map +0 -1
  402. package/dist/chunk-SB7D4BT7.js.map +0 -1
  403. package/dist/chunk-SBDCZ2AX.js.map +0 -1
  404. package/dist/chunk-TF77C7SV.js.map +0 -1
  405. package/dist/chunk-TZNQUNKL.js.map +0 -1
  406. package/dist/chunk-V3MXUL3D.js.map +0 -1
  407. package/dist/chunk-XCTXS7VY.js +0 -402
  408. package/dist/chunk-XCTXS7VY.js.map +0 -1
  409. package/dist/chunk-YLDXWCZP.js.map +0 -1
  410. /package/dist/{chunk-R7JAM2XF.js.map → chunk-2VXWFYJE.js.map} +0 -0
  411. /package/dist/{chunk-UXO5IMBK.js.map → chunk-4XDGYCZI.js.map} +0 -0
  412. /package/dist/{chunk-OTHZWLUD.js.map → chunk-57HVIG5U.js.map} +0 -0
  413. /package/dist/{chunk-EILZG5VC.js.map → chunk-6TAX66IA.js.map} +0 -0
  414. /package/dist/{chunk-NKKD4ZAU.js.map → chunk-6VALIZKG.js.map} +0 -0
  415. /package/dist/{chunk-2E36UXVW.js.map → chunk-7KFIMGMP.js.map} +0 -0
  416. /package/dist/{chunk-LGO6A7GJ.js.map → chunk-7QOO4LGC.js.map} +0 -0
  417. /package/dist/{chunk-DZUJ46DO.js.map → chunk-AUO2LJ3K.js.map} +0 -0
  418. /package/dist/{chunk-ZO6AXLEH.js.map → chunk-AYDDB3XI.js.map} +0 -0
  419. /package/dist/{chunk-NTWN6HB2.js.map → chunk-AYYGRVRP.js.map} +0 -0
  420. /package/dist/{chunk-7YQ4IYS2.js.map → chunk-AZAA5DNP.js.map} +0 -0
  421. /package/dist/{chunk-Z7PBQPJ4.js.map → chunk-BQUEAVNW.js.map} +0 -0
  422. /package/dist/{chunk-5YQ63T4B.js.map → chunk-D27J626P.js.map} +0 -0
  423. /package/dist/{chunk-LZH5FR2W.js.map → chunk-DHTQP334.js.map} +0 -0
  424. /package/dist/{chunk-Y7DK7HHJ.js.map → chunk-E4MVQQVX.js.map} +0 -0
  425. /package/dist/{chunk-JYYBA7TF.js.map → chunk-EIVIB73I.js.map} +0 -0
  426. /package/dist/{chunk-S2DPMJXH.js.map → chunk-EV3YFLCB.js.map} +0 -0
  427. /package/dist/{chunk-3I7ASZN3.js.map → chunk-G57LQRP6.js.map} +0 -0
  428. /package/dist/{chunk-UIN2BWDD.js.map → chunk-GN55KU2P.js.map} +0 -0
  429. /package/dist/{chunk-PE255F4T.js.map → chunk-GUXS322H.js.map} +0 -0
  430. /package/dist/{chunk-YB42WZCY.js.map → chunk-IQSC3V2E.js.map} +0 -0
  431. /package/dist/{chunk-MN4KB2A4.js.map → chunk-MBDJJTAT.js.map} +0 -0
  432. /package/dist/{chunk-SQI57UCJ.js.map → chunk-MRTUN665.js.map} +0 -0
  433. /package/dist/{chunk-SPUJNJJC.js.map → chunk-NZWEEALH.js.map} +0 -0
  434. /package/dist/{chunk-2XDWWPTN.js.map → chunk-OJD4WS2E.js.map} +0 -0
  435. /package/dist/{chunk-ZWXJBFZU.js.map → chunk-OLWTCWQ2.js.map} +0 -0
  436. /package/dist/{chunk-XRYZRW54.js.map → chunk-P4DHENRE.js.map} +0 -0
  437. /package/dist/{chunk-IID2CMUU.js.map → chunk-PDVQ5JU3.js.map} +0 -0
  438. /package/dist/{chunk-5NM4OH4P.js.map → chunk-QFMO32TO.js.map} +0 -0
  439. /package/dist/{chunk-SIHSCCUQ.js.map → chunk-QXIQPCCN.js.map} +0 -0
  440. /package/dist/{chunk-2YI72KM3.js.map → chunk-R47WINCY.js.map} +0 -0
  441. /package/dist/{chunk-ZQHI2BZB.js.map → chunk-RK4WXUI2.js.map} +0 -0
  442. /package/dist/{chunk-QOS5LBZN.js.map → chunk-S7Q5CM7E.js.map} +0 -0
  443. /package/dist/{chunk-GDFVPUJ5.js.map → chunk-TDKSCV62.js.map} +0 -0
  444. /package/dist/{chunk-OCZIYAE5.js.map → chunk-TYUS5TLG.js.map} +0 -0
  445. /package/dist/{chunk-GNCCUR6K.js.map → chunk-URHCUXKI.js.map} +0 -0
  446. /package/dist/{chunk-CT56C22C.js.map → chunk-Z4RPXHJH.js.map} +0 -0
  447. /package/dist/{contradiction-review-QKJH5DKD.js.map → contradiction-review-WRVLRWHS.js.map} +0 -0
  448. /package/dist/{contradiction-scan-VOTQRRSX.js.map → contradiction-scan-AKUWPX5N.js.map} +0 -0
  449. /package/dist/{resolution-MFY72STR.js.map → resolution-4552BNRV.js.map} +0 -0
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Preference drift detection (issue #2371).
3
+ *
4
+ * Covers the acceptance matrix:
5
+ * 1. Fixture namespace with a preference restated last week, one untouched
6
+ * for six months, and one with recent opposing evidence: shadow reports
7
+ * corroborated / stale / drifted respectively and writes NOTHING; apply
8
+ * stamps the first two and opens exactly one review item for the third.
9
+ * 2. Resolving that review item with `supersede` writes a superseding
10
+ * memory through the real storage path, and the replacement is what a
11
+ * later read returns.
12
+ * 3. `recallDamping: true` sinks a stale preference below an otherwise-equal
13
+ * fresh memory; with the flag off — including the string `"false"` — the
14
+ * ordering is byte-identical to the input.
15
+ * 4. A failing evidence lookup marks the preference
16
+ * `skipped: backend_unavailable`, never `stale`.
17
+ * 5. Window bounds are half-open `[start, end)`; runs are deterministic.
18
+ * 6. Every non-active status is excluded from candidacy.
19
+ */
20
+ import test from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
25
+
26
+ import { StorageManager } from "../storage.js";
27
+ import { parseConfig } from "../config.js";
28
+ import type { MemoryFile, PluginConfig, QmdSearchResult } from "../types.js";
29
+ import type { SemanticDedupHit, SemanticDedupLookup } from "../dedup/semantic.js";
30
+ import { listPairs, readPair } from "../contradiction/contradiction-review.js";
31
+ import { executeResolution } from "../contradiction/resolution.js";
32
+ import {
33
+ runPreferenceDriftScan,
34
+ readPreferenceDriftMarker,
35
+ type PreferenceDriftFinding,
36
+ } from "./preference-drift.js";
37
+ import { applyPreferenceDriftRanking, isPreferenceDriftStageActive, driftAgeNote } from "./drift-recall.js";
38
+ import { parseDriftDetectionConfig, DRIFT_DETECTION_DEFAULTS } from "./drift-config.js";
39
+
40
+ const NOW = new Date("2026-08-18T12:00:00.000Z");
41
+ const DAY_MS = 24 * 60 * 60 * 1000;
42
+
43
+ function driftConfig(overrides: Record<string, unknown> = {}): PluginConfig {
44
+ return parseConfig({
45
+ openaiApiKey: "sk-test",
46
+ driftDetection: { enabled: true, ...overrides },
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Raw memory file with full frontmatter control, mirroring the memory-worth /
52
+ * procedural test pattern so the parser and serializer under test are the real
53
+ * ones rather than a fixture shape.
54
+ */
55
+ async function writeMemory(
56
+ storage: StorageManager,
57
+ id: string,
58
+ category: string,
59
+ content: string,
60
+ extraFrontmatter: string[] = [],
61
+ ): Promise<string> {
62
+ const dir = path.join(storage.dir, "facts", "2026-01-01");
63
+ await mkdir(dir, { recursive: true });
64
+ const lines = [
65
+ "---",
66
+ `id: ${id}`,
67
+ `category: ${category}`,
68
+ `created: ${new Date(NOW.getTime() - 200 * DAY_MS).toISOString()}`,
69
+ `updated: ${new Date(NOW.getTime() - 200 * DAY_MS).toISOString()}`,
70
+ "confidence: 0.8",
71
+ "tags: []",
72
+ ...extraFrontmatter,
73
+ "---",
74
+ ];
75
+ const filePath = path.join(dir, `${id}.md`);
76
+ await writeFile(filePath, `${lines.join("\n")}\n\n${content}\n`, "utf-8");
77
+ return filePath;
78
+ }
79
+
80
+ /**
81
+ * Deterministic evidence lookup keyed on the querying preference's own text,
82
+ * so each fixture preference gets exactly the neighbors the scenario needs.
83
+ */
84
+ function lookupFor(map: Record<string, SemanticDedupHit[]>): SemanticDedupLookup {
85
+ return async (content: string) => {
86
+ for (const [needle, hits] of Object.entries(map)) {
87
+ if (content.includes(needle)) return hits;
88
+ }
89
+ return [];
90
+ };
91
+ }
92
+
93
+ /**
94
+ * LLM stub shaped like `LocalLlmClient.chatCompletion`. The judge builds its
95
+ * own `pairKey` (sorted memory ids) and echoes it in the prompt, so the stub
96
+ * reads the keys back out rather than guessing their order, then answers each
97
+ * one from the evidence id it contains. Anything unlisted is `independent`,
98
+ * which is the "evidence about something else" case.
99
+ */
100
+ function judgeStub(verdictByEvidenceId: Record<string, string>) {
101
+ return {
102
+ chatCompletion: async (messages: Array<{ role: string; content: string }>) => {
103
+ const prompt = messages.map((m) => m.content).join("\n");
104
+ const keys = [...prompt.matchAll(/pairKey: "([^"]+)"/g)].map((m) => m[1]!);
105
+ const entries = keys.map((pairKey) => {
106
+ const hit = Object.entries(verdictByEvidenceId).find(([evidenceId]) =>
107
+ pairKey.includes(evidenceId),
108
+ );
109
+ const verdict = hit?.[1] ?? "independent";
110
+ return { pairKey, verdict, rationale: `stub verdict ${verdict}`, confidence: 0.9 };
111
+ });
112
+ return { content: JSON.stringify(entries) };
113
+ },
114
+ };
115
+ }
116
+
117
+ const PREF_IDS = {
118
+ corroborated: "pref-corroborated",
119
+ stale: "pref-stale",
120
+ drifted: "pref-drifted",
121
+ };
122
+
123
+ /** The acceptance fixture: three aging preferences plus their evidence. */
124
+ async function seedFixture(storage: StorageManager): Promise<void> {
125
+ await writeMemory(
126
+ storage,
127
+ PREF_IDS.corroborated,
128
+ "preference",
129
+ "User prefers dark mode in every editor.",
130
+ );
131
+ await writeMemory(storage, PREF_IDS.stale, "preference", "User prefers tabs over spaces.");
132
+ await writeMemory(storage, PREF_IDS.drifted, "preference", "User prefers pnpm for package installs.");
133
+
134
+ // Recent restatement, 7 days old — inside the 45-day lookback window.
135
+ await writeMemory(storage, "ev-restates", "fact", "User said again they want dark mode everywhere.", [
136
+ `updated: ${new Date(NOW.getTime() - 7 * DAY_MS).toISOString()}`,
137
+ ]);
138
+ // Recent opposing behavioral evidence, 3 days old.
139
+ await writeMemory(storage, "ev-opposes", "fact", "User ran bun install for every project this week.", [
140
+ `updated: ${new Date(NOW.getTime() - 3 * DAY_MS).toISOString()}`,
141
+ ]);
142
+ }
143
+
144
+ async function scan(
145
+ storage: StorageManager,
146
+ memoryDir: string,
147
+ config: PluginConfig,
148
+ options: { apply?: boolean; lookup?: SemanticDedupLookup; localLlm?: unknown } = {},
149
+ ) {
150
+ return runPreferenceDriftScan({
151
+ storage,
152
+ config,
153
+ memoryDir,
154
+ storageForNamespace: () => storage,
155
+ embeddingLookup:
156
+ options.lookup ??
157
+ lookupFor({
158
+ "dark mode": [{ id: "ev-restates", score: 0.9 }],
159
+ "tabs over spaces": [],
160
+ pnpm: [{ id: "ev-opposes", score: 0.88 }],
161
+ }),
162
+ localLlm: (options.localLlm
163
+ ?? judgeStub({
164
+ "ev-restates": "duplicates",
165
+ "ev-at-start": "duplicates",
166
+ "ev-opposes": "contradicts",
167
+ })) as never,
168
+ fallbackLlm: null,
169
+ apply: options.apply,
170
+ now: NOW,
171
+ });
172
+ }
173
+
174
+ function findingFor(findings: PreferenceDriftFinding[], id: string): PreferenceDriftFinding {
175
+ const found = findings.find((f) => f.memoryId === id);
176
+ assert.ok(found, `expected a finding for ${id}`);
177
+ return found;
178
+ }
179
+
180
+ // ════════════════════════════════════════════════════════════════════════════
181
+ // 1. Classification + shadow/apply
182
+ // ════════════════════════════════════════════════════════════════════════════
183
+
184
+ test("shadow scan classifies corroborated / stale / drifted and writes nothing", async () => {
185
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-shadow-"));
186
+ try {
187
+ const storage = new StorageManager(dir);
188
+ await seedFixture(storage);
189
+
190
+ const report = await scan(storage, dir, driftConfig());
191
+
192
+ assert.equal(report.mode, "shadow");
193
+ assert.equal(report.scanned, 3, "all three aging preferences are candidates");
194
+ assert.equal(findingFor(report.findings, PREF_IDS.corroborated).classification, "corroborated");
195
+ assert.equal(findingFor(report.findings, PREF_IDS.stale).classification, "stale");
196
+ assert.equal(findingFor(report.findings, PREF_IDS.drifted).classification, "drifted");
197
+ assert.equal(report.appliedCount, 0);
198
+ assert.equal(report.reviewItemsOpened, 0);
199
+
200
+ // Shadow writes NOTHING — not the stamps, not the review queue, not even
201
+ // the run marker.
202
+ const after = await storage.getMemoryById(PREF_IDS.stale);
203
+ assert.equal(after?.frontmatter.driftState, undefined);
204
+ assert.equal(listPairs(dir).total, 0);
205
+ assert.equal(await readPreferenceDriftMarker(dir), null);
206
+ } finally {
207
+ await rm(dir, { recursive: true, force: true });
208
+ }
209
+ });
210
+
211
+ test("apply stamps corroborated + stale and opens exactly one review item for drifted", async () => {
212
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-apply-"));
213
+ try {
214
+ const storage = new StorageManager(dir);
215
+ await seedFixture(storage);
216
+
217
+ const report = await scan(storage, dir, driftConfig(), { apply: true });
218
+
219
+ assert.equal(report.mode, "apply");
220
+ assert.equal(report.reviewItemsOpened, 1, "exactly one review item for the drifted preference");
221
+
222
+ const corroborated = await storage.getMemoryById(PREF_IDS.corroborated);
223
+ assert.equal(corroborated?.frontmatter.lastCorroborated, NOW.toISOString());
224
+ assert.equal(corroborated?.frontmatter.driftState, undefined, "corroboration clears any stale mark");
225
+
226
+ const stale = await storage.getMemoryById(PREF_IDS.stale);
227
+ assert.equal(stale?.frontmatter.driftState, "stale");
228
+ assert.equal(stale?.frontmatter.lastCorroborated, undefined);
229
+ assert.equal(stale?.frontmatter.status ?? "active", "active", "stale is never a lifecycle change");
230
+
231
+ const drifted = await storage.getMemoryById(PREF_IDS.drifted);
232
+ assert.equal(drifted?.frontmatter.status ?? "active", "active", "drift never auto-supersedes");
233
+
234
+ const queued = listPairs(dir, { filter: "all" });
235
+ assert.equal(queued.total, 1);
236
+ const item = queued.pairs[0]!;
237
+ assert.equal(item.kind, "preference-drift");
238
+ assert.deepEqual(item.memoryIds, [PREF_IDS.drifted, "ev-opposes"]);
239
+ assert.equal(item.verdict, "contradicts");
240
+ assert.equal(item.resolution, undefined, "a fresh drift item is unresolved");
241
+
242
+ const marker = await readPreferenceDriftMarker(dir);
243
+ assert.equal(marker?.lastApplyAt, NOW.toISOString());
244
+ } finally {
245
+ await rm(dir, { recursive: true, force: true });
246
+ }
247
+ });
248
+
249
+ test("re-running apply on identical fixtures is deterministic", async () => {
250
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-determinism-"));
251
+ try {
252
+ const storage = new StorageManager(dir);
253
+ await seedFixture(storage);
254
+
255
+ const first = await scan(storage, dir, driftConfig());
256
+ const second = await scan(storage, dir, driftConfig());
257
+
258
+ assert.deepEqual(
259
+ first.findings.map((f) => [f.memoryId, f.classification]),
260
+ second.findings.map((f) => [f.memoryId, f.classification]),
261
+ "classification order and verdicts must be stable across runs",
262
+ );
263
+ } finally {
264
+ await rm(dir, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ // ════════════════════════════════════════════════════════════════════════════
269
+ // 2. Review resolution
270
+ // ════════════════════════════════════════════════════════════════════════════
271
+
272
+ test("resolving a drift item with supersede retires the old preference for the new one", async () => {
273
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-supersede-"));
274
+ try {
275
+ const storage = new StorageManager(dir);
276
+ await seedFixture(storage);
277
+ await scan(storage, dir, driftConfig(), { apply: true });
278
+
279
+ const pairId = listPairs(dir, { filter: "all" }).pairs[0]!.pairId;
280
+ const outcome = await executeResolution(dir, storage, pairId, "supersede", {
281
+ mergedContent: "User prefers bun for package installs.",
282
+ });
283
+
284
+ assert.ok(outcome.affectedIds.includes(PREF_IDS.drifted), outcome.message);
285
+ const replacementId = outcome.affectedIds.find((id) => id !== PREF_IDS.drifted);
286
+ assert.ok(replacementId, `expected a replacement id in ${JSON.stringify(outcome.affectedIds)}`);
287
+
288
+ const old = await storage.getMemoryById(PREF_IDS.drifted);
289
+ assert.equal(old?.frontmatter.status, "superseded");
290
+ assert.equal(old?.frontmatter.supersededBy, replacementId);
291
+
292
+ const replacement = await storage.getMemoryById(replacementId!);
293
+ assert.equal(replacement?.frontmatter.category, "preference");
294
+ assert.match(replacement!.content, /bun for package installs/);
295
+ assert.equal(replacement?.frontmatter.status ?? "active", "active");
296
+
297
+ // The item is terminally resolved and leaves the actionable queue.
298
+ assert.equal(readPair(dir, pairId)?.resolution, "supersede");
299
+ assert.equal(listPairs(dir, { filter: "unresolved" }).total, 0);
300
+ } finally {
301
+ await rm(dir, { recursive: true, force: true });
302
+ }
303
+ });
304
+
305
+ test("keep confirms the preference and archive demotes it", async () => {
306
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-keep-"));
307
+ try {
308
+ const storage = new StorageManager(dir);
309
+ await seedFixture(storage);
310
+ await scan(storage, dir, driftConfig(), { apply: true });
311
+ const pairId = listPairs(dir, { filter: "all" }).pairs[0]!.pairId;
312
+
313
+ const kept = await executeResolution(dir, storage, pairId, "keep", {});
314
+ assert.deepEqual(kept.affectedIds, [PREF_IDS.drifted], kept.message);
315
+ const confirmed = await storage.getMemoryById(PREF_IDS.drifted);
316
+ assert.ok(confirmed?.frontmatter.lastCorroborated, "keep stamps lastCorroborated");
317
+ assert.equal(confirmed?.frontmatter.status ?? "active", "active");
318
+ } finally {
319
+ await rm(dir, { recursive: true, force: true });
320
+ }
321
+ });
322
+
323
+ test("a contradiction verb is refused on a drift item and vice versa", async () => {
324
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-verbs-"));
325
+ try {
326
+ const storage = new StorageManager(dir);
327
+ await seedFixture(storage);
328
+ await scan(storage, dir, driftConfig(), { apply: true });
329
+ const pairId = listPairs(dir, { filter: "all" }).pairs[0]!.pairId;
330
+
331
+ const refused = await executeResolution(dir, storage, pairId, "keep-a", {});
332
+ assert.deepEqual(refused.affectedIds, []);
333
+ assert.match(refused.message, /not valid for a preference-drift item/);
334
+ assert.equal(readPair(dir, pairId)?.resolution, undefined, "a refusal must not resolve the item");
335
+ } finally {
336
+ await rm(dir, { recursive: true, force: true });
337
+ }
338
+ });
339
+
340
+ // ════════════════════════════════════════════════════════════════════════════
341
+ // 3. Recall damping + annotation
342
+ // ════════════════════════════════════════════════════════════════════════════
343
+
344
+ const stalePreferenceFm = { category: "preference", driftState: "stale" as const };
345
+ const freshFactFm = { category: "fact" };
346
+
347
+ test("recallDamping sinks a stale preference below an otherwise-equal fresh memory", () => {
348
+ const config = parseDriftDetectionConfig({ enabled: true, recallDamping: true });
349
+ const ordered = applyPreferenceDriftRanking(
350
+ [
351
+ { key: "stale.md", rank: 2, frontmatter: stalePreferenceFm },
352
+ { key: "fresh.md", rank: 2, frontmatter: freshFactFm },
353
+ ],
354
+ { config, now: NOW },
355
+ );
356
+ assert.deepEqual(
357
+ ordered.map((o) => o.key),
358
+ ["fresh.md", "stale.md"],
359
+ );
360
+ assert.equal(ordered.find((o) => o.key === "stale.md")?.multiplier, 0.8);
361
+ assert.equal(ordered.find((o) => o.key === "fresh.md")?.multiplier, 1);
362
+ });
363
+
364
+ test("damping off — including the string \"false\" — leaves order and multipliers untouched", () => {
365
+ for (const raw of [undefined, false, "false", "0", "no", "off"]) {
366
+ const config = parseDriftDetectionConfig(
367
+ raw === undefined ? { enabled: true } : { enabled: true, recallDamping: raw },
368
+ );
369
+ const ordered = applyPreferenceDriftRanking(
370
+ [
371
+ { key: "stale.md", rank: 2, frontmatter: stalePreferenceFm },
372
+ { key: "fresh.md", rank: 1, frontmatter: freshFactFm },
373
+ ],
374
+ { config, now: NOW },
375
+ );
376
+ assert.deepEqual(
377
+ ordered.map((o) => o.key),
378
+ ["stale.md", "fresh.md"],
379
+ `recallDamping=${JSON.stringify(raw)} must not reorder`,
380
+ );
381
+ assert.ok(
382
+ ordered.every((o) => o.multiplier === 1),
383
+ `recallDamping=${JSON.stringify(raw)} must leave every multiplier at 1`,
384
+ );
385
+ assert.equal(isPreferenceDriftStageActive(config), false);
386
+ }
387
+ });
388
+
389
+ test("stalePenalty 1 is a documented no-op that keeps the stage inert", () => {
390
+ const config = parseDriftDetectionConfig({ enabled: true, recallDamping: true, stalePenalty: 1 });
391
+ assert.equal(config.stalePenalty, 1);
392
+ assert.equal(isPreferenceDriftStageActive(config), false);
393
+ const ordered = applyPreferenceDriftRanking(
394
+ [
395
+ { key: "stale.md", rank: 2, frontmatter: stalePreferenceFm },
396
+ { key: "fresh.md", rank: 1, frontmatter: freshFactFm },
397
+ ],
398
+ { config, now: NOW },
399
+ );
400
+ assert.deepEqual(
401
+ ordered.map((o) => o.key),
402
+ ["stale.md", "fresh.md"],
403
+ );
404
+ });
405
+
406
+ test("annotateAfterDays emits an age note only past the window, and never mutates state", () => {
407
+ const off = parseDriftDetectionConfig({ enabled: true });
408
+ assert.equal(driftAgeNote({ category: "preference", created: "2026-01-05T00:00:00.000Z" }, off, NOW), undefined);
409
+
410
+ const on = parseDriftDetectionConfig({ enabled: true, annotateAfterDays: 90 });
411
+ assert.equal(
412
+ driftAgeNote({ category: "preference", created: "2026-01-05T00:00:00.000Z" }, on, NOW),
413
+ "(stated 2026-01; not corroborated since)",
414
+ );
415
+ // Inside the window → no note.
416
+ assert.equal(
417
+ driftAgeNote(
418
+ { category: "preference", created: new Date(NOW.getTime() - 10 * DAY_MS).toISOString() },
419
+ on,
420
+ NOW,
421
+ ),
422
+ undefined,
423
+ );
424
+ // A non-preference is never annotated.
425
+ assert.equal(driftAgeNote({ category: "fact", created: "2026-01-05T00:00:00.000Z" }, on, NOW), undefined);
426
+ // lastCorroborated wins over created as the age anchor.
427
+ assert.equal(
428
+ driftAgeNote(
429
+ { category: "preference", created: "2020-01-01T00:00:00.000Z", lastCorroborated: "2026-02-01T00:00:00.000Z" },
430
+ on,
431
+ NOW,
432
+ ),
433
+ "(corroborated 2026-02; not since)",
434
+ );
435
+ });
436
+
437
+ test("ranking is a total comparator — equal damped scores preserve input order", () => {
438
+ const config = parseDriftDetectionConfig({ enabled: true, recallDamping: true });
439
+ const inputs = [
440
+ { key: "b.md", rank: 5, frontmatter: freshFactFm },
441
+ { key: "a.md", rank: 5, frontmatter: freshFactFm },
442
+ ];
443
+ const first = applyPreferenceDriftRanking(inputs, { config, now: NOW });
444
+ const second = applyPreferenceDriftRanking(inputs, { config, now: NOW });
445
+ assert.deepEqual(first.map((o) => o.key), ["b.md", "a.md"]);
446
+ assert.deepEqual(first, second);
447
+ });
448
+
449
+ // ════════════════════════════════════════════════════════════════════════════
450
+ // 4. Backend failure is not staleness
451
+ // ════════════════════════════════════════════════════════════════════════════
452
+
453
+ test("a failing evidence lookup reports skipped: backend_unavailable, never stale", async () => {
454
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-backend-"));
455
+ try {
456
+ const storage = new StorageManager(dir);
457
+ await seedFixture(storage);
458
+
459
+ const report = await scan(storage, dir, driftConfig(), {
460
+ apply: true,
461
+ lookup: async () => {
462
+ throw new Error("embedding backend unreachable");
463
+ },
464
+ });
465
+
466
+ for (const finding of report.findings) {
467
+ assert.equal(finding.classification, "skipped", `${finding.memoryId} must not be classified`);
468
+ assert.equal(finding.skipped, "backend_unavailable");
469
+ }
470
+ assert.equal(report.counts.stale, 0, "a failed lookup is never counted as no-evidence");
471
+ assert.equal(report.appliedCount, 0, "nothing is stamped when classification failed");
472
+ const stale = await storage.getMemoryById(PREF_IDS.stale);
473
+ assert.equal(stale?.frontmatter.driftState, undefined);
474
+ } finally {
475
+ await rm(dir, { recursive: true, force: true });
476
+ }
477
+ });
478
+
479
+ test("evidence with no judge verdict reports verification_unavailable, not corroborated", async () => {
480
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-nojudge-"));
481
+ try {
482
+ const storage = new StorageManager(dir);
483
+ await seedFixture(storage);
484
+
485
+ // No LLM at all: the judge returns `needs-user` for every pair.
486
+ const report = await runPreferenceDriftScan({
487
+ storage,
488
+ config: driftConfig(),
489
+ memoryDir: dir,
490
+ storageForNamespace: () => storage,
491
+ embeddingLookup: lookupFor({ "dark mode": [{ id: "ev-restates", score: 0.9 }] }),
492
+ localLlm: null,
493
+ fallbackLlm: null,
494
+ apply: true,
495
+ now: NOW,
496
+ });
497
+
498
+ const corroborated = findingFor(report.findings, PREF_IDS.corroborated);
499
+ assert.equal(corroborated.classification, "skipped");
500
+ assert.equal(corroborated.skipped, "verification_unavailable");
501
+ const memory = await storage.getMemoryById(PREF_IDS.corroborated);
502
+ assert.equal(
503
+ memory?.frontmatter.lastCorroborated,
504
+ undefined,
505
+ "an unverified preference must never be stamped as corroborated",
506
+ );
507
+ } finally {
508
+ await rm(dir, { recursive: true, force: true });
509
+ }
510
+ });
511
+
512
+ // ════════════════════════════════════════════════════════════════════════════
513
+ // 5. Window bounds + candidate gating
514
+ // ════════════════════════════════════════════════════════════════════════════
515
+
516
+ test("evidence exactly at the window start counts; at the run instant it does not", async () => {
517
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-window-"));
518
+ try {
519
+ const storage = new StorageManager(dir);
520
+ await writeMemory(storage, PREF_IDS.stale, "preference", "User prefers tabs over spaces.");
521
+ // Exactly at the inclusive lower bound of a 45-day lookback.
522
+ await writeMemory(storage, "ev-at-start", "fact", "Tabs again, confirmed.", [
523
+ `updated: ${new Date(NOW.getTime() - 45 * DAY_MS).toISOString()}`,
524
+ ]);
525
+ // Exactly at the exclusive upper bound.
526
+ await writeMemory(storage, "ev-at-now", "fact", "Tabs again, confirmed.", [
527
+ `updated: ${NOW.toISOString()}`,
528
+ ]);
529
+
530
+ const atStart = await scan(storage, dir, driftConfig(), {
531
+ lookup: lookupFor({ "tabs over spaces": [{ id: "ev-at-start", score: 0.9 }] }),
532
+ });
533
+ assert.equal(findingFor(atStart.findings, PREF_IDS.stale).evidence.length, 1, "[start is inclusive");
534
+
535
+ const atNow = await scan(storage, dir, driftConfig(), {
536
+ lookup: lookupFor({ "tabs over spaces": [{ id: "ev-at-now", score: 0.9 }] }),
537
+ });
538
+ const finding = findingFor(atNow.findings, PREF_IDS.stale);
539
+ assert.equal(finding.evidence.length, 0, "end) is exclusive");
540
+ assert.equal(finding.classification, "stale");
541
+ } finally {
542
+ await rm(dir, { recursive: true, force: true });
543
+ }
544
+ });
545
+
546
+ test("only active preferences past minAgeDays are candidates", async () => {
547
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-gating-"));
548
+ try {
549
+ const storage = new StorageManager(dir);
550
+ // Every non-active status, plus a too-young preference and a non-preference.
551
+ for (const status of ["pending_review", "rejected", "quarantined", "superseded", "archived"]) {
552
+ await writeMemory(storage, `pref-${status}`, "preference", `Aging preference in ${status}.`, [
553
+ `status: ${status}`,
554
+ ]);
555
+ }
556
+ await writeMemory(storage, "pref-young", "preference", "Brand new preference.", [
557
+ `created: ${new Date(NOW.getTime() - 5 * DAY_MS).toISOString()}`,
558
+ ]);
559
+ await writeMemory(storage, "fact-old", "fact", "An old plain fact.");
560
+ await writeMemory(storage, PREF_IDS.stale, "preference", "User prefers tabs over spaces.");
561
+
562
+ const report = await scan(storage, dir, driftConfig(), { lookup: async () => [] });
563
+
564
+ assert.deepEqual(
565
+ report.findings.map((f) => f.memoryId),
566
+ [PREF_IDS.stale],
567
+ "non-active statuses, young preferences, and non-preferences are all excluded",
568
+ );
569
+ } finally {
570
+ await rm(dir, { recursive: true, force: true });
571
+ }
572
+ });
573
+
574
+ test("maxCandidatesPerRun caps the run and 0 disables the scan entirely", async () => {
575
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-cap-"));
576
+ try {
577
+ const storage = new StorageManager(dir);
578
+ await seedFixture(storage);
579
+
580
+ const capped = await scan(storage, dir, driftConfig({ maxCandidatesPerRun: 1 }));
581
+ assert.equal(capped.scanned, 1);
582
+ assert.equal(capped.eligible, 3, "eligible reports the pre-cap population");
583
+
584
+ const disabled = await scan(storage, dir, driftConfig({ maxCandidatesPerRun: 0 }));
585
+ assert.equal(disabled.skippedReason, "scan_disabled");
586
+ assert.equal(disabled.findings.length, 0);
587
+ } finally {
588
+ await rm(dir, { recursive: true, force: true });
589
+ }
590
+ });
591
+
592
+ test("the scan is a no-op with driftDetection.enabled off", async () => {
593
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-off-"));
594
+ try {
595
+ const storage = new StorageManager(dir);
596
+ await seedFixture(storage);
597
+ const report = await scan(storage, dir, parseConfig({ openaiApiKey: "sk-test" }), { apply: true });
598
+ assert.equal(report.skippedReason, "drift_disabled");
599
+ assert.equal(report.appliedCount, 0);
600
+ assert.equal(await readPreferenceDriftMarker(dir), null);
601
+ } finally {
602
+ await rm(dir, { recursive: true, force: true });
603
+ }
604
+ });
605
+
606
+ // ════════════════════════════════════════════════════════════════════════════
607
+ // 6. Config parsing + frontmatter round-trip
608
+ // ════════════════════════════════════════════════════════════════════════════
609
+
610
+ test("driftDetection defaults are off and parse from string-typed values", () => {
611
+ assert.deepEqual(parseConfig({ openaiApiKey: "sk-test" }).driftDetection, DRIFT_DETECTION_DEFAULTS);
612
+ const coerced = parseDriftDetectionConfig({
613
+ enabled: "true",
614
+ recallDamping: "1",
615
+ minAgeDays: "30",
616
+ lookbackDays: "10",
617
+ maxCandidatesPerRun: "5",
618
+ stalePenalty: "0.5",
619
+ annotateAfterDays: "120",
620
+ });
621
+ assert.deepEqual(coerced, {
622
+ enabled: true,
623
+ minAgeDays: 30,
624
+ lookbackDays: 10,
625
+ maxCandidatesPerRun: 5,
626
+ recallDamping: true,
627
+ stalePenalty: 0.5,
628
+ annotateAfterDays: 120,
629
+ });
630
+ });
631
+
632
+ test("invalid driftDetection values are rejected loudly, never silently defaulted", () => {
633
+ assert.throws(() => parseDriftDetectionConfig([]), /must be an object/);
634
+ assert.throws(() => parseDriftDetectionConfig({ enabled: "maybe" }), /driftDetection\.enabled/);
635
+ assert.throws(() => parseDriftDetectionConfig({ minAgeDays: 1.5 }), /must be an integer/);
636
+ assert.throws(() => parseDriftDetectionConfig({ minAgeDays: -1 }), /between 0 and 36500/);
637
+ assert.throws(() => parseDriftDetectionConfig({ maxCandidatesPerRun: -1 }), /integer >= 0/);
638
+ assert.throws(() => parseDriftDetectionConfig({ stalePenalty: 0 }), /in \(0, 1\]/);
639
+ assert.throws(() => parseDriftDetectionConfig({ stalePenalty: 1.5 }), /in \(0, 1\]/);
640
+ });
641
+
642
+ test("driftState and lastCorroborated round-trip through storage; corrupt values drop", async () => {
643
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-drift-roundtrip-"));
644
+ try {
645
+ const storage = new StorageManager(dir);
646
+ await writeMemory(storage, "pref-rt", "preference", "Round-trip me.", [
647
+ "driftState: stale",
648
+ "lastCorroborated: 2026-05-01T00:00:00.000Z",
649
+ ]);
650
+ await writeMemory(storage, "pref-corrupt", "preference", "Hand-edited verdict.", [
651
+ "driftState: probably",
652
+ ]);
653
+
654
+ const ok = await storage.getMemoryById("pref-rt");
655
+ assert.equal(ok?.frontmatter.driftState, "stale");
656
+ assert.equal(ok?.frontmatter.lastCorroborated, "2026-05-01T00:00:00.000Z");
657
+
658
+ const corrupt = await storage.getMemoryById("pref-corrupt");
659
+ assert.equal(corrupt?.frontmatter.driftState, undefined, "an unrecognized verdict must not survive");
660
+
661
+ // An unrecognized verdict is a caller bug on write, so the serializer throws.
662
+ await assert.rejects(
663
+ storage.writeMemoryFrontmatter(ok as MemoryFile, {
664
+ driftState: "probably" as never,
665
+ }),
666
+ /invalid driftState/,
667
+ );
668
+ } finally {
669
+ await rm(dir, { recursive: true, force: true });
670
+ }
671
+ });
672
+
673
+ test("the recall stage annotates results without mutating the input array", () => {
674
+ const config = parseDriftDetectionConfig({ enabled: true, annotateAfterDays: 30 });
675
+ const results: QmdSearchResult[] = [
676
+ { docid: "1", path: "a.md", snippet: "s", score: 1 },
677
+ ];
678
+ const ordered = applyPreferenceDriftRanking(
679
+ [
680
+ {
681
+ key: "a.md",
682
+ rank: 1,
683
+ frontmatter: { category: "preference", created: "2026-01-01T00:00:00.000Z" },
684
+ },
685
+ ],
686
+ { config, now: NOW },
687
+ );
688
+ assert.ok(ordered[0]!.note, "note is produced for an uncorroborated preference");
689
+ assert.equal(results[0]!.driftNote, undefined, "the pure ranker never touches result objects");
690
+ });