@remnic/core 9.66.12 → 9.68.0

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 (217) hide show
  1. package/dist/access-admin-ops-surface.js +26 -26
  2. package/dist/access-audit.js +2 -2
  3. package/dist/access-authorization-probe.js +27 -27
  4. package/dist/access-boundary.js +26 -26
  5. package/dist/access-cli.js +87 -87
  6. package/dist/access-extraction-force-flush.js +26 -26
  7. package/dist/access-http-query.js +28 -28
  8. package/dist/access-http.js +44 -44
  9. package/dist/access-identity-continuity-surface.js +26 -26
  10. package/dist/access-lcm-surface.js +26 -26
  11. package/dist/access-mcp.js +34 -34
  12. package/dist/access-namespace-preflight.js +26 -26
  13. package/dist/access-observe-write-surface.js +26 -26
  14. package/dist/access-operations-batch.js +28 -28
  15. package/dist/access-operations.js +33 -33
  16. package/dist/access-recall-concurrency.js +26 -26
  17. package/dist/access-recall-response.js +26 -26
  18. package/dist/access-recall-surface.js +26 -26
  19. package/dist/access-schema.js +3 -3
  20. package/dist/access-service.js +26 -26
  21. package/dist/access-surface-catalog.js +28 -28
  22. package/dist/adapters/index.js +7 -7
  23. package/dist/adapters/registry.js +3 -3
  24. package/dist/briefing.js +11 -11
  25. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  26. package/dist/causal-consolidation.js +12 -12
  27. package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
  28. package/dist/{chunk-FJP6MVRK.js → chunk-25P3LW6U.js} +2 -2
  29. package/dist/{chunk-JKXPFIPC.js → chunk-2MHMHF76.js} +2 -2
  30. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  31. package/dist/{chunk-QMIKXIAM.js → chunk-327JMA2N.js} +2 -2
  32. package/dist/{chunk-H6VYVOVP.js → chunk-363VZTVH.js} +2 -2
  33. package/dist/{chunk-PKRYQ222.js → chunk-3IRH5MYM.js} +6 -6
  34. package/dist/{chunk-GEHEBLVY.js → chunk-4PEZMGZD.js} +2 -2
  35. package/dist/{chunk-EU5PDHU2.js → chunk-4PMPOZK3.js} +2 -2
  36. package/dist/{chunk-6SRUPUTY.js → chunk-52GQNSLC.js} +5 -5
  37. package/dist/{chunk-SV3MDTJI.js → chunk-5UI7WQA6.js} +2 -2
  38. package/dist/{chunk-JGXMA6HL.js → chunk-6FUMA6AA.js} +2 -2
  39. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  40. package/dist/{chunk-WGVAEVT7.js → chunk-7KG5V4VV.js} +2 -2
  41. package/dist/{chunk-A6HVSRGM.js → chunk-A7NMS5II.js} +3 -3
  42. package/dist/{chunk-M3BFW3VX.js → chunk-ABPCF4MW.js} +1 -1
  43. package/dist/{chunk-NNBZS3DY.js → chunk-AG46P24C.js} +4 -4
  44. package/dist/{chunk-I7FVQMAA.js → chunk-BGD3T743.js} +2 -2
  45. package/dist/{chunk-QRHKJT7F.js → chunk-BHH736UQ.js} +4 -4
  46. package/dist/{chunk-4L7OKRGP.js → chunk-BS5BD3DF.js} +2 -2
  47. package/dist/{chunk-BT7AQZQX.js → chunk-BTRY6UH3.js} +2 -2
  48. package/dist/{chunk-BE4Y6GK6.js → chunk-D35XLYQ7.js} +11 -11
  49. package/dist/{chunk-CJTZO5MW.js → chunk-D6Q5EPOV.js} +2 -2
  50. package/dist/{chunk-6KGBI54C.js → chunk-DJ7TZLGU.js} +2 -2
  51. package/dist/{chunk-IDPFBRPJ.js → chunk-ERKFG4RV.js} +38 -38
  52. package/dist/{chunk-K7VTD5AZ.js → chunk-F25CL3IQ.js} +2 -2
  53. package/dist/{chunk-QZPSIQRB.js → chunk-FOT5GP2U.js} +2 -2
  54. package/dist/{chunk-OMA2B7GY.js → chunk-HACVBEAP.js} +2 -2
  55. package/dist/{chunk-IEZ6CGTW.js → chunk-HCM7ZYJD.js} +2 -2
  56. package/dist/{chunk-46XRYYOJ.js → chunk-HDQQRRCZ.js} +3 -3
  57. package/dist/{chunk-QXDICB4J.js → chunk-I6T4SYFW.js} +3 -3
  58. package/dist/{chunk-HQJM3BKH.js → chunk-J2R275FN.js} +2 -2
  59. package/dist/{chunk-STUPJFG7.js → chunk-KAY2C6VX.js} +2 -2
  60. package/dist/{chunk-I3X5NWDU.js → chunk-KJUH3QIQ.js} +2 -2
  61. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  62. package/dist/{chunk-KZAN4JXY.js → chunk-KRNFJKMH.js} +2 -2
  63. package/dist/{chunk-SBUQ4KIE.js → chunk-L7MJJAG2.js} +2 -2
  64. package/dist/{chunk-IVKIODHO.js → chunk-MB6GLPRY.js} +4 -4
  65. package/dist/{chunk-NROLO2HY.js → chunk-ND6NL2YR.js} +2 -2
  66. package/dist/{chunk-O2HDDAUM.js → chunk-NFITGYMQ.js} +3 -3
  67. package/dist/{chunk-GRZYF6KK.js → chunk-NYQHD7H4.js} +12 -12
  68. package/dist/{chunk-REJCULWA.js → chunk-P342D5BP.js} +2 -2
  69. package/dist/{chunk-HGRRJDUH.js → chunk-PIBQIYF2.js} +2 -2
  70. package/dist/{chunk-UUGU26P2.js → chunk-QOE3EM5Z.js} +2 -2
  71. package/dist/{chunk-N37X5RU7.js → chunk-RACZB5HT.js} +4 -4
  72. package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
  73. package/dist/{chunk-F3H7VNM4.js → chunk-USQAH6PN.js} +2 -2
  74. package/dist/{chunk-ZFE7NXB7.js → chunk-VLSUKXCJ.js} +4 -4
  75. package/dist/{chunk-GHSJBVVQ.js → chunk-WFFGUFFN.js} +265 -49
  76. package/dist/chunk-WFFGUFFN.js.map +1 -0
  77. package/dist/{chunk-YWD5UIWY.js → chunk-WQCET3N3.js} +2 -2
  78. package/dist/{chunk-MUYJQ273.js → chunk-WSYACIKD.js} +2 -2
  79. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  80. package/dist/{chunk-AWMS3IFB.js → chunk-XPDWNWH6.js} +3 -3
  81. package/dist/{chunk-K5JFIA44.js → chunk-XW3G4BJI.js} +7 -7
  82. package/dist/{chunk-GIC6BASP.js → chunk-XZBXINXZ.js} +4 -4
  83. package/dist/{chunk-KDCPIXIY.js → chunk-YFK64FYK.js} +2 -2
  84. package/dist/{chunk-DDAHG5ZD.js → chunk-YYAWJRGL.js} +2 -2
  85. package/dist/{chunk-IYFL3534.js → chunk-Z2FXSE5P.js} +2 -2
  86. package/dist/{chunk-CR73YTWC.js → chunk-ZDJMCXC7.js} +11 -11
  87. package/dist/{chunk-OQT34EVH.js → chunk-ZVW724ZK.js} +2 -2
  88. package/dist/cli.js +63 -63
  89. package/dist/coding/export-okf-codegraph.js +11 -11
  90. package/dist/compounding/engine.js +11 -11
  91. package/dist/connectors/codex-materialize-runner.js +11 -11
  92. package/dist/connectors/index.js +11 -11
  93. package/dist/convergence-refresh.js +13 -13
  94. package/dist/conversation-index/backend.js +2 -2
  95. package/dist/corpus-watermark.js +13 -13
  96. package/dist/entity-retrieval.js +11 -11
  97. package/dist/external-wiki-access.js +27 -27
  98. package/dist/extraction-span-gate.d.ts +31 -0
  99. package/dist/extraction-span-gate.js +37 -0
  100. package/dist/extraction-span-gate.js.map +1 -0
  101. package/dist/extraction.js +14 -14
  102. package/dist/harmonic-retrieval.js +1 -1
  103. package/dist/index.d.ts +61 -6
  104. package/dist/index.js +106 -100
  105. package/dist/lcm/index.js +3 -3
  106. package/dist/local-llm.js +2 -2
  107. package/dist/location/index.js +13 -13
  108. package/dist/maintenance/memory-governance.js +11 -11
  109. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +11 -11
  110. package/dist/maintenance/rebuild-memory-projection.js +12 -12
  111. package/dist/namespaces/migrate.js +12 -12
  112. package/dist/namespaces/search.js +9 -9
  113. package/dist/namespaces/storage.js +11 -11
  114. package/dist/operator-doctor-corpus.js +14 -14
  115. package/dist/operator-toolkit.js +19 -19
  116. package/dist/orchestration/maintenance.js +19 -19
  117. package/dist/orchestrator.js +87 -87
  118. package/dist/recall-navigate-window.d.ts +26 -0
  119. package/dist/recall-navigate-window.js +32 -0
  120. package/dist/recall-navigate-window.js.map +1 -0
  121. package/dist/search/factory.js +8 -8
  122. package/dist/search/index.js +13 -13
  123. package/dist/search/lancedb-backend.js +2 -2
  124. package/dist/search/meilisearch-backend.js +2 -2
  125. package/dist/search/orama-backend.js +2 -2
  126. package/dist/semantic-consolidation.js +12 -12
  127. package/dist/semantic-rule-promotion.js +11 -11
  128. package/dist/semantic-rule-verifier.js +11 -11
  129. package/dist/source-agent-qualifier.js +11 -11
  130. package/dist/standup/build.js +11 -11
  131. package/dist/storage.js +10 -10
  132. package/dist/summarizer.js +3 -3
  133. package/dist/support-passport/index.js +29 -29
  134. package/dist/transfer/backup.js +2 -2
  135. package/dist/transfer/capsule-export.js +2 -2
  136. package/dist/transfer/capsule-import.js +2 -2
  137. package/dist/transfer/export-okf.js +12 -12
  138. package/dist/verified-recall.js +11 -11
  139. package/dist/who-knows-cli.js +12 -12
  140. package/dist/who-knows.js +11 -11
  141. package/package.json +2 -2
  142. package/src/activity/index.ts +2 -0
  143. package/src/activity/journal-section.ts +1 -1
  144. package/src/activity/journal-strip.test.ts +126 -0
  145. package/src/activity/journal-strip.ts +115 -0
  146. package/src/activity/privacy-delete-plan.test.ts +215 -0
  147. package/src/activity/privacy-delete-plan.ts +116 -0
  148. package/src/activity/timeline/index.ts +1 -0
  149. package/src/activity/timeline/recap-merge-edits.test.ts +181 -0
  150. package/src/activity/timeline/recap-merge-edits.ts +70 -0
  151. package/src/extraction-span-gate.test.ts +202 -0
  152. package/src/extraction-span-gate.ts +58 -0
  153. package/src/recall-navigate-window.test.ts +104 -0
  154. package/src/recall-navigate-window.ts +48 -0
  155. package/src/shared-context/supersession-chain.test.ts +158 -0
  156. package/src/shared-context/supersession-chain.ts +95 -0
  157. package/dist/chunk-GHSJBVVQ.js.map +0 -1
  158. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  159. /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
  160. /package/dist/{chunk-FJP6MVRK.js.map → chunk-25P3LW6U.js.map} +0 -0
  161. /package/dist/{chunk-JKXPFIPC.js.map → chunk-2MHMHF76.js.map} +0 -0
  162. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  163. /package/dist/{chunk-QMIKXIAM.js.map → chunk-327JMA2N.js.map} +0 -0
  164. /package/dist/{chunk-H6VYVOVP.js.map → chunk-363VZTVH.js.map} +0 -0
  165. /package/dist/{chunk-PKRYQ222.js.map → chunk-3IRH5MYM.js.map} +0 -0
  166. /package/dist/{chunk-GEHEBLVY.js.map → chunk-4PEZMGZD.js.map} +0 -0
  167. /package/dist/{chunk-EU5PDHU2.js.map → chunk-4PMPOZK3.js.map} +0 -0
  168. /package/dist/{chunk-6SRUPUTY.js.map → chunk-52GQNSLC.js.map} +0 -0
  169. /package/dist/{chunk-SV3MDTJI.js.map → chunk-5UI7WQA6.js.map} +0 -0
  170. /package/dist/{chunk-JGXMA6HL.js.map → chunk-6FUMA6AA.js.map} +0 -0
  171. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  172. /package/dist/{chunk-WGVAEVT7.js.map → chunk-7KG5V4VV.js.map} +0 -0
  173. /package/dist/{chunk-A6HVSRGM.js.map → chunk-A7NMS5II.js.map} +0 -0
  174. /package/dist/{chunk-M3BFW3VX.js.map → chunk-ABPCF4MW.js.map} +0 -0
  175. /package/dist/{chunk-NNBZS3DY.js.map → chunk-AG46P24C.js.map} +0 -0
  176. /package/dist/{chunk-I7FVQMAA.js.map → chunk-BGD3T743.js.map} +0 -0
  177. /package/dist/{chunk-QRHKJT7F.js.map → chunk-BHH736UQ.js.map} +0 -0
  178. /package/dist/{chunk-4L7OKRGP.js.map → chunk-BS5BD3DF.js.map} +0 -0
  179. /package/dist/{chunk-BT7AQZQX.js.map → chunk-BTRY6UH3.js.map} +0 -0
  180. /package/dist/{chunk-BE4Y6GK6.js.map → chunk-D35XLYQ7.js.map} +0 -0
  181. /package/dist/{chunk-CJTZO5MW.js.map → chunk-D6Q5EPOV.js.map} +0 -0
  182. /package/dist/{chunk-6KGBI54C.js.map → chunk-DJ7TZLGU.js.map} +0 -0
  183. /package/dist/{chunk-IDPFBRPJ.js.map → chunk-ERKFG4RV.js.map} +0 -0
  184. /package/dist/{chunk-K7VTD5AZ.js.map → chunk-F25CL3IQ.js.map} +0 -0
  185. /package/dist/{chunk-QZPSIQRB.js.map → chunk-FOT5GP2U.js.map} +0 -0
  186. /package/dist/{chunk-OMA2B7GY.js.map → chunk-HACVBEAP.js.map} +0 -0
  187. /package/dist/{chunk-IEZ6CGTW.js.map → chunk-HCM7ZYJD.js.map} +0 -0
  188. /package/dist/{chunk-46XRYYOJ.js.map → chunk-HDQQRRCZ.js.map} +0 -0
  189. /package/dist/{chunk-QXDICB4J.js.map → chunk-I6T4SYFW.js.map} +0 -0
  190. /package/dist/{chunk-HQJM3BKH.js.map → chunk-J2R275FN.js.map} +0 -0
  191. /package/dist/{chunk-STUPJFG7.js.map → chunk-KAY2C6VX.js.map} +0 -0
  192. /package/dist/{chunk-I3X5NWDU.js.map → chunk-KJUH3QIQ.js.map} +0 -0
  193. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  194. /package/dist/{chunk-KZAN4JXY.js.map → chunk-KRNFJKMH.js.map} +0 -0
  195. /package/dist/{chunk-SBUQ4KIE.js.map → chunk-L7MJJAG2.js.map} +0 -0
  196. /package/dist/{chunk-IVKIODHO.js.map → chunk-MB6GLPRY.js.map} +0 -0
  197. /package/dist/{chunk-NROLO2HY.js.map → chunk-ND6NL2YR.js.map} +0 -0
  198. /package/dist/{chunk-O2HDDAUM.js.map → chunk-NFITGYMQ.js.map} +0 -0
  199. /package/dist/{chunk-GRZYF6KK.js.map → chunk-NYQHD7H4.js.map} +0 -0
  200. /package/dist/{chunk-REJCULWA.js.map → chunk-P342D5BP.js.map} +0 -0
  201. /package/dist/{chunk-HGRRJDUH.js.map → chunk-PIBQIYF2.js.map} +0 -0
  202. /package/dist/{chunk-UUGU26P2.js.map → chunk-QOE3EM5Z.js.map} +0 -0
  203. /package/dist/{chunk-N37X5RU7.js.map → chunk-RACZB5HT.js.map} +0 -0
  204. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
  205. /package/dist/{chunk-F3H7VNM4.js.map → chunk-USQAH6PN.js.map} +0 -0
  206. /package/dist/{chunk-ZFE7NXB7.js.map → chunk-VLSUKXCJ.js.map} +0 -0
  207. /package/dist/{chunk-YWD5UIWY.js.map → chunk-WQCET3N3.js.map} +0 -0
  208. /package/dist/{chunk-MUYJQ273.js.map → chunk-WSYACIKD.js.map} +0 -0
  209. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  210. /package/dist/{chunk-AWMS3IFB.js.map → chunk-XPDWNWH6.js.map} +0 -0
  211. /package/dist/{chunk-K5JFIA44.js.map → chunk-XW3G4BJI.js.map} +0 -0
  212. /package/dist/{chunk-GIC6BASP.js.map → chunk-XZBXINXZ.js.map} +0 -0
  213. /package/dist/{chunk-KDCPIXIY.js.map → chunk-YFK64FYK.js.map} +0 -0
  214. /package/dist/{chunk-DDAHG5ZD.js.map → chunk-YYAWJRGL.js.map} +0 -0
  215. /package/dist/{chunk-IYFL3534.js.map → chunk-Z2FXSE5P.js.map} +0 -0
  216. /package/dist/{chunk-CR73YTWC.js.map → chunk-ZDJMCXC7.js.map} +0 -0
  217. /package/dist/{chunk-OQT34EVH.js.map → chunk-ZVW724ZK.js.map} +0 -0
@@ -0,0 +1,126 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { stripRemnicOwnedRegions } from "./journal-strip.js";
5
+
6
+ test("stripRemnicOwnedRegions removes a marker pair and keeps user text", () => {
7
+ const text = [
8
+ "Morning standup.",
9
+ "<!-- remnic:timeline:start -->",
10
+ "9:00 deep work",
11
+ "<!-- remnic:timeline:end -->",
12
+ "Wrote the parser.",
13
+ ].join("\n");
14
+ assert.equal(stripRemnicOwnedRegions(text, []), "Morning standup.\nWrote the parser.");
15
+ });
16
+
17
+ test("stripRemnicOwnedRegions keeps one blank line where a removal doubled it", () => {
18
+ const text = [
19
+ "Before.",
20
+ "",
21
+ "<!-- remnic:timeline:start -->",
22
+ "generated",
23
+ "<!-- remnic:timeline:end -->",
24
+ "",
25
+ "After.",
26
+ ].join("\n");
27
+ assert.equal(stripRemnicOwnedRegions(text, []), "Before.\n\nAfter.");
28
+ });
29
+
30
+ test("stripRemnicOwnedRegions removes two distinct marker pairs", () => {
31
+ const text = [
32
+ "head",
33
+ "<!-- remnic:timeline:start -->",
34
+ "timeline body",
35
+ "<!-- remnic:timeline:end -->",
36
+ "middle",
37
+ "<!-- remnic:digest:start -->",
38
+ "digest body",
39
+ "<!-- remnic:digest:end -->",
40
+ "tail",
41
+ ].join("\n");
42
+ assert.equal(stripRemnicOwnedRegions(text, []), "head\nmiddle\ntail");
43
+ });
44
+
45
+ test("stripRemnicOwnedRegions strips an unterminated start marker to the end of input", () => {
46
+ const text = ["kept", "<!-- remnic:timeline:start -->", "generated", "later user text"].join("\n");
47
+ assert.equal(stripRemnicOwnedRegions(text, []), "kept");
48
+ });
49
+
50
+ test("stripRemnicOwnedRegions removes an orphan end marker and keeps surrounding text", () => {
51
+ const text = ["Before.", "<!-- remnic:timeline:end -->", "After."].join("\n");
52
+ assert.equal(stripRemnicOwnedRegions(text, []), "Before.\nAfter.");
53
+ });
54
+
55
+ test("stripRemnicOwnedRegions removes an owned heading section up to the next same-level heading", () => {
56
+ const text = ["## Journal", "intro", "## Timeline", "card one", "## Notes", "kept"].join("\n");
57
+ assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), "## Journal\nintro\n## Notes\nkept");
58
+ });
59
+
60
+ test("stripRemnicOwnedRegions does not end an owned section at a deeper heading", () => {
61
+ const text = [
62
+ "## Journal",
63
+ "## Timeline",
64
+ "card one",
65
+ "### Deep",
66
+ "deep card",
67
+ "## Notes",
68
+ "kept",
69
+ ].join("\n");
70
+ assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), "## Journal\n## Notes\nkept");
71
+ });
72
+
73
+ test("stripRemnicOwnedRegions preserves a non-owned heading section", () => {
74
+ const text = ["## Journal", "## Timeline", "card one", "## Notes", "kept"].join("\n");
75
+ assert.equal(stripRemnicOwnedRegions(text, ["Digest"]), text);
76
+ });
77
+
78
+ test("stripRemnicOwnedRegions ignores blank and whitespace owned heading entries", () => {
79
+ const text = ["## Timeline", "card one", ""].join("\n");
80
+ assert.equal(stripRemnicOwnedRegions(text, ["", " ", "\t"]), text);
81
+ });
82
+
83
+ test("stripRemnicOwnedRegions trims configured owned heading entries", () => {
84
+ const text = ["## Timeline", "card", "## Notes", "kept"].join("\n");
85
+ assert.equal(stripRemnicOwnedRegions(text, [" Timeline "]), "## Notes\nkept");
86
+ });
87
+
88
+ test("stripRemnicOwnedRegions returns empty string for empty input", () => {
89
+ assert.equal(stripRemnicOwnedRegions("", ["Timeline"]), "");
90
+ });
91
+
92
+ test("stripRemnicOwnedRegions returns unchanged text when nothing is owned", () => {
93
+ const text = ["first", "", "second", "", "third"].join("\n");
94
+ assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), text);
95
+ });
96
+
97
+ // Regression: the journal-section reader normalizes tab separators, extra
98
+ // spaces, trailing whitespace, and closing # sequences. The stripper must
99
+ // recognize exactly the same set, or an owned heading the reader treats as
100
+ // the journal section would keep its generated body in the journal text.
101
+ test("owned heading matching accepts every ATX form the section reader accepts", () => {
102
+ const body = ["generated line", "", "## User Notes", "kept"].join("\n");
103
+ for (const headingLine of [
104
+ "## Timeline",
105
+ "## Timeline",
106
+ "##\tTimeline",
107
+ "## Timeline ",
108
+ "## Timeline ##",
109
+ "## Timeline ### ",
110
+ ]) {
111
+ const stripped = stripRemnicOwnedRegions(
112
+ [headingLine, body].join("\n"),
113
+ ["Timeline"],
114
+ );
115
+ assert.equal(
116
+ stripped,
117
+ ["## User Notes", "kept"].join("\n"),
118
+ `heading line ${JSON.stringify(headingLine)} must strip as owned`,
119
+ );
120
+ }
121
+ });
122
+
123
+ test("an indented heading is not owned, matching the section reader", () => {
124
+ const text = [" ## Timeline", "not owned body"].join("\n");
125
+ assert.equal(stripRemnicOwnedRegions(text, ["Timeline"]), text);
126
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Strip Remnic-owned regions from a vault journal section (issue #1987).
3
+ *
4
+ * Loop prevention for vault journals: before daily-note text is treated
5
+ * as journal text, every managed marker region and every configured
6
+ * owned heading section is removed, so published output can never
7
+ * re-enter the journal. An unterminated start marker fails closed and
8
+ * strips to the end of the section. Pure string-in/string-out. No
9
+ * filesystem. Heading lines parse through the SAME exported ATX
10
+ * parser the journal-section reader uses, so a heading the reader
11
+ * recognizes is always a heading this stripper strips.
12
+ */
13
+
14
+ import { parseAtxHeading } from "./journal-section.js";
15
+
16
+ const MARKER_OPEN = "<!-- remnic:";
17
+ const MARKER_CLOSE = "-->";
18
+ const START_SUFFIX = ":start";
19
+ const END_SUFFIX = ":end";
20
+
21
+ type RegionMarker = { kind: "start" | "end"; name: string };
22
+
23
+ export function stripRemnicOwnedRegions(
24
+ sectionText: string,
25
+ ownedHeadings: readonly string[],
26
+ ): string {
27
+ return stripOwnedHeadings(stripMarkerRegions(sectionText), ownedHeadings);
28
+ }
29
+
30
+ function stripMarkerRegions(text: string): string {
31
+ const lines = text.split("\n");
32
+ const keep = lines.map(() => true);
33
+ let openName: string | null = null;
34
+ for (let i = 0; i < lines.length; i++) {
35
+ const marker = parseRegionMarker(lines[i]!);
36
+ if (openName !== null) {
37
+ keep[i] = false;
38
+ if (marker && marker.kind === "end" && marker.name === openName) openName = null;
39
+ continue;
40
+ }
41
+ if (marker) {
42
+ keep[i] = false;
43
+ if (marker.kind === "start") openName = marker.name;
44
+ }
45
+ }
46
+ return emitKeptLines(lines, keep);
47
+ }
48
+
49
+ function stripOwnedHeadings(text: string, ownedHeadings: readonly string[]): string {
50
+ const wanted = ownedHeadingSet(ownedHeadings);
51
+ if (wanted.size === 0 || text.length === 0) return text;
52
+ const lines = text.split("\n");
53
+ const keep = lines.map(() => true);
54
+ for (let i = 0; i < lines.length; i++) {
55
+ if (!keep[i]) continue;
56
+ const heading = parseAtxHeading(lines[i]!);
57
+ if (!heading || !wanted.has(heading.text)) continue;
58
+ keep[i] = false;
59
+ for (let j = i + 1; j < lines.length; j++) {
60
+ const next = parseAtxHeading(lines[j]!);
61
+ if (next && next.level <= heading.level) break;
62
+ keep[j] = false;
63
+ }
64
+ }
65
+ return emitKeptLines(lines, keep);
66
+ }
67
+
68
+ /**
69
+ * Emit kept lines, collapsing the one blank line each removal would
70
+ * otherwise double. Text outside removals is preserved byte-for-byte.
71
+ */
72
+ function emitKeptLines(lines: readonly string[], keep: readonly boolean[]): string {
73
+ const out: string[] = [];
74
+ let dropped = false;
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (!keep[i]) {
77
+ dropped = true;
78
+ continue;
79
+ }
80
+ const line = lines[i]!;
81
+ const previous = out.length > 0 ? out[out.length - 1]! : null;
82
+ if (dropped && previous !== null && previous.trim().length === 0 && line.trim().length === 0) {
83
+ dropped = false;
84
+ continue;
85
+ }
86
+ out.push(line);
87
+ dropped = false;
88
+ }
89
+ return out.join("\n");
90
+ }
91
+
92
+ function parseRegionMarker(line: string): RegionMarker | null {
93
+ const trimmed = line.trim();
94
+ if (!trimmed.startsWith(MARKER_OPEN) || !trimmed.endsWith(MARKER_CLOSE)) return null;
95
+ const inner = trimmed.slice(MARKER_OPEN.length, trimmed.length - MARKER_CLOSE.length).trim();
96
+ if (inner.endsWith(START_SUFFIX)) {
97
+ const name = inner.slice(0, -START_SUFFIX.length);
98
+ return name.length > 0 ? { kind: "start", name } : null;
99
+ }
100
+ if (inner.endsWith(END_SUFFIX)) {
101
+ const name = inner.slice(0, -END_SUFFIX.length);
102
+ return name.length > 0 ? { kind: "end", name } : null;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function ownedHeadingSet(ownedHeadings: readonly string[]): Set<string> {
108
+ const wanted = new Set<string>();
109
+ for (const entry of ownedHeadings) {
110
+ if (typeof entry !== "string") continue;
111
+ const trimmed = entry.trim();
112
+ if (trimmed.length > 0) wanted.add(trimmed);
113
+ }
114
+ return wanted;
115
+ }
@@ -0,0 +1,215 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { MS_PER_DAY } from "./privacy.js";
5
+ import {
6
+ ACTIVITY_DELETE_SCOPES,
7
+ planActivityDeletion,
8
+ type ActivityDeleteCandidate,
9
+ } from "./privacy-delete-plan.js";
10
+
11
+ const NOW = 100 * MS_PER_DAY;
12
+
13
+ function cand(
14
+ scope: string,
15
+ relPath: string,
16
+ capturedAtMs: number,
17
+ ): ActivityDeleteCandidate {
18
+ return { scope, relPath, capturedAtMs };
19
+ }
20
+
21
+ function plan(
22
+ candidates: readonly ActivityDeleteCandidate[],
23
+ overrides: Partial<Parameters<typeof planActivityDeletion>[0]> = {},
24
+ ) {
25
+ return planActivityDeletion({
26
+ candidates,
27
+ scopes: ACTIVITY_DELETE_SCOPES,
28
+ retentionDays: 30,
29
+ nowMs: NOW,
30
+ ...overrides,
31
+ });
32
+ }
33
+
34
+ test("expired in-scope activity paths are planned across every owned root", () => {
35
+ const expired = NOW - 40 * MS_PER_DAY;
36
+ const result = plan([
37
+ cand("observations", "activity/2026-01-01.md", expired),
38
+ cand("cards", "wearables/limitless/2026-01-01.md", expired),
39
+ cand("journal", "meetings/2026-01-01/mtg-x.md", expired),
40
+ ]);
41
+ assert.deepEqual(result.deletePaths, [
42
+ "activity/2026-01-01.md",
43
+ "meetings/2026-01-01/mtg-x.md",
44
+ "wearables/limitless/2026-01-01.md",
45
+ ]);
46
+ assert.equal(result.keptCount, 0);
47
+ assert.deepEqual(result.refused, []);
48
+ });
49
+
50
+ test("fresh in-scope candidates are kept, not deleted", () => {
51
+ const result = plan([cand("observations", "activity/2026-08-19.md", NOW - MS_PER_DAY)]);
52
+ assert.deepEqual(result.deletePaths, []);
53
+ assert.equal(result.keptCount, 1);
54
+ assert.deepEqual(result.refused, []);
55
+ });
56
+
57
+ test("retentionDays 0 deletes nothing but counts kept", () => {
58
+ const ancient = NOW - 4000 * MS_PER_DAY;
59
+ const result = plan([cand("weekly", "activity/2015-01-01.md", ancient)], {
60
+ retentionDays: 0,
61
+ });
62
+ assert.deepEqual(result.deletePaths, []);
63
+ assert.equal(result.keptCount, 1);
64
+ });
65
+
66
+ test("facts paths are refused even when expired and in scope", () => {
67
+ const expired = NOW - 40 * MS_PER_DAY;
68
+ const result = plan([cand("observations", "facts/2026-01-01/fact-1.md", expired)]);
69
+ assert.deepEqual(result.deletePaths, []);
70
+ assert.deepEqual(result.refused, ["facts/2026-01-01/fact-1.md"]);
71
+ assert.equal(result.keptCount, 0);
72
+ });
73
+
74
+ test("profile.md, entities, and state files are refused", () => {
75
+ const expired = NOW - 40 * MS_PER_DAY;
76
+ const result = plan([
77
+ cand("journal", "profile.md", expired),
78
+ cand("journal", "entities/person-a.md", expired),
79
+ cand("providerCache", "state/buffer.json", expired),
80
+ ]);
81
+ assert.deepEqual(result.deletePaths, []);
82
+ assert.deepEqual(result.refused, ["entities/person-a.md", "profile.md", "state/buffer.json"]);
83
+ });
84
+
85
+ test("absolute, traversal, and blank paths are refused", () => {
86
+ const expired = NOW - 40 * MS_PER_DAY;
87
+ const result = plan([
88
+ cand("observations", "/etc/passwd", expired),
89
+ cand("observations", "activity/../../facts/fact-1.md", expired),
90
+ cand("observations", " ", expired),
91
+ ]);
92
+ assert.deepEqual(result.deletePaths, []);
93
+ assert.deepEqual(result.refused, [
94
+ " ",
95
+ "/etc/passwd",
96
+ "activity/../../facts/fact-1.md",
97
+ ]);
98
+ });
99
+
100
+ test("unknown candidate scope is refused", () => {
101
+ const expired = NOW - 40 * MS_PER_DAY;
102
+ const result = plan([cand("bogus", "activity/2026-01-01.md", expired)]);
103
+ assert.deepEqual(result.deletePaths, []);
104
+ assert.deepEqual(result.refused, ["activity/2026-01-01.md"]);
105
+ assert.equal(result.keptCount, 0);
106
+ });
107
+
108
+ test("scope absent from scopes is skipped entirely", () => {
109
+ const expired = NOW - 40 * MS_PER_DAY;
110
+ const result = plan([cand("cards", "activity/2026-01-01.md", expired)], {
111
+ scopes: ["observations"],
112
+ });
113
+ assert.deepEqual(result.deletePaths, []);
114
+ assert.deepEqual(result.refused, []);
115
+ assert.equal(result.keptCount, 0);
116
+ });
117
+
118
+ test("unknown value in scopes throws RangeError", () => {
119
+ assert.throws(
120
+ () => plan([], { scopes: ["observations", "everything"] }),
121
+ (error: unknown) => error instanceof RangeError && /scope/.test(error.message),
122
+ );
123
+ });
124
+
125
+ test("negative or float retentionDays throws RangeError", () => {
126
+ assert.throws(
127
+ () => plan([], { retentionDays: -1 }),
128
+ (error: unknown) => error instanceof RangeError && /retentionDays/.test(error.message),
129
+ );
130
+ assert.throws(
131
+ () => plan([], { retentionDays: 2.5 }),
132
+ (error: unknown) => error instanceof RangeError && /retentionDays/.test(error.message),
133
+ );
134
+ });
135
+
136
+ test("non-finite nowMs throws RangeError", () => {
137
+ assert.throws(
138
+ () => plan([], { nowMs: Number.POSITIVE_INFINITY }),
139
+ (error: unknown) => error instanceof RangeError && /nowMs/.test(error.message),
140
+ );
141
+ assert.throws(
142
+ () => plan([], { nowMs: Number.NaN }),
143
+ (error: unknown) => error instanceof RangeError && /nowMs/.test(error.message),
144
+ );
145
+ });
146
+
147
+ test("deletePaths and refused are sorted and deduplicated", () => {
148
+ const expired = NOW - 40 * MS_PER_DAY;
149
+ const result = plan([
150
+ cand("observations", "activity/b.md", expired),
151
+ cand("cards", "activity/a.md", expired),
152
+ cand("journal", "activity/b.md", expired),
153
+ cand("observations", "facts/z.md", expired),
154
+ cand("cards", "facts/z.md", expired),
155
+ ]);
156
+ assert.deepEqual(result.deletePaths, ["activity/a.md", "activity/b.md"]);
157
+ assert.deepEqual(result.refused, ["facts/z.md"]);
158
+ });
159
+
160
+ test("half-open boundary: age exactly retentionDays * MS_PER_DAY is deleted", () => {
161
+ const boundary = NOW - 30 * MS_PER_DAY;
162
+ const result = plan([
163
+ cand("observations", "activity/boundary.md", boundary),
164
+ cand("observations", "activity/inside.md", boundary + 1),
165
+ ]);
166
+ assert.deepEqual(result.deletePaths, ["activity/boundary.md"]);
167
+ assert.equal(result.keptCount, 1);
168
+ });
169
+
170
+ test("input candidates are not mutated", () => {
171
+ const candidates = [
172
+ cand("observations", "activity/2026-01-01.md", NOW - 40 * MS_PER_DAY),
173
+ cand("observations", "facts/2026-01-01/fact-1.md", NOW - 40 * MS_PER_DAY),
174
+ ];
175
+ const snapshot = structuredClone(candidates);
176
+ plan(candidates);
177
+ assert.deepEqual(candidates, snapshot);
178
+ });
179
+
180
+ // Review round 1: the planner trimmed relPath before validating it, so
181
+ // " activity/x.md" validated as owned and the plan then targeted
182
+ // "activity/x.md" — a path the caller never supplied.
183
+ test("a path with surrounding whitespace is refused, not normalized into a delete", () => {
184
+ const expired = 0;
185
+ for (const relPath of [" activity/day.md", "activity/day.md ", "\tactivity/day.md"]) {
186
+ const plan = planActivityDeletion({
187
+ candidates: [{ scope: "cards", relPath, capturedAtMs: expired }],
188
+ scopes: ["cards"],
189
+ retentionDays: 1,
190
+ nowMs: 10 * 86_400_000,
191
+ });
192
+ assert.deepEqual(plan.deletePaths, [], `${JSON.stringify(relPath)} must not be planned`);
193
+ assert.deepEqual(plan.refused, [relPath], "the exact candidate path is reported as refused");
194
+ }
195
+ });
196
+
197
+ // Review round 1: every comparison against NaN is false, so shouldRetain
198
+ // reported "not retained" and a corrupt timestamp resolved toward deletion.
199
+ test("a non-finite capturedAtMs is refused instead of deleted", () => {
200
+ for (const capturedAtMs of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
201
+ const plan = planActivityDeletion({
202
+ candidates: [{ scope: "cards", relPath: "activity/corrupt.md", capturedAtMs }],
203
+ scopes: ["cards"],
204
+ retentionDays: 1,
205
+ nowMs: 10 * 86_400_000,
206
+ });
207
+ assert.deepEqual(
208
+ plan.deletePaths,
209
+ [],
210
+ `capturedAtMs ${String(capturedAtMs)} must never resolve toward deletion`,
211
+ );
212
+ assert.deepEqual(plan.refused, ["activity/corrupt.md"]);
213
+ assert.equal(plan.keptCount, 0);
214
+ }
215
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Activity retention deletion planner (issue #2053).
3
+ *
4
+ * Pure: decides what a retention pass may delete. Never performs I/O.
5
+ * Safety rule: a path outside the activity-owned roots is refused, never
6
+ * planned for deletion, so unrelated Remnic memories survive.
7
+ */
8
+ import { shouldRetain } from "./privacy.js";
9
+
10
+ export const ACTIVITY_DELETE_SCOPES = [
11
+ "observations",
12
+ "cards",
13
+ "journal",
14
+ "weekly",
15
+ "providerCache",
16
+ ] as const;
17
+ export type ActivityDeleteScope = (typeof ACTIVITY_DELETE_SCOPES)[number];
18
+
19
+ const KNOWN_SCOPES: readonly string[] = ACTIVITY_DELETE_SCOPES;
20
+
21
+ /** Deletion only ever touches these memory-dir-relative roots. */
22
+ const OWNED_ROOTS: readonly string[] = ["activity", "wearables", "meetings"];
23
+
24
+ export interface ActivityDeleteCandidate {
25
+ /** Which activity artifact family this item belongs to. */
26
+ scope: string;
27
+ /** Memory-dir-relative path, POSIX separators. */
28
+ relPath: string;
29
+ capturedAtMs: number;
30
+ }
31
+
32
+ export interface ActivityDeletePlan {
33
+ deletePaths: string[];
34
+ keptCount: number;
35
+ /** Candidates refused because they are not activity-owned paths. */
36
+ refused: string[];
37
+ }
38
+
39
+ function isRefusablePath(relPath: unknown): boolean {
40
+ if (typeof relPath !== "string") return true;
41
+ if (relPath.trim() === "") return true;
42
+ // Validate the EXACT candidate path. Trimming first would let
43
+ // " activity/x.md" validate as owned and then plan a delete for
44
+ // "activity/x.md" — a path the caller never supplied. Surrounding
45
+ // whitespace means this is not a clean owned path: refuse it.
46
+ if (relPath !== relPath.trim()) return true;
47
+ if (relPath.startsWith("/")) return true;
48
+ const segments = relPath.split("/");
49
+ if (segments.includes("..")) return true;
50
+ const [root = "", ...rest] = segments;
51
+ return (
52
+ rest.length === 0 ||
53
+ !OWNED_ROOTS.includes(root) ||
54
+ rest.some((segment) => segment.length === 0)
55
+ );
56
+ }
57
+
58
+ function sortedUnique(paths: Iterable<string>): string[] {
59
+ return [...new Set(paths)].sort();
60
+ }
61
+
62
+ export function planActivityDeletion(input: {
63
+ candidates: readonly ActivityDeleteCandidate[];
64
+ scopes: readonly string[];
65
+ retentionDays: number;
66
+ nowMs: number;
67
+ }): ActivityDeletePlan {
68
+ const { candidates, scopes, retentionDays, nowMs } = input;
69
+
70
+ for (const scope of scopes) {
71
+ if (!KNOWN_SCOPES.includes(scope)) {
72
+ throw new RangeError(`unknown activity delete scope: ${scope}`);
73
+ }
74
+ }
75
+ if (!Number.isInteger(retentionDays) || retentionDays < 0) {
76
+ throw new RangeError("retentionDays must be a non-negative integer");
77
+ }
78
+ if (!Number.isFinite(nowMs)) {
79
+ throw new RangeError("nowMs must be a finite number");
80
+ }
81
+
82
+ const selectedScopes = new Set(scopes);
83
+ const deletePaths: string[] = [];
84
+ const refused: string[] = [];
85
+ let keptCount = 0;
86
+
87
+ for (const candidate of candidates) {
88
+ if (!KNOWN_SCOPES.includes(candidate.scope)) {
89
+ refused.push(String(candidate.relPath));
90
+ continue;
91
+ }
92
+ if (!selectedScopes.has(candidate.scope)) continue;
93
+ if (isRefusablePath(candidate.relPath)) {
94
+ refused.push(String(candidate.relPath));
95
+ continue;
96
+ }
97
+ // A corrupt timestamp must never resolve toward deletion: every
98
+ // comparison against NaN is false, which shouldRetain reads as
99
+ // "not retained". Refuse the candidate instead.
100
+ if (typeof candidate.capturedAtMs !== "number" || !Number.isFinite(candidate.capturedAtMs)) {
101
+ refused.push(candidate.relPath);
102
+ continue;
103
+ }
104
+ if (shouldRetain(candidate.capturedAtMs, nowMs, retentionDays, true)) {
105
+ keptCount += 1;
106
+ continue;
107
+ }
108
+ deletePaths.push(candidate.relPath);
109
+ }
110
+
111
+ return {
112
+ deletePaths: sortedUnique(deletePaths),
113
+ keptCount,
114
+ refused: sortedUnique(refused),
115
+ };
116
+ }
@@ -22,3 +22,4 @@ export * from "./analysis-claim.js";
22
22
  export * from "./analysis-order.js";
23
23
  export * from "./recap-window.js";
24
24
  export * from "./analysis-failure.js";
25
+ export * from "./recap-merge-edits.js";