@remnic/core 9.69.11 → 9.69.13

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 (360) hide show
  1. package/dist/access-admin-ops-surface.d.ts +11 -11
  2. package/dist/access-admin-ops-surface.js +4 -4
  3. package/dist/access-authorization-probe.d.ts +11 -11
  4. package/dist/access-authorization-probe.js +5 -5
  5. package/dist/access-boundary.d.ts +11 -11
  6. package/dist/access-boundary.js +4 -4
  7. package/dist/access-cli.js +28 -28
  8. package/dist/access-coding-context-resolution.d.ts +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +11 -11
  10. package/dist/access-extraction-force-flush.js +4 -4
  11. package/dist/access-health-types.d.ts +1 -1
  12. package/dist/access-http-lcm-compaction.d.ts +11 -11
  13. package/dist/access-http-lifecycle-flush.d.ts +11 -11
  14. package/dist/access-http-offline-stream.d.ts +11 -11
  15. package/dist/access-http-query.d.ts +2 -2
  16. package/dist/access-http-query.js +4 -4
  17. package/dist/access-http.d.ts +12 -12
  18. package/dist/access-http.js +15 -15
  19. package/dist/access-identity-continuity-surface.d.ts +9 -9
  20. package/dist/access-identity-continuity-surface.js +4 -4
  21. package/dist/access-lcm-surface.d.ts +11 -11
  22. package/dist/access-lcm-surface.js +4 -4
  23. package/dist/access-mcp.d.ts +11 -11
  24. package/dist/access-mcp.js +11 -11
  25. package/dist/access-memory-search-fanout.d.ts +2 -2
  26. package/dist/{access-namespace-preflight-Cy0yPsaE.d.ts → access-namespace-preflight-Rzuo1nGR.d.ts} +1 -1
  27. package/dist/access-namespace-preflight.d.ts +3 -3
  28. package/dist/access-namespace-preflight.js +4 -4
  29. package/dist/access-observe-write-surface.d.ts +11 -11
  30. package/dist/access-observe-write-surface.js +4 -4
  31. package/dist/access-offline-manifest.d.ts +9 -9
  32. package/dist/access-operations-batch.js +5 -5
  33. package/dist/access-operations.d.ts +17 -17
  34. package/dist/access-operations.js +10 -10
  35. package/dist/access-recall-concurrency.d.ts +11 -11
  36. package/dist/access-recall-concurrency.js +4 -4
  37. package/dist/access-recall-response.d.ts +11 -11
  38. package/dist/access-recall-response.js +4 -4
  39. package/dist/access-recall-surface.d.ts +11 -11
  40. package/dist/access-recall-surface.js +4 -4
  41. package/dist/access-schema.d.ts +91 -91
  42. package/dist/{access-service-Cqfk3dD1.d.ts → access-service-Bb2Fykzc.d.ts} +71 -71
  43. package/dist/access-service-helpers.d.ts +11 -11
  44. package/dist/access-service.d.ts +11 -11
  45. package/dist/access-service.js +4 -4
  46. package/dist/access-surface-catalog.d.ts +11 -11
  47. package/dist/access-surface-catalog.js +6 -6
  48. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  49. package/dist/action-confidence.d.ts +1 -1
  50. package/dist/active-memory-bridge.d.ts +1 -1
  51. package/dist/active-recall.d.ts +1 -1
  52. package/dist/active-recall.js +4 -4
  53. package/dist/ambient-provenance.d.ts +1 -1
  54. package/dist/artifact-search.d.ts +1 -1
  55. package/dist/behavior-learner.d.ts +1 -1
  56. package/dist/behavior-signals.d.ts +1 -1
  57. package/dist/bootstrap.d.ts +9 -9
  58. package/dist/briefing-window.d.ts +2 -2
  59. package/dist/briefing.d.ts +2 -2
  60. package/dist/buffer-surprise-report.d.ts +1 -1
  61. package/dist/buffer-turn-helpers.d.ts +1 -1
  62. package/dist/buffer.d.ts +2 -2
  63. package/dist/buffer.js +4 -4
  64. package/dist/bulk-import/index.d.ts +3 -3
  65. package/dist/calibration.d.ts +1 -1
  66. package/dist/capabilities.d.ts +1 -1
  67. package/dist/{catalog-9X8WpZNO.d.ts → catalog-DjTLVOYp.d.ts} +1 -1
  68. package/dist/causal-behavior.d.ts +1 -1
  69. package/dist/causal-consolidation.d.ts +1 -1
  70. package/dist/causal-trajectory-graph.d.ts +1 -1
  71. package/dist/{chunk-TEZ2HTC7.js → chunk-33JKYLPX.js} +2 -2
  72. package/dist/{chunk-MNH2GM4O.js → chunk-3BS2OGDT.js} +6 -19
  73. package/dist/{chunk-MNH2GM4O.js.map → chunk-3BS2OGDT.js.map} +1 -1
  74. package/dist/{chunk-5PTS43CH.js → chunk-4LG4CPOC.js} +2 -2
  75. package/dist/{chunk-Z33VUMKY.js → chunk-5H2G5X4R.js} +2 -2
  76. package/dist/{chunk-XFPHV65N.js → chunk-5JQV4T5Q.js} +2 -2
  77. package/dist/{chunk-OASGWNYL.js → chunk-63I7LMLG.js} +2 -2
  78. package/dist/{chunk-J3NGQ5SC.js → chunk-7GSSHAXC.js} +2 -2
  79. package/dist/{chunk-X4FIZAVQ.js → chunk-7JV4JST3.js} +854 -42
  80. package/dist/chunk-7JV4JST3.js.map +1 -0
  81. package/dist/{chunk-VHAKBUBD.js → chunk-B64IVEOH.js} +2 -2
  82. package/dist/{chunk-R7FXPG4T.js → chunk-C25N5HWX.js} +2 -2
  83. package/dist/{chunk-DH3V7RCQ.js → chunk-F2ERDKY2.js} +7 -7
  84. package/dist/{chunk-LJNLLNEM.js → chunk-HODFGQGT.js} +2 -2
  85. package/dist/{chunk-B75VCEX5.js → chunk-K2IJH4D3.js} +2 -2
  86. package/dist/{chunk-227WJVPY.js → chunk-KLSPKMIO.js} +2 -2
  87. package/dist/{chunk-5P2KNFJZ.js → chunk-MSDIETFC.js} +3 -3
  88. package/dist/{chunk-HFRGCNFY.js → chunk-OAKATRAN.js} +2 -2
  89. package/dist/chunk-OGUKSLYZ.js +560 -0
  90. package/dist/chunk-OGUKSLYZ.js.map +1 -0
  91. package/dist/{chunk-M6L7HI7R.js → chunk-P6FHFTZ7.js} +16 -1
  92. package/dist/chunk-P6FHFTZ7.js.map +1 -0
  93. package/dist/{chunk-LWBSEYW3.js → chunk-PAWIY5OQ.js} +5 -5
  94. package/dist/{chunk-3ITTF7YH.js → chunk-QVI63XSO.js} +90 -383
  95. package/dist/chunk-QVI63XSO.js.map +1 -0
  96. package/dist/{chunk-6TOWUN2R.js → chunk-RE3H6Y2X.js} +2 -2
  97. package/dist/{chunk-V3HUBF7T.js → chunk-S35IIP5P.js} +2 -2
  98. package/dist/{chunk-3YKOFCKP.js → chunk-SXHBZXHK.js} +2 -2
  99. package/dist/{chunk-7NCUHD5U.js → chunk-VAMZYOOV.js} +2 -2
  100. package/dist/{chunk-PGCZLHRB.js → chunk-W272SWN7.js} +3 -3
  101. package/dist/{chunk-5HO3QBAD.js → chunk-W6GEMUZM.js} +2 -2
  102. package/dist/{chunk-UCNQV43K.js → chunk-XQYTSZW3.js} +2 -2
  103. package/dist/{chunk-GR4C3FHT.js → chunk-ZKZCJRRO.js} +2 -2
  104. package/dist/{cli-Dk-lM_tI.d.ts → cli-DXXy60A4.d.ts} +5 -5
  105. package/dist/cli.d.ts +13 -13
  106. package/dist/cli.js +21 -21
  107. package/dist/coding/export-okf-codegraph.d.ts +1 -1
  108. package/dist/coding/pre-action-gate.d.ts +1 -1
  109. package/dist/compounding/engine.d.ts +2 -2
  110. package/dist/compounding/preference-consolidator.d.ts +1 -1
  111. package/dist/compression-optimizer.d.ts +1 -1
  112. package/dist/config.d.ts +1 -1
  113. package/dist/config.js +4 -4
  114. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  115. package/dist/connectors/codex-materialize.d.ts +1 -1
  116. package/dist/connectors/index.d.ts +1 -1
  117. package/dist/consolidation-provenance-check.d.ts +2 -2
  118. package/dist/consolidation-undo.d.ts +2 -2
  119. package/dist/contradiction/index.d.ts +3 -3
  120. package/dist/converge-config.d.ts +1 -1
  121. package/dist/convergence-refresh.d.ts +2 -2
  122. package/dist/conversation-index/backend.d.ts +1 -1
  123. package/dist/conversation-index/chunker.d.ts +1 -1
  124. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  125. package/dist/conversation-index/indexer.d.ts +1 -1
  126. package/dist/conversation-index/search.d.ts +1 -1
  127. package/dist/corpus-watermark.d.ts +1 -1
  128. package/dist/cross-lingual-recall.d.ts +1 -1
  129. package/dist/day-summary.d.ts +1 -1
  130. package/dist/deep-recall-http-glue.d.ts +11 -11
  131. package/dist/deep-recall-mcp-tools.d.ts +11 -11
  132. package/dist/deep-recall-policy-llm.d.ts +1 -1
  133. package/dist/deep-recall-seeds.d.ts +1 -1
  134. package/dist/delinearize.d.ts +1 -1
  135. package/dist/dependency-propagation-config.d.ts +1 -1
  136. package/dist/{dependency-propagation-delivery-BZGUecZf.d.ts → dependency-propagation-delivery-37JdQv_3.d.ts} +1 -1
  137. package/dist/direct-answer-wiring.d.ts +1 -1
  138. package/dist/direct-answer.d.ts +1 -1
  139. package/dist/embedding-fallback.d.ts +1 -1
  140. package/dist/enrichment/index.d.ts +1 -1
  141. package/dist/entity-origin-fields.d.ts +1 -1
  142. package/dist/entity-retrieval.d.ts +2 -2
  143. package/dist/entity-schema.d.ts +1 -1
  144. package/dist/episodic-context.d.ts +1 -1
  145. package/dist/eval-trajectory-contracts.d.ts +1 -1
  146. package/dist/eval-trajectory-store.d.ts +1 -1
  147. package/dist/eval-trajectory.d.ts +1 -1
  148. package/dist/explicit-capture.d.ts +9 -9
  149. package/dist/external-wiki-access.d.ts +11 -11
  150. package/dist/external-wiki-access.js +5 -5
  151. package/dist/external-wiki-collection-registration.d.ts +1 -1
  152. package/dist/external-wiki-collection.d.ts +1 -1
  153. package/dist/external-wiki-mcp-tools.d.ts +11 -11
  154. package/dist/extraction-error-classification.d.ts +1 -1
  155. package/dist/extraction-faithfulness.d.ts +1 -1
  156. package/dist/extraction-judge-telemetry.d.ts +1 -1
  157. package/dist/extraction-judge-training.d.ts +1 -1
  158. package/dist/extraction-judge.d.ts +1 -1
  159. package/dist/extraction-liveness.d.ts +1 -1
  160. package/dist/extraction-normalization.d.ts +1 -1
  161. package/dist/extraction-prompt.d.ts +1 -1
  162. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  163. package/dist/extraction-source-grounding.d.ts +1 -1
  164. package/dist/extraction.d.ts +1 -1
  165. package/dist/extraction.js +5 -5
  166. package/dist/fallback-llm.d.ts +1 -1
  167. package/dist/graph-dashboard-diff.d.ts +1 -1
  168. package/dist/graph-dashboard-key.d.ts +1 -1
  169. package/dist/graph-dashboard-parser.d.ts +1 -1
  170. package/dist/graph-edge-reinforcement.d.ts +1 -1
  171. package/dist/graph-path-reconstruction.d.ts +1 -1
  172. package/dist/graph-path-scoring.d.ts +1 -1
  173. package/dist/graph-snapshot.d.ts +1 -1
  174. package/dist/graph.d.ts +1 -1
  175. package/dist/importance.d.ts +1 -1
  176. package/dist/importers/index.d.ts +1 -1
  177. package/dist/in-flight-reads.d.ts +1 -1
  178. package/dist/{index-B4iTvT-w.d.ts → index-DquhYqsc.d.ts} +2 -2
  179. package/dist/index.d.ts +876 -738
  180. package/dist/index.js +59 -37
  181. package/dist/intent.d.ts +1 -1
  182. package/dist/lcm/engine.d.ts +1 -1
  183. package/dist/lcm/index.d.ts +1 -1
  184. package/dist/lcm/tools.d.ts +1 -1
  185. package/dist/lifecycle.d.ts +1 -1
  186. package/dist/live-connectors-runner.d.ts +1 -1
  187. package/dist/local-llm.d.ts +1 -1
  188. package/dist/local-model-endpoint.d.ts +1 -1
  189. package/dist/location/index.d.ts +3 -3
  190. package/dist/location/index.js +5 -5
  191. package/dist/maintenance/memory-governance.d.ts +1 -1
  192. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  193. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  194. package/dist/{maintenance-BBpNqEeJ.d.ts → maintenance-CizZOycr.d.ts} +3 -3
  195. package/dist/mcp-memory-inspector-app.d.ts +11 -11
  196. package/dist/memory-action-policy.d.ts +1 -1
  197. package/dist/memory-cache.d.ts +1 -1
  198. package/dist/memory-controller.d.ts +1 -1
  199. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  200. package/dist/memory-projection-store.d.ts +1 -1
  201. package/dist/memory-provenance.d.ts +1 -1
  202. package/dist/memory-snapshot.d.ts +1 -1
  203. package/dist/memory-subject-cli.d.ts +11 -11
  204. package/dist/memory-subject.d.ts +11 -11
  205. package/dist/memory-worth-outcomes.d.ts +2 -2
  206. package/dist/models-json.d.ts +1 -1
  207. package/dist/namespaces/migrate.d.ts +3 -3
  208. package/dist/namespaces/principal.d.ts +1 -1
  209. package/dist/namespaces/search.d.ts +1 -1
  210. package/dist/namespaces/storage.d.ts +3 -3
  211. package/dist/native-knowledge.d.ts +1 -1
  212. package/dist/offline-sync-impression-drain.d.ts +1 -1
  213. package/dist/operator-doctor-corpus.d.ts +1 -1
  214. package/dist/operator-doctor-replica.d.ts +1 -1
  215. package/dist/operator-toolkit.d.ts +3 -3
  216. package/dist/operator-toolkit.js +5 -5
  217. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  218. package/dist/orchestration/maintenance.d.ts +4 -4
  219. package/dist/orchestration/maintenance.js +6 -6
  220. package/dist/{orchestrator-DglY1H0w.d.ts → orchestrator-DqLcuNMP.d.ts} +8 -8
  221. package/dist/orchestrator.d.ts +9 -9
  222. package/dist/orchestrator.js +28 -28
  223. package/dist/patterns-cli.d.ts +1 -1
  224. package/dist/{pipeline-C7qRsy6g.d.ts → pipeline-DbJpmcRu.d.ts} +1 -1
  225. package/dist/policy-runtime.d.ts +1 -1
  226. package/dist/proactive-contention.d.ts +1 -1
  227. package/dist/provenance.d.ts +1 -1
  228. package/dist/{public-http-CPpCsi35.d.ts → public-http-NQ3UUp6W.d.ts} +1 -1
  229. package/dist/{qmd-YRLTa3AE.d.ts → qmd-DA_7fbSz.d.ts} +1 -1
  230. package/dist/qmd-preflight.d.ts +2 -2
  231. package/dist/qmd-recall-cache.d.ts +1 -1
  232. package/dist/qmd.d.ts +2 -2
  233. package/dist/recall-concurrency-config.d.ts +1 -1
  234. package/dist/recall-disclosure-escalation.d.ts +1 -1
  235. package/dist/recall-explain-renderer.d.ts +1 -1
  236. package/dist/recall-memory-map.d.ts +1 -1
  237. package/dist/recall-navigate-access.d.ts +1 -1
  238. package/dist/recall-navigate-disclosure.d.ts +1 -1
  239. package/dist/recall-navigate.d.ts +1 -1
  240. package/dist/recall-planner-llm.d.ts +1 -1
  241. package/dist/recall-state.d.ts +1 -1
  242. package/dist/recall-tag-filter.d.ts +1 -1
  243. package/dist/recall-timings.d.ts +1 -1
  244. package/dist/recall-xray-cli.d.ts +1 -1
  245. package/dist/recall-xray-renderer.d.ts +1 -1
  246. package/dist/recall-xray.d.ts +1 -1
  247. package/dist/reconcile/cursor.d.ts +1 -1
  248. package/dist/reconcile/manifest.d.ts +1 -1
  249. package/dist/reconcile/plan.d.ts +1 -1
  250. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  251. package/dist/replay/normalizers/claude.d.ts +1 -1
  252. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  253. package/dist/replay/normalizers/shared.d.ts +1 -1
  254. package/dist/replay/runner.d.ts +1 -1
  255. package/dist/replay/types.d.ts +1 -1
  256. package/dist/replica-divergence.d.ts +1 -1
  257. package/dist/replica-peers-config.d.ts +1 -1
  258. package/dist/resolve-auth-token.d.ts +1 -1
  259. package/dist/resume-bundles.js +5 -5
  260. package/dist/retrieval-agents.d.ts +2 -2
  261. package/dist/retrieval-tiers.d.ts +1 -1
  262. package/dist/routing/engine.d.ts +1 -1
  263. package/dist/routing/store.d.ts +1 -1
  264. package/dist/salvage-envelope.d.ts +1 -1
  265. package/dist/schemas.d.ts +176 -176
  266. package/dist/{scope-profiles-BVvqVzJX.d.ts → scope-profiles-BpiWKZzx.d.ts} +1 -1
  267. package/dist/search/embed-helper.d.ts +1 -1
  268. package/dist/search/factory.d.ts +1 -1
  269. package/dist/search/index.d.ts +1 -1
  270. package/dist/search/lancedb-backend.d.ts +1 -1
  271. package/dist/search/meilisearch-backend.d.ts +1 -1
  272. package/dist/search/noop-backend.d.ts +1 -1
  273. package/dist/search/orama-backend.d.ts +1 -1
  274. package/dist/search/port.d.ts +1 -1
  275. package/dist/search/remote-backend.d.ts +1 -1
  276. package/dist/{semantic-consolidation-BzCI6Kuu.d.ts → semantic-consolidation-NJpaZpkY.d.ts} +1 -1
  277. package/dist/semantic-consolidation.d.ts +2 -2
  278. package/dist/semantic-rule-verifier.d.ts +1 -1
  279. package/dist/{service-DQ3Gscy5.d.ts → service-DdlZjX1D.d.ts} +2 -2
  280. package/dist/session-observer-bands.d.ts +1 -1
  281. package/dist/session-observer-state.d.ts +1 -1
  282. package/dist/shared-context/manager.d.ts +7 -7
  283. package/dist/signal.d.ts +1 -1
  284. package/dist/source-agent-qualifier.d.ts +1 -1
  285. package/dist/source-agent-qualifier.js +4 -4
  286. package/dist/standup/build.js +2 -2
  287. package/dist/{storage-DmTbQdD0.d.ts → storage-C7BWrpU9.d.ts} +1 -1
  288. package/dist/storage.d.ts +2 -2
  289. package/dist/subject-config.d.ts +1 -1
  290. package/dist/summarizer.d.ts +1 -1
  291. package/dist/summary-snapshot.d.ts +1 -1
  292. package/dist/support-passport/index.d.ts +14 -14
  293. package/dist/support-passport/index.js +7 -7
  294. package/dist/temporal-supersession.d.ts +2 -2
  295. package/dist/temporal-timeline-recall.d.ts +1 -1
  296. package/dist/temporal-validity.d.ts +1 -1
  297. package/dist/threading.d.ts +1 -1
  298. package/dist/tier-migration.d.ts +2 -2
  299. package/dist/tier-routing.d.ts +1 -1
  300. package/dist/topics.d.ts +1 -1
  301. package/dist/transcript.d.ts +1 -1
  302. package/dist/transfer/export-okf.d.ts +1 -1
  303. package/dist/transfer/types.d.ts +78 -78
  304. package/dist/trust-score-stage.d.ts +1 -1
  305. package/dist/trust-score.d.ts +1 -1
  306. package/dist/{types-CLRTESSD.d.ts → types-Cy1l6fzp.d.ts} +48 -1
  307. package/dist/types.d.ts +1 -1
  308. package/dist/utility-runtime.d.ts +1 -1
  309. package/dist/who-knows-cli.d.ts +11 -11
  310. package/dist/who-knows.d.ts +11 -11
  311. package/dist/write-envelope.d.ts +1 -1
  312. package/package.json +2 -2
  313. package/src/activity/config.test.ts +94 -1
  314. package/src/activity/config.ts +256 -3
  315. package/src/activity/digest.ts +21 -0
  316. package/src/activity/index.ts +2 -0
  317. package/src/activity/runner.ts +2 -17
  318. package/src/activity/timeline/publish-cli.test.ts +169 -0
  319. package/src/activity/timeline/publish-cli.ts +144 -0
  320. package/src/activity/timeline/query.ts +51 -15
  321. package/src/activity/types.ts +45 -0
  322. package/src/activity/vault-insert.ts +14 -33
  323. package/src/activity/vault-path.ts +100 -22
  324. package/src/activity/vault-publish.test.ts +51 -0
  325. package/src/activity/vault-publish.ts +157 -17
  326. package/src/activity/vault-publisher.test.ts +881 -0
  327. package/src/activity/vault-publisher.ts +597 -0
  328. package/src/activity/vault-region.test.ts +5 -0
  329. package/src/activity/vault-region.ts +7 -2
  330. package/src/cli/timeline-publish-commands.ts +34 -0
  331. package/src/cli.ts +2 -2
  332. package/src/config.test.ts +24 -1
  333. package/dist/chunk-3ITTF7YH.js.map +0 -1
  334. package/dist/chunk-KJUWWN3T.js +0 -237
  335. package/dist/chunk-KJUWWN3T.js.map +0 -1
  336. package/dist/chunk-M6L7HI7R.js.map +0 -1
  337. package/dist/chunk-X4FIZAVQ.js.map +0 -1
  338. /package/dist/{chunk-TEZ2HTC7.js.map → chunk-33JKYLPX.js.map} +0 -0
  339. /package/dist/{chunk-5PTS43CH.js.map → chunk-4LG4CPOC.js.map} +0 -0
  340. /package/dist/{chunk-Z33VUMKY.js.map → chunk-5H2G5X4R.js.map} +0 -0
  341. /package/dist/{chunk-XFPHV65N.js.map → chunk-5JQV4T5Q.js.map} +0 -0
  342. /package/dist/{chunk-OASGWNYL.js.map → chunk-63I7LMLG.js.map} +0 -0
  343. /package/dist/{chunk-J3NGQ5SC.js.map → chunk-7GSSHAXC.js.map} +0 -0
  344. /package/dist/{chunk-VHAKBUBD.js.map → chunk-B64IVEOH.js.map} +0 -0
  345. /package/dist/{chunk-R7FXPG4T.js.map → chunk-C25N5HWX.js.map} +0 -0
  346. /package/dist/{chunk-DH3V7RCQ.js.map → chunk-F2ERDKY2.js.map} +0 -0
  347. /package/dist/{chunk-LJNLLNEM.js.map → chunk-HODFGQGT.js.map} +0 -0
  348. /package/dist/{chunk-B75VCEX5.js.map → chunk-K2IJH4D3.js.map} +0 -0
  349. /package/dist/{chunk-227WJVPY.js.map → chunk-KLSPKMIO.js.map} +0 -0
  350. /package/dist/{chunk-5P2KNFJZ.js.map → chunk-MSDIETFC.js.map} +0 -0
  351. /package/dist/{chunk-HFRGCNFY.js.map → chunk-OAKATRAN.js.map} +0 -0
  352. /package/dist/{chunk-LWBSEYW3.js.map → chunk-PAWIY5OQ.js.map} +0 -0
  353. /package/dist/{chunk-6TOWUN2R.js.map → chunk-RE3H6Y2X.js.map} +0 -0
  354. /package/dist/{chunk-V3HUBF7T.js.map → chunk-S35IIP5P.js.map} +0 -0
  355. /package/dist/{chunk-3YKOFCKP.js.map → chunk-SXHBZXHK.js.map} +0 -0
  356. /package/dist/{chunk-7NCUHD5U.js.map → chunk-VAMZYOOV.js.map} +0 -0
  357. /package/dist/{chunk-PGCZLHRB.js.map → chunk-W272SWN7.js.map} +0 -0
  358. /package/dist/{chunk-5HO3QBAD.js.map → chunk-W6GEMUZM.js.map} +0 -0
  359. /package/dist/{chunk-UCNQV43K.js.map → chunk-XQYTSZW3.js.map} +0 -0
  360. /package/dist/{chunk-GR4C3FHT.js.map → chunk-ZKZCJRRO.js.map} +0 -0
@@ -0,0 +1,881 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { chmodSync, existsSync, mkdtempSync, mkdirSync, lstatSync, readdirSync, readFileSync, symlinkSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { publishVaultNote, setVaultPublisherTestHooks } from "./vault-publisher.js";
8
+
9
+ const DATE = "2026-08-21";
10
+
11
+ /**
12
+ * Fixture daily note shaped like a real vault user's: frontmatter, human
13
+ * sections, a foreign agent's owned section, and a Remnic marker pair.
14
+ * Synthetic data only (public repo).
15
+ */
16
+ const NOTE = [
17
+ "---",
18
+ "tags: daily-notes",
19
+ "mood: good",
20
+ "---",
21
+ "# Friday",
22
+ "",
23
+ "scratchpad text that must never change",
24
+ "",
25
+ "## Tasks",
26
+ "",
27
+ "- [ ] water the plants",
28
+ "",
29
+ "<!-- other-agent:section:start -->",
30
+ "foreign body",
31
+ "<!-- other-agent:section:end -->",
32
+ "",
33
+ "<!-- remnic:timeline:start -->",
34
+ "stale recap",
35
+ "<!-- remnic:timeline:end -->",
36
+ "",
37
+ "trailing human text",
38
+ "",
39
+ ].join("\n");
40
+
41
+ function makeVault(): { vault: string; notePath: string } {
42
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
43
+ const notePath = path.join(vault, "Daily Notes", "2026", "08", `${DATE}.md`);
44
+ mkdirSync(path.dirname(notePath), { recursive: true });
45
+ writeFileSync(notePath, NOTE, "utf8");
46
+ return { vault, notePath };
47
+ }
48
+
49
+ test("publishVaultNote updates only the managed region and preserves every other byte", () => {
50
+ const { vault, notePath } = makeVault();
51
+ const before = readFileSync(notePath, "utf8");
52
+ const status = publishVaultNote({
53
+ vaultPath: vault,
54
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
55
+ date: DATE,
56
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)\n- card-b: writing (18m)" }],
57
+ });
58
+
59
+ assert.equal(status.counts.updated, 1);
60
+ assert.equal(status.results[0]?.outcome, "updated");
61
+
62
+ const after = readFileSync(notePath, "utf8");
63
+ const startMarker = "<!-- remnic:timeline:start -->";
64
+ const endMarker = "<!-- remnic:timeline:end -->";
65
+ const beforeStart = before.slice(0, before.indexOf(startMarker));
66
+ const afterEnd = before.slice(before.indexOf(endMarker) + endMarker.length);
67
+ assert.equal(after.slice(0, after.indexOf(startMarker)), beforeStart);
68
+ assert.equal(after.slice(after.indexOf(endMarker) + endMarker.length), afterEnd);
69
+ assert.match(after, /- card-a: code review \(42m\)/);
70
+ assert.doesNotMatch(after, /stale recap/);
71
+ assert.equal(after.match(/---/g)?.length, 2, "frontmatter delimiters untouched");
72
+ });
73
+
74
+ test("republishing unchanged content performs no write (mtime preserved)", () => {
75
+ const { vault, notePath } = makeVault();
76
+ const content = "- card-a: code review (42m)";
77
+ publishVaultNote({
78
+ vaultPath: vault,
79
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
80
+ date: DATE,
81
+ sections: [{ name: "timeline", content }],
82
+ });
83
+ const mtimeAfterFirst = statSync(notePath).mtimeMs;
84
+ const second = publishVaultNote({
85
+ vaultPath: vault,
86
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
87
+ date: DATE,
88
+ sections: [{ name: "timeline", content }],
89
+ });
90
+ assert.equal(second.counts.unchanged, 1);
91
+ assert.equal(statSync(notePath).mtimeMs, mtimeAfterFirst);
92
+ });
93
+
94
+ test("dry-run reports the would-be outcome and writes nothing", () => {
95
+ const { vault, notePath } = makeVault();
96
+ const before = readFileSync(notePath, "utf8");
97
+ const mtime = statSync(notePath).mtimeMs;
98
+ const status = publishVaultNote({
99
+ vaultPath: vault,
100
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
101
+ date: DATE,
102
+ sections: [{ name: "timeline", content: "fresh recap" }],
103
+ dryRun: true,
104
+ });
105
+ assert.equal(status.counts.updated, 1);
106
+ assert.equal(readFileSync(notePath, "utf8"), before);
107
+ assert.equal(statSync(notePath).mtimeMs, mtime);
108
+ });
109
+
110
+ test("a crossed begin/end marker pair is refused and the file is untouched", () => {
111
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
112
+ const notePath = path.join(vault, `${DATE}.md`);
113
+ const crossed = [
114
+ "<!-- remnic:timeline:start -->",
115
+ "stale recap",
116
+ "<!-- remnic:standup:end -->",
117
+ "",
118
+ ].join("\n");
119
+ writeFileSync(notePath, crossed, "utf8");
120
+
121
+ const status = publishVaultNote({
122
+ vaultPath: vault,
123
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
124
+ date: DATE,
125
+ sections: [{ name: "timeline", content: "fresh recap" }],
126
+ });
127
+
128
+ assert.equal(status.results[0]?.outcome, "skipped");
129
+ assert.match(status.results[0]?.reason ?? "", /name_mismatch:timeline:standup/);
130
+ assert.equal(readFileSync(notePath, "utf8"), crossed);
131
+ });
132
+
133
+ test("a symlink inside the vault pointing outside is refused before any write", () => {
134
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
135
+ const notePath = path.join(vault, "Daily Notes", "2026", "08", `${DATE}.md`);
136
+ mkdirSync(path.dirname(notePath), { recursive: true });
137
+ const outside = mkdtempSync(path.join(tmpdir(), "remnic-vault-outside-"));
138
+ const outsideNote = path.join(outside, `${DATE}.md`);
139
+ writeFileSync(outsideNote, NOTE, "utf8");
140
+ symlinkSync(outsideNote, notePath);
141
+
142
+ const status = publishVaultNote({
143
+ vaultPath: vault,
144
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
145
+ date: DATE,
146
+ sections: [{ name: "timeline", content: "must not land" }],
147
+ });
148
+
149
+ assert.equal(status.results[0]?.outcome, "error");
150
+ assert.equal(status.results[0]?.reason, "symlink_escape");
151
+ assert.equal(readFileSync(outsideNote, "utf8"), NOTE);
152
+ });
153
+
154
+ test("a crossed pair AFTER a valid pair still refuses the whole note", () => {
155
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
156
+ const notePath = path.join(vault, `${DATE}.md`);
157
+ // The valid standup pair comes first, so a scan that stops at the first
158
+ // closed region never sees the crossed timeline/weekly pair below it.
159
+ // `applyManagedRegion` would then pair the timeline start with the LAST
160
+ // timeline end and delete every byte in between.
161
+ const note = [
162
+ "<!-- remnic:standup:start -->",
163
+ "standup body",
164
+ "<!-- remnic:standup:end -->",
165
+ "",
166
+ "user content that must survive",
167
+ "",
168
+ "<!-- remnic:timeline:start -->",
169
+ "stale recap",
170
+ "<!-- remnic:weekly:end -->",
171
+ "more user content",
172
+ "<!-- remnic:timeline:end -->",
173
+ "",
174
+ ].join("\n");
175
+ writeFileSync(notePath, note, "utf8");
176
+
177
+ const status = publishVaultNote({
178
+ vaultPath: vault,
179
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
180
+ date: DATE,
181
+ sections: [{ name: "timeline", content: "fresh recap" }],
182
+ });
183
+
184
+ assert.equal(status.results[0]?.outcome, "skipped");
185
+ assert.match(status.results[0]?.reason ?? "", /name_mismatch:timeline:weekly/);
186
+ const after = readFileSync(notePath, "utf8");
187
+ assert.equal(after, note, "every byte of the note is preserved");
188
+ assert.ok(after.includes("user content that must survive"));
189
+ assert.ok(after.includes("more user content"));
190
+ assert.ok(after.includes("<!-- remnic:weekly:end -->"));
191
+ });
192
+
193
+ test("frontmatter updates only match top-level keys, never nested user metadata", () => {
194
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
195
+ const notePath = path.join(vault, `${DATE}.md`);
196
+ const note = [
197
+ "---",
198
+ "tags: daily-notes",
199
+ "custom:",
200
+ " remnic_focus: old",
201
+ "---",
202
+ "<!-- remnic:timeline:start -->",
203
+ "stale recap",
204
+ "<!-- remnic:timeline:end -->",
205
+ "",
206
+ ].join("\n");
207
+ writeFileSync(notePath, note, "utf8");
208
+
209
+ const status = publishVaultNote({
210
+ vaultPath: vault,
211
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
212
+ date: DATE,
213
+ sections: [{ name: "timeline", content: "fresh recap", properties: { focus: "new" } }],
214
+ propertiesMode: "frontmatter",
215
+ });
216
+
217
+ assert.equal(status.results[0]?.outcome, "updated");
218
+ const after = readFileSync(notePath, "utf8");
219
+ assert.ok(after.includes(" remnic_focus: old"), "nested user key is untouched");
220
+ assert.ok(after.includes("\nremnic_focus: new"), "top-level key is inserted");
221
+ assert.ok(after.includes("custom:"));
222
+ assert.ok(after.includes("tags: daily-notes"));
223
+ });
224
+
225
+ test("nested start markers (start:A, start:B, end:A, end:B) refuse the whole note", () => {
226
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
227
+ const notePath = path.join(vault, `${DATE}.md`);
228
+ // Marker order start:timeline, start:standup, end:timeline, end:standup.
229
+ // A scan that only looks for a mismatched END accepts end:timeline as a
230
+ // valid close and never sees the orphaned end:standup, so
231
+ // `applyManagedRegion` pairs start:timeline with end:timeline and deletes
232
+ // the standup start marker plus every user byte between them.
233
+ const note = [
234
+ "<!-- remnic:timeline:start -->",
235
+ "stale recap",
236
+ "<!-- remnic:standup:start -->",
237
+ "standup body the user wrote",
238
+ "<!-- remnic:timeline:end -->",
239
+ "user content between the ends",
240
+ "<!-- remnic:standup:end -->",
241
+ "",
242
+ ].join("\n");
243
+ writeFileSync(notePath, note, "utf8");
244
+
245
+ const status = publishVaultNote({
246
+ vaultPath: vault,
247
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
248
+ date: DATE,
249
+ sections: [{ name: "timeline", content: "fresh recap" }],
250
+ });
251
+
252
+ assert.equal(status.results[0]?.outcome, "skipped");
253
+ assert.match(status.results[0]?.reason ?? "", /nested_start:timeline:standup/);
254
+ const after = readFileSync(notePath, "utf8");
255
+ assert.equal(after, note, "every byte of the note is preserved");
256
+ // Each intervening byte the crossed replacement would have eaten.
257
+ assert.ok(after.includes("<!-- remnic:standup:start -->"), "inner start marker survives");
258
+ assert.ok(after.includes("standup body the user wrote"), "inner region body survives");
259
+ assert.ok(after.includes("user content between the ends"), "text between the ends survives");
260
+ assert.ok(after.includes("<!-- remnic:standup:end -->"), "inner end marker survives");
261
+ assert.ok(after.includes("stale recap"), "nothing is published over the stale region");
262
+ });
263
+
264
+ test("an orphan end marker with no open region refuses the whole note", () => {
265
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
266
+ const notePath = path.join(vault, `${DATE}.md`);
267
+ const note = ["user notes", "<!-- remnic:timeline:end -->", "more user notes", ""].join("\n");
268
+ writeFileSync(notePath, note, "utf8");
269
+
270
+ const status = publishVaultNote({
271
+ vaultPath: vault,
272
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
273
+ date: DATE,
274
+ sections: [{ name: "timeline", content: "fresh recap" }],
275
+ });
276
+
277
+ assert.equal(status.results[0]?.outcome, "skipped");
278
+ assert.match(status.results[0]?.reason ?? "", /orphan_end:timeline/);
279
+ assert.equal(readFileSync(notePath, "utf8"), note);
280
+ });
281
+
282
+ test("colon-bearing marker names cannot delete the user bytes between a malformed pair", () => {
283
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
284
+ const notePath = path.join(vault, `${DATE}.md`);
285
+ // Marker order start:Work:Timeline, end:Other:Section, end:Work:Timeline.
286
+ // A scanner that captures the name with `[^:]+?` sees NO markers here, so
287
+ // the malformed sequence passes, and `applyManagedRegion` — which pairs by
288
+ // literal `indexOf` — pairs the start with the LAST end and deletes the
289
+ // orphan end marker plus every user byte between them.
290
+ const note = [
291
+ "<!-- remnic:Work:Timeline:start -->",
292
+ "stale recap",
293
+ "<!-- remnic:Other:Section:end -->",
294
+ "user content that must survive",
295
+ "<!-- remnic:Work:Timeline:end -->",
296
+ "",
297
+ ].join("\n");
298
+ writeFileSync(notePath, note, "utf8");
299
+
300
+ // A colon-bearing section name is refused before the note is opened.
301
+ assert.throws(
302
+ () =>
303
+ publishVaultNote({
304
+ vaultPath: vault,
305
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
306
+ date: DATE,
307
+ sections: [{ name: "Work:Timeline", content: "fresh recap" }],
308
+ }),
309
+ /not a valid region name/,
310
+ );
311
+ assert.equal(readFileSync(notePath, "utf8"), note, "every byte of the note is preserved");
312
+
313
+ // The note's own colon-bearing markers are still parsed and still refuse it,
314
+ // even when the configured section name is legal.
315
+ writeFileSync(
316
+ notePath,
317
+ `${note}<!-- remnic:timeline:start -->\nstale\n<!-- remnic:timeline:end -->\n`,
318
+ "utf8",
319
+ );
320
+ const mixed = readFileSync(notePath, "utf8");
321
+ const status = publishVaultNote({
322
+ vaultPath: vault,
323
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
324
+ date: DATE,
325
+ sections: [{ name: "timeline", content: "fresh recap" }],
326
+ });
327
+
328
+ assert.equal(status.results[0]?.outcome, "skipped");
329
+ assert.match(status.results[0]?.reason ?? "", /name_mismatch:Work:Timeline:Other:Section/);
330
+ const after = readFileSync(notePath, "utf8");
331
+ assert.equal(after, mixed, "every byte of the note is preserved");
332
+ assert.ok(after.includes("user content that must survive"), "intervening user bytes survive");
333
+ assert.ok(after.includes("<!-- remnic:Other:Section:end -->"), "orphan end marker survives");
334
+ assert.ok(after.includes("stale recap"), "nothing is published over the stale region");
335
+ });
336
+
337
+ test("a marker line with no parseable name refuses the whole note", () => {
338
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
339
+ const notePath = path.join(vault, `${DATE}.md`);
340
+ const note = ["<!-- remnic::start -->", "user bytes", "<!-- remnic:timeline:end -->", ""].join("\n");
341
+ writeFileSync(notePath, note, "utf8");
342
+
343
+ const status = publishVaultNote({
344
+ vaultPath: vault,
345
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
346
+ date: DATE,
347
+ sections: [{ name: "timeline", content: "fresh recap" }],
348
+ });
349
+
350
+ assert.equal(status.results[0]?.outcome, "skipped");
351
+ assert.match(status.results[0]?.reason ?? "", /unparsable_marker/);
352
+ assert.equal(readFileSync(notePath, "utf8"), note);
353
+ });
354
+
355
+ test("heading strategy ignores a heading inside a fenced code block", () => {
356
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
357
+ const notePath = path.join(vault, `${DATE}.md`);
358
+ // The only `## Timeline` line lives inside a fenced example. A scanner
359
+ // without fence state accepts it as the owned heading and replaces the rest
360
+ // of the code block plus the human text after it, through EOF.
361
+ const note = [
362
+ "# Daily",
363
+ "",
364
+ "## Notes",
365
+ "",
366
+ "How to publish:",
367
+ "",
368
+ "```md",
369
+ "## Timeline",
370
+ "- example card inside the fence",
371
+ "~~~",
372
+ "```",
373
+ "",
374
+ "human paragraph after the fence",
375
+ "",
376
+ ].join("\n");
377
+ writeFileSync(notePath, note, "utf8");
378
+
379
+ const status = publishVaultNote({
380
+ vaultPath: vault,
381
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
382
+ date: DATE,
383
+ strategy: "heading",
384
+ sections: [{ name: "Timeline", content: "fresh recap" }],
385
+ });
386
+
387
+ assert.equal(status.results[0]?.outcome, "skipped");
388
+ assert.equal(status.results[0]?.reason, "no_heading");
389
+ assert.equal(readFileSync(notePath, "utf8"), note, "every byte of the note is preserved");
390
+ });
391
+
392
+ test("a fenced heading does not terminate the owned section early", () => {
393
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
394
+ const notePath = path.join(vault, `${DATE}.md`);
395
+ // The owned `## Timeline` body contains a fenced `## Notes`, and the real
396
+ // `## Notes` follows the fence. A scanner without fence state ends the
397
+ // owned section at the FENCED heading, so the replacement lands mid-fence
398
+ // and leaves an unterminated code block plus an orphaned fence tail.
399
+ const note = [
400
+ "## Timeline",
401
+ "stale recap",
402
+ "",
403
+ "```",
404
+ "## Notes",
405
+ "fenced example line",
406
+ "```",
407
+ "",
408
+ "## Notes",
409
+ "",
410
+ "human paragraph",
411
+ "",
412
+ ].join("\n");
413
+ writeFileSync(notePath, note, "utf8");
414
+
415
+ const status = publishVaultNote({
416
+ vaultPath: vault,
417
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
418
+ date: DATE,
419
+ strategy: "heading",
420
+ sections: [{ name: "Timeline", content: "fresh recap" }],
421
+ });
422
+
423
+ assert.equal(status.results[0]?.outcome, "updated");
424
+ const after = readFileSync(notePath, "utf8");
425
+ // The whole fenced block sat inside the region Remnic owns, so it is
426
+ // replaced as one unit; the human content after the real heading is intact.
427
+ assert.equal(after, ["## Timeline", "fresh recap", "## Notes", "", "human paragraph", ""].join("\n"));
428
+ assert.ok(!after.includes("```"), "no unterminated fence is left behind");
429
+ assert.ok(after.includes("human paragraph"), "content after the real heading survives");
430
+ });
431
+
432
+ test("heading strategy republish is idempotent when the recap has its own headings", () => {
433
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
434
+ const notePath = path.join(vault, `${DATE}.md`);
435
+ // The persisted journal recap begins with `# Journal` and carries `##`
436
+ // sections. Untouched, those headings terminate the managed region on
437
+ // the next publish, so every republish leaves the previous copy behind.
438
+ const note = [
439
+ "# Daily",
440
+ "",
441
+ "## Timeline",
442
+ "",
443
+ "stale body",
444
+ "",
445
+ "## Notes",
446
+ "",
447
+ "human paragraph",
448
+ "",
449
+ ].join("\n");
450
+ writeFileSync(notePath, note, "utf8");
451
+ const recap = [
452
+ "# Journal — 2026-08-21 (UTC)",
453
+ "",
454
+ "## Categories",
455
+ "",
456
+ "- code review: 42m",
457
+ ].join("\n");
458
+ const publish = () =>
459
+ publishVaultNote({
460
+ vaultPath: vault,
461
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
462
+ date: DATE,
463
+ strategy: "heading",
464
+ sections: [{ name: "Timeline", content: recap }],
465
+ });
466
+
467
+ publish();
468
+ const first = readFileSync(notePath, "utf8");
469
+ const second = publish();
470
+ const afterSecond = readFileSync(notePath, "utf8");
471
+ const third = publish();
472
+
473
+ assert.equal(second.results[0]?.outcome, "unchanged");
474
+ assert.equal(third.results[0]?.outcome, "unchanged");
475
+ assert.equal(afterSecond, first, "second publish is byte-identical to the first");
476
+ assert.equal(readFileSync(notePath, "utf8"), first, "third publish is byte-identical too");
477
+ assert.equal((first.match(/Journal — 2026-08-21/g) ?? []).length, 1, "exactly one copy of the recap heading");
478
+ assert.ok(first.includes("### Journal — 2026-08-21 (UTC)"), "recap heading is demoted under the owning heading");
479
+ assert.ok(first.includes("#### Categories"), "recap sections are demoted one level deeper");
480
+ assert.ok(first.includes("## Notes\n\nhuman paragraph"), "the next real section survives");
481
+ });
482
+
483
+ test("a 1-3 space indented heading is a real boundary and survives byte-for-byte", () => {
484
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
485
+ const notePath = path.join(vault, `${DATE}.md`);
486
+ // CommonMark permits 1-3 columns before the hashes, so ` ## Human Notes`
487
+ // terminates the owned section; a fourth column is an indented code block,
488
+ // which is owned content of the recap and replaced with it.
489
+ const note = [
490
+ "## Timeline",
491
+ "stale recap",
492
+ "",
493
+ " ## Not a heading (code)",
494
+ "",
495
+ " ## Human Notes",
496
+ "",
497
+ "human paragraph",
498
+ "",
499
+ ].join("\n");
500
+ writeFileSync(notePath, note, "utf8");
501
+
502
+ const status = publishVaultNote({
503
+ vaultPath: vault,
504
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
505
+ date: DATE,
506
+ strategy: "heading",
507
+ sections: [{ name: "Timeline", content: "fresh recap" }],
508
+ });
509
+
510
+ assert.equal(status.results[0]?.outcome, "updated");
511
+ assert.equal(
512
+ readFileSync(notePath, "utf8"),
513
+ ["## Timeline", "fresh recap", " ## Human Notes", "", "human paragraph", ""].join("\n"),
514
+ );
515
+ });
516
+
517
+ test("createMissingNotes creates the confined nested parent hierarchy for a new note", () => {
518
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
519
+ writeFileSync(path.join(vault, "daily.md"), "# {yyyy}-{MM}-{dd}\n", "utf8");
520
+
521
+ const status = publishVaultNote({
522
+ vaultPath: vault,
523
+ notePathTemplate: "Daily/{yyyy}/{MM}/{dd}.md",
524
+ date: DATE,
525
+ sections: [{ name: "timeline", content: "- card-a: review (42m)" }],
526
+ createMissingNotes: true,
527
+ noteTemplate: "daily.md",
528
+ });
529
+
530
+ assert.equal(status.results[0]?.outcome, "updated");
531
+ const notePath = path.join(vault, "Daily", "2026", "08", "21.md");
532
+ const text = readFileSync(notePath, "utf8");
533
+ assert.match(text, /- card-a: review \(42m\)/);
534
+ assert.match(text, /remnic:timeline:start/);
535
+ });
536
+
537
+ test("a `..`-prefixed in-vault name publishes; real parent traversal still refuses", () => {
538
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
539
+ // `..notes` is a literal directory name, not the parent directory.
540
+ const notesDir = path.join(vault, "..notes");
541
+ mkdirSync(notesDir);
542
+ const notePath = path.join(notesDir, `${DATE}.md`);
543
+ writeFileSync(notePath, "<!-- remnic:timeline:start -->\nstale\n<!-- remnic:timeline:end -->\n", "utf8");
544
+
545
+ const status = publishVaultNote({
546
+ vaultPath: vault,
547
+ notePathTemplate: "..notes/{yyyy}-{MM}-{dd}.md",
548
+ date: DATE,
549
+ sections: [{ name: "timeline", content: "- card-a: review (42m)" }],
550
+ });
551
+ assert.equal(status.results[0]?.outcome, "updated");
552
+ assert.match(readFileSync(notePath, "utf8"), /- card-a: review \(42m\)/);
553
+
554
+ writeFileSync(path.join(vault, "tpl.md"), "# {yyyy}-{MM}-{dd}\n", "utf8");
555
+ const refused = publishVaultNote({
556
+ vaultPath: vault,
557
+ notePathTemplate: "..notes/{yyyy}-{MM}-{dd}-new.md",
558
+ date: DATE,
559
+ sections: [{ name: "timeline", content: "- card-a: review (42m)" }],
560
+ createMissingNotes: true,
561
+ noteTemplate: "../tpl.md",
562
+ });
563
+ assert.equal(refused.results[0]?.outcome, "error");
564
+ assert.equal(refused.results[0]?.reason, "template_escape");
565
+ assert.equal(existsSync(path.join(notesDir, `${DATE}-new.md`)), false, "no note created from an escaping template");
566
+ });
567
+
568
+ test("an escaping noteTemplate refuses and creates no directories", () => {
569
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
570
+
571
+ const status = publishVaultNote({
572
+ vaultPath: vault,
573
+ notePathTemplate: "Daily/{yyyy}/{MM}/{dd}.md",
574
+ date: DATE,
575
+ sections: [{ name: "timeline", content: "must not land" }],
576
+ createMissingNotes: true,
577
+ noteTemplate: "../outside-vault.md",
578
+ });
579
+
580
+ assert.equal(status.results[0]?.outcome, "error");
581
+ assert.equal(status.results[0]?.reason, "template_escape");
582
+ assert.ok(!existsSync(path.join(vault, "Daily")), "no parent directory was created");
583
+ assert.ok(!existsSync(path.join(vault, "Daily", "2026", "08", "21.md")));
584
+ });
585
+
586
+
587
+ test("marker strategy ignores a marker pair inside a fenced code block", () => {
588
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
589
+ const notePath = path.join(vault, `${DATE}.md`);
590
+ // The fenced example carries a complete `timeline` pair BEFORE the real
591
+ // region. A scanner without fence state pairs the fenced markers first
592
+ // and replaces the example's contents while reporting a successful publish.
593
+ const fencedExample = [
594
+ "```markdown",
595
+ "## How to publish",
596
+ "<!-- remnic:timeline:start -->",
597
+ "replace this sample text to see it work",
598
+ "<!-- remnic:timeline:end -->",
599
+ "```",
600
+ ].join("\n");
601
+ const note = [
602
+ "---",
603
+ "title: daily",
604
+ "---",
605
+ "",
606
+ "Example from the docs:",
607
+ "",
608
+ fencedExample,
609
+ "",
610
+ "Real region:",
611
+ "",
612
+ "<!-- remnic:timeline:start -->",
613
+ "stale recap",
614
+ "<!-- remnic:timeline:end -->",
615
+ "",
616
+ ].join("\n");
617
+ writeFileSync(notePath, note, "utf8");
618
+
619
+ const status = publishVaultNote({
620
+ vaultPath: vault,
621
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
622
+ date: DATE,
623
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
624
+ });
625
+
626
+ assert.equal(status.results[0]?.outcome, "updated");
627
+ const after = readFileSync(notePath, "utf8");
628
+ assert.ok(after.includes(fencedExample), "the fenced example is byte-identical");
629
+ assert.ok(!after.includes("stale recap"), "the real region was replaced");
630
+ assert.ok(after.includes("- card-a: code review (42m)"));
631
+ });
632
+
633
+ test("marker strategy ignores a marker pair inside a four-space-indented code block", () => {
634
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
635
+ const notePath = path.join(vault, `${DATE}.md`);
636
+ // The indented example carries a complete `timeline` pair BEFORE the real
637
+ // region. A scanner that trims a row before classifying it erases the
638
+ // indentation, pairs the example's markers first, and replaces the
639
+ // example's contents while reporting a successful publish (issue #1985).
640
+ const indentedExample = [
641
+ " <!-- remnic:timeline:start -->",
642
+ " replace this sample text to see it work",
643
+ " <!-- remnic:timeline:end -->",
644
+ ].join("\n");
645
+ const note = [
646
+ "---",
647
+ "title: daily",
648
+ "---",
649
+ "",
650
+ "Example from the docs:",
651
+ "",
652
+ indentedExample,
653
+ "",
654
+ "Real region:",
655
+ "",
656
+ "<!-- remnic:timeline:start -->",
657
+ "stale recap",
658
+ "<!-- remnic:timeline:end -->",
659
+ "",
660
+ ].join("\n");
661
+ writeFileSync(notePath, note, "utf8");
662
+
663
+ const status = publishVaultNote({
664
+ vaultPath: vault,
665
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
666
+ date: DATE,
667
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
668
+ });
669
+
670
+ assert.equal(status.results[0]?.outcome, "updated");
671
+ const after = readFileSync(notePath, "utf8");
672
+ assert.ok(after.includes(indentedExample), "the indented example is byte-identical");
673
+ assert.ok(!after.includes("stale recap"), "the real region was replaced");
674
+ assert.ok(after.includes("- card-a: code review (42m)"));
675
+ });
676
+
677
+ test("marker strategy ignores a marker pair inside a tab-indented code block", () => {
678
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
679
+ const notePath = path.join(vault, `${DATE}.md`);
680
+ // One tab is a full four-column indent (CommonMark tab handling), so the
681
+ // tab-indented example is code exactly like the four-space variant.
682
+ const indentedExample = [
683
+ "\t<!-- remnic:timeline:start -->",
684
+ "\treplace this sample text to see it work",
685
+ "\t<!-- remnic:timeline:end -->",
686
+ ].join("\n");
687
+ const note = [
688
+ "Example from the docs:",
689
+ "",
690
+ indentedExample,
691
+ "",
692
+ "<!-- remnic:timeline:start -->",
693
+ "stale recap",
694
+ "<!-- remnic:timeline:end -->",
695
+ "",
696
+ ].join("\n");
697
+ writeFileSync(notePath, note, "utf8");
698
+
699
+ const status = publishVaultNote({
700
+ vaultPath: vault,
701
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
702
+ date: DATE,
703
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
704
+ });
705
+
706
+ assert.equal(status.results[0]?.outcome, "updated");
707
+ const after = readFileSync(notePath, "utf8");
708
+ assert.ok(after.includes(indentedExample), "the tab-indented example is byte-identical");
709
+ assert.ok(!after.includes("stale recap"), "the real region was replaced");
710
+ assert.ok(after.includes("- card-a: code review (42m)"));
711
+ });
712
+
713
+ test("a marker pair indented under a list item is skipped, not published", () => {
714
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
715
+ const notePath = path.join(vault, `${DATE}.md`);
716
+ // CommonMark measures indentation relative to the containing block, so a
717
+ // marker indented under a list item CAN be live content rather than code.
718
+ // Distinguishing that needs full list tracking; this fix classifies any
719
+ // indent >= 4 as code instead. The ambiguity resolves to "skip": a refused
720
+ // publish is recoverable, an overwritten region is not.
721
+ const note = [
722
+ "- morning routine",
723
+ "",
724
+ " <!-- remnic:timeline:start -->",
725
+ " stale recap",
726
+ " <!-- remnic:timeline:end -->",
727
+ "",
728
+ ].join("\n");
729
+ writeFileSync(notePath, note, "utf8");
730
+
731
+ const status = publishVaultNote({
732
+ vaultPath: vault,
733
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
734
+ date: DATE,
735
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
736
+ });
737
+
738
+ assert.equal(status.results[0]?.outcome, "skipped");
739
+ assert.equal(status.results[0]?.reason, "no_marker");
740
+ assert.equal(readFileSync(notePath, "utf8"), note, "the ambiguous region is left untouched");
741
+ });
742
+
743
+ test("insertUnderHeading never inserts into a fenced heading", () => {
744
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
745
+ const notePath = path.join(vault, `${DATE}.md`);
746
+ // The only `## Timeline` heading lives inside a fenced example. An
747
+ // insertion scanner without fence state treats it as the target and
748
+ // writes a live managed region into the user's code block.
749
+ const note = [
750
+ "# Daily",
751
+ "",
752
+ "```markdown",
753
+ "## Timeline",
754
+ "- sample entry inside the fence",
755
+ "```",
756
+ "",
757
+ ].join("\n");
758
+ writeFileSync(notePath, note, "utf8");
759
+
760
+ const status = publishVaultNote({
761
+ vaultPath: vault,
762
+ notePathTemplate: "{yyyy}-{MM}-{dd}.md",
763
+ date: DATE,
764
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
765
+ insertUnderHeading: "Timeline",
766
+ });
767
+
768
+ assert.equal(status.results[0]?.outcome, "skipped");
769
+ assert.equal(status.results[0]?.reason, "no_marker");
770
+ assert.equal(readFileSync(notePath, "utf8"), note, "nothing is inserted into the fence");
771
+ });
772
+
773
+ test("a symlinked destination note is refused, not replaced", () => {
774
+ const vault = mkdtempSync(path.join(tmpdir(), "remnic-vault-publisher-"));
775
+ const target = ["<!-- remnic:timeline:start -->", "stale recap", "<!-- remnic:timeline:end -->", ""].join("\n");
776
+ const targetPath = path.join(vault, "real-note.md");
777
+ const linkPath = path.join(vault, "link-note.md");
778
+ writeFileSync(targetPath, target, "utf8");
779
+ symlinkSync(targetPath, linkPath);
780
+
781
+ const status = publishVaultNote({
782
+ vaultPath: vault,
783
+ notePathTemplate: "link-note.md",
784
+ date: DATE,
785
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
786
+ });
787
+
788
+ assert.equal(status.results[0]?.outcome, "error");
789
+ assert.equal(status.results[0]?.reason, "symlinked_note");
790
+ assert.ok(lstatSync(linkPath).isSymbolicLink(), "the symlink still exists");
791
+ assert.equal(readFileSync(targetPath, "utf8"), target, "the target is not left stale");
792
+ });
793
+
794
+ test("publishing preserves a 0600 note's mode across the atomic replace", () => {
795
+ const { vault, notePath } = makeVault();
796
+ chmodSync(notePath, 0o600);
797
+
798
+ const status = publishVaultNote({
799
+ vaultPath: vault,
800
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
801
+ date: DATE,
802
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
803
+ });
804
+
805
+ assert.equal(status.counts.updated, 1);
806
+ assert.equal(
807
+ statSync(notePath).mode & 0o7777,
808
+ 0o600,
809
+ "the replacement inode inherits the note's mode, not 0666 & ~umask",
810
+ );
811
+ });
812
+
813
+ test("a partial publish writes only the applied section's properties and surfaces the skipped one", () => {
814
+ const { vault, notePath } = makeVault();
815
+ const status = publishVaultNote({
816
+ vaultPath: vault,
817
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
818
+ date: DATE,
819
+ propertiesMode: "frontmatter",
820
+ sections: [
821
+ { name: "timeline", content: "fresh recap", properties: { focus_minutes: "120" } },
822
+ // No `remnic:standup` marker pair exists in the fixture note.
823
+ { name: "standup", content: "standup body", properties: { standup_items: "3" } },
824
+ ],
825
+ });
826
+
827
+ // The omission is surfaced: the file-level outcome is `updated` for the
828
+ // applied section AND a `skipped` row names the section that did not land.
829
+ assert.equal(status.counts.updated, 1);
830
+ assert.equal(status.counts.skipped, 1);
831
+ const skippedRow = status.results.find((row) => row.outcome === "skipped");
832
+ assert.equal(skippedRow?.reason, "no_marker");
833
+
834
+ const after = readFileSync(notePath, "utf8");
835
+ assert.match(after, /remnic_focus_minutes: 120/, "the applied section's properties are written");
836
+ assert.doesNotMatch(after, /remnic_standup_items/, "the skipped section's properties never reach the file");
837
+ assert.doesNotMatch(after, /standup body/, "the skipped section's body never reaches the file");
838
+ });
839
+
840
+ test("a concurrent editor save between read and rename aborts the publish and keeps the user's bytes", () => {
841
+ const { vault, notePath } = makeVault();
842
+ // The editor's autosave touches only human text OUTSIDE the managed region.
843
+ const editorSaved = NOTE.replace("trailing human text", "trailing human text (editor autosave)");
844
+
845
+ // Simulate Obsidian's autosave landing inside the publish's read→render→
846
+ // replace window: the hook runs after the note was read and rendered but
847
+ // before the pre-replace verification inside writeAtomic.
848
+ setVaultPublisherTestHooks({
849
+ beforeReplaceVerify: (dest) => {
850
+ if (path.resolve(dest) === path.resolve(notePath)) {
851
+ writeFileSync(notePath, editorSaved, "utf8");
852
+ }
853
+ },
854
+ });
855
+ try {
856
+ const status = publishVaultNote({
857
+ vaultPath: vault,
858
+ notePathTemplate: "Daily Notes/{yyyy}/{MM}/{yyyy}-{MM}-{dd}.md",
859
+ date: DATE,
860
+ sections: [{ name: "timeline", content: "- card-a: code review (42m)" }],
861
+ });
862
+
863
+ assert.equal(status.results[0]?.outcome, "error", "must not report `updated`");
864
+ assert.equal(status.results[0]?.reason, "concurrent_write");
865
+ assert.equal(status.counts.updated, 0);
866
+ assert.equal(status.counts.error, 1);
867
+
868
+ // The concurrent edit survives byte-for-byte — including the stale
869
+ // managed region: refusal keeps the user's bytes, it does not merge.
870
+ const after = readFileSync(notePath, "utf8");
871
+ assert.equal(after, editorSaved);
872
+ assert.match(after, /editor autosave/);
873
+ assert.doesNotMatch(after, /card-a/);
874
+
875
+ // The aborted replace leaves no temp litter next to the note.
876
+ const litter = readdirSync(path.dirname(notePath)).filter((name) => name.includes(".remnic-vault-"));
877
+ assert.deepEqual(litter, []);
878
+ } finally {
879
+ setVaultPublisherTestHooks(null);
880
+ }
881
+ });