@remnic/core 9.66.1 → 9.66.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 (606) hide show
  1. package/dist/access-admin-ops-surface.js +30 -30
  2. package/dist/access-authorization-probe.js +31 -31
  3. package/dist/access-boundary.js +30 -30
  4. package/dist/access-cli.js +96 -94
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +30 -30
  7. package/dist/access-http-query.js +32 -32
  8. package/dist/access-http.js +45 -45
  9. package/dist/access-identity-continuity-surface.js +30 -30
  10. package/dist/access-lcm-surface.js +30 -30
  11. package/dist/access-mcp.js +38 -38
  12. package/dist/access-namespace-preflight.js +30 -30
  13. package/dist/access-observe-write-surface.js +30 -30
  14. package/dist/access-operations-batch.js +33 -33
  15. package/dist/access-operations.js +37 -37
  16. package/dist/access-recall-concurrency.js +30 -30
  17. package/dist/access-recall-response.js +30 -30
  18. package/dist/access-recall-surface.js +30 -30
  19. package/dist/access-schema.js +3 -3
  20. package/dist/access-service.js +30 -30
  21. package/dist/access-surface-catalog.js +32 -32
  22. package/dist/active-recall.js +3 -3
  23. package/dist/briefing.js +13 -13
  24. package/dist/buffer.js +4 -4
  25. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  26. package/dist/causal-behavior.js +2 -2
  27. package/dist/causal-chain.js +2 -2
  28. package/dist/causal-consolidation.js +16 -16
  29. package/dist/causal-retrieval.js +2 -2
  30. package/dist/causal-trajectory-graph.js +2 -2
  31. package/dist/causal-trajectory.js +1 -1
  32. package/dist/{chunk-XKNHFDTT.js → chunk-3A2Z4OOS.js} +2 -2
  33. package/dist/{chunk-CHGG74CY.js → chunk-3HHTN5X7.js} +2 -2
  34. package/dist/{chunk-TCF4MU7L.js → chunk-3PO35HUU.js} +1218 -104
  35. package/dist/chunk-3PO35HUU.js.map +1 -0
  36. package/dist/{chunk-XOHYGIWF.js → chunk-3Z4SFJA5.js} +2 -2
  37. package/dist/{chunk-RDGKVH6T.js → chunk-4NKIGSJM.js} +2 -2
  38. package/dist/{chunk-GPFZFCRN.js → chunk-4PTTMTTQ.js} +1 -1
  39. package/dist/{chunk-VG4SSPAW.js → chunk-4XESAT4V.js} +2 -2
  40. package/dist/{chunk-S4CDUXQY.js → chunk-5KGLNBUB.js} +2 -2
  41. package/dist/{chunk-2X6BGKQL.js → chunk-5XRZANCW.js} +10 -10
  42. package/dist/{chunk-TCQRIIIN.js → chunk-6DODJYBY.js} +2 -2
  43. package/dist/{chunk-KWIGDKM2.js → chunk-73CAEYI3.js} +2 -2
  44. package/dist/{chunk-J2R275FN.js → chunk-7DXW5V2W.js} +2 -2
  45. package/dist/{chunk-LYUUA6QB.js → chunk-7JJHIVMG.js} +2 -2
  46. package/dist/{chunk-2FA7U5XY.js → chunk-7NNMRMLZ.js} +2 -2
  47. package/dist/{chunk-227UFSEQ.js → chunk-7RZJWZ46.js} +5 -5
  48. package/dist/{chunk-EWKVYCFP.js → chunk-B2OKCCXX.js} +1 -1
  49. package/dist/{chunk-NSTNGMSS.js → chunk-B7AGLMGP.js} +7 -7
  50. package/dist/chunk-BLUYQFRH.js +36 -0
  51. package/dist/chunk-BLUYQFRH.js.map +1 -0
  52. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  53. package/dist/{chunk-CEH2AFXG.js → chunk-C24UZSFW.js} +6 -6
  54. package/dist/{chunk-LTBKKSCX.js → chunk-C2JCVZWX.js} +2 -2
  55. package/dist/{chunk-65JOGLIS.js → chunk-D4R3OUZA.js} +2 -2
  56. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  57. package/dist/chunk-E4X2YX2F.js +22 -0
  58. package/dist/chunk-E4X2YX2F.js.map +1 -0
  59. package/dist/{chunk-IGZCAHFL.js → chunk-EGGNFLAT.js} +2 -2
  60. package/dist/{chunk-H2IRSXZN.js → chunk-ESDOAP7W.js} +3 -3
  61. package/dist/{chunk-D3UXGE5D.js → chunk-EZWIEFYD.js} +8 -8
  62. package/dist/{chunk-ZPS724LT.js → chunk-F5H5QINZ.js} +2 -2
  63. package/dist/{chunk-S4Z2OBTX.js → chunk-FPSOXMLK.js} +3 -3
  64. package/dist/chunk-FYDW33ZB.js +13 -0
  65. package/dist/chunk-FYDW33ZB.js.map +1 -0
  66. package/dist/{chunk-CX4OLNIH.js → chunk-G5UKF2GJ.js} +2 -2
  67. package/dist/{chunk-OJUEUQDJ.js → chunk-GGY2747C.js} +2 -2
  68. package/dist/{chunk-TNGH4ZYI.js → chunk-GNWLTCKC.js} +2 -2
  69. package/dist/{chunk-YWPLOJ7T.js → chunk-GZPGI45F.js} +2 -2
  70. package/dist/{chunk-NFP4KPTA.js → chunk-IDEPEHUG.js} +2 -2
  71. package/dist/{chunk-LJABGXC4.js → chunk-IN5EUWCA.js} +3 -3
  72. package/dist/{chunk-U6NGKS32.js → chunk-JJAOVJ3E.js} +9 -9
  73. package/dist/{chunk-RPIS5FQ7.js → chunk-JWCS3VYQ.js} +50 -50
  74. package/dist/{chunk-U65VJSCJ.js → chunk-KE23ZIEW.js} +13 -13
  75. package/dist/{chunk-NACLNNFQ.js → chunk-KSZKQTZS.js} +2 -2
  76. package/dist/{chunk-KJXCD7VR.js → chunk-KWJNEWTM.js} +5 -5
  77. package/dist/chunk-MAG6M3B6.js +51 -0
  78. package/dist/chunk-MAG6M3B6.js.map +1 -0
  79. package/dist/chunk-MKD3OMPS.js +20 -0
  80. package/dist/chunk-MKD3OMPS.js.map +1 -0
  81. package/dist/{chunk-UW4RYPWC.js → chunk-MLTGALDY.js} +2 -2
  82. package/dist/{chunk-MUEGIKKY.js → chunk-OKGQDI4Y.js} +17 -16
  83. package/dist/chunk-OKGQDI4Y.js.map +1 -0
  84. package/dist/{chunk-YMOPYG3K.js → chunk-OLT3YTDH.js} +4 -4
  85. package/dist/{chunk-2PSX5BJU.js → chunk-OPQKVVYG.js} +2 -2
  86. package/dist/{chunk-D6Q5EPOV.js → chunk-PGD3DXZP.js} +2 -2
  87. package/dist/chunk-Q3SHEAXR.js +80 -0
  88. package/dist/chunk-Q3SHEAXR.js.map +1 -0
  89. package/dist/{chunk-GBEOUCBB.js → chunk-Q6LC3ZD5.js} +2 -2
  90. package/dist/{chunk-QBNE55VA.js → chunk-QELYSDAU.js} +2 -2
  91. package/dist/{chunk-INUBU6I5.js → chunk-S6PRJGQL.js} +2 -2
  92. package/dist/{chunk-B4AOWRYF.js → chunk-TCJO7LOG.js} +5 -5
  93. package/dist/{chunk-SZMYGDSM.js → chunk-TO5U47YK.js} +2 -2
  94. package/dist/{chunk-GAJK3DQ5.js → chunk-U3L6GRUX.js} +2 -2
  95. package/dist/{chunk-4XLTUR6R.js → chunk-U4RGSLI4.js} +3 -3
  96. package/dist/{chunk-M5ERTI4I.js → chunk-UH53XVQ3.js} +2 -2
  97. package/dist/{chunk-NUF7VSEX.js → chunk-UXERIBOJ.js} +2 -2
  98. package/dist/{chunk-FFUMTH27.js → chunk-VA2CSIPC.js} +2 -2
  99. package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
  100. package/dist/{chunk-AF5H7TKI.js → chunk-VPJNGE2R.js} +2 -2
  101. package/dist/{chunk-OMX6TKXH.js → chunk-VTJZSLDR.js} +2 -2
  102. package/dist/{chunk-TBCDF556.js → chunk-WHFFAS4A.js} +4 -4
  103. package/dist/{chunk-7UOXCCCI.js → chunk-WVNETF4Q.js} +2 -2
  104. package/dist/{chunk-EH65UV2Z.js → chunk-XSTMPZMY.js} +2 -2
  105. package/dist/{chunk-PD66CVSD.js → chunk-Z3SQEEB6.js} +2 -2
  106. package/dist/{chunk-JKTV3JNX.js → chunk-ZBYOXIIF.js} +3 -3
  107. package/dist/{chunk-2ITZ74A6.js → chunk-ZC5IHL5X.js} +11 -11
  108. package/dist/{chunk-KI64TTBL.js → chunk-ZDMTCO7N.js} +2 -2
  109. package/dist/{chunk-C5IUOBCN.js → chunk-ZET6A3OF.js} +2 -2
  110. package/dist/{chunk-ZZZWEDF4.js → chunk-ZVAKRZZD.js} +2 -2
  111. package/dist/cli.js +67 -67
  112. package/dist/coding/export-okf-codegraph.js +13 -13
  113. package/dist/coding/pre-action-gate.js +2 -2
  114. package/dist/compounding/engine.js +13 -13
  115. package/dist/config.js +3 -3
  116. package/dist/connectors/codex-materialize-runner.js +13 -13
  117. package/dist/connectors/index.js +13 -13
  118. package/dist/convergence-refresh.js +15 -15
  119. package/dist/conversation-index/backend.js +2 -2
  120. package/dist/corpus-watermark.js +15 -15
  121. package/dist/entity-retrieval.js +13 -13
  122. package/dist/external-wiki-access.js +31 -31
  123. package/dist/extraction-span-bench.d.ts +14 -0
  124. package/dist/extraction-span-bench.js +17 -0
  125. package/dist/extraction-span-bench.js.map +1 -0
  126. package/dist/extraction-span-clip.d.ts +22 -0
  127. package/dist/extraction-span-clip.js +19 -0
  128. package/dist/extraction-span-clip.js.map +1 -0
  129. package/dist/extraction-span-contains.d.ts +12 -0
  130. package/dist/extraction-span-contains.js +17 -0
  131. package/dist/extraction-span-contains.js.map +1 -0
  132. package/dist/extraction-span-join.d.ts +23 -0
  133. package/dist/extraction-span-join.js +19 -0
  134. package/dist/extraction-span-join.js.map +1 -0
  135. package/dist/extraction-span-length.d.ts +11 -0
  136. package/dist/extraction-span-length.js +17 -0
  137. package/dist/extraction-span-length.js.map +1 -0
  138. package/dist/extraction-span-overlap.d.ts +14 -0
  139. package/dist/extraction-span-overlap.js +16 -0
  140. package/dist/extraction-span-overlap.js.map +1 -0
  141. package/dist/extraction-span-shift.d.ts +15 -0
  142. package/dist/extraction-span-shift.js +17 -0
  143. package/dist/extraction-span-shift.js.map +1 -0
  144. package/dist/extraction-span-slice.d.ts +12 -0
  145. package/dist/extraction-span-slice.js +17 -0
  146. package/dist/extraction-span-slice.js.map +1 -0
  147. package/dist/extraction-span-validate.d.ts +20 -0
  148. package/dist/extraction-span-validate.js +20 -0
  149. package/dist/extraction-span-validate.js.map +1 -0
  150. package/dist/extraction-span-wire.d.ts +17 -0
  151. package/dist/extraction-span-wire.js +13 -0
  152. package/dist/extraction-span-wire.js.map +1 -0
  153. package/dist/extraction-span.d.ts +17 -0
  154. package/dist/extraction-span.js +8 -0
  155. package/dist/extraction.js +16 -16
  156. package/dist/{graph-edge-decay-A4SDFMMS.js → graph-edge-decay-DGTBFALQ.js} +4 -4
  157. package/dist/graph-edge-decay-DGTBFALQ.js.map +1 -0
  158. package/dist/graph-snapshot.js +3 -3
  159. package/dist/graph.js +2 -2
  160. package/dist/harmonic-retrieval.js +1 -1
  161. package/dist/index.d.ts +387 -1
  162. package/dist/index.js +203 -122
  163. package/dist/lcm/engine.js +3 -3
  164. package/dist/lcm/index.js +9 -9
  165. package/dist/location/index.js +15 -15
  166. package/dist/maintenance/memory-governance.js +13 -13
  167. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +13 -13
  168. package/dist/maintenance/rebuild-memory-projection.js +14 -14
  169. package/dist/namespaces/migrate.js +14 -14
  170. package/dist/namespaces/search.js +9 -9
  171. package/dist/namespaces/storage.js +13 -13
  172. package/dist/operator-doctor-corpus.js +16 -16
  173. package/dist/operator-toolkit.js +26 -26
  174. package/dist/orchestration/maintenance.js +21 -21
  175. package/dist/orchestrator.js +98 -96
  176. package/dist/qmd.js +4 -4
  177. package/dist/recall-deep-access.d.ts +30 -0
  178. package/dist/recall-deep-access.js +50 -0
  179. package/dist/recall-deep-access.js.map +1 -0
  180. package/dist/recall-deep-budget-left.d.ts +8 -0
  181. package/dist/recall-deep-budget-left.js +15 -0
  182. package/dist/recall-deep-budget-left.js.map +1 -0
  183. package/dist/recall-deep-budget.d.ts +9 -0
  184. package/dist/recall-deep-budget.js +15 -0
  185. package/dist/recall-deep-budget.js.map +1 -0
  186. package/dist/recall-deep-expand.d.ts +12 -0
  187. package/dist/recall-deep-expand.js +17 -0
  188. package/dist/recall-deep-expand.js.map +1 -0
  189. package/dist/recall-deep-policy.d.ts +17 -0
  190. package/dist/recall-deep-policy.js +18 -0
  191. package/dist/recall-deep-policy.js.map +1 -0
  192. package/dist/recall-deep-query.d.ts +15 -0
  193. package/dist/recall-deep-query.js +12 -0
  194. package/dist/recall-deep-query.js.map +1 -0
  195. package/dist/recall-deep-refine.d.ts +12 -0
  196. package/dist/recall-deep-refine.js +17 -0
  197. package/dist/recall-deep-refine.js.map +1 -0
  198. package/dist/recall-deep-render.d.ts +11 -0
  199. package/dist/recall-deep-render.js +28 -0
  200. package/dist/recall-deep-render.js.map +1 -0
  201. package/dist/recall-deep-step-count.d.ts +8 -0
  202. package/dist/recall-deep-step-count.js +15 -0
  203. package/dist/recall-deep-step-count.js.map +1 -0
  204. package/dist/recall-deep-stop-reason.d.ts +17 -0
  205. package/dist/recall-deep-stop-reason.js +23 -0
  206. package/dist/recall-deep-stop-reason.js.map +1 -0
  207. package/dist/recall-deep-stop.d.ts +17 -0
  208. package/dist/recall-deep-stop.js +23 -0
  209. package/dist/recall-deep-stop.js.map +1 -0
  210. package/dist/recall-deep-working-set.d.ts +9 -0
  211. package/dist/recall-deep-working-set.js +14 -0
  212. package/dist/recall-deep-working-set.js.map +1 -0
  213. package/dist/recall-deep.js +6 -73
  214. package/dist/recall-deep.js.map +1 -1
  215. package/dist/recall-navigate-access.d.ts +39 -0
  216. package/dist/recall-navigate-access.js +31 -0
  217. package/dist/recall-navigate-access.js.map +1 -0
  218. package/dist/recall-navigate-action.d.ts +17 -0
  219. package/dist/recall-navigate-action.js +14 -0
  220. package/dist/recall-navigate-action.js.map +1 -0
  221. package/dist/recall-navigate-budget.d.ts +16 -0
  222. package/dist/recall-navigate-budget.js +18 -0
  223. package/dist/recall-navigate-budget.js.map +1 -0
  224. package/dist/recall-navigate-cost.d.ts +8 -0
  225. package/dist/recall-navigate-cost.js +15 -0
  226. package/dist/recall-navigate-cost.js.map +1 -0
  227. package/dist/recall-navigate-depth.d.ts +8 -0
  228. package/dist/recall-navigate-depth.js +15 -0
  229. package/dist/recall-navigate-depth.js.map +1 -0
  230. package/dist/recall-navigate-link.d.ts +17 -0
  231. package/dist/recall-navigate-link.js +20 -0
  232. package/dist/recall-navigate-link.js.map +1 -0
  233. package/dist/recall-navigate-max-children.d.ts +8 -0
  234. package/dist/recall-navigate-max-children.js +15 -0
  235. package/dist/recall-navigate-max-children.js.map +1 -0
  236. package/dist/recall-navigate-node.d.ts +15 -0
  237. package/dist/recall-navigate-node.js +13 -0
  238. package/dist/recall-navigate-node.js.map +1 -0
  239. package/dist/recall-navigate-path.d.ts +9 -0
  240. package/dist/recall-navigate-path.js +14 -0
  241. package/dist/recall-navigate-path.js.map +1 -0
  242. package/dist/recall-navigate-render.d.ts +15 -0
  243. package/dist/recall-navigate-render.js +23 -0
  244. package/dist/recall-navigate-render.js.map +1 -0
  245. package/dist/recall-navigate.js +6 -42
  246. package/dist/recall-navigate.js.map +1 -1
  247. package/dist/recall-state-view-current.d.ts +18 -0
  248. package/dist/recall-state-view-current.js +14 -0
  249. package/dist/recall-state-view-current.js.map +1 -0
  250. package/dist/recall-state-view-inject.d.ts +15 -0
  251. package/dist/recall-state-view-inject.js +16 -0
  252. package/dist/recall-state-view-inject.js.map +1 -0
  253. package/dist/recall-state-view-kind.d.ts +17 -0
  254. package/dist/recall-state-view-kind.js +14 -0
  255. package/dist/recall-state-view-kind.js.map +1 -0
  256. package/dist/recall-state-view-label.d.ts +17 -0
  257. package/dist/recall-state-view-label.js +16 -0
  258. package/dist/recall-state-view-label.js.map +1 -0
  259. package/dist/recall-state-view-render.d.ts +18 -0
  260. package/dist/recall-state-view-render.js +11 -0
  261. package/dist/recall-state-view-render.js.map +1 -0
  262. package/dist/recall-state-view-successor.d.ts +15 -0
  263. package/dist/recall-state-view-successor.js +13 -0
  264. package/dist/recall-state-view-successor.js.map +1 -0
  265. package/dist/recall-state-view-superseded.d.ts +15 -0
  266. package/dist/recall-state-view-superseded.js +14 -0
  267. package/dist/recall-state-view-superseded.js.map +1 -0
  268. package/dist/recall-state-view-transition.d.ts +18 -0
  269. package/dist/recall-state-view-transition.js +14 -0
  270. package/dist/recall-state-view-transition.js.map +1 -0
  271. package/dist/recall-state-view-widen.d.ts +13 -0
  272. package/dist/recall-state-view-widen.js +11 -0
  273. package/dist/recall-state-view-widen.js.map +1 -0
  274. package/dist/recall-state-view-wire.d.ts +3 -4
  275. package/dist/recall-state-view-wire.js +2 -1
  276. package/dist/resume-bundles.js +4 -4
  277. package/dist/search/factory.js +8 -8
  278. package/dist/search/index.js +14 -14
  279. package/dist/semantic-consolidation.js +14 -14
  280. package/dist/semantic-rule-promotion.js +13 -13
  281. package/dist/semantic-rule-verifier.js +13 -13
  282. package/dist/source-agent-qualifier.js +15 -15
  283. package/dist/storage.js +12 -12
  284. package/dist/support-passport/index.js +33 -33
  285. package/dist/transfer/backup.js +2 -2
  286. package/dist/transfer/capsule-export.js +2 -2
  287. package/dist/transfer/capsule-import.js +2 -2
  288. package/dist/transfer/export-okf.js +14 -14
  289. package/dist/verified-recall.js +13 -13
  290. package/dist/who-knows-cli.js +14 -14
  291. package/dist/who-knows.js +13 -13
  292. package/package.json +2 -2
  293. package/src/activity/dwell.test.ts +28 -0
  294. package/src/activity/dwell.ts +22 -0
  295. package/src/activity/from-ms.test.ts +25 -0
  296. package/src/activity/from-ms.ts +13 -0
  297. package/src/activity/heading-level.test.ts +23 -0
  298. package/src/activity/heading-level.ts +21 -0
  299. package/src/activity/heading-title.test.ts +21 -0
  300. package/src/activity/heading-title.ts +26 -0
  301. package/src/activity/index.ts +9 -0
  302. package/src/activity/journal-heading.test.ts +22 -0
  303. package/src/activity/journal-heading.ts +18 -0
  304. package/src/activity/journal-mode.test.ts +32 -0
  305. package/src/activity/journal-mode.ts +20 -0
  306. package/src/activity/journal-path.test.ts +25 -0
  307. package/src/activity/journal-path.ts +27 -0
  308. package/src/activity/journal-section.test.ts +37 -0
  309. package/src/activity/journal-section.ts +75 -0
  310. package/src/activity/journal-source.test.ts +37 -0
  311. package/src/activity/journal-source.ts +23 -0
  312. package/src/activity/journal-vault-read.test.ts +153 -0
  313. package/src/activity/journal-vault-read.ts +214 -0
  314. package/src/activity/journal.ts +1 -1
  315. package/src/activity/privacy-enabled.test.ts +30 -0
  316. package/src/activity/privacy-enabled.ts +33 -0
  317. package/src/activity/privacy-keys.test.ts +22 -0
  318. package/src/activity/privacy-keys.ts +12 -0
  319. package/src/activity/privacy-redact.test.ts +25 -0
  320. package/src/activity/privacy-redact.ts +23 -0
  321. package/src/activity/privacy-window.test.ts +49 -0
  322. package/src/activity/privacy-window.ts +32 -0
  323. package/src/activity/retain-days.test.ts +20 -0
  324. package/src/activity/retain-days.ts +14 -0
  325. package/src/activity/timeline/analysis-batch.test.ts +56 -0
  326. package/src/activity/timeline/analysis-batch.ts +45 -0
  327. package/src/activity/timeline/analysis-claim.test.ts +33 -0
  328. package/src/activity/timeline/analysis-claim.ts +16 -0
  329. package/src/activity/timeline/analysis-dup.test.ts +28 -0
  330. package/src/activity/timeline/analysis-dup.ts +10 -0
  331. package/src/activity/timeline/analysis-empty.test.ts +21 -0
  332. package/src/activity/timeline/analysis-empty.ts +14 -0
  333. package/src/activity/timeline/analysis-evidence.test.ts +32 -0
  334. package/src/activity/timeline/analysis-evidence.ts +20 -0
  335. package/src/activity/timeline/analysis-max.test.ts +26 -0
  336. package/src/activity/timeline/analysis-max.ts +14 -0
  337. package/src/activity/timeline/analysis-order.test.ts +28 -0
  338. package/src/activity/timeline/analysis-order.ts +10 -0
  339. package/src/activity/timeline/analysis-prompt.test.ts +50 -0
  340. package/src/activity/timeline/analysis-prompt.ts +24 -0
  341. package/src/activity/timeline/analysis.test.ts +413 -0
  342. package/src/activity/timeline/analysis.ts +362 -0
  343. package/src/activity/timeline/index.ts +13 -0
  344. package/src/activity/timeline/journal-recap-persist.test.ts +148 -0
  345. package/src/activity/timeline/journal-recap-persist.ts +37 -0
  346. package/src/activity/timeline/recap-card-id.test.ts +21 -0
  347. package/src/activity/timeline/recap-card-id.ts +16 -0
  348. package/src/activity/timeline/recap-date.test.ts +21 -0
  349. package/src/activity/timeline/recap-date.ts +19 -0
  350. package/src/activity/timeline/recap-empty.test.ts +21 -0
  351. package/src/activity/timeline/recap-empty.ts +14 -0
  352. package/src/activity/timeline/recap-export.test.ts +78 -0
  353. package/src/activity/timeline/recap-export.ts +41 -0
  354. package/src/activity/timeline/recap-id.test.ts +28 -0
  355. package/src/activity/timeline/recap-id.ts +10 -0
  356. package/src/activity/timeline/recap-markdown.test.ts +56 -0
  357. package/src/activity/timeline/recap-markdown.ts +26 -0
  358. package/src/activity/timeline/recap-sort-key.test.ts +18 -0
  359. package/src/activity/timeline/recap-sort-key.ts +14 -0
  360. package/src/activity/timeline/recap-tz.test.ts +35 -0
  361. package/src/activity/timeline/recap-tz.ts +19 -0
  362. package/src/activity/timeline/week-count.test.ts +19 -0
  363. package/src/activity/timeline/week-count.ts +13 -0
  364. package/src/activity/timeline/week-days.test.ts +26 -0
  365. package/src/activity/timeline/week-days.ts +23 -0
  366. package/src/activity/timeline/week-export.test.ts +59 -0
  367. package/src/activity/timeline/week-export.ts +44 -0
  368. package/src/activity/timeline/week-iso.test.ts +29 -0
  369. package/src/activity/timeline/week-iso.ts +20 -0
  370. package/src/activity/timeline/week-monday.test.ts +17 -0
  371. package/src/activity/timeline/week-monday.ts +12 -0
  372. package/src/activity/timeline/week-range.test.ts +23 -0
  373. package/src/activity/timeline/week-range.ts +26 -0
  374. package/src/activity/timeline/week-snapshot.test.ts +66 -0
  375. package/src/activity/timeline/week-snapshot.ts +40 -0
  376. package/src/activity/timeline/week-sunday.test.ts +17 -0
  377. package/src/activity/timeline/week-sunday.ts +12 -0
  378. package/src/activity/timeline/weekly-persist.test.ts +121 -0
  379. package/src/activity/timeline/weekly-persist.ts +103 -0
  380. package/src/activity/to-ms.test.ts +25 -0
  381. package/src/activity/to-ms.ts +13 -0
  382. package/src/activity/vault-begin.test.ts +20 -0
  383. package/src/activity/vault-begin.ts +13 -0
  384. package/src/activity/vault-end.test.ts +20 -0
  385. package/src/activity/vault-end.ts +13 -0
  386. package/src/activity/vault-frontmatter.test.ts +28 -0
  387. package/src/activity/vault-frontmatter.ts +39 -0
  388. package/src/activity/vault-join.test.ts +21 -0
  389. package/src/activity/vault-join.ts +31 -0
  390. package/src/activity/vault-marker.test.ts +23 -0
  391. package/src/activity/vault-marker.ts +27 -0
  392. package/src/activity/vault-name-trim.test.ts +22 -0
  393. package/src/activity/vault-name-trim.ts +16 -0
  394. package/src/activity/vault-path.test.ts +38 -0
  395. package/src/activity/vault-path.ts +59 -0
  396. package/src/activity/vault-publish.test.ts +102 -0
  397. package/src/activity/vault-publish.ts +92 -13
  398. package/src/activity/vault-region-pair.test.ts +26 -0
  399. package/src/activity/vault-region-pair.ts +24 -0
  400. package/src/activity/vault-region.test.ts +24 -0
  401. package/src/activity/vault-region.ts +24 -0
  402. package/src/activity/vault-suffix.test.ts +20 -0
  403. package/src/activity/vault-suffix.ts +12 -0
  404. package/src/activity/vault-wikilink.test.ts +27 -0
  405. package/src/activity/vault-wikilink.ts +28 -0
  406. package/src/activity/window-order.test.ts +27 -0
  407. package/src/activity/window-order.ts +24 -0
  408. package/src/dedup/merge-count.test.ts +19 -0
  409. package/src/dedup/merge-count.ts +13 -0
  410. package/src/dedup/merge-dup.test.ts +21 -0
  411. package/src/dedup/merge-dup.ts +16 -0
  412. package/src/dedup/merge-empty.test.ts +22 -0
  413. package/src/dedup/merge-empty.ts +18 -0
  414. package/src/dedup/merge-id.test.ts +18 -0
  415. package/src/dedup/merge-id.ts +14 -0
  416. package/src/dedup/merge-min-score.test.ts +37 -0
  417. package/src/dedup/merge-min-score.ts +22 -0
  418. package/src/dedup/merge-on-write-wire.test.ts +101 -0
  419. package/src/dedup/merge-on-write-wire.ts +34 -0
  420. package/src/dedup/merge-pair.test.ts +19 -0
  421. package/src/dedup/merge-pair.ts +13 -0
  422. package/src/dedup/merge-score.test.ts +20 -0
  423. package/src/dedup/merge-score.ts +14 -0
  424. package/src/dedup/merge-self.test.ts +19 -0
  425. package/src/dedup/merge-self.ts +17 -0
  426. package/src/dedup/merge-threshold.test.ts +46 -0
  427. package/src/dedup/merge-threshold.ts +26 -0
  428. package/src/dedup/merge-tie.test.ts +22 -0
  429. package/src/dedup/merge-tie.ts +13 -0
  430. package/src/extraction-span-bench.test.ts +27 -0
  431. package/src/extraction-span-bench.ts +28 -0
  432. package/src/extraction-span-clip.test.ts +71 -0
  433. package/src/extraction-span-clip.ts +26 -0
  434. package/src/extraction-span-contains.test.ts +30 -0
  435. package/src/extraction-span-contains.ts +20 -0
  436. package/src/extraction-span-join.test.ts +50 -0
  437. package/src/extraction-span-join.ts +30 -0
  438. package/src/extraction-span-length.test.ts +25 -0
  439. package/src/extraction-span-length.ts +16 -0
  440. package/src/extraction-span-overlap.test.ts +34 -0
  441. package/src/extraction-span-overlap.ts +23 -0
  442. package/src/extraction-span-shift.test.ts +25 -0
  443. package/src/extraction-span-shift.ts +20 -0
  444. package/src/extraction-span-slice.test.ts +32 -0
  445. package/src/extraction-span-slice.ts +20 -0
  446. package/src/extraction-span-validate.test.ts +42 -0
  447. package/src/extraction-span-validate.ts +27 -0
  448. package/src/extraction-span-wire.test.ts +20 -0
  449. package/src/extraction-span-wire.ts +17 -0
  450. package/src/extraction-span.test.ts +59 -0
  451. package/src/extraction-span.ts +39 -0
  452. package/src/index.ts +1 -0
  453. package/src/recall-deep-access.test.ts +58 -0
  454. package/src/recall-deep-access.ts +71 -0
  455. package/src/recall-deep-budget-left.test.ts +20 -0
  456. package/src/recall-deep-budget-left.ts +13 -0
  457. package/src/recall-deep-budget.test.ts +19 -0
  458. package/src/recall-deep-budget.ts +14 -0
  459. package/src/recall-deep-expand.test.ts +23 -0
  460. package/src/recall-deep-expand.ts +19 -0
  461. package/src/recall-deep-policy.test.ts +32 -0
  462. package/src/recall-deep-policy.ts +22 -0
  463. package/src/recall-deep-query.test.ts +23 -0
  464. package/src/recall-deep-query.ts +15 -0
  465. package/src/recall-deep-refine.test.ts +23 -0
  466. package/src/recall-deep-refine.ts +19 -0
  467. package/src/recall-deep-render.test.ts +68 -0
  468. package/src/recall-deep-render.ts +32 -0
  469. package/src/recall-deep-step-count.test.ts +20 -0
  470. package/src/recall-deep-step-count.ts +13 -0
  471. package/src/recall-deep-stop-reason.test.ts +46 -0
  472. package/src/recall-deep-stop-reason.ts +27 -0
  473. package/src/recall-deep-stop.test.ts +34 -0
  474. package/src/recall-deep-stop.ts +31 -0
  475. package/src/recall-deep-working-set.test.ts +21 -0
  476. package/src/recall-deep-working-set.ts +14 -0
  477. package/src/recall-navigate-access.test.ts +73 -0
  478. package/src/recall-navigate-access.ts +48 -0
  479. package/src/recall-navigate-action.test.ts +32 -0
  480. package/src/recall-navigate-action.ts +20 -0
  481. package/src/recall-navigate-budget.test.ts +23 -0
  482. package/src/recall-navigate-budget.ts +22 -0
  483. package/src/recall-navigate-cost.test.ts +20 -0
  484. package/src/recall-navigate-cost.ts +13 -0
  485. package/src/recall-navigate-depth.test.ts +20 -0
  486. package/src/recall-navigate-depth.ts +13 -0
  487. package/src/recall-navigate-link.test.ts +19 -0
  488. package/src/recall-navigate-link.ts +26 -0
  489. package/src/recall-navigate-max-children.test.ts +20 -0
  490. package/src/recall-navigate-max-children.ts +13 -0
  491. package/src/recall-navigate-node.test.ts +21 -0
  492. package/src/recall-navigate-node.ts +16 -0
  493. package/src/recall-navigate-path.test.ts +21 -0
  494. package/src/recall-navigate-path.ts +14 -0
  495. package/src/recall-navigate-render.test.ts +63 -0
  496. package/src/recall-navigate-render.ts +31 -0
  497. package/src/recall-state-view-current.test.ts +23 -0
  498. package/src/recall-state-view-current.ts +24 -0
  499. package/src/recall-state-view-inject.test.ts +32 -0
  500. package/src/recall-state-view-inject.ts +14 -0
  501. package/src/recall-state-view-kind.test.ts +19 -0
  502. package/src/recall-state-view-kind.ts +20 -0
  503. package/src/recall-state-view-label.test.ts +36 -0
  504. package/src/recall-state-view-label.ts +22 -0
  505. package/src/recall-state-view-render.test.ts +94 -0
  506. package/src/recall-state-view-render.ts +24 -0
  507. package/src/recall-state-view-successor.test.ts +21 -0
  508. package/src/recall-state-view-successor.ts +16 -0
  509. package/src/recall-state-view-superseded.test.ts +20 -0
  510. package/src/recall-state-view-superseded.ts +17 -0
  511. package/src/recall-state-view-transition.test.ts +30 -0
  512. package/src/recall-state-view-transition.ts +24 -0
  513. package/src/recall-state-view-widen.test.ts +66 -0
  514. package/src/recall-state-view-widen.ts +49 -0
  515. package/src/recall-state-view-wire.ts +6 -16
  516. package/src/shared-context/authority.test.ts +35 -0
  517. package/src/shared-context/authority.ts +25 -0
  518. package/src/shared-context/claim-key.test.ts +21 -0
  519. package/src/shared-context/claim-key.ts +16 -0
  520. package/src/shared-context/contradictions.test.ts +39 -0
  521. package/src/shared-context/contradictions.ts +32 -0
  522. package/src/shared-context/envelope-actor.test.ts +21 -0
  523. package/src/shared-context/envelope-actor.ts +16 -0
  524. package/src/shared-context/envelope-at.test.ts +19 -0
  525. package/src/shared-context/envelope-at.ts +17 -0
  526. package/src/shared-context/envelope-id.test.ts +21 -0
  527. package/src/shared-context/envelope-id.ts +16 -0
  528. package/src/shared-context/envelope-required.test.ts +24 -0
  529. package/src/shared-context/envelope-required.ts +15 -0
  530. package/src/shared-context/envelope-ttl.test.ts +29 -0
  531. package/src/shared-context/envelope-ttl.ts +35 -0
  532. package/src/shared-context/provenance.test.ts +35 -0
  533. package/src/shared-context/provenance.ts +68 -0
  534. package/src/shared-context/staleness.test.ts +36 -0
  535. package/src/shared-context/staleness.ts +38 -0
  536. package/src/shared-context/trajectories.test.ts +77 -0
  537. package/src/shared-context/trajectories.ts +49 -0
  538. package/dist/chunk-MUEGIKKY.js.map +0 -1
  539. package/dist/chunk-TCF4MU7L.js.map +0 -1
  540. package/dist/chunk-WFCWP4XU.js +0 -17
  541. package/dist/chunk-WFCWP4XU.js.map +0 -1
  542. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  543. /package/dist/{chunk-XKNHFDTT.js.map → chunk-3A2Z4OOS.js.map} +0 -0
  544. /package/dist/{chunk-CHGG74CY.js.map → chunk-3HHTN5X7.js.map} +0 -0
  545. /package/dist/{chunk-XOHYGIWF.js.map → chunk-3Z4SFJA5.js.map} +0 -0
  546. /package/dist/{chunk-RDGKVH6T.js.map → chunk-4NKIGSJM.js.map} +0 -0
  547. /package/dist/{chunk-GPFZFCRN.js.map → chunk-4PTTMTTQ.js.map} +0 -0
  548. /package/dist/{chunk-VG4SSPAW.js.map → chunk-4XESAT4V.js.map} +0 -0
  549. /package/dist/{chunk-S4CDUXQY.js.map → chunk-5KGLNBUB.js.map} +0 -0
  550. /package/dist/{chunk-2X6BGKQL.js.map → chunk-5XRZANCW.js.map} +0 -0
  551. /package/dist/{chunk-TCQRIIIN.js.map → chunk-6DODJYBY.js.map} +0 -0
  552. /package/dist/{chunk-KWIGDKM2.js.map → chunk-73CAEYI3.js.map} +0 -0
  553. /package/dist/{chunk-J2R275FN.js.map → chunk-7DXW5V2W.js.map} +0 -0
  554. /package/dist/{chunk-LYUUA6QB.js.map → chunk-7JJHIVMG.js.map} +0 -0
  555. /package/dist/{chunk-2FA7U5XY.js.map → chunk-7NNMRMLZ.js.map} +0 -0
  556. /package/dist/{chunk-227UFSEQ.js.map → chunk-7RZJWZ46.js.map} +0 -0
  557. /package/dist/{chunk-EWKVYCFP.js.map → chunk-B2OKCCXX.js.map} +0 -0
  558. /package/dist/{chunk-NSTNGMSS.js.map → chunk-B7AGLMGP.js.map} +0 -0
  559. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  560. /package/dist/{chunk-CEH2AFXG.js.map → chunk-C24UZSFW.js.map} +0 -0
  561. /package/dist/{chunk-LTBKKSCX.js.map → chunk-C2JCVZWX.js.map} +0 -0
  562. /package/dist/{chunk-65JOGLIS.js.map → chunk-D4R3OUZA.js.map} +0 -0
  563. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  564. /package/dist/{chunk-IGZCAHFL.js.map → chunk-EGGNFLAT.js.map} +0 -0
  565. /package/dist/{chunk-H2IRSXZN.js.map → chunk-ESDOAP7W.js.map} +0 -0
  566. /package/dist/{chunk-D3UXGE5D.js.map → chunk-EZWIEFYD.js.map} +0 -0
  567. /package/dist/{chunk-ZPS724LT.js.map → chunk-F5H5QINZ.js.map} +0 -0
  568. /package/dist/{chunk-S4Z2OBTX.js.map → chunk-FPSOXMLK.js.map} +0 -0
  569. /package/dist/{chunk-CX4OLNIH.js.map → chunk-G5UKF2GJ.js.map} +0 -0
  570. /package/dist/{chunk-OJUEUQDJ.js.map → chunk-GGY2747C.js.map} +0 -0
  571. /package/dist/{chunk-TNGH4ZYI.js.map → chunk-GNWLTCKC.js.map} +0 -0
  572. /package/dist/{chunk-YWPLOJ7T.js.map → chunk-GZPGI45F.js.map} +0 -0
  573. /package/dist/{chunk-NFP4KPTA.js.map → chunk-IDEPEHUG.js.map} +0 -0
  574. /package/dist/{chunk-LJABGXC4.js.map → chunk-IN5EUWCA.js.map} +0 -0
  575. /package/dist/{chunk-U6NGKS32.js.map → chunk-JJAOVJ3E.js.map} +0 -0
  576. /package/dist/{chunk-RPIS5FQ7.js.map → chunk-JWCS3VYQ.js.map} +0 -0
  577. /package/dist/{chunk-U65VJSCJ.js.map → chunk-KE23ZIEW.js.map} +0 -0
  578. /package/dist/{chunk-NACLNNFQ.js.map → chunk-KSZKQTZS.js.map} +0 -0
  579. /package/dist/{chunk-KJXCD7VR.js.map → chunk-KWJNEWTM.js.map} +0 -0
  580. /package/dist/{chunk-UW4RYPWC.js.map → chunk-MLTGALDY.js.map} +0 -0
  581. /package/dist/{chunk-YMOPYG3K.js.map → chunk-OLT3YTDH.js.map} +0 -0
  582. /package/dist/{chunk-2PSX5BJU.js.map → chunk-OPQKVVYG.js.map} +0 -0
  583. /package/dist/{chunk-D6Q5EPOV.js.map → chunk-PGD3DXZP.js.map} +0 -0
  584. /package/dist/{chunk-GBEOUCBB.js.map → chunk-Q6LC3ZD5.js.map} +0 -0
  585. /package/dist/{chunk-QBNE55VA.js.map → chunk-QELYSDAU.js.map} +0 -0
  586. /package/dist/{chunk-INUBU6I5.js.map → chunk-S6PRJGQL.js.map} +0 -0
  587. /package/dist/{chunk-B4AOWRYF.js.map → chunk-TCJO7LOG.js.map} +0 -0
  588. /package/dist/{chunk-SZMYGDSM.js.map → chunk-TO5U47YK.js.map} +0 -0
  589. /package/dist/{chunk-GAJK3DQ5.js.map → chunk-U3L6GRUX.js.map} +0 -0
  590. /package/dist/{chunk-4XLTUR6R.js.map → chunk-U4RGSLI4.js.map} +0 -0
  591. /package/dist/{chunk-M5ERTI4I.js.map → chunk-UH53XVQ3.js.map} +0 -0
  592. /package/dist/{chunk-NUF7VSEX.js.map → chunk-UXERIBOJ.js.map} +0 -0
  593. /package/dist/{chunk-FFUMTH27.js.map → chunk-VA2CSIPC.js.map} +0 -0
  594. /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
  595. /package/dist/{chunk-AF5H7TKI.js.map → chunk-VPJNGE2R.js.map} +0 -0
  596. /package/dist/{chunk-OMX6TKXH.js.map → chunk-VTJZSLDR.js.map} +0 -0
  597. /package/dist/{chunk-TBCDF556.js.map → chunk-WHFFAS4A.js.map} +0 -0
  598. /package/dist/{chunk-7UOXCCCI.js.map → chunk-WVNETF4Q.js.map} +0 -0
  599. /package/dist/{chunk-EH65UV2Z.js.map → chunk-XSTMPZMY.js.map} +0 -0
  600. /package/dist/{chunk-PD66CVSD.js.map → chunk-Z3SQEEB6.js.map} +0 -0
  601. /package/dist/{chunk-JKTV3JNX.js.map → chunk-ZBYOXIIF.js.map} +0 -0
  602. /package/dist/{chunk-2ITZ74A6.js.map → chunk-ZC5IHL5X.js.map} +0 -0
  603. /package/dist/{chunk-KI64TTBL.js.map → chunk-ZDMTCO7N.js.map} +0 -0
  604. /package/dist/{chunk-C5IUOBCN.js.map → chunk-ZET6A3OF.js.map} +0 -0
  605. /package/dist/{chunk-ZZZWEDF4.js.map → chunk-ZVAKRZZD.js.map} +0 -0
  606. /package/dist/{graph-edge-decay-A4SDFMMS.js.map → extraction-span.js.map} +0 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Trim a managed-region name (issue #1985).
3
+ *
4
+ * Empty after trim throws. Newline throws. Else the trimmed string.
5
+ */
6
+
7
+ export function trimRegionName(value: string): string {
8
+ const name = value.trim();
9
+ if (name.length === 0) {
10
+ throw new RangeError("Region name must be non-empty.");
11
+ }
12
+ if (name.includes("\n") || name.includes("\r")) {
13
+ throw new RangeError("Region name must not contain a newline.");
14
+ }
15
+ return name;
16
+ }
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { resolveVaultNotePath } from "./vault-path.js";
5
+
6
+ test("resolveVaultNotePath expands date tokens", () => {
7
+ assert.equal(resolveVaultNotePath("{yyyy}-{MM}-{dd}.md", "2026-08-18"), "2026-08-18.md");
8
+ assert.equal(
9
+ resolveVaultNotePath("Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md", "2026-08-18"),
10
+ "Daily Notes/2026/08/2026-08-18.md",
11
+ );
12
+ assert.equal(resolveVaultNotePath("{yy}/{M}/{d}.md", "2026-08-08"), "26/8/8.md");
13
+ assert.equal(
14
+ resolveVaultNotePath("{yyyy}-{MM}-{dd}.md", "2026-08-18", { timezone: "America/Chicago" }),
15
+ "2026-08-18.md",
16
+ );
17
+ });
18
+
19
+ test("resolveVaultNotePath expands ISO week {ww} as zero-padded", () => {
20
+ assert.equal(resolveVaultNotePath("W{ww}.md", "2021-01-01"), "W53.md");
21
+ assert.equal(resolveVaultNotePath("W{ww}.md", "2024-01-01"), "W01.md");
22
+ assert.equal(
23
+ resolveVaultNotePath("{yyyy}/W{ww}/{yyyy}-{MM}-{dd}.md", "2021-01-01"),
24
+ "2021/W53/2021-01-01.md",
25
+ );
26
+ });
27
+
28
+ test("resolveVaultNotePath rejects traversal and absolute templates", () => {
29
+ assert.throws(() => resolveVaultNotePath("../outside/{yyyy}.md", "2026-08-18"), /\.\.|absolute|traversal/i);
30
+ assert.throws(() => resolveVaultNotePath("{yyyy}/../../secret.md", "2026-08-18"), /\.\.|absolute|traversal/i);
31
+ assert.throws(() => resolveVaultNotePath("/abs/{yyyy}.md", "2026-08-18"), /\.\.|absolute|traversal/i);
32
+ assert.throws(() => resolveVaultNotePath("C:/vault/{yyyy}.md", "2026-08-18"), /\.\.|absolute|traversal/i);
33
+ });
34
+
35
+ test("resolveVaultNotePath requires a real YYYY-MM-DD date", () => {
36
+ assert.throws(() => resolveVaultNotePath("{yyyy}.md", "2026-02-30"), /YYYY-MM-DD/);
37
+ assert.throws(() => resolveVaultNotePath("{yyyy}.md", "20260818"), /YYYY-MM-DD/);
38
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Vault note path templates (issue #1985).
3
+ *
4
+ * Expands date tokens on a YYYY-MM-DD day. Returns a vault-relative path.
5
+ * Rejects `..` segments and absolute templates.
6
+ */
7
+ import path from "node:path";
8
+
9
+ import { assertValidTimezone, isValidActivityDate } from "./digest.js";
10
+
11
+ const TOKEN_RE = /\{yyyy\}|\{yy\}|\{MM\}|\{dd\}|\{M\}|\{d\}|\{ww\}/g;
12
+
13
+ export function resolveVaultNotePath(
14
+ template: string,
15
+ date: string,
16
+ options?: { timezone?: string },
17
+ ): string {
18
+ if (!isValidActivityDate(date)) {
19
+ throw new RangeError(`Invalid vault date "${date}"; expected YYYY-MM-DD.`);
20
+ }
21
+ if (options?.timezone !== undefined) {
22
+ assertValidTimezone(options.timezone);
23
+ }
24
+ if (typeof template !== "string" || template.length === 0) {
25
+ throw new RangeError("Vault note path template must be a non-empty relative path.");
26
+ }
27
+ if (path.posix.isAbsolute(template) || path.win32.isAbsolute(template)) {
28
+ throw new RangeError("Vault note path template must be relative; absolute templates are rejected.");
29
+ }
30
+
31
+ const [yyyy, mm, dd] = date.split("-");
32
+ const tokens: Record<string, string> = {
33
+ "{yyyy}": yyyy,
34
+ "{yy}": yyyy.slice(2),
35
+ "{MM}": mm,
36
+ "{dd}": dd,
37
+ "{M}": String(Number(mm)),
38
+ "{d}": String(Number(dd)),
39
+ "{ww}": isoWeekNumber(date),
40
+ };
41
+ const expanded = template.replace(TOKEN_RE, (token) => tokens[token] ?? token);
42
+ if (expanded.split(/[\\/]/).some((segment) => segment === "..")) {
43
+ throw new RangeError("Vault note path template must not contain `..` segments.");
44
+ }
45
+ if (path.posix.isAbsolute(expanded) || path.win32.isAbsolute(expanded)) {
46
+ throw new RangeError("Vault note path template must be relative; absolute templates are rejected.");
47
+ }
48
+ return expanded;
49
+ }
50
+
51
+ function isoWeekNumber(date: string): string {
52
+ const [year, month, day] = date.split("-").map(Number);
53
+ const utc = new Date(Date.UTC(year, month - 1, day));
54
+ const dayNum = utc.getUTCDay() || 7;
55
+ utc.setUTCDate(utc.getUTCDate() + 4 - dayNum);
56
+ const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
57
+ const weekNo = Math.ceil(((utc.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
58
+ return String(weekNo).padStart(2, "0");
59
+ }
@@ -164,3 +164,105 @@ test("publishVaultRegion never creates a missing file", () => {
164
164
  assert.equal(existsSync(path.join(vaultPath, relativeFile)), false);
165
165
  });
166
166
  });
167
+
168
+ test("applyManagedRegion heading replaces only the named section", () => {
169
+ const fileText = [
170
+ "---",
171
+ "title: daily",
172
+ "---",
173
+ "",
174
+ "scratchpad",
175
+ "## Timeline",
176
+ "old cards",
177
+ "## Notes",
178
+ "keep me",
179
+ "",
180
+ ].join("\n");
181
+ const result = applyManagedRegion(fileText, {
182
+ strategy: "heading",
183
+ name: "Timeline",
184
+ content: "new cards",
185
+ });
186
+ assert.equal(result.ok, true);
187
+ if (!result.ok) return;
188
+ assert.equal(
189
+ result.text,
190
+ [
191
+ "---",
192
+ "title: daily",
193
+ "---",
194
+ "",
195
+ "scratchpad",
196
+ "## Timeline",
197
+ "new cards",
198
+ "## Notes",
199
+ "keep me",
200
+ "",
201
+ ].join("\n"),
202
+ );
203
+
204
+ const crlf = fileText.replace(/\n/g, "\r\n");
205
+ const crlfResult = applyManagedRegion(crlf, {
206
+ strategy: "heading",
207
+ name: "Timeline",
208
+ content: "new cards\nline two",
209
+ });
210
+ assert.equal(crlfResult.ok, true);
211
+ if (!crlfResult.ok) return;
212
+ assert.equal(
213
+ crlfResult.text,
214
+ [
215
+ "---",
216
+ "title: daily",
217
+ "---",
218
+ "",
219
+ "scratchpad",
220
+ "## Timeline",
221
+ "new cards",
222
+ "line two",
223
+ "## Notes",
224
+ "keep me",
225
+ "",
226
+ ].join("\r\n"),
227
+ );
228
+
229
+ const prefix = "---\nkeep: me\n---\n\nscratchpad\n## Timeline";
230
+ const suffix = "\n## Notes\nkeep me\n";
231
+ const outside = `${prefix}\nold\n${suffix}`;
232
+ const headingAt = outside.indexOf("## Timeline");
233
+ const notesAt = outside.indexOf("\n## Notes");
234
+ const headingLineEnd = headingAt + "## Timeline".length;
235
+ const before = Buffer.from(outside.slice(0, headingLineEnd), "utf8");
236
+ const after = Buffer.from(outside.slice(notesAt), "utf8");
237
+ const byteResult = applyManagedRegion(outside, {
238
+ strategy: "heading",
239
+ name: "Timeline",
240
+ content: "replacement",
241
+ });
242
+ assert.equal(byteResult.ok, true);
243
+ if (!byteResult.ok) return;
244
+ const nextHeading = byteResult.text.indexOf("## Timeline");
245
+ const nextNotes = byteResult.text.indexOf("\n## Notes");
246
+ assert.deepEqual(Buffer.from(byteResult.text.slice(0, nextHeading + "## Timeline".length), "utf8"), before);
247
+ assert.deepEqual(Buffer.from(byteResult.text.slice(nextNotes), "utf8"), after);
248
+ });
249
+
250
+ test("applyManagedRegion heading is a no-op when the heading is missing", () => {
251
+ const fileText = "# Daily\n\nno timeline here\n";
252
+ const result = applyManagedRegion(fileText, {
253
+ strategy: "heading",
254
+ name: "Timeline",
255
+ content: "new cards",
256
+ });
257
+ assert.deepEqual(result, { ok: false, reason: "no_heading", text: fileText });
258
+ });
259
+
260
+ test("applyManagedRegion heading refuses duplicate headings and lists line numbers", () => {
261
+ const fileText = ["# Daily", "", "## Timeline", "first", "", "## Timeline", "second", ""].join("\n");
262
+ const result = applyManagedRegion(fileText, {
263
+ strategy: "heading",
264
+ name: "Timeline",
265
+ content: "new cards",
266
+ });
267
+ assert.deepEqual(result, { ok: false, reason: "duplicate_heading", lines: [3, 6], text: fileText });
268
+ });
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Managed-region vault publisher (issue #1985 first slice).
2
+ * Managed-region vault publisher (issue #1985).
3
3
  *
4
- * Replaces only the bytes between `<!-- remnic:<name>:start -->` and
5
- * `<!-- remnic:<name>:end -->`. Missing markers leave the file unchanged.
6
- * `activity.timeline.vault.enabled` stays default-false until a later parse
7
- * slice; this module does not read config.
4
+ * Markers replace bytes between `<!-- remnic:<name>:start -->` and
5
+ * `<!-- remnic:<name>:end -->`. Heading replaces the body after a unique
6
+ * ATX heading until the next same-or-higher heading. The heading line stays.
7
+ * This module does not read config.
8
8
  */
9
9
  import { createHash, randomBytes } from "node:crypto";
10
10
  import { readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
@@ -14,7 +14,8 @@ import { expandTildePath } from "../utils/path.js";
14
14
 
15
15
  export type ApplyManagedRegionResult =
16
16
  | { ok: true; text: string }
17
- | { ok: false; reason: "no_marker"; text: string };
17
+ | { ok: false; reason: "no_marker" | "no_heading"; text: string }
18
+ | { ok: false; reason: "duplicate_heading"; lines: readonly number[]; text: string };
18
19
 
19
20
  export type PublishVaultRegionResult =
20
21
  | { ok: true; status: "updated" | "unchanged" }
@@ -22,8 +23,10 @@ export type PublishVaultRegionResult =
22
23
 
23
24
  export function applyManagedRegion(
24
25
  fileText: string,
25
- opts: { strategy: "markers"; name: string; content: string },
26
+ opts: { strategy: "markers" | "heading"; name: string; content: string },
26
27
  ): ApplyManagedRegionResult {
28
+ if (opts.strategy === "heading") return applyHeadingRegion(fileText, opts.name, opts.content);
29
+
27
30
  const startMarker = `<!-- remnic:${opts.name}:start -->`;
28
31
  const endMarker = `<!-- remnic:${opts.name}:end -->`;
29
32
  const startIdx = fileText.indexOf(startMarker);
@@ -31,17 +34,93 @@ export function applyManagedRegion(
31
34
  const endIdx = fileText.indexOf(endMarker, startIdx + startMarker.length);
32
35
  if (endIdx === -1) return { ok: false, reason: "no_marker", text: fileText };
33
36
 
34
- const eol = fileText.includes("\r\n") ? "\r\n" : "\n";
35
- let body = opts.content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
36
- if (eol === "\r\n") body = body.replace(/\n/g, "\r\n");
37
- if (body.endsWith(eol)) body = body.slice(0, -eol.length);
38
-
37
+ const { body, eol } = ownedBody(fileText, opts.content);
39
38
  return {
40
39
  ok: true,
41
40
  text: `${fileText.slice(0, startIdx + startMarker.length)}${eol}${body}${eol}${fileText.slice(endIdx)}`,
42
41
  };
43
42
  }
44
43
 
44
+ function applyHeadingRegion(fileText: string, name: string, content: string): ApplyManagedRegionResult {
45
+ const wanted = name.trim();
46
+ const hits: Array<{ line: number; level: number; next: number }> = [];
47
+ for (const row of fileLines(fileText)) {
48
+ const heading = parseAtxHeading(row.line);
49
+ if (heading && heading.text === wanted) {
50
+ hits.push({ line: row.number, level: heading.level, next: row.next });
51
+ }
52
+ }
53
+ if (hits.length === 0) return { ok: false, reason: "no_heading", text: fileText };
54
+ if (hits.length > 1) {
55
+ return { ok: false, reason: "duplicate_heading", lines: hits.map((hit) => hit.line), text: fileText };
56
+ }
57
+
58
+ const hit = hits[0]!;
59
+ let sectionEnd = fileText.length;
60
+ for (const row of fileLines(fileText)) {
61
+ if (row.start < hit.next) continue;
62
+ const heading = parseAtxHeading(row.line);
63
+ if (heading && heading.level <= hit.level) {
64
+ sectionEnd = row.start;
65
+ break;
66
+ }
67
+ }
68
+
69
+ const { body, eol } = ownedBody(fileText, content);
70
+ const prefix =
71
+ hit.next === fileText.length && !fileText.endsWith("\n") && !fileText.endsWith("\r")
72
+ ? `${fileText}${eol}`
73
+ : fileText.slice(0, hit.next);
74
+ return { ok: true, text: `${prefix}${body}${eol}${fileText.slice(sectionEnd)}` };
75
+ }
76
+
77
+ function ownedBody(fileText: string, content: string): { body: string; eol: string } {
78
+ const eol = fileText.includes("\r\n") ? "\r\n" : "\n";
79
+ let body = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
80
+ if (eol === "\r\n") body = body.replace(/\n/g, "\r\n");
81
+ if (body.endsWith(eol)) body = body.slice(0, -eol.length);
82
+ return { body, eol };
83
+ }
84
+
85
+ function fileLines(fileText: string): Array<{ line: string; start: number; next: number; number: number }> {
86
+ const rows: Array<{ line: string; start: number; next: number; number: number }> = [];
87
+ let start = 0;
88
+ let number = 1;
89
+ while (start < fileText.length) {
90
+ let i = start;
91
+ while (i < fileText.length && fileText[i] !== "\n" && fileText[i] !== "\r") i++;
92
+ let next = i;
93
+ if (fileText[i] === "\r" && fileText[i + 1] === "\n") next = i + 2;
94
+ else if (fileText[i] === "\n" || fileText[i] === "\r") next = i + 1;
95
+ else next = fileText.length;
96
+ rows.push({ line: fileText.slice(start, i), start, next, number });
97
+ number += 1;
98
+ start = next;
99
+ }
100
+ return rows;
101
+ }
102
+
103
+ function parseAtxHeading(line: string): { level: number; text: string } | null {
104
+ let level = 0;
105
+ while (level < line.length && level < 6 && line[level] === "#") level++;
106
+ if (level === 0) return null;
107
+ const after = line[level];
108
+ if (after !== " " && after !== "\t") return null;
109
+
110
+ let start = level + 1;
111
+ let end = line.length;
112
+ while (start < end && (line[start] === " " || line[start] === "\t")) start++;
113
+ while (end > start && (line[end - 1] === " " || line[end - 1] === "\t")) end--;
114
+
115
+ let hashEnd = end;
116
+ while (hashEnd > start && line[hashEnd - 1] === "#") hashEnd--;
117
+ if (hashEnd < end && hashEnd > start && (line[hashEnd - 1] === " " || line[hashEnd - 1] === "\t")) {
118
+ end = hashEnd;
119
+ while (end > start && (line[end - 1] === " " || line[end - 1] === "\t")) end--;
120
+ }
121
+ return { level, text: line.slice(start, end) };
122
+ }
123
+
45
124
  export function publishVaultRegion(input: {
46
125
  vaultPath: string;
47
126
  relativeFile: string;
@@ -79,7 +158,7 @@ export function publishVaultRegion(input: {
79
158
  name: input.name,
80
159
  content: input.content,
81
160
  });
82
- if (!applied.ok) return { ok: false, reason: applied.reason };
161
+ if (!applied.ok) return { ok: false, reason: "no_marker" };
83
162
 
84
163
  const prevHash = createHash("sha256").update(existing, "utf8").digest("hex");
85
164
  const nextHash = createHash("sha256").update(applied.text, "utf8").digest("hex");
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { assertBeginEndPair } from "./vault-region-pair.js";
5
+
6
+ test("assertBeginEndPair accepts a matching pair", () => {
7
+ assert.deepEqual(assertBeginEndPair({ beginName: "timeline", endName: "timeline" }), { ok: true });
8
+ assert.deepEqual(assertBeginEndPair({ beginName: " timeline ", endName: "timeline" }), {
9
+ ok: true,
10
+ });
11
+ assert.deepEqual(assertBeginEndPair({ beginName: "timeline", endName: " timeline " }), {
12
+ ok: true,
13
+ });
14
+ });
15
+
16
+ test("assertBeginEndPair rejects a name mismatch", () => {
17
+ assert.deepEqual(assertBeginEndPair({ beginName: "timeline", endName: "weekly" }), {
18
+ ok: false,
19
+ error: "name_mismatch",
20
+ });
21
+ });
22
+
23
+ test("assertBeginEndPair throws on an empty name", () => {
24
+ assert.throws(() => assertBeginEndPair({ beginName: "", endName: "timeline" }), /empty/i);
25
+ assert.throws(() => assertBeginEndPair({ beginName: "timeline", endName: " " }), /empty/i);
26
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Assert a managed-region begin/end name pair (issue #1985).
3
+ *
4
+ * Trim both names. Empty throws. Mismatch is name_mismatch. Else ok.
5
+ */
6
+
7
+ export type BeginEndPairInput = {
8
+ beginName: string;
9
+ endName: string;
10
+ };
11
+
12
+ export type BeginEndPairResult = { ok: true } | { ok: false; error: "name_mismatch" };
13
+
14
+ export function assertBeginEndPair({ beginName, endName }: BeginEndPairInput): BeginEndPairResult {
15
+ const begin = beginName.trim();
16
+ const end = endName.trim();
17
+ if (begin.length === 0 || end.length === 0) {
18
+ throw new RangeError("Region name must be non-empty.");
19
+ }
20
+ if (begin !== end) {
21
+ return { ok: false, error: "name_mismatch" };
22
+ }
23
+ return { ok: true };
24
+ }
@@ -0,0 +1,24 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { validateRegionName } from "./vault-region.js";
5
+
6
+ test("validateRegionName trims a valid name", () => {
7
+ assert.deepEqual(validateRegionName("timeline"), { ok: true, name: "timeline" });
8
+ assert.deepEqual(validateRegionName(" weekly "), { ok: true, name: "weekly" });
9
+ });
10
+
11
+ test("validateRegionName rejects empty and whitespace-only names", () => {
12
+ assert.deepEqual(validateRegionName(""), { ok: false, error: "invalid_name" });
13
+ assert.deepEqual(validateRegionName(" "), { ok: false, error: "invalid_name" });
14
+ });
15
+
16
+ test("validateRegionName rejects names with newlines", () => {
17
+ assert.deepEqual(validateRegionName("time\nline"), { ok: false, error: "invalid_name" });
18
+ assert.deepEqual(validateRegionName("time\rline"), { ok: false, error: "invalid_name" });
19
+ });
20
+
21
+ test("validateRegionName rejects the HTML comment closer", () => {
22
+ assert.deepEqual(validateRegionName("foo-->bar"), { ok: false, error: "invalid_name" });
23
+ assert.deepEqual(validateRegionName("timeline-->"), { ok: false, error: "invalid_name" });
24
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Validate a managed-region name for vault markers (issue #1985).
3
+ *
4
+ * Empty, whitespace-only, newline, and `-->` names are rejected so they
5
+ * cannot break `<!-- remnic:<name>:start -->` markers.
6
+ */
7
+
8
+ export type ValidateRegionNameResult =
9
+ | { ok: true; name: string }
10
+ | { ok: false; error: "invalid_name" };
11
+
12
+ export function validateRegionName(name: string): ValidateRegionNameResult {
13
+ if (typeof name !== "string") {
14
+ return { ok: false, error: "invalid_name" };
15
+ }
16
+ if (name.includes("\n") || name.includes("\r") || name.includes("-->")) {
17
+ return { ok: false, error: "invalid_name" };
18
+ }
19
+ const trimmed = name.trim();
20
+ if (trimmed.length === 0) {
21
+ return { ok: false, error: "invalid_name" };
22
+ }
23
+ return { ok: true, name: trimmed };
24
+ }
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { stripVaultMdSuffix } from "./vault-suffix.js";
5
+
6
+ test("stripVaultMdSuffix drops a trailing .md", () => {
7
+ assert.equal(stripVaultMdSuffix("Daily Notes/2026-08-18.md"), "Daily Notes/2026-08-18");
8
+ });
9
+
10
+ test("stripVaultMdSuffix leaves a path with no suffix unchanged", () => {
11
+ assert.equal(stripVaultMdSuffix("Daily Notes/2026-08-18"), "Daily Notes/2026-08-18");
12
+ });
13
+
14
+ test("stripVaultMdSuffix throws on empty input", () => {
15
+ assert.throws(() => stripVaultMdSuffix(""), /empty/i);
16
+ });
17
+
18
+ test("stripVaultMdSuffix does not strip .markdown", () => {
19
+ assert.equal(stripVaultMdSuffix("Daily Notes/2026-08-18.markdown"), "Daily Notes/2026-08-18.markdown");
20
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Strip a trailing `.md` suffix from a vault note path (issue #1985).
3
+ *
4
+ * Case-sensitive. Does not strip `.markdown`. Empty input is rejected.
5
+ */
6
+
7
+ export function stripVaultMdSuffix(notePath: string): string {
8
+ if (typeof notePath !== "string" || notePath.length === 0) {
9
+ throw new RangeError("Vault path must be non-empty.");
10
+ }
11
+ return notePath.endsWith(".md") ? notePath.slice(0, -3) : notePath;
12
+ }
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { formatVaultWikilink } from "./vault-wikilink.js";
5
+
6
+ test("formatVaultWikilink returns a wikilink without the .md suffix", () => {
7
+ assert.equal(formatVaultWikilink("Daily Notes/2026-08-18.md"), "[[Daily Notes/2026-08-18]]");
8
+ assert.equal(formatVaultWikilink("Places/ACME Office"), "[[Places/ACME Office]]");
9
+ });
10
+
11
+ test("formatVaultWikilink rejects absolute paths", () => {
12
+ assert.throws(() => formatVaultWikilink("/abs/note.md"), /absolute/i);
13
+ assert.throws(() => formatVaultWikilink("C:/vault/note.md"), /absolute/i);
14
+ });
15
+
16
+ test("formatVaultWikilink rejects parent segments", () => {
17
+ assert.throws(() => formatVaultWikilink("../outside.md"), /\.\./);
18
+ assert.throws(() => formatVaultWikilink("notes/../../secret.md"), /\.\./);
19
+ });
20
+
21
+ test("formatVaultWikilink rejects empty and newline paths", () => {
22
+ assert.throws(() => formatVaultWikilink(""), /empty/i);
23
+ assert.throws(() => formatVaultWikilink(" "), /empty/i);
24
+ assert.throws(() => formatVaultWikilink(".md"), /empty/i);
25
+ assert.throws(() => formatVaultWikilink("foo\nbar.md"), /newline/i);
26
+ assert.throws(() => formatVaultWikilink("foo\rbar.md"), /newline/i);
27
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Format a vault-relative note path as an Obsidian wikilink (issue #1985).
3
+ *
4
+ * Relative paths only. Rejects absolute paths, `..` segments, empty input,
5
+ * and newlines. Strips a trailing `.md` suffix.
6
+ */
7
+ import path from "node:path";
8
+
9
+ export function formatVaultWikilink(notePath: string): string {
10
+ if (typeof notePath !== "string" || notePath.trim().length === 0) {
11
+ throw new RangeError("Vault wikilink path must be a non-empty relative path.");
12
+ }
13
+ if (notePath.includes("\n") || notePath.includes("\r")) {
14
+ throw new RangeError("Vault wikilink path must not contain newlines.");
15
+ }
16
+ if (path.posix.isAbsolute(notePath) || path.win32.isAbsolute(notePath)) {
17
+ throw new RangeError("Vault wikilink path must be relative; absolute paths are rejected.");
18
+ }
19
+ if (notePath.split(/[\\/]/).some((segment) => segment === "..")) {
20
+ throw new RangeError("Vault wikilink path must not contain `..` segments.");
21
+ }
22
+
23
+ const stripped = notePath.endsWith(".md") ? notePath.slice(0, -3) : notePath;
24
+ if (stripped.length === 0) {
25
+ throw new RangeError("Vault wikilink path must be a non-empty relative path.");
26
+ }
27
+ return `[[${stripped}]]`;
28
+ }
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { assertHalfOpenWindow } from "./window-order.js";
5
+
6
+ test("ordered window is ok", () => {
7
+ assert.deepEqual(assertHalfOpenWindow({ fromMs: 0, toMs: 1 }), { ok: true });
8
+ });
9
+
10
+ test("equal bounds are empty_window", () => {
11
+ assert.deepEqual(assertHalfOpenWindow({ fromMs: 10, toMs: 10 }), {
12
+ ok: false,
13
+ error: "empty_window",
14
+ });
15
+ });
16
+
17
+ test("inverted bounds are empty_window", () => {
18
+ assert.deepEqual(assertHalfOpenWindow({ fromMs: 20, toMs: 10 }), {
19
+ ok: false,
20
+ error: "empty_window",
21
+ });
22
+ });
23
+
24
+ test("NaN bounds throw", () => {
25
+ assert.throws(() => assertHalfOpenWindow({ fromMs: Number.NaN, toMs: 1 }), /finite/);
26
+ assert.throws(() => assertHalfOpenWindow({ fromMs: 0, toMs: Number.NaN }), /finite/);
27
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Assert a half-open activity window [fromMs, toMs) (issue #2053 leftover).
3
+ *
4
+ * Non-finite bounds throw. `toMs <= fromMs` is empty_window.
5
+ */
6
+
7
+ export type AssertHalfOpenWindowResult =
8
+ | { ok: true }
9
+ | { ok: false; error: "empty_window" };
10
+
11
+ /** Validate `[fromMs, toMs)`. Empty or inverted windows fail closed. */
12
+ export function assertHalfOpenWindow(window: {
13
+ fromMs: number;
14
+ toMs: number;
15
+ }): AssertHalfOpenWindowResult {
16
+ const { fromMs, toMs } = window;
17
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) {
18
+ throw new RangeError(
19
+ `activity window bounds must be finite; got fromMs=${fromMs} toMs=${toMs}`,
20
+ );
21
+ }
22
+ if (toMs <= fromMs) return { ok: false, error: "empty_window" };
23
+ return { ok: true };
24
+ }
@@ -0,0 +1,19 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { countMergeCandidates } from "./merge-count.js";
5
+
6
+ test("empty list is 0", () => {
7
+ assert.equal(countMergeCandidates([]), 0);
8
+ });
9
+
10
+ test("counts unique ids", () => {
11
+ assert.equal(countMergeCandidates(["alpha", "beta", "alpha"]), 2);
12
+ assert.equal(countMergeCandidates(["alpha", "beta", "gamma"]), 3);
13
+ });
14
+
15
+ test("drops empty strings and does not mutate", () => {
16
+ const ids = ["alpha", "", "beta", "", "alpha"];
17
+ assert.equal(countMergeCandidates(ids), 2);
18
+ assert.deepEqual(ids, ["alpha", "", "beta", "", "alpha"]);
19
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Merge-candidate counter (issue #2330 leftover).
3
+ *
4
+ * Unique ids after dropping empty strings. Does not mutate input.
5
+ */
6
+
7
+ export function countMergeCandidates(ids: readonly string[]): number {
8
+ const unique = new Set<string>();
9
+ for (const id of ids) {
10
+ if (id !== "") unique.add(id);
11
+ }
12
+ return unique.size;
13
+ }