@remnic/core 9.44.0 → 9.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/dist/access-admin-ops-surface.d.ts +10 -10
  2. package/dist/access-admin-ops-surface.js +26 -25
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-authorization-probe.d.ts +10 -10
  5. package/dist/access-authorization-probe.js +27 -26
  6. package/dist/access-boundary.d.ts +10 -10
  7. package/dist/access-boundary.js +26 -25
  8. package/dist/access-cli.js +70 -68
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-extraction-force-flush.d.ts +10 -10
  11. package/dist/access-extraction-force-flush.js +26 -25
  12. package/dist/access-health-types.d.ts +1 -1
  13. package/dist/access-http-lcm-compaction.d.ts +10 -10
  14. package/dist/access-http-lifecycle-flush.d.ts +10 -10
  15. package/dist/access-http-query.js +26 -25
  16. package/dist/access-http.d.ts +10 -10
  17. package/dist/access-http.js +34 -33
  18. package/dist/access-identity-continuity-surface.d.ts +8 -8
  19. package/dist/access-identity-continuity-surface.js +26 -25
  20. package/dist/access-lcm-surface.d.ts +10 -10
  21. package/dist/access-lcm-surface.js +26 -25
  22. package/dist/access-mcp.d.ts +10 -10
  23. package/dist/access-mcp.js +30 -29
  24. package/dist/access-memory-search-fanout.d.ts +2 -2
  25. package/dist/{access-namespace-preflight-47dOyty9.d.ts → access-namespace-preflight-5yZ4g6F8.d.ts} +1 -1
  26. package/dist/access-namespace-preflight.d.ts +3 -3
  27. package/dist/access-namespace-preflight.js +26 -25
  28. package/dist/access-observe-write-surface.d.ts +10 -10
  29. package/dist/access-observe-write-surface.js +26 -25
  30. package/dist/access-operations-batch.js +28 -27
  31. package/dist/access-operations.d.ts +10 -10
  32. package/dist/access-operations.js +29 -28
  33. package/dist/access-recall-concurrency.d.ts +10 -10
  34. package/dist/access-recall-concurrency.js +26 -25
  35. package/dist/access-recall-response.d.ts +10 -10
  36. package/dist/access-recall-response.js +26 -25
  37. package/dist/access-recall-surface.d.ts +10 -10
  38. package/dist/access-recall-surface.js +26 -25
  39. package/dist/{access-service-BCZJbX0f.d.ts → access-service-CiJRsgAB.d.ts} +7 -7
  40. package/dist/access-service-helpers.d.ts +10 -10
  41. package/dist/access-service.d.ts +10 -10
  42. package/dist/access-service.js +26 -25
  43. package/dist/access-surface-catalog.d.ts +10 -10
  44. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  45. package/dist/action-confidence.d.ts +1 -1
  46. package/dist/active-memory-bridge.d.ts +1 -1
  47. package/dist/active-recall.d.ts +1 -1
  48. package/dist/active-recall.js +3 -2
  49. package/dist/active-recall.js.map +1 -1
  50. package/dist/adapters/index.js +4 -4
  51. package/dist/adapters/registry.js +2 -2
  52. package/dist/behavior-learner.d.ts +1 -1
  53. package/dist/behavior-signals.d.ts +1 -1
  54. package/dist/bootstrap.d.ts +8 -8
  55. package/dist/briefing.d.ts +2 -2
  56. package/dist/briefing.js +10 -9
  57. package/dist/buffer-surprise-report.d.ts +1 -1
  58. package/dist/buffer-turn-helpers.d.ts +1 -1
  59. package/dist/buffer.d.ts +2 -2
  60. package/dist/bulk-import/index.d.ts +3 -3
  61. package/dist/calibration.d.ts +1 -1
  62. package/dist/capabilities.d.ts +1 -1
  63. package/dist/{catalog-DPQynQZG.d.ts → catalog-scBmy0sO.d.ts} +1 -1
  64. package/dist/causal-behavior.d.ts +1 -1
  65. package/dist/causal-behavior.js +2 -2
  66. package/dist/causal-chain.js +2 -2
  67. package/dist/causal-consolidation.d.ts +1 -1
  68. package/dist/causal-consolidation.js +13 -12
  69. package/dist/causal-consolidation.js.map +1 -1
  70. package/dist/causal-retrieval.js +2 -2
  71. package/dist/causal-trajectory-graph.d.ts +1 -1
  72. package/dist/causal-trajectory-graph.js +2 -2
  73. package/dist/causal-trajectory.js +1 -1
  74. package/dist/{chunk-CH2WTP6R.js → chunk-33PLLLL7.js} +2 -2
  75. package/dist/{chunk-TXBU7YDV.js → chunk-3TBLDMJA.js} +2 -2
  76. package/dist/{chunk-MUKLQDJP.js → chunk-4UH4BTGK.js} +35 -35
  77. package/dist/{chunk-N7LPS4Z3.js → chunk-5SM35XZR.js} +2 -2
  78. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  79. package/dist/{chunk-YUOKZ3LJ.js → chunk-A5OJCXZL.js} +2 -2
  80. package/dist/{chunk-I22U5YYD.js → chunk-AAIUERNB.js} +2 -2
  81. package/dist/{chunk-UUQLB2HJ.js → chunk-BJKPPZ6Z.js} +2 -2
  82. package/dist/{chunk-EUVPB5TA.js → chunk-CJ2ZR7UA.js} +17 -17
  83. package/dist/{chunk-TM72UOX2.js → chunk-CUM2BBPN.js} +5 -1
  84. package/dist/chunk-CUM2BBPN.js.map +1 -0
  85. package/dist/{chunk-WMHO3WLC.js → chunk-D3J5SLQK.js} +2 -2
  86. package/dist/{chunk-PJUHQC4T.js → chunk-DL7ZD3JH.js} +2 -2
  87. package/dist/{chunk-O4BADU6R.js → chunk-DNWXFTLD.js} +2 -2
  88. package/dist/{chunk-IXFI6E3Q.js → chunk-E53DSZEN.js} +2 -2
  89. package/dist/{chunk-WX6KOVVK.js → chunk-E5UK4VPE.js} +2 -2
  90. package/dist/{chunk-OHLNFI6Z.js → chunk-E6PNLPGJ.js} +25 -23
  91. package/dist/{chunk-OHLNFI6Z.js.map → chunk-E6PNLPGJ.js.map} +1 -1
  92. package/dist/{chunk-Z54EM5YZ.js → chunk-GF22HWS2.js} +4 -4
  93. package/dist/chunk-HBVPYRDW.js +39 -0
  94. package/dist/chunk-HBVPYRDW.js.map +1 -0
  95. package/dist/{chunk-W6ROK2KU.js → chunk-HNZKVLAN.js} +2 -2
  96. package/dist/{chunk-GDC4INBF.js → chunk-HO3QBJEY.js} +2 -2
  97. package/dist/{chunk-J43XN7N3.js → chunk-HREE4JHI.js} +50 -50
  98. package/dist/{chunk-GSL47NPM.js → chunk-J7VGGECD.js} +1 -1
  99. package/dist/chunk-J7VGGECD.js.map +1 -0
  100. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  101. package/dist/{chunk-WUDWRUGK.js → chunk-JE7EK33X.js} +2 -2
  102. package/dist/{chunk-IADUAH35.js → chunk-JVLCVFQ4.js} +2 -2
  103. package/dist/{chunk-UZGJFIIG.js → chunk-JYIFGGEM.js} +2 -2
  104. package/dist/chunk-K442KOID.js +443 -0
  105. package/dist/chunk-K442KOID.js.map +1 -0
  106. package/dist/{chunk-3SHB4YAW.js → chunk-L2DPF4DF.js} +4 -4
  107. package/dist/{chunk-JDDTNK5P.js → chunk-LZ4LDKHZ.js} +2 -2
  108. package/dist/{chunk-PO5LSKQO.js → chunk-MU7ZHSZE.js} +2 -2
  109. package/dist/{chunk-CAGPP5NB.js → chunk-NRV4Y3PM.js} +1 -1
  110. package/dist/{chunk-7PLHD7EO.js → chunk-OAEQLRCY.js} +2 -2
  111. package/dist/{chunk-HGFILS2Q.js → chunk-OQ5PRDDS.js} +2 -2
  112. package/dist/{chunk-E6JYVPBK.js → chunk-P4DRWEYC.js} +2 -2
  113. package/dist/{chunk-MYFFOPO2.js → chunk-PDLBLTOK.js} +2 -2
  114. package/dist/{chunk-EMC5GT4H.js → chunk-PKNLM44P.js} +4 -4
  115. package/dist/{chunk-35IC3EVP.js → chunk-QN25OCUF.js} +2 -2
  116. package/dist/{chunk-OZL7KB6B.js → chunk-RN7DY3FC.js} +5 -5
  117. package/dist/{chunk-2FTRRQKZ.js → chunk-RVLJEII5.js} +30 -511
  118. package/dist/chunk-RVLJEII5.js.map +1 -0
  119. package/dist/{chunk-G24HJJ67.js → chunk-S2NCTZGJ.js} +6 -6
  120. package/dist/chunk-S5SLVSVY.js +517 -0
  121. package/dist/chunk-S5SLVSVY.js.map +1 -0
  122. package/dist/{chunk-R4Q7CQGZ.js → chunk-TDHTVMPG.js} +2 -2
  123. package/dist/{chunk-FXGLFGIG.js → chunk-TIHMZFIV.js} +3 -3
  124. package/dist/{chunk-U5SX74DV.js → chunk-U7GW7NE7.js} +7 -7
  125. package/dist/{chunk-PLQCPMJA.js → chunk-UVIKWC7M.js} +10 -10
  126. package/dist/{chunk-2QR2RRH6.js → chunk-UYLMNVY6.js} +4 -4
  127. package/dist/{chunk-OPVOUBHT.js → chunk-VUPTKTDV.js} +2 -2
  128. package/dist/{chunk-HXTUA7QM.js → chunk-VWAXN2BN.js} +7 -7
  129. package/dist/{chunk-TRBIJS2T.js → chunk-XYTOI63Z.js} +2 -2
  130. package/dist/{cli-U30A3jyx.d.ts → cli-BUGN3d6_.d.ts} +5 -5
  131. package/dist/cli.d.ts +12 -12
  132. package/dist/cli.js +53 -51
  133. package/dist/compounding/engine.d.ts +2 -2
  134. package/dist/compounding/engine.js +10 -9
  135. package/dist/compounding/preference-consolidator.d.ts +1 -1
  136. package/dist/compression-optimizer.d.ts +1 -1
  137. package/dist/config.d.ts +1 -1
  138. package/dist/config.js +3 -2
  139. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  140. package/dist/connectors/codex-materialize-runner.js +10 -9
  141. package/dist/connectors/codex-materialize.d.ts +1 -1
  142. package/dist/connectors/index.d.ts +1 -1
  143. package/dist/connectors/index.js +10 -9
  144. package/dist/consolidation-provenance-check.d.ts +2 -2
  145. package/dist/consolidation-undo.d.ts +2 -2
  146. package/dist/contradiction/index.d.ts +2 -2
  147. package/dist/converge-config.d.ts +11 -0
  148. package/dist/converge-config.js +12 -0
  149. package/dist/convergence-refresh.d.ts +2 -2
  150. package/dist/convergence-refresh.js +12 -11
  151. package/dist/conversation-index/backend.d.ts +1 -1
  152. package/dist/conversation-index/chunker.d.ts +1 -1
  153. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  154. package/dist/conversation-index/indexer.d.ts +1 -1
  155. package/dist/conversation-index/search.d.ts +1 -1
  156. package/dist/corpus-watermark.d.ts +1 -1
  157. package/dist/corpus-watermark.js +12 -11
  158. package/dist/day-summary.d.ts +1 -1
  159. package/dist/delinearize.d.ts +1 -1
  160. package/dist/direct-answer-wiring.d.ts +1 -1
  161. package/dist/direct-answer.d.ts +1 -1
  162. package/dist/embedding-fallback.d.ts +1 -1
  163. package/dist/enrichment/index.d.ts +1 -1
  164. package/dist/entity-retrieval.d.ts +2 -2
  165. package/dist/entity-retrieval.js +10 -9
  166. package/dist/entity-schema.d.ts +1 -1
  167. package/dist/explicit-capture.d.ts +8 -8
  168. package/dist/explicit-capture.js +3 -2
  169. package/dist/extraction-error-classification.d.ts +1 -1
  170. package/dist/extraction-faithfulness.d.ts +1 -1
  171. package/dist/extraction-judge-telemetry.d.ts +1 -1
  172. package/dist/extraction-judge-training.d.ts +1 -1
  173. package/dist/extraction-judge.d.ts +1 -1
  174. package/dist/extraction-liveness.d.ts +1 -1
  175. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  176. package/dist/extraction-source-grounding.d.ts +1 -1
  177. package/dist/extraction.d.ts +1 -1
  178. package/dist/extraction.js +12 -11
  179. package/dist/fallback-llm.d.ts +1 -1
  180. package/dist/graph-dashboard-diff.d.ts +1 -1
  181. package/dist/graph-dashboard-key.d.ts +1 -1
  182. package/dist/graph-dashboard-parser.d.ts +1 -1
  183. package/dist/{graph-edge-decay-GF3ZSGSP.js → graph-edge-decay-Z6ENALU2.js} +4 -4
  184. package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
  185. package/dist/graph-edge-reinforcement.d.ts +1 -1
  186. package/dist/graph-snapshot.d.ts +1 -1
  187. package/dist/graph-snapshot.js +3 -3
  188. package/dist/graph.d.ts +1 -1
  189. package/dist/graph.js +2 -2
  190. package/dist/importance.d.ts +1 -1
  191. package/dist/importers/index.d.ts +1 -1
  192. package/dist/in-flight-reads.d.ts +1 -1
  193. package/dist/index.d.ts +20 -19
  194. package/dist/index.js +78 -71
  195. package/dist/intent.d.ts +1 -1
  196. package/dist/lcm/engine.d.ts +1 -1
  197. package/dist/lcm/engine.js +2 -2
  198. package/dist/lcm/index.d.ts +1 -1
  199. package/dist/lcm/index.js +4 -4
  200. package/dist/lcm/tools.d.ts +1 -1
  201. package/dist/lifecycle.d.ts +1 -1
  202. package/dist/live-connectors-runner.d.ts +1 -1
  203. package/dist/local-llm.d.ts +1 -1
  204. package/dist/local-model-endpoint.d.ts +1 -1
  205. package/dist/maintenance/memory-governance.d.ts +1 -1
  206. package/dist/maintenance/memory-governance.js +10 -9
  207. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  208. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
  209. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  210. package/dist/maintenance/rebuild-memory-projection.js +11 -10
  211. package/dist/{maintenance-DVBLXkFw.d.ts → maintenance-LbuS2rUA.d.ts} +3 -3
  212. package/dist/mcp-memory-inspector-app.d.ts +10 -10
  213. package/dist/memory-action-policy.d.ts +1 -1
  214. package/dist/memory-cache.d.ts +1 -1
  215. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  216. package/dist/memory-projection-store.d.ts +1 -1
  217. package/dist/memory-provenance.d.ts +1 -1
  218. package/dist/memory-worth-outcomes.d.ts +2 -2
  219. package/dist/models-json.d.ts +1 -1
  220. package/dist/namespaces/migrate.d.ts +3 -3
  221. package/dist/namespaces/migrate.js +11 -10
  222. package/dist/namespaces/principal.d.ts +1 -1
  223. package/dist/namespaces/search.d.ts +1 -1
  224. package/dist/namespaces/storage.d.ts +3 -3
  225. package/dist/namespaces/storage.js +10 -9
  226. package/dist/native-knowledge.d.ts +1 -1
  227. package/dist/offline-sync-impression-drain.d.ts +1 -1
  228. package/dist/operator-doctor-corpus.d.ts +1 -1
  229. package/dist/operator-doctor-corpus.js +13 -12
  230. package/dist/operator-doctor-replica.d.ts +1 -1
  231. package/dist/operator-toolkit.d.ts +3 -3
  232. package/dist/operator-toolkit.js +21 -19
  233. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  234. package/dist/orchestration/maintenance.d.ts +4 -4
  235. package/dist/orchestration/maintenance.js +15 -14
  236. package/dist/{orchestrator-DUxbhhqO.d.ts → orchestrator-rHiWt32X.d.ts} +7 -7
  237. package/dist/orchestrator.d.ts +8 -8
  238. package/dist/orchestrator.js +70 -68
  239. package/dist/patterns-cli.d.ts +1 -1
  240. package/dist/{pipeline-Cs76XC-p.d.ts → pipeline-W_97O8BV.d.ts} +1 -1
  241. package/dist/policy-runtime.d.ts +1 -1
  242. package/dist/proactive-contention.d.ts +1 -1
  243. package/dist/provenance.d.ts +1 -1
  244. package/dist/{qmd-WJwvBgiF.d.ts → qmd-LWtMofAf.d.ts} +1 -1
  245. package/dist/qmd-preflight.d.ts +2 -2
  246. package/dist/qmd-recall-cache.d.ts +1 -1
  247. package/dist/qmd.d.ts +2 -2
  248. package/dist/recall-concurrency-config.d.ts +1 -1
  249. package/dist/recall-disclosure-escalation.d.ts +1 -1
  250. package/dist/recall-explain-renderer.d.ts +1 -1
  251. package/dist/recall-explain-renderer.js +3 -3
  252. package/dist/recall-memory-map.d.ts +1 -1
  253. package/dist/recall-planner-llm.d.ts +1 -1
  254. package/dist/recall-state.d.ts +1 -1
  255. package/dist/recall-tag-filter.d.ts +1 -1
  256. package/dist/recall-timings.d.ts +1 -1
  257. package/dist/recall-xray-cli.d.ts +1 -1
  258. package/dist/recall-xray-cli.js +4 -4
  259. package/dist/recall-xray-renderer.d.ts +1 -1
  260. package/dist/recall-xray-renderer.js +3 -3
  261. package/dist/recall-xray.d.ts +1 -1
  262. package/dist/recall-xray.js +2 -2
  263. package/dist/reconcile/manifest.d.ts +33 -0
  264. package/dist/reconcile/manifest.js +249 -0
  265. package/dist/reconcile/manifest.js.map +1 -0
  266. package/dist/reconcile/plan.d.ts +19 -24
  267. package/dist/reconcile/plan.js +9 -404
  268. package/dist/reconcile/plan.js.map +1 -1
  269. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  270. package/dist/replay/normalizers/claude.d.ts +1 -1
  271. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  272. package/dist/replay/normalizers/shared.d.ts +1 -1
  273. package/dist/replay/runner.d.ts +1 -1
  274. package/dist/replay/types.d.ts +1 -1
  275. package/dist/replica-divergence.d.ts +1 -1
  276. package/dist/replica-peers-config.d.ts +1 -1
  277. package/dist/resolve-auth-token.d.ts +1 -1
  278. package/dist/resume-bundles.js +4 -3
  279. package/dist/retrieval-agents.d.ts +2 -2
  280. package/dist/retrieval-tiers.d.ts +1 -1
  281. package/dist/routing/engine.d.ts +1 -1
  282. package/dist/routing/store.d.ts +1 -1
  283. package/dist/salvage-envelope.d.ts +1 -1
  284. package/dist/{scope-profiles-CQ0NAleS.d.ts → scope-profiles-BiQKOE-p.d.ts} +1 -1
  285. package/dist/search/embed-helper.d.ts +1 -1
  286. package/dist/search/factory.d.ts +1 -1
  287. package/dist/search/index.d.ts +1 -1
  288. package/dist/search/lancedb-backend.d.ts +1 -1
  289. package/dist/search/meilisearch-backend.d.ts +1 -1
  290. package/dist/search/noop-backend.d.ts +1 -1
  291. package/dist/search/orama-backend.d.ts +1 -1
  292. package/dist/search/port.d.ts +1 -1
  293. package/dist/search/remote-backend.d.ts +1 -1
  294. package/dist/{semantic-consolidation-BWxOBMtz.d.ts → semantic-consolidation-dHYCyqgh.d.ts} +1 -1
  295. package/dist/semantic-consolidation.d.ts +2 -2
  296. package/dist/semantic-consolidation.js +11 -10
  297. package/dist/semantic-rule-promotion.js +10 -9
  298. package/dist/semantic-rule-verifier.d.ts +1 -1
  299. package/dist/semantic-rule-verifier.js +10 -9
  300. package/dist/{service-CLuMO-pa.d.ts → service-DJG3KCQ4.d.ts} +2 -2
  301. package/dist/session-observer-bands.d.ts +1 -1
  302. package/dist/session-observer-state.d.ts +1 -1
  303. package/dist/shared-context/manager.d.ts +1 -1
  304. package/dist/signal.d.ts +1 -1
  305. package/dist/source-agent-qualifier.d.ts +1 -1
  306. package/dist/source-agent-qualifier.js +10 -9
  307. package/dist/{storage-Blr71GpS.d.ts → storage-F9FVh-w9.d.ts} +13 -1
  308. package/dist/storage.d.ts +2 -2
  309. package/dist/storage.js +9 -8
  310. package/dist/summarizer.d.ts +1 -1
  311. package/dist/summary-snapshot.d.ts +1 -1
  312. package/dist/temporal-supersession.d.ts +2 -2
  313. package/dist/temporal-timeline-recall.d.ts +1 -1
  314. package/dist/temporal-validity.d.ts +1 -1
  315. package/dist/threading.d.ts +1 -1
  316. package/dist/tier-migration.d.ts +2 -2
  317. package/dist/tier-routing.d.ts +1 -1
  318. package/dist/topics.d.ts +1 -1
  319. package/dist/transcript.d.ts +1 -1
  320. package/dist/trust-score-stage.d.ts +1 -1
  321. package/dist/trust-score.d.ts +1 -1
  322. package/dist/{types-CqWwMPr_.d.ts → types-rEwubvim.d.ts} +6 -1
  323. package/dist/types.d.ts +1 -1
  324. package/dist/types.js +1 -1
  325. package/dist/utility-runtime.d.ts +1 -1
  326. package/dist/verified-recall.js +10 -9
  327. package/dist/write-envelope.d.ts +1 -1
  328. package/package.json +12 -2
  329. package/src/config.test.ts +24 -0
  330. package/src/config.ts +3 -0
  331. package/src/converge-config.ts +39 -0
  332. package/src/index.ts +1 -2
  333. package/src/reconcile/manifest.test.ts +261 -0
  334. package/src/reconcile/manifest.ts +278 -0
  335. package/src/reconcile/plan.test.ts +40 -52
  336. package/src/reconcile/plan.ts +79 -63
  337. package/src/storage/content-hash-index.ts +26 -0
  338. package/src/types.ts +5 -0
  339. package/dist/chunk-2FTRRQKZ.js.map +0 -1
  340. package/dist/chunk-GSL47NPM.js.map +0 -1
  341. package/dist/chunk-TM72UOX2.js.map +0 -1
  342. /package/dist/{chunk-CH2WTP6R.js.map → chunk-33PLLLL7.js.map} +0 -0
  343. /package/dist/{chunk-TXBU7YDV.js.map → chunk-3TBLDMJA.js.map} +0 -0
  344. /package/dist/{chunk-MUKLQDJP.js.map → chunk-4UH4BTGK.js.map} +0 -0
  345. /package/dist/{chunk-N7LPS4Z3.js.map → chunk-5SM35XZR.js.map} +0 -0
  346. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  347. /package/dist/{chunk-YUOKZ3LJ.js.map → chunk-A5OJCXZL.js.map} +0 -0
  348. /package/dist/{chunk-I22U5YYD.js.map → chunk-AAIUERNB.js.map} +0 -0
  349. /package/dist/{chunk-UUQLB2HJ.js.map → chunk-BJKPPZ6Z.js.map} +0 -0
  350. /package/dist/{chunk-EUVPB5TA.js.map → chunk-CJ2ZR7UA.js.map} +0 -0
  351. /package/dist/{chunk-WMHO3WLC.js.map → chunk-D3J5SLQK.js.map} +0 -0
  352. /package/dist/{chunk-PJUHQC4T.js.map → chunk-DL7ZD3JH.js.map} +0 -0
  353. /package/dist/{chunk-O4BADU6R.js.map → chunk-DNWXFTLD.js.map} +0 -0
  354. /package/dist/{chunk-IXFI6E3Q.js.map → chunk-E53DSZEN.js.map} +0 -0
  355. /package/dist/{chunk-WX6KOVVK.js.map → chunk-E5UK4VPE.js.map} +0 -0
  356. /package/dist/{chunk-Z54EM5YZ.js.map → chunk-GF22HWS2.js.map} +0 -0
  357. /package/dist/{chunk-W6ROK2KU.js.map → chunk-HNZKVLAN.js.map} +0 -0
  358. /package/dist/{chunk-GDC4INBF.js.map → chunk-HO3QBJEY.js.map} +0 -0
  359. /package/dist/{chunk-J43XN7N3.js.map → chunk-HREE4JHI.js.map} +0 -0
  360. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  361. /package/dist/{chunk-WUDWRUGK.js.map → chunk-JE7EK33X.js.map} +0 -0
  362. /package/dist/{chunk-IADUAH35.js.map → chunk-JVLCVFQ4.js.map} +0 -0
  363. /package/dist/{chunk-UZGJFIIG.js.map → chunk-JYIFGGEM.js.map} +0 -0
  364. /package/dist/{chunk-3SHB4YAW.js.map → chunk-L2DPF4DF.js.map} +0 -0
  365. /package/dist/{chunk-JDDTNK5P.js.map → chunk-LZ4LDKHZ.js.map} +0 -0
  366. /package/dist/{chunk-PO5LSKQO.js.map → chunk-MU7ZHSZE.js.map} +0 -0
  367. /package/dist/{chunk-CAGPP5NB.js.map → chunk-NRV4Y3PM.js.map} +0 -0
  368. /package/dist/{chunk-7PLHD7EO.js.map → chunk-OAEQLRCY.js.map} +0 -0
  369. /package/dist/{chunk-HGFILS2Q.js.map → chunk-OQ5PRDDS.js.map} +0 -0
  370. /package/dist/{chunk-E6JYVPBK.js.map → chunk-P4DRWEYC.js.map} +0 -0
  371. /package/dist/{chunk-MYFFOPO2.js.map → chunk-PDLBLTOK.js.map} +0 -0
  372. /package/dist/{chunk-EMC5GT4H.js.map → chunk-PKNLM44P.js.map} +0 -0
  373. /package/dist/{chunk-35IC3EVP.js.map → chunk-QN25OCUF.js.map} +0 -0
  374. /package/dist/{chunk-OZL7KB6B.js.map → chunk-RN7DY3FC.js.map} +0 -0
  375. /package/dist/{chunk-G24HJJ67.js.map → chunk-S2NCTZGJ.js.map} +0 -0
  376. /package/dist/{chunk-R4Q7CQGZ.js.map → chunk-TDHTVMPG.js.map} +0 -0
  377. /package/dist/{chunk-FXGLFGIG.js.map → chunk-TIHMZFIV.js.map} +0 -0
  378. /package/dist/{chunk-U5SX74DV.js.map → chunk-U7GW7NE7.js.map} +0 -0
  379. /package/dist/{chunk-PLQCPMJA.js.map → chunk-UVIKWC7M.js.map} +0 -0
  380. /package/dist/{chunk-2QR2RRH6.js.map → chunk-UYLMNVY6.js.map} +0 -0
  381. /package/dist/{chunk-OPVOUBHT.js.map → chunk-VUPTKTDV.js.map} +0 -0
  382. /package/dist/{chunk-HXTUA7QM.js.map → chunk-VWAXN2BN.js.map} +0 -0
  383. /package/dist/{chunk-TRBIJS2T.js.map → chunk-XYTOI63Z.js.map} +0 -0
  384. /package/dist/{graph-edge-decay-GF3ZSGSP.js.map → converge-config.js.map} +0 -0
@@ -0,0 +1,278 @@
1
+ import { createHash } from "node:crypto";
2
+ import { inferMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
3
+ import { ContentHashIndex, type ContentHashPathEntry } from "../storage/content-hash-index.js";
4
+ import type { MemoryFrontmatter, MemoryStatus } from "../types.js";
5
+ import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
6
+ import {
7
+ type ReconcileFileState,
8
+ type ReconcilePlan,
9
+ type ReconcilePlanEntry,
10
+ summarizeReconcilePlan,
11
+ } from "./plan.js";
12
+
13
+ export const RECONCILE_MANIFEST_FORMAT = "remnic-reconcile-manifest";
14
+ export const RECONCILE_MANIFEST_SCHEMA_VERSION = 1;
15
+
16
+ export interface ReconcileMemoryIdentity {
17
+ id: string;
18
+ category: string;
19
+ contentHash: string;
20
+ status: MemoryStatus;
21
+ }
22
+
23
+ export interface ReconcileManifestFile extends ReconcileFileState {
24
+ memory?: ReconcileMemoryIdentity;
25
+ }
26
+
27
+ type ActiveFactManifestFile = ReconcileManifestFile & { memory: ReconcileMemoryIdentity };
28
+
29
+ export interface ReconcileManifest {
30
+ format: typeof RECONCILE_MANIFEST_FORMAT;
31
+ schemaVersion: typeof RECONCILE_MANIFEST_SCHEMA_VERSION;
32
+ files: ReconcileManifestFile[];
33
+ }
34
+
35
+ export interface BuildReconcileManifestOptions {
36
+ files: Iterable<ReconcileFileState>;
37
+ readFile: (file: ReconcileFileState) => Promise<Buffer | string | null>;
38
+ cachedFiles?: Iterable<ReconcileManifestFile>;
39
+ }
40
+
41
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
42
+ const MEMORY_DIRS = new Set(RECALL_FALLBACK_DIRS);
43
+
44
+ function isMemoryPath(filePath: string): boolean {
45
+ if (!filePath.endsWith(".md")) return false;
46
+ const segments = filePath.split("/");
47
+ let index = 0;
48
+ if (segments[index] === "cold" || segments[index] === "archive") index += 1;
49
+ return MEMORY_DIRS.has(segments[index] ?? "");
50
+ }
51
+
52
+ function parseScalar(value: string | undefined): string | undefined {
53
+ if (value === undefined) return undefined;
54
+ const trimmed = value.trim();
55
+ if (trimmed.length === 0) return undefined;
56
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
57
+ try {
58
+ const parsed = JSON.parse(trimmed) as unknown;
59
+ return typeof parsed === "string" ? parsed : trimmed;
60
+ } catch {
61
+ return trimmed.slice(1, -1).replace(/\\"/g, '"');
62
+ }
63
+ }
64
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
65
+ return trimmed.slice(1, -1).replace(/''/g, "'");
66
+ }
67
+ return trimmed;
68
+ }
69
+
70
+ function parsedMemoryIdentity(filePath: string, raw: Buffer | string): ReconcileMemoryIdentity | undefined {
71
+ if (!isMemoryPath(filePath)) return undefined;
72
+ const match = (Buffer.isBuffer(raw) ? raw.toString("utf8") : raw).match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
73
+ if (!match) return undefined;
74
+ const fields = new Map<string, string>();
75
+ for (const line of match[1].split("\n")) {
76
+ const separator = line.indexOf(":");
77
+ if (separator > 0) fields.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
78
+ }
79
+ const id = parseScalar(fields.get("id"));
80
+ if (!id) return undefined;
81
+ const category = parseScalar(fields.get("category")) ?? "fact";
82
+ const storedHash = parseScalar(fields.get("contentHash"));
83
+ const contentHash =
84
+ storedHash && SHA256_PATTERN.test(storedHash)
85
+ ? storedHash.toLowerCase()
86
+ : ContentHashIndex.computeHash(match[2].trim());
87
+ const status = inferMemoryStatus(
88
+ {
89
+ status: parseScalar(fields.get("status")) as MemoryStatus | undefined,
90
+ archivedAt: parseScalar(fields.get("archivedAt")),
91
+ } as MemoryFrontmatter,
92
+ filePath
93
+ );
94
+ return { id, category, contentHash, status };
95
+ }
96
+
97
+ export async function buildReconcileManifest(options: BuildReconcileManifestOptions): Promise<ReconcileManifest> {
98
+ const cachedByPath = new Map<string, ReconcileManifestFile>();
99
+ for (const cached of options.cachedFiles ?? []) {
100
+ cachedByPath.set(cached.path, cached);
101
+ }
102
+
103
+ const files: ReconcileManifestFile[] = [];
104
+ for (const file of options.files) {
105
+ const cached = cachedByPath.get(file.path);
106
+ if (cached?.sha256.toLowerCase() === file.sha256.toLowerCase()) {
107
+ files.push({ ...file, ...(cached.memory ? { memory: cached.memory } : {}) });
108
+ continue;
109
+ }
110
+
111
+ let raw: Buffer | string | null = null;
112
+ if (isMemoryPath(file.path)) {
113
+ try {
114
+ raw = await options.readFile(file);
115
+ } catch {
116
+ raw = null;
117
+ }
118
+ }
119
+ if (raw !== null && createHash("sha256").update(raw).digest("hex") !== file.sha256.toLowerCase()) {
120
+ raw = null;
121
+ }
122
+ const memory = raw === null ? undefined : parsedMemoryIdentity(file.path, raw);
123
+ files.push({ ...file, ...(memory ? { memory } : {}) });
124
+ }
125
+ files.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
126
+ return {
127
+ format: RECONCILE_MANIFEST_FORMAT,
128
+ schemaVersion: RECONCILE_MANIFEST_SCHEMA_VERSION,
129
+ files,
130
+ };
131
+ }
132
+
133
+ function activeFactByPath(manifest: ReconcileManifest | undefined): Map<string, ActiveFactManifestFile> {
134
+ const result = new Map<string, ActiveFactManifestFile>();
135
+ for (const file of manifest?.files ?? []) {
136
+ if (file.memory?.category === "fact" && file.memory.status === "active") {
137
+ result.set(file.path, file as ActiveFactManifestFile);
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+
143
+ function contentHashRows(files: Iterable<ActiveFactManifestFile>): ContentHashPathEntry[] {
144
+ const rows: ContentHashPathEntry[] = [];
145
+ for (const file of files) {
146
+ rows.push({ path: file.path, contentHash: file.memory.contentHash });
147
+ }
148
+ return rows;
149
+ }
150
+
151
+ function comparePlanEntries(left: ReconcilePlanEntry, right: ReconcilePlanEntry): number {
152
+ if (left.namespace !== right.namespace) return left.namespace < right.namespace ? -1 : 1;
153
+ return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
154
+ }
155
+
156
+ export function collapseActiveFactDuplicates(
157
+ plan: ReconcilePlan,
158
+ localManifests: ReadonlyMap<string, ReconcileManifest>,
159
+ peerManifests: ReadonlyMap<string, ReconcileManifest>
160
+ ): ReconcilePlan {
161
+ const entriesByNamespace = new Map<string, ReconcilePlanEntry[]>();
162
+ for (const entry of plan.entries) {
163
+ const entries = entriesByNamespace.get(entry.namespace) ?? [];
164
+ entries.push({ ...entry });
165
+ entriesByNamespace.set(entry.namespace, entries);
166
+ }
167
+
168
+ let changed = false;
169
+ for (const namespace of new Set([...localManifests.keys(), ...peerManifests.keys()])) {
170
+ const entries = entriesByNamespace.get(namespace) ?? [];
171
+ const localManifest = localManifests.get(namespace);
172
+ const peerManifest = peerManifests.get(namespace);
173
+ const localByPath = activeFactByPath(localManifest);
174
+ const peerByPath = activeFactByPath(peerManifest);
175
+ const localFilesByPath = new Map((localManifest?.files ?? []).map((file) => [file.path, file]));
176
+ const peerFilesByPath = new Map((peerManifest?.files ?? []).map((file) => [file.path, file]));
177
+ const localByHash = new Map<string, ActiveFactManifestFile[]>();
178
+ const peerByHash = new Map<string, ActiveFactManifestFile[]>();
179
+
180
+ for (const file of localByPath.values()) {
181
+ const hash = file.memory.contentHash;
182
+ const bucket = localByHash.get(hash) ?? [];
183
+ bucket.push(file);
184
+ localByHash.set(hash, bucket);
185
+ }
186
+ for (const file of peerByPath.values()) {
187
+ const hash = file.memory.contentHash;
188
+ const bucket = peerByHash.get(hash) ?? [];
189
+ bucket.push(file);
190
+ peerByHash.set(hash, bucket);
191
+ }
192
+
193
+ const removed = new Set<ReconcilePlanEntry>();
194
+ const replacements: ReconcilePlanEntry[] = [];
195
+ for (const hash of new Set([...localByHash.keys(), ...peerByHash.keys()])) {
196
+ const localCandidates = (localByHash.get(hash) ?? []).filter((file) => {
197
+ const opposite = peerFilesByPath.get(file.path);
198
+ const activeOpposite = peerByPath.get(file.path);
199
+ return opposite === undefined || activeOpposite?.memory.contentHash === hash;
200
+ });
201
+ const peerCandidates = (peerByHash.get(hash) ?? []).filter((file) => {
202
+ const opposite = localFilesByPath.get(file.path);
203
+ const activeOpposite = localByPath.get(file.path);
204
+ return opposite === undefined || activeOpposite?.memory.contentHash === hash;
205
+ });
206
+ const localPath = ContentHashIndex.resolvePathByHash(hash, contentHashRows(localCandidates));
207
+ const peerPath = ContentHashIndex.resolvePathByHash(hash, contentHashRows(peerCandidates));
208
+
209
+ if (localPath && peerPath && localPath !== peerPath) {
210
+ const localFile = localByPath.get(localPath);
211
+ const peerFile = peerByPath.get(peerPath);
212
+ if (!localFile || !peerFile) continue;
213
+ const duplicatePaths = new Set([
214
+ ...localCandidates.map((file) => file.path),
215
+ ...peerCandidates.map((file) => file.path),
216
+ ]);
217
+ const unsafeEntry = entries.some(
218
+ (entry) => duplicatePaths.has(entry.path) && (entry.action === "suppress" || entry.action === "conflict")
219
+ );
220
+ if (unsafeEntry) continue;
221
+ for (const entry of entries) {
222
+ if (
223
+ duplicatePaths.has(entry.path) &&
224
+ (entry.action === "pull" || entry.action === "push" || entry.action === "identical")
225
+ ) {
226
+ removed.add(entry);
227
+ }
228
+ }
229
+ replacements.push({
230
+ path: localPath < peerPath ? localPath : peerPath,
231
+ namespace,
232
+ action: "identical",
233
+ reason: "semantic_duplicate",
234
+ localSha256: localFile.sha256,
235
+ peerSha256: peerFile.sha256,
236
+ });
237
+ changed = true;
238
+ continue;
239
+ }
240
+
241
+ const sameSideEntries = entries.filter((entry) => {
242
+ if (localPath && entry.reason === "local_only") {
243
+ return localCandidates.some((file) => file.path === entry.path);
244
+ }
245
+ if (peerPath && entry.reason === "peer_only") {
246
+ return peerCandidates.some((file) => file.path === entry.path);
247
+ }
248
+ return false;
249
+ });
250
+ const canonicalPath = localPath ?? peerPath;
251
+ const hasSharedCanonical = localPath !== undefined && localPath === peerPath;
252
+ if (!canonicalPath || sameSideEntries.length === 0 || (!hasSharedCanonical && sameSideEntries.length < 2)) {
253
+ continue;
254
+ }
255
+ for (const entry of sameSideEntries) {
256
+ if (entry.path !== canonicalPath) {
257
+ removed.add(entry);
258
+ changed = true;
259
+ }
260
+ }
261
+ }
262
+
263
+ if (removed.size > 0 || replacements.length > 0) {
264
+ entriesByNamespace.set(
265
+ namespace,
266
+ [...entries.filter((entry) => !removed.has(entry)), ...replacements].sort(comparePlanEntries)
267
+ );
268
+ }
269
+ }
270
+
271
+ if (!changed) return plan;
272
+ const entries = [...entriesByNamespace.values()].flat().sort(comparePlanEntries);
273
+ return {
274
+ entries,
275
+ byNamespace: summarizeReconcilePlan(entries),
276
+ converged: entries.every((entry) => entry.action === "identical"),
277
+ };
278
+ }
@@ -60,7 +60,7 @@ test("a bootstrap merge moves unique data both ways and deletes nothing", () =>
60
60
  );
61
61
  });
62
62
 
63
- test("manual is the default policy: a both-modified path is reported, never auto-picked", () => {
63
+ test("newest-wins is the default policy", () => {
64
64
  const entries = planNamespaceReconciliation({
65
65
  namespace: "default",
66
66
  local: [file("facts/a.md", "local", 2000)],
@@ -69,7 +69,7 @@ test("manual is the default policy: a both-modified path is reported, never auto
69
69
  const conflict = entryFor(entries, "facts/a.md");
70
70
  assert.equal(conflict.action, "conflict");
71
71
  assert.equal(conflict.reason, "both_modified");
72
- assert.equal(conflict.resolution, "unresolved", "equally authoritative sides must not be settled by default");
72
+ assert.equal(conflict.resolution, "local-wins");
73
73
  });
74
74
 
75
75
  test("newest-wins picks the later side by mtime", () => {
@@ -101,17 +101,6 @@ test("newest-wins degrades to keeping both when the order is not decidable", ()
101
101
  assert.equal(entryFor(entries, "facts/tied.md").resolution, "supersede-link");
102
102
  });
103
103
 
104
- test("keep-both never designates a loser", () => {
105
- const entries = planNamespaceReconciliation(
106
- {
107
- namespace: "default",
108
- local: [file("facts/a.md", "local", 2000)],
109
- peer: [file("facts/a.md", "peer", 1000)],
110
- },
111
- { conflictPolicy: "keep-both" },
112
- );
113
- assert.equal(entryFor(entries, "facts/a.md").resolution, "supersede-link");
114
- });
115
104
 
116
105
  test("a base turns a one-sided change back into an ordinary transfer", () => {
117
106
  const base = [file("facts/a.md", "v1"), file("facts/b.md", "v1")];
@@ -200,6 +189,41 @@ test("delete-versus-modify stays a conflict in both directions", () => {
200
189
  assert.equal(peerEdited.resolution, "unresolved");
201
190
  });
202
191
 
192
+ test("newest-wins compares delete revision times with surviving modifications", () => {
193
+ const entries = planNamespaceReconciliation(
194
+ {
195
+ namespace: "default",
196
+ local: [
197
+ file("facts/local-modification-wins.md", "local-v2", 4000),
198
+ file("facts/peer-deletion-wins.md", "local-v2", 2000),
199
+ ],
200
+ peer: [
201
+ file("facts/peer-modification-wins.md", "peer-v2", 4000),
202
+ file("facts/local-deletion-wins.md", "peer-v2", 2000),
203
+ ],
204
+ base: [
205
+ file("facts/local-modification-wins.md", "v1"),
206
+ file("facts/peer-deletion-wins.md", "v1"),
207
+ file("facts/peer-modification-wins.md", "v1"),
208
+ file("facts/local-deletion-wins.md", "v1"),
209
+ ],
210
+ localDeletionMtimeMs: new Map([
211
+ ["facts/peer-modification-wins.md", 3000],
212
+ ["facts/local-deletion-wins.md", 3000],
213
+ ]),
214
+ peerDeletionMtimeMs: new Map([
215
+ ["facts/local-modification-wins.md", 3000],
216
+ ["facts/peer-deletion-wins.md", 3000],
217
+ ]),
218
+ },
219
+ { conflictPolicy: "newest-wins" },
220
+ );
221
+ assert.equal(entryFor(entries, "facts/local-modification-wins.md").resolution, "local-wins");
222
+ assert.equal(entryFor(entries, "facts/peer-deletion-wins.md").resolution, "peer-wins");
223
+ assert.equal(entryFor(entries, "facts/peer-modification-wins.md").resolution, "peer-wins");
224
+ assert.equal(entryFor(entries, "facts/local-deletion-wins.md").resolution, "local-wins");
225
+ });
226
+
203
227
  test("namespaces are planned independently and reported separately", () => {
204
228
  const plan = planReconciliation([
205
229
  { namespace: "alpha", local: [file("facts/a.md", "x")], peer: [] },
@@ -774,29 +798,6 @@ test("Win32-aliasing path segments are rejected", () => {
774
798
  }
775
799
  });
776
800
 
777
- test("a supersede link names the newer revision", () => {
778
- // Without a direction transport must re-derive it or guess which side is
779
- // linked as superseding the other.
780
- const newerPeer = planNamespaceReconciliation(
781
- {
782
- namespace: "default",
783
- local: [file("facts/a.md", "local", 1000)],
784
- peer: [file("facts/a.md", "peer", 2000)],
785
- },
786
- { conflictPolicy: "keep-both" },
787
- );
788
- assert.equal(entryFor(newerPeer, "facts/a.md").newerSide, "peer");
789
-
790
- const newerLocal = planNamespaceReconciliation(
791
- {
792
- namespace: "default",
793
- local: [file("facts/a.md", "local", 2000)],
794
- peer: [file("facts/a.md", "peer", 1000)],
795
- },
796
- { conflictPolicy: "keep-both" },
797
- );
798
- assert.equal(entryFor(newerLocal, "facts/a.md").newerSide, "local");
799
- });
800
801
 
801
802
  test("an unorderable supersede link says so instead of picking a side", () => {
802
803
  for (const pair of [
@@ -805,11 +806,10 @@ test("an unorderable supersede link says so instead of picking a side", () => {
805
806
  ] as const) {
806
807
  const entries = planNamespaceReconciliation(
807
808
  { namespace: "default", local: [pair[0]], peer: [pair[1]] },
808
- { conflictPolicy: "keep-both" },
809
+ { conflictPolicy: "newest-wins" },
809
810
  );
810
811
  const entry = entryFor(entries, "facts/a.md");
811
812
  assert.equal(entry.resolution, "supersede-link");
812
- assert.equal(entry.newerSide, undefined, "an unordered link must be visibly unordered");
813
813
  }
814
814
  });
815
815
 
@@ -845,25 +845,13 @@ test("a directionless supersede link is reported as unresolved", () => {
845
845
  local: [file("facts/a.md", "local", 5000)],
846
846
  peer: [file("facts/a.md", "peer", 5000)],
847
847
  },
848
- { conflictPolicy: "keep-both" },
848
+ { conflictPolicy: "newest-wins" },
849
849
  );
850
- assert.equal(entryFor(entries, "facts/a.md").newerSide, undefined);
851
850
  assert.deepEqual(summarizeReconcilePlan(entries), [
852
851
  { namespace: "default", pull: 0, push: 0, identical: 0, conflict: 1, suppress: 0, unresolved: 1 },
853
852
  ]);
854
853
  });
855
854
 
856
- test("an ordered supersede link is not counted as unresolved", () => {
857
- const entries = planNamespaceReconciliation(
858
- {
859
- namespace: "default",
860
- local: [file("facts/a.md", "local", 1000)],
861
- peer: [file("facts/a.md", "peer", 2000)],
862
- },
863
- { conflictPolicy: "keep-both" },
864
- );
865
- assert.equal(summarizeReconcilePlan(entries)[0]?.unresolved, 0);
866
- });
867
855
 
868
856
  test("superscript COM and LPT device aliases are rejected", () => {
869
857
  for (const bad of ["facts/COM\u00b9.txt", "facts/LPT\u00b2", "facts/com\u00b3.md"]) {
@@ -926,7 +914,7 @@ test("an empty namespace list still validates its options", () => {
926
914
  `options ${Object.prototype.toString.call(bad)}`,
927
915
  );
928
916
  }
929
- assert.equal(planReconciliation([], { conflictPolicy: "keep-both" }).converged, true);
917
+ assert.equal(planReconciliation([], { conflictPolicy: "manual" }).converged, true);
930
918
  });
931
919
 
932
920
  test("sharp-S aliases fold together", () => {
@@ -2,6 +2,11 @@ import { normalizeNamespaceIdentity } from "../namespaces/identity.js";
2
2
  import { OFFLINE_SYNC_MAX_MTIME_MS } from "../offline-sync.js";
3
3
  import { validateArchiveRelativePath } from "../transfer/fs-utils.js";
4
4
  import type { OfflineSyncFileState } from "../offline-sync.js";
5
+ import {
6
+ CONVERGE_CONFLICT_POLICIES,
7
+ DEFAULT_CONVERGE_CONFLICT_POLICY,
8
+ } from "../converge-config.js";
9
+ import type { ConvergeConflictPolicy } from "../types.js";
5
10
 
6
11
  /**
7
12
  * Bootstrap reconciliation planner for two peer daemons whose corpora have
@@ -24,21 +29,11 @@ import type { OfflineSyncFileState } from "../offline-sync.js";
24
29
  /** Which way a path must move for the two corpora to agree. */
25
30
  export type ReconcileAction = "pull" | "push" | "identical" | "conflict" | "suppress";
26
31
 
27
- /**
28
- * How a path present on BOTH sides with different content is settled.
29
- *
30
- * `manual` is the safe default: on a bootstrap merge the two sides are equally
31
- * authoritative, so silently picking one is data loss with extra steps. An
32
- * operator opts into an automatic rule once they know the shape of their split.
33
- */
34
- export type ReconcileConflictPolicy = "manual" | "newest-wins" | "keep-both";
35
-
36
32
  /**
37
33
  * What the planner decided for a conflicting path.
38
- *
39
- * `keep-both` never overwrites: the older side is retained and linked as
40
- * superseded by the newer one, which is the only resolution that cannot lose a
41
- * fact neither corpus has seen.
34
+ * `supersede-link` records that both revisions need durable preservation when
35
+ * timestamps cannot safely select a winner. Apply must stop until transport
36
+ * can assign distinct durable identities to both revisions.
42
37
  */
43
38
  export type ReconcileResolution = "local-wins" | "peer-wins" | "supersede-link" | "unresolved";
44
39
 
@@ -61,15 +56,6 @@ export interface ReconcilePlanEntry {
61
56
  * delete the live copy instead of the retracted one.
62
57
  */
63
58
  suppressSide?: "local" | "peer" | "both";
64
- /**
65
- * Which revision is newer, set only for a `supersede-link` resolution.
66
- *
67
- * The contract is that the older revision is linked as superseded by the
68
- * newer one, so transport needs the direction. Absent when the timestamps
69
- * cannot order the two - which is exactly when `newest-wins` degrades to
70
- * `supersede-link` - and the link direction is then an operator decision.
71
- */
72
- newerSide?: "local" | "peer";
73
59
  }
74
60
 
75
61
  export type ReconcileReason =
@@ -79,6 +65,7 @@ export type ReconcileReason =
79
65
  | "peer_changed"
80
66
  | "local_changed"
81
67
  | "same_content"
68
+ | "semantic_duplicate"
82
69
  | "both_modified"
83
70
  | "peer_deleted"
84
71
  | "local_deleted"
@@ -130,6 +117,14 @@ export interface ReconcileNamespaceInput {
130
117
  * read as "never seen" rather than "deleted".
131
118
  */
132
119
  base?: Iterable<ReconcileFileState>;
120
+ /**
121
+ * Revision timestamps for paths deleted locally since the base cursor.
122
+ * A deletion is a revision and must carry a comparable time for
123
+ * `newest-wins`; absence alone has no timestamp.
124
+ */
125
+ localDeletionMtimeMs?: ReadonlyMap<string, number>;
126
+ /** Revision timestamps for paths deleted by the peer since the base cursor. */
127
+ peerDeletionMtimeMs?: ReadonlyMap<string, number>;
133
128
  /**
134
129
  * Digests of FILES this side has retracted, in the same form as
135
130
  * `ReconcileFileState.sha256` — a hash of the serialized file.
@@ -162,7 +157,7 @@ export interface ReconcileNamespaceInput {
162
157
  }
163
158
 
164
159
  export interface ReconcileOptions {
165
- conflictPolicy?: ReconcileConflictPolicy;
160
+ conflictPolicy?: ConvergeConflictPolicy;
166
161
  }
167
162
 
168
163
  /**
@@ -472,13 +467,11 @@ function indexDigestsByPath(
472
467
  return index;
473
468
  }
474
469
 
475
- const RECONCILE_CONFLICT_POLICIES: readonly ReconcileConflictPolicy[] = ["manual", "newest-wins", "keep-both"];
476
-
477
- function assertConflictPolicy(value: ReconcileConflictPolicy | undefined): ReconcileConflictPolicy {
478
- if (value === undefined) return "manual";
479
- if (!RECONCILE_CONFLICT_POLICIES.includes(value)) {
470
+ function assertConflictPolicy(value: ConvergeConflictPolicy | undefined): ConvergeConflictPolicy {
471
+ if (value === undefined) return DEFAULT_CONVERGE_CONFLICT_POLICY;
472
+ if (!CONVERGE_CONFLICT_POLICIES.includes(value)) {
480
473
  throw new ReconcilePlanInputError(
481
- `reconcile: unknown conflictPolicy ${JSON.stringify(value)}; expected one of ${RECONCILE_CONFLICT_POLICIES.join(", ")}`,
474
+ `reconcile: unknown conflictPolicy ${JSON.stringify(value)}; expected one of ${CONVERGE_CONFLICT_POLICIES.join(", ")}`,
482
475
  );
483
476
  }
484
477
  return value;
@@ -496,36 +489,42 @@ function compareEntries(a: ReconcilePlanEntry, b: ReconcilePlanEntry): number {
496
489
  return 0;
497
490
  }
498
491
 
499
- /**
500
- * Direction for a supersede link, when the timestamps can order the pair.
501
- * Omitted otherwise so an unordered link is visibly unordered rather than
502
- * silently defaulted to one side.
503
- */
504
- function newerSideOf(
505
- local: ReconcileFileState,
506
- peer: ReconcileFileState,
507
- ): { newerSide?: "local" | "peer" } {
508
- const localMs = local.mtimeMs;
509
- const peerMs = peer.mtimeMs;
510
- if (typeof localMs !== "number" || typeof peerMs !== "number" || localMs === peerMs) return {};
511
- return { newerSide: localMs > peerMs ? "local" : "peer" };
512
- }
513
492
 
514
493
  function resolveConflict(
515
- policy: ReconcileConflictPolicy,
516
- local: ReconcileFileState,
517
- peer: ReconcileFileState,
494
+ policy: ConvergeConflictPolicy,
495
+ localMtimeMs: number | undefined,
496
+ peerMtimeMs: number | undefined,
497
+ missingTimestampResolution: ReconcileResolution,
518
498
  ): ReconcileResolution {
519
- if (policy === "keep-both") return "supersede-link";
520
499
  if (policy !== "newest-wins") return "unresolved";
521
- const localMs = typeof local.mtimeMs === "number" && Number.isFinite(local.mtimeMs) ? local.mtimeMs : null;
522
- const peerMs = typeof peer.mtimeMs === "number" && Number.isFinite(peer.mtimeMs) ? peer.mtimeMs : null;
523
- // Without a usable timestamp on both sides "newest" is not decidable, and a
524
- // coin flip here silently discards one side's history. Fall back to keeping
525
- // both rather than inventing an order.
526
- if (localMs === null || peerMs === null) return "supersede-link";
527
- if (localMs === peerMs) return "supersede-link";
528
- return localMs > peerMs ? "local-wins" : "peer-wins";
500
+ if (localMtimeMs === undefined || peerMtimeMs === undefined || localMtimeMs === peerMtimeMs) {
501
+ return missingTimestampResolution;
502
+ }
503
+ return localMtimeMs > peerMtimeMs ? "local-wins" : "peer-wins";
504
+ }
505
+
506
+ function parseDeletionMtimes(
507
+ value: ReadonlyMap<string, number> | undefined,
508
+ side: string,
509
+ namespace: string,
510
+ ): ReadonlyMap<string, number> {
511
+ if (value === undefined) return new Map();
512
+ if (!(value instanceof Map)) {
513
+ throw new ReconcilePlanInputError(
514
+ `reconcile: ${side} deletion mtimes for namespace ${namespace} must be a Map`,
515
+ );
516
+ }
517
+ const parsed = new Map<string, number>();
518
+ for (const [path, mtimeMs] of value) {
519
+ try {
520
+ validateArchiveRelativePath(path, `reconcile: ${side} deletion mtimes for namespace ${namespace}`);
521
+ } catch (err) {
522
+ throw new ReconcilePlanInputError(err instanceof Error ? err.message : String(err));
523
+ }
524
+ assertPortablePathSegments(path, side, namespace);
525
+ parsed.set(path, assertMtimeMs(mtimeMs, `${side} deletion for namespace ${namespace} entry ${path}`));
526
+ }
527
+ return parsed;
529
528
  }
530
529
 
531
530
  /**
@@ -571,6 +570,8 @@ export function planNamespaceReconciliation(
571
570
  namespace,
572
571
  "peerTombstonedFileSha256",
573
572
  );
573
+ const localDeletionMtimeMs = parseDeletionMtimes(input.localDeletionMtimeMs, "local", namespace);
574
+ const peerDeletionMtimeMs = parseDeletionMtimes(input.peerDeletionMtimeMs, "peer", namespace);
574
575
  const entries: ReconcilePlanEntry[] = [];
575
576
 
576
577
  // Path -> digest only, never the file objects: enough to make the stream
@@ -638,7 +639,14 @@ export function planNamespaceReconciliation(
638
639
  reason: baseSha256 === localFile.sha256 ? "peer_deleted" : "local_modified_peer_deleted",
639
640
  localSha256: localFile.sha256,
640
641
  baseSha256,
641
- resolution: "unresolved",
642
+ resolution: baseSha256 === localFile.sha256
643
+ ? "unresolved"
644
+ : resolveConflict(
645
+ policy,
646
+ localFile.mtimeMs,
647
+ peerDeletionMtimeMs.get(path),
648
+ "unresolved",
649
+ ),
642
650
  });
643
651
  continue;
644
652
  }
@@ -689,7 +697,12 @@ export function planNamespaceReconciliation(
689
697
  });
690
698
  continue;
691
699
  }
692
- const resolution = resolveConflict(policy, localFile, peerFile);
700
+ const resolution = resolveConflict(
701
+ policy,
702
+ localFile.mtimeMs,
703
+ peerFile.mtimeMs,
704
+ "supersede-link",
705
+ );
693
706
  entries.push({
694
707
  path,
695
708
  namespace,
@@ -699,7 +712,6 @@ export function planNamespaceReconciliation(
699
712
  peerSha256: peerFile.sha256,
700
713
  ...(baseSha256 === undefined ? {} : { baseSha256 }),
701
714
  resolution,
702
- ...(resolution === "supersede-link" ? newerSideOf(localFile, peerFile) : {}),
703
715
  });
704
716
  }
705
717
 
@@ -731,7 +743,14 @@ export function planNamespaceReconciliation(
731
743
  reason: baseSha256 === peerFile.sha256 ? "local_deleted" : "local_deleted_peer_modified",
732
744
  peerSha256: peerFile.sha256,
733
745
  baseSha256,
734
- resolution: "unresolved",
746
+ resolution: baseSha256 === peerFile.sha256
747
+ ? "unresolved"
748
+ : resolveConflict(
749
+ policy,
750
+ localDeletionMtimeMs.get(path),
751
+ peerFile.mtimeMs,
752
+ "unresolved",
753
+ ),
735
754
  });
736
755
  continue;
737
756
  }
@@ -757,12 +776,9 @@ export function summarizeReconcilePlan(entries: readonly ReconcilePlanEntry[]):
757
776
  byNamespace.set(entry.namespace, report);
758
777
  }
759
778
  report[entry.action] += 1;
760
- // A supersede link with no `newerSide` could not be ordered, and the link
761
- // direction is then an operator decision - so it counts as unresolved even
762
- // though the policy nominally settled it.
763
779
  const needsOperator =
764
780
  entry.resolution === "unresolved"
765
- || (entry.resolution === "supersede-link" && entry.newerSide === undefined);
781
+ || entry.resolution === "supersede-link";
766
782
  if (entry.action === "conflict" && needsOperator) report.unresolved += 1;
767
783
  }
768
784
  return [...byNamespace.values()].sort((a, b) => (a.namespace === b.namespace ? 0 : a.namespace < b.namespace ? -1 : 1));