@remnic/core 9.69.12 → 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 (357) 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 +12 -12
  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-service-Baz5xW5X.d.ts → access-service-Bb2Fykzc.d.ts} +7 -7
  42. package/dist/access-service-helpers.d.ts +11 -11
  43. package/dist/access-service.d.ts +11 -11
  44. package/dist/access-service.js +4 -4
  45. package/dist/access-surface-catalog.d.ts +11 -11
  46. package/dist/access-surface-catalog.js +6 -6
  47. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  48. package/dist/action-confidence.d.ts +1 -1
  49. package/dist/active-memory-bridge.d.ts +1 -1
  50. package/dist/active-recall.d.ts +1 -1
  51. package/dist/active-recall.js +4 -4
  52. package/dist/ambient-provenance.d.ts +1 -1
  53. package/dist/artifact-search.d.ts +1 -1
  54. package/dist/behavior-learner.d.ts +1 -1
  55. package/dist/behavior-signals.d.ts +1 -1
  56. package/dist/bootstrap.d.ts +9 -9
  57. package/dist/briefing-window.d.ts +2 -2
  58. package/dist/briefing.d.ts +2 -2
  59. package/dist/buffer-surprise-report.d.ts +1 -1
  60. package/dist/buffer-turn-helpers.d.ts +1 -1
  61. package/dist/buffer.d.ts +2 -2
  62. package/dist/buffer.js +4 -4
  63. package/dist/bulk-import/index.d.ts +3 -3
  64. package/dist/calibration.d.ts +1 -1
  65. package/dist/capabilities.d.ts +1 -1
  66. package/dist/{catalog-9X8WpZNO.d.ts → catalog-DjTLVOYp.d.ts} +1 -1
  67. package/dist/causal-behavior.d.ts +1 -1
  68. package/dist/causal-consolidation.d.ts +1 -1
  69. package/dist/causal-trajectory-graph.d.ts +1 -1
  70. package/dist/{chunk-TEZ2HTC7.js → chunk-33JKYLPX.js} +2 -2
  71. package/dist/{chunk-MNH2GM4O.js → chunk-3BS2OGDT.js} +6 -19
  72. package/dist/{chunk-MNH2GM4O.js.map → chunk-3BS2OGDT.js.map} +1 -1
  73. package/dist/{chunk-5PTS43CH.js → chunk-4LG4CPOC.js} +2 -2
  74. package/dist/{chunk-Z33VUMKY.js → chunk-5H2G5X4R.js} +2 -2
  75. package/dist/{chunk-XFPHV65N.js → chunk-5JQV4T5Q.js} +2 -2
  76. package/dist/{chunk-OASGWNYL.js → chunk-63I7LMLG.js} +2 -2
  77. package/dist/{chunk-J3NGQ5SC.js → chunk-7GSSHAXC.js} +2 -2
  78. package/dist/{chunk-X4FIZAVQ.js → chunk-7JV4JST3.js} +854 -42
  79. package/dist/chunk-7JV4JST3.js.map +1 -0
  80. package/dist/{chunk-VHAKBUBD.js → chunk-B64IVEOH.js} +2 -2
  81. package/dist/{chunk-R7FXPG4T.js → chunk-C25N5HWX.js} +2 -2
  82. package/dist/{chunk-DH3V7RCQ.js → chunk-F2ERDKY2.js} +7 -7
  83. package/dist/{chunk-LJNLLNEM.js → chunk-HODFGQGT.js} +2 -2
  84. package/dist/{chunk-B75VCEX5.js → chunk-K2IJH4D3.js} +2 -2
  85. package/dist/{chunk-227WJVPY.js → chunk-KLSPKMIO.js} +2 -2
  86. package/dist/{chunk-5P2KNFJZ.js → chunk-MSDIETFC.js} +3 -3
  87. package/dist/{chunk-HFRGCNFY.js → chunk-OAKATRAN.js} +2 -2
  88. package/dist/chunk-OGUKSLYZ.js +560 -0
  89. package/dist/chunk-OGUKSLYZ.js.map +1 -0
  90. package/dist/{chunk-M6L7HI7R.js → chunk-P6FHFTZ7.js} +16 -1
  91. package/dist/chunk-P6FHFTZ7.js.map +1 -0
  92. package/dist/{chunk-LWBSEYW3.js → chunk-PAWIY5OQ.js} +5 -5
  93. package/dist/{chunk-3ITTF7YH.js → chunk-QVI63XSO.js} +90 -383
  94. package/dist/chunk-QVI63XSO.js.map +1 -0
  95. package/dist/{chunk-6TOWUN2R.js → chunk-RE3H6Y2X.js} +2 -2
  96. package/dist/{chunk-V3HUBF7T.js → chunk-S35IIP5P.js} +2 -2
  97. package/dist/{chunk-3YKOFCKP.js → chunk-SXHBZXHK.js} +2 -2
  98. package/dist/{chunk-7NCUHD5U.js → chunk-VAMZYOOV.js} +2 -2
  99. package/dist/{chunk-PGCZLHRB.js → chunk-W272SWN7.js} +3 -3
  100. package/dist/{chunk-5HO3QBAD.js → chunk-W6GEMUZM.js} +2 -2
  101. package/dist/{chunk-UCNQV43K.js → chunk-XQYTSZW3.js} +2 -2
  102. package/dist/{chunk-GR4C3FHT.js → chunk-ZKZCJRRO.js} +2 -2
  103. package/dist/{cli-CMtkGBdz.d.ts → cli-DXXy60A4.d.ts} +5 -5
  104. package/dist/cli.d.ts +13 -13
  105. package/dist/cli.js +21 -21
  106. package/dist/coding/export-okf-codegraph.d.ts +1 -1
  107. package/dist/coding/pre-action-gate.d.ts +1 -1
  108. package/dist/compounding/engine.d.ts +2 -2
  109. package/dist/compounding/preference-consolidator.d.ts +1 -1
  110. package/dist/compression-optimizer.d.ts +1 -1
  111. package/dist/config.d.ts +1 -1
  112. package/dist/config.js +4 -4
  113. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  114. package/dist/connectors/codex-materialize.d.ts +1 -1
  115. package/dist/connectors/index.d.ts +1 -1
  116. package/dist/consolidation-provenance-check.d.ts +2 -2
  117. package/dist/consolidation-undo.d.ts +2 -2
  118. package/dist/contradiction/index.d.ts +3 -3
  119. package/dist/converge-config.d.ts +1 -1
  120. package/dist/convergence-refresh.d.ts +2 -2
  121. package/dist/conversation-index/backend.d.ts +1 -1
  122. package/dist/conversation-index/chunker.d.ts +1 -1
  123. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  124. package/dist/conversation-index/indexer.d.ts +1 -1
  125. package/dist/conversation-index/search.d.ts +1 -1
  126. package/dist/corpus-watermark.d.ts +1 -1
  127. package/dist/cross-lingual-recall.d.ts +1 -1
  128. package/dist/day-summary.d.ts +1 -1
  129. package/dist/deep-recall-http-glue.d.ts +11 -11
  130. package/dist/deep-recall-mcp-tools.d.ts +11 -11
  131. package/dist/deep-recall-policy-llm.d.ts +1 -1
  132. package/dist/deep-recall-seeds.d.ts +1 -1
  133. package/dist/delinearize.d.ts +1 -1
  134. package/dist/dependency-propagation-config.d.ts +1 -1
  135. package/dist/{dependency-propagation-delivery-BZGUecZf.d.ts → dependency-propagation-delivery-37JdQv_3.d.ts} +1 -1
  136. package/dist/direct-answer-wiring.d.ts +1 -1
  137. package/dist/direct-answer.d.ts +1 -1
  138. package/dist/embedding-fallback.d.ts +1 -1
  139. package/dist/enrichment/index.d.ts +1 -1
  140. package/dist/entity-origin-fields.d.ts +1 -1
  141. package/dist/entity-retrieval.d.ts +2 -2
  142. package/dist/entity-schema.d.ts +1 -1
  143. package/dist/episodic-context.d.ts +1 -1
  144. package/dist/eval-trajectory-contracts.d.ts +1 -1
  145. package/dist/eval-trajectory-store.d.ts +1 -1
  146. package/dist/eval-trajectory.d.ts +1 -1
  147. package/dist/explicit-capture.d.ts +9 -9
  148. package/dist/external-wiki-access.d.ts +11 -11
  149. package/dist/external-wiki-access.js +5 -5
  150. package/dist/external-wiki-collection-registration.d.ts +1 -1
  151. package/dist/external-wiki-collection.d.ts +1 -1
  152. package/dist/external-wiki-mcp-tools.d.ts +11 -11
  153. package/dist/extraction-error-classification.d.ts +1 -1
  154. package/dist/extraction-faithfulness.d.ts +1 -1
  155. package/dist/extraction-judge-telemetry.d.ts +1 -1
  156. package/dist/extraction-judge-training.d.ts +1 -1
  157. package/dist/extraction-judge.d.ts +1 -1
  158. package/dist/extraction-liveness.d.ts +1 -1
  159. package/dist/extraction-normalization.d.ts +1 -1
  160. package/dist/extraction-prompt.d.ts +1 -1
  161. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  162. package/dist/extraction-source-grounding.d.ts +1 -1
  163. package/dist/extraction.d.ts +1 -1
  164. package/dist/extraction.js +5 -5
  165. package/dist/fallback-llm.d.ts +1 -1
  166. package/dist/graph-dashboard-diff.d.ts +1 -1
  167. package/dist/graph-dashboard-key.d.ts +1 -1
  168. package/dist/graph-dashboard-parser.d.ts +1 -1
  169. package/dist/graph-edge-reinforcement.d.ts +1 -1
  170. package/dist/graph-path-reconstruction.d.ts +1 -1
  171. package/dist/graph-path-scoring.d.ts +1 -1
  172. package/dist/graph-snapshot.d.ts +1 -1
  173. package/dist/graph.d.ts +1 -1
  174. package/dist/importance.d.ts +1 -1
  175. package/dist/importers/index.d.ts +1 -1
  176. package/dist/in-flight-reads.d.ts +1 -1
  177. package/dist/{index-B4iTvT-w.d.ts → index-DquhYqsc.d.ts} +2 -2
  178. package/dist/index.d.ts +200 -62
  179. package/dist/index.js +59 -37
  180. package/dist/intent.d.ts +1 -1
  181. package/dist/lcm/engine.d.ts +1 -1
  182. package/dist/lcm/index.d.ts +1 -1
  183. package/dist/lcm/tools.d.ts +1 -1
  184. package/dist/lifecycle.d.ts +1 -1
  185. package/dist/live-connectors-runner.d.ts +1 -1
  186. package/dist/local-llm.d.ts +1 -1
  187. package/dist/local-model-endpoint.d.ts +1 -1
  188. package/dist/location/index.d.ts +3 -3
  189. package/dist/location/index.js +5 -5
  190. package/dist/maintenance/memory-governance.d.ts +1 -1
  191. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  192. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  193. package/dist/{maintenance-BBpNqEeJ.d.ts → maintenance-CizZOycr.d.ts} +3 -3
  194. package/dist/mcp-memory-inspector-app.d.ts +11 -11
  195. package/dist/memory-action-policy.d.ts +1 -1
  196. package/dist/memory-cache.d.ts +1 -1
  197. package/dist/memory-controller.d.ts +1 -1
  198. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  199. package/dist/memory-projection-store.d.ts +1 -1
  200. package/dist/memory-provenance.d.ts +1 -1
  201. package/dist/memory-snapshot.d.ts +1 -1
  202. package/dist/memory-subject-cli.d.ts +11 -11
  203. package/dist/memory-subject.d.ts +11 -11
  204. package/dist/memory-worth-outcomes.d.ts +2 -2
  205. package/dist/models-json.d.ts +1 -1
  206. package/dist/namespaces/migrate.d.ts +3 -3
  207. package/dist/namespaces/principal.d.ts +1 -1
  208. package/dist/namespaces/search.d.ts +1 -1
  209. package/dist/namespaces/storage.d.ts +3 -3
  210. package/dist/native-knowledge.d.ts +1 -1
  211. package/dist/offline-sync-impression-drain.d.ts +1 -1
  212. package/dist/operator-doctor-corpus.d.ts +1 -1
  213. package/dist/operator-doctor-replica.d.ts +1 -1
  214. package/dist/operator-toolkit.d.ts +3 -3
  215. package/dist/operator-toolkit.js +5 -5
  216. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  217. package/dist/orchestration/maintenance.d.ts +4 -4
  218. package/dist/orchestration/maintenance.js +6 -6
  219. package/dist/{orchestrator-DglY1H0w.d.ts → orchestrator-DqLcuNMP.d.ts} +8 -8
  220. package/dist/orchestrator.d.ts +9 -9
  221. package/dist/orchestrator.js +28 -28
  222. package/dist/patterns-cli.d.ts +1 -1
  223. package/dist/{pipeline-C7qRsy6g.d.ts → pipeline-DbJpmcRu.d.ts} +1 -1
  224. package/dist/policy-runtime.d.ts +1 -1
  225. package/dist/proactive-contention.d.ts +1 -1
  226. package/dist/provenance.d.ts +1 -1
  227. package/dist/{public-http-CU-Cwwdb.d.ts → public-http-NQ3UUp6W.d.ts} +1 -1
  228. package/dist/{qmd-YRLTa3AE.d.ts → qmd-DA_7fbSz.d.ts} +1 -1
  229. package/dist/qmd-preflight.d.ts +2 -2
  230. package/dist/qmd-recall-cache.d.ts +1 -1
  231. package/dist/qmd.d.ts +2 -2
  232. package/dist/recall-concurrency-config.d.ts +1 -1
  233. package/dist/recall-disclosure-escalation.d.ts +1 -1
  234. package/dist/recall-explain-renderer.d.ts +1 -1
  235. package/dist/recall-memory-map.d.ts +1 -1
  236. package/dist/recall-navigate-access.d.ts +1 -1
  237. package/dist/recall-navigate-disclosure.d.ts +1 -1
  238. package/dist/recall-navigate.d.ts +1 -1
  239. package/dist/recall-planner-llm.d.ts +1 -1
  240. package/dist/recall-state.d.ts +1 -1
  241. package/dist/recall-tag-filter.d.ts +1 -1
  242. package/dist/recall-timings.d.ts +1 -1
  243. package/dist/recall-xray-cli.d.ts +1 -1
  244. package/dist/recall-xray-renderer.d.ts +1 -1
  245. package/dist/recall-xray.d.ts +1 -1
  246. package/dist/reconcile/cursor.d.ts +1 -1
  247. package/dist/reconcile/manifest.d.ts +1 -1
  248. package/dist/reconcile/plan.d.ts +1 -1
  249. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  250. package/dist/replay/normalizers/claude.d.ts +1 -1
  251. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  252. package/dist/replay/normalizers/shared.d.ts +1 -1
  253. package/dist/replay/runner.d.ts +1 -1
  254. package/dist/replay/types.d.ts +1 -1
  255. package/dist/replica-divergence.d.ts +1 -1
  256. package/dist/replica-peers-config.d.ts +1 -1
  257. package/dist/resolve-auth-token.d.ts +1 -1
  258. package/dist/resume-bundles.js +5 -5
  259. package/dist/retrieval-agents.d.ts +2 -2
  260. package/dist/retrieval-tiers.d.ts +1 -1
  261. package/dist/routing/engine.d.ts +1 -1
  262. package/dist/routing/store.d.ts +1 -1
  263. package/dist/salvage-envelope.d.ts +1 -1
  264. package/dist/{scope-profiles-BVvqVzJX.d.ts → scope-profiles-BpiWKZzx.d.ts} +1 -1
  265. package/dist/search/embed-helper.d.ts +1 -1
  266. package/dist/search/factory.d.ts +1 -1
  267. package/dist/search/index.d.ts +1 -1
  268. package/dist/search/lancedb-backend.d.ts +1 -1
  269. package/dist/search/meilisearch-backend.d.ts +1 -1
  270. package/dist/search/noop-backend.d.ts +1 -1
  271. package/dist/search/orama-backend.d.ts +1 -1
  272. package/dist/search/port.d.ts +1 -1
  273. package/dist/search/remote-backend.d.ts +1 -1
  274. package/dist/{semantic-consolidation-BzCI6Kuu.d.ts → semantic-consolidation-NJpaZpkY.d.ts} +1 -1
  275. package/dist/semantic-consolidation.d.ts +2 -2
  276. package/dist/semantic-rule-verifier.d.ts +1 -1
  277. package/dist/{service-DQ3Gscy5.d.ts → service-DdlZjX1D.d.ts} +2 -2
  278. package/dist/session-observer-bands.d.ts +1 -1
  279. package/dist/session-observer-state.d.ts +1 -1
  280. package/dist/shared-context/manager.d.ts +1 -1
  281. package/dist/signal.d.ts +1 -1
  282. package/dist/source-agent-qualifier.d.ts +1 -1
  283. package/dist/source-agent-qualifier.js +4 -4
  284. package/dist/standup/build.js +2 -2
  285. package/dist/{storage-DmTbQdD0.d.ts → storage-C7BWrpU9.d.ts} +1 -1
  286. package/dist/storage.d.ts +2 -2
  287. package/dist/subject-config.d.ts +1 -1
  288. package/dist/summarizer.d.ts +1 -1
  289. package/dist/summary-snapshot.d.ts +1 -1
  290. package/dist/support-passport/index.d.ts +14 -14
  291. package/dist/support-passport/index.js +7 -7
  292. package/dist/temporal-supersession.d.ts +2 -2
  293. package/dist/temporal-timeline-recall.d.ts +1 -1
  294. package/dist/temporal-validity.d.ts +1 -1
  295. package/dist/threading.d.ts +1 -1
  296. package/dist/tier-migration.d.ts +2 -2
  297. package/dist/tier-routing.d.ts +1 -1
  298. package/dist/topics.d.ts +1 -1
  299. package/dist/transcript.d.ts +1 -1
  300. package/dist/transfer/export-okf.d.ts +1 -1
  301. package/dist/trust-score-stage.d.ts +1 -1
  302. package/dist/trust-score.d.ts +1 -1
  303. package/dist/{types-CLRTESSD.d.ts → types-Cy1l6fzp.d.ts} +48 -1
  304. package/dist/types.d.ts +1 -1
  305. package/dist/utility-runtime.d.ts +1 -1
  306. package/dist/who-knows-cli.d.ts +11 -11
  307. package/dist/who-knows.d.ts +11 -11
  308. package/dist/write-envelope.d.ts +1 -1
  309. package/package.json +2 -2
  310. package/src/activity/config.test.ts +94 -1
  311. package/src/activity/config.ts +256 -3
  312. package/src/activity/digest.ts +21 -0
  313. package/src/activity/index.ts +2 -0
  314. package/src/activity/runner.ts +2 -17
  315. package/src/activity/timeline/publish-cli.test.ts +169 -0
  316. package/src/activity/timeline/publish-cli.ts +144 -0
  317. package/src/activity/timeline/query.ts +51 -15
  318. package/src/activity/types.ts +45 -0
  319. package/src/activity/vault-insert.ts +14 -33
  320. package/src/activity/vault-path.ts +100 -22
  321. package/src/activity/vault-publish.test.ts +51 -0
  322. package/src/activity/vault-publish.ts +157 -17
  323. package/src/activity/vault-publisher.test.ts +881 -0
  324. package/src/activity/vault-publisher.ts +597 -0
  325. package/src/activity/vault-region.test.ts +5 -0
  326. package/src/activity/vault-region.ts +7 -2
  327. package/src/cli/timeline-publish-commands.ts +34 -0
  328. package/src/cli.ts +2 -2
  329. package/src/config.test.ts +24 -1
  330. package/dist/chunk-3ITTF7YH.js.map +0 -1
  331. package/dist/chunk-KJUWWN3T.js +0 -237
  332. package/dist/chunk-KJUWWN3T.js.map +0 -1
  333. package/dist/chunk-M6L7HI7R.js.map +0 -1
  334. package/dist/chunk-X4FIZAVQ.js.map +0 -1
  335. /package/dist/{chunk-TEZ2HTC7.js.map → chunk-33JKYLPX.js.map} +0 -0
  336. /package/dist/{chunk-5PTS43CH.js.map → chunk-4LG4CPOC.js.map} +0 -0
  337. /package/dist/{chunk-Z33VUMKY.js.map → chunk-5H2G5X4R.js.map} +0 -0
  338. /package/dist/{chunk-XFPHV65N.js.map → chunk-5JQV4T5Q.js.map} +0 -0
  339. /package/dist/{chunk-OASGWNYL.js.map → chunk-63I7LMLG.js.map} +0 -0
  340. /package/dist/{chunk-J3NGQ5SC.js.map → chunk-7GSSHAXC.js.map} +0 -0
  341. /package/dist/{chunk-VHAKBUBD.js.map → chunk-B64IVEOH.js.map} +0 -0
  342. /package/dist/{chunk-R7FXPG4T.js.map → chunk-C25N5HWX.js.map} +0 -0
  343. /package/dist/{chunk-DH3V7RCQ.js.map → chunk-F2ERDKY2.js.map} +0 -0
  344. /package/dist/{chunk-LJNLLNEM.js.map → chunk-HODFGQGT.js.map} +0 -0
  345. /package/dist/{chunk-B75VCEX5.js.map → chunk-K2IJH4D3.js.map} +0 -0
  346. /package/dist/{chunk-227WJVPY.js.map → chunk-KLSPKMIO.js.map} +0 -0
  347. /package/dist/{chunk-5P2KNFJZ.js.map → chunk-MSDIETFC.js.map} +0 -0
  348. /package/dist/{chunk-HFRGCNFY.js.map → chunk-OAKATRAN.js.map} +0 -0
  349. /package/dist/{chunk-LWBSEYW3.js.map → chunk-PAWIY5OQ.js.map} +0 -0
  350. /package/dist/{chunk-6TOWUN2R.js.map → chunk-RE3H6Y2X.js.map} +0 -0
  351. /package/dist/{chunk-V3HUBF7T.js.map → chunk-S35IIP5P.js.map} +0 -0
  352. /package/dist/{chunk-3YKOFCKP.js.map → chunk-SXHBZXHK.js.map} +0 -0
  353. /package/dist/{chunk-7NCUHD5U.js.map → chunk-VAMZYOOV.js.map} +0 -0
  354. /package/dist/{chunk-PGCZLHRB.js.map → chunk-W272SWN7.js.map} +0 -0
  355. /package/dist/{chunk-5HO3QBAD.js.map → chunk-W6GEMUZM.js.map} +0 -0
  356. /package/dist/{chunk-UCNQV43K.js.map → chunk-XQYTSZW3.js.map} +0 -0
  357. /package/dist/{chunk-GR4C3FHT.js.map → chunk-ZKZCJRRO.js.map} +0 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Vault publisher service (issue #1985): composes the landed primitives —
3
+ * path templates (`vault-path.ts`), managed regions (`vault-publish.ts`),
4
+ * marker insertion (`vault-insert.ts`), region-name validation
5
+ * (`vault-region.ts`), begin/end pair assertion (`vault-region-pair.ts`) —
6
+ * into one publish pass over a single vault note.
7
+ *
8
+ * Guarantees: everything outside the managed region (and, in frontmatter
9
+ * mode, everything except `prefix`-owned keys) is byte-identical after
10
+ * publish; unchanged content produces no write; all writes are temp-file +
11
+ * rename in the note's own directory; dry-run performs zero writes; a
12
+ * destination that changed after it was read aborts with `concurrent_write`
13
+ * instead of clobbering an editor's concurrent save; symlinked paths that
14
+ * resolve outside the vault, and symlinked note files, are refused before
15
+ * any write.
16
+ */
17
+ import { createHash, randomBytes } from "node:crypto";
18
+ import { chmodSync, chownSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, type Stats } from "node:fs";
19
+ import path from "node:path";
20
+
21
+ import { expandTildePath } from "../utils/path.js";
22
+ import { applyManagedRegion, fileLines } from "./vault-publish.js";
23
+ import { insertMarkersUnderHeading } from "./vault-insert.js";
24
+ import { validateRegionName } from "./vault-region.js";
25
+ import { assertBeginEndPair } from "./vault-region-pair.js";
26
+ import { expandVaultTemplateTokens, resolveVaultNotePath } from "./vault-path.js";
27
+ import { summarizeVaultPublish, type VaultPublishResult, type VaultPublishStatus } from "./vault-status.js";
28
+
29
+ export interface VaultSectionProvenance {
30
+ generatorVersion: string;
31
+ /** Local-time generation stamp of the artifact; supplied by the caller so republishing an unchanged artifact stays a no-op. */
32
+ generatedAt: string;
33
+ }
34
+
35
+ export interface VaultSectionPublish {
36
+ /** Managed-region name (the heading name under the heading strategy). */
37
+ name: string;
38
+ /** Rendered artifact body the region owns. */
39
+ content: string;
40
+ /** Unprefixed stats, e.g. `{ focus_minutes: "220" }`; keys get the configured prefix. */
41
+ properties?: Readonly<Record<string, string>>;
42
+ /** When present, a provenance footer line is appended inside the region. */
43
+ provenance?: VaultSectionProvenance;
44
+ }
45
+
46
+ export interface PublishVaultNoteInput {
47
+ /** Vault root; absolute or `~` path; must be an existing directory. */
48
+ vaultPath: string;
49
+ /** Vault-relative note path template (date tokens). */
50
+ notePathTemplate: string;
51
+ /** Local day, YYYY-MM-DD. */
52
+ date: string;
53
+ sections: readonly VaultSectionPublish[];
54
+ strategy?: "markers" | "heading";
55
+ /** Markers strategy: heading under which missing marker pairs are inserted. */
56
+ insertUnderHeading?: string;
57
+ createMissingNotes?: boolean;
58
+ /** Vault-relative template file used only when creating a missing note. */
59
+ noteTemplate?: string;
60
+ propertiesMode?: "off" | "frontmatter" | "dataview-inline";
61
+ /** Property key prefix, default `remnic_`. */
62
+ propertiesPrefix?: string;
63
+ dryRun?: boolean;
64
+ }
65
+
66
+ // Bounded whitespace runs: an unbounded `\s*` chain around a lazy capture is the
67
+ // CodeQL js/polynomial-redos shape `check:regex-safety` rejects (issue #2439).
68
+ // The name is captured up to the TERMINAL `:start`/`:end` suffix, so a marker
69
+ // carrying a stray colon (`remnic:A:B:start`) is still seen by the scanner
70
+ // instead of being silently invisible to it. `validateRegionName` rejects `:`
71
+ // in a configured name, so such a marker can never be a legitimate pair and
72
+ // always resolves to a refusal below.
73
+ const START_MARKER_RE = /^<!--\s{0,8}remnic:(.+):start\s{0,8}-->$/;
74
+ const END_MARKER_RE = /^<!--\s{0,8}remnic:(.+):end\s{0,8}-->$/;
75
+
76
+ type SectionOutcome =
77
+ | { ok: true; text: string }
78
+ | { ok: false; outcome: "skipped" | "error"; reason: string };
79
+
80
+ export function publishVaultNote(input: PublishVaultNoteInput): VaultPublishStatus {
81
+ if (typeof input !== "object" || input === null) {
82
+ throw new TypeError("publishVaultNote requires an input object.");
83
+ }
84
+ if (!Array.isArray(input.sections) || input.sections.length === 0) {
85
+ throw new RangeError("publishVaultNote requires at least one section.");
86
+ }
87
+ const strategy = input.strategy ?? "markers";
88
+ const propertiesMode = input.propertiesMode ?? "off";
89
+ const prefix = input.propertiesPrefix ?? "remnic_";
90
+ if (typeof prefix !== "string" || prefix.length === 0) {
91
+ throw new RangeError("propertiesPrefix must be a non-empty string.");
92
+ }
93
+ for (const section of input.sections) {
94
+ const name = validateRegionName(section.name);
95
+ if (!name.ok) {
96
+ throw new RangeError(`Section name ${JSON.stringify(section.name)} is not a valid region name.`);
97
+ }
98
+ }
99
+
100
+ const relative = resolveVaultNotePath(input.notePathTemplate, input.date);
101
+ const result = publishRelative(input, relative, { strategy, propertiesMode, prefix });
102
+ return summarizeVaultPublish(result);
103
+ }
104
+
105
+ function publishRelative(
106
+ input: PublishVaultNoteInput,
107
+ relative: string,
108
+ opts: { strategy: "markers" | "heading"; propertiesMode: string; prefix: string },
109
+ ): VaultPublishResult[] {
110
+ const vault = expandTildePath(input.vaultPath);
111
+ try {
112
+ // A symlinked vault root is rejected outright (security checklist §3):
113
+ // statSync follows links, lstatSync does not.
114
+ const rootStat = lstatSync(vault);
115
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return refuse(relative, "error", "not_directory");
116
+ } catch {
117
+ return refuse(relative, "error", "not_directory");
118
+ }
119
+
120
+ const root = path.resolve(vault);
121
+ const dest = path.resolve(root, relative);
122
+ const lexical = path.relative(root, dest);
123
+ // `..notes/x.md` is an in-vault name, not parent traversal: only `..`
124
+ // exactly or a `..${path.sep}` prefix escapes.
125
+ if (lexical.length === 0 || lexical === ".." || lexical.startsWith(`..${path.sep}`) || path.isAbsolute(lexical)) {
126
+ return refuse(relative, "error", "path_escape");
127
+ }
128
+ if (!containedBySymlinks(root, dest)) {
129
+ return refuse(relative, "error", "symlink_escape");
130
+ }
131
+ let currentText: string | null = null;
132
+ let expected: DestExpectation | null = null;
133
+ try {
134
+ // A symlinked note is refused even when its target lives inside the
135
+ // vault (issue #1985): writeAtomic renames over the link itself, which
136
+ // would destroy the symlink and leave the target stale while the
137
+ // status still says `updated`.
138
+ if (lstatSync(dest).isSymbolicLink()) {
139
+ return refuse(relative, "error", "symlinked_note");
140
+ }
141
+ const st = statSync(dest);
142
+ if (st.isFile()) {
143
+ currentText = readFileSync(dest, "utf8");
144
+ expected = {
145
+ ino: st.ino,
146
+ mtimeMs: st.mtimeMs,
147
+ size: st.size,
148
+ sha256: createHash("sha256").update(currentText, "utf8").digest("hex"),
149
+ };
150
+ } else return refuse(relative, "error", "not_a_file");
151
+ } catch (err) {
152
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
153
+ }
154
+
155
+ let created = false;
156
+ if (currentText === null) {
157
+ if (input.createMissingNotes === true && typeof input.noteTemplate === "string" && input.noteTemplate.length > 0) {
158
+ const templatePath = path.resolve(root, input.noteTemplate);
159
+ const templateRel = path.relative(root, templatePath);
160
+ if (templateRel === ".." || templateRel.startsWith(`..${path.sep}`) || path.isAbsolute(templateRel) || templateRel.length === 0 || !containedBySymlinks(root, templatePath)) {
161
+ return refuse(relative, "error", "template_escape");
162
+ }
163
+ try {
164
+ const templateText = readFileSync(templatePath, "utf8");
165
+ currentText = expandVaultTemplateTokens(templateText, input.date);
166
+ if (!currentText.endsWith("\n")) currentText += "\n";
167
+ created = true;
168
+ } catch {
169
+ return refuse(relative, "error", "template_unreadable");
170
+ }
171
+ } else {
172
+ return refuse(relative, "skipped", "missing_file");
173
+ }
174
+ }
175
+
176
+ const original = currentText;
177
+ let text = original;
178
+ const failures: Array<{ outcome: "skipped" | "error"; reason: string }> = [];
179
+ const appliedSections: VaultSectionPublish[] = [];
180
+
181
+ for (const section of input.sections) {
182
+ const prefixed =
183
+ opts.propertiesMode === "off" || !section.properties
184
+ ? undefined
185
+ : Object.fromEntries(
186
+ Object.entries(section.properties).map(([key, value]) => [`${opts.prefix}${key}`, value]),
187
+ );
188
+ const content = decorate(section, prefixed, opts.propertiesMode);
189
+ const step = applySection(text, section, content, opts.strategy, input.insertUnderHeading, created);
190
+ if (step.ok) {
191
+ text = step.text;
192
+ appliedSections.push(section);
193
+ } else {
194
+ failures.push({ outcome: step.outcome, reason: step.reason });
195
+ }
196
+ }
197
+
198
+ if (failures.length > 0 && appliedSections.length === 0) {
199
+ const worst = failures.find((f) => f.outcome === "error") ?? failures[0]!;
200
+ return [{ path: relative, outcome: worst.outcome, reason: failures.map((f) => f.reason).join("; ") }];
201
+ }
202
+ if (failures.some((f) => f.outcome === "error")) {
203
+ return [{ path: relative, outcome: "error", reason: failures.map((f) => f.reason).join("; ") }];
204
+ }
205
+
206
+ // Partial publish (issue #1985 review): when some sections applied and
207
+ // others were skipped, the write covers ONLY the applied sections — their
208
+ // frontmatter properties, and the reported status. A skipped section's
209
+ // properties must never reach the file, and the omission must be surfaced
210
+ // as its own `skipped` row rather than reported as a plain `updated`.
211
+ const skippedReason = failures.length > 0 ? failures.map((f) => f.reason).join("; ") : undefined;
212
+ const withSkipped = (rows: VaultPublishResult[]): VaultPublishResult[] =>
213
+ skippedReason === undefined ? rows : [...rows, { path: relative, outcome: "skipped", reason: skippedReason }];
214
+
215
+ if (opts.propertiesMode === "frontmatter") {
216
+ const updates = frontmatterUpdates(appliedSections, opts.prefix);
217
+ if (Object.keys(updates).length > 0) {
218
+ const merged = mergeFrontmatterKeys(text, updates);
219
+ if (!merged.ok) {
220
+ return [{ path: relative, outcome: "error", reason: merged.reason }];
221
+ }
222
+ text = merged.text;
223
+ }
224
+ }
225
+
226
+ const prevHash = createHash("sha256").update(original, "utf8").digest("hex");
227
+ const nextHash = createHash("sha256").update(text, "utf8").digest("hex");
228
+ if (prevHash === nextHash) {
229
+ return withSkipped([{ path: relative, outcome: "unchanged" }]);
230
+ }
231
+ if (input.dryRun === true) {
232
+ return withSkipped([{ path: relative, outcome: "updated" }]);
233
+ }
234
+ if (created) {
235
+ // A brand-new note may live in absent folders (Daily/{yyyy}/{MM}). Make
236
+ // the parent chain only here, after every refusal above has passed: the
237
+ // lexical path check and `containedBySymlinks` already proved the deepest
238
+ // existing ancestor is a real directory inside the vault, so every segment
239
+ // `mkdirSync` adds lands beneath that contained ancestor.
240
+ try {
241
+ mkdirSync(path.dirname(dest), { recursive: true });
242
+ } catch {
243
+ return refuse(relative, "error", "mkdir_failed");
244
+ }
245
+ }
246
+
247
+ const write = writeAtomic(dest, text, expected);
248
+ if (write === "concurrent") {
249
+ // The note changed after it was read (editor autosave): keep the user's
250
+ // bytes and surface the abort — never a silent overwrite, never `updated`.
251
+ return [{ path: relative, outcome: "error", reason: "concurrent_write" }];
252
+ }
253
+ if (write !== "written") {
254
+ return [{ path: relative, outcome: "error", reason: "rename_failed" }];
255
+ }
256
+ return withSkipped([{ path: relative, outcome: "updated" }]);
257
+ }
258
+
259
+ function refuse(relative: string, outcome: "skipped" | "error", reason: string): VaultPublishResult[] {
260
+ return [{ path: relative, outcome, reason }];
261
+ }
262
+
263
+ function decorate(
264
+ section: VaultSectionPublish,
265
+ prefixed: Record<string, string> | undefined,
266
+ mode: string,
267
+ ): string {
268
+ const parts: string[] = [section.content];
269
+ if (prefixed !== undefined && mode === "dataview-inline") {
270
+ for (const [key, value] of Object.entries(prefixed)) {
271
+ parts.push(`${key}:: ${value}`);
272
+ }
273
+ }
274
+ if (section.provenance !== undefined) {
275
+ parts.push(
276
+ `*(generated by Remnic timeline · ${section.provenance.generatorVersion} · ${section.provenance.generatedAt})*`,
277
+ );
278
+ }
279
+ return parts.join("\n");
280
+ }
281
+
282
+ function applySection(
283
+ text: string,
284
+ section: VaultSectionPublish,
285
+ content: string,
286
+ strategy: "markers" | "heading",
287
+ insertUnderHeading: string | undefined,
288
+ freshNote: boolean,
289
+ ): SectionOutcome {
290
+ if (strategy === "markers") {
291
+ const mismatch = findMarkerMismatch(text);
292
+ if (mismatch !== null) {
293
+ return { ok: false, outcome: "skipped", reason: mismatch };
294
+ }
295
+ const applied = applyManagedRegion(text, { strategy: "markers", name: section.name, content });
296
+ if (applied.ok) return { ok: true, text: applied.text };
297
+ if (typeof insertUnderHeading === "string" && insertUnderHeading.length > 0) {
298
+ const inserted = insertMarkersUnderHeading(text, {
299
+ heading: insertUnderHeading,
300
+ name: section.name,
301
+ content,
302
+ });
303
+ if (inserted.ok) return { ok: true, text: inserted.text };
304
+ if (inserted.reason === "duplicate_heading") {
305
+ return {
306
+ ok: false,
307
+ outcome: "error",
308
+ reason: `duplicate_heading:${inserted.lines.join(",")}`,
309
+ };
310
+ }
311
+ return { ok: false, outcome: "skipped", reason: "no_marker" };
312
+ }
313
+ if (freshNote) {
314
+ const suffix = `${text.endsWith("\n") ? "" : "\n"}<!-- remnic:${section.name}:start -->\n${content}\n<!-- remnic:${section.name}:end -->\n`;
315
+ return { ok: true, text: text + suffix };
316
+ }
317
+ return { ok: false, outcome: "skipped", reason: "no_marker" };
318
+ }
319
+
320
+ const applied = applyManagedRegion(text, { strategy: "heading", name: section.name, content });
321
+ if (applied.ok) return { ok: true, text: applied.text };
322
+ if (applied.reason === "duplicate_heading") {
323
+ return { ok: false, outcome: "error", reason: `duplicate_heading:${applied.lines.join(",")}` };
324
+ }
325
+ return { ok: false, outcome: "skipped", reason: "no_heading" };
326
+ }
327
+
328
+ /**
329
+ * Refuse any note whose `remnic:<name>:{start,end}` markers are not a flat
330
+ * sequence of correctly named pairs. Every line outside a code block is
331
+ * scanned — a fenced or indented pair is sample text, invisible to this
332
+ * scan and to replacement —
333
+ * because `applyManagedRegion` pairs a start with the next end of the SAME
334
+ * name and would otherwise span a malformed region and delete every byte
335
+ * in between. Four shapes are malformed:
336
+ * - a crossed pair (`start:A` closed by `end:B`)
337
+ * - a nested start (`start:A`, `start:B`, `end:A`, `end:B`), where the
338
+ * inner region's end is the ONLY end left after A closes, so a
339
+ * replacement spans B's start marker and the user bytes around it
340
+ * - an orphan end with no open region, which cannot be paired at all
341
+ * - a marker line that carries the `remnic:…:start`/`:end` shape but no
342
+ * parseable name (empty name, or whitespace past the bounded run), which
343
+ * the pairing scan cannot classify at all
344
+ * Ambiguity always resolves to "keep the user's bytes".
345
+ */
346
+ function findMarkerMismatch(text: string): string | null {
347
+ let begin: string | null = null;
348
+ for (const row of fileLines(text)) {
349
+ if (row.fenced) continue;
350
+ const trimmed = row.line.trim();
351
+ const start = START_MARKER_RE.exec(trimmed);
352
+ if (start) {
353
+ const name = start[1]!;
354
+ if (begin !== null) return `nested_start:${begin}:${name}`;
355
+ begin = name;
356
+ continue;
357
+ }
358
+ const match = END_MARKER_RE.exec(trimmed);
359
+ if (!match) {
360
+ if (looksLikeRemnicMarkerLine(trimmed)) return `unparsable_marker:${trimmed}`;
361
+ continue;
362
+ }
363
+ const end = match[1]!;
364
+ if (begin === null) return `orphan_end:${end}`;
365
+ // A blank marker name cannot be paired at all: refuse rather than let
366
+ // assertBeginEndPair throw out of the publisher.
367
+ if (begin.trim().length === 0 || end.trim().length === 0) return `name_mismatch:${begin}:${end}`;
368
+ if (!assertBeginEndPair({ beginName: begin, endName: end }).ok) return `name_mismatch:${begin}:${end}`;
369
+ begin = null;
370
+ }
371
+ return null;
372
+ }
373
+
374
+ /**
375
+ * True for a line shaped like this publisher's own marker — `<!--` … `-->`
376
+ * wrapping `remnic:` … terminal `:start`/`:end` — that neither marker regex
377
+ * accepted. Classification only; the caller refuses the file. The exact
378
+ * trimmed line is inspected, never a normalized rewrite of it, so no
379
+ * "helpful" transform can widen what counts as parseable.
380
+ */
381
+ function looksLikeRemnicMarkerLine(trimmed: string): boolean {
382
+ if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return false;
383
+ const inner = trimmed.slice(4, -3).trim();
384
+ if (!inner.startsWith("remnic:")) return false;
385
+ return inner.endsWith(":start") || inner.endsWith(":end");
386
+ }
387
+
388
+ function frontmatterUpdates(
389
+ sections: readonly VaultSectionPublish[],
390
+ prefix: string,
391
+ ): Record<string, string> {
392
+ const updates: Record<string, string> = {};
393
+ for (const section of sections) {
394
+ if (!section.properties) continue;
395
+ for (const [key, value] of Object.entries(section.properties)) {
396
+ updates[`${prefix}${key}`] = value;
397
+ }
398
+ }
399
+ return updates;
400
+ }
401
+
402
+ /**
403
+ * Targeted frontmatter edit: only `updates` keys are inserted or replaced;
404
+ * every other byte of the note (key order, comments, anchors, spacing) is
405
+ * preserved. Never parses-and-redumps the YAML block.
406
+ */
407
+ export function mergeFrontmatterKeys(
408
+ noteText: string,
409
+ updates: Readonly<Record<string, string>>,
410
+ ): { ok: true; text: string } | { ok: false; reason: string } {
411
+ const entries = Object.entries(updates);
412
+ if (entries.length === 0) return { ok: true, text: noteText };
413
+ for (const [key] of entries) {
414
+ if (key.includes("\n") || key.includes("\r") || key.trim() !== key || key.length === 0) {
415
+ return { ok: false, reason: "invalid_property_key" };
416
+ }
417
+ }
418
+
419
+ const eol = noteText.includes("\r\n") ? "\r\n" : "\n";
420
+ const openRe = /^---\r?$/;
421
+ const firstLineEnd = noteText.indexOf("\n");
422
+ const firstLine = firstLineEnd === -1 ? noteText : noteText.slice(0, firstLineEnd);
423
+
424
+ if (!openRe.test(firstLine)) {
425
+ const block = ["---", ...entries.map(([key, value]) => `${key}: ${value}`), "---", ""].join(eol);
426
+ return { ok: true, text: block + eol + noteText };
427
+ }
428
+
429
+ const lines = noteText.split(eol);
430
+ let closeIdx = -1;
431
+ for (let i = 1; i < lines.length; i += 1) {
432
+ if (openRe.test(lines[i]!)) {
433
+ closeIdx = i;
434
+ break;
435
+ }
436
+ }
437
+ if (closeIdx === -1) {
438
+ return { ok: false, reason: "malformed_frontmatter" };
439
+ }
440
+
441
+ const pending = new Map(entries);
442
+ for (let i = 1; i < closeIdx; i += 1) {
443
+ const line = lines[i]!;
444
+ const idx = line.indexOf(":");
445
+ if (idx === -1) continue;
446
+ const rawKey = line.slice(0, idx);
447
+ // Leading whitespace is exactly the signal that this is a nested,
448
+ // user-owned child key (`custom:` / ` remnic_focus: old`). Only an
449
+ // unindented top-level mapping may satisfy a pending update, so the
450
+ // key is NOT trimmed before that decision.
451
+ if (rawKey !== rawKey.trimStart()) continue;
452
+ const key = rawKey.trimEnd();
453
+ if (!pending.has(key)) continue;
454
+ lines[i] = `${rawKey}: ${pending.get(key)}`;
455
+ pending.delete(key);
456
+ }
457
+ const insertAt = closeIdx;
458
+ const added: string[] = [];
459
+ for (const [key, value] of pending.entries()) {
460
+ added.push(`${key}: ${value}`);
461
+ }
462
+ lines.splice(insertAt, 0, ...added);
463
+ return { ok: true, text: lines.join(eol) };
464
+ }
465
+
466
+ /**
467
+ * Containment under symlinks: the deepest existing ancestor of `dest` must
468
+ * still resolve inside the (real) vault root. A symlink chain that leaves
469
+ * the vault is refused before any note byte is read.
470
+ */
471
+ function containedBySymlinks(root: string, dest: string): boolean {
472
+ const realRoot = realpathSync(root);
473
+ let probe = dest;
474
+ for (;;) {
475
+ try {
476
+ if (lstatSync(probe).isSymbolicLink() || statSync(probe).isFile() || statSync(probe).isDirectory()) {
477
+ const real = realpathSync(probe);
478
+ return real === realRoot || real.startsWith(`${realRoot}${path.sep}`);
479
+ }
480
+ } catch {
481
+ // ENOENT: walk up to the parent.
482
+ }
483
+ const parent = path.dirname(probe);
484
+ if (parent === probe) return false;
485
+ probe = parent;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Destination identity captured when the note was read for this publish:
491
+ * inode + mtime + size + content hash. Any drift by replace time is a
492
+ * concurrent write (an editor that saves in place changes mtimeMs/size/
493
+ * bytes; one that saves atomically changes the inode) and aborts the
494
+ * replace. `null` means the destination did not exist at read time.
495
+ */
496
+ interface DestExpectation {
497
+ ino: number;
498
+ mtimeMs: number;
499
+ size: number;
500
+ sha256: string;
501
+ }
502
+
503
+ /** Test-only injection point (issue #1985 review): runs after the note was
504
+ * read and rendered but before the pre-replace verification, so tests can
505
+ * simulate a concurrent editor save landing inside that window. Never set
506
+ * in production code. */
507
+ export interface VaultPublisherTestHooks {
508
+ beforeReplaceVerify?: (dest: string) => void;
509
+ }
510
+
511
+ let vaultPublisherTestHooks: VaultPublisherTestHooks | null = null;
512
+
513
+ export function setVaultPublisherTestHooks(hooks: VaultPublisherTestHooks | null): void {
514
+ vaultPublisherTestHooks = hooks;
515
+ }
516
+
517
+ /**
518
+ * Temp file + rename in the note's own directory; one retry on EBUSY/EPERM.
519
+ *
520
+ * The replacement inode must inherit the destination's permissions: a fresh
521
+ * temp file is `0666 & ~umask`, so renaming it over a `0600` note under a
522
+ * `0022` umask silently widens the note to `0644`. Mode is carried via
523
+ * chmodSync (not a writeFileSync mode option, which umask would mask);
524
+ * ownership is carried only as far as the process's privilege allows
525
+ * (chownSync to another owner fails EPERM for non-root and is ignored).
526
+ *
527
+ * Concurrent-write guard (issue #1985 final review): `text` is derived from
528
+ * the snapshot in `expect`; if the destination no longer matches that
529
+ * snapshot — different inode, mtime, size, bytes, or existence — the rename
530
+ * is refused (`"concurrent"`) and the user's bytes are kept. Deliberately
531
+ * abort-only, no re-read/re-render retry: the refusal direction is the
532
+ * contract, and the next scheduled publish converges against fresh content.
533
+ */
534
+ function writeAtomic(dest: string, text: string, expect: DestExpectation | null): "written" | "concurrent" | "failed" {
535
+ const tmpPath = path.join(path.dirname(dest), `.remnic-vault-${randomBytes(8).toString("hex")}.tmp`);
536
+ writeFileSync(tmpPath, text);
537
+ let prev: Stats | null = null;
538
+ try {
539
+ prev = statSync(dest);
540
+ } catch {
541
+ prev = null; // destination absent (new note) — not an error yet
542
+ }
543
+ if (prev !== null) {
544
+ try {
545
+ chmodSync(tmpPath, prev.mode & 0o7777);
546
+ chownSync(tmpPath, prev.uid, prev.gid);
547
+ } catch {
548
+ // Insufficient privilege (EPERM): the temp file keeps its default
549
+ // mode/owner.
550
+ }
551
+ }
552
+ vaultPublisherTestHooks?.beforeReplaceVerify?.(dest);
553
+ try {
554
+ const concurrent =
555
+ expect === null
556
+ ? prev !== null // someone created the note after we planned to
557
+ : prev === null || // the note vanished since it was read
558
+ prev.ino !== expect.ino ||
559
+ prev.mtimeMs !== expect.mtimeMs ||
560
+ prev.size !== expect.size ||
561
+ createHash("sha256").update(readFileSync(dest, "utf8"), "utf8").digest("hex") !== expect.sha256;
562
+ if (concurrent) {
563
+ try {
564
+ unlinkSync(tmpPath);
565
+ } catch {
566
+ // best-effort cleanup
567
+ }
568
+ return "concurrent";
569
+ }
570
+ } catch {
571
+ // Destination unreadable at verify time: refuse to replace.
572
+ try {
573
+ unlinkSync(tmpPath);
574
+ } catch {
575
+ // best-effort cleanup
576
+ }
577
+ return "concurrent";
578
+ }
579
+ for (let attempt = 0; attempt < 2; attempt += 1) {
580
+ try {
581
+ renameSync(tmpPath, dest);
582
+ return "written";
583
+ } catch (err) {
584
+ const code = (err as NodeJS.ErrnoException).code;
585
+ if (attempt === 1 || (code !== "EBUSY" && code !== "EPERM")) {
586
+ try {
587
+ unlinkSync(tmpPath);
588
+ } catch {
589
+ // best-effort cleanup
590
+ }
591
+ if (code === "EBUSY" || code === "EPERM") return "failed";
592
+ throw err;
593
+ }
594
+ }
595
+ }
596
+ return "failed";
597
+ }
@@ -22,3 +22,8 @@ test("validateRegionName rejects the HTML comment closer", () => {
22
22
  assert.deepEqual(validateRegionName("foo-->bar"), { ok: false, error: "invalid_name" });
23
23
  assert.deepEqual(validateRegionName("timeline-->"), { ok: false, error: "invalid_name" });
24
24
  });
25
+
26
+ test("validateRegionName rejects a colon, which the marker grammar delimits with", () => {
27
+ assert.deepEqual(validateRegionName("Work:Timeline"), { ok: false, error: "invalid_name" });
28
+ assert.deepEqual(validateRegionName("timeline:start"), { ok: false, error: "invalid_name" });
29
+ });
@@ -2,7 +2,12 @@
2
2
  * Validate a managed-region name for vault markers (issue #1985).
3
3
  *
4
4
  * Empty, whitespace-only, newline, and `-->` names are rejected so they
5
- * cannot break `<!-- remnic:<name>:start -->` markers.
5
+ * cannot break `<!-- remnic:<name>:start -->` markers. `:` is rejected
6
+ * too: the marker grammar is colon-delimited, so a colon-bearing name
7
+ * makes a marker line ambiguous (`remnic:A:B:start` reads as name `A:B`
8
+ * or as name `A` with a `B:start` suffix) and a publisher that resolved
9
+ * that ambiguity differently from the note's own markers would pair a
10
+ * start with a later end and delete the user bytes in between.
6
11
  */
7
12
 
8
13
  export type ValidateRegionNameResult =
@@ -13,7 +18,7 @@ export function validateRegionName(name: string): ValidateRegionNameResult {
13
18
  if (typeof name !== "string") {
14
19
  return { ok: false, error: "invalid_name" };
15
20
  }
16
- if (name.includes("\n") || name.includes("\r") || name.includes("-->")) {
21
+ if (name.includes("\n") || name.includes("\r") || name.includes("-->") || name.includes(":")) {
17
22
  return { ok: false, error: "invalid_name" };
18
23
  }
19
24
  const trimmed = name.trim();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Timeline command group (issue #1985) — `remnic timeline publish` writes
3
+ * the day's timeline artifact into the user's markdown vault through the
4
+ * managed-region publisher. The command BODY lives in
5
+ * `activity/timeline/publish-cli.ts` so the standalone `@remnic/cli` binary
6
+ * dispatches the same implementation through the shared timeline runner.
7
+ */
8
+
9
+ import type { Orchestrator } from "../orchestrator.js";
10
+ import type { CliCommand } from "../cli.js";
11
+ import { runTimelinePublishCli } from "../activity/timeline/publish-cli.js";
12
+
13
+ export function registerTimelineCommands(cmd: CliCommand, orchestrator: Orchestrator): void {
14
+ const timelineCmd = cmd
15
+ .command("timeline")
16
+ .description("Timeline artifacts: publish the day's recap into a markdown vault (issue #1985)");
17
+
18
+ timelineCmd
19
+ .command("publish")
20
+ .description("Publish timeline artifacts into the vault daily note via managed regions")
21
+ .option("--date <date>", "Local day to publish (YYYY-MM-DD; defaults to today)")
22
+ .option("--week <week>", "ISO week to publish (YYYY-Www)")
23
+ .option("--what <kinds>", "Comma-separated artifacts to publish (default: timeline)")
24
+ .option("--dry-run", "Report per-file outcomes without writing")
25
+ .action((...args: unknown[]) => {
26
+ const options = (args[0] ?? {}) as Record<string, unknown>;
27
+ const code = runTimelinePublishCli(orchestrator.config, options, {
28
+ stdout: process.stdout,
29
+ stderr: process.stderr,
30
+ });
31
+ if (code !== 0) process.exitCode = code;
32
+ });
33
+ }
34
+
package/src/cli.ts CHANGED
@@ -18,7 +18,7 @@ import { registerMeetingsCommands } from "./cli/meetings-commands.js"; import {
18
18
  import { registerExportOkfCommand, registerOkfCommands } from "./cli/okf-commands.js";
19
19
  import { registerSkillsCommands } from "./cli/skills-commands.js";
20
20
  import { registerResearchStatusCommands } from "./cli/research-status-commands.js";
21
- import { registerCreationLedgerCommands } from "./cli/creation-ledger-commands.js";
21
+ import { registerCreationLedgerCommands } from "./cli/creation-ledger-commands.js"; import { registerTimelineCommands } from "./cli/timeline-publish-commands.js";
22
22
  import type {
23
23
  BehaviorSignalEvent,
24
24
  ContinuityIncidentRecord,
@@ -6230,7 +6230,7 @@ export function registerCli(
6230
6230
 
6231
6231
  registerResearchStatusCommands(cmd, orchestrator);
6232
6232
 
6233
- registerCreationLedgerCommands(cmd, orchestrator); registerMeetingsCommands(cmd, orchestrator); registerOkfCommands(cmd, orchestrator); registerDeepRecallCommands(cmd, orchestrator);
6233
+ registerCreationLedgerCommands(cmd, orchestrator); registerMeetingsCommands(cmd, orchestrator); registerOkfCommands(cmd, orchestrator); registerDeepRecallCommands(cmd, orchestrator); registerTimelineCommands(cmd, orchestrator);
6234
6234
 
6235
6235
  cmd
6236
6236
  .command("trust-zone-promote")