@remnic/core 9.69.4 → 9.69.5

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 (196) hide show
  1. package/dist/access-admin-ops-surface.js +20 -20
  2. package/dist/access-authorization-probe.js +21 -21
  3. package/dist/access-boundary.js +20 -20
  4. package/dist/access-cli.js +68 -68
  5. package/dist/access-extraction-force-flush.js +20 -20
  6. package/dist/access-http-query.js +22 -22
  7. package/dist/access-http.js +36 -36
  8. package/dist/access-identity-continuity-surface.js +20 -20
  9. package/dist/access-lcm-surface.js +20 -20
  10. package/dist/access-mcp.js +29 -29
  11. package/dist/access-namespace-preflight.js +20 -20
  12. package/dist/access-observe-write-surface.js +20 -20
  13. package/dist/access-operations-batch.js +23 -23
  14. package/dist/access-operations.js +28 -28
  15. package/dist/access-recall-concurrency.js +20 -20
  16. package/dist/access-recall-response.js +20 -20
  17. package/dist/access-recall-surface.js +20 -20
  18. package/dist/access-schema.js +4 -4
  19. package/dist/access-service.js +20 -20
  20. package/dist/access-surface-catalog.js +22 -22
  21. package/dist/briefing.js +8 -8
  22. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  23. package/dist/causal-consolidation.js +9 -9
  24. package/dist/{chunk-N3VPKMU3.js → chunk-3X7Z467X.js} +2 -2
  25. package/dist/{chunk-EB3V7PJC.js → chunk-4OH6FGAO.js} +13 -13
  26. package/dist/{chunk-EA2C5Z7V.js → chunk-5C4LXMSK.js} +26 -26
  27. package/dist/{chunk-YSD4MK5Q.js → chunk-5HP6LVWU.js} +2 -2
  28. package/dist/{chunk-OM4VYQPH.js → chunk-6EXRODDF.js} +2 -2
  29. package/dist/{chunk-2L5FSNPC.js → chunk-6F5WBXE2.js} +4 -4
  30. package/dist/{chunk-DZPX5WSG.js → chunk-6JYTP3JP.js} +3 -3
  31. package/dist/{chunk-RFYKLES2.js → chunk-6KQLDPKA.js} +2 -2
  32. package/dist/{chunk-J2R275FN.js → chunk-7DXW5V2W.js} +2 -2
  33. package/dist/chunk-7O5EW7KX.js +27 -0
  34. package/dist/chunk-7O5EW7KX.js.map +1 -0
  35. package/dist/{chunk-227UFSEQ.js → chunk-7RZJWZ46.js} +5 -5
  36. package/dist/{chunk-WOA7DS7O.js → chunk-7TCISOAJ.js} +210 -47
  37. package/dist/chunk-7TCISOAJ.js.map +1 -0
  38. package/dist/{chunk-3KURGLO7.js → chunk-7VA7PARC.js} +2 -2
  39. package/dist/{chunk-TOUZM2RQ.js → chunk-AJFKQJQK.js} +2 -2
  40. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  41. package/dist/{chunk-WZBDOI2C.js → chunk-C5FRCM4B.js} +2 -2
  42. package/dist/{chunk-PRCJDCDD.js → chunk-CGFDT752.js} +4 -4
  43. package/dist/{chunk-V63BBOFS.js → chunk-DD2BKHSD.js} +2 -2
  44. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  45. package/dist/{chunk-WJTB4SF5.js → chunk-EAG7YMIS.js} +2 -2
  46. package/dist/{chunk-DA2XDNS5.js → chunk-EJF6AV2U.js} +7 -7
  47. package/dist/{chunk-3HEYRDNF.js → chunk-EJJ57PTY.js} +2 -2
  48. package/dist/{chunk-YHSZTMM3.js → chunk-EVLHZ6F3.js} +2 -2
  49. package/dist/{chunk-35J5ABJC.js → chunk-FGV7V2N4.js} +2 -2
  50. package/dist/{chunk-6AYPXHOF.js → chunk-GM35IHF7.js} +2 -2
  51. package/dist/{chunk-MU4FAKGV.js → chunk-HDEQRI2O.js} +2 -2
  52. package/dist/{chunk-PBSZQBFM.js → chunk-IFHT77WK.js} +2 -2
  53. package/dist/{chunk-OZFHE6FY.js → chunk-IHFN3JTR.js} +3 -3
  54. package/dist/{chunk-LXMYIJOO.js → chunk-ISDSX7GW.js} +1 -1
  55. package/dist/{chunk-EZPBQVGZ.js → chunk-JJAOVJ3E.js} +5 -5
  56. package/dist/{chunk-76X3F4WV.js → chunk-LGVIKA7O.js} +6 -6
  57. package/dist/{chunk-7DQQYJKU.js → chunk-MDQXW235.js} +2 -2
  58. package/dist/{chunk-ASDK4GEV.js → chunk-MOUCCUET.js} +2 -2
  59. package/dist/{chunk-DT7LKLET.js → chunk-MVH7YAWY.js} +2 -2
  60. package/dist/{chunk-GH477IGR.js → chunk-NAB4MKTJ.js} +3 -3
  61. package/dist/{chunk-FGORNFTZ.js → chunk-NM5EOKN7.js} +2 -2
  62. package/dist/{chunk-LXBVTRBG.js → chunk-OUAK37ID.js} +2 -2
  63. package/dist/{chunk-HCERYDRI.js → chunk-PGD3DXZP.js} +2 -2
  64. package/dist/{chunk-6CT4HSVJ.js → chunk-PY5E3UOD.js} +2 -2
  65. package/dist/{chunk-HBOQO4GB.js → chunk-QDBG45NK.js} +7 -7
  66. package/dist/{chunk-EJCKJS4X.js → chunk-R3YVNAJB.js} +2 -2
  67. package/dist/{chunk-XVI2TR26.js → chunk-R4FPBSFD.js} +2 -2
  68. package/dist/{chunk-VVE5SLO5.js → chunk-SKVSBTFQ.js} +2 -2
  69. package/dist/{chunk-S7363OIV.js → chunk-SZ3SRRYZ.js} +2 -2
  70. package/dist/{chunk-WRPRUMUI.js → chunk-SZZGVXKC.js} +2 -2
  71. package/dist/{chunk-TFN3PPYO.js → chunk-TGDFKC7X.js} +2 -2
  72. package/dist/{chunk-7SL6EJLT.js → chunk-TP3N5364.js} +2 -2
  73. package/dist/{chunk-IPLYGWQF.js → chunk-VDX2J7OX.js} +6 -6
  74. package/dist/{chunk-VJERIMIX.js → chunk-W6V2XVUO.js} +3 -3
  75. package/dist/{chunk-5FBA6UXN.js → chunk-WTWHORVW.js} +7 -7
  76. package/dist/{chunk-UFY4G6EC.js → chunk-XOGYSWQZ.js} +10 -10
  77. package/dist/{chunk-J6SWAE74.js → chunk-YERBUSU2.js} +2 -2
  78. package/dist/{chunk-D65XGENZ.js → chunk-YPNTBYEV.js} +3 -3
  79. package/dist/{chunk-FNNWK7G7.js → chunk-ZET6A3OF.js} +2 -2
  80. package/dist/cli.js +51 -51
  81. package/dist/coding/export-okf-codegraph.js +8 -8
  82. package/dist/compounding/engine.js +8 -8
  83. package/dist/connectors/codex-materialize-runner.js +8 -8
  84. package/dist/connectors/index.js +8 -8
  85. package/dist/convergence-refresh.js +10 -10
  86. package/dist/conversation-index/backend.js +2 -2
  87. package/dist/corpus-watermark.js +10 -10
  88. package/dist/entity-retrieval.js +8 -8
  89. package/dist/external-wiki-access.js +21 -21
  90. package/dist/extraction.js +8 -8
  91. package/dist/harmonic-retrieval.js +1 -1
  92. package/dist/index.d.ts +77 -1
  93. package/dist/index.js +101 -91
  94. package/dist/lcm/engine.js +2 -2
  95. package/dist/lcm/index.js +5 -5
  96. package/dist/location/index.js +10 -10
  97. package/dist/maintenance/memory-governance.js +8 -8
  98. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
  99. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  100. package/dist/namespaces/migrate.js +9 -9
  101. package/dist/namespaces/search.js +6 -6
  102. package/dist/namespaces/storage.js +8 -8
  103. package/dist/operator-doctor-corpus.js +11 -11
  104. package/dist/operator-toolkit.js +16 -16
  105. package/dist/orchestration/maintenance.js +16 -16
  106. package/dist/orchestrator.js +68 -68
  107. package/dist/recall-navigate-link.d.ts +9 -3
  108. package/dist/recall-navigate-link.js +3 -15
  109. package/dist/recall-navigate-link.js.map +1 -1
  110. package/dist/recall-navigate-traverse.d.ts +21 -0
  111. package/dist/recall-navigate-traverse.js +44 -0
  112. package/dist/recall-navigate-traverse.js.map +1 -0
  113. package/dist/search/factory.js +5 -5
  114. package/dist/search/index.js +9 -9
  115. package/dist/semantic-consolidation.js +9 -9
  116. package/dist/semantic-rule-promotion.js +8 -8
  117. package/dist/semantic-rule-verifier.js +8 -8
  118. package/dist/source-agent-qualifier.js +8 -8
  119. package/dist/standup/build.js +8 -8
  120. package/dist/storage.js +7 -7
  121. package/dist/support-passport/index.js +23 -23
  122. package/dist/transfer/autodetect.js +1 -1
  123. package/dist/transfer/backup.js +2 -2
  124. package/dist/transfer/capsule-export.js +3 -3
  125. package/dist/transfer/capsule-import.js +2 -2
  126. package/dist/transfer/export-okf.js +9 -9
  127. package/dist/verified-recall.js +8 -8
  128. package/dist/who-knows-cli.js +9 -9
  129. package/dist/who-knows.js +8 -8
  130. package/package.json +2 -2
  131. package/src/activity/index.ts +2 -0
  132. package/src/activity/privacy-gate-resolve.test.ts +175 -0
  133. package/src/activity/privacy-gate-resolve.ts +84 -0
  134. package/src/activity/timeline/index.ts +1 -0
  135. package/src/activity/timeline/recap-fallback.test.ts +184 -0
  136. package/src/activity/timeline/recap-fallback.ts +106 -0
  137. package/src/activity/vault-dry-run.test.ts +114 -0
  138. package/src/activity/vault-dry-run.ts +76 -0
  139. package/src/recall-navigate-link.test.ts +28 -6
  140. package/src/recall-navigate-link.ts +16 -2
  141. package/src/recall-navigate-traverse.test.ts +247 -0
  142. package/src/recall-navigate-traverse.ts +73 -0
  143. package/dist/chunk-WOA7DS7O.js.map +0 -1
  144. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  145. /package/dist/{chunk-N3VPKMU3.js.map → chunk-3X7Z467X.js.map} +0 -0
  146. /package/dist/{chunk-EB3V7PJC.js.map → chunk-4OH6FGAO.js.map} +0 -0
  147. /package/dist/{chunk-EA2C5Z7V.js.map → chunk-5C4LXMSK.js.map} +0 -0
  148. /package/dist/{chunk-YSD4MK5Q.js.map → chunk-5HP6LVWU.js.map} +0 -0
  149. /package/dist/{chunk-OM4VYQPH.js.map → chunk-6EXRODDF.js.map} +0 -0
  150. /package/dist/{chunk-2L5FSNPC.js.map → chunk-6F5WBXE2.js.map} +0 -0
  151. /package/dist/{chunk-DZPX5WSG.js.map → chunk-6JYTP3JP.js.map} +0 -0
  152. /package/dist/{chunk-RFYKLES2.js.map → chunk-6KQLDPKA.js.map} +0 -0
  153. /package/dist/{chunk-J2R275FN.js.map → chunk-7DXW5V2W.js.map} +0 -0
  154. /package/dist/{chunk-227UFSEQ.js.map → chunk-7RZJWZ46.js.map} +0 -0
  155. /package/dist/{chunk-3KURGLO7.js.map → chunk-7VA7PARC.js.map} +0 -0
  156. /package/dist/{chunk-TOUZM2RQ.js.map → chunk-AJFKQJQK.js.map} +0 -0
  157. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  158. /package/dist/{chunk-WZBDOI2C.js.map → chunk-C5FRCM4B.js.map} +0 -0
  159. /package/dist/{chunk-PRCJDCDD.js.map → chunk-CGFDT752.js.map} +0 -0
  160. /package/dist/{chunk-V63BBOFS.js.map → chunk-DD2BKHSD.js.map} +0 -0
  161. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  162. /package/dist/{chunk-WJTB4SF5.js.map → chunk-EAG7YMIS.js.map} +0 -0
  163. /package/dist/{chunk-DA2XDNS5.js.map → chunk-EJF6AV2U.js.map} +0 -0
  164. /package/dist/{chunk-3HEYRDNF.js.map → chunk-EJJ57PTY.js.map} +0 -0
  165. /package/dist/{chunk-YHSZTMM3.js.map → chunk-EVLHZ6F3.js.map} +0 -0
  166. /package/dist/{chunk-35J5ABJC.js.map → chunk-FGV7V2N4.js.map} +0 -0
  167. /package/dist/{chunk-6AYPXHOF.js.map → chunk-GM35IHF7.js.map} +0 -0
  168. /package/dist/{chunk-MU4FAKGV.js.map → chunk-HDEQRI2O.js.map} +0 -0
  169. /package/dist/{chunk-PBSZQBFM.js.map → chunk-IFHT77WK.js.map} +0 -0
  170. /package/dist/{chunk-OZFHE6FY.js.map → chunk-IHFN3JTR.js.map} +0 -0
  171. /package/dist/{chunk-LXMYIJOO.js.map → chunk-ISDSX7GW.js.map} +0 -0
  172. /package/dist/{chunk-EZPBQVGZ.js.map → chunk-JJAOVJ3E.js.map} +0 -0
  173. /package/dist/{chunk-76X3F4WV.js.map → chunk-LGVIKA7O.js.map} +0 -0
  174. /package/dist/{chunk-7DQQYJKU.js.map → chunk-MDQXW235.js.map} +0 -0
  175. /package/dist/{chunk-ASDK4GEV.js.map → chunk-MOUCCUET.js.map} +0 -0
  176. /package/dist/{chunk-DT7LKLET.js.map → chunk-MVH7YAWY.js.map} +0 -0
  177. /package/dist/{chunk-GH477IGR.js.map → chunk-NAB4MKTJ.js.map} +0 -0
  178. /package/dist/{chunk-FGORNFTZ.js.map → chunk-NM5EOKN7.js.map} +0 -0
  179. /package/dist/{chunk-LXBVTRBG.js.map → chunk-OUAK37ID.js.map} +0 -0
  180. /package/dist/{chunk-HCERYDRI.js.map → chunk-PGD3DXZP.js.map} +0 -0
  181. /package/dist/{chunk-6CT4HSVJ.js.map → chunk-PY5E3UOD.js.map} +0 -0
  182. /package/dist/{chunk-HBOQO4GB.js.map → chunk-QDBG45NK.js.map} +0 -0
  183. /package/dist/{chunk-EJCKJS4X.js.map → chunk-R3YVNAJB.js.map} +0 -0
  184. /package/dist/{chunk-XVI2TR26.js.map → chunk-R4FPBSFD.js.map} +0 -0
  185. /package/dist/{chunk-VVE5SLO5.js.map → chunk-SKVSBTFQ.js.map} +0 -0
  186. /package/dist/{chunk-S7363OIV.js.map → chunk-SZ3SRRYZ.js.map} +0 -0
  187. /package/dist/{chunk-WRPRUMUI.js.map → chunk-SZZGVXKC.js.map} +0 -0
  188. /package/dist/{chunk-TFN3PPYO.js.map → chunk-TGDFKC7X.js.map} +0 -0
  189. /package/dist/{chunk-7SL6EJLT.js.map → chunk-TP3N5364.js.map} +0 -0
  190. /package/dist/{chunk-IPLYGWQF.js.map → chunk-VDX2J7OX.js.map} +0 -0
  191. /package/dist/{chunk-VJERIMIX.js.map → chunk-W6V2XVUO.js.map} +0 -0
  192. /package/dist/{chunk-5FBA6UXN.js.map → chunk-WTWHORVW.js.map} +0 -0
  193. /package/dist/{chunk-UFY4G6EC.js.map → chunk-XOGYSWQZ.js.map} +0 -0
  194. /package/dist/{chunk-J6SWAE74.js.map → chunk-YERBUSU2.js.map} +0 -0
  195. /package/dist/{chunk-D65XGENZ.js.map → chunk-YPNTBYEV.js.map} +0 -0
  196. /package/dist/{chunk-FNNWK7G7.js.map → chunk-ZET6A3OF.js.map} +0 -0
package/dist/who-knows.js CHANGED
@@ -9,25 +9,25 @@ import {
9
9
  loadWhoKnowsEntities,
10
10
  tokenizeTopic,
11
11
  validateWhoKnowsInput
12
- } from "./chunk-FGORNFTZ.js";
13
- import "./chunk-PBSZQBFM.js";
12
+ } from "./chunk-NM5EOKN7.js";
13
+ import "./chunk-IFHT77WK.js";
14
14
  import "./chunk-UHVXE24Y.js";
15
15
  import "./chunk-4ZWCQJNM.js";
16
16
  import "./chunk-WVGXCBOC.js";
17
- import "./chunk-FNNWK7G7.js";
18
- import "./chunk-EZPBQVGZ.js";
17
+ import "./chunk-ZET6A3OF.js";
18
+ import "./chunk-JJAOVJ3E.js";
19
+ import "./chunk-N37X5RU7.js";
19
20
  import "./chunk-NNBZS3DY.js";
20
21
  import "./chunk-CYEPCZN5.js";
21
22
  import "./chunk-GIC6BASP.js";
22
23
  import "./chunk-YQNNHLCG.js";
23
- import "./chunk-6KTXE6A3.js";
24
- import "./chunk-N37X5RU7.js";
25
24
  import "./chunk-W7IGLX4G.js";
25
+ import "./chunk-6KTXE6A3.js";
26
26
  import "./chunk-GUMZC2BO.js";
27
- import "./chunk-227UFSEQ.js";
27
+ import "./chunk-7RZJWZ46.js";
28
+ import "./chunk-A7Q43EUZ.js";
28
29
  import "./chunk-ET356KSB.js";
29
30
  import "./chunk-72A4L5JJ.js";
30
- import "./chunk-A7Q43EUZ.js";
31
31
  import "./chunk-S2V7RUCL.js";
32
32
  import "./chunk-XXW32OL7.js";
33
33
  import "./chunk-S2HLHAZ4.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.69.4",
3
+ "version": "9.69.5",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3217,7 +3217,7 @@
3217
3217
  "core"
3218
3218
  ],
3219
3219
  "peerDependencies": {
3220
- "@remnic/coding-graph": "^9.69.4"
3220
+ "@remnic/coding-graph": "^9.69.5"
3221
3221
  },
3222
3222
  "peerDependenciesMeta": {
3223
3223
  "@remnic/coding-graph": {
@@ -23,6 +23,7 @@ export * from "./memory-gen.js";
23
23
  export * from "./privacy.js";
24
24
  export * from "./privacy-delete-plan.js";
25
25
  export * from "./privacy-status.js";
26
+ export * from "./privacy-gate-resolve.js";
26
27
  export * from "./privacy-window.js";
27
28
  export * from "./vault-path.js";
28
29
  export * from "./vault-publish.js";
@@ -33,3 +34,4 @@ export * from "./vault-wikilink.js";
33
34
  export * from "./vault-region.js";
34
35
  export * from "./vault-suffix.js";
35
36
  export * from "./vault-join.js";
37
+ export * from "./vault-dry-run.js";
@@ -0,0 +1,175 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ type ActivityFeatureGate,
6
+ ACTIVITY_FEATURE_GATES,
7
+ resolveActivityGates,
8
+ } from "./privacy-gate-resolve.js";
9
+
10
+ function allTrue(): Record<string, unknown> {
11
+ return { analysis: true, journal: true, weekly: true, export: true, memoryCreation: true };
12
+ }
13
+
14
+ function allFalse(gates: Record<string, boolean>): boolean {
15
+ return ACTIVITY_FEATURE_GATES.every((gate) => gates[gate] === false);
16
+ }
17
+
18
+ test("master off forces every explicitly-true gate false", () => {
19
+ const gates = resolveActivityGates({ enabled: false, gates: allTrue() });
20
+ for (const gate of ACTIVITY_FEATURE_GATES) {
21
+ assert.equal(gates[gate], false, `gate "${gate}" must be false`);
22
+ }
23
+ });
24
+
25
+ test("master absent behaves as off even with all gates true", () => {
26
+ const gates = resolveActivityGates({ gates: allTrue() });
27
+ assert.ok(allFalse(gates));
28
+ });
29
+
30
+ test("master on with no gates yields all false", () => {
31
+ const gates = resolveActivityGates({ enabled: true });
32
+ assert.ok(allFalse(gates));
33
+ const empty = resolveActivityGates({ enabled: true, gates: {} });
34
+ assert.ok(allFalse(empty));
35
+ });
36
+
37
+ test("master on with one gate on leaves the other four off", () => {
38
+ for (const gate of ACTIVITY_FEATURE_GATES) {
39
+ const gates = resolveActivityGates({ enabled: true, gates: { [gate]: true } });
40
+ for (const other of ACTIVITY_FEATURE_GATES) {
41
+ assert.equal(gates[other], other === gate, `gate "${gate}" case: "${other}" wrong`);
42
+ }
43
+ }
44
+ });
45
+
46
+ test("master on accepts boolean and string token values", () => {
47
+ const on = resolveActivityGates({
48
+ enabled: true,
49
+ gates: { analysis: "true", journal: "1", weekly: "yes", export: "on" },
50
+ });
51
+ assert.equal(on.analysis, true);
52
+ assert.equal(on.journal, true);
53
+ assert.equal(on.weekly, true);
54
+ assert.equal(on.export, true);
55
+
56
+ const off = resolveActivityGates({
57
+ enabled: "true",
58
+ gates: { analysis: "false", journal: "0", weekly: "no", export: "off" },
59
+ });
60
+ assert.equal(off.analysis, false);
61
+ assert.equal(off.journal, false);
62
+ assert.equal(off.weekly, false);
63
+ assert.equal(off.export, false);
64
+ });
65
+
66
+ test("unrecognized gate values throw TypeError naming the gate", () => {
67
+ for (const value of ["maybe", 2, {}, null]) {
68
+ assert.throws(
69
+ () => resolveActivityGates({ enabled: true, gates: { weekly: value } }),
70
+ (err: unknown) =>
71
+ err instanceof TypeError && String(err).includes('"weekly"'),
72
+ `value ${JSON.stringify(value)} must throw`,
73
+ );
74
+ }
75
+ });
76
+
77
+ test("unrecognized gate values throw even when master is off", () => {
78
+ assert.throws(
79
+ () => resolveActivityGates({ enabled: false, gates: { journal: "maybe" } }),
80
+ TypeError,
81
+ );
82
+ });
83
+
84
+ test("unknown gate key throws and lists the allow-list", () => {
85
+ assert.throws(
86
+ () => resolveActivityGates({ enabled: true, gates: { analytics: true } }),
87
+ (err: unknown) =>
88
+ err instanceof TypeError &&
89
+ /unknown activity gate/.test(String(err)) &&
90
+ ACTIVITY_FEATURE_GATES.every((gate) => String(err).includes(gate)),
91
+ );
92
+ });
93
+
94
+ test("result contains exactly the five gate keys", () => {
95
+ const gates = resolveActivityGates({ enabled: true, gates: allTrue() });
96
+ assert.deepEqual(Object.keys(gates).sort(), [...ACTIVITY_FEATURE_GATES].sort());
97
+ });
98
+
99
+ test("result is frozen and resists mutation", () => {
100
+ const gates = resolveActivityGates({ enabled: true, gates: { analysis: true } });
101
+ assert.equal(Object.isFrozen(gates), true);
102
+ assert.equal(Reflect.set(gates, "analysis", false), false);
103
+ assert.equal(Reflect.set(gates, "journal", true), false);
104
+ assert.equal(gates.analysis, true);
105
+ assert.equal(gates.journal, false);
106
+ });
107
+
108
+ test("input gates object is not mutated", () => {
109
+ const input = { analysis: "yes" };
110
+ resolveActivityGates({ enabled: true, gates: input });
111
+ assert.deepEqual(input, { analysis: "yes" });
112
+ });
113
+
114
+ // Review: Object.keys enumerates own properties only, so the unknown-key
115
+ // check never saw an inherited "analysis"; reading by bracket would have
116
+ // honored it anyway. Own-property reads close that.
117
+ test("an inherited gate property is ignored, not honored", () => {
118
+ const proto = { analysis: true } as Record<string, unknown>;
119
+ const gates: Record<string, unknown> = Object.create(proto);
120
+ gates.journal = true; // own, allowed, and genuinely set by the caller
121
+ const resolved = resolveActivityGates({ enabled: true, gates });
122
+ assert.equal(resolved.analysis, false, "inherited analysis must not enable");
123
+ assert.equal(resolved.journal, true, "an own gate still works");
124
+ });
125
+
126
+ test("the exported gate registry resists runtime mutation", () => {
127
+ const registry = ACTIVITY_FEATURE_GATES as unknown as string[];
128
+ assert.throws(() => registry.push("injected"), /not extensible|frozen|read only/i);
129
+ assert.equal(
130
+ (ACTIVITY_FEATURE_GATES as readonly string[]).includes("injected"),
131
+ false,
132
+ );
133
+ assert.throws(
134
+ () => resolveActivityGates({ enabled: true, gates: { injected: true } }),
135
+ /unknown activity gate/,
136
+ );
137
+ });
138
+
139
+ // Round 2: an explicit `readonly string[]` widened the union to `string`.
140
+ test("the frozen registry keeps its literal type", () => {
141
+ const gates: readonly string[] = ACTIVITY_FEATURE_GATES;
142
+ // Compile-time narrowing is asserted via the resolver's own typing: at
143
+ // runtime, the frozen array rejects mutation and injection.
144
+ assert.throws(() => (gates as string[]).push("bogus"), /not extensible|frozen|read only/i);
145
+ });
146
+
147
+ // Round 2: a present-but-undefined own value is invalid, not absent.
148
+ test("an explicitly undefined gate value throws rather than reading as false", () => {
149
+ assert.throws(
150
+ () => resolveActivityGates({ enabled: true, gates: { analysis: undefined } }),
151
+ /invalid value for activity gate "analysis"/,
152
+ );
153
+ // An absent key is still a legitimate default-false.
154
+ const resolved = resolveActivityGates({ enabled: true, gates: {} });
155
+ assert.equal(resolved.analysis, false);
156
+ });
157
+
158
+ // Round 3: a non-enumerable own key must still hit the unknown-key check.
159
+ test("a non-enumerable unknown gate key is rejected", () => {
160
+ const gates: Record<string, unknown> = {};
161
+ Object.defineProperty(gates, "sneaky", { value: true, enumerable: false });
162
+ assert.throws(
163
+ () => resolveActivityGates({ enabled: true, gates }),
164
+ /unknown activity gate "sneaky"/,
165
+ );
166
+ });
167
+
168
+ // Compile-time: the frozen registry must keep its literal type, so "bogus"
169
+ // is not assignable to ActivityFeatureGate. This assertion fails the build
170
+ // if a future annotation widens the union back to string.
171
+ test("ActivityFeatureGate stays a literal union", () => {
172
+ // @ts-expect-error "bogus" is not an ActivityFeatureGate
173
+ const bogus: ActivityFeatureGate = "bogus";
174
+ assert.equal(bogus, "bogus");
175
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Master-override resolution for activity feature gates (issue #2053).
3
+ *
4
+ * With `activity.enabled` false, every feature gate reads false regardless of
5
+ * its own config value. Gate values accept booleans and the same string tokens
6
+ * as `parsePrivacyEnabled`; anything else throws so a typo can never silently
7
+ * disable (or enable) a gate. Internal helper — wiring it into parseConfig is
8
+ * a later slice.
9
+ */
10
+
11
+ import { parsePrivacyEnabled } from "./privacy-enabled.js";
12
+
13
+ // Frozen at runtime AND still literal-typed: `Object.freeze` preserves the
14
+ // `as const` tuple, so ActivityFeatureGate stays a five-member union for
15
+ // TypeScript consumers while a JavaScript consumer cannot push a key into
16
+ // the array and have resolveActivityGates accept it forever. An explicit
17
+ // `readonly string[]` annotation would widen the union back to `string`.
18
+ export const ACTIVITY_FEATURE_GATES = Object.freeze([
19
+ "analysis",
20
+ "journal",
21
+ "weekly",
22
+ "export",
23
+ "memoryCreation",
24
+ ] as const);
25
+ export type ActivityFeatureGate = (typeof ACTIVITY_FEATURE_GATES)[number];
26
+
27
+ export type ActivityGateSet = Readonly<Record<ActivityFeatureGate, boolean>>;
28
+
29
+
30
+ function parseGateValue(key: string, value: unknown): boolean {
31
+ const parsed = parsePrivacyEnabled(value);
32
+ if (!parsed.ok) {
33
+ throw new TypeError(
34
+ `invalid value for activity gate "${key}": ${JSON.stringify(value)} is not a boolean or known token`,
35
+ );
36
+ }
37
+ return parsed.enabled;
38
+ }
39
+
40
+ export function resolveActivityGates(input: {
41
+ enabled?: unknown;
42
+ gates?: Readonly<Record<string, unknown>>;
43
+ }): ActivityGateSet {
44
+ const rawGates = input.gates;
45
+ if (rawGates !== undefined && (typeof rawGates !== "object" || rawGates === null || Array.isArray(rawGates))) {
46
+ throw new TypeError("activity gates must be an object");
47
+ }
48
+ if (rawGates) {
49
+ // getOwnPropertyNames, not Object.keys: a non-enumerable own property
50
+ // would skip the unknown-key check below while still being readable by
51
+ // the hasOwn path that resolves values.
52
+ for (const key of Object.getOwnPropertyNames(rawGates)) {
53
+ if (!(ACTIVITY_FEATURE_GATES as readonly string[]).includes(key)) {
54
+ throw new TypeError(
55
+ `unknown activity gate "${key}"; allowed gates: ${ACTIVITY_FEATURE_GATES.join(", ")}`,
56
+ );
57
+ }
58
+ }
59
+ }
60
+
61
+ const master =
62
+ input.enabled === undefined ? false : parseGateValue("enabled", input.enabled);
63
+
64
+ const result = {} as Record<ActivityFeatureGate, boolean>;
65
+ for (const gate of ACTIVITY_FEATURE_GATES) {
66
+ // Object.keys above enumerates OWN properties only, so an inherited
67
+ // "analysis" never reaches the unknown-key check. Reading it here anyway
68
+ // would let a gate object with an altered prototype enable a gate the
69
+ // caller never set: read own properties only.
70
+ // A PRESENT own key whose value is undefined is not the same as an
71
+ // absent key: composed config objects often carry explicit undefined,
72
+ // and the API promises to throw for invalid values, so present-but-
73
+ // undefined goes through parseGateValue (which refuses it) rather than
74
+ // silently reading as false.
75
+ const present = rawGates !== undefined && Object.hasOwn(rawGates, gate);
76
+ result[gate] = present ? parseGateValue(gate, (rawGates as Record<string, unknown>)[gate]) : false;
77
+ }
78
+ if (!master) {
79
+ for (const gate of ACTIVITY_FEATURE_GATES) {
80
+ result[gate] = false;
81
+ }
82
+ }
83
+ return Object.freeze(result);
84
+ }
@@ -25,3 +25,4 @@ export * from "./recap-window.js";
25
25
  export * from "./analysis-failure.js";
26
26
  export * from "./recap-merge-edits.js";
27
27
  export * from "./analysis-metadata.js";
28
+ export * from "./recap-fallback.js";
@@ -0,0 +1,184 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { selectRecapForDay } from "./recap-fallback.js";
5
+
6
+ test("ai wins when present, body returned verbatim", () => {
7
+ const result = selectRecapForDay({
8
+ ai: { body: " AI recap ", kind: "ai" },
9
+ previous: { body: "stored journal", kind: "previous" },
10
+ deterministic: { body: "deterministic render", kind: "deterministic" },
11
+ });
12
+ assert.deepEqual(result, { ok: true, body: " AI recap ", kind: "ai" });
13
+ });
14
+
15
+ test("provider failure with a stored previous returns the previous body and carries the failure", () => {
16
+ const result = selectRecapForDay({
17
+ previous: { body: "stored journal", kind: "previous" },
18
+ deterministic: { body: "deterministic render", kind: "deterministic" },
19
+ failure: "provider_unavailable",
20
+ });
21
+ assert.deepEqual(result, {
22
+ ok: true,
23
+ body: "stored journal",
24
+ kind: "previous",
25
+ failure: "provider_unavailable",
26
+ });
27
+ });
28
+
29
+ test("failure with no previous falls to deterministic and still carries the failure", () => {
30
+ const result = selectRecapForDay({
31
+ deterministic: { body: "deterministic render", kind: "deterministic" },
32
+ failure: "timeout",
33
+ });
34
+ assert.deepEqual(result, {
35
+ ok: true,
36
+ body: "deterministic render",
37
+ kind: "deterministic",
38
+ failure: "timeout",
39
+ });
40
+ });
41
+
42
+ test("blank ai body falls through to previous", () => {
43
+ const result = selectRecapForDay({
44
+ ai: { body: " ", kind: "ai" },
45
+ previous: { body: "stored journal", kind: "previous" },
46
+ });
47
+ assert.deepEqual(result, {
48
+ ok: true,
49
+ body: "stored journal",
50
+ kind: "previous",
51
+ });
52
+ });
53
+
54
+ test("blank previous falls through to deterministic", () => {
55
+ const result = selectRecapForDay({
56
+ previous: { body: "", kind: "previous" },
57
+ deterministic: { body: "deterministic render", kind: "deterministic" },
58
+ });
59
+ assert.deepEqual(result, {
60
+ ok: true,
61
+ body: "deterministic render",
62
+ kind: "deterministic",
63
+ });
64
+ });
65
+
66
+ test("all candidates absent gives no_recap_available without failure key", () => {
67
+ const result = selectRecapForDay({});
68
+ assert.deepEqual(result, { ok: false, error: "no_recap_available" });
69
+ assert.ok(!("failure" in result));
70
+ });
71
+
72
+ test("all candidates blank with a failure reports the failure", () => {
73
+ const result = selectRecapForDay({
74
+ ai: { body: " ", kind: "ai" },
75
+ deterministic: { body: "", kind: "deterministic" },
76
+ failure: "malformed_json",
77
+ });
78
+ assert.deepEqual(result, {
79
+ ok: false,
80
+ error: "no_recap_available",
81
+ failure: "malformed_json",
82
+ });
83
+ });
84
+
85
+ test("a mislabelled candidate throws", () => {
86
+ assert.throws(
87
+ () =>
88
+ selectRecapForDay({
89
+ previous: { body: "stored journal", kind: "ai" },
90
+ }),
91
+ /kind/,
92
+ );
93
+ });
94
+
95
+ test("an unknown kind throws and lists the allow-list", () => {
96
+ assert.throws(
97
+ () =>
98
+ selectRecapForDay({
99
+ // @ts-expect-error deliberately invalid kind
100
+ ai: { body: "AI recap", kind: "carrier-pigeon" },
101
+ }),
102
+ (error: unknown) => {
103
+ assert.ok(error instanceof TypeError);
104
+ assert.match(String(error), /kind/);
105
+ assert.match(String(error), /ai, deterministic, previous/);
106
+ return true;
107
+ },
108
+ );
109
+ });
110
+
111
+ test("an absent failure omits the key; whitespace is refused, not cleaned", () => {
112
+ const result = selectRecapForDay({
113
+ deterministic: { body: "deterministic render", kind: "deterministic" },
114
+ });
115
+ assert.deepEqual(result, {
116
+ ok: true,
117
+ body: "deterministic render",
118
+ kind: "deterministic",
119
+ });
120
+ // With the typed contract, whitespace is an unknown kind, not a blank to
121
+ // silently drop: a caller must omit the field, not send a placeholder.
122
+ assert.throws(
123
+ () =>
124
+ selectRecapForDay({
125
+ deterministic: { body: "deterministic render", kind: "deterministic" },
126
+ failure: " ",
127
+ }),
128
+ /unknown recap failure kind/,
129
+ );
130
+ });
131
+
132
+ test("input is not mutated", () => {
133
+ const input = {
134
+ ai: { body: " AI recap ", kind: "ai" },
135
+ previous: { body: "stored journal", kind: "previous" },
136
+ deterministic: { body: "deterministic render", kind: "deterministic" },
137
+ failure: "rate_limited",
138
+ } as const;
139
+ const snapshot = structuredClone(input);
140
+ selectRecapForDay(input);
141
+ assert.deepEqual(input, snapshot);
142
+ });
143
+
144
+ // Review: an unknown failure kind must be refused, not echoed into telemetry
145
+ // as though it were typed.
146
+ test("an unknown failure kind throws instead of being echoed", () => {
147
+ assert.throws(
148
+ () =>
149
+ selectRecapForDay({
150
+ deterministic: { body: "cards", kind: "deterministic" },
151
+ failure: "timeot",
152
+ }),
153
+ /unknown recap failure kind/,
154
+ );
155
+ for (const kind of ["timeout", "aborted", "provider_unavailable"]) {
156
+ const result = selectRecapForDay({
157
+ deterministic: { body: "cards", kind: "deterministic" },
158
+ failure: kind,
159
+ });
160
+ assert.ok(result.ok);
161
+ assert.equal(result.failure, kind);
162
+ }
163
+ });
164
+
165
+ // Review: a malformed lower-priority candidate must not hide behind a valid
166
+ // higher-priority one. The contract throws on a bad kind whatever wins.
167
+ test("a valid ai candidate does not hide a malformed previous candidate", () => {
168
+ assert.throws(
169
+ () =>
170
+ selectRecapForDay({
171
+ ai: { body: "fresh", kind: "ai" },
172
+ previous: { body: "old", kind: "ai" } as never,
173
+ }),
174
+ /kind must match its slot/,
175
+ );
176
+ assert.throws(
177
+ () =>
178
+ selectRecapForDay({
179
+ ai: { body: "fresh", kind: "ai" },
180
+ deterministic: { body: "x", kind: "weird" } as never,
181
+ }),
182
+ /unknown recap source kind/,
183
+ );
184
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Fallback selection for the daily journal recap (issue #2051).
3
+ *
4
+ * Picks the journal body in precedence order: AI render, then the last stored
5
+ * journal, then the deterministic render. A blank body at any level falls
6
+ * through to the next one, so a provider failure never replaces a stored good
7
+ * journal with a thinner deterministic render. This function takes no card
8
+ * list and performs no writes, so discarding deterministic source cards is not
9
+ * something it can do; storage callers own that decision.
10
+ *
11
+ * Pure: no I/O, no clock. Does not mutate its input. Internal helper; wiring
12
+ * into the recap build path is a later slice.
13
+ */
14
+
15
+ import { isAnalysisFailureKind } from "./analysis-failure.js";
16
+
17
+ export const RECAP_SOURCE_KINDS = ["ai", "deterministic", "previous"] as const;
18
+ export type RecapSourceKind = (typeof RECAP_SOURCE_KINDS)[number];
19
+
20
+ export interface RecapCandidate {
21
+ /** Rendered journal body. */
22
+ body: string;
23
+ kind: RecapSourceKind;
24
+ }
25
+
26
+ export type RecapSelection =
27
+ | { ok: true; body: string; kind: RecapSourceKind; failure?: string }
28
+ | { ok: false; error: "no_recap_available"; failure?: string };
29
+
30
+ const SLOT_ORDER: readonly RecapSourceKind[] = [
31
+ "ai",
32
+ "previous",
33
+ "deterministic",
34
+ ];
35
+
36
+ function isRecapSourceKind(value: unknown): value is RecapSourceKind {
37
+ return (
38
+ typeof value === "string" &&
39
+ (RECAP_SOURCE_KINDS as readonly string[]).includes(value)
40
+ );
41
+ }
42
+
43
+ /** Select the journal body for one day. Throws TypeError on a bad kind. */
44
+ export function selectRecapForDay(input: {
45
+ /** Present only when the AI run succeeded and validated. */
46
+ ai?: RecapCandidate | null;
47
+ /** Always available when cards exist: the deterministic render. */
48
+ deterministic?: RecapCandidate | null;
49
+ /** The last valid stored journal for this day, if any. */
50
+ previous?: RecapCandidate | null;
51
+ /**
52
+ * Typed failure kind from a provider error, when one occurred. Validated
53
+ * against the analysis-failure allow-list: an arbitrary string here would
54
+ * flow into telemetry and downstream exhaustive handling as though it were
55
+ * a known kind, so a misspelled "timeot" is refused instead of echoed.
56
+ */
57
+ failure?: string | null;
58
+ }): RecapSelection {
59
+ let failure: string | undefined;
60
+ if (input.failure !== undefined && input.failure !== null) {
61
+ if (!isAnalysisFailureKind(input.failure)) {
62
+ throw new TypeError(
63
+ `unknown recap failure kind; expected one of the analysis failure kinds, got a non-allow-listed value`,
64
+ );
65
+ }
66
+ failure = input.failure;
67
+ }
68
+
69
+ // Validate every supplied candidate FIRST. Returning on the first valid
70
+ // body would let a malformed lower-priority candidate hide behind a valid
71
+ // higher-priority one, so the kind contract would hold only sometimes.
72
+ for (const slot of SLOT_ORDER) {
73
+ const candidate = input[slot];
74
+ if (candidate == null) continue;
75
+
76
+ const kind = candidate.kind;
77
+ if (!isRecapSourceKind(kind)) {
78
+ throw new TypeError(
79
+ `unknown recap source kind: ${String(kind)}; allowed kinds: ${RECAP_SOURCE_KINDS.join(", ")}`,
80
+ );
81
+ }
82
+ if (kind !== slot) {
83
+ throw new TypeError(
84
+ `recap candidate passed as "${slot}" has kind "${kind}"; kind must match its slot`,
85
+ );
86
+ }
87
+ if (typeof candidate.body !== "string") {
88
+ throw new TypeError(
89
+ `recap candidate body for slot "${slot}" must be a string`,
90
+ );
91
+ }
92
+ }
93
+
94
+ for (const slot of SLOT_ORDER) {
95
+ const candidate = input[slot];
96
+ if (candidate == null) continue;
97
+ if (candidate.body.trim() === "") continue;
98
+ return failure === undefined
99
+ ? { ok: true, body: candidate.body, kind: candidate.kind }
100
+ : { ok: true, body: candidate.body, kind: candidate.kind, failure };
101
+ }
102
+
103
+ return failure === undefined
104
+ ? { ok: false, error: "no_recap_available" }
105
+ : { ok: false, error: "no_recap_available", failure };
106
+ }